justhtml

repository·main·Indexed 22 days ago

https://github.com/emilstenstrom/justhtml

A pure-Python HTML5 parser and sanitizer (version 3.10.1) designed for reliability and security. It provides browser-style parsing, safe-by-default sanitization, and a DOM for handling untrusted or malformed HTML. Features include a CLI for pretty-printing and text extraction, CSS selector queries, a programmatic Builder API, and serialization to HTML, plain text, or a subset of GitHub Flavored Markdown.

Tokens
49.4K
Snippets
184
Records
224
Agent score
78%

What's inside justhtml

  1. Use built-in transforms in JustHTML

    main

    JustHTML uses a pipeline of transforms passed to the JustHTML constructor to modify the HTML tree in memory. Transforms can be categorized into built-in high-level transforms, core selector-based transforms, and advanced building blocks for custom pipelines.

    Built-in Transforms

    • Linkify(...): Converts URLs/emails in text nodes to <a> elements.
    • CollapseWhitespace(...): Normalizes whitespace in text nodes.
    • Sanitize(...): Runs a security-focused sanitization pipeline.
    • PruneEmpty(...): Recursively removes empty elements.
    • Stage([...]): Splits transforms into explicit passes.

    Core Selector Transforms

    These target specific elements using selectors:

    • SetAttrs(selector, attributes=None, **attrs): Overwrites attributes.
    • Drop(selector): Removes nodes and their children.
    • Unwrap(selector): Removes the tag but keeps the children.
    • Escape(selector): Turns tags into escaped text but keeps children.
    • Empty(selector): Removes all children of the matching element.
    • Edit(selector, func): Executes custom logic on matching nodes.
  2. How HTML cleaning and sanitization works

    main

    JustHTML uses a policy-driven sanitizer to render untrusted HTML safely. By default, it follows a conservative allowlist approach (DEFAULT_POLICY) that strips dangerous tags (like <script> or <style>) and removes event handlers (on*), srcdoc, and namespace-style attributes (containing :).

    Key behaviors include:

    • Tag Handling: Disallowed tags are removed, but their children may be kept depending on the policy.
    • Content Stripping: Dangerous containers like <script> have their entire content dropped.
    • Unicode Cleaning: Invisible Unicode characters used for obfuscation are stripped before sanitization.
    • Foreign Namespaces: SVG and MathML are always dropped by the sanitizer, even if explicitly allowed in a custom policy.
    • URL Safety: By default, a[href] allows common schemes, but img[src] only allows relative URLs to prevent remote resource loading unless configured otherwise.
  3. When to use JustHTML vs other HTML tools

    main

    JustHTML is a pure-Python package designed for a complete HTML pipeline: browser-grade parsing, safe-by-default sanitization, CSS selectors, transforms, text extraction, and serialization.

    Use JustHTML when:

    • You need a single package that handles parsing, sanitization, and querying on a single DOM.
    • You want predictable behavior when dealing with untrusted input.
    • You need an easy-to-install, pure-Python solution (e.g., for Pyodide or environments where native extensions are difficult).

    Choose another tool if:

    • Maximum throughput is the priority: Use selectolax (C-based) or turbohtml (compiled).
    • You need a BeautifulSoup-specific API: Use BeautifulSoup.
    • You are doing XPath-heavy XML work: Use lxml.
    • You only need fast sanitization: Use nh3.
    • You need a tiny stdlib-only script for trusted input: Use html.parser.
  4. Map Bleach filters to JustHTML transforms

    main

    JustHTML uses a transform pipeline that is applied once immediately after parsing. This replaces the html5lib filter pipeline used in Bleach. Use the following mapping to migrate your logic:

    Bleach / html5lib FeatureJustHTML Transform
    bleach.linkify(...)Linkify()
    html5lib.filters.whitespace.FilterCollapseWhitespace()
    "Strip tag but keep contents"Unwrap(selector)
    "Drop tag and contents"Drop(selector)
    "Remove children"Empty(selector)
    "Set attributes"SetAttrs(selector, **attrs)
    "Custom rewrite"Edit(selector, func)

    Example of composing transforms to linkify text and add rel="nofollow noopener" attributes:

    from justhtml import JustHTML, Linkify, SetAttrs
    
    doc = JustHTML(
        "<p>See example.com</p>",
        fragment=True,
        transforms=[
            Linkify(),
            SetAttrs("a", rel="nofollow noopener", target="_blank"),
        ],
    )
    
    # Still sanitized by default (construction time)
    print(doc.to_html(pretty=False))
  5. Handle byte input and encoding sniffing in JustHTML

    main

    JustHTML supports both Unicode strings (str) and raw byte streams (bytes, bytearray, memoryview).

    When you provide bytes, JustHTML automatically performs encoding sniffing and decoding to convert the input into a string before tokenization. The detected encoding is accessible via the doc.encoding attribute on the resulting document object.

    Encoding Precedence Logic: JustHTML follows the standard HTML precedence for byte input:

    1. Explicit override: The encoding= parameter passed to the constructor.
    2. BOM: The Byte Order Mark.
    3. Meta tags: <meta charset=...> or <meta http-equiv=... content=...> found in the initial bytes.
    4. Fallback: Defaults to windows-1252 (cp1252) if no other information is found. Note that utf-7 is treated as unsafe and will trigger a fallback to windows-1252.
    from justhtml import JustHTML
    from pathlib import Path
    
    data = Path("page.html").read_bytes()
    doc = JustHTML(data)
    print(doc.encoding)  # Access the detected encoding
  6. How JustHTML handles sanitization and the DOM pipeline

    main

    JustHTML performs all operations (parsing, sanitizing, querying, and serializing) on a single DOM. This ensures that the behavior is consistent and easier to reason about, especially with untrusted input.

    Key Behaviors:

    • Safe-by-default: Sanitization occurs automatically during the process unless you explicitly disable it by passing sanitize=False.
    • Sanitization Scope: The sanitizer emits HTML-only output. While SVG and MathML can be parsed when sanitization is disabled, sanitized output will drop foreign-namespace content to maintain a smaller, more secure model.
    • Pipeline Integrity: Because everything happens on one DOM, you don't have to worry about the discrepancies that occur when passing data between different libraries (e.g., a parser and a separate sanitizer).
    from justhtml import JustHTML
    
    # By default, malicious tags like <script> and dangerous attributes are removed
    doc = JustHTML("<p>Hello<script>alert(1)</script><a href='javascript:x'>link</a></p>", fragment=True)
    
    print(doc.to_html(pretty=False))
    # <p>Hello<a href='link'>link</a></p>
    # Note: The exact output depends on the sanitizer's rules for the href attribute
  7. Configure URL policies and protocols

    main

    JustHTML's protocols concept from Bleach is implemented via UrlPolicy and UrlRule.

    To configure allowed schemes for specific attributes:

    1. Use UrlRule(allowed_schemes=[...]) to define which protocols are permitted.
    2. Note that URL-like attributes (such as href, src, srcset, etc.) require explicit (tag, attr) allow rules in the policy, even if the attribute itself is allowed for that tag.
    3. Under sanitize=True, URLs are automatically rewritten or stripped according to the defined policy.
  8. How URL cleaning works in JustHTML

    main

    JustHTML treats certain attributes (like href, src, srcset, action) as URL-like. Because these can trigger navigation or resource loading, they require explicit rules to be kept. Even if a tag and attribute are allowed in SanitizationPolicy, the URL-like attribute will be dropped unless a matching rule exists in UrlPolicy(allow_rules=...).

    The URL cleaning lifecycle:

    1. Tag Check: The tag must be in SanitizationPolicy.allowed_tags.
    2. Attribute Check: The attribute must be in SanitizationPolicy.allowed_attributes.
    3. Rule Match: An explicit matching rule must exist in UrlPolicy(allow_rules=...) for the (tag, attr) pair.
    4. Filter (Optional): If UrlPolicy.url_filter is configured, it runs and can rewrite or drop the value.
    5. Validation: The value is normalized and validated by the matching UrlRule (checking schemes, hosts, etc.).
    6. Handling: If validation passes, the final action is applied based on UrlRule.handling or, if not set, UrlPolicy.default_handling.
  9. When to use Streaming vs DOM API

    main

    Choosing between the stream() API and the JustHTML (DOM) API depends on your requirements:

    Use stream() when:

    • Processing very large HTML files.
    • You only need specific elements (e.g., all links).
    • Memory is constrained.
    • You do not need to traverse the tree (up/down/siblings).

    Use JustHTML (DOM) when:

    • You need to query with CSS selectors.
    • You need to traverse the tree structure.
    • You need to serialize the document back to HTML.
    • The document fits comfortably in memory.
  10. How Linkify handles Unicode and Punycode (IDNA)

    main

    Linkify supports domains containing Unicode characters. When generating a link, it normalizes the hostname portion of the href attribute using IDNA (punycode). This ensures the href remains ASCII-only while the visible link text remains human-readable.

    Note that only the host is punycoded; paths and query parameters remain in Unicode. This applies to http://, https://, ftp://, and protocol-relative //... URLs.

    from justhtml import JustHTML, Linkify
    
    doc = JustHTML("<p>See bücher.de</p>", fragment=True, transforms=[Linkify()])
    print(doc.to_html(pretty=False))
    # => <p>See <a href="http://xn--bcher-kva.de">bücher.de</a></p>
  11. Traverse the HTML tree using Node objects

    main

    The parser returns a tree of Node objects. You can navigate the tree using .children, .parent, and access node properties like .name and .attrs.

    from justhtml import JustHTML
    
    html = "<html><body><div id='main'><p>Hello, <b>world</b>!</p></div></body></html>"
    doc = JustHTML(html)
    
    root = doc.root              # #document
    html_node = root.children[0] # <html>
    body = html_node.children[1] # <body>
    div = body.children[0]       # <div>
    
    print(div.name)                                # => div
    print(div.attrs)                               # => {'id': 'main'}
    print([child.name for child in div.children])  # => ['p']
    print(div.parent.name)                         # => body
  12. Understand child coercion rules in the builder

    main

    The builder applies specific coercion rules to children to ensure valid HTML construction:

    Children Coercion:

    • Strings: Converted to text nodes.
    • Iterables: Flattened into the parent node.
    • None and False: Ignored (useful for conditional logic).
    • Numbers: Rejected. Passing a number as a child will raise a TypeError.

    Attribute Coercion:

    • None: Represents a present boolean attribute (e.g., required).
    • Other values: Converted to strings.

    Example:

    # Valid: string becomes text
    element("p", "Hello") 
    
    # Valid: None becomes boolean attribute
    element("input", {"required": None}) # => <input required>
    
    # Invalid: raises TypeError
    element("p", 1)
    from justhtml.dom.builder import element
    
    # Boolean attribute example
    print(element("input", {"maxlength": 10, "required": None}).to_html(pretty=False))
    # => <input maxlength="10" required>
    
    # This will raise TypeError
    try:
        element("p", 1)
    except TypeError:
        print("Numbers are not allowed as children")