module

Hwaro::Utils::HtmlMinifier

Constants

BLOCK_TAGS = Set {"address", "article", "aside", "base", "blockquote", "body", "caption", "col", "colgroup", "dd", "details", "dialog", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "li", "link", "main", "menu", "meta", "nav", "noscript", "ol", "p", "pre", "script", "section", "source", "style", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul"}

HTML block-level elements. Whitespace adjacent to a block-level neighbour has no visual effect:

  • between two block siblings the user agent renders them on separate lines anyway,
  • leading whitespace inside a block parent is collapsed by the browser before the first inline child,
  • trailing whitespace before a block close is collapsed after the last inline child. Anything not listed here is treated as inline, where whitespace between siblings collapses to a single rendered space and must therefore be kept as one literal space. Replaced elements that default to display:inline (iframe, video, audio, canvas, embed, object — and picture, an inline wrapper around its <img>) are deliberately NOT listed: whitespace adjacent to them renders as a visible space and must be preserved.
PROTECTED_HIDDEN = Set {"script", "style", "noscript"}

Protected tags that render nothing (display:none; noscript while scripting is on). They take no part in whitespace collapsing, so the space beside one still separates the inline content around it: <b>a</b> <script>…</script> <b>c</b> renders "a c", and treating the script as block deleted that gap ("ac").

PROTECTED_INLINE = Set {"code", "svg", "math", "textarea"}

Classification of protected tags by default HTML display so the inter-token whitespace pass can treat sealed blocks as if they were their original tag.

PROTECTED_TAGS = ["pre", "textarea", "style", "script", "code", "svg", "math", "noscript"] of ::String

Tags whose content must be preserved verbatim. Extracted into placeholders before any whitespace pass and restored afterward. code is included because inline-code snippets often carry whitespace that authors expect to ship unchanged.

Order matters: a <style> body may legitimately contain the literal string <script> (e.g. content: "<script>"), so style is extracted first to remove that text from view before the script pass runs.

Instance methods

minify(html : String) : String
Source
restore_sensitive_blocks(html : String, preserves : Array(String)) : String

Restore in a fixed-point loop: a protected block (e.g. a <style> extracted before <script>) can be captured inside a later block, so the first restore leaves an inner placeholder behind. Re-scan until no token remains; a dangling index returns $0 unchanged. The original termination argument — "restored content is author HTML, which cannot contain a valid \x00-delimited token because NUL is illegal in author input" — turned out to be false (NUL reaches the rendered page through data-file values), so termination now rests on minify()'s NUL scrub plus the hard pass cap below rather than on that invariant alone.

Public (not private) so the counterfeit-token guards below stay testable: with minify's NUL scrub in front of it, no forged token can reach this method through the public entry point any more, and a spec that goes through minify therefore exercises neither guard — a mutation that deletes both still passes. They are defence-in-depth for any future caller, and dead guards nobody can test are how they rot.

Source
scrub_nul(html : String) : String

Minify the given HTML.

Example: minify("

\n

Hello

\n
")

=> "

Hello

"

minify("x\ny")

=> "x y"

Drop every NUL byte from a rendered document.

NUL is not valid in HTML text (parsers replace it with U+FFFD), and it never appears in author input — but it does reach the rendered page through a data-file value (data/x.json, data/x.yml), and its absence is the ONLY thing that makes the \x00-delimited sentinels below collision-proof: a forged \x00HW_HTML_PB_0\x00 arriving that way made restore_sensitive_blocks substitute a preserved block into itself, the string grew by one copy per pass, and the fixed point never converged — hwaro build --minify hung forever.

Applied by the RENDER phase to every page, minified or not, so the two build modes cannot disagree on page bytes: scrubbing only inside minify made an optional whitespace pass silently delete a byte of author-controlled content that a plain build emitted. Dropping (rather than substituting U+FFFD) is deliberate — the byte carries no meaning here and a replacement character in the page would be a worse surprise than its absence.

Filtered byte-wise rather than with String#delete so that invalid UTF-8 elsewhere in the page survives verbatim instead of being folded to U+FFFD; NUL never appears inside a multi-byte sequence, so this is safe.

Source