################## From simple-html ################## .. package-meta:: simple-html keithasaurus/simple_html `simple-html `_ is a pure-Python library for *generating* HTML from the inside out: each element is a tag object you call with a leading attribute mapping and then its children (``div({"class": "card"}, h1("Title"))``), and ``render`` walks the resulting tuples into a string. It carries no dependencies, is fully type-checked, and is scoped deliberately narrow -- it builds and escapes markup and nothing else, which makes it a common choice for server-rendered views and email templates where a full DOM would be overkill. turbohtml covers the same construction ground with the terse :data:`turbohtml.build.E` builder, which keeps simple-html's call shape (attributes first, children after) but hands back a real :class:`~turbohtml.Element` tree instead of a string. That one difference in result type is the whole reason to move: the same call that builds your markup also leaves something you can query, edit, and re-serialize by exactly the rules that parse it back. ************************** turbohtml vs simple-html ************************** .. list-table:: :header-rows: 1 :widths: 22 39 39 - - Dimension - turbohtml - simple-html - - Scope - Full WHATWG parse, query, edit, and serialize; ``E`` is one builder over that tree - HTML generation and escaping only; no parser, no queryable tree - - Feature breadth - Build, ``find``/``select``, mutate, ``to_markdown``, re-parse - Build and ``render`` a string; the result is inert once produced - - Performance - Assembles in a C arena, serializes in C; ~3x slower than simple-html on raw string generation - Concatenates tuples straight into a string, the faster path for one-shot rendering - - Typing - ``E.`` resolves dynamically via ``__getattr__``; the tree API is fully typed - Every tag is an explicitly typed symbol, so a mistyped tag name fails at type-check time - - Dependencies - Requires the compiled C extension - Zero dependencies, pure Python - - Maintenance - Active; ``E`` shares the parser's escape and serialize rules - Small, stable, single-purpose Feature overlap =============== The construction surface ports 1:1 -- both put attributes first and children after: - ``div({"class": "card"}, h1("Title"))`` -> :data:`E.div({"class": "card"}, E.h1("Title")) `. - A string child becomes escaped text in both. - A leading mapping sets attributes; everything after is a child in order. - Nested calls compose the same way, so an existing simple-html view tree maps node for node onto ``E`` calls. What turbohtml adds =================== ``E`` returns a real :class:`~turbohtml.Element`, so the entire tree surface is available on what you just built: - Query the fragment you constructed with :meth:`~turbohtml.Node.find` and :meth:`~turbohtml.Node.select`. - Mutate it with :meth:`~turbohtml.Element.append`, :meth:`~turbohtml.Element.extend`, and the rest of the edit API. - Convert it with :meth:`~turbohtml.Node.to_markdown`. - Serialize by the same C rules that parse HTML back, so round-tripping generated markup is exact. - A list-valued attribute joins on a space, so ``{"class": ["card", "lg"]}`` reads as a token list naturally. What simple-html has that turbohtml does not ============================================ - **Per-tag static typing.** simple-html exposes each tag as its own typed symbol, so a typo like ``dvi(...)`` is a type-check error. ``E.dvi(...)`` resolves through ``__getattr__`` and would build a literal ```` element; the builder cannot catch an unknown tag name statically. No equivalent -- rely on runtime output or tests. - **A pre-escaped-markup escape hatch.** simple-html's ``SafeString("hi")`` injects trusted raw markup. ``E`` has no ``SafeString``; express the markup as child elements (:data:`E.b("hi") `) or parse the string into nodes first. - **Zero dependencies / pure Python.** simple-html installs with no build step. turbohtml requires its compiled C extension, which matters on locked-down or exotic targets. Performance =========== ``E`` assembles the fragment in turbohtml's arena and serializes it in C; simple-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/simple-html.json simple-html renders about three times faster than ``E`` on this microbenchmark -- it concatenates tuples straight into a string -- but 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 import and the render step. simple-html and turbohtml share the call shape, so most calls port unchanged: .. code-block:: python # before from simple_html import div, h1, render # after from turbohtml.build import E The API mapping: .. list-table:: :header-rows: 1 :widths: 50 50 - - `simple-html `__ - turbohtml - - ``render(div({"class": "card"}, h1("Title")))`` - :data:`E.div({"class": "card"}, E.h1("Title")) ` ``.serialize()``; same shape, :meth:`~turbohtml.Node.serialize` replaces ``render`` - - ``li({"class": "item", "data-i": "1"}, "text")`` - :data:`E.li({"class": "item", "data-i": "1"}, "text") `, unchanged - - ``SafeString("hi")`` for pre-escaped markup - build the markup as child elements instead: :data:`E.b("hi") ` - - ``del_({}, "removed")`` (keyword-shadowing tag) - :data:`E("del", {}, "removed") ` via the call form - - ``DOCTYPE_HTML5 + render(...)`` for a full page - :func:`turbohtml.build.document(body=[...]) `, which emits the doctype and shell Before and after: .. testcode:: from turbohtml.build import E card = E.div({"class": "card"}, E.h1("Title"), E.p("body")) print(card.serialize()) .. testoutput::

    Title

    body

    ``E("tag", ...)`` is the call form for a tag that is not a Python identifier (a custom element, say), 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 Where simple-html prepends ``DOCTYPE_HTML5`` to a rendered page, :func:`turbohtml.build.document` emits the doctype and the ````/````/```` shell around the content you pass, returning a :class:`~turbohtml.Document`: .. testcode:: from turbohtml.build import E, document page = document(title="Report", lang="en", body=[E.h1("Sales"), E.p("Up 4%")]) print(page.serialize()) .. testoutput:: Report

    Sales

    Up 4%

    ********************** Gotchas and pitfalls ********************** - ``E`` builds a fragment, not a document: there is no implicit ````/````/```` wrapper and no doctype, where simple-html ships a ``DOCTYPE_HTML5`` constant to prepend. Serialize the element you built, or reach for :func:`turbohtml.build.document` when you need the full page shell. - Both escape string children by default; simple-html's ``SafeString`` escape hatch has no equivalent, so express raw markup as child elements (or parse it into nodes first). - simple-html suffixes tags that shadow keywords (``del_``); ``E.del_`` would build a literal ````, so use the call form ``E("del", ...)`` for those tags. - A mistyped tag on ``E`` silently builds that element rather than erroring, since ``E.`` resolves dynamically; simple-html would flag the same typo at type-check time. Cover generated markup with a test. - A mapping argument is always read as attributes and must come first; passing one after a child raises ``TypeError``. - The result is an ordinary :class:`~turbohtml.Element`, so the whole edit and query surface (``append``, ``find``, ``select``, ``serialize``, ``to_markdown``) is available -- the builder only saves the construction boilerplate.