@inditextech/docouture-asciidoc-extensions 0.1.0-SNAPSHOT.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ 'use strict'
2
+
3
+ // Shared between the two halves of GH-44's Kroki support — see
4
+ // kroki.js's own header for why there are two halves at all.
5
+ //
6
+ // THE URL IS NOT SITE-CONFIGURABLE, ON PURPOSE. Every other piece of Kroki
7
+ // config here (whether it runs at all, which diagram types) is an authored
8
+ // `asciidoc.attributes` key a site can set — but the endpoint itself is a
9
+ // single fixed value, `http://localhost:8500`, the one
10
+ // @inditextech/docouture-antora-extensions' `kroki-prewarm.js`/`kroki-docker.js`
11
+ // auto-start against, and the one baked into their bundled
12
+ // `resources/kroki-compose.yml`. That was a deliberate call (see GH-44's own
13
+ // thread): letting a site point this at an arbitrary URL turns a
14
+ // same-machine, no-network-egress diagram renderer into an arbitrary
15
+ // outbound HTTP call an author's `asciidoc.attributes` YAML can redirect —
16
+ // not a risk worth taking for a value with exactly one legitimate answer per
17
+ // environment (the loopback Kroki this repo's own tooling starts). 8500
18
+ // rather than Kroki's own factory default (8000) only to keep this
19
+ // project's fixed port away from whatever else a contributor's machine
20
+ // already has bound to 8000 — it carries no other meaning and does not need
21
+ // to match Kroki's upstream docs.
22
+ const KROKI_URL = 'http://localhost:8500'
23
+
24
+ // Every block style name this extension recognizes, and the exact path
25
+ // segment Kroki's own `/{diagramType}/svg` API expects for it — identical
26
+ // strings for every type Kroki supports, so this is also the full set of
27
+ // values `kroki-diagram-types` accepts.
28
+ //
29
+ // Curated, not Kroki's entire catalogue: each entry here is a type this
30
+ // project has actually verified end to end (see the sibling
31
+ // `kroki-prewarm.js` and `@inditextech/docouture-antora-extensions`'
32
+ // `resources/kroki-compose.yml`). Kroki supports more; add to this list —
33
+ // and to that compose file's companions, for any type that needs one,
34
+ // mermaid, bpmn and excalidraw being the three in Kroki's own catalogue
35
+ // that do (each is its own headless-Chrome/Puppeteer service, on its own
36
+ // fixed port — see that compose file's own header for the port each one
37
+ // actually listens on, verified by inspecting the running container rather
38
+ // than assumed) — before authors can rely on a new one. A site can
39
+ // customize that compose file for itself via `docouture eject kroki` without
40
+ // forking this package.
41
+ const SUPPORTED_TYPES = [
42
+ 'mermaid',
43
+ 'plantuml',
44
+ 'graphviz',
45
+ 'c4plantuml',
46
+ 'excalidraw',
47
+ 'blockdiag',
48
+ 'ditaa',
49
+ 'erd',
50
+ 'nomnoml',
51
+ 'svgbob',
52
+ 'vega',
53
+ 'vegalite',
54
+ 'wavedrom',
55
+ 'bpmn',
56
+ ]
57
+
58
+ // Kroki's own `/{type}/{format}` API rejects a format it doesn't support for
59
+ // that type with a plain 400 rather than degrading — verified against a
60
+ // live server, one `SUPPORTED_TYPES` entry at a time (posting a
61
+ // deliberately-invalid body to every type's own `/png` endpoint: a syntax
62
+ // error means the format itself was accepted, "Unsupported output format"
63
+ // means it wasn't). Five entries are SVG-only in Kroki's own catalogue:
64
+ // `excalidraw`, `nomnoml`, `svgbob`, `wavedrom`, and — despite drawing
65
+ // ordinary rectangles and circles that look no different from any other
66
+ // diagram-as-code output — `bpmn`. The other nine, listed here, all render
67
+ // PNG (and Kroki formats beyond that this package doesn't expose a way to
68
+ // request) the same way they render SVG. `format=png` on an unlisted type
69
+ // falls back to `svg` with a warning — see `resolveFormat` — the same
70
+ // degrade-not-fail posture as an unknown `kroki-diagram-types` entry.
71
+ const PNG_SUPPORTED_TYPES = new Set([
72
+ 'mermaid',
73
+ 'plantuml',
74
+ 'graphviz',
75
+ 'c4plantuml',
76
+ 'blockdiag',
77
+ 'ditaa',
78
+ 'erd',
79
+ 'vega',
80
+ 'vegalite',
81
+ ])
82
+
83
+ const DEFAULT_FORMAT = 'svg'
84
+
85
+ // The two `asciidoc.attributes` keys a site sets in its playbook. Both are
86
+ // read from the same `document` object on the synchronous (Asciidoctor)
87
+ // side, in kroki.js, and from `playbook.asciidoc.attributes` on the async
88
+ // (Antora pipeline) side, in kroki-prewarm.js — kept together here so the
89
+ // two never drift into checking different key strings.
90
+ const ENABLED_ATTR = 'kroki-enabled'
91
+ const TYPES_ATTR = 'kroki-diagram-types'
92
+
93
+ /**
94
+ * Antora/Asciidoctor attribute truthiness: an attribute given as YAML `true`
95
+ * arrives as the boolean `true`; one written `kroki-enabled: "true"` (or set
96
+ * via a document `:kroki-enabled: true` line) arrives as the string
97
+ * `'true'`. Anything else — unset, `false`, `'false'`, empty string — is
98
+ * off. There is no third state: this package has no other boolean-attribute
99
+ * precedent to match against, so the two truthy shapes actually reachable
100
+ * from YAML and from AsciiDoc source are both covered and nothing else is
101
+ * guessed at.
102
+ *
103
+ * @param {unknown} value
104
+ * @returns {boolean}
105
+ */
106
+ function isTruthy(value) {
107
+ return value === true || value === 'true'
108
+ }
109
+
110
+ /**
111
+ * Resolves which of `SUPPORTED_TYPES` are actually active, given the two
112
+ * attribute values a site may have set. Shared verbatim by both the sync
113
+ * block processor (which must not attempt a type that was never prewarmed)
114
+ * and the async prewarm listener (which must not fetch a type nobody asked
115
+ * for) — the two have to agree, or a type enabled in one place and not the
116
+ * other either wastes a network call or never gets rendered.
117
+ *
118
+ * @param {unknown} enabledAttr - raw `kroki-enabled` attribute value.
119
+ * @param {unknown} typesAttr - raw `kroki-diagram-types` attribute value;
120
+ * comma-separated, whitespace tolerated. Omitted (or empty) while enabled
121
+ * means "every supported type".
122
+ * @param {(unknown: string) => void} [onUnknownType] - called once per
123
+ * comma-separated entry that isn't in `SUPPORTED_TYPES`, so the caller can
124
+ * warn with its own node context. Not called when `typesAttr` is absent —
125
+ * omitting the attribute is the documented way to mean "all of them", not
126
+ * an authoring mistake.
127
+ * @returns {Set<string>} the active subset of `SUPPORTED_TYPES`; empty when
128
+ * `kroki-enabled` is not truthy.
129
+ */
130
+ function resolveEnabledTypes(enabledAttr, typesAttr, onUnknownType) {
131
+ if (!isTruthy(enabledAttr)) return new Set()
132
+ if (typesAttr == null || typesAttr === '') return new Set(SUPPORTED_TYPES)
133
+
134
+ const requested = String(typesAttr)
135
+ .split(',')
136
+ .map((entry) => entry.trim())
137
+ .filter(Boolean)
138
+
139
+ const active = new Set()
140
+ for (const type of requested) {
141
+ if (SUPPORTED_TYPES.includes(type)) {
142
+ active.add(type)
143
+ } else if (onUnknownType) {
144
+ onUnknownType(type)
145
+ }
146
+ }
147
+ return active
148
+ }
149
+
150
+ /**
151
+ * Resolves the actual output format to request from Kroki for one block:
152
+ * an author writes `[mermaid,format=png]`; omitted, empty, or explicitly
153
+ * `svg` all mean the existing default. Shared verbatim by the sync block
154
+ * processor (kroki.js) and the async prewarm scanner (kroki-prewarm.js) for
155
+ * the same reason `resolveEnabledTypes` is — both derive a cache key from
156
+ * this value (kroki-instance.js's `keyFor`), so a type this function
157
+ * silently downgrades in one place and not the other would look up a key
158
+ * the other side never populated.
159
+ *
160
+ * @param {string} type - a `SUPPORTED_TYPES` entry.
161
+ * @param {unknown} requestedFormat - the block's own `format` attribute,
162
+ * however Asciidoctor or the raw-text regex handed it over — anything at
163
+ * all, not just `"svg"`/`"png"`, since this is the one place both sides
164
+ * actually validate it (kroki-prewarm.js's own regex deliberately doesn't
165
+ * restrict the value it captures, for exactly this reason).
166
+ * @param {(type: string, requestedFormat: string) => void} [onUnsupported] -
167
+ * called once for any `requestedFormat` that isn't `svg`/absent and isn't
168
+ * a `png` this type's Kroki companion actually supports — an unrecognized
169
+ * value (a typo, `format=jpeg`) and a recognized-but-unsupported one
170
+ * (`format=png` on `bpmn`) both go through this the same way, so the
171
+ * caller can warn with its own node context either way.
172
+ * @returns {'svg' | 'png'}
173
+ */
174
+ function resolveFormat(type, requestedFormat, onUnsupported) {
175
+ if (requestedFormat == null || requestedFormat === '' || requestedFormat === DEFAULT_FORMAT) return DEFAULT_FORMAT
176
+ if (requestedFormat === 'png' && PNG_SUPPORTED_TYPES.has(type)) return 'png'
177
+ if (onUnsupported) onUnsupported(type, String(requestedFormat))
178
+ return DEFAULT_FORMAT
179
+ }
180
+
181
+ module.exports = {
182
+ KROKI_URL,
183
+ SUPPORTED_TYPES,
184
+ PNG_SUPPORTED_TYPES,
185
+ DEFAULT_FORMAT,
186
+ ENABLED_ATTR,
187
+ TYPES_ATTR,
188
+ isTruthy,
189
+ resolveEnabledTypes,
190
+ resolveFormat,
191
+ }
@@ -0,0 +1,89 @@
1
+ 'use strict'
2
+
3
+ const { createHash } = require('node:crypto')
4
+
5
+ // Shared cache of prewarmed Kroki renders for an entire Antora build — one
6
+ // entry per distinct (diagram type, source, output format) triple, keyed by
7
+ // a hash of all three.
8
+ //
9
+ // Same split as shiki-instance.js, for the same reason: rendering a diagram
10
+ // means an HTTP round trip to the Kroki service (see kroki-config.js's own
11
+ // header for why that's a fixed loopback URL), which is asynchronous, but
12
+ // `@asciidoctor/core ~2.2`'s Opal conversion loop that actually calls this
13
+ // package's block processor (kroki.js) is fully synchronous and cannot
14
+ // itself await anything. So all the fetching happens up front, for every
15
+ // `[mermaid]`/`[plantuml]`/etc. block the raw content aggregate contains,
16
+ // in `@inditextech/docouture-antora-extensions`' `kroki-prewarm.js` — an Antora
17
+ // PIPELINE extension hooking `contentAggregated`, well before
18
+ // `documentsConverted` ever reaches Asciidoctor. This file is the seam
19
+ // between that async producer and kroki.js's synchronous consumer, exactly
20
+ // as shiki-instance.js is for Shiki — see that file's own header for why a
21
+ // plain module-level variable (a Map, here, since there are many diagrams
22
+ // rather than one highlighter) is enough: one build per process, both
23
+ // packages `require()` the same file, node's module cache doing the rest.
24
+ const cache = new Map()
25
+
26
+ /**
27
+ * The cache key for one diagram: its block style (`mermaid`, `plantuml`, …),
28
+ * its exact source, and its output format, hashed together. Format has to
29
+ * be part of the key alongside type and source — `[mermaid]` and
30
+ * `[mermaid,format=png]` over the same source are two different Kroki
31
+ * fetches (`/mermaid/svg` vs `/mermaid/png`) and two different cached
32
+ * payloads (see `set`'s own doc), not one. The source is trimmed first so a
33
+ * trailing blank line an author left in (or Asciidoctor's own line handling
34
+ * adds) doesn't produce a cache miss for what is, semantically, the same
35
+ * diagram.
36
+ *
37
+ * Both halves of this feature call this with identical inputs: kroki.js
38
+ * with the block's own style, its reader's joined source (after
39
+ * kroki-mermaid-theme.js's own transform, for `mermaid`), and its resolved
40
+ * format; kroki-prewarm.js with the same three, its own raw-file regex
41
+ * extracted. They have to derive the same key from the same diagram, or a
42
+ * prewarmed entry is unreachable from the synchronous side.
43
+ *
44
+ * @param {string} type - a `SUPPORTED_TYPES` entry (kroki-config.js).
45
+ * @param {string} source - the raw diagram source, untrimmed is fine.
46
+ * @param {string} [format] - `resolveFormat`'s return value; defaults to
47
+ * `'svg'` so every call site written before `format` existed still keys
48
+ * the same way it always did.
49
+ * @returns {string} a hex digest, opaque beyond being a stable cache key.
50
+ */
51
+ function keyFor(type, source, format) {
52
+ return createHash('sha256')
53
+ .update(type + '\u0000' + String(source).trim() + '\u0000' + (format || 'svg'))
54
+ .digest('hex')
55
+ }
56
+
57
+ module.exports = {
58
+ keyFor,
59
+ /**
60
+ * Called once per distinct diagram by kroki-prewarm.js, after its fetch to
61
+ * Kroki resolves.
62
+ *
63
+ * @param {string} key - see `keyFor` below; the same function, called with
64
+ * the same inputs, is how kroki.js looks this back up.
65
+ * @param {{ format: 'svg' | 'png', data: string }} payload - `format` is
66
+ * `resolveFormat`'s return value for this diagram; `data` is Kroki's own
67
+ * response body — the `<svg>...</svg>` markup verbatim for `svg`, or a
68
+ * base64-encoded PNG for `png` (kroki.js embeds the latter as a `data:`
69
+ * URI — see that file's own header for why a data URI rather than a
70
+ * written-to-disk image file).
71
+ * @returns {void}
72
+ */
73
+ set(key, payload) {
74
+ cache.set(key, payload)
75
+ },
76
+
77
+ /**
78
+ * @param {string} key
79
+ * @returns {{ format: 'svg' | 'png', data: string } | undefined} the
80
+ * cached payload, or `undefined` when this diagram was never prewarmed
81
+ * — the ui-bundle preview harness (no Antora pipeline at all, so
82
+ * kroki-prewarm.js never runs), a build where `kroki-enabled` was off at
83
+ * prewarm time, or a Kroki fetch that failed. kroki.js degrades to the
84
+ * plain literal-block fallback in every one of these cases.
85
+ */
86
+ get(key) {
87
+ return cache.get(key)
88
+ },
89
+ }
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ // Mermaid is the one SUPPORTED_TYPES entry (kroki-config.js) with a
4
+ // documented, source-level theming mechanism: a `%%{init: {...}}%%`
5
+ // directive as the diagram's own first line, which Mermaid's renderer
6
+ // itself reads before laying anything out — `themeVariables` recolors its
7
+ // built-in palette, `themeCSS` is arbitrary CSS Mermaid inlines into the
8
+ // `<style>` block it already generates. Unlike the CSS-based overrides this
9
+ // package used to apply from the OUTSIDE (ui-bundle's diagram.css, fighting
10
+ // Mermaid's own `#container`-scoped rules with `!important`), this bakes
11
+ // the result INTO the SVG (or PNG — Kroki's mermaid companion renders PNG
12
+ // by literally screenshotting the same themed SVG in headless Chrome, so
13
+ // this applies identically to both) that Kroki returns. Verified against a
14
+ // live render both ways: computed `rx`/`fill` on the resulting `<rect>`
15
+ // match this file's own values, with zero page CSS involved at all.
16
+ //
17
+ // COLORS ARE HARDCODED HEX, NOT `--ids-*` TOKENS — deliberately. This runs
18
+ // server-side, at prewarm/render time, against a plain HTTP POST body; there
19
+ // is no CSS custom-property resolution available there (no page, no
20
+ // cascade, nothing). The values below are IOP DS's own light-theme
21
+ // `--ids-color-{bg,border,content}-default` (ids-tokens.css) copied
22
+ // verbatim — same exception the iop-ds-foundations skill already carves out
23
+ // for `--hljs-*`'s own syntax palette, for the same reason: a value that
24
+ // cannot be expressed as a token in the context it's actually used. DARK
25
+ // MODE IS NOT BAKED HERE: the result is always this fixed, light-theme
26
+ // palette; `ui-bundle/src/css/diagram.css`'s dark-mode `invert()` filter is
27
+ // what flips it for a reader in dark mode — same mechanism every other
28
+ // SUPPORTED_TYPES entry's own baked-in colors already rely on, since this
29
+ // is now one of them rather than a page-CSS-recolored special case.
30
+ const DEFAULT_THEME_INIT = {
31
+ theme: 'base',
32
+ themeVariables: {
33
+ primaryColor: '#ffffff',
34
+ primaryBorderColor: '#000000',
35
+ primaryTextColor: '#000000',
36
+ lineColor: '#000000',
37
+ textColor: '#000000',
38
+ },
39
+ // Selectors and colors copied from a real render's own injected <style>
40
+ // block (see diagram.css's git history for the CSS-side version this
41
+ // replaces) — not guessed at, and not Mermaid's documented API surface
42
+ // (themeCSS accepts arbitrary CSS; these particular selectors are
43
+ // specific to the `stateDiagram-v2` type this package ships as its own
44
+ // example). A different Mermaid diagram type (flowchart, sequence, ...)
45
+ // draws through entirely different classes this won't reach — same
46
+ // documented limitation the old CSS-based version had.
47
+ themeCSS:
48
+ '.node rect,.statediagram-state rect.basic,.statediagram-cluster rect{rx:0!important;ry:0!important;}' +
49
+ // `.transition` is the edge/connector PATH itself, not an arrowhead —
50
+ // it always ships its own inline `fill:none` (paths are lines, never
51
+ // filled shapes). Setting `fill` here too (as a previous version of
52
+ // this rule did) beats that inline `fill:none` (an `!important`
53
+ // stylesheet rule always wins over a plain inline declaration,
54
+ // regardless of specificity) and silently turns any edge whose path
55
+ // happens to double back on itself — e.g. a self-loop like `G --> G`
56
+ // in a stateDiagram — into a solid black blob, since the now-closed-ish
57
+ // curve gets a fill. `stroke` is the only channel `.transition` should
58
+ // ever touch; arrowheads are separate elements matched below.
59
+ '.transition{stroke:#000000!important;}' +
60
+ '.marker,.node circle.state-start,[id$="-barbEnd"]{stroke:#000000!important;fill:#000000!important;}' +
61
+ '.node circle.state-end{fill:#000000!important;stroke:#ffffff!important;}' +
62
+ '.nodeLabel,.edgeLabel,.stateLabel,.cluster-label{color:#000000!important;}',
63
+ }
64
+
65
+ const INIT_DIRECTIVE = '%%{init: ' + JSON.stringify(DEFAULT_THEME_INIT) + ' }%%\n'
66
+
67
+ /**
68
+ * Prepends this package's own default Mermaid theme to `source`, UNLESS
69
+ * the author already opened their diagram with their own `%%{init...}%%`
70
+ * directive — Mermaid only ever honors the first one, so prepending ours
71
+ * ahead of an author-supplied one would silently discard whatever theme (or
72
+ * other init-only setting) they actually asked for. An author who wants
73
+ * this package's own light-touch styling untouched writes a plain
74
+ * `stateDiagram-v2`/`flowchart`/... block, same as before this existed; one
75
+ * who wants their own look opts out simply by writing `%%{init...}%%`
76
+ * themselves — no separate attribute needed to turn this off.
77
+ *
78
+ * Both halves of this feature (kroki.js, synchronous; kroki-prewarm.js,
79
+ * async) must call this with the SAME input before either computing a
80
+ * cache key or sending anything to Kroki — the whole point of a shared,
81
+ * pure function rather than two independent copies.
82
+ *
83
+ * @param {string} source - the diagram's raw literal-block content, as
84
+ * Asciidoctor's reader or kroki-prewarm.js's own regex handed it over.
85
+ * @returns {string}
86
+ */
87
+ function applyDefaultMermaidTheme(source) {
88
+ const text = String(source)
89
+ if (text.trimStart().startsWith('%%{init')) return text
90
+ return INIT_DIRECTIVE + text
91
+ }
92
+
93
+ module.exports = { applyDefaultMermaidTheme, DEFAULT_THEME_INIT }
package/lib/kroki.js ADDED
@@ -0,0 +1,165 @@
1
+ 'use strict'
2
+
3
+ const { escapeHtml } = require('./html')
4
+ const warn = require('./warn')
5
+ const kroki = require('./kroki-instance')
6
+ const {
7
+ SUPPORTED_TYPES,
8
+ PNG_SUPPORTED_TYPES,
9
+ ENABLED_ATTR,
10
+ TYPES_ATTR,
11
+ resolveEnabledTypes,
12
+ resolveFormat,
13
+ } = require('./kroki-config')
14
+ const { applyDefaultMermaidTheme } = require('./kroki-mermaid-theme')
15
+
16
+ // GH-44: renders diagram source — Mermaid, PlantUML, GraphViz, … (see
17
+ // kroki-config.js's `SUPPORTED_TYPES`) — as an actual diagram, via a
18
+ // self-hosted Kroki (https://kroki.io) service, instead of showing the raw
19
+ // source as literal text.
20
+ //
21
+ // SYNTAX
22
+ //
23
+ // [mermaid]
24
+ // ....
25
+ // stateDiagram-v2
26
+ // [*] --> Started
27
+ // ....
28
+ //
29
+ // A LITERAL block (four dots, not four hyphens — a `[mermaid]` block on
30
+ // four HYPHENS is a listing block, a different context, and is not
31
+ // intercepted here), styled with one of `SUPPORTED_TYPES`. This is exactly
32
+ // the shape `tools/fumadocs-migrate/lib/emit.mjs` already emits for every
33
+ // migrated `<Mermaid chart={...}>` — this extension is what turns that
34
+ // previously-inert shape into a real diagram, nothing upstream of it
35
+ // changes.
36
+ //
37
+ // `[mermaid,format=png]` renders a transparent PNG instead of inline SVG —
38
+ // see kroki-config.js's `PNG_SUPPORTED_TYPES` for which of `SUPPORTED_TYPES`
39
+ // actually support it (Kroki's own `/{type}/png` rejects the rest with a
40
+ // plain 400); anything else falls back to `svg` with a warning, the same
41
+ // degrade-not-fail posture as an unsupported `kroki-diagram-types` entry.
42
+ //
43
+ // OPT IN, PER SITE, DISABLED BY DEFAULT
44
+ //
45
+ // Every other extension in this package is unconditionally active once the
46
+ // package is listed (see README.md's "the whole set"). This one is not: a
47
+ // site sets two `asciidoc.attributes` in its playbook —
48
+ //
49
+ // asciidoc:
50
+ // attributes:
51
+ // kroki-enabled: true
52
+ // kroki-diagram-types: mermaid,plantuml # optional; omitted = every SUPPORTED_TYPES entry
53
+ //
54
+ // — because rendering a diagram means depending on a Kroki service actually
55
+ // running at the fixed local URL kroki-config.js hardcodes (see that file's
56
+ // own header for why the URL itself is NOT one of these attributes), and a
57
+ // site that hasn't set that up should get today's plain, always-worked
58
+ // literal-text rendering, not a build that silently produces broken
59
+ // diagrams. `kroki-diagram-types` lets a site narrow which of
60
+ // `SUPPORTED_TYPES` it actually wants — most sites need only `mermaid`, and
61
+ // each additional type may need its own companion container (see the
62
+ // sibling @inditextech/docouture-antora-extensions package's
63
+ // resources/kroki-compose.yml, which kroki-prewarm.js starts automatically —
64
+ // no manual setup required).
65
+ //
66
+ // WHY THIS BLOCK PROCESSOR NEVER MAKES THE HTTP CALL ITSELF
67
+ //
68
+ // Rendering via Kroki means an HTTP round trip — inherently asynchronous —
69
+ // but `@asciidoctor/core ~2.2`'s Opal conversion loop, which is what
70
+ // actually calls this file's `process()` during a real site build, is fully
71
+ // synchronous (see async-compat.js's own header: a process function cannot
72
+ // simply be `async`, that renders the literal text "[object Promise]").
73
+ // Unlike card-grid.js's or accordion.js's own async work — `parseContent`,
74
+ // which 4.0 makes Promise-returning but 2.2 keeps synchronous — a network
75
+ // call has no synchronous form under EITHER major. So none happens here.
76
+ // All of it happens up front, for the whole build, in
77
+ // `@inditextech/docouture-antora-extensions`' `kroki-prewarm.js` — an Antora
78
+ // PIPELINE extension (`antora.extensions`, not this package's
79
+ // `asciidoc.extensions`) that scans the raw content aggregate for these
80
+ // blocks and fetches every one from Kroki BEFORE Asciidoctor conversion
81
+ // starts. This file only ever reads the result back out of the shared cache
82
+ // (`kroki-instance.js`) — the same seam shiki-syntax-highlighter.js uses for
83
+ // Shiki, and for the identical reason; see that file's own header.
84
+ //
85
+ // DEGRADATION
86
+ //
87
+ // Not enabled, type not requested, or a cache miss (prewarm never ran, or
88
+ // its fetch for this exact diagram failed) all degrade to the same output:
89
+ // the plain `<div class="literalblock">` markup Asciidoctor's own built-in
90
+ // literal-block converter would have produced — i.e. exactly what every one
91
+ // of these blocks already rendered as before this extension existed. A
92
+ // missing/unreachable Kroki service is therefore never a build failure;
93
+ // only a genuine authoring mistake (an unknown entry in
94
+ // `kroki-diagram-types`) is, via `warn()`, per this package's own
95
+ // "Reporting authoring mistakes" convention.
96
+
97
+ /** What Asciidoctor's own literal-block HTML5 converter emits — reproduced
98
+ * exactly so a disabled/unavailable Kroki changes nothing an author or a
99
+ * reader would notice. */
100
+ function literalFallback(source) {
101
+ return '<div class="literalblock"><div class="content"><pre>' + escapeHtml(source) + '</pre></div></div>'
102
+ }
103
+
104
+ function renderDiagram(parent, type, source, requestedFormat) {
105
+ const document = parent.getDocument()
106
+ const enabledAttr = document.getAttribute(ENABLED_ATTR)
107
+ const typesAttr = document.getAttribute(TYPES_ATTR)
108
+ const enabledTypes = resolveEnabledTypes(enabledAttr, typesAttr, (unknown) =>
109
+ warn(
110
+ parent,
111
+ '[' + TYPES_ATTR + '=' + typesAttr + ']',
112
+ 'unknown Kroki diagram type "' + unknown + '"',
113
+ SUPPORTED_TYPES
114
+ )
115
+ )
116
+ if (!enabledTypes.has(type)) return literalFallback(source)
117
+
118
+ const format = resolveFormat(type, requestedFormat, (t, requested) =>
119
+ warn(
120
+ parent,
121
+ '[' + t + ',format=' + requested + ']',
122
+ 'unsupported format "' + requested + '" for a ' + t + ' diagram — falling back to svg',
123
+ PNG_SUPPORTED_TYPES.has(t) ? ['svg', 'png'] : ['svg']
124
+ )
125
+ )
126
+ // Mermaid gets its own default theme baked in server-side (see that
127
+ // file's own header for why this happens here rather than via ui-bundle
128
+ // CSS) — applied to the LOOKUP key only; `literalFallback` below still
129
+ // shows the author's own original source, untouched, on a cache miss.
130
+ const effectiveSource = type === 'mermaid' ? applyDefaultMermaidTheme(source) : source
131
+
132
+ const payload = kroki.get(kroki.keyFor(type, effectiveSource, format))
133
+ if (!payload) {
134
+ warn(
135
+ parent,
136
+ '[' + type + ']',
137
+ 'no prewarmed Kroki render for this diagram (service unreachable at build time, or the source changed after prewarm ran); showing raw source instead'
138
+ )
139
+ return literalFallback(source)
140
+ }
141
+ const body = payload.format === 'png' ? '<img src="data:image/png;base64,' + payload.data + '" alt="">' : payload.data
142
+ return '<div class="docouture-diagram" data-diagram-type="' + type + '">' + body + '</div>'
143
+ }
144
+
145
+ function krokiBlock(type) {
146
+ return function () {
147
+ this.named(type)
148
+ this.onContext('literal')
149
+ this.process((parent, reader, attrs) => {
150
+ // See card-grid.js's own comment: Opal (2.2) can hand this a bare JS
151
+ // `null` for a block with no attributes beyond its style.
152
+ attrs = attrs || {}
153
+ const source = reader.getLines().join('\n')
154
+ const html = renderDiagram(parent, type, source, attrs.format)
155
+ return this.createBlock(parent, 'pass', html, attrs)
156
+ })
157
+ }
158
+ }
159
+
160
+ module.exports = function registerKroki(registry) {
161
+ for (const type of SUPPORTED_TYPES) {
162
+ registry.block(krokiBlock(type))
163
+ }
164
+ }
165
+ module.exports.krokiBlock = krokiBlock
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const firstPositional = require('./first-positional')
4
+ const warn = require('./warn')
5
+
6
+ // IDS Label variants shipped in `label.css` (@inditex/sewingiopdsweb-react-
7
+ // components), already vendored into ui-bundle's ids-components.css — see
8
+ // .opencode/skills/iop-ds-components. Anything outside this list is an
9
+ // authoring mistake, not a themeable extension point, so it fails the build
10
+ // (see the logger call below) instead of silently rendering unstyled.
11
+ const VARIANTS = ['white', 'grey', 'red', 'orange', 'green', 'blue', 'purple', 'pink', 'teal']
12
+
13
+ // The Datagrid cell's own default (Figma 2735:55639 light / 2735:56411
14
+ // dark) — the one variant a table cell actually uses. Every other variant
15
+ // exists so `label:` is useful outside a table too.
16
+ const DEFAULT_VARIANT = 'grey'
17
+
18
+ // Asciidoctor's built-in "long format" inline macro regex requires a
19
+ // non-empty target (`\S+?` between `:` and `[`) — verified empirically
20
+ // against both Asciidoctor versions this repo runs (2.2 for site builds,
21
+ // 4.0 for the ui-bundle preview harness, see reference/extensions.md): a
22
+ // bare `label:[String]` with the colour omitted never reaches `process`
23
+ // at all under the default regex, it renders as literal text. Overriding
24
+ // the match regexp (`self.match`, the DSL's documented escape hatch for
25
+ // this) with our own — target group `([a-z]*)`, zero or more — is what
26
+ // makes the empty-colour form work.
27
+ const MACRO_RX = /\blabel:([a-z]*)\[((?:\\\]|[^\]])*?)\]/
28
+
29
+ /**
30
+ * `label:red[Blocked]` / `label:[String]` (colour omitted → grey, the
31
+ * table default) →
32
+ *
33
+ * <span class="ids-label ids-label--grey">
34
+ * <span class="ids-label__content">String</span>
35
+ * </span>
36
+ *
37
+ * Exactly the DS's own BEM markup (label/label.css) — every colour, every
38
+ * theme, comes free from the component already vendored into
39
+ * ids-components.css. This macro emits markup only, no styling of its own.
40
+ */
41
+ module.exports = function registerLabelMacro(registry) {
42
+ registry.inlineMacro('label', function () {
43
+ // `self` is captured so `self.createInline(...)` below can still reach
44
+ // the processor instance from inside the nested `process` callback,
45
+ // where `this` is rebound to the macro's runtime context instead —
46
+ // Asciidoctor's own extension DSL idiom (see extend/extensions/
47
+ // inline-macro-processor/), not an avoidable local alias.
48
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
49
+ const self = this
50
+ self.match(MACRO_RX)
51
+ self.process(function (parent, target, attrs) {
52
+ const variant = target || DEFAULT_VARIANT
53
+ if (VARIANTS.indexOf(variant) === -1) {
54
+ warn(parent, 'label:' + target + '[]', 'unknown IDS Label variant "' + variant + '"', VARIANTS)
55
+ }
56
+ // The bracket content has already been through Asciidoctor's own
57
+ // `specialcharacters` substitution by the time macros run (verified:
58
+ // `label:[A & B <x>]` arrives here as `A &amp; B &lt;x&gt;`, both
59
+ // Asciidoctor versions), so it is already safe to place inside HTML
60
+ // without a second escaping pass. See lib/html.js's own header for the
61
+ // full inline-vs-block table this is one row of — an inline macro's
62
+ // attributes are substituted, a BLOCK's are not.
63
+ const text = firstPositional(attrs) || ''
64
+ const html =
65
+ '<span class="ids-label ids-label--' + variant + '"><span class="ids-label__content">' + text + '</span></span>'
66
+ return self.createInline(parent, 'quoted', html)
67
+ })
68
+ })
69
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const firstPositional = require('./first-positional')
4
+
5
+ // No variant, no target — `mono:[text]` always renders the same way, so
6
+ // unlike `label:` there is nothing to capture before the bracket. The
7
+ // regex still needs two groups (an empty, unused one for "target") because
8
+ // Asciidoctor 4.0's inline-macro engine (the ui-bundle preview harness)
9
+ // unconditionally tries to parse a second group as an attribute list and
10
+ // throws if it's missing — verified empirically; 2.2 (site builds) doesn't
11
+ // care either way, so the two-group shape is what's portable.
12
+ const MACRO_RX = /\bmono:()\[((?:\\\]|[^\]])*?)\]/
13
+
14
+ /**
15
+ * `mono:[className]` →
16
+ *
17
+ * <code class="ids-mono">className</code>
18
+ *
19
+ * Plain monospaced text with none of the GH-12 inline-code chip
20
+ * (background, padding) — for a property/table cell whose ENTIRE content
21
+ * is a name or token (Figma 2735:55589's `className` column) where the
22
+ * chip would otherwise apply to literally every cell and read as noise.
23
+ * `` `backtick code` `` keeps its chip; this is the deliberate opt-out.
24
+ * The `.ids-mono` class is doc.css's own — nothing in the DS itself models
25
+ * "code with no surrounding treatment", so this one rule is ours to own,
26
+ * not a component skipped.
27
+ */
28
+ module.exports = function registerMonoMacro(registry) {
29
+ registry.inlineMacro('mono', function () {
30
+ // See label-macro.js's own comment on this same pattern.
31
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
32
+ const self = this
33
+ self.match(MACRO_RX)
34
+ self.process(function (parent, target, attrs) {
35
+ // Already specialcharacters-substituted by the time macros run
36
+ // (verified, same as label-macro.js) — safe to place directly.
37
+ const text = firstPositional(attrs) || ''
38
+ return self.createInline(parent, 'quoted', '<code class="ids-mono">' + text + '</code>')
39
+ })
40
+ })
41
+ }