Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - arrowtail-precision/html_generators: Functional, streaming HTML generation · GitHub
Skip to content

Repository files navigation

html_generators

For anyone who wants to generate html with python.

Inspired by "hyperscript" libraries from the javascript world.

Developed with Django in mind, but designed to work any where.

Installation/Quick Start

pip install html_generators

importhtml_generatorsash# A "page template"defstandard_page(title, *content, user=None, page_title=None):
# h.Document just adds the DOCTYPE linereturnh.Document(
# h.Title is just an HTML element# We have factories defined for all standard HTML elementsh.Title(title),
# Keyword arguments become element attributesh.Meta(charset='utf-8'),
h.Link(href=my_static_file('site_styles.css')),
# Setting an attribute to True will render it with no valueh.Script(defer=True, src=my_static_file('site_script.js')),
h.Nav(
# Elements can have both content (positional args)# and attributes (keyword args)# At first, it looks odd that an element's attributes are listed # after its content, but you get used to it quickly.# With decent syntax highlighting, the attributes stand out nicelyh.A('Foo', href='/foo/'),
h.A('Bar', href='/bar/'),
# Any argument that is False or is None will be skipped.# This makes "conditional children" easyuserand (
h.A('My Profile', href='/profile/'),
h.A('Log Out', href='/logout/'),
)
),
h.Main(
# If the user doesn't provide a custom page title,# generate one matching document titlepage_titleorh.H1(title),
content,
),
)
# A "chunk template"defbook_section(book):
returnh.Section(
h.H2(book.title),
h.P(book.summary),
# "class" is a reserved word in python, so add a trailing underscore# Trailing underscores are trimmed when converting to attribute namesclass_='book',
# This will render a 'data-id' attribute# All underscores (other than trailing) will be converted to hyphensdata_id=book.id,
)
# A particular pagedefmy_books_page(user):
returnstandard_page(
'My Books',
h.P(h.A('Create new book', href='/books/add/')),
# Join is not an element - it works like str.join# Here we're printing an <hr> between each book sectionh.Join(
h.Hr(), # This is a generator expression# We could also pass a list, but this reduces memory usage
(
book_section(book) forbookinget_user_books(user)
ifbook.published
)
),
)

Background/Philosophy

Incremental Adoption

You easily can use html_generators to generate all of your site's HTML, or mix it with your existing framework's template system.

HTMLGenerators are completely inter-operable with Django's template system and HTML utility functions, as well as the python "markupsafe" library.

You can pass an HTMLGenerator instance to any of the following, and it will not be escaped:

  • django.template.utils.html.format_html
  • django.template.utils.html.conditional_escape
  • a django template
  • markupsafe.escape
  • markupsafe.Markup.format()

If you "pre-render" an HTMLGenerator instance (by calling str() on it), you'll get a "safe string", which can also be passed to any of the above and won't be escaped.

The converse is also true - you can pass any of the following to an HTMLGenerator, and we'll know not to escape it:

  • the result of django's format_html, mark_safe, escape, or conditional_escape
  • a markupsafe.Markup instance

All of this interoperability is achieved by a fairly straightforward __html__() protocol, which is likely also adopted by other python templating/html generation frameworks.

Lazy/Streaming

We don't do incremental string concatenation. We produce a single stream of strings, which are only ''.join()ed by the outermost element.

This should help with performance and memory usage. Additionally, it means you can actually produce "infinite" HTMLGenerators and pass them to django's StreamingHttpResponse:

importhtml_generatorsashfromitertoolsimportcountfromdjango.httpimportStreamingHttpResponsedefmake_infinite_response():
returnStreamingHttpResponse(h.Document(
h.Title('Stupid infinite page demo'),
(
h.Div(x)
forxincount(),
),
))

Performance

We haven't yet written any performance benchmarks to compare to Django's template system. In our use, it has been fast enough that testing hasn't been warranted (we generate the entirety of each page from scratch on every request - but not yet on any high-traffic sites).

That said, if anyone finds performance to be an issue, or wants to write some benchmarks, we'd be glad to share them here. We have an idea for an optional "pre-compile" step, but don't want to add that complexity to the project if no one needs it.

Tips/Warnings

Don't List - Generate!

Consider these two functions:

defbooks_list(user):
returnh.Div([book_section(book) forbookinuser.books])
defbooks_generator(user):
returnh.Div(book_section(book) forbookinuser.books)

The first version creates a (rather useless) list of book sections. The second version is (theoretically) more efficient. It just iterates over the books, and the entire list of sections is never stored in memory. This is the approach we endorse.

Don't Reuse Instances

HTMLGenerator instances are only intended to be rendered or iterated once. Given the above example, if you did:

books=books_generator(user)
print(books)
print(books)

The first print statement would do what you expect, but the second would print an empty div. The generator expression passed to h.Div in the books_generator function gets "exhausted" the first time you render the result. The second time, there are no more items to generate.

Altering Elements

Sometimes you need to tweak the output of one your "reusable-component-functions". Element provides 3 methods to help with this, which are best demonstrated by example:

# A super simple reusable componentdeffancy_button(*content, large=False):
returnh.Button(content, class_="Fancy-button", style=largeand'font-size: 2em;')
# Now imagine we need a fancy button, with some tweaksprint(
fancy_button('Print this page', large=True)
# with_attrs lets you add/alter any attributes
.with_attrs(onclick='window.print()')
# with_classes will merge the given classes with any already present
.with_classes('no-print')
# with_styles will merge the given styles with any already present
.with_styles('float: right')
)

RawTextElement

html_generators.Script and html_generators.Style create RawTextElement instances. These elements do not escape their contents when rendered. These elements are actually defined separately in the HTML spec, and browsers don't parse HTML entities inside them, so there's really nothing we can do.

You need to make sure the content you pass to them doesn't contain text that would be interpreted as an end tag.

You Can't Do This With Templates!

Wrapper Components

Consider this example:

defaccordion(sections):
returnh.Div(
(
h.Div(
h.H2(heading, class_='accordion-heading'),
h.Div(content, class_='accordion-content'),
class_='accordion-section',
)
forheading, contentinsections
),
class_='accordion',
)
print(accordion(
('Section 1', 'Section 1 content...'),
('Section 2', 'Section 2 content...'),
))

There's really no clean way to do this with django templates.

HTML in Element Attributes

Sometimes it's convenient to put a chunk of HTML inside an element attribute, for consumption by javascript. That HTML needs to be escaped. In a template, you can't just write that HTML (unescaped) in the attribute. With html_generators, you can. HTMLGenerators don't escape other HTMLGenerators that are passed as children, but they escape everything that is passed as an attribute.

h.Button(
'Click here for details!',
data_modal=h.Fragment(
h.H1('Here are the details!', class_='modal-heading'),
h.P("We'll tell you everything you need to know"),
...
),
)

In the above example, the Fragment will be converted to a str, and then escaped.

API Reference

We haven't yet written/generated a complete API reference, but the code is well documented and fairly straight-forward. We recommend reading the source directly. All "private" interfaces are properly identified with leading underscores - everything else is public and should remain stable between releases.

About

Functional, streaming HTML generation

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages