################
From fast-html
################
.. package-meta:: fast-html pcarbonn/fast_html
`fast-html `_ is a small pure-Python library that assembles HTML5 from the inside
out. Each tag is a function that takes its children first (a single node or a list) and its attributes as trailing
keyword arguments, with the usual underscore mangling (``class_`` to ``class``, ``data_i`` to ``data-i``). A tag yields
its markup lazily as a generator of string fragments, and ``render`` joins those fragments into the final string. It
escapes nothing and produces a string, not a tree, so it is a generation-only tool: composition happens in Python and
the result is markup you hand off to a response or a file.
turbohtml covers that ground with :data:`turbohtml.build.E`, a terse builder over :class:`~turbohtml.Element`:
``E.(attrs, *children)``, where a leading mapping is the attributes and each child is a node or a string that
becomes text. The difference is the result type. ``E`` hands back a real turbohtml tree, so the same call that builds
the markup also leaves an element you can query, edit, and re-serialize, and the markup it produces serializes by
exactly the rules that parse it back.
************************
turbohtml vs fast-html
************************
.. list-table::
:header-rows: 1
:widths: 20 40 40
- - Dimension
- turbohtml
- fast-html
- - Scope
- Full HTML parse, query, edit, and serialize engine, with the ``E`` builder as one front end
- HTML generation only: tag functions that render to a string
- - Feature breadth
- Build, parse, CSS ``select``, ``find``, mutate, ``serialize``, streaming ``serialize_iter``, ``to_markdown``,
minify
- Tag functions, keyword-mangled attributes, lazy fragment rendering
- - Performance
- Builds in turbohtml's arena and serializes in C, about twice as fast on the corpus below
- Pure-Python generator that yields and joins string fragments
- - Typing
- Typed public API and shipped stubs (:data:`turbohtml.build.E`, :class:`~turbohtml.Element`)
- Untyped
- - Dependencies
- Ships the C extension, no Python dependencies
- Pure Python, zero dependencies
- - Maintenance
- Actively developed alongside the turbohtml serializer
- Small, stable, single-maintainer
Feature overlap
===============
The construction path ports 1:1:
- Build an element with attributes and children: ``render(div([h1("Title"), p("body")], class_="card"))`` maps to
:data:`E.div({"class": "card"}, E.h1("Title"), E.p("body")) ` ``.serialize()``. Children are plain
arguments, with no list wrapper.
- Nest elements by passing built nodes as children, to any depth.
- Turn a string argument into a text child: ``li("text", ...)`` maps to ``E.li({...}, "text")``.
- Set a valueless boolean attribute (``disabled``) by mapping its name to ``None``.
What turbohtml adds
===================
- A real tree, not a string. The result is an ordinary :class:`~turbohtml.Element`, so the whole edit and query surface
(``append``, ``extend``, ``find``, ``select``, ``serialize``, ``to_markdown``) stays available; the builder only saves
the construction boilerplate.
- Escaping. ``E`` builds text nodes, so ``E.div("")`` serializes as ``<b>`` instead of emitting the markup
verbatim.
- Round-tripping. The markup ``E`` generates serializes by the same rules that parse it back, so a built fragment and a
parsed one behave identically.
- Splicing into parsed documents. Because a built node is a real tree, you can ``append`` it under a document you parsed
from existing HTML, not just render it in isolation.
- A native-C serialize path, about twice as fast as fast-html on the corpus below.
- Lazy, streaming output. :meth:`~turbohtml.Node.serialize_iter` yields the markup in bounded ``str`` chunks, so a very
large page streams to a socket or file without ever materializing the whole string -- the same shape as a fast-html
tag's fragment generator. ``''.join(node.serialize_iter())`` equals ``node.serialize()``.
- A typed surface with shipped stubs.
What fast-html has that turbohtml does not
==========================================
- A dependency-free pure-Python install. fast-html runs anywhere CPython does with no compiled extension; turbohtml
ships a C extension, so it needs a wheel for the platform or a build toolchain. No equivalent if a pure-Python install
is a hard requirement.
- Keyword-argument attribute syntax (``class_="card"``). turbohtml takes a leading mapping instead. Minor: pass a dict,
writing the real attribute name as the key.
Performance
===========
``E`` assembles the fragment in turbohtml's arena and serializes it in C; fast-html stays in Python. The same ````
of rows -- a class, a ``data`` attribute, and a text child apiece -- built both ways:
.. bench-table::
:file: bench/fast-html.json
``E`` is about twice as fast as fast-html, and the decisive difference is the result type: ``E`` hands back a real
:class:`~turbohtml.Element`, not a string, so the call that builds the markup also leaves a tree you can query, edit,
and re-:meth:`~turbohtml.Node.serialize`.
****************
How to migrate
****************
Swap the tag imports for the single builder. fast-html takes children first and attributes last; turbohtml leads with
the attributes:
.. list-table::
:header-rows: 1
:widths: 50 50
- - fast-html
- turbohtml
- - ``from fast_html import div, h1, p, li, render``
- ``from turbohtml.build import E``
- - ``render(div([h1("Title"), p("body")], class_="card"))``
- :data:`E.div({"class": "card"}, E.h1("Title"), E.p("body")) ` ``.serialize()``
- - ``li("text", class_="item", data_i="1")``
- :data:`E.li({"class": "item", "data-i": "1"}, "text") `
- - a non-identifier tag (a custom element)
- ``E("my-card", ...)``, the call form for any tag name
- - a class list
- a list value that joins on a space: ``{"class": ["card", "lg"]}``
``E("tag", ...)`` is the call form for a tag that is not a Python identifier, and a list-valued attribute joins on a
space so a class list reads naturally:
.. testcode::
from turbohtml.build import E
print(E("my-card", {"class": ["card", "lg"]}, "hi").serialize())
.. testoutput::
hi
Before and after, reusing the same card:
.. code-block:: python
# fast-html
from fast_html import div, h1, p, render
render(div([h1("Title"), p("body")], class_="card")) # a string, in Python
# turbohtml
from turbohtml.build import E
E.div({"class": "card"}, E.h1("Title"), E.p("body")).serialize() # a tree, serialized in C
**********************
Gotchas and pitfalls
**********************
- Fragment, not document. ``E`` builds a fragment: there is no implicit ````/````/```` wrapper and no
doctype. Serialize the element you built, or append it under a parsed document when you need the full page shell.
- Attribute names. fast-html mangles keyword arguments (``class_`` to ``class``, ``data_i`` to ``data-i``); ``E`` takes
a plain mapping, so write the real attribute name as the dict key, with no underscore convention to remember.
- Escaping changes the output. fast-html never escapes: ``render(div(""))`` emits the ```` markup verbatim. ``E``
builds text nodes, so the same call serializes as ``<b>``. Markup that used to pass through as a string must
become child elements.
- Consumption. A fast-html tag is a generator, consumed once by the ``render`` that joins it; an
:class:`~turbohtml.Element` is a tree you can serialize as many times as you like.
- Argument order. A mapping argument to ``E`` sets attributes and must come first; a mapping passed as a later child
raises :class:`TypeError` rather than being rendered.