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/elseternary if both branches are safeht.m(f"<div>{ 'a' if some_conditional else 'b' }</div>") -
orexpression 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
gettextcalls. 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.documentfunction to wrap the top-level template. Useht.mfor 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)