New endpoint GET /<path>/foo.md?convert=docx|html|pdf renders a markdown source on demand. Surfaced as the Download buttons in browse's markdown editor (separate commit). Execution model — two upstream container images, lazy-pulled: • docker.io/pandoc/latex:latest — MD→DOCX, MD→HTML (entrypoint pandoc) • docker.io/zenika/alpine-chrome — HTML→PDF (entrypoint chromium-browser) No custom image build. The runner passes --pull=missing on every podman/ docker invocation so the operator only needs the runtime installed — first request pulls the image, subsequent requests use the local cache. Overrides: --convert-pandoc-image / --convert-chromium-image (and the matching ZDDC_CONVERT_* env vars). Engine: --convert-engine (podman preferred, docker fallback). Resource caps: --convert-mem-mib (512), --convert-cpus (2), --convert-pids (100), --convert-timeout (30s). PDF flow is two-stage: pandoc renders the markdown through the embedded viewer-template.html to standalone HTML, then chromium prints that HTML via --print-to-pdf. Preserves the print-media CSS already authored in viewer-template.html rather than going through pandoc's LaTeX template. Each conversion runs in a throw-away container with --rm --network=none --read-only --tmpfs=/tmp --cap-drop=ALL --security-opt=no-new-privileges --env=HOME=/tmp plus a bind-mounted scratch dir for I/O. Pandoc reads markdown from stdin / writes to stdout; the viewer template lives at /tpl (ro). Chromium reads HTML from a read-write bind mount at /pdf and writes the PDF to the same mount; the host reads it back. No shell wrappers, no shell quoting — argv flows straight into each image's entrypoint. On-disk cache at <dir>/.converted/<base>.<ext> with mtime synced to the source. Fast path is a stat-and-serve with no exec; slow path singleflights concurrent requests for the same target. PUT/DELETE/MOVE on the source .md purges the .converted/ sidecars. Per-project template variables (client/project/contractor/project_number) come from a new .zddc `convert:` cascade block, walked leaf→root with per-key latest-wins. Filename-derived variables (title, tracking_number, revision, status, is_draft) come from a new zddc.ParseFilename helper. If neither podman nor docker is on PATH, the endpoint serves 503 with a clear Retry-After. The rest of the server keeps working. This is the first os/exec site in the codebase. The hardening in internal/convert/runner.go — context.CancelFunc → process kill, cmd.WaitDelay, platform-specific SysProcAttr (Setpgid + Pdeathsig on Linux), minimal env, stdout cap via limitWriter, stderr ring buffer — sets the pattern for any future shell-outs. Public surface: convert.ToDocx(ctx, source, meta) / .ToHTML / .ToPDF convert.Probe(ctx, engineOverride) → install Runner if engine present convert.SetImages(pandoc, chromium) convert.ConfigureLimits(memMiB, cpus, pids, timeout) convert.Available() Container handler at internal/handler/converthandler.go; dispatcher branch in cmd/zddc-server/main.go inserts the convert lookup after the existing ACL gate, reusing the source file's read policy verbatim.
163 lines
3.4 KiB
CSS
163 lines
3.4 KiB
CSS
/*
|
|
* Legal-style heading numbering for ZDDC documents
|
|
* Adds hierarchical numbering like 1, 1.1, 1.1.1, etc.
|
|
*/
|
|
|
|
/* Reset counters at document level */
|
|
.document-content {
|
|
counter-reset: h1-counter;
|
|
}
|
|
|
|
/* H1 counters */
|
|
h1 {
|
|
counter-reset: h2-counter h3-counter h4-counter h5-counter h6-counter;
|
|
counter-increment: h1-counter;
|
|
}
|
|
|
|
h1::before {
|
|
content: counter(h1-counter) ". ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* H2 counters */
|
|
h2 {
|
|
counter-reset: h3-counter h4-counter h5-counter h6-counter;
|
|
counter-increment: h2-counter;
|
|
}
|
|
|
|
h2::before {
|
|
content: counter(h1-counter) "." counter(h2-counter) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* H3 counters */
|
|
h3 {
|
|
counter-reset: h4-counter h5-counter h6-counter;
|
|
counter-increment: h3-counter;
|
|
}
|
|
|
|
h3::before {
|
|
content: counter(h1-counter) "." counter(h2-counter) "." counter(h3-counter) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* H4 counters */
|
|
h4 {
|
|
counter-reset: h5-counter h6-counter;
|
|
counter-increment: h4-counter;
|
|
}
|
|
|
|
h4::before {
|
|
content: counter(h1-counter) "." counter(h2-counter) "." counter(h3-counter) "." counter(h4-counter) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* H5 counters */
|
|
h5 {
|
|
counter-reset: h6-counter;
|
|
counter-increment: h5-counter;
|
|
}
|
|
|
|
h5::before {
|
|
content: counter(h1-counter) "." counter(h2-counter) "." counter(h3-counter) "." counter(h4-counter) "." counter(h5-counter) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* H6 counters */
|
|
h6 {
|
|
counter-increment: h6-counter;
|
|
}
|
|
|
|
h6::before {
|
|
content: counter(h1-counter) "." counter(h2-counter) "." counter(h3-counter) "." counter(h4-counter) "." counter(h5-counter) "." counter(h6-counter) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
/* TOC numbering to match document headings */
|
|
.toc {
|
|
counter-reset: toc-h1;
|
|
}
|
|
|
|
.toc ul {
|
|
list-style: none;
|
|
}
|
|
|
|
.toc > ul > li {
|
|
counter-increment: toc-h1;
|
|
counter-reset: toc-h2 toc-h3 toc-h4 toc-h5 toc-h6;
|
|
}
|
|
|
|
.toc > ul > li > a::before {
|
|
content: counter(toc-h1) ". ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
margin-right: 0.25em;
|
|
}
|
|
|
|
.toc > ul > li > ul > li {
|
|
counter-increment: toc-h2;
|
|
counter-reset: toc-h3 toc-h4 toc-h5 toc-h6;
|
|
}
|
|
|
|
.toc > ul > li > ul > li > a::before {
|
|
content: counter(toc-h1) "." counter(toc-h2) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
margin-right: 0.25em;
|
|
}
|
|
|
|
.toc > ul > li > ul > li > ul > li {
|
|
counter-increment: toc-h3;
|
|
counter-reset: toc-h4 toc-h5 toc-h6;
|
|
}
|
|
|
|
.toc > ul > li > ul > li > ul > li > a::before {
|
|
content: counter(toc-h1) "." counter(toc-h2) "." counter(toc-h3) " ";
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
margin-right: 0.25em;
|
|
}
|
|
|
|
/* Optional: Add some spacing after the numbers */
|
|
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
|
|
margin-right: 0.5em;
|
|
}
|
|
|
|
/* Print-specific adjustments */
|
|
@media print {
|
|
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
|
|
color: #000 !important; /* Ensure numbers print in black */
|
|
}
|
|
}
|
|
|
|
/* Optional: Style adjustments for better visual hierarchy */
|
|
h1 {
|
|
border-bottom: 2px solid var(--primary-color);
|
|
padding-bottom: 0.3em;
|
|
margin-top: 1em;
|
|
}
|
|
|
|
/* Reduce margin for first heading */
|
|
h1:first-of-type {
|
|
margin-top: 0.5em;
|
|
}
|
|
|
|
h2 {
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding-bottom: 0.2em;
|
|
margin-top: 1.5em;
|
|
}
|
|
|
|
h3 {
|
|
margin-top: 1.2em;
|
|
}
|
|
|
|
h4, h5, h6 {
|
|
margin-top: 1em;
|
|
}
|