Skip to main content

Backend Python library for rendering LaTeX problem statements and CP editorial animations to self-contained HTML

Project description

Scriba

Status: v0.23.1 · MIT · Python 3.10+

Scriba is a backend Python library that renders LaTeX problem statements and competitive-programming editorials to self-contained HTML fragments. It is LaTeX-first: drop a .tex source in, get out HTML plus the exact CSS/JS asset basenames needed to display it.

What is Scriba?

  • LaTeX-first rendering for CP problem statements and editorials, with KaTeX math, Pygments code highlighting, lists, tables, sections, figures, \href / \url with XSS hardening, and \begin{lstlisting} code blocks.
  • Self-contained output contract: every render produces an HTML fragment plus a namespaced set of required CSS and JS basenames and a block-data map — consumers decide how to serve the static assets.
  • \begin{animation} environment (shipping since 0.2.0) for step-through editorial walkthroughs with 15 built-in primitives (arrays, grids, graphs, trees, DP tables, number lines, matrices/heatmaps, stacks, plane-2D, metric plots, and the 5 data-structure primitives: code panel, hash map, linked list, queue, variable watch). \begin{diagram} for inline static graph/tree figures is reserved under extension E5. See docs/spec/ruleset.md for the full grammar and error catalog.

What's new in v0.23.0

  • Every navigation direction now animates — Prev/ArrowLeft tween by inverting the step's delta manifest; multi-step jumps snap and pulse exactly what changed (cap 8, prefers-reduced-motion honoured, SCRIBA_NO_EMPHASIS=1 opt-out).
  • Named binding carets\cursor{a}{id=i, at="w.var[i]"}: multiple ▲ markers slide between cells, re-reading a VariableWatch value each frame. Legacy \cursor unchanged.
  • Narration welds to the frame\ref{sel}{text} tints a word with the referenced element's CURRENT state color each frame; \focus{sel} spotlights the active set; \step[title=] headings; \invariant{...} pinned predicate panel.
  • \playeach — one auto-frame per element of a range/block (recolor sweep + caret + ${i} narration), byte-identical to hand-written steps.
  • Array grows honestlyinsert=/remove= shift values on a fixed grid (positions never move), sentinels=true adds a.before/a.after slots for out-of-range iterators.
  • SCRIBA_VERSION 15→16; new spec: docs/spec/motion-ruleset.md (A-0..A-8).
v0.22.2 changelog

What's new in v0.22.2

  • \trace — an arrow that follows a sequence of cells (cells=[[2,0],[2,1],...]): traversal/fill direction is SHOWN, not inferred from cell numbering; the interactive widget draws the arrow along its path on the step it appears.
  • block[r0:r1][c0:c1] — the 2-D twin of range for Grid/DPTable-2D, with bracket=true for a dashed outline hugging the area.
  • color="state:X" + leader=true — labels can carry the exact color of the state they describe (current/done/dim/good/error/path, dark-adapted, WCAG-AA inks) and connect to their cell with a dotted leader.
  • SCRIBA_VERSION 14→15 (new CSS tokens change rendered bytes; the new commands are opt-in). See CHANGELOG.md.
v0.22.1 changelog

What's new in v0.22.1

  • Exact label-math metrics — annotation/caption/tick labels containing $math$ are measured by a KaTeX advance-sum over the vendored font tables (p50 0.06% vs Chromium; the old heuristic sat at p50 43% and measured $\to$ as 0px). Label foreignObjects grow instead of clipping, tall math (\frac, big-operator limits) gets adaptive line heights, and every FO div is font-pinned so painted widths match measured ones.
  • Content-based cells — DPTable/Grid cells widen to their widest value across the whole timeline (frame-stable, no breathing); Matrix floors cell_size under show_values. No-KaTeX fallbacks paint a stripped max(y,x) instead of raw $\max(y,x)$, and are measured as painted.
  • SCRIBA_VERSION 13→14 — rendered bytes change; caches keyed on output must invalidate. See CHANGELOG.md for the full list.
v0.22.0 changelog

What's new in v0.22.0

  • Exact text metrics — cell/node text is measured against a shipped, pinned 34 KB Inter subset ("Scriba Sans", full Vietnamese coverage) using the font's own advance table: browser deltas drop from 5–30% to <1%, tabular-nums honoured, stdlib-only runtime.
  • One runtime source — the inline widget <script> (and the standalone theme toggle) are derived from scriba.js by sentinel slicing; a generation-token guard kills the orphaned-transition race; rapid Next/Prev never swallows frames.
  • Every-script rung 0 — Thai/Devanagari identifiers parse (combining marks), spaceless scripts wrap cluster-safely, RTL text renders in logical order (unicode-bidi:plaintext, dir="auto"), complex-script widths warn once (W1301) that they are safe over-estimates. CJK is exact at 1em by construction.
  • Layout cannot drift — viewBox + stacking offsets come from one shared timeline replay; content labels join one registry per primitive; the smart-label lint backlog is zero and gated there.
v0.21.0 changelog

What's new in v0.21.0

  • Annotation & caption legibility across all primitives — long label= captions wrap and fold into every primitive's bounding box (no more clipping at the figure edge); range[a:b] and position=below annotation targets that were silently dropped now render on all data-structure primitives; position=below labels sit in a leader-connected callout lane, wide left/right pills reserve horizontal space, competing above-labels stack (ranges get a span bracket), and cross-primitive obstacle avoidance is restored. Rendered output bytes differ (SCRIBA_VERSION 8→9) — caches keyed on rendered output MUST invalidate.
  • (v0.20.0) Compact embed widget — overlay step controls, tidy spacing, overlap-safe annotations.
  • (v0.19.0) Graph layout stability — node pinning, isolated-node lane, auto-seed.
  • (v0.18.0) Render-content fixes — narration interpolation, captions, recolor, shared defs.
  • (v0.17.0) Fail-loud validation, render fixes, reference overhaul.
  • (v0.16.0) Embedder font-scale knob + boundary validation.
  • See CHANGELOG.md for the full history.
v0.8.2 changelog
  • Position-aware auto-ID generation. Duplicate animation/diagram blocks with identical content now produce distinct HTML element IDs.
  • Duplicate block ID warning. The pipeline emits CollectedWarning(code="E1019", severity="dangerous") when two blocks share the same block_id.
v0.8.0 changelog
  • Fixed state styling regression. Cell/node/edge state colors (current, error, good, highlight, etc.) were silently overridden by primitive base selectors due to a CSS specificity conflict. Primitive base selectors now use :where() to zero their qualifying specificity, so .scriba-state-* rules always win.
v0.7.0 changelog
  • Fully portable HTML output. render.py now produces single-file, offline-ready HTML. All CSS (scene primitives, animation, widget chrome, Pygments syntax highlighting), KaTeX math fonts (20 woff2 files, base64-encoded), and \includegraphics images (data URIs) are inlined. Zero CDN dependencies — just open the .html in any browser.
  • CSS deduplication. The ~470-line inline CSS block in render.py was replaced by a new scriba.core.css_bundler module that reads CSS from source .css files at render time. Single source of truth for all styling.
  • traversable_to_path() helper. Centralised the Path(str(traversable)) anti-pattern across 3 files into a documented helper in scriba.core.artifact, ready for future as_file() upgrade.
  • text_outline= parameter removed. The per-call text outline parameter on primitives, deprecated in v0.6.0, is removed. Use the CSS halo cascade instead.
v0.6.0 changelog
  • Wave 8 — vstack layout. Array, DP-table, and related primitives now compose their caption, index labels, and cells through a shared scriba/animation/primitives/layout.py vstack helper.
  • Wave 9 — CSS-first text halo cascade. Every [data-primitive] text element now inherits paint-order: stroke fill markers with a --scriba-halo CSS variable that each state class overrides.
  • RFC-001 — structural mutation ops. Tree, Graph, and Plane2D primitives gained safe structural ops (add_node, remove_node, reparent).
  • RFC-002 — strict mode and document warnings. Pipeline surfaces non-fatal issues on Document.warnings. See docs/guides/strict-mode.md.
  • Examples reorganized. 53 .tex examples across examples/quickstart/, examples/algorithms/, examples/cses/, and examples/primitives/. See docs/cookbook/README.md.

Install

pip install scriba-tex

Scriba shells out to a small Node.js worker for KaTeX math, so the host environment needs Node.js 18+ on PATH:

# System prerequisite — Node.js only
apt-get install nodejs   # or: brew install node

KaTeX 0.16.11 is vendored inside the wheel (at scriba/tex/vendor/katex/katex.min.js), so no separate npm install -g katex step is required. pip install scriba-tex is all you need once Node is present.

Using Scriba with an AI assistant

To have an AI write .tex for Scriba, give it one file: docs/SCRIBA-TEX-REFERENCE.md.

It's self-contained — all commands, all 15 primitives, all selectors, all gotchas. No other spec files needed.

Prompt template:

Read SCRIBA-TEX-REFERENCE.md. Write a Scriba .tex file that animates [algorithm]. Use only commands and primitives documented in that file.

Hello world

from scriba import Pipeline, RenderContext, SubprocessWorkerPool
from scriba.tex import TexRenderer

pool = SubprocessWorkerPool()
pipeline = Pipeline([TexRenderer(worker_pool=pool, pygments_theme="one-light")])

ctx = RenderContext(
    resource_resolver=lambda name: f"/cdn/problems/1/{name}",
    theme="light", dark_mode=False, metadata={}, render_inline_tex=None,
)

doc = pipeline.render(r"\section{Hello} Let $x^2$ be the square.", ctx)
print(doc.html)          # HTML fragment
print(doc.required_css)  # namespaced CSS keys
pipeline.close()

Standalone CLI

For quick rendering without writing Python, use render.py directly:

python render.py input.tex                # → input.html
python render.py input.tex -o out.html    # → custom output path
python render.py input.tex --open         # → render and open in browser

Output is a single, fully portable HTML file — all CSS, KaTeX math fonts, syntax highlighting, and images (via \includegraphics) are inlined as data URIs. No internet connection or external files needed. Just open the .html file in any browser.

For legacy filmstrip mode (static frames, no JavaScript):

python render.py input.tex --static

Sanitize before embedding

Scriba does not sanitize its output — consumers must pass it through a vetted sanitizer before embedding in a page. Scriba ships an allowlist that matches its output contract:

import bleach
from bleach.css_sanitizer import CSSSanitizer
from scriba import ALLOWED_TAGS, ALLOWED_ATTRS

css = CSSSanitizer(allowed_css_properties=("transform","transform-origin","width","height"))
safe = bleach.clean(doc.html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS,
                    css_sanitizer=css, strip=True)

Serving static assets

Assets ship inside the Python package. Copy them at deploy time:

from importlib.resources import files
import shutil
shutil.copytree(str(files("scriba.tex.static")), "./public/scriba", dirs_exist_ok=True)

Then include them alongside the rendered fragment:

<link rel="stylesheet" href="/cdn/katex/katex.min.css">
<link rel="stylesheet" href="/public/scriba/scriba-tex-content.css">
<link rel="stylesheet" href="/public/scriba/scriba-tex-pygments-light.css">
<script defer src="/public/scriba/scriba-tex-copy.js"></script>

<article class="scriba-tex-content">{{ doc.html }}</article>

Note: This section applies to the Pipeline API (library usage), where you serve assets yourself. If you use render.py instead, the output HTML is fully self-contained — all CSS, KaTeX fonts (base64), and Pygments highlighting are inlined. No separate asset serving needed.

Documentation

Full architecture, contracts, and roadmap live under the project docs tree: https://github.com/Danchuong/scriba/tree/main/docs

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scriba_tex-0.23.1.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scriba_tex-0.23.1-py3-none-any.whl (860.6 kB view details)

Uploaded Python 3

File details

Details for the file scriba_tex-0.23.1.tar.gz.

File metadata

  • Download URL: scriba_tex-0.23.1.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for scriba_tex-0.23.1.tar.gz
Algorithm Hash digest
SHA256 b42ea94a787f86a09efb7142b6fddc283170a30ed8aa96cef83f6a00e568d485
MD5 bf5640e91c5799166cd4d982b1935312
BLAKE2b-256 dcd9ee551305281c6a57113366d92e5144fbc841f51ecceef7b24b467422ba02

See more details on using hashes here.

File details

Details for the file scriba_tex-0.23.1-py3-none-any.whl.

File metadata

  • Download URL: scriba_tex-0.23.1-py3-none-any.whl
  • Upload date:
  • Size: 860.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.20

File hashes

Hashes for scriba_tex-0.23.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fa78df9e4bfda2ee2e2704cf5a53198a12d1b91ca6c180b2856d2613fe5fc98b
MD5 c69809bf4046e928844c1b61ab6c66a4
BLAKE2b-256 23e4598d1d3c6fe1c29f9b35a0b1755af658ddf83bad6f397da10dce6e0e5129

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page