NAME

HTML::Blitz::Builder - create HTML code dynamically and safely

SYNOPSIS

use HTML::Blitz::Builder qw(
    mk_doctype
    mk_comment
    mk_elem
    to_html
    fuse_fragment
);

DESCRIPTION

This module is useful for creating snippets of HTML code (or entire documents) programmatically. It takes care of automatically escaping any strings you pass to it, which prevents the injection of HTML and script code (XSS).

To use it, call the mk_elem, mk_comment, and mk_doctype constructor functions as needed to create the document structure you want. At the very end, pass everything to to_html to obtain the corresponding HTML code.

The basic data structure used by this module is the document fragment, which in Perl is represented as a list of (zero or more) node values. A node value is one of the following:

  • A text node, represented by a plain string.

  • An element node, represented by an object returned from mk_elem.

  • A comment node, represented by an object returned from mk_comment.

  • A DOCTYPE declaration, represented by an object returned from mk_doctype.

See below for the list of public functions provided by this module, which are exportable on request.

FUNCTIONS

to_html(@nodes)

Takes a list of node values (as described above) and returns the corresponding HTML code.

The list of elements can be empty: to_html() (which is the same as to_html(())) simply returns the empty string. More generally, to_html is a homomorphism from lists to strings in that it preserves concatenation:

to_html(@A, @B) eq (to_html(@A) . to_html(@B))

mk_elem($name, [\%attributes, ] @nodes)

Creates an element with the specified name, attributes, and child nodes.

The only required argument is the element name. The optional second argument is a reference to a hash of attribute name/value pairs; omitting it is equivalent to passing {} (an empty hashref), meaning no attributes. All remaining arguments are taken to be child nodes.

For example:

to_html( mk_elem("div") )
# => '<div></div>'

to_html( mk_elem("div", "I <3 U") )
# => '<div>I &lt;3 U</div>'

mk_elem is aware of "void" (i.e. content-free) elements and will not generate closing tags for them:

to_html( mk_elem("br") )
# => '<br>'

If you attempt to create a void element with child nodes, an exception will be thrown:

to_html( mk_elem("br", "Hello!") )
# error!

The same is true for elements that cannot contain nested child elements (title, textarea, style, script):

to_html( mk_elem("title", "Hello!") )
# => '<title>Hello!</title>'

to_html( mk_elem("title", mk_elem("span", "Hello!")) )
# error!

Because the contents of style and script elements do not follow the usual HTML parsing rules, not all values can be represented:

to_html( mk_elem("p", "</p>") )
# => '<p>&lt;/p></p>'

to_html( mk_elem("style", "</style>") )
# error!

to_html( mk_elem("script", "</script>") )
# error!

All attributes and text nodes are properly HTML escaped:

to_html( mk_elem("a", { href => "/q?a=1&b=2&c=3" }, "things, stuff, &c.") )
# => '<a href="/q?a=1&amp;b=2&amp;c=3">things, stuff, &amp;c.</a>'

The result of to_html is deterministic; in particular, attributes are always generated in the same order (despite being passed in the form of a hash).

mk_comment($comment)

Creates a comment with the specified contents.

to_html( mk_comment("a b c") )
# => '<!--a b c-->'

Certain strings (most notably -->) cannot appear in an HTML comment, so the following will throw an exception:

to_html( mk_comment("A --> B") )
# error!

mk_doctype()

Creates a DOCTYPE declaration. It takes no arguments and always produces an html doctype:

to_html( mk_doctype() )
# => '<!DOCTYPE html>'

fuse_fragment(@nodes)

Sometimes you want to store a document fragment (a list of nodes) in a single Perl value without wrapping an extra element around it. This is what fuse_fragment does: It takes a list of node values and returns a single object representing that list.

fuse_fragment is idempotent:

fuse_fragment(fuse_fragment(@nodes))
# is equivalent to
fuse_fragment(@nodes)

Another way of looking at it is that as far as fragment consumers (i.e. to_html, mk_elem, and fuse_fragment itself) are concerned, fuse_fragment is transparent:

to_html( fuse_fragment(@values) )
# is the same as
to_html( @values )

mk_elem($name, $attrs, fuse_fragment(@values))
# is the same as
mk_elem($name, $attrs, @values)

EXAMPLE

my @fragment =
    mk_elem('form', { method => 'POST', action => '/login' },
        mk_elem('label',
            'Username: ',
            mk_elem('input', { name => 'user' }),
        ),
        mk_elem('label',
            'Password: ',
            mk_elem('input', { name => 'password', type => 'password' }),
        ),
    );

my @document = (
    mk_doctype,
    mk_elem('title', 'Log in'),
    mk_comment('hello, world'),
    @fragment,
);

my $html = to_html @document;
# Equivalent to:
# $html =
#     '<!DOCTYPE html>'
#     . '<title>Log in</title>'
#     . '<!--hello, world-->'
#     . '<form method="POST" action="/login">'
#     .    '<label>Username: <input name="user"></label>'
#     .    '<label>Password: <input name="password" type="password"></label>'
#     . '</form>';

AUTHOR

Lukas Mai, <lmai at web.de>

COPYRIGHT & LICENSE

Copyright 2022 Lukas Mai.

This module is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

SEE ALSO

HTML::Blitz