Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Jx

MiniJx

MiniJx is a template engine that precompiles Jx components into plain Python functions before rendering them. It render pages about 4x faster than regular Jx and does not requires Jx nor Jinja to run.

It can be used as a drop-in for existing Jx templates since it supports almost all of Jinja syntax except for macros, template inheritance, and, most notably, Jinja extensions. The lack of support for Jinja extensions is the reason this starts as a separated project from Jx.

The compiler it's implemented in FreePascal and can be used as a regular Python library and as a command line utility.

The wheel includes the compiler binary, so FreePascal is not needed to use it. Wheels are built for Linux (x86_64, aarch64; glibc and musl) and macOS (arm64, x86_64). It is tested on Python 3.13, 3.14 and free-threaded 3.14t: the runtime has no C extensions, and one catalog can render from many threads at once.

minijx components/ [more/folders/]
minijx --version

Import rules:

  • ui/button.jx is searched in the folders, in the order given.
  • ./x.jx and ../x.jx are relative to the importing file, and cannot leave the folder it was found in.
  • Prefixed imports (@ui/button.jx) are not supported.

Every name.jx under the folders becomes a .py next to it. Dots in the name become _, so sitemap.xml.jx becomes sitemap_xml.py. Two sources that would produce the same .py are a compile error. Each module exposes:

def render(
  *,
  <arguments from {# def #}>,
  content="",
  _globals=None,
  _fills=None,
  **attrs
) -> str

The generated module imports only the minijx package. It has no dependency on Jinja or on Jx. Components a file imports are copied into the same module as private functions, so each .py is self contained.

Catalog

minijx.Catalog has the same shape as jx.Catalog. It finds a component's generated module by name and calls its render.

from minijx import Catalog

catalog = Catalog("components/", site_name="Demo")
catalog.compile()
html = catalog.render("pages/home.jx", globals={"user": user}, items=items)
  • The catalog uses the compiler bundled with the package, or a minijx in the PATH. compiler= points it to another binary, and compiler=False disables compiling: then the modules must be built ahead, e.g. when the application is packaged.
  • It refuses a binary that generates modules of a format this runtime cannot load, such as a minijx from another version.
  • Names are paths relative to a folder, with or without .jx. Folders added with add_folder are searched in order.
  • Globals are the catalog's, then the render's, then assets (assets.render(), render_css(), render_js(), collect_css(), collect_js()), as in Jx. _get_random_id is available too.
  • With auto_reload=True (the default) a changed module is reloaded. A module older than any .jx copied into it (its SOURCES) is recompiled when compiler is given, and raises ComponentNotCompiledError if not.
  • A syntax error raises CompileError only for components that use the broken file. The rest keep rendering.
  • catalog.compile() compiles every folder at once. It uses compiler= or a minijx in the PATH, and raises CompileError listing every error.
  • Jx's add_package, asset_resolver, get_assets_folder and collect_assets are not supported, since they depend on prefixes.

The language

A component is a .jx file with an optional header and a template. As in Jx, the header is the run of {# def #}, {# import #}, {# css #} and {# js #} comments the file starts with; the same comment further down is an ordinary comment.

{# import "./header.jx" as Header #}
{# import "ui/button.jx" as Button #}
{# css "card.css" #}
{# js "card.js" #}
{# def title, items: list = [], show_footer=true #}

<div {{ attrs.render(class="card") }}>
  <Header title={{ title }} count={{ items | length }} />
  {{ content }}
  {% for item in items if item.visible %}
    <p>{{ loop.index }}: {{ item.name | upper }}</p>
  {% else %}
    <p>No items</p>
  {% endfor %}
  {% slot footer %}<Button text="OK" />{% endslot %}
</div>

Jinja syntax supported:

  • Rendering of variables {{ }}
  • if/elif/else
  • for ... if ... recursive with else and the full loop object
  • set name = value
  • do,
  • call (see below),
  • raw
  • Every Jinja builtin filter except xmlattr, pprint and urlize
  • custom filters and tests.

Not supported, by design: extends, include, macro, Jinja's call with caller, block set, namespace(), tuple unpacking in set, and Jinja extensions other than do.

{% call %}

Like {% filter %}, but the function is a variable: the body is rendered and passed as the first argument of the call.

{% call markdown %}# {{ title }}{% endcall %}          {# markdown(body) #}
{% call highlight("python", lines=true) %}...{% endcall %} {# highlight(body, "python", lines=True) #}
{% call obj.render %}...{% endcall %}                  {# obj.render(body) #}

The callable can be an argument, a set variable or a global. This is not Jinja's call, which calls a macro with a caller.

Custom filters and tests

catalog = Catalog(
    "components/",
    filters={"markdown": markdown, "upper": my_upper},
    tests={"admin": lambda user: user.is_admin},
)

As in Jx, a filter receives the filtered value first, and a custom filter can replace a builtin one. map, select, reject, selectattr and rejectattr see the custom filters and tests too. The tests minijx compiles into plain Python (defined, undefined, none, in, callable, sameas and the comparisons like eq or gt) cannot be replaced; the catalog raises ValueError if you try.

The generated code looks filters and tests up by name at render time, so an unknown filter is a KeyError when it runs, not a compile error.

{# def #} defaults

As in Jx, a default value is a Python expression, not a Jinja one: 1 | 2 is 3. It can only use literals, true, false, len, max, min, pow, sum, and the names it binds itself (comprehension variables, lambda parameters). Anything else is a compile error.

Whitespace

The output is the same as Jx's, byte for byte:

  • Whitespace control works as in Jinja with its default settings: {%-, {{- and {#- trim the text before the tag, -%}, -}} and -#} the text after it. + does nothing. Newlines are normalised to \n, and one newline at the very end of a file is dropped.
  • A component's output never starts with whitespace.
  • The content between a component's tags is trimmed at both ends.
  • The markers inside {% slot %} and {% fill %} trim their bodies. The ones outside a fill do nothing, since the fill is moved out of the content, and the text around it becomes one piece.

Semantics that differ from Jinja

  • Strict names. A name that is not an argument, a set variable, a loop variable, content, attrs, loop or a Python builtin compiles to _globals["name"]. A missing global raises KeyError. x is defined and x | default(...) are the exceptions: they compile to a safe lookup.
  • Attribute access a.b tries getattr then getitem, like Jinja. a["b"] tries the reverse. A miss raises AttributeError.
  • No autoescape. Only the escape (e) filter escapes. safe is the identity.
  • Python scoping. A set inside a for is visible after the loop.
  • Arguments are keyword-only. Type annotations are copied into the signature and not validated.
  • render returns str
  • The CSS and JS module constants list the assets of the component and of everything it imports, in Jx's order: the component's own first, then each import's.
  • MINIJX_FORMAT marks the module layout; the catalog treats a module from another minijx version as stale.

Example app

make example      # http://127.0.0.1:8000, PORT=... to change it

A small site in example/ built only with the standard library. It shows layouts with slots, attrs, loops with loop, recursive loops, filters, globals, a sitemap.xml.jx, and a component broken on purpose at /broken. The app compiles everything at startup with catalog.compile(). Edit any .jx in example/components/ and reload the page: the catalog recompiles it. Render times go to the console and to the Server-Timing header.

Benchmark

make bench        # or: PYTHONPATH=src ../jx/.venv/bin/python bench/bench_example.py [--reload]

Renders every page of the example app with minijx and with Jx, from the same .jx files and data. Before timing, it checks that minijx and Jx with autoescape off produce the same HTML. It reports the median warm render time per page, the first render with a new catalog, and the time to compile every component with the binary.

Layout

compiler/       FreePascal sources: mjlexer, mjparser, mjexpr (expression tree), mjdefs,
                mjgen (Python functions), mjcompiler (modules), minijx.lpr
src/minijx/     Python package: catalog.py, filters.py, tests.py, attrs.py, loop.py,
                runtime.py, __main__.py (the `minijx` command), bin/ (the binary)
hatch_build.py  wheel build hook
example/        demo app: app.py, components/, static/
bench/          benchmark against Jx
tests/          pytest suite; tests/fixtures has a sample component set

Errors are printed as file:line:col: message on stderr and the exit code is 1. Other files still compile.

Development

make build        # needs fpc 3.2+; writes build/minijx and copies it into src/minijx/bin/
make test         # pytest: golden tests against Jx, filters/tests against jinja2
make dist         # the wheel for this platform and the sdist, in dist/

make test uses ../jx/.venv/bin/python, a Python with jx, jinja2 and pytest. make test JX_PYTHON="uv run --group test python" uses the test dependencies from pyproject.toml instead, with Jx from PyPI.

The wheel build (hatch_build.py) compiles the binary with fpc, or takes it from $MINIJX_BINARY, checks that its version and module format match the runtime's, and tags the wheel py3-none-<platform>. The version is only written in pyproject.toml (uv version --bump patch changes it): the Makefile and the wheel build pass it to fpc in $MINIJX_VERSION, and minijx.__version__ reads it from the package metadata, or from pyproject.toml in a source checkout. The fpc options are in compiler/minijx.cfg, which both use.

MINIJX_TEST_INSTALLED=1 makes the tests import the installed package instead of src/, to test a wheel. On a free-threaded Python, run the tests with PYTHON_GIL=0: the concurrency tests then run truly in parallel, and one of them fails if something turned the GIL back on.

Releasing

The wheels workflow tests every push on Python 3.13, 3.14 and 3.14t, and builds a wheel on each platform and tests it on 3.13 and 3.14t.

Metadata

Release files for minijx 0.1.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for minijx 0.1.0rc1
File Size Uploaded
minijx-0.1.0rc1.tar.gz 82.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for minijx 0.1.0rc1
File
minijx-0.1.0rc1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64, Linux musl 1.1+ x86-64 Details
minijx-0.1.0rc1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl Python 3 none Linux musl 1.1+ ARM64, Linux glibc 2.17+ ARM64 Details
minijx-0.1.0rc1-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
minijx-0.1.0rc1-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 841.0 kB

Release files / minijx-0.1.0rc1.tar.gz

Download URL minijx-0.1.0rc1.tar.gz
Size 82.6 kB
Tags Source
SHA-256 checksum
How to use checksums
4cd9b3b9fceaadacc7091f31dd0399cc842afaf55b06505aa61a1b08d6bc5e7c
BLAKE2b-256 checksum
How to use checksums
57279ef81b0880457181e29401478e9f60f13c7409b9ae817c05260cffb3f45b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / minijx-0.1.0rc1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl

Download URL minijx-0.1.0rc1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
Size 158.0 kB
Tags Linux glibc 2.17+ x86-64 Linux musl 1.1+ x86-64 Python 3
SHA-256 checksum
How to use checksums
1e8f9fa73493f1634037ff2fe4a455135c3fbe717867f7f152392cc7da7ad4d1
BLAKE2b-256 checksum
How to use checksums
86e24ebe81926138954543b898b23a36d112a961bdd8af252f125491fb9cd406
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / minijx-0.1.0rc1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl

Download URL minijx-0.1.0rc1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
Size 153.2 kB
Tags Linux glibc 2.17+ ARM64 Linux musl 1.1+ ARM64 Python 3
SHA-256 checksum
How to use checksums
5c008b6c966f5836dd81eb02e15fa7bd75999b9f6427c64baec33b9b5cc7dcc4
BLAKE2b-256 checksum
How to use checksums
e54c12633c2666ceb71dd1a6b461eb367971f850f6077bf51ecee7d627275832
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / minijx-0.1.0rc1-py3-none-macosx_11_0_arm64.whl

Download URL minijx-0.1.0rc1-py3-none-macosx_11_0_arm64.whl
Size 218.2 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
dcc1892ced5027d076e6420c2fea819a2eebc4a7f51a5e144bd1928b79fafdcc
BLAKE2b-256 checksum
How to use checksums
19d3ba335b857828d904f0e59608cca2319c1a0ae1a219bbbb176c64832e8400
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / minijx-0.1.0rc1-py3-none-macosx_10_12_x86_64.whl

Download URL minijx-0.1.0rc1-py3-none-macosx_10_12_x86_64.whl
Size 229.0 kB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
c98877d80b0f7810fcac95699090eb315957493efdc76e5e710c6fcc6607b656
BLAKE2b-256 checksum
How to use checksums
945ebc5bd5ccd6f00d3168e64639d14e987bc2bb316397409d31ebb060ca3385
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.1.0

5 release files

This release

0.1.0rc1 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page