Changelog¶
vUnreleased (2026-07-09)¶
No significant changes.
v1.2.0 (2026-07-09)¶
Backward incompatible changes - 1.2.0¶
turbohtml.detect.detect()reportswindows-1252, notascii, for pure-ASCII input;EncodingMatchgained a trailingcodecfield; andIncrementalParsertakes WHATWG labels, solatin-1raises whereiso-8859-1works. (#622)
Features - 1.2.0¶
Add
EncodingMatch.codec;data.decode(match.codec)reproduces whatturbohtml.parse()saw, wherematch.encodingcorrupted or raised. (#622)
Bug fixes - 1.2.0¶
Lock free-threaded DOM tree reads and attribute views; concurrent mutation no longer crashes those readers. (#617)
Resolve local
file://stylesheet URLs forxsl:import;Path.as_uri()bases now load sibling imports. (#618)Normalize configured
Linkify.skip_tagsnames before matching parsed HTML tags;CODEnow skips<code>text. (#619)Keep controls inside the first
<legend>child of a disabled<fieldset>inform_data()output. (#621)Decode legacy bytes with the WHATWG decoders rather than CPython’s same-named codecs, whose tables and error handling both differ:
koi8-uis KOI8-RU, and GBK0x80is the euro sign. The<meta>prescan, the content detector, andIncrementalParsernow follow the spec too. (#622)
v1.1.1 (2026-07-08)¶
No significant changes.
v1.1.0 (2026-07-08)¶
Features - 1.1.0¶
Policygainedstrip_template_markers: with it on, sanitizing collapses template-engine expressions ({{ }},${ },<% %>) in kept text and attribute values to a single space, so the output cannot re-inject when a template engine renders it. This matches DOMPurify’sSAFE_FOR_TEMPLATES. (#527)turbohtml.clean.sanitize_report()(andturbohtml.clean.Sanitizer.sanitize_report()) sanitize a fragment and return what the policy dropped alongside the cleaned HTML: oneturbohtml.clean.Removedrecord per removed element or stripped attribute, in walk order. This matches DOMPurify’sDOMPurify.removed. (#528)turbohtml.convert.css_specificity()returns the(a, b, c)specificity of each selector in a comma-separated list, per CSS Selectors Level 4 §17, the valuecssselectexposes asSelector.specificity(). It weighs the parsed selector in one C pass, with:is()/:not()/:has()taking their most specific argument and:where()contributing zero. (#529)turbohtml.extract.feed()normalizes an RSS 2.0, Atom 1.0, or RDF/RSS-1.0 document into one frozen, typedFeedofEntryrecords, thefeedparser.parseentry point overturbohtml.Document.feed(). It detects the format from the root element and maps each dialect’s spelling of a field – the entrytitle,link,id,updated/published,summary/content, andauthor– onto one shape in a single C walk of the parsed tree, over 12x faster than feedparser on a 30-item feed. (#530)turbohtml.clean.Policygainstransform_tags: a map that renames elements while sanitizing, sanitize-html’stransformTags. Map a source tag to a string to rename it, or to aturbohtml.clean.Transformto rename it and add attributes (sanitize-html’ssimpleTransform). The rename runs before the allowlist in the same C walk, so the renamed element is re-checked from scratch – a transform decides an element’s name but never its safety: mapping a tag toscriptstill drops it, and an added attribute is scrubbed like the element’s own. (#531)A new
turbohtml.saxparsemodule adds a DOM-less, event-driven parse.turbohtml.saxparse.sax_parse()drives a document through the WHATWG tree builder and fires a callback on aturbohtml.saxparse.SaxHandlersubclass for each construct it builds – a start tag, an end tag, a run of text, a comment, the doctype, and a<?...>processing instruction – whileturbohtml.saxparse.iter_events()yields the same stream as typed records. The events reflect the fully spec-correct tree (impliedhtml/head/body, foster parenting, the adoption agency), so unlikehtml.parser.HTMLParseryou see the tree the parser built; no per-node Python object is created and nothing is retained after the parse, so a one-pass extraction never builds a document-sized object graph. The tokenization, tree construction, and walk all run in C. (#532)turbohtml.clean.Policygainsallowed_styles, a per-element, per-property value allowlist for thestyleattribute keyed{tag: {property: [pattern, ...]}}with"*"matching every tag. A declaration survives only when its value matches one of the property’s patterns, porting sanitize-html’sallowedStyles. It narrowscss_propertiesby value and never weakens the baseline that dropsexpression()and disallowed-schemeurl(). (#533)Policygainedisolate_named_props: with it on, sanitizing prefixes every keptidandnamevalue withuser-content-, moving it out of the property namespace so it cannot shadow a built-indocumentor form property through named access (DOM clobbering, where<input name="attributes">makesform.attributesresolve to the input and<img name="body">hidesdocument.body). An already-prefixed value is left alone, so re-sanitizing is a fixpoint. This matches DOMPurify’sSANITIZE_NAMED_PROPS. (#534)Htmlgainedxml: withxml=True,serialize(),encode(), andserialize_iter()emit XML/XHTML instead of HTML – the equivalent of lxml’stostring(method="xml"). Every empty element self-closes (<br/>), foreign SVG and MathML subtrees carry their namespace declarations, and text and attribute values follow the XML escaping rules, with no HTML void-element or raw-text special casing. It composes withsort_attributesand anIndentlayout. (#535)Policygained a predicate-based custom-element allowance and split content profiles, porting DOMPurify’sCUSTOM_ELEMENT_HANDLINGandUSE_PROFILES.custom_element_checkkeeps an unlisted hyphenated custom element (my-widget,x-card) when a caller-supplied matcher admits its name,custom_attribute_checkextends the same idea to that element’s attributes, andallow_customized_builtinskeeps anisattribute whose value names a custom element.allow_html,allow_svg, andallow_mathmlgate each namespace independently, so a policy can keep SVG but drop MathML, or the reverse. All of it runs in the one C sanitize walk, and the non-configurable safety baseline –on*handlers,javascript:URLs, unsafe tags – still applies to whatever a matcher keeps. (#536)turbohtml.transformadds a full XSLT 1.0 processor, the job lxml’setree.XSLTdoes.turbohtml.transform.Transformcompiles a stylesheet (parsed withturbohtml.parse_xml()) and applies it to source documents, andturbohtml.transform.transform()does both in one call. The whole transform runs in the C extension, reusing turbohtml’s XPath 1.0 engine for every match pattern and select expression. It covers the entire XSLT 1.0 instruction set: templates withmatch/name/mode/priority,apply-templateswithsortandwith-param,call-template,for-each,if,choose,value-of,copy/copy-of,element/attribute/text,variable/param, multi-levelnumber,keywith thekey()function,strip-space/preserve-space,attribute-setwithuse-attribute-sets,namespace-alias,fallback, simplified literal-result-element stylesheets,xsl:importwith import precedence (resolved against abase_url),cdata-section-elements, and thexml/html/textoutput methods (html auto-selected for a null-namespacehtmlroot, with<meta>injection). Validated against libxslt’s XSLT 1.0 Recommendation corpus at 76 of 79 cases byte-for-byte; the three remaining need a locale-collation, DTD, or XPath-namespace-axis layer turbohtml does not carry. (#537)turbohtml.Node.canonicalize()serializes a subtree to Canonical XML (c14n), the byte-exact form an XML signature signs. Aturbohtml.Canonicalconfig selects the algorithm: Canonical XML 1.0 or 1.1, the exclusive variant that renders only the namespaces a subtree visibly uses, the with-comments variant, and aninclusive_ns_prefixesprefix list for exclusive mode. Attributes are reordered (namespace declarations first, then by namespace URI and local name), redundant namespace declarations are dropped, empty elements are written as start-end pairs, and character references are normalized, matchinglxml’stostring(method="c14n")byte-for-byte over the same infoset. (#538)turbohtml.validate.XMLSchemaandturbohtml.validate.RelaxNGvalidate a document parsed withturbohtml.parse_xml()against an XSD 1.0 or RELAX NG schema, mirroring lxml’setree.XMLSchema/etree.RelaxNG. A schema compiles once in the C core and eachvalidate()returns aValidationResult– avalidflag plus oneValidationErrorper violation, each with the document-order path that located it. XSD covers the element/attribute declarations, the sequence/choice/all content models withminOccurs/maxOccurs, references, complex/simple types with extension, the built-in datatypes, and the constraining facets; RELAX NG covers the full XML-syntax pattern set (includinginterleave) through the derivative algorithm. (#539)turbohtml.parse_xml()parses a document under XML 1.0 well-formedness instead of the WHATWG HTML tree builder, returning the same navigableDocument. Names stay case-sensitive,<x/>self-closes any element, CDATA sections and processing instructions becomeCDataandProcessingInstructionnodes, only the five predefined entities and numeric references resolve, and a namespace prefix must be declared withxmlns. Names follow the exact XML 1.0NameStartChar/NameCharproductions, and the Namespaces in XML 1.0 well-formedness constraints hold in full: the reservedxmlandxmlnsprefixes and their namespace names cannot be rebound, a prefix declaration cannot be empty, a processing-instruction target carries no colon, and no two attributes share an expanded name. There is no HTML recovery: the first well-formedness violation – a mismatched or unclosed tag, an undeclared prefix, an undefined entity, a duplicate attribute – raisesHTMLParseError. This is the equivalent oflxml.etree.fromstring/etree.XMLParserover turbohtml’s dependency-free, fully typed node API. (#540)Added an HTML5 authoring-conformance checker with a severity model.
turbohtml.conformance.check()walks a parsed document and returns aConformanceReport– avalidverdict plus everyConformanceMessage, each carrying a stablecode, aseverity("error","warning", or"info"), a human-readable message, and a source line and column. It flags the document-conformance requirements the parser does not raise as aParseError: a missingimgalt, obsolete presentational elements and attributes, duplicate ids, invalid or redundant ARIA roles, empty headings, asectionwithout a heading, and a document with no title orlang. The document is valid exactly when nothing is an error, so warnings and info notes never change the verdict. The whole walk runs in the C core against the WHATWG authoring rules and WAI-ARIA 1.2, the model the Nu Html Checker (validator.nu) uses;check_html()parses a markup string first. (#541)xpath()gained the string subset of XPath 2.0:ends-with,string-join(seq, sep),lower-caseandupper-case(Unicode case mapping), and the regexmatches(input, pattern[, flags])andreplace(input, pattern, repl[, flags])spellings, wherereplacereads$1-style group references and rewrites every match. They dispatch in the compiled-C engine alongside the XPath 1.0 core and the EXSLT namespaces, so an expression ported fromelementpath,lxml, orhtmlquerythat leans on them runs without registration. (#542)turbohtml.detect.normalize()returns text in a Unicode normalization form (UAX #15) –NFC,NFD,NFKC, orNFKD– the C successor tounicodedata.normalize(), andturbohtml.detect.is_normalized()tests membership. Both run over tables generated from the interpreter’s ownunicodedata, so they agree with it exactly, and a quick check returns already-normalized text without allocating. (#543)turbohtml.rewrite.rewrite()transforms HTML in a single streaming pass without building a tree, the model Cloudflare’s lol-html popularized. It runs the WHATWG tokenizer over the input while tracking only the open-element stack, hands each element a CSS selector matches – and, on request, each run of text, each comment, and the doctype – to a Python handler that edits it in place (set or remove an attribute, insert markup before, after, or around it, replace its inner content, unwrap it, or drop it), and emits the result incrementally. Working memory stays proportional to the open-element depth, not the document size, so a multi-megabyte page rewrites in a fixed footprint, and an untouched construct is reproduced verbatim. Because the pass never looks ahead, the matchable selector subset is the one decidable from an element and its ancestors – type, universal, id, class, and attribute selectors, the descendant and child combinators,:root, and:is()/:where()/:not()over that subset; a sibling combinator, a positional or structural pseudo-class, or:has()raisesSelectorSyntaxError. (#544)A new
turbohtml.treebuildmodule retargets the parser at a tree of your own.turbohtml.treebuild.parse_into()runs the WHATWG tree builder and drives a builder object – acreate_*method per node kind plus anappendthat links a child under its parent – to construct the tree directly, returning whatever the builder made its document root. No navigableturbohtml.Nodeis materialized and the tree is walked only once, so an index, a diff tree, or another library’s nodes is populated straight from the parse rather than by a second descent. Each element carries its namespace URI and its attributes as(name, value)pairs, a<template>’s content is appended under the template handle, and a bogus<?...>construct reaches a distinctcreate_pi. This is Rust html5ever’sTreeSinkand Node parse5’sTreeAdapterin turbohtml shape; the tree construction, the walk, and the string extraction all run in C. (#545)turbohtml.cssomruns the CSS Object Model cascade:computed_style()resolves thegetComputedStyleof an element by collecting every<style>sheet plus the inlinestylealong its ancestor chain, matching the native selector engine, ordering the declarations by origin importance, the style attribute, specificity, and source order, then applying inheritance, shorthand expansion, and each property’s initial value – all in the C core under the per-tree critical section. Alongside it,StyleSheet,RuleList,StyleRule, andStyleDeclarationare the read-only, turbohtml-native spelling of the CSSOMCSSStyleSheet/CSSRuleList/CSSStyleRule/CSSStyleDeclarationinterfaces. The returned value is the computed value, not the used value: turbohtml runs no layout, so lengths and percentages come back as written, the same boundary jsdom and cssstyle draw. Shorthand expansion covers the distributive families (margin,padding,border-width/style/color,overflow) and the<line-width> || <line-style> || <color>shorthandsborder, eachborder-<side>, andoutline, whose components resolve in any order and reset every longhand they cover. (#546)turbohtml.Node.to_source()losslessly serializes a tree back to HTML, re-emitting the verbatim source bytes of every element and text run a parse left untouched and reserializing only the parts a mutation changed. Parse withsource_locations=Trueand an unedited round trip reproduces the input byte for byte – author quoting, tag-name case, character-reference spelling, and insignificant whitespace intact – for markup that parsed without implied elements or content reordering; after an edit only the changed node’s markup is rewritten while every untouched sibling and subtree keeps its original span. It is the tree-based counterpart to the streamingturbohtml.rewrite.rewrite(), the model Cloudflare’s lol-html popularized. (#547)turbohtml.parse(),turbohtml.parse_fragment(), andIncrementalParsergained asource_locationsflag (defaultFalse) that records the granular source spans parse5 exposes assourceCodeLocationInfo. With it on, each element’ssource_locationreturns aSourceLocationgiving theSourceSpanof its start tag, its end tag (Nonewhen the source never closed it), and each attribute’s wholename="value", every span carrying start/end line, column, and code-point offset sosource[start_offset:end_offset]slices the construct out. The tokenizer stamps the spans in C as it runs and the tree builder hangs the record off each element, so the feature is zero-overhead when off; it impliespositionswhen on, keepingsource_lineandpositionavailable beside the spans. (#548)Added declarative Shadow DOM to the parser. When the tree builder meets a
<template>carrying ashadowrootmodeofopenorclosedon a valid shadow host, it attaches a shadow root to the template’s parent and parses the template’s content into it, reusing the Shadow DOM tree model – the template element never joins the light tree.shadowrootdelegatesfocusandshadowrootclonableset the matching flags, readable as the newdelegates_focusandclonableproperties. Following the WHATWG per-document flag,turbohtml.parse()honors declarative shadow roots by default (a browser navigation) whileturbohtml.parse_fragment()does not (aninnerHTMLassignment); the newallow_declarative_shadow_rootsargument flips either default, matchingsetHTMLUnsafewhen turned on for a fragment. (#549)Added the DOM Living Standard traversal objects
turbohtml.TreeWalkerandturbohtml.NodeIterator, with theturbohtml.NodeFilterconstants for thewhat_to_showbitmask and the filter verdicts. ATreeWalkeris a movable cursor over a subtree –parent_node,first_child,last_child,next_sibling,previous_sibling,next_node,previous_node– while aNodeIteratoris the flat, filtered forward/backward view and iterates directly in aforloop. Both take awhat_to_shownode-type mask and an optional filter callback returningFILTER_ACCEPT,FILTER_REJECT, orFILTER_SKIP; reject drops a node and its whole subtree while skip drops only the node, so aTreeWalkerprunes where aNodeIterator(having no subtree) treats the two alike. The state machine and thewhat_to_showtest run in the C core; the filter is the one callback into Python. This ports traversal code written against the browser DOM or jsdom. (#550)turbohtml.parse()andturbohtml.parse_fragment()gained ascriptingflag (defaultFalse). With it on, turbohtml sets the WHATWG scripting flag:<noscript>becomes a raw-text element, so its content is one text run rather than parsed markup and serializes back unescaped, reproducing the tree a scripting browser builds. The flag is a property of the parsed tree, so the serializer andinner_htmlstay consistent with how it was parsed. parse5 and html5ever default this on for browser fidelity; turbohtml keeps it off so<noscript>fallback content stays navigable. (#551)Added the DOM Living Standard
RangeandStaticRangetypes. ARangeholds two boundary points – each a(container, offset)pair – and carries the full boundary API (set_start()/set_end()and their_before/_aftervariants,select_node(),select_node_contents(),collapse()), the derivedcollapsedandcommon_ancestor_containerproperties, the comparisons (compare_boundary_points(),compare_point(),is_point_in_range(),intersects_node()), and the content operations (clone_contents(),extract_contents(),delete_contents(),insert_node(),surround_contents(),clone_range()), each following the WHATWG boundary-point ordering and extract/clone/delete algorithms in C under the per-tree critical section.StaticRangeis the immutable four-value snapshot. Offsets index code points in character data and children elsewhere, so a Python string’s own indexing lines up with a text-node offset. (#552)Added the DOM Living Standard Shadow DOM tree model.
attach_shadow()attaches an open or closed shadow tree and returns aShadowRoot– a document-fragment-like root held off the light tree, so it never appears among the host’s children or in its serialization – reachable throughshadow_root(Nonefor a closed root) and carryingmode,host,set_inner_html(), andappend().<slot>elements assign the host’s children by name (the unnamed default slot takes the rest):assigned_nodes()andassigned_elements()read what a slot received, with aflattenoption that falls back to a slot’s own children and expands nested shadow slots, andassigned_slotgives the slot a child landed in.flattened_childrenreturns the composed tree with every slot replaced by its assigned nodes. The assignment and flattening algorithms run in C under the per-tree critical section and are computed on demand, so they always reflect the current tree. (#553)Added
MutationObserver, a synchronous take on the DOMMutationObserverfor recording tree edits. Register a node withobserve()and the DOM options (child_list,attributes,character_data,subtree,attribute_old_value,character_data_old_value,attribute_filter); every change made through the mutation API queues aMutationRecordcarrying the added and removed nodes, the surrounding siblings, and the attribute name and old value when asked, following the WHATWG “queue a mutation record” algorithm in C under the per-tree critical section. Because turbohtml has no event loop, delivery is synchronous rather than microtask-scheduled:take_records()returns and clears the queued batch, anddeliver()drains it and calls the observer’s callback.disconnect()stops observing and discards pending records. (#554)Policygainedxml: with it on, the sanitizer serializes the cleaned tree as well-formed XML/XHTML instead of HTML. Every kept empty element self-closes (<br/>), foreign SVG and MathML subtrees declare their namespace, text and attribute values follow the XML escaping rules, and a kept comment, a control character outside XML’sCharproduction, or an attribute name XML cannot hold is neutralized, so the output always reparses throughturbohtml.parse_xml(). The walk and the safety baseline are unchanged, so an XML-mode policy is exactly as safe as its HTML-mode twin. This clones DOMPurify’sPARSER_MEDIA_TYPE: 'application/xhtml+xml'and replaces the brittle.replace("<br>", "<br/>")a bleach-based cleaner needs to feed a strict XHTML consumer such as Reportlab’s RML.turbohtml.Node.inner_xmlexposes the same children-only XML serialization for any node. (#565)The
rewritebenchmark now runs against a fair in-process peer. lxml and BeautifulSoup do the same edits –rel=nofollowon every link,loading=lazyon every image, every comment dropped – through the parse, mutate, and serialize round trip thatturbohtml.rewrite.rewrite()skips, and the table reports each party’s peak resident memory beside throughput, so the tree the streaming rewriter never builds shows up as memory it never holds. The lol-html migration guide carries the numbers. (#612)
v1.0.0 (2026-07-05)¶
The 1.0 release finishes the native-C port, settles one canonical public API, and closes the feature gap against the libraries turbohtml replaces. The notes below fold the whole 0.4.0 to 1.0.0 span into one overview; the anchor issues point at the epics behind each theme.
Backward incompatible changes - 1.0.0¶
Give the public surface one name per concept. CSS matching folds from
turbohtml.matchintoturbohtml.query; the sanitizer, linkifier, and every minifier gather underturbohtml.clean; a malformed selector raises oneturbohtml.SelectorSyntaxErrorfrom every parse path; the twoDetectorclasses split intoturbohtml.detect.EncodingDetectorandturbohtml.clean.LinkDetector; and each surface with more than six arguments takes one frozenoptionsconfig. (#478)Select a serialization mode with a single
layoutargument in place ofindent, soserialize(indent=2)becomesserialize(layout=Indent(2))andMinifyselects minified output. (#171)Report a valueless attribute (
<x a>) as the empty string rather thanNoneinturbohtml.Element.attrs, matching the WHATWG tokenizer and the DOM. (#87)
Features - 1.0.0¶
Query a tree with the full Selectors Level 4 grammar (
:is(),:where(),:has(),:not(),:nth-child(An+B of S), the structural, input,:lang(),:dir(), and:scopepseudo-classes) and an XPath 1.0 engine:xpath(), a compiled reusableturbohtml.XPath, the EXSLT function set, and namespace, variable, and extension binding. A pyquery-styleturbohtml.query.Query, a soupsieve-shaped matcher, andturbohtml.convert.css_to_xpath()cover the BeautifulSoup, cssselect, parsel, and pyquery surfaces. (#179)Serialize back to HTML with pretty-print, whitespace minification, and lazy streaming (
serialize_iter()), and minify HTML, CSS, and JavaScript through native engines underturbohtml.clean. Every transform is round-trip safe, replacing rcssmin, csscompressor, rjsmin, jsmin, minify-html, and htmlmin. (#343, #346)Convert a tree to GitHub-Flavored Markdown, layout-aware text, or annotated
(start, end, label)spans, and extract a page’s main article, boilerplate paragraphs, publication date, tables, links, and structured data (JSON-LD, Microdata, RDFa, Dublin Core, Open Graph) throughturbohtml.extract, replacing markdownify, html2text, inscriptis, trafilatura, readability, htmldate, extruct, and microdata. (#273, #276)Detect a byte stream’s encoding and a text’s natural language from the standalone
turbohtml.detectmodule, which reports confidence and a BOM label, covers 69 languages across nine scripts, and replaces chardet, charset-normalizer, and cchardet. (#315, #474)Parse incrementally from a stream with
turbohtml.IncrementalParser, read the WHATWG parse errors recovery swallows througherrors, and locate each element in the source throughsource_lineandsource_col. (#210, #212)Sanitize untrusted HTML against a frozen allowlist
turbohtml.clean.Policythat is safe with no arguments and scrubs inline and embedded CSS, foreign namespaces, and media hosts, and auto-link URLs and email addresses, together replacing bleach. (#8, #9)Build and edit the tree in place: construct nodes and a whole HTML5 page with
turbohtml.build.Eandturbohtml.build.document(), edit the class token set and inner HTML or text, wrap and unwrap subtrees, trim a document to a selector withprune(), read and fill form fields, and compare two subtrees withequals(). (#275, #225, #468)Clean, canonicalize, and extract page URLs through
turbohtml.extract, backed by a native URL pipeline (WHATWG splitting, percent-coding, relative-reference joining, UTS #46 IDNA, and Public Suffix List registrable-domain filtering), and run the toolkit from aturbohtmlcommand line covering minify, detect, to-markdown, to-text, and sanitize. (#321, #470)
Bug fixes - 1.0.0¶
Bring tree construction to WHATWG conformance across foster parenting, foreign content, the select, table, and template insertion modes, doctype and quirks detection, duplicate and void-element handling, validated against the html5lib-tests corpus. (#32)
Resolve every label in the WHATWG Encoding Standard, decode the replacement and gb18030 families and the windows-1252 C1 bytes, and honor the 1024-byte
<meta>prescan. (#54, #423)Match the Selectors Level 4 corrections: forgiving
:is()/:where()lists, quirks-mode case folding, CSS escape decoding, namespace prefixes, whitespace-only:empty, and:scoperesolution inside:has(). (#174)Fix XPath 1.0 semantics: shortest round-tripping number formatting, half-to-positive-infinity rounding, fixed function arity, node-set typing, and arena-safe predicate compilation. (#398)
Correct the Markdown and text exporters on nested and loose lists, link and image escaping, the
<pre>leading newline, ordinal<li value>, and raw-text suppression in every namespace. (#384)Fix extraction on landmark-wrapped and list-structured articles, Microdata
itemrefmerging, tablerowspanbounds, and the WHATWG formselect/optgroupsubmission rules. (#385, #408)Keep the CSS and JavaScript minifiers value-safe:
calc()type mixing, duplicate-declaration fallbacks,currentcolor, conditional folding, and adjacent-string-literal concatenation. (#415)Raise a precise, typed error from every public failure path in place of a silent wrong result or a leaked low-level type, and document each callable’s exceptions. (#434)
Improved documentation - 1.0.0¶
Ship a migration guide for each library turbohtml replaces, every one carrying a monthly-downloads badge, a measured benchmark, and a speed-up multiplier, with the index ordered by adoption. (#313)
Restructure the reference, how-to, and migration trees around the eight-namespace taxonomy (parse/DOM, detect, query, clean, convert, extract, build, serialize), and add
llms.txtmaps, a sitemap, and per-page meta descriptions. (#478)Add a written security policy, a “How turbohtml was built” page, and the seven design principles that shape the library. (#500)
Packaging updates - 1.0.0¶
Build the release wheels with profile-guided, link-time optimization trained offline over a real-world corpus of clean, malformed, legacy-encoded, and structured-data markup. (#481)
Pin every network-sourced C data table (IANA TLDs, the Public Suffix List, and the Unicode IDNA and NFC tables) to a named source commit and a SHA-256 checksum, so a rebuild is reproducible and aborts on a poisoned upstream. (#478)
Strip the compiled extension’s local symbol table from the release wheels at link time, trimming about 65 KB from the Linux
.soand 46 KB from the macOS bundle; the source distribution still carries every test, tool, and generated table needed to build from source. (#478)
Miscellaneous internal changes - 1.0.0¶
Finish the native-C port: URL splitting, percent-coding, relative joining, IDNA, registrable-domain lookup, and date parsing move from
urllib,re, anddatetimeinto the C extension, leaving Python a thin configure-and-wrap shim. (#478)Speed the read path: a per-tree atom index runs a tag-pinned
find()several times faster,:has()evaluates in one amortized-linear pass, streaming serialization sizes its buffer up front, and link-time optimization re-inlines across a subsystem-first C layout. (#162, #509)Harden against untrusted input: ASan and UBSan fuzz gates on every entry point, a DOMPurify XSS oracle and an html5lib-tests tree-equality oracle over the sanitizer, mutation-XSS namespace checks, caps on element nesting and duplicate attributes, and one overflow-safe buffer-growth helper across the C core. (#503, #511)
Validate free-threaded safety under pytest-run-parallel and ThreadSanitizer, and gate performance on every pull request with a per-operation CodSpeed benchmark over the real corpora. (#380)
v0.4.0 (2026-06-16)¶
Features - 0.4.0¶
Build and edit the tree, not just read it: construct
Element,Text, andCommentnodes and rearrange them with the full set of insert, wrap, extract, and normalize methods, withattrsand.text/.dataas live setters.copy,deepcopy, andpickleduplicate a subtree - by @gaborbernat. (#19)Round out the node model:
ProcessingInstructionandCDatajoin the hierarchy,Doctypeexposes itspublic_idandsystem_id, and every node type supports structural pattern matching - by @gaborbernat. (#22)
Improved documentation - 0.4.0¶
Learn the write path through new tutorial, how-to, and explanation docs, backed by benchmarks showing turbohtml builds and rewrites trees about twice as fast as lxml and an order of magnitude faster than BeautifulSoup - by @gaborbernat. (#19)
Port to turbohtml with migration guides from BeautifulSoup, lxml, selectolax, html5lib, and the standard library, each mapping the source library’s idioms to their turbohtml equivalents and flagging behavior differences - by @gaborbernat. (#23)
v0.3.0 (2026-06-16)¶
Features - 0.3.0¶
Query any node with CSS through
select()andselect_one(), a native matcher covering type, universal,#id,.class, and attribute selectors (all operators plus the case-sensitivity flag) across the descendant, child, adjacent, and sibling combinators, returning comma groups in document order. An invalid selector raisesValueError- by @gaborbernat. (#14)Search with a richer
find()andfind_all()filter grammar: match the tag and attributes by string, regex, bool, callable, or list (includingclass_and theattrsmapping), and choose the search direction with theaxiskeyword.find_alltakes alimitand returns alist- by @gaborbernat. (#15)Test a node against a selector with
matches()andclosest():matches()reports whether the node satisfies a CSS selector in context, andclosest()returns the nearest matching ancestor (or the node itself), orNone- by @gaborbernat. (#16)Walk the tree by axis with new iterators:
next_siblings,previous_siblings, document-orderfollowingandpreceding, plus thestringsandstripped_stringstext iterators - by @gaborbernat. (#17)Read HTML token-list attributes (
class,rel,headers,sizes,sandbox, and the rest) as alist[str]inturbohtml.Element.attrs, split on ASCII whitespace; other attributes stay strings and valueless ones stayNone- by @gaborbernat. (#18)Control serialization on any node:
inner_htmlreturns the children, whileserialize()andencode()take aformatter(theFormatterenum picks the escape policy) and anindentfor pretty output. The default stays WHATWG-conformant HTML - by @gaborbernat. (#20)Parse
bytesdirectly:turbohtml.parse()sniffs the encoding with the WHATWG algorithm (BOM,encodingargument,<meta>charset, then windows-1252), decodes with U+FFFD replacement, and reports the result inencoding- by @gaborbernat. (#21)
v0.2.0 (2026-06-11)¶
Features - 0.2.0¶
Tokenize HTML directly with a WHATWG-conformant tokenizer:
turbohtml.tokenize()for whole strings, the streamingturbohtml.Tokenizer, and theturbohtml.Token/turbohtml.TokenTypetypes, validated against the html5lib-tests tokenizer conformance suite. (#6)Run
turbohtml.escape()andturbohtml.unescape()faster: vectorized scanning and bulk copying speed up both calls, with unescaping of real escaped HTML about three times faster than the general lookup path. The benchmark now uses pyperf over multi-MiB real documents - by @gaborbernat. (#7)
v0.1.1 (2026-06-09)¶
Packaging updates - 0.1.1¶
Install reliably from PyPI again: publishing each wheel in its own job keeps PEP 740 attestations within the Sigstore identity’s lifetime, fixing the
sigstore.oidc.ExpiredIdentityfailure that blocked the first upload - by @gaborbernat. (#4)
v0.1.0 (2026-06-09)¶
Features - 0.1.0¶
Speed up entity handling with C-accelerated
turbohtml.escape()andturbohtml.unescape(), drop-in replacements forhtml.escape()andhtml.unescape(), shipped as wheels for CPython 3.10 through 3.15 - by @gaborbernat. (#1)Escape non-ASCII text that needs no escaping several times faster with a vectorized special-character scan, ahead of
html.escape()- by @gaborbernat. (#3)
Improved documentation - 0.1.0¶
See the measured
turbohtml.escape()/turbohtml.unescape()speedups in the README and docs, reproduce them withtox -e bench, and browse a typed API reference with intersphinx links - by @gaborbernat. (#2)
Miscellaneous internal changes - 0.1.0¶
Automate releases with git-tag-derived versioning, a towncrier-managed changelog, and a prepare-release workflow that tags and triggers the trusted-publishing wheel build - by @gaborbernat. (#1)