Skip to content

Commit c6e485a

Browse files
Merge pull request #781 from SKaiNET-developers/chore/docs-markup-antora
docs(antora): consolidate docs image to offline "markup-antora" build
2 parents 0d4d962 + 16fce84 commit c6e485a

4 files changed

Lines changed: 221 additions & 57 deletions

File tree

docs/.docker/Dockerfile

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,103 @@
1+
# markup-antora — one Antora image to use everywhere.
2+
#
3+
# Consolidates the best of the ~9 Antora images that grew across the
4+
# projects tree. Design priorities, in order:
5+
# 1. Mermaid rendered FULLY OFFLINE — no Kroki server, no kroki.io,
6+
# no CDN at build time and none at view time. Diagrams are baked
7+
# to inline SVG by mermaid-cli (Alpine Chromium + Puppeteer)
8+
# through the local-mermaid-extension.js Asciidoctor block
9+
# processor, with content-hash caching so repeated diagrams render
10+
# once per build.
11+
# 2. Works under `--user $(id -u):$(id -g)` (rootless) without the
12+
# Chromium crashpad / cosmiconfig EACCES failures.
13+
# 3. Offline extras available but not forced: lunr full-text search,
14+
# a pre-baked Antora UI bundle, and MathJax es5 for LaTeX.
15+
# 4. asciidoctor-kroki installed-but-unused as an escape hatch.
116
FROM node:20-alpine
217

3-
LABEL org.opencontainers.image.title="SKaiNET Antora" \
4-
org.opencontainers.image.description="Antora site generator with direct local Mermaid rendering (no Kroki round trip)" \
18+
LABEL org.opencontainers.image.title="markup-antora" \
19+
org.opencontainers.image.description="Universal Antora site generator with offline Mermaid (mermaid-cli), offline search (lunr), pre-baked UI bundle + MathJax. No Kroki, no CDN." \
520
org.opencontainers.image.source="https://github.com/SKaiNET-developers/SKaiNET"
621

7-
# Chromium for mermaid-cli (puppeteer)
8-
RUN apk add --no-cache chromium font-noto
22+
# Chromium for mermaid-cli (Puppeteer). Full font set so diagram labels,
23+
# emoji and CJK render correctly (merged from the Daily-StandAPP image).
24+
RUN apk add --no-cache \
25+
chromium \
26+
nss \
27+
freetype \
28+
harfbuzz \
29+
ttf-freefont \
30+
font-noto \
31+
font-noto-emoji \
32+
ca-certificates \
33+
git
934

10-
# HOME=/tmp: chromium's crashpad handler writes its database under $HOME and
11-
# aborts with `chrome_crashpad_handler: --database is required` when the
12-
# container runs as `--user $(id -u):$(id -g)` and $HOME falls back to `/`
13-
# (no passwd entry, not writable). Same motivation as runtime.cache_dir in
14-
# antora-playbook.yml.
35+
# HOME=/tmp: Chromium's crashpad handler writes its database under $HOME
36+
# and aborts with `--database is required` when the container runs as a
37+
# non-root --user and $HOME falls back to `/` (no passwd entry, not
38+
# writable). Same motivation as runtime.cache_dir in the playbook.
1539
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
1640
PUPPETEER_SKIP_DOWNLOAD=true \
1741
HOME=/tmp
1842

19-
# Install Antora + mermaid-cli into /opt/antora (not /antora which gets
20-
# volume-mounted at run time). asciidoctor-kroki is intentionally NOT
21-
# installed — it depends on a Kroki HTTP server (kroki.io or local)
22-
# which returns 400 for large diagrams when using GET and has no
23-
# offline fallback. We render mermaid directly via mermaid-cli through
24-
# the local-mermaid-extension.js asciidoctor block processor.
43+
# Install Antora + tooling into /opt/antora (NOT /antora, which is where
44+
# the project gets volume-mounted at run time).
45+
# - @mermaid-js/mermaid-cli : offline diagram rendering (the point)
46+
# - @antora/lunr-extension : offline full-text search
47+
# - asciidoctor-kroki : escape hatch only; the playbook should
48+
# use the local mermaid extension instead.
2549
WORKDIR /opt/antora
2650
RUN npm init -y && npm i --save-exact \
2751
@antora/cli@3.1 \
2852
@antora/site-generator@3.1 \
53+
@antora/lunr-extension@1.0.0-alpha.8 \
2954
@mermaid-js/mermaid-cli@11 \
55+
asciidoctor-kroki@0.18 \
3056
&& npm cache clean --force
3157

32-
# Make installed modules visible when workdir is the mounted project
58+
# Make installed modules resolvable even when the workdir is the mounted
59+
# project (which has no node_modules of its own).
3360
ENV NODE_PATH=/opt/antora/node_modules
3461

35-
# Mermaid-cli config — used by the local-mermaid-extension to drive
36-
# Puppeteer against the pre-installed Alpine Chromium.
37-
RUN echo '{ \
38-
"executablePath": "/usr/bin/chromium-browser", \
39-
"args": ["--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"] \
40-
}' > /opt/antora/puppeteer-config.json
41-
42-
# Bake the local mermaid extension in at an absolute path so the
43-
# Antora playbook can reference it without any volume-mount gymnastics.
62+
# Mermaid-cli / Puppeteer config and the offline block processor, baked
63+
# in at absolute paths the playbook can reference without mount gymnastics.
64+
COPY puppeteer-config.json /opt/antora/puppeteer-config.json
4465
COPY local-mermaid-extension.js /opt/antora/local-mermaid-extension.js
4566

46-
# Verify mermaid-cli works end to end at image build time. The cleanup
47-
# also removes mode-0700 root-owned dirs (e.g. /tmp/.config/puppeteer,
48-
# /tmp/.local/share/chromium) that puppeteer/chromium drop into $HOME
49-
# during this run — leaving them in place would make cosmiconfig EACCES
50-
# when the container is later launched with a non-root --user.
67+
# --- Offline assets (available, not forced) -------------------------------
68+
69+
# Pre-download the default Antora UI bundle so sites build without hitting
70+
# gitlab.com. Reference it from a playbook with:
71+
# ui:
72+
# bundle:
73+
# url: /opt/antora-ui/ui-bundle.zip
74+
# snapshot: true
75+
RUN mkdir -p /opt/antora-ui \
76+
&& wget -q -O /opt/antora-ui/ui-bundle.zip \
77+
"https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable"
78+
79+
# Pre-download MathJax es5 for offline LaTeX. Copy /opt/mathjax/es5 into a
80+
# supplemental UI or reference it from your UI template for client-side math.
81+
RUN mkdir -p /opt/mathjax \
82+
&& npm pack mathjax@3 --pack-destination /tmp \
83+
&& tar -xzf /tmp/mathjax-*.tgz -C /tmp \
84+
&& cp -r /tmp/package/es5 /opt/mathjax/es5 \
85+
&& rm -rf /tmp/mathjax-* /tmp/package
86+
87+
# --- Build-time smoke test + rootless cleanup -----------------------------
88+
89+
# Verify mermaid-cli works end to end so a broken image fails the build,
90+
# not the user's first run. The cleanup also removes the mode-0700
91+
# root-owned dirs (/tmp/.config/puppeteer, /tmp/.local/share/chromium,
92+
# /tmp/.cache, /tmp/.npm) that Puppeteer/Chromium drop into $HOME during
93+
# this run — leaving them would make cosmiconfig EACCES when the container
94+
# is later launched with a non-root --user.
5195
RUN echo 'graph TD; A-->B;' > /tmp/test.mmd \
52-
&& npx mmdc -i /tmp/test.mmd -o /tmp/test.svg -p /opt/antora/puppeteer-config.json \
96+
&& /opt/antora/node_modules/.bin/mmdc \
97+
-i /tmp/test.mmd -o /tmp/test.svg \
98+
-p /opt/antora/puppeteer-config.json --quiet \
5399
&& rm -rf /tmp/test.mmd /tmp/test.svg /tmp/.config /tmp/.local /tmp/.npm /tmp/.cache
54100

101+
WORKDIR /antora
55102
ENTRYPOINT ["/opt/antora/node_modules/.bin/antora"]
56103
CMD ["--stacktrace", "antora-playbook.yml"]

docs/.docker/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Docs Antora image (`docs/.docker`)
2+
3+
Self-contained Antora image used to build this repo's documentation site.
4+
It is the consolidated "markup-antora" image — one definition shared across
5+
the SKaiNET docs projects — vendored here until the public registry image
6+
is published (after which this `Dockerfile` collapses to a single `FROM`).
7+
8+
## Features
9+
10+
- **Offline Mermaid** — every `[mermaid]` block is rendered to **inline SVG**
11+
at build time by `mermaid-cli` (Alpine Chromium + Puppeteer) via the baked-in
12+
`local-mermaid-extension.js` Asciidoctor block processor. No Kroki server,
13+
no `kroki.io`, no network — at build time *or* view time. Removes the
14+
asciidoctor-kroki 4 KB GET-URL limit that rejected large diagrams.
15+
- **Diagram caching** — content-hash, in-memory + optional on-disk
16+
(`MERMAID_CACHE_DIR`); identical diagrams render once.
17+
- **Rootless-safe** — runs under `--user $(id -u):$(id -g)` without the
18+
Chromium crashpad / cosmiconfig `EACCES` failures (`HOME=/tmp`, build-time
19+
cleanup of root-owned `/tmp` dirs).
20+
- **Build-time smoke test** — a broken image fails `docker build`, not your
21+
first render.
22+
- **Offline extras**`@antora/lunr-extension` (search), a pre-baked Antora
23+
UI bundle, and MathJax es5 for LaTeX are available in the image.
24+
- **Kroki escape hatch**`asciidoctor-kroki` is installed (unused here) for
25+
other diagram types if ever needed.
26+
- Full Alpine font set (`font-noto`, `font-noto-emoji`, `ttf-freefont`, …) so
27+
diagram labels, emoji and CJK render correctly.
28+
29+
## Files
30+
31+
| File | Purpose |
32+
|---|---|
33+
| `Dockerfile` | The consolidated image definition (build context = this dir). |
34+
| `local-mermaid-extension.js` | Offline Mermaid block processor; baked to `/opt/antora/`. |
35+
| `puppeteer-config.json` | Chromium flags for mermaid-cli; baked to `/opt/antora/`. |
36+
37+
The playbook wires the extension via
38+
`asciidoc.extensions: [ /opt/antora/local-mermaid-extension.js ]`.
39+
40+
## Usage
41+
42+
Build the image (context is this directory):
43+
44+
```bash
45+
docker build -t skainet-antora:local -f docs/.docker/Dockerfile docs/.docker/
46+
```
47+
48+
Render the site (run from the repo root; mount the repo at `/antora`, run as
49+
your user so output isn't root-owned):
50+
51+
```bash
52+
docker run --rm \
53+
--user "$(id -u):$(id -g)" \
54+
-v "$PWD:/antora" \
55+
--workdir /antora/docs \
56+
skainet-antora:local \
57+
--stacktrace antora-playbook.yml
58+
59+
# Output: docs/build/site/index.html
60+
```
61+
62+
This is exactly what `.github/workflows/docs.yml` does in CI — it builds the
63+
image from this directory and runs the container the same way.
64+
65+
Write diagrams as normal Asciidoctor blocks:
66+
67+
```adoc
68+
[mermaid]
69+
----
70+
graph TD; A-->B; B-->C;
71+
----
72+
```

docs/.docker/local-mermaid-extension.js

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,78 @@
11
'use strict'
22

33
/*
4-
* Local mermaid block processor for Asciidoctor.js.
4+
* Offline Mermaid block processor for Asciidoctor.js / Antora.
55
*
6-
* Replaces the asciidoctor-kroki dependency on kroki.io (and its
7-
* GET URL length limit / 400 rejections on large diagrams) with a
8-
* direct invocation of `mmdc` — the @mermaid-js/mermaid-cli binary
9-
* that the SKaiNET Antora Docker image already bakes in for its
10-
* Chromium-backed Puppeteer rendering path.
6+
* Replaces asciidoctor-kroki's dependency on a Kroki HTTP server
7+
* (kroki.io or a local container — GET URL length limits, 400s on
8+
* large diagrams, no offline fallback) with a direct, synchronous
9+
* invocation of `mmdc` (@mermaid-js/mermaid-cli) baked into the
10+
* markup-antora image at /opt/antora. Nothing leaves the container.
1111
*
12-
* The extension is registered via the Antora playbook's
13-
* `asciidoc.extensions` list and gets passed the Asciidoctor.js
14-
* `registry` object. For every `[mermaid]\n----\n...\n----` block
15-
* in any page, we:
12+
* For every `[mermaid]\n----\n...\n----` (or literal `....`) block:
13+
* 1. hash the source (md5) and consult the cache
14+
* 2. on miss: write source to a temp file, run mmdc to produce SVG,
15+
* read it back, store in the cache
16+
* 3. inline the SVG via a `pass` block so Asciidoctor emits raw SVG
17+
* straight into the HTML output
1618
*
17-
* 1. write the source to a temp file
18-
* 2. exec `mmdc -i in.mmd -o out.svg -p puppeteer-config.json`
19-
* (synchronous — Antora processes one page at a time and the
20-
* mermaid-cli call is fast enough that sync is fine)
21-
* 3. read the produced SVG
22-
* 4. inline it via a `pass` block so Asciidoctor emits the raw
23-
* SVG markup straight into the HTML output
19+
* Caching (merged from the Daily-StandAPP pipeline extension):
20+
* - In-memory Map, keyed by content hash, dedupes identical diagrams
21+
* across every page in a single build run.
22+
* - If MERMAID_CACHE_DIR is set, the SVG is also persisted there as
23+
* <hash>.svg, so unchanged diagrams survive across build runs
24+
* (point it at a host-mounted, writable dir to benefit).
2425
*
25-
* On render failure we fall back to a literal block containing
26-
* the original source plus the error message, matching the
27-
* degradation mode asciidoctor-kroki uses.
26+
* On render failure we degrade to a literal block containing the
27+
* original source plus the error — matching asciidoctor-kroki's mode.
28+
*
29+
* Register it from the Antora playbook:
30+
* asciidoc:
31+
* extensions:
32+
* - /opt/antora/local-mermaid-extension.js
2833
*/
2934

3035
const { execSync } = require('child_process')
31-
const { mkdtempSync, writeFileSync, readFileSync, rmSync } = require('fs')
36+
const { mkdtempSync, writeFileSync, readFileSync, rmSync,
37+
existsSync, mkdirSync } = require('fs')
3238
const { tmpdir } = require('os')
3339
const { join } = require('path')
40+
const { createHash } = require('crypto')
3441

35-
// Absolute paths baked into /opt/antora at image build time.
36-
// These have to match the Dockerfile that installs mermaid-cli and
37-
// writes the puppeteer config.
42+
// Absolute paths baked into /opt/antora at image build time. These must
43+
// match the Dockerfile that installs mermaid-cli and the puppeteer config.
3844
const MMDC_BIN = '/opt/antora/node_modules/.bin/mmdc'
3945
const PUPPETEER_CONFIG = '/opt/antora/puppeteer-config.json'
4046

47+
// Optional cross-run disk cache.
48+
const DISK_CACHE_DIR = process.env.MERMAID_CACHE_DIR || null
49+
50+
// In-process cache: hash -> svg. Dedupes within a single build run.
51+
const memCache = new Map()
52+
53+
function hashOf (source) {
54+
return createHash('md5').update(source).digest('hex').slice(0, 16)
55+
}
56+
4157
function renderMermaidToSvg (source) {
42-
const dir = mkdtempSync(join(tmpdir(), 'skainet-mm-'))
58+
const key = hashOf(source)
59+
60+
// 1. in-memory hit
61+
if (memCache.has(key)) return memCache.get(key)
62+
63+
// 2. disk hit
64+
let diskPath = null
65+
if (DISK_CACHE_DIR) {
66+
diskPath = join(DISK_CACHE_DIR, `${key}.svg`)
67+
if (existsSync(diskPath)) {
68+
const cached = readFileSync(diskPath, 'utf8')
69+
memCache.set(key, cached)
70+
return cached
71+
}
72+
}
73+
74+
// 3. miss — render with mermaid-cli
75+
const dir = mkdtempSync(join(tmpdir(), 'markup-antora-mm-'))
4376
const inputPath = join(dir, 'in.mmd')
4477
const outputPath = join(dir, 'out.svg')
4578
writeFileSync(inputPath, source, 'utf8')
@@ -48,7 +81,15 @@ function renderMermaidToSvg (source) {
4881
`${MMDC_BIN} -i ${inputPath} -o ${outputPath} -p ${PUPPETEER_CONFIG} --quiet`,
4982
{ stdio: ['ignore', 'ignore', 'pipe'] }
5083
)
51-
return readFileSync(outputPath, 'utf8')
84+
const svg = readFileSync(outputPath, 'utf8')
85+
memCache.set(key, svg)
86+
if (diskPath) {
87+
try {
88+
if (!existsSync(DISK_CACHE_DIR)) mkdirSync(DISK_CACHE_DIR, { recursive: true })
89+
writeFileSync(diskPath, svg, 'utf8')
90+
} catch (_) { /* cache is best-effort */ }
91+
}
92+
return svg
5293
} finally {
5394
try { rmSync(dir, { recursive: true, force: true }) } catch (_) { /* noop */ }
5495
}

docs/.docker/puppeteer-config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"executablePath": "/usr/bin/chromium-browser",
3+
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage"]
4+
}

0 commit comments

Comments
 (0)