Skip to content

Linting

The linting is implemented as Pylint plugin, making it's easy to integrate HTML checks into the workflow.

Installation

pip install pylint-htmf

Simple usage

htmf-check <path>

This command just invokes Pylint with pylint-htmf plugin to run HTML checks over the files in path, while disabling all other rules:

pylint --load-plugin=pylint_htmf --disable=all --enable=htmf-checker <path>

More info on running checks and CI integration is in the Pylint documentation

Annotating the code

There is no way to hook into Python f-strings interpolation in runtime and make f-expressions safe. So htmf uses the typehints and linting instead.

Linter tries to prove that the f-expressions are either instances of the Safe class or simple HTML-safe literals.


The expressions are recognized as safe if they are:

  • literal strings containing no HTML-special characters

    ht.m(f"<p>{ 'known to be safe' }{ '<this will fail>' }</p>")

  • function calls returning Safe. htmf's own utilities return Safe, so ht.m, ht.t, etc are ok.

    def MyComponent() -> Safe: ...
    ...
    ht.m(f"<div>{ MyComponent() }</div>")

    Note

    The class methods are currently not recognized, only the simple functions

  • variables (including arguments) type-annotated as Safe. Linter will check the f-expression, while your typechecker will help you to supply the safe arguments.

    def Widget(header: Safe, body: Safe) -> Safe:
        return ht.m(f"<div>{ header } { body }</div>")

  • if/else ternary if both branches are safe

    ht.m(f"<div>{ 'a' if some_conditional else 'b' }</div>")

  • or expression if left operands are safe (or None) and right operand is safe

    ht.m(f"<div>{ safe_or_none or another_safe_or_none or '?' }</div>")

  • simple gettext calls. It's assumed that if original strings are ok, the translated strings would be ok too. This rule was added to reduce noise in templates. Enabled by default.

    ht.m(f"<div>{ _('This is assumed safe') } { _('<... but this is not>') }</div>"

  • whitelisted functions known to return HTML-safe results. Empty by default.

Note

The int and float are considered safe too. Linter will accept them everywhere the Safe is accepted.

Verifying the markup

The second task of the linter is checking if the markup is valid HTML5. It does it by replacing all f-expressions with whitespaces and parsing result with the html5lib in strict mode.

There are a few things to be aware of:

  • The html5lib makes a distinction between HTML document and fragment. Well-formed document should contain the <!DOCTYPE html><html>...</html> tags. Use ht.document function to wrap the top-level template. Use ht.m for the partials/components.

  • html5lib will complain for some standalone fragments invalid outside of the parent tags. Notable example is the <tr> tags allowed only inside <table>. Use the magic comment above the markup function to define the scopes:

    # htmf-scopes=html,table
    ht.m("<tr> ... </tr>") # verifies in context of <html><table> ... </table></html>

Advanced typing

There are the cases when you want to additionally limit the acceptable arguments types. For example, you may want the Table to accept only Safe Thead, not just any Safe's markup. You may use the Python's NewType and htmf's SafeOf annotation to achieve it:

Thead = NewType("Thead", Safe)

def TableHead() -> Thead:
    return Thead(ht.m("<thead> ... </thead>"))

def Table(head: SafeOf[Thead], items: ...):
    return ht.m(f"<table>{ head } ... </table>")

Now the linter knows the head argument is ok since it's annotated as SafeOf[something], while typechecker allows passing only the Thead-compatible argument.

Options

Dump of pylint --load-plugin=pylint_htmf --help follows:

...
--htmf-markup-func <regexp>
                    Function wrapping the HTML fragment (default: htmf\.m|htmf\.markup|ht\.m|ht\.markup)
--htmf-document-func <regexp>
                    Function wrapping the HTML document (default: htmf\.document|ht\.document)
--htmf-safetype <regexp>
                    Type annotating the function return type, variable or argument as HTML-safe (default:
                    htmf\.SafeOf|ht\.SafeOf|SafeOf|htmf\.Safe|ht\.Safe|Safe|int|float)
--htmf-safe-func <regexp>
                    Whitelist of safe functions (default: None)
--htmf-allow-simple-gettext <y or n>
                    Treat simple non-interpolated gettext calls as safe (default: True)
--htmf-allow-flat-markup <y or n>
                    Allow markup of multiple elements without the parent (default: False)