gain.templates package

Submodules

gain.templates.breadcrumb module

The trail a resource’s generated pages show back to the repository.

A resource page and its statistics page carry the same breadcrumb the index page’s hierarchical view draws (gain#1477): the root, one crumb per id segment, and the page itself last. Each crumb’s address is decided here rather than in the templates, from the same two facts the root climb is computed from – the resource id and where the page sits inside the resource directory – so the trail and the climb cannot drift. The templates only loop over what this returns.

A crumb into the tree addresses a folder the way the index page addresses one itself, index.html#/<segments> (gain#579), so it is a plain link and the index page does the rest on load. The root crumb goes to the tree’s root, not to a bare index.html: an empty fragment means the table view, and the trail is one kind of navigation.

class gain.templates.breadcrumb.Crumb(name: str, href: str | None)[source]

Bases: object

One step of the trail: its name, and where it leads.

href is None for the page itself, which is never a link.

href: str | None
name: str
gain.templates.breadcrumb.ROOT_CRUMB_NAME = 'All resources'

The root crumb’s name, the same words the index page’s breadcrumb gives its own root.

gain.templates.breadcrumb.page_breadcrumb(resource_id: str, page: str) list[Crumb][source]

The trail to one of a resource’s pages, outermost crumb first.

page is the page’s path inside the resource directory, as the publisher names it – index.html for the info page, statistics/index.html for the statistics page – the same argument the root climb takes. Every directory in it after the id’s own is a crumb of its own, and the last crumb is the current page.

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 &lt;, 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 &lt; 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 &lt; 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_markdown when a caller names no extras of 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-blocks on 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-lang makes 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_EXTRAS unless the caller names its own extras; passing extras=[] asks for plain Markdown and gets it. Keyword options (extras=... and the rest) pass through to markdown2.markdown. Returns a plain str: 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-wasm npm 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_path is 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:

  1. Physical files under gain/templates/template_files/ via PackageLoader.

  2. 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.