') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - Acture/docpack: 🔁 Convert structured data (CSV/JSON/YAML/TOML/XLSX) into Typst syntax for embedding in documents. · GitHub
Skip to content

Repository files navigation

docpack

crates.iohomebrewCIReleaseLicense: AGPL v3TypstLaTeX

Freeze structured data into document-native modules.

docpack turns external data sources into checked-in document assets.

Feed it CSV, JSON, YAML, TOML, or XLSX. Get back generated Typst or LaTeX code that you can commit, diff, review, and ship with the rest of your document sources.

It is built for the cases where runtime data loading is the wrong tradeoff:

  • reproducible reports
  • compliance or approval workflows
  • offline or single-file document bundles
  • generated artifacts that need stable diffs

Why docpack

  • Manifest-first: use docpack build for repeatable project outputs.
  • CLI-friendly: use docpack emit for one-shot pipes and previews.
  • Backend-aware: Typst and LaTeX outputs share one normalized data model.
  • Explicit failures: format, inference, sheet, and shape errors are surfaced with concrete diagnostics.
  • Reviewable output: generated artifacts are plain text modules, not opaque caches.

Capability Matrix

AreaSupport
Inputscsv, json, yaml, toml, xlsx
Backendstypst, latex
Artifactsdata-module, table-fragment
Typst stylestypst-official, typst-table
LaTeX styleslatex-expl3, latex-classic-macro, latex-booktabs-longtable, latex-plain-tabular
Workflow modesbuild, emit, inspect, init

Quickstart

Install

cargo install docpack

Or via Homebrew:

brew tap acture/tools
brew install docpack

Emit to stdout

emit is the fast path. If you do not pass --output, generated code is written to stdout.

docpack emit data/profile.json --backend typst

Example output:

#letprofile= ("name": "Alice", "role": "Engineer")

Emit to a file

docpack emit data/table.csv \
--output build/table.tex \
--artifact table-fragment

Pipe from stdin

Stdin requires an explicit format, and stdout requires an explicit backend.

cat data/input.yaml | docpack emit - --format yaml --backend typst

Bootstrap a manifest

docpack init

Build all declared outputs

docpack build

Manifest-First Workflow

Minimal docpack.toml:

[project]
name = "quarterly-report"output_dir = "generated"
[[sources]]
id = "sales"path = "data/sales.csv"format = "csv"
[[outputs]]
id = "sales_typst"source = "sales"path = "sales.typ"backend = "typst"artifact = "data-module"style = "typst-official"root_name = "sales"
[[outputs]]
id = "sales_tex"source = "sales"path = "sales.tex"backend = "latex"artifact = "table-fragment"style = "latex-booktabs-longtable"

Then:

docpack build

Command Surface

docpack build [manifest-path]
docpack emit <input> [--output <path>] [--format <format>] [--backend <backend>]
docpack inspect <input-or-manifest> [--as <source|manifest>] [...]
docpack init [path] [--force]

What each command is for

  • build: resolve a manifest and write all outputs in order.
  • emit: convert one source into one artifact.
  • inspect: show normalized shape, metadata, and resolved render defaults.
  • init: generate a starter manifest.

Output Styles

BackendArtifactDefaultAlternatives
Typstdata-moduletypst-officialnone
Typsttable-fragmenttypst-tablenone
LaTeXdata-modulelatex-expl3latex-classic-macro
LaTeXtable-fragmentlatex-booktabs-longtablelatex-plain-tabular

How It Works

docpack is intentionally small at the center:

source bytes
-> input normalization
-> Value + SourceMeta
-> backend render request
-> Typst / LaTeX artifact

Current library layers:

  • core: normalized value tree and source metadata
  • input: format adapters and normalization
  • backend: Typst and LaTeX rendering
  • manifest: project resolution, inference, and build planning
  • error: structured failure model

Examples

Inspect before generating

docpack inspect data/sales.csv --output build/sales.typ

This prints:

  • source format
  • normalized top-level shape
  • tabular metadata
  • inferred backend / artifact / style / root name

Force a specific sheet

docpack emit workbook.xlsx \
--sheet Sales \
--backend typst \
--artifact table-fragment

Generate classic LaTeX macros

docpack emit data/profile.json \
--backend latex \
--style latex-classic-macro

Status

What is already in place:

  • manifest-driven build flow
  • one-shot emit and inspect flow
  • Typst data modules and table fragments
  • LaTeX expl3, classic macro, longtable, and plain tabular outputs
  • reference fixture coverage across json, yaml, toml, csv, and xlsx
  • real pdflatex smoke coverage for classic macro output
  • structured diagnostics for format inference, tabular shape mismatches, and missing sheets

What is still open:

  • benchmark fixtures for large csv / xlsx sources
  • memory profiling for larger manifest builds

Development

cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test

Maintainers

Release automation is tag-driven. See docs/releasing.md.

Design Notes

docpack is not trying to become a general-purpose ETL framework or a plugin platform.

The core idea is narrower and more useful:

  • normalize external structured data once
  • render it into document-native code
  • keep the generated artifact reviewable and reproducible

License

docpack is licensed under AGPL-3.0-only.

About

🔁 Convert structured data (CSV/JSON/YAML/TOML/XLSX) into Typst syntax for embedding in documents.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages