@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.
- package/README.md +254 -0
- package/index.js +117 -0
- package/lib/accordion.js +243 -0
- package/lib/async-compat.js +90 -0
- package/lib/card-grid.js +327 -0
- package/lib/cta.js +334 -0
- package/lib/feature-tabs.js +400 -0
- package/lib/first-positional.js +13 -0
- package/lib/html.js +121 -0
- package/lib/kroki-config.js +191 -0
- package/lib/kroki-instance.js +89 -0
- package/lib/kroki-mermaid-theme.js +93 -0
- package/lib/kroki.js +165 -0
- package/lib/label-macro.js +69 -0
- package/lib/mono-macro.js +41 -0
- package/lib/nowrap-cols.js +61 -0
- package/lib/shiki-config.js +94 -0
- package/lib/shiki-instance.js +64 -0
- package/lib/shiki-syntax-highlighter.js +137 -0
- package/lib/table-container.js +143 -0
- package/lib/table-width.js +70 -0
- package/lib/tabs.js +263 -0
- package/lib/unique-id.js +78 -0
- package/lib/video-size.js +89 -0
- package/lib/warn.js +75 -0
- package/package.json +33 -0
|
@@ -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 & B <x>`, 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
|
+
}
|