gain.templates package
Submodules
gain.templates.markdown_support module
Markdown rendering for GAIn’s generated documentation pages.
render_markdown is the one place GAIn turns documentation prose into
HTML. It has the same str -> str contract as markdown2.markdown
and post-processes its output: a raw < (or </) whose tag name is
neither a known HTML/SVG/MathML element nor hyphenated is escaped to
<, so prose like values <thresh are dropped reaches the reader
whole instead of being swallowed by the browser as a bogus tag.
This is not a sanitizer. A recognized element – including
<script> – passes through untouched, attributes and all: GRR content
is trusted by authorship (ADR 0016), and the characterization tests in
tests/small/annotation/test_annotate_doc_trust.py pin that trust.
Hyphenated names pass too, so custom elements from out-of-tree plugin
documentation keep rendering.
Accepted limitations: prose whose accidental tag name collides with a
real element name (values <var are dropped, <b, <a, …) is
still eaten by the browser – rescuing it would mean guessing the
author’s intent. So is prose a browser reads as a bogus comment:
</ followed by a non-letter, and <! outside a real comment or
doctype, each consume to the next > and are left alone here.
Escaping happens on the rendered HTML, not the Markdown source:
markdown2 already escapes < inside code spans and fenced blocks, and
pre-escaping the source would make it re-escape the & of a
pre-inserted < into visible garbage. The content of raw-text
elements (<script>, <style>, …) is exempt from the rescue:
character references do not decode there, so an inserted < would
reach the script engine or CSS parser as five literal characters instead
of the author’s <.
- gain.templates.markdown_support.DEFAULT_EXTRAS: Final = ('tables', 'fenced-code-blocks', 'highlightjs-lang')
The Markdown dialect every GAIn caller renders GRR prose in. Applied by
render_markdownwhen a caller names noextrasof its own, so the dialect is one decision rather than one per call site – which is how the about page and the resource page drifted into rendering the same source differently (gain#1278).fenced-code-blockson its own highlights through Pygments when a fence names a language and Pygments is importable, and falls back to a plain<pre><code>otherwise. Pygments is not a declared dependency – absent from the gain-core runtime closure, present in a dev venv – so that shape would be decided by the build environment rather than by the GRR content.highlightjs-langmakes markdown2 skip Pygments unconditionally and carry the language only as a class on<code>(<pre><code class="python language-python">): no server-side highlighting, by decision (gain#1289).
- gain.templates.markdown_support.render_markdown(text: str, **kwargs: object) str[source]
Render Markdown to HTML, rescuing prose from bogus tags.
Renders in
DEFAULT_EXTRASunless the caller names its ownextras; passingextras=[]asks for plain Markdown and gets it. Keyword options (extras=...and the rest) pass through tomarkdown2.markdown. Returns a plainstr: the attribute side channel of markdown2’s return type (toc_html,metadata) is not carried over.
gain.templates.static_assets module
Static files a repository’s generated pages need, shipped with gain.
The GRR index page’s search runs on sqlite-wasm, and every generated
page sets its text in Roboto and draws its icons from a Material
Symbols subset. Rather than loading any of that from a CDN or a font
host at view time, gain vendors the files under
gain/templates/static/ and publishes them into every repository it
indexes, beside index.html, at paths the pages reach by relative URL
(gain#1335 for the engine, gain#1400 for the fonts). A repository then
carries everything its pages need – no third party at view time, a
working search and real icons on an intranet or behind an air gap, and
the bytes that run are the bytes the generating gain was tested with.
Two ways of versioning what is published, for two kinds of coupling.
The sqlite-wasm version is in the published directory name, not a
query string: the module locates its sqlite3.wasm through
import.meta.url, so a ?v= on the import would never reach the
wasm, and a gain upgrade must not run a browser’s cached module against
a new wasm. version.txt beside the vendored files is the single
source of truth – the directory name and the page’s import are both
derived from it here, so a bump is one edit. The fonts have no such
sibling to keep in step, so each is named by a digest of its own bytes
(_published_font_path()), with nothing to bump.
The directory is dot-prefixed on purpose. A resource id can never
start with a dot, so nothing under .static/ can collide with one,
and the repository scanner passes over every dot-prefixed entry.
repository_static_files() is the registry the publisher iterates:
it names no asset, so the next thing the pages should load from the
repository rather than a CDN is added here and reaches every repository
without the publisher changing.
- gain.templates.static_assets.SQLITE_WASM_PATH: str = '.static/sqlite-wasm-3.51.2-build6'
The directory sqlite-wasm is published to, relative to the repository root. Forward slashes, no leading
./: it is a URL path for the page as much as a repository path for the publisher, and both join it themselves.
- gain.templates.static_assets.SQLITE_WASM_VERSION: str = '3.51.2-build6'
The
@sqlite.org/sqlite-wasmnpm version the vendored files came from. Read once at import; the file is one line.
- gain.templates.static_assets.climb_to_root(page_path: str) str[source]
The relative prefix that takes a page back to the repository root.
page_pathis where the page is published, relative to the root –<resource id>/index.html, say – and each directory in it is one../for the page’s urls to climb before.static/. Computed here, from the path the publisher writes, rather than in the templates from the resource id plus an offset per page kind: the two cannot then drift. Empty for a page at the root.
- gain.templates.static_assets.repository_static_files() Iterator[tuple[str, bytes]][source]
Each file to publish: its repository-relative path, and its bytes.
The bytes are read on every call rather than cached: the publisher runs once per
repo-info, and 1.2 MB held for the life of every process that imports the templates is the wrong trade.
Module contents
Central Jinja2 template environment for GAIn.
Provides a singleton Environment that resolves templates in two stages:
Physical files under gain/templates/template_files/ via PackageLoader.
Strings supplied by callables registered under the “gain.templates.providers” entry-point group. Each callable must return a
dict[str, str]mapping template name to template source. All provider dictionaries are merged lazily on first miss.
Raises jinja2.TemplateNotFound if a name is not found in either stage.
The environment autoescapes, so a template interpolating a value that is
already markup – a nested render, markdown2 output, a pandas
to_html table – has to say so with |safe. Autoescaping is HTML
escaping, which is wrong for the few templates in MARKDOWN_TEMPLATES:
they emit Markdown that GPF renders downstream, and escaping there
mangles the Markdown syntax and the & in histogram image URLs. The
decision is by template name rather than by extension because every
template here is named *.jinja, HTML and Markdown alike.
Templates supplied by an out-of-tree provider load through this same environment and are autoescaped along with the rest.
The environment carries two globals. The first is markdown: the
Markdown wrapper from gain.templates.markdown_support. It is
registered here rather than passed by each caller as a render kwarg so
that a template calling markdown(...) gets the rescuing wrapper
whether or not whoever renders it thought about the name – gain#742 was
a render site that did not (gain#751). A render kwarg still shadows a
global, so a caller that passes its own markdown= wins; no caller in
this repo does.
That global serves templates. The several gain modules that render a
resource’s meta description or an about.md in Python – before
the result enters a dict some template dumps generically – still import
render_markdown and call it themselves; a template global cannot
reach them. Those imports are what the architecture fence governs.
render_markdown is imported under its own name deliberately. The
wrapper module renders through markdown2 and so re-exports the raw
function under the bare name markdown: binding that here would
leave every template in the stack without the bogus-tag rescue while
looking correct. core’s architecture tests refuse that import; they
read imports, so reaching the same function as an attribute of an
imported module would pass them – what catches that is the rescue being
asserted on rendered output.
The import sits inside get_jinja_env rather than at module scope so
that importing gain.templates does not drag in markdown2 for callers
that only ever fetch a template: it costs about 10ms, and the annotation
workers pay module import per spawned process.
The second global, natural_chromosome_key, orders a contig name by
its digit runs so a per-chromosome table reads chr1, chr2, chr10 rather
than chr1, chr10, chr2. The info page’s Coverage and Alleles tables
emit it as the Chromosome column’s data-sort-value, which is what
lets the client-side sorter reorder those rows without shipping any
ordering logic of its own (gain#983, gain#984). It is a global rather
than a field on the row objects because the two tables that use it do
not share a row type – Coverage renders a CoverageRow NamedTuple
while Alleles renders a plain dict – and because the reference genome
and gene models pages carry per-chromosome tables that will want the
same key.
Unlike render_markdown it is imported at module scope: the module it
comes from imports only re, so there is no start-up cost to defer,
and gain.utils is where it deliberately lives so that the template
layer can reach it without importing genomic_resources.
The third global, sqlite_wasm_path, is the repository-relative
directory the index page imports its search engine from – the same
directory build_index_info publishes the vendored sqlite-wasm files
to (gain.templates.static_assets, gain#1335). A global rather than
a render kwarg for the same reason as markdown: the page has more
than one render site, the tests among them, and none of them should be
able to render an import that points somewhere the publisher did not.
- gain.templates.get_jinja_env() Environment[source]
Return the singleton GAIn Jinja2 Environment.
- gain.templates.get_template(name: str) Template[source]
Convenience wrapper — raises TemplateNotFound if name is absent.
- gain.templates.reset_caches() None[source]
Forget the built environment and the merged provider templates.
Both are process-wide and built on first use, so a test that registers a template provider, or patches what one returns, needs the next call to build afresh – this is the one way to ask for that without reaching into the cache itself.