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 returnSafe
, soht.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 safeht.m(f"<div>{ 'a' if some_conditional else 'b' }</div>") -
or
expression if left operands are safe (orNone
) and right operand is safeht.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. Useht.document
function to wrap the top-level template. Useht.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,tableht.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:
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)