{"id":15018457,"url":"https://github.com/mauke/html-blitz","last_synced_at":"2026-03-14T18:15:12.343Z","repository":{"id":65426023,"uuid":"592087084","full_name":"mauke/HTML-Blitz","owner":"mauke","description":"HTML::Blitz - high-performance, selector-based, content-aware HTML template engine","archived":false,"fork":false,"pushed_at":"2024-05-30T18:36:11.000Z","size":220,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-13T11:43:08.980Z","etag":null,"topics":["html","html-template-engine","html5","perl","template-engine"],"latest_commit_sha":null,"homepage":"","language":"Perl","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mauke.png","metadata":{"files":{"readme":".github/README.md","changelog":"Changes","contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-22T21:42:16.000Z","updated_at":"2024-05-30T18:36:14.000Z","dependencies_parsed_at":"2024-09-28T19:00:47.326Z","dependency_job_id":"b4c8b501-dbac-4e0a-8bb0-bd50de0de710","html_url":"https://github.com/mauke/HTML-Blitz","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauke%2FHTML-Blitz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauke%2FHTML-Blitz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauke%2FHTML-Blitz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauke%2FHTML-Blitz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mauke","download_url":"https://codeload.github.com/mauke/HTML-Blitz/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243318751,"owners_count":20272137,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["html","html-template-engine","html5","perl","template-engine"],"created_at":"2024-09-24T19:51:59.475Z","updated_at":"2025-12-24T18:10:18.149Z","avatar_url":"https://github.com/mauke.png","language":"Perl","readme":"[![Coverage Status](https://coveralls.io/repos/github/mauke/HTML-Blitz/badge.svg?branch=main)](https://coveralls.io/github/mauke/HTML-Blitz?branch=main)\n\n# NAME\n\nHTML::Blitz - high-performance, selector-based, content-aware HTML template engine\n\n# SYNOPSIS\n\n```perl\nuse HTML::Blitz ();\nmy $blitz = HTML::Blitz-\u003enew;\n\n$blitz-\u003eadd_rules(@rules);\n\nmy $template = $blitz-\u003eapply_to_file(\"template.html\");\nmy $html = $template-\u003eprocess($variables);\n\nmy $fn = $template-\u003ecompile_to_sub;\nmy $html = $fn-\u003e($variables);\n```\n\n# DESCRIPTION\n\nHTML::Blitz is a high-performance, CSS-selector-based, content-aware template\nengine for HTML5. Let's unpack that:\n\n- You want to generate web pages. Those are written in HTML5.\n- Your HTML documents are mostly static in nature, but some parts need to be\nfilled in dynamically (often with data obtained from a database query). This is\nwhere a template engine shines.\n\n    (On the other hand, if you prefer to generate your HTML completely dynamically\n    with ad-hoc code, but you still want to be safe from HTML injection and XSS\n    vulnerabilities, have a look at [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder).)\n\n- Most template systems are content agnostic: They can be used for pretty much\nany format or language as long as it is textual.\n\n    HTML::Blitz is different. It is restricted to HTML, but that also means it\n    understands more about the documents it processes, which eliminates certain\n    classes of bugs. (For example, HTML::Blitz will never produce mismatched tags\n    or forget to properly encode HTML entities.)\n\n- The format for HTML::Blitz template files is plain HTML. Instead of embedding\nspecial template directives in the source document (like with most other\ntemplate systems), you write a separate piece of Perl code that instructs\nHTML::Blitz to fill in or repeat elements of the source document. Those\nelements are targeted with CSS selectors.\n- Having written the HTML document template and the corresponding processing\nrules (consisting of CSS selectors and actions to be applied to matching\nelements), you then compile them together into an [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate)\nobject. This object provides functions that take a set of input values, insert\nthem into the document template, and return the finished HTML page.\n\n    This latter step is quite fast. See [\"PERFORMANCE\"](#performance) for details.\n\n## General flow\n\nIn a typical web application, HTML::Blitz is intended to be used in the\nfollowing way (\"compile on startup\"):\n\n1. When the application starts up, do the following steps:\n\n    For each template, create an HTML::Blitz object by calling [\"new\"](#new).\n\n2. Tell the object what rules to apply, either by passing them to [\"new\"](#new), or by\ncalling [\"add\\_rules\"](#add_rules) afterwards (or both). This doesn't do much yet; it just\naccumulates rules inside the object.\n3. Apply the rules to the source document by calling [\"apply\\_to\\_file\"](#apply_to_file) (if the\nsource document is stored in a file) or [\"apply\\_to\\_html\"](#apply_to_html) (if you have the\nsource document in a string). This gives you an [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate)\nobject.\n4. Turn the [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate) object into a function by calling\n[\"compile\\_to\\_sub\" in HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate#compile_to_sub). Stash the function away somewhere.\n\n    (The previous steps are meant to be performed once, when the application starts\n    up and initializes.)\n\n5. When a request comes in, retrieve the corresponding template function from\nwhere you stashed it in step 4, then call it with the set of variables you want\nto use to populate the template document. The result is the finished HTML page.\n\nAlternatively, if your application is not persistent (e.g. because it exits\nafter processing each request, like a CGI script) or if you just don't want to\nspend time recompiling each template on startup, you can use a different model\n(\"precompiled\") as follows:\n\n1. In a separate script, run steps 1 to 3 from the list above in advance.\n2. Serialize each template to a string by calling\n[\"compile\\_to\\_string\" in HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate#compile_to_string) and store it where you can load it\nback later, e.g. in a database or on disk. In the latter case, you can simply\ncall [\"compile\\_to\\_file\" in HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate#compile_to_file) directly.\n3. Take care to recompile your templates as needed by rerunning steps 1 and 2 each\ntime the source documents or processing rules change.\n4. In your application, load your template functions by `eval`'ing the code\nstored in step 2. In the case of files, you can simply use [\"do EXPR\" in perlfunc](https://perldoc.perl.org/perlfunc#do-EXPR).\nThe return value will be a subroutine reference.\n5. Call your template functions as described in step 5 above.\n\n## Processing model\n\nConceptually, HTML::Blitz operates in two phases: First all selectors are\ntested against the source document and their matches recorded. Then, in the\nsecond phase, all matching actions are applied.\n\nConsider the following document fragment:\n\n```html\n\u003cdiv class=\"foo\"\u003e ... \u003c/div\u003e\n```\n\nAnd these rules:\n\n```perl\n[ 'div' =\u003e ['remove_all_attributes'] ],\n[ '.foo' =\u003e ['replace_inner_text', 'Hello!'] ],\n```\n\nThe second rule matches against the `class` attribute, but the first rule\nremoves all attributes. However, it doesn't matter in what order you define\nthese rules: Both selectors are matched first, and then both actions are\napplied together. The attribute removal does not prevent the second rule from\nmatching. The result will always come out as:\n\n```html\n\u003cdiv\u003eHello!\u003c/div\u003e\n```\n\nIn cases where multiple actions apply to the same element, all actions are run,\nbut their order is unspecified. Consider the following document fragment:\n\n```html\n\u003cdiv class=\"foo\"\u003e ... \u003c/div\u003e\n```\n\nAnd these rules:\n\n```perl\n[ 'div' =\u003e ['replace_inner_text', 'A'], ['replace_inner_text', 'B'] ],\n[ '.foo' =\u003e ['replace_inner_text', 'C'] ],\n```\n\nAll three actions will run and replace the contents of the `div` element, but\nsince their order is unspecified, you may end up with any of the following\nthree results (depending on which action runs last):\n\n```html\n\u003cdiv class=\"foo\"\u003eA\u003c/div\u003e\n```\n\nor\n\n```html\n\u003cdiv class=\"foo\"\u003eB\u003c/div\u003e\n```\n\nor\n\n```html\n\u003cdiv class=\"foo\"\u003eC\u003c/div\u003e\n```\n\n\u003e **Implementation details** (results not guaranteed, your mileage may vary, void\n\u003e where prohibited, not financial advice): The current implementation tries to\n\u003e maximize an internal metric called \"unhelpfulness\". Consider the following\n\u003e document fragment:\n\u003e\n\u003e ```html\n\u003e \u003cimg class=\"profile\" src=\"dummy.jpg\"\u003e\n\u003e ```\n\u003e\n\u003e And these actions:\n\u003e\n\u003e ```perl\n\u003e ['remove_all_attributes'],                                          #1\n\u003e ['set_attribute_text', src =\u003e 'kitten.jpg'],                        #2\n\u003e ['set_attribute_text', alt =\u003e \"Photo of a sleeping kitten\"],        #3\n\u003e ['transform_attribute_sub', src =\u003e sub { \"/media/images/$_[0]\" }],  #4\n\u003e ```\n\u003e\n\u003e Clearly the most sensible way to arrange these actions is from #1 to #4; first\n\u003e removing all existing attributes, giving `\u003cimg\u003e`, then gradually setting\n\u003e new attributes, giving `\u003cimg src=\"kitten.jpg\" alt=\"Photo of a sleeping kitten\"\u003e`,\n\u003e and finally transforming them. This would result in:\n\u003e\n\u003e ```html\n\u003e \u003cimg src=\"/media/images/kitten.jpg\" alt=\"Photo of a sleeping kitten\"\u003e\n\u003e ```\n\u003e\n\u003e However, that's too helpful.\n\u003e\n\u003e In order to maximize unhelpfulness, you would apply these actions from #4 back\n\u003e to #1; first transforming the `src` attribute, giving\n\u003e `\u003cimg class=\"profile\" src=\"/media/images/dummy.jpg\"\u003e`, then\n\u003e adding/overwriting other attributes, giving\n\u003e `\u003cimg class=\"profile\" src=\"kitten.jpg\" alt=\"Photo of a sleeping kitten\"\u003e`,\n\u003e and finally removing all attributes. This would result in:\n\u003e\n\u003e ```html\n\u003e \u003cimg\u003e\n\u003e ```\n\u003e\n\u003e And that's what HTML::Blitz actually does.\n\n# METHODS\n\n## new\n\n```perl\nmy $blitz = HTML::Blitz-\u003enew;\nmy $blitz = HTML::Blitz-\u003enew(\\%options);\nmy $blitz = HTML::Blitz-\u003enew(@rules);\nmy $blitz = HTML::Blitz-\u003enew(\\%options, @rules);\n```\n\nCreates a new `HTML::Blitz` object.\n\nYou can optionally specify initial options by passing a hash reference as the\nfirst argument. The following keys are supported:\n\n- keep\\_doctype\n\n    Default: _true_\n\n    By default, `\u003c!DOCTYPE html\u003e` declarations in template files are retained.\n    If you set this option to a false value, they are removed instead.\n\n- keep\\_comments\\_re\n\n    Default: `qr/\\A/`\n\n    By default, HTML comments in template files are retained. This option accepts a\n    regex object (as created by [`qr//`](https://perldoc.perl.org/perlfunc#qr-STRING)), which is matched\n    against the contents of all HTML comments. Only those that match the regex are\n    retained; all others are removed.\n\n    For example, to remove all comments except for copyright notices, you could use\n    the following:\n\n    ```perl\n    HTML::Blitz-\u003enew({\n        keep_comments_re =\u003e qr/ \\(c\\) | \\b copyright \\b | \\N{COPYRIGHT SIGN} /xi,\n    })\n    ```\n\n    If you want to invert this functionality, e.g. to remove comments containing\n    `DELETEME` and keep everything else, use negative look-ahead:\n\n    ```perl\n    HTML::Blitz-\u003enew({\n        keep_comments_re =\u003e qr/\\A(?!.*DELETEME)/s,\n    })\n    ```\n\n- dummy\\_marker\\_re\n\n    Default: `qr/\\A(?!)/`\n\n    Sometimes you might have dummy content or filler text in your templates that is\n    intended to be replaced by your processing rules (like \"Lorem ipsum\" or user\n    details for \"Firstname Lastname\"). To make sure all such instances are actually\n    found and replaced by your processing rules, come up with a distinctive piece\n    of marker text (e.g. _XXX_), include it in all of your dummy content, and pass\n    a regex object (as created by [`qr//`](https://perldoc.perl.org/perlfunc#qr-STRING)) that detects\n    it. For example:\n\n    ```perl\n    HTML::Blitz-\u003enew({\n        dummy_marker_re =\u003e qr/\\bXXX\\b/,\n    })\n    ```\n\n    If any of the attribute values or plain text parts of your source template\n    match this regex, template processing will stop and an exception will be\n    thrown.\n\n    Note that this only applies to text from the template; strings that are\n    substituted in by your processing rules are not checked.\n\n    The default behavior is to not detect/reject dummy content.\n\nAll other arguments are interpreted as processing rules:\n\n```perl\nmy $blitz = HTML::Blitz-\u003enew(@rules);\n```\n\nis just a shorter way to write\n\n```perl\nmy $blitz = HTML::Blitz-\u003enew;\n$blitz-\u003eadd_rules(@rules);\n```\n\nSee [\"add\\_rules\"](#add_rules).\n\n## set\\_keep\\_doctype\n\n```perl\n$blitz-\u003eset_keep_doctype(1);\n$blitz-\u003eset_keep_doctype(0);\n```\n\nTurns the [\"keep\\_doctype\"](#keep_doctype) option on/off. See the description of [\"new\"](#new) for\ndetails.\n\n## set\\_keep\\_comments\\_re\n\n```perl\n$blitz-\u003eset_keep_comments_re( qr/copyright/i );\n```\n\nSets the [\"keep\\_comments\\_re\"](#keep_comments_re) option. See the description of [\"new\"](#new) for\ndetails.\n\n## set\\_dummy\\_marker\\_re\n\n```perl\n$blitz-\u003eset_dummy_marker_re( qr/\\bXXX\\b/ );\n```\n\nSets the [\"dummy\\_marker\\_re\"](#dummy_marker_re) option. See the description of [\"new\"](#new) for\ndetails.\n\n## add\\_rules\n\n```perl\n$blitz-\u003eadd_rules(\n    [ 'a.info, a.next' =\u003e\n        [ set_attribute_text =\u003e 'href', 'https://example.com/' ],\n        [ replace_inner_text =\u003e \"click here\" ],\n    ],\n    [ '#list-container' =\u003e\n        [ repeat_inner =\u003e 'list',\n            [ '.name'  =\u003e [ replace_inner_var =\u003e 'name' ] ],\n            [ '.group' =\u003e [ replace_inner_var =\u003e 'group' ] ],\n            [ 'hr'     =\u003e ['separator'] ],\n        ],\n    ],\n);\n```\n\nThe `add_rules` method adds processing rules to the `HTML::Blitz` object. It\naccepts any number of rules (even 0, but calling it without arguments is a\nno-op).\n\nA _rule_ is an array reference whose first element is a selector and whose\nremaining elements are processing actions. The actions will be applied to all\nHTML elements in the template document that match the selector.\n\nA _selector_ is a CSS selector group in the form of a string.\n\nAn _action_ is an array reference whose first element is a string that\nspecifies the type of the action; the remaining elements are arguments.\nDifferent types of actions take different kinds of arguments.\n\nA selector group is a comma-separated list of one or more selectors. It matches\nany element matched by any of the selectors in the list.\n\nA selector is one or more simple selector sequences separated by a combinator.\n\nThe available combinators are whitespace, `\u003e`, `~`, and `+`. For all\nsimple selector sequences _S1_, _S2_:\n\n- The descendant combinator `S1 S2` matches any element _S2_ that has an\nancestor matching _S1_.\n- The child combinator `S1 \u003e S2` matches any element _S2_ that has an\nimmediate parent element matching _S1_.\n- The sibling combinator `S1 ~ S2` matches any element _S2_ that has a\npreceding sibling element matching _S1_.\n- The adjacent sibling combinator `S1 + S2` matches any element _S2_ that has\nan immediately preceding sibling element matching _S1_.\n\n**Limitation:** In the current implementation, the number of adjacent\nnon-descendant combinators is limited by the number of bits that perl uses for\nintegers. That is, you cannot use more than 32 (if your perl uses 32-bit\nintegers) or 64 (if your perl uses 64-bit integers) simple selector sequences\nin a row if they are all joined using `\u003e`, `~`, or `+`. (No limit is\nplaced on the number of simple selectors in a sequence, nor on simple selector\nsequences joined using the descendant combinator (whitespace).)\n\nA simple selector sequence is a sequence of one or more simple selectors\nseparated by nothing (not even whitespace). If a universal or type selector is\npresent, it must come first in the sequence. A sequence matches any element\nthat is matched by all of the simple selectors in the sequence.\n\nA simple selector is one of the following:\n\n- universal selector\n\n    The universal selector `*` matches all elements. It is generally redundant and\n    can be omitted unless it is the only component of a selector sequence.\n    (Selector sequences cannot be empty.)\n\n- type selector\n\n    A type selector consists of a name. It matches all elements of that name. For\n    example, a selector of `form` matches all form elements, `p` matches all\n    paragraph elements, etc.\n\n- attribute presence selector\n\n    A selector of the form `[FOO]` (where `FOO` is a CSS identifier) matches all\n    elements that have a `FOO` attribute.\n\n- attribute value selector\n\n    A selector of the form `[FOO=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value is exactly `BAR`.\n\n- attribute prefix selector\n\n    A selector of the form `[FOO^=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value starts with `BAR`. However, if `BAR` is the empty\n    string (i.e. the selector looks like `[FOO^=\"\"]` or `FOO^='']`), then it\n    matches nothing.\n\n- attribute suffix selector\n\n    A selector of the form `[FOO$=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value ends with `BAR`. However, if `BAR` is the empty\n    string (i.e. the selector looks like `[FOO$=\"\"]` or `FOO$='']`), then it\n    matches nothing.\n\n- attribute infix selector\n\n    A selector of the form `[FOO*=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value contains `BAR` as a substring. However, if `BAR` is the\n    empty string (i.e. the selector looks like `[FOO*=\"\"]` or `FOO*='']`), then\n    it matches nothing.\n\n- attribute word selector\n\n    A selector of the form `[FOO~=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value is a list of whitespace-separated words, one of which is\n    exactly `BAR`.\n\n- attribute language prefix selector\n\n    A selector of the form `[FOO|=BAR]` (where `BAR` is a CSS identifier or a CSS\n    string in single or double quotes) matches all elements that have a `FOO`\n    attribute whose value is either exactly `BAR` or starts with `BAR` followed\n    by a `-` (minus) character. For example, `[lang|=en]` would match an\n    attribute of the form `lang=\"en\"`, but also `lang=\"en-us\"`, `lang=\"en-uk\"`,\n    `lang=\"en-fr\"`, etc.\n\n- class selector\n\n    A selector of the form `.FOO` (where `FOO` is a CSS identifier) matches all\n    elements whose `class` attribute contains a list of whitespace-separated\n    words, one of which is exactly `FOO`. It is equivalent to `[class~=FOO]`.\n\n- identity selector\n\n    A selector of the form `#FOO` (where `FOO` is a CSS name) matches all\n    elements whose `id` attribute is exactly `FOO`. It is equivalent to\n    `[id=FOO]`.\n\n- _n_th child selector\n\n    A selector of the form `:nth-child(An+B)` or `:nth-child(An-B)` (where `A`\n    and `B` are integers) matches all elements that are the _An+B_th (or\n    _An-B_th, respectively) child of their parent element, for any non-negative\n    integer _n_. For the purposes of this selector, counting starts at 1.\n\n    The full syntax is a bit more complicated: `A` can be negative; if `A` is 1,\n    it can be omitted (i.e. `1n` can be shortened to just `n`); if `A` is 0, the\n    whole `An` part can be omitted; if `B` is 0, the `+B` (or `-B`) part can be\n    omitted unless the `An` part is also gone; `n` can also be written `N`.\n\n    In short, all of these are valid arguments to `:nth-child`:\n\n    ```css\n    3n+1\n    3n-2\n    -4n+7\n    2n\n    9\n    n-2\n    1n-0\n    ```\n\n    In addition, the special keywords `odd` and `even` are also accepted.\n    `:nth-child(odd)` is equivalent to `:nth-child(2n+1)` and `:nth-child(even)`\n    is equivalent to `:nth-child(2n)`.\n\n- _n_th child of type selector\n\n    A selector of the form `:nth-of-type(An+B)` or `:nth-of-type(An-B)` (where\n    `A` and `B` are integers) matches all elements that are the _An+B_th (or\n    _An-B_th, respectively) child of their parent element, only counting elements\n    of the same type, for any non-negative integer _n_. Counting starts at 1.\n\n    It accepts the same argument syntax as the [\"_n_th child selector\"](#nth-child-selector), which\n    see for details.\n\n    For example, `span:nth-of-type(3)` matches every `span` element whose list of\n    preceding sibling contains exactly two elements of type `span`.\n\n- first child selector\n\n    A selector of the form `:first-child` matches all elements that have no\n    preceding sibling elements. It is equivalent to `:nth-child(1)`.\n\n- first child of type selector\n\n    A selector of the form `:first-of-type` matches all elements that have no\n    preceding sibling elements of the same type. It is equivalent to\n    `:nth-of-type(1)`.\n\n- negated selector\n\n    A selector of the form `:not(FOO)` (where `FOO` is any simple selector\n    excluding the negated selector itself) matches all elements that are not\n    matched by `FOO`.\n\n    For example, `img:not([alt])` matches all `img` elements without an `alt`\n    attribute, and `:not(*)` matches nothing.\n\nOther selectors or pseudo-classes are not currently implemented.\n\nIn the following section, a _variable name_ refers to a string that starts\nwith a letter or `_` (underscore), followed by 0 or more letters, `_`,\ndigits, `.`, or `-`. Template variables identify sections that are filled in\nlater when the template is expanded (at runtime, so to speak).\n\nThe following types of actions are available:\n\n- `['remove']`\n\n    Removes the matched element. Equivalent to `['replace_outer_text', '']`.\n\n- `['remove_inner']`\n\n    Removes the contents of the matched element, leaving it empty. Equivalent to `['replace_inner_text', '']`.\n\n- `['remove_if', VAR]`\n\n    Removes the matched element if _VAR_ (a runtime variable) contains a true value.\n\n- `['replace_inner_text', STR]`\n\n    Replaces the contents of the matched element by the fixed string _STR_.\n\n- `['replace_inner_var', VAR]`\n\n    Replaces the contents of the matched element by the value of the runtime\n    variable _VAR_, which is interpreted as plain text (and properly HTML\n    escaped).\n\n- `['replace_inner_template', TEMPLATE]`\n\n    Replaces the contents of the matched element by _TEMPLATE_, which must be an\n    instance of [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate). This action lets you include a\n    sub-template as part of an outer template; all variables of the inner template\n    become variables of the outer template.\n\n- `['replace_inner_dyn_builder', VAR]`\n\n    Replaces the contents of the matched element by the value of the runtime\n    variable _VAR_, which must be an instance of [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder). This is\n    the only way to incorporate dynamic HTML in a template (without interpreting\n    the HTML code as text and escaping everything).\n\n- `['replace_outer_text', STR]`\n\n    Replaces the matched element (and all of its contents) by the fixed string _STR_.\n\n- `['replace_outer_var', VAR]`\n\n    Replaces the matched element (and all of its contents) by the value of the\n    runtime variable _VAR_, which is interpreted as plain text (and properly HTML\n    escaped).\n\n- `['replace_outer_template', TEMPLATE]`\n\n    Replaces the matched element by _TEMPLATE_, which must be an instance of\n    [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate). This action lets you include a sub-template as part\n    of an outer template; all variables of the inner template become variable of\n    the outer template.\n\n- `['replace_outer_dyn_builder', VAR]`\n\n    Replaces the matched element by the value of the runtime variable _VAR_, which\n    must be an instance of [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder). This is the only way to\n    incorporate dynamic HTML in a template (without interpreting the HTML code as\n    text and escaping everything).\n\n- `['transform_inner_sub', SUB]`\n\n    Collects the text contents of the matched element and all of its descendants in\n    a string and passes it to _SUB_, which must be a code reference (or an object\n    with an overloaded `\u0026{}` operator). The returned string replaces the previous\n    contents of the matched element.\n\n    It is analogous to `elem.textContent = SUB(elem.textContent)` in JavaScript.\n\n- `['transform_inner_var', VAR]`\n\n    Collects the text contents of the matched element and all of its descendants in\n    a string and passes it to the runtime variable _VAR_, which must be a code\n    reference (or an object with an overloaded `\u0026{}` operator). The returned\n    string replaces the previous contents of the matched element.\n\n- `['transform_outer_sub', SUB]`\n\n    Collects the text contents of the matched element and all of its descendants in\n    a string and passes it to _SUB_, which must be a code reference (or an object\n    with an overloaded `\u0026{}` operator). The returned string replaces the entire\n    matched element. (Thus, if _SUB_ returns an empty string, it effectively\n    removes the matched element from the document.)\n\n- `['transform_outer_var', VAR]`\n\n    Collects the text contents of the matched element and all of its descendants in\n    a string and passes it to the runtime variable _VAR_, which must be a code\n    reference (or an object with an overloaded `\u0026{}` operator). The returned\n    string replaces the entire matched element.\n\n- `['remove_attribute', ATTR_NAMES]`\n\n    Removes all attributes from the matched element whose names are listed in\n    _ATTR\\_NAMES_, which must be a list of strings.\n\n- `['remove_all_attributes']`\n\n    Removes all attributes from the matched elements.\n\n- `['replace_all_attributes', ATTR_HASHREF]`\n\n    Removes all attributes from the matched elements and creates new attributes\n    based on _ATTR\\_HASHREF_, which must be a reference to a hash. Its keys are\n    attribute names; its values are array references with two elements: The first\n    is either the string `text`, in which case the second element is the attribute\n    value as a string, or the string `var`, in which case the second element is a\n    variable name and the attribute value is substituted in at runtime.\n\n    For example:\n\n    ```perl\n    ['replace_all_attributes', {\n        class =\u003e [text =\u003e 'button cta-1'],\n        title =\u003e [var =\u003e 'btn_title'],\n    }]\n    ```\n\n    This specifies that the matched element should only have two attributes:\n    `class`, with a value of `button cta-1`, and `title`, whose final value will\n    come from the runtime variable `btn_title`.\n\n- `['set_attribute_text', ATTR, STR]`\n\n    Creates an attribute named _ATTR_ with a value of _STR_ (a string) in the\n    matched element. If an attribute of that name already exists, it is replaced.\n\n    For example:\n\n    ```perl\n    ['set_attribute_text', href =\u003e 'https://example.com/']\n    ```\n\n- `['set_attribute_text', HASHREF]`\n\n    If you want to set multiple attributes at once, you can this form. The keys of\n    _HASHREF_ specify the attribute names, and the values specify the attribute\n    values.\n\n    For example:\n\n    ```perl\n    ['set_attribute_text', { src =\u003e $src, alt =\u003e $alt, title =\u003e $title }]\n\n    # is equivalent to:\n    ['set_attribute_text', src =\u003e $src],\n    ['set_attribute_text', alt =\u003e $alt],\n    ['set_attribute_text', title =\u003e $title],\n    ```\n\n- `['set_attribute_var', ATTR, VAR]`\n\n    Creates an attribute named _ATTR_ whose value comes from _VAR_ (a runtime\n    variable) in the matched element. If an attribute of that name already exists,\n    it is replaced.\n\n    For example:\n\n    ```perl\n    ['set_attribute_var', href =\u003e 'target_url']\n    ```\n\n- `['set_attribute_var', HASHREF]`\n\n    If you want to set multiple attributes at once, you can this form. The keys of\n    _HASHREF_ specify the attribute names, and the values specify the names of\n    runtime variables from which the attribute values will be taken.\n\n    For example:\n\n    ```perl\n    ['set_attribute_var', { src =\u003e 'img_src', alt =\u003e 'img_alt', title =\u003e 'img_title' }]\n\n    # is equivalent to:\n    ['set_attribute_var', src =\u003e 'img_src'],\n    ['set_attribute_var', alt =\u003e 'img_alt'],\n    ['set_attribute_var', title =\u003e 'img_title'],\n    ```\n\n- `['set_attributes', ATTR_HASHREF]`\n\n    Works exactly like [\"`['replace_all_attributes', ATTR_HASHREF]`\"](#replace_all_attributes-attr_hashref), but\n    without removing any existing attributes from the matched element.\n\n    For example:\n\n    ```perl\n    ['set_attributes', {\n        class =\u003e [text =\u003e 'button cta-1'],\n        title =\u003e [var =\u003e 'btn_title'],\n    }]\n    ```\n\n    This specifies that the matched element should have two attributes: `class`,\n    with a value of `button cta-1`, and `title`, whose final value will come from\n    the runtime variable `btn_title`. All other attributes remain unchanged.\n\n- `['transform_attribute_sub', ATTR, SUB]`\n\n    Calls _SUB_, which must be a code reference (or an object with an overloaded\n    `\u0026{}` operator), with the value of the attribute named _ATTR_ in the matched\n    element. If there is no such attribute, `undef` is passed instead.\n\n    The return value, normally a string, is used as the new value for _ATTR_.\n    However, if _SUB_ returns `undef` instead, the attribute is removed entirely.\n\n- `['transform_attribute_var', ATTR, VAR]`\n\n    Calls the runtime variable _VAR_, whose value must be a code reference (or an\n    object with an overloaded `\u0026{}` operator), with the value of the attribute\n    named _ATTR_ in the matched element. If there is no such attribute, `undef`\n    is passed instead.\n\n    The return value, normally a string, is used as the new value for _ATTR_.\n    However, if _VAR_ returns `undef` instead, the attribute is removed\n    entirely.\n\n- `['add_attribute_word', ATTR, WORDS]`\n\n    Takes the attribute named _ATTR_ from the matched element and treats it as a\n    list of whitespace-separated words. Any words from _WORDS_ (a list of strings)\n    that are not already present in _ATTR_ will be added to it. If the matched\n    element has no _ATTR_ attribute, it is treated as an empty list (thus all\n    _WORDS_ are added).\n\n    As a side effect, duplicate words in the original attribute value may be\n    removed.\n\n- `['remove_attribute_word', ATTR, WORDS]`\n\n    Takes the attribute named _ATTR_ from the matched element and treats it as a\n    list of whitespace-separated words. Any words from _WORDS_ (a list of strings)\n    that are present in _ATTR_ will be removed from it. If the resulting value of\n    _ATTR_ is empty, the attribute is removed entirely. If the matched element has\n    no _ATTR_ attribute to begin with, nothing changes.\n\n    As a side effect, duplicate words in the original attribute value may be\n    removed.\n\n- `['add_class', WORDS]`\n\n    Adds the words in _WORDS_ (a list of strings) to the `class` attribute of the\n    matched element (unless they are already present there). Equivalent to\n    `['add_attribute_word', 'class', WORDS]`.\n\n- `['remove_class', WORDS]`\n\n    Removes the words in _WORDS_ (a list of strings) from the `class` attribute\n    of the matched element (if they exist there). Equivalent to\n    `['remove_attribute_word', 'class', WORDS]`.\n\n- `['repeat_outer', VAR, ACTIONS?, RULES]`\n\n    Clones the matched element (along with its descendants), once for each element\n    of the runtime variable _VAR_, which must contain an array of variable\n    environments. Each copy of the matched element has _RULES_ (a list of\n    processing rules) applied to it, with variables looked up in the corresponding\n    environment taken from _VAR_.\n\n    For example:\n\n    ```perl\n    ['repeat_outer', 'things',\n        ['.name', ['replace_inner_var', 'name']],\n        ['.phone', ['replace_inner_var', 'phone']],\n    ]\n    ```\n\n    This specifies that the matched element should be repeated once for each\n    element of the `things` variable. In each copy, elements with a class of\n    `name` should have their contents replaced by the value of the `name`\n    variable in the current environment (i.e. the current element of `things`),\n    and elements with a class of `phone` should have their contents replaced by\n    the value of the `phone` variable in the current loop environment.\n\n    The optional _ACTIONS_ argument, if present, is a reference to a reference to\n    an array of actions (yes, that's a reference to a reference). It specifies what\n    to do with the matched element itself within the context of the repetition.\n\n    For example, consider the following rule:\n\n    ```perl\n    ['.foo' =\u003e\n        ['set_attribute_var', title =\u003e 'title'],\n        ['replace_inner_var', 'content'],\n        ['repeat_outer', 'things',\n            ...\n        ],\n    ]\n    ```\n\n    This says that elements with a class of `foo` should have their `title`\n    attribute set to the value of the string variable `title` and their text\n    content replaced by the value of the string variable `content`, and then be\n    repeated as directed by the array variable `things`. While this will clone the\n    element as many times as there are elements in `things`, the clones will all\n    have the same attributes and content.\n\n    On the other hand:\n\n    ```perl\n    ['.foo' =\u003e\n        ['repeat_outer', 'things',\n            \\[\n                ['set_attribute_var', title =\u003e 'title'],\n                ['replace_inner_var', 'content'],\n            ],\n            ...\n        ],\n    ]\n    ```\n\n    With this rule, the `title` attribute and contents of elements with class\n    `foo` are taken from the `title` and `content` (sub-)variables inside\n    `things`. That is, the variable references `title` and `content` are scoped\n    within the loop. This way each copy of the matched element will be different.\n\n    As a special case, if the _ACTIONS_ list only contains one action, the outer\n    array can be omitted. That is, instead of a reference to an array reference of\n    actions, you can use a reference to an action:\n\n    ```perl\n    ['repeat_outer', 'things',\n        \\[\n            ['replace_inner_var', 'content'],\n        ],\n        ...\n    ]\n\n    # can be simplified to:\n\n    ['repeat_outer', 'things',\n        \\['replace_inner_var', 'content'],\n        ...\n    ]\n    ```\n\n- `['repeat_inner', VAR, RULES]`\n\n    Clones the descendants of the matched element (but not the element itself),\n    once for each element of the runtime variable _VAR_, which must contain an\n    array of variable environments. Each copy of the descendants has _RULES_\n    (a list of processing rules) applied to it, with variables looked up in the\n    corresponding environment taken from _VAR_.\n\n    This is very similar to [\"`['repeat_outer', VAR, ACTIONS?, RULES]`\"](#repeat_outer-var-actions-rules), with\n    the following differences:\n\n    1. The matched element acts as a list container and is not repeated.\n    2. The _ACTIONS_ argument is not supported.\n    3. The _RULES_ list may contain the special [\"`['separator']`\"](#separator) action, which\n    is only allowed in the context of `repeat_inner`.\n\n- `['separator']`\n\n    This action is only available within a [\"`['repeat_inner', VAR, RULES]`\"](#repeat_inner-var-rules)\n    section. It indicates that the matched element is to be removed from the first\n    copy of the repeated elements. The results are probably not useful unless the\n    matched element is the first child of the parent whose contents are repeated.\n\n    For example, consider the following template code:\n\n    ```html\n    \u003cdiv id=\"list\"\u003e\n        \u003chr class=\"sep\"\u003e\n        \u003cp class=\"c1\"\u003eother stuff\u003c/p\u003e\n        \u003cp class=\"c2\"\u003emore stuff\u003c/p\u003e\n    \u003c/div\u003e\n    ```\n\n    ... with this set of rules:\n\n    ```perl\n    ['#list' =\u003e\n        ['repeat_inner', 'things',\n            ['.c1' =\u003e [...]],\n            ['.c2' =\u003e [...]],\n            ['.sep' =\u003e ['separator']],\n        ],\n    ]\n    ```\n\n    Since the `hr` element targeted by the `separator` action occurs at the\n    beginning of the section, it acts as a separator: It will not appear in the\n    first copy of the section, but every following copy will include it. The\n    result will look Like this:\n\n    ```html\n    \u003cdiv id=\"list\"\u003e\n        \n        \u003cp class=\"c1\"\u003e...\u003c/p\u003e\n        \u003cp class=\"c2\"\u003e...\u003c/p\u003e\n\n        \u003chr class=\"sep\"\u003e\n        \u003cp class=\"c1\"\u003e...\u003c/p\u003e\n        \u003cp class=\"c2\"\u003e...\u003c/p\u003e\n\n        \u003chr class=\"sep\"\u003e\n        \u003cp class=\"c1\"\u003e...\u003c/p\u003e\n        \u003cp class=\"c2\"\u003e...\u003c/p\u003e\n        ...\n    \u003c/div\u003e\n    ```\n\n## apply\\_to\\_html\n\n```perl\nmy $template = $blitz-\u003eapply_to_html($name, $html_code);\n```\n\nApplies the processing rules (added in [the constructor](#new) or via\n[\"add\\_rules\"](#add_rules)) to the specified source document. The first argument is a purely\ninformational string; it is used to refer to the document in error messages and\nthe like. The second argument is the HTML code of the source document. The\nreturned value is an instance of [HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate), which see.\n\nThere are some restrictions on the HTML code you can pass in. This module does\nnot implement the full HTML specification; in particular, implicit tags of any\nkind are not supported. For example, the following fragment is valid HTML:\n\n```html\n\u003cdiv\u003e\n    \u003cp\u003e A\n    \u003cp\u003e B\n    \u003cp\u003e C\n\u003c/div\u003e\n```\n\nBut HTML::Blitz requires you to write this instead:\n\n```html\n\u003cdiv\u003e\n    \u003cp\u003e A \u003c/p\u003e\n    \u003cp\u003e B \u003c/p\u003e\n    \u003cp\u003e C \u003c/p\u003e\n\u003c/div\u003e\n```\n\nThis is because implicit closing tags are not supported. In fact, HTML::Blitz\nthinks `\u003cp\u003e A \u003cp\u003e B \u003c/p\u003e C \u003c/p\u003e` is valid HTML code containing a `p` element\nnested within another `p`. (It is not; `p` elements don't nest and the second\n`\u003c/p\u003e` tag should be a syntax error. So don't do that.)\n\nSimilarly, a real HTML parser would not create `tr` elements as direct\nchildren of a `table`:\n\n```html\n\u003ctable\n    \u003ctr\u003e\u003ctd\u003eA\u003c/td\u003e\u003c/tr\u003e\n\u003c/table\u003e\n```\n\nHere an implicit `tbody` element is supposed to be inserted instead:\n\n```html\n\u003ctable\n    \u003ctbody\u003e\n        \u003ctr\u003e\u003ctd\u003eA\u003c/td\u003e\u003c/tr\u003e\n    \u003c/tbody\u003e\n\u003c/table\u003e\n```\n\nHTML::Blitz does not do that. If you have a rule with a `tbody` selector, it\nwill only apply to elements explicitly written out in the source document.\n\nIn other matters, HTML::Blitz tries to follow HTML parsing rules closely. For\nexample, it knows about _void elements_ (i.e. elements that have no content),\nlike `br`, `img`, or `input`. Such elements do not have closing tags:\n\n```html\n\u003c!-- this is a syntax error; you cannot \"close\" a \u003cbr\u003e tag  --\u003e\n\u003cbr\u003e\u003c/br\u003e\n```\n\nIt is not generally possible to have a self-closing opening tag:\n\n```html\n\u003c!-- syntax error: --\u003e\n\u003cdiv /\u003e\n\n\u003c!-- you need to write this instead: --\u003e\n\u003cdiv\u003e\u003c/div\u003e\n```\n\nHowever, a trailing slash in the opening tag is accepted (and ignored) in void\nelements. The following are all equivalent:\n\n```html\n\u003cbr\u003e\n\u003cbr/\u003e\n\u003cbr /\u003e\n```\n\nAnother exception applies to descendants of `math` and `svg` elements, which\nfollow slightly different rules:\n\n```html\n\u003csvg\u003e\n    \u003c!-- this is OK; it is parsed as if it were \u003ccircle\u003e\u003c/circle\u003e --\u003e\n    \u003ccircle/\u003e\n\u003c/svg\u003e\n\n\u003c!-- this is a syntax error: attempt to self-close a non-void tag outside of svg/math --\u003e\n\u003ccircle /\u003e\n```\n\nSimilarly, within `math` and `svg` elements you can use `CDATA` blocks with\nraw text inside:\n\n```html\n\u003cmath\u003e\n    \u003c!-- OK: equivalent to \"a\u0026lt;b\u0026amp;b\u0026lt;c\" --\u003e\n    \u003c![CDATA[a\u003cb\u0026b\u003cc]]\u003e\n\u003c/math\u003e\n\n\u003c!-- syntax error: CDATA outside of math/svg --\u003e\n\u003c![CDATA[...]]\u003e\n```\n\nThe (utterly bonkers) special parsing rules for `script` elements are\nfaithfully implemented:\n\n```html\n\u003c!-- OK: --\u003e\n\u003cscript\u003e /* \u003c!-- */ \u003c/script\u003e\n\n\u003c!-- OK: --\u003e\n\u003cscript\u003e /* \u003cscript\u003e \u003c!-- */ \u003c/script\u003e\n\n\u003c!-- still OK (script containing raw \"\u003c/script\u003e\" text): --\u003e\n\u003cscript\u003e /* \u003c!-- \u003cscript\u003e \u003c/script\u003e --\u003e */ \u003c/script\u003e\n\n\u003c!-- still OK: --\u003e\n\u003cscript\u003e /* \u003c!-- \u003cscript\u003e --\u003e */ \u003c/script\u003e\n```\n\nAttributes may contain whitespace around `=`:\n\n```html\n\u003cimg src = \"kitten.jpg\" alt = \"photo of a kitten\"\u003e\n```\n\nAttribute values don't need to be quoted if they don't contain whitespace or\n\"special\" characters (one of `` \u003c\u003e=\"'` ``):\n\n```html\n\u003cimg src=kitten.jpg alt=\"photo of a kitten\"\u003e\n\u003cimg src = kitten.jpg alt = photo\u0026#32;of\u0026#32;a\u0026#32;kitten\u003e\n```\n\nAttributes without values are allowed (and implicitly assigned the empty string as a value):\n\n```html\n\u003cinput disabled class\u003e\n\u003c!-- is equivalent to --\u003e\n\u003cinput disabled=\"\" class=\"\"\u003e\n```\n\n## apply\\_to\\_file\n\n```perl\nmy $template = $blitz-\u003eapply_to_file($filename);\n```\n\nA convenience wrapper around [\"apply\\_to\\_html\"](#apply_to_html). It reads the contents of\n`$filename` (which must be UTF-8 encoded) and calls `apply_to_html($filename, $contents)`.\n\n# EXAMPLES\n\n## Basic variables and lists/repetition\n\nThe following is a complete program:\n\n```perl\nuse strict;\nuse warnings;\nuse HTML::Blitz ();\n\nmy $template_html = \u003c\u003c'EOF';\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\n        \u003ctitle\u003e@@@ Hello, people!\u003c/title\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        \u003ch1 id=\"greeting\"\u003e@@@ placeholder heading\u003c/h1\u003e\n        \u003cdiv id=\"list\"\u003e\n            \u003chr class=\"between\"\u003e\n            \u003cp\u003e\n                Name: \u003cspan class=\"name\"\u003e@@@Bob\u003c/span\u003e \u003cbr\u003e\n                Age: \u003cspan class=\"age\"\u003e@@@42\u003c/span\u003e\n            \u003c/p\u003e\n        \u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\nEOF\n\nmy $blitz = HTML::Blitz-\u003enew({\n    # sanity check: die() if any template parts marked '@@@' above are not\n    # replaced by processing rules\n    dummy_marker_re =\u003e qr/\\@\\@\\@/,\n});\n\n$blitz-\u003eadd_rules(\n    [ 'html'             =\u003e ['set_attribute_text', lang =\u003e 'en'] ],\n    [ 'title, #greeting' =\u003e ['replace_inner_var', 'title'] ],\n    [ '#list' =\u003e\n        [ 'repeat_inner', 'people',\n            [ '.between' =\u003e ['separator'] ],\n            [ '.name'    =\u003e ['replace_inner_var', 'name'] ],\n            [ '.age'     =\u003e ['replace_inner_var', 'age'] ],\n        ],\n    ],\n);\n\nmy $template = $blitz-\u003eapply_to_html('(inline document)', $template_html);\nmy $template_fn = $template-\u003ecompile_to_sub;\n\nmy $data = {\n    title  =\u003e \"Hello, friends, family \u0026 other creatures of the sea!\",\n    people =\u003e [\n        { name =\u003e 'Edward', age =\u003e 17 },\n        { name =\u003e 'Marvin', age =\u003e 510_119_077_042 },\n        { name =\u003e 'Bronze', age =\u003e '\u003credacted\u003e' },\n    ],\n};\n\nmy $html = $template_fn-\u003e($data);\nprint $html;\n```\n\nIt produces the following output:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=en\u003e\n    \u003chead\u003e\n        \u003ctitle\u003eHello, friends, family \u0026amp; other creatures of the sea!\u003c/title\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        \u003ch1 id=greeting\u003eHello, friends, family \u0026amp; other creatures of the sea!\u003c/h1\u003e\n        \u003cdiv id=list\u003e\n            \n            \u003cp\u003e\n                Name: \u003cspan class=name\u003eEdward\u003c/span\u003e \u003cbr\u003e\n                Age: \u003cspan class=age\u003e17\u003c/span\u003e\n            \u003c/p\u003e\n        \n            \u003chr class=between\u003e\n            \u003cp\u003e\n                Name: \u003cspan class=name\u003eMarvin\u003c/span\u003e \u003cbr\u003e\n                Age: \u003cspan class=age\u003e510119077042\u003c/span\u003e\n            \u003c/p\u003e\n        \n            \u003chr class=between\u003e\n            \u003cp\u003e\n                Name: \u003cspan class=name\u003eBronze\u003c/span\u003e \u003cbr\u003e\n                Age: \u003cspan class=age\u003e\u0026lt;redacted\u003e\u003c/span\u003e\n            \u003c/p\u003e\n        \u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Hashing inline scripts for Content-Security-Policy (CSP)\n\nIf you want to protect against JavaScript code injection (also known as\ncross-site scripting or XSS), the first step is to properly escape all user\ninput that is presented on a web page. HTML::Blitz aims to make this easy (by\nmaking it hard to interpolate raw HTML strings into a template).\n\nA second layer of defense is available in the form of the\n[Content-Security-Policy\n(CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) HTTP response\nheader. This header gives a web site fine-grained control over which sources a\nbrowser is allowed to load resources (including script code) from. In\nparticular, for inline scripts (i.e. those embedded in HTML within\n`\u003cscript\u003e...\u003c/script\u003e` tags) their [SHA-2-based\nhashes](https://en.wikipedia.org/wiki/SHA-2) can be added to the\nCSP header. Any other scripts (such as those injected by an attacker) will not\nbe executed by the browser, thwarting any XSS attempts.\n\nHTML::Blitz can be used to automatically extract and hash any script code\nembedded in templates. That way you can automatically compute a tailored CSP\nvalue.\n\nSample HTML template code:\n\n```html\n\u003c!doctype html\u003e\n\u003chead\u003e\n    \u003cstyle\u003e\n        #big-red-button {\n            color: white;\n            background-color: red;\n            font-size: larger;\n        }\n    \u003c/style\u003e\n    \u003cscript\u003e\n        console.log(\"Hello from the browser console\");\n    \u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003ch1\u003eCSP Example\u003c/h1\u003e\n    \u003cbutton id=\"big-red-button\"\u003eClick me!\u003c/button\u003e\n    \u003cscript\u003e\n        document.getElementById('big-red-button').onclick = function () {\n            alert(\"Hello!\");\n        };\n    \u003c/script\u003e\n\u003c/body\u003e\n```\n\nAnd the corresponding Perl code:\n\n```perl\nuse strict;\nuse warnings;\nuse HTML::Blitz;\nuse Digest::SHA qw(sha256_base64);\n\n# Digest::SHA generates unpadded base64, but CSP requires padding on all\n# base64 strings. This function adds the required padding.\nsub sha256_base64_padded {\n    my ($data) = @_;\n    my $hash = sha256_base64 $data;\n    $hash . '=' x (-length($hash) % 4)\n}\n\n# Returns the script code unchanged, but (as a side effect) adds its\n# SHA-256 hash to the %seen_script_hashes variable.\nmy %seen_script_hashes;\nmy $add_hash = sub {\n    my ($script) = @_;\n    $seen_script_hashes{sha256_base64_padded $script} = 1;\n    $script\n};\n\nmy $blitz = HTML::Blitz-\u003enew(\n    # hash the contents of all \u003cscript\u003e tags without a src attribute\n    [ 'script:not([src])', [ transform_inner_sub =\u003e $add_hash ] ],\n    # ... other rules ...\n);\n\nmy $html = $blitz-\u003eapply_to_file('scripts.html')-\u003eprocess();\n\nmy @hashes = sort keys %seen_script_hashes;\nmy $csp = \"script-src \" . (@hashes ? join(' ', map \"'sha256-$_'\", @hashes) : \"'none'\");\n# Now you can set a response header of\n#   Content-Security-Policy: $csp\n# (and a response body of $html) and be sure that only scripts from the\n# original template file will execute.\n```\n\n# RATIONALE\n\n(I.e. why does this module exist?)\n\nTemplate systems like [Template::Toolkit](https://metacpan.org/pod/Template%3A%3AToolkit) are both powerful and general. In my\nopinion, that's a disadvantage: TT is both too powerful and too stupid for its\nown good. Since TT embeds its own programming language that can call arbitrary\nmethods in Perl, it is possible to write \"templates\" that send their own\ndatabase queries, iterate over resultsets, and do pretty much anything they\nwant, completely bypassing the notional \"controller\" or \"model\" in an\napplication. On the other hand, since TT knows nothing about the document\nstructure it is generating (to TT it's all just strings being concatenated),\nyou have to make sure to manually HTML escape every piece of text. Anything you\noverlook may end up being used for HTML injection and XSS exploits.\n\n[HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) offers an intriguing alternative: Templates are plain HTML\nwithout any special template directives or variables at all. Instead, these\nstatic HTML documents are manipulated through structure-aware selectors and\nmodification actions. Not only does this eliminate the disadvantages listed\nabove, it also means you can automatically validate your templates to make sure\nthey're well-formed HTML, which is basically impossible with TT.\n\nThere is only one tiny problem: [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) is slow. A template page that\nseems fine when fed with 10 or 20 variables during development can suddenly\ncrawl to a near halt when fed with an unexpectedly large dataset (with hundreds\nor thousands of entries) in production.\n\n(In fact, I once had reports of a single page in a big web app taking 50-60\nseconds to load, which is clearly unacceptable. At first I tried to optimize\nthe database queries behind it, but without much success. That's when I\nrealized that \u003e85% of the time was spent in the [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) based view, just\nslowly churning through the template, and nothing I changed in the code before\nthat would significantly improve loading times.)\n\nThis module was born from an attempt to retain the general concept behind\n[HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) (which I'm a big fan of) while reimplementing every part of the\nAPI and code with a focus on pure execution speed.\n\n# PERFORMANCE\n\nFor benchmarking purposes I set up a simple HTML template and filled it with a\nmedium-sized dataset consisting of 5 \"categories\" with 40 \"products\" each (200\nin total). Each \"product\" had a custom image, description, and other bits of\nmetadata.\n\nTo get a performance baseline, I timed a hand-written piece of Perl code\nconsisting only of string constants, variables, calls to `encode_entities`\n(from [HTML::Entities](https://metacpan.org/pod/HTML%3A%3AEntities)), concatenation, and nested loops. Everything was\nhard-coded; nothing was modularized or factored out into subroutines.\n\nAgainst this, I timed a few template systems ([HTML::Blitz](https://metacpan.org/pod/HTML%3A%3ABlitz), [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom),\n[Template::Toolkit](https://metacpan.org/pod/Template%3A%3AToolkit), [HTML::Template](https://metacpan.org/pod/HTML%3A%3ATemplate), [HTML::Template::Pro](https://metacpan.org/pod/HTML%3A%3ATemplate%3A%3APro),\n[Mojo::Template](https://metacpan.org/pod/Mojo%3A%3ATemplate), [Text::Xslate](https://metacpan.org/pod/Text%3A%3AXslate)) as well as [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder), which\nis rather the opposite of a template system.\n\nResults:\n\n- [Text::Xslate](https://metacpan.org/pod/Text%3A%3AXslate) v3.5.9\n\n    1375/s (0.0007s per iteration), 380.9%\n\n- [HTML::Blitz](https://metacpan.org/pod/HTML%3A%3ABlitz) 0.06\n\n    678/s (0.0015s per iteration), 187.8%\n\n- [HTML::Template::Pro](https://metacpan.org/pod/HTML%3A%3ATemplate%3A%3APro) 0.9524\n\n    653/s (0.0015s per iteration), 180.9%\n\n- [Mojo::Template](https://metacpan.org/pod/Mojo%3A%3ATemplate) 9.31\n\n    463/s (0.0022s per iteration), 128.3%\n\n- handwritten\n\n    361/s (0.0028s per iteration), 100.0%\n\n- [Template::Toolkit](https://metacpan.org/pod/Template%3A%3AToolkit) 3.101\n\n    38.6/s (0.0259s per iteration), 10.7%\n\n- [HTML::Template](https://metacpan.org/pod/HTML%3A%3ATemplate) 2.97\n\n    33.5/s (0.0299s per iteration), 9.3%\n\n- [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder) 0.06\n\n    32.9/s (0.0304s per iteration), 9.1%\n\n- [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) 0.009009\n\n    1.24/s (0.8065s per iteration), 0.3%\n\nConclusions:\n\n- [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) is slooooooow. Using it in anything but the most simple cases has\na noticeable impact on performance.\n- HTML::Blitz is orders of magnitude faster. It can easily outperform\n[HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) by a factor of 200 or 300. A dataset that might take HTML::Blitz\n20 milliseconds to zip through would lock up [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom) for over 5 seconds.\n- HTML::Blitz and [Mojo::Template](https://metacpan.org/pod/Mojo%3A%3ATemplate) are faster than hand-written code. This is\nprobably because the hand-written code appends each line separately to the\noutput string and escapes each template parameter by calling\n[`HTML::Entities::encode_entities`](https://metacpan.org/pod/HTML%3A%3AEntities#encode_entities-string)\n– whereas HTML::Blitz and [Mojo::Template](https://metacpan.org/pod/Mojo%3A%3ATemplate) fold all adjacent constant HTML\npieces into one big string in advance and use their own optimized HTML escape\nroutine.\n- HTML::Blitz can, depending on your workload, run faster than\n[HTML::Template::Pro](https://metacpan.org/pod/HTML%3A%3ATemplate%3A%3APro), which is written in C for speed.\n- In this comparison, the only system that beats HTML::Blitz in terms of raw\nspeed is the XS version of [Text::Xslate](https://metacpan.org/pod/Text%3A%3AXslate) (by a factor of about 2). The only\ndownsides are that it requires a C compiler and pulls in an entire object\nsystem as a dependency ([Mouse](https://metacpan.org/pod/Mouse)). (Without a C compiler it will still run, but\nthe performance of its pure Perl backend is not competitive, reaching only\nabout two thirds of the speed of [HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder).)\n\n# WHY THE NAME\n\nI'm German, and _Blitz_ is the German word for \"lightning\" or \"flash\" (or\n\"thunderbolt\"). Because the main motivation behind this module is performance,\nI wanted something that represents speed. Something _lightning-fast_, in fact\n(that's _blitzschnell_ in German). I didn't want to use the English name\n\"Flash\" because that name is already taken by the infamous \"Flash Player\"\nbrowser plugin (even if it is currently dead).\n\nThe second reason is also connected to speed: I wanted a template system that\nassembles pages as effortlessly and efficiently as copying around blocks of\nmemory, with minimal additional computation. Something roughly like a\n[bit blit](https://en.wikipedia.org/wiki/Bit_blit) (\"bit block transfer\")\noperation. HTML::Blitz \"blits\" in the sense that it efficiently transfers\nblocks of HTML.\n\nThe third reason relates to my frustration with [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom)'s performance.\nWhen I was struggling with [HTML::Zoom](https://metacpan.org/pod/HTML%3A%3AZoom), fruitlessly trying to come up with\nways to optimize or work around its code, I remembered a funny coincidence: It\njust so happens that (Professor) Zoom is the name of a supervillain in the\nsuperhero comic books published by DC Comics. He is the archenemy of the Flash\n(\"the fastest man alive\"), whose main ability is super-speed. When the Flash\ncomics were published in Germany in the 1970s and 1980s, his name was\ntranslated as _der Rote Blitz_ (\"the Red Flash\"). Thus: \"Blitz\" is the hero\nthat triumphs over \"Zoom\" through superior speed. :-)\n\n# AUTHOR\n\nLukas Mai, `\u003clmai at web.de\u003e`\n\n# COPYRIGHT \u0026 LICENSE\n\nCopyright 2022-2023 Lukas Mai.\n\nThis module is free software: you can redistribute it and/or modify it under\nthe terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)\nas published by the Free Software Foundation, either version 3 of the License,\nor (at your option) any later version.\n\n# SEE ALSO\n\n[HTML::Blitz::Template](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ATemplate),\n[HTML::Blitz::Builder](https://metacpan.org/pod/HTML%3A%3ABlitz%3A%3ABuilder)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmauke%2Fhtml-blitz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmauke%2Fhtml-blitz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmauke%2Fhtml-blitz/lists"}