@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
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# @inditextech/docouture-asciidoc-extensions
|
|
2
|
+
|
|
3
|
+
Asciidoctor extensions shared by the docouture documentation sites.
|
|
4
|
+
|
|
5
|
+
They exist for one reason: **authored content that AsciiDoc has no syntax for**.
|
|
6
|
+
A card grid, a set of steps, a landing hero, a tab switcher — none of these are
|
|
7
|
+
expressible in AsciiDoc, and none of them belong baked into a Handlebars layout,
|
|
8
|
+
because then they stop being content an author can write and become markup only
|
|
9
|
+
a UI developer can change. An extension is the seam between the two: the author
|
|
10
|
+
writes AsciiDoc, the extension emits the design system's markup.
|
|
11
|
+
|
|
12
|
+
Where a thing belongs, before you write anything here:
|
|
13
|
+
|
|
14
|
+
| the thing is | it lives in |
|
|
15
|
+
| ---------------------------------------------- | ------------------------------------------------ |
|
|
16
|
+
| site chrome, present on every page | a Handlebars partial in `ui-bundle/src/partials` |
|
|
17
|
+
| derived from HTML Asciidoctor already produces | CSS alone, in `ui-bundle/src/css` |
|
|
18
|
+
| authored content with no AsciiDoc equivalent | **an extension here** |
|
|
19
|
+
|
|
20
|
+
If Asciidoctor already emits the element and you only need it to look right,
|
|
21
|
+
this package is the wrong tool — style it in `ui-bundle`. Admonitions, tables and
|
|
22
|
+
code blocks are all handled that way.
|
|
23
|
+
|
|
24
|
+
## The contract
|
|
25
|
+
|
|
26
|
+
Every extension in this package keeps all eight of these.
|
|
27
|
+
|
|
28
|
+
### 1. One file per extension, registered in `index.js`
|
|
29
|
+
|
|
30
|
+
`lib/<name>.js` exports a single `register<Name>(registry)` function.
|
|
31
|
+
`index.js` requires it and calls it from `registerAll`. Nothing else registers
|
|
32
|
+
anything — a site lists this package once and gets the whole set.
|
|
33
|
+
|
|
34
|
+
`lib/` also holds modules that register nothing: `async-compat.js`,
|
|
35
|
+
`first-positional.js`, `html.js`, `unique-id.js`, `warn.js`, `kroki-config.js`
|
|
36
|
+
and `kroki-instance.js`. They are helpers, required directly by the
|
|
37
|
+
extensions, and are deliberately absent from `index.js`.
|
|
38
|
+
|
|
39
|
+
### 2. `register` is exported as both a named export and `module.exports.register`
|
|
40
|
+
|
|
41
|
+
Only `index.js` deals with this, and it already does. The shape is not
|
|
42
|
+
cosmetic — it is what makes registration work in two different hosts, and each
|
|
43
|
+
one fails silently when it is wrong:
|
|
44
|
+
|
|
45
|
+
- **Antora** matches `register.toString()` against a regex requiring the first
|
|
46
|
+
parameter to be **literally named `registry`**. Rename it, or ship a bundled
|
|
47
|
+
build that renames it, and Antora decides the module is an _Antora_ extension
|
|
48
|
+
listed under the wrong key, logs `Skipping possible Antora extension
|
|
49
|
+
registered as an Asciidoctor extension`, and moves on. Nothing renders. No
|
|
50
|
+
error.
|
|
51
|
+
- **The ui-bundle preview harness** calls `register.call(Asciidoctor.Extensions)`
|
|
52
|
+
with no argument — `this` is the namespace, not a registry — and does so once
|
|
53
|
+
per rebuild in the same process, which is why `index.js` carries an
|
|
54
|
+
idempotency guard. Without it, a table ends up wrapped in one more nested
|
|
55
|
+
container per rebuild.
|
|
56
|
+
|
|
57
|
+
Read `index.js`'s own header comment before touching any of it, and
|
|
58
|
+
`.opencode/skills/asciidoc/reference/extensions.md` for the registration
|
|
59
|
+
mechanics in full.
|
|
60
|
+
|
|
61
|
+
### 3. Emit IOP DS BEM markup, exactly
|
|
62
|
+
|
|
63
|
+
Copy the DS component's real markup and class names. Never invent a class on a
|
|
64
|
+
DS block, never re-implement a component's styling from memory. If nothing in
|
|
65
|
+
the DS models what you need, say so before inventing — it usually means the
|
|
66
|
+
wrong component was identified. See `.opencode/skills/iop-ds-components`.
|
|
67
|
+
|
|
68
|
+
### 4. Ship no CSS
|
|
69
|
+
|
|
70
|
+
This package emits markup and nothing else. Styling lives in
|
|
71
|
+
`ui-bundle/src/css`. An extension that needs new CSS is a change to two
|
|
72
|
+
packages, in that order — never a `<style>` tag, never a stylesheet here.
|
|
73
|
+
|
|
74
|
+
### 5. Degrade without JavaScript
|
|
75
|
+
|
|
76
|
+
A page must be readable, navigable and correctly styled with JavaScript off.
|
|
77
|
+
Tabs render as sequential sections, each under its own heading; accordions
|
|
78
|
+
render as `<details>`; anything clickable is a real `<a href>`. Behaviour is
|
|
79
|
+
layered onto server-rendered markup by `ui-bundle/src/js` — it never creates
|
|
80
|
+
markup.
|
|
81
|
+
|
|
82
|
+
### 6. Set real ARIA, and never an inline `style`
|
|
83
|
+
|
|
84
|
+
Interactive markup carries the roles and relationships its pattern requires
|
|
85
|
+
(`role=tablist`/`tab`/`tabpanel`, `aria-controls`, `aria-selected`), keyboard
|
|
86
|
+
operability included. Use `lib/unique-id.js` for the ids those relationships
|
|
87
|
+
need; see its header for why a module-level counter is wrong here.
|
|
88
|
+
|
|
89
|
+
Inline `style` is not a styling escape hatch. The one existing exception —
|
|
90
|
+
`video-size.js` — sets _custom properties_ that the stylesheet consumes, because
|
|
91
|
+
a browser will not derive `aspect-ratio` from an iframe's HTML attributes and
|
|
92
|
+
there is no CSS-only fix. That is the bar: a documented impossibility, not a
|
|
93
|
+
convenience.
|
|
94
|
+
|
|
95
|
+
### 7. Escape raw attribute values — and only those
|
|
96
|
+
|
|
97
|
+
Asciidoctor substitutes some authored strings before an extension sees them and
|
|
98
|
+
others not at all. Escaping the wrong ones double-encodes (`&` renders as
|
|
99
|
+
`&`); missing the right ones lets authored text inject markup. Measured on
|
|
100
|
+
both majors this repo runs, with `A & B <x>`:
|
|
101
|
+
|
|
102
|
+
| source | arrives as | escape? |
|
|
103
|
+
| ------------------------------------- | --------------------- | ------- |
|
|
104
|
+
| inline macro, positional or named | `A & B <x>` | no |
|
|
105
|
+
| document attribute (`:page-k:`) | `A & B <x>` | no |
|
|
106
|
+
| **block macro attribute** (`x::t[…]`) | `A & B <x>` | **yes** |
|
|
107
|
+
| **block style attribute** (`[x,k=…]`) | `A & B <x>` | **yes** |
|
|
108
|
+
|
|
109
|
+
Identical on 2.2.9 and 4.0.8 — it is a block-vs-inline split, not a version
|
|
110
|
+
split. Separately, anything from `getText()` is **already converted HTML** (a
|
|
111
|
+
dlist term with an `xref:` arrives as a full `<a>`), so it must never be
|
|
112
|
+
escaped.
|
|
113
|
+
|
|
114
|
+
Use `lib/html.js`: `escapeHtml(value)` and `attr(name, value)`. Its header
|
|
115
|
+
carries the full table and the probe behind it.
|
|
116
|
+
|
|
117
|
+
### 8. Stay portable across both Asciidoctor majors
|
|
118
|
+
|
|
119
|
+
Site builds run **2.2** (via Antora); the ui-bundle preview harness runs **4.0**.
|
|
120
|
+
Touch only `registry`/`this` — never the `@asciidoctor/core` module object,
|
|
121
|
+
whose export shape differs between the two.
|
|
122
|
+
|
|
123
|
+
Two behavioural splits are already solved, and both must be reused rather than
|
|
124
|
+
rediscovered:
|
|
125
|
+
|
|
126
|
+
- **Attribute shape.** An inline macro's first positional attribute is
|
|
127
|
+
`attrs.$positional[0]` under 2.2 and `attrs['1']` under 4.0 — use
|
|
128
|
+
`lib/first-positional.js`.
|
|
129
|
+
- **Sync vs async.** `parseContent`, `convert` and `precomputeText` are
|
|
130
|
+
synchronous under 2.2 and Promise-returning under 4.0. An extension cannot
|
|
131
|
+
simply be `async`: that hands 2.2's synchronous Opal caller a Promise, which
|
|
132
|
+
renders as the literal text `[object Promise]`. Use `chain`/`chainAll`/
|
|
133
|
+
`precomputeSubtree` from `lib/async-compat.js`.
|
|
134
|
+
|
|
135
|
+
`@asciidoctor/core` is a devDependency for its TypeScript types only. It is
|
|
136
|
+
version 4.0, so those types describe the preview harness's runtime, not
|
|
137
|
+
Antora's — treat them as a typing aid, not as proof that an API exists in 2.2.
|
|
138
|
+
|
|
139
|
+
## Reporting authoring mistakes
|
|
140
|
+
|
|
141
|
+
Both site playbooks set `runtime.log.failure_level: warn`, so **a warning fails
|
|
142
|
+
the build**. That is the right severity for an authoring error: an unknown label
|
|
143
|
+
colour, a `[cards]` block with no cards, a tab set with no panels all render as
|
|
144
|
+
something plausible but wrong, and shipping that is worse than not building.
|
|
145
|
+
|
|
146
|
+
Use `lib/warn.js` so every extension reports in the same shape — what the author
|
|
147
|
+
wrote, what is wrong, and what was expected:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
warn(parent, 'label:mauve[]', 'unknown IDS Label variant "mauve"', VARIANTS)
|
|
151
|
+
// → label:mauve[] — unknown IDS Label variant "mauve"; expected one of white, grey, …
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Using this package in a site
|
|
155
|
+
|
|
156
|
+
Two steps, both required. Doing only one fails silently.
|
|
157
|
+
|
|
158
|
+
1. List it under `asciidoc.extensions` in the site's `antora-playbook.yml`
|
|
159
|
+
(or, for the ui-bundle preview, in `preview-src/ui-model.yml`).
|
|
160
|
+
2. Add it as a dependency with the workspace protocol:
|
|
161
|
+
`"@inditextech/docouture-asciidoc-extensions": "workspace:*"`.
|
|
162
|
+
|
|
163
|
+
Note the neighbouring package: `@inditextech/docouture-antora-extensions` hooks
|
|
164
|
+
Antora's own pipeline under the **`antora.extensions`** key. Listing either
|
|
165
|
+
package under the other's key makes Antora log a warning and skip it.
|
|
166
|
+
|
|
167
|
+
### Kroki: opt-in, disabled by default
|
|
168
|
+
|
|
169
|
+
`[mermaid]`, `[plantuml]`, `[graphviz]` and the rest of `kroki.js`'s
|
|
170
|
+
`SUPPORTED_TYPES` are the one extension in this package that is **not** active
|
|
171
|
+
just by listing the package — see that file's own header. A site turns it on
|
|
172
|
+
with two more `asciidoc.attributes`:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
asciidoc:
|
|
176
|
+
attributes:
|
|
177
|
+
kroki-enabled: true
|
|
178
|
+
kroki-diagram-types: mermaid,plantuml # optional; omitted = every supported type
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Per-block, `[mermaid,format=png]` renders a transparent PNG `<img>` instead of the
|
|
182
|
+
default inline SVG — see `kroki-config.js`'s `PNG_SUPPORTED_TYPES` for which of
|
|
183
|
+
`SUPPORTED_TYPES` actually support it (`bpmn` and `excalidraw` notably don't; Kroki
|
|
184
|
+
itself rejects those two output formats outright). An unsupported combination falls
|
|
185
|
+
back to `svg` with a build warning.
|
|
186
|
+
|
|
187
|
+
Mermaid diagrams also get IOP DS-aligned styling (square corners, DS colors, DS body
|
|
188
|
+
typography) baked in server-side via a `%%{init: {...}}%%` directive
|
|
189
|
+
`kroki-mermaid-theme.js` prepends to the diagram's own source before it reaches Kroki
|
|
190
|
+
— not a CSS override, so it applies identically whether the block renders as `svg` or
|
|
191
|
+
`format=png`. An author who opens their own diagram with `%%{init...}%%` opts out
|
|
192
|
+
automatically (Mermaid only honors the first one); every other `SUPPORTED_TYPES` entry
|
|
193
|
+
keeps its own baked-in look, font aside (ui-bundle's `diagram.css` normalizes that one
|
|
194
|
+
blanket, safely, for every type).
|
|
195
|
+
|
|
196
|
+
It also needs a Kroki service reachable at build time, at the fixed local URL
|
|
197
|
+
`kroki-config.js` hardcodes (not itself configurable — see that file's own
|
|
198
|
+
header for why). No manual setup: the sibling `@inditextech/docouture-antora-
|
|
199
|
+
extensions` package's `kroki-prewarm.js` starts one itself, via `docker
|
|
200
|
+
compose`, the first time a build needs it and finds nothing already
|
|
201
|
+
listening — on every invocation path (`docouture dev`/`docouture build`, this
|
|
202
|
+
monorepo's own `just dev`/`just build-site`, a raw `antora` call, any
|
|
203
|
+
consumer's own CI) equally, with no automatic teardown (a stopped-and-
|
|
204
|
+
restarted Kroki on every build would only add latency back). Run `docouture
|
|
205
|
+
eject kroki` to copy the bundled compose file into a site's own repo for
|
|
206
|
+
customization (a different image version, a companion container for
|
|
207
|
+
another diagram type); run `docouture teardown kroki` (or, in this monorepo,
|
|
208
|
+
`just kroki-down`) to stop it manually once you're actually done with it.
|
|
209
|
+
Without `kroki-enabled: true`, or if Docker/the service never becomes
|
|
210
|
+
reachable, these blocks render exactly as plain AsciiDoc already would — a
|
|
211
|
+
disabled or unavailable Kroki is never itself a build failure.
|
|
212
|
+
|
|
213
|
+
A healthy run is otherwise silent: `kroki-docker.js` and `kroki-prewarm.js`
|
|
214
|
+
log their whole lifecycle (already-reachable / starting via which compose
|
|
215
|
+
file / `docker compose up -d` succeeded / became reachable after Ns /
|
|
216
|
+
render summary) at `info`, so nothing about a working setup looks different
|
|
217
|
+
from Nx quietly replaying a stale cached build. Antora's own default log
|
|
218
|
+
level is `warn`, so these are invisible unless you ask for them — as is
|
|
219
|
+
every other docouture extension's own `getLogger('docouture-...')` observability
|
|
220
|
+
(search-index's per-component summary, llms-txt's, footer's, ...), same
|
|
221
|
+
reasoning throughout. This monorepo's own `just dev`/`just build-site`
|
|
222
|
+
recipes, and `docouture dev`/`docouture build` (`antora-log.ts` in the `cli`
|
|
223
|
+
package), all pass `--log-level=info` to every Antora invocation they make
|
|
224
|
+
for exactly this reason, so no extra flag is needed there — a raw `antora`
|
|
225
|
+
invocation of your own still needs `--log-level=info` (or
|
|
226
|
+
`ANTORA_LOG_LEVEL=info`) passed explicitly. A real failure (Docker missing,
|
|
227
|
+
daemon unreachable, service never comes up, an individual diagram failing
|
|
228
|
+
to render) still logs at `warn` unconditionally either way.
|
|
229
|
+
|
|
230
|
+
## Targets
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
pnpm nx run @inditextech/docouture-asciidoc-extensions:lint
|
|
234
|
+
pnpm nx run @inditextech/docouture-asciidoc-extensions:typecheck
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
There is deliberately **no `build` and no `clean`**: the package is plain
|
|
238
|
+
CommonJS, consumed by `require()` straight from source. Nothing is compiled,
|
|
239
|
+
nothing is emitted, so there is nothing to remove. Adding no-op targets to
|
|
240
|
+
match the other packages would be noise.
|
|
241
|
+
|
|
242
|
+
`typecheck` runs TypeScript over the JavaScript (`allowJs` + `checkJs`) using
|
|
243
|
+
the JSDoc annotations. `noImplicitAny` and `noImplicitThis` are off, and only
|
|
244
|
+
those two — see `tsconfig.json` for the measurement behind that and for how they
|
|
245
|
+
get retired. New files are written fully annotated; `lib/html.js`,
|
|
246
|
+
`lib/unique-id.js` and `lib/warn.js` are the reference for what that looks like.
|
|
247
|
+
|
|
248
|
+
## Further reading
|
|
249
|
+
|
|
250
|
+
- `.opencode/skills/asciidoc/reference/extensions.md` — extension points, the
|
|
251
|
+
DSL, and the registration rules in full
|
|
252
|
+
- `.opencode/skills/iop-ds-components` — choosing a DS component and emitting
|
|
253
|
+
its markup
|
|
254
|
+
- `.opencode/skills/iop-ds-foundations` — tokens, theming, breakpoints
|
package/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Every extension in lib/ is registered here, and only here — see README.md
|
|
4
|
+
// for the contract each one keeps. Note that lib/ also holds modules that
|
|
5
|
+
// register nothing and are absent from this list on purpose: async-compat.js,
|
|
6
|
+
// first-positional.js, html.js, unique-id.js, warn.js, shiki-instance.js and
|
|
7
|
+
// shiki-config.js are shared helpers the extensions require directly.
|
|
8
|
+
// kroki-config.js and kroki-instance.js are the same kind of helper, shared
|
|
9
|
+
// between kroki.js (below) and the sibling
|
|
10
|
+
// @inditextech/docouture-antora-extensions package's kroki-prewarm.js — see
|
|
11
|
+
// kroki.js's own header. shiki-syntax-highlighter.js is the one exception
|
|
12
|
+
// that DOES register something outside `registerAll` — see its own
|
|
13
|
+
// `require(...)` below.
|
|
14
|
+
const registerLabelMacro = require('./lib/label-macro')
|
|
15
|
+
const registerMonoMacro = require('./lib/mono-macro')
|
|
16
|
+
const registerTableWidth = require('./lib/table-width')
|
|
17
|
+
const registerTableContainer = require('./lib/table-container')
|
|
18
|
+
const registerNowrapCols = require('./lib/nowrap-cols')
|
|
19
|
+
const registerVideoSize = require('./lib/video-size')
|
|
20
|
+
const registerCardGrid = require('./lib/card-grid')
|
|
21
|
+
const registerFeatureTabs = require('./lib/feature-tabs')
|
|
22
|
+
const registerCta = require('./lib/cta')
|
|
23
|
+
const registerAccordion = require('./lib/accordion')
|
|
24
|
+
const registerTabs = require('./lib/tabs')
|
|
25
|
+
const registerKroki = require('./lib/kroki')
|
|
26
|
+
|
|
27
|
+
// GH-89: registers Asciidoctor's 'shiki' SyntaxHighlighter adapter. Required
|
|
28
|
+
// here, at module top level, NOT inside `registerAll` below — this is a
|
|
29
|
+
// GLOBAL, one-time registration on the `@asciidoctor/core` module itself
|
|
30
|
+
// (node's require cache is what makes "once" hold), not a per-page
|
|
31
|
+
// `registry` registration like everything else in this file. See that
|
|
32
|
+
// module's own header for the full explanation.
|
|
33
|
+
require('./lib/shiki-syntax-highlighter')
|
|
34
|
+
|
|
35
|
+
function registerAll(target) {
|
|
36
|
+
registerLabelMacro(target)
|
|
37
|
+
registerMonoMacro(target)
|
|
38
|
+
// table-width.js's tree processor must run (be registered) before
|
|
39
|
+
// table-container.js's postprocessor for its stash to exist by the time
|
|
40
|
+
// the postprocessor reads it — tree processors always run before
|
|
41
|
+
// postprocessors regardless of registration order (Asciidoctor's own
|
|
42
|
+
// fixed processor-phase pipeline), but keeping the registration order
|
|
43
|
+
// matching that phase order here too, for anyone reading top to bottom.
|
|
44
|
+
registerTableWidth(target)
|
|
45
|
+
registerTableContainer(target)
|
|
46
|
+
registerNowrapCols(target)
|
|
47
|
+
registerVideoSize(target)
|
|
48
|
+
// [cards] — Weave.js migration Phase 2, see its own
|
|
49
|
+
// header comment for the DS components behind it and why.
|
|
50
|
+
registerCardGrid(target)
|
|
51
|
+
// [feature-tabs] — the landing's Key features switcher (GH-22). The only
|
|
52
|
+
// block here with behaviour layered on top of it; see its own header for
|
|
53
|
+
// why the tab ARIA is applied by ui-bundle's 07-feature-tabs.ts rather
|
|
54
|
+
// than emitted from here.
|
|
55
|
+
registerFeatureTabs(target)
|
|
56
|
+
// [cta] — the landing's call to action (GH-23), styled after Fumadocs'
|
|
57
|
+
// "Free & Open Source" block rather than a Figma frame; see its own header
|
|
58
|
+
// for why it deviates from GH-23's original macro sketch.
|
|
59
|
+
registerCta(target)
|
|
60
|
+
// [accordion] — GH-61 Part 2, grouping semantics (role=group, single-open)
|
|
61
|
+
// over a run of ordinary [%collapsible] blocks (Part 1's own restyle of
|
|
62
|
+
// those, in ui-bundle/src/css/accordion.css). An OPEN block, not an
|
|
63
|
+
// example block like every entry above it — see its own header for why.
|
|
64
|
+
registerAccordion(target)
|
|
65
|
+
// [tabs] — GH-45, real tabs for the migrated quickstart's package-manager
|
|
66
|
+
// code blocks (and any other authored content wanting a switcher). Also
|
|
67
|
+
// an OPEN block, for the mirror-image reason accordion.js gives — see its
|
|
68
|
+
// own header.
|
|
69
|
+
registerTabs(target)
|
|
70
|
+
// [mermaid]/[plantuml]/etc — GH-44, real diagrams via a self-hosted Kroki
|
|
71
|
+
// service, over the literal diagram source the Weave.js migration left
|
|
72
|
+
// behind (see tools/fumadocs-migrate/lib/emit.mjs). Unlike every extension
|
|
73
|
+
// above, OPT IN and disabled by default per site — see its own header for
|
|
74
|
+
// why, and for the `kroki-enabled`/`kroki-diagram-types` attributes that
|
|
75
|
+
// turn it on. Registers once per supported diagram type, not once overall.
|
|
76
|
+
registerKroki(target)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Registers docouture' AsciiDoc extensions.
|
|
81
|
+
*
|
|
82
|
+
* Antora calls this once per page with a fresh, per-page `registry` (see
|
|
83
|
+
* @antora/asciidoc-loader's resolve-asciidoc-config.js) — the first
|
|
84
|
+
* parameter must be literally named `registry` or Antora's own regex match
|
|
85
|
+
* against `register.toString()` fails, decides this is an Antora pipeline
|
|
86
|
+
* extension mistakenly listed here, logs a warning, and skips it entirely.
|
|
87
|
+
*
|
|
88
|
+
* The ui-bundle preview harness (gulp.d/tasks/build-preview-pages.js) calls
|
|
89
|
+
* this differently: `extension.register.call(Asciidoctor.Extensions)` — no
|
|
90
|
+
* argument, `this` bound to the Asciidoctor 4.0 `Extensions` NAMESPACE, not
|
|
91
|
+
* a registry. That object has `.register`/`.create` but no `.inlineMacro`
|
|
92
|
+
* of its own (verified empirically) — `.register(fn)` is what hands `fn` a
|
|
93
|
+
* real registry as ITS `this`, one more hop than the Antora case.
|
|
94
|
+
*
|
|
95
|
+
* That global path is a one-time registration for the life of the process —
|
|
96
|
+
* fine for a single `gulp preview:build` run, but `gulp preview` (the
|
|
97
|
+
* watch server) re-runs build-preview-pages.js, and so this whole module,
|
|
98
|
+
* on every rebuild WITHOUT restarting the node process. `Extensions.register`
|
|
99
|
+
* has no dedupe of its own: each rebuild adds one more global registration,
|
|
100
|
+
* so a table ends up wrapped in as many nested `.tableblock-wrap`s as
|
|
101
|
+
* rebuilds have happened — a real bug reproduced live in that watch server
|
|
102
|
+
* (`.tableblock-wrap` nested inside its own `.tablecontainer`, doubling the
|
|
103
|
+
* width cap). Guarded here with a flag on the namespace object itself,
|
|
104
|
+
* rather than in gulp.d (out of this package's own control) — idempotent
|
|
105
|
+
* across any number of calls in the same process.
|
|
106
|
+
*/
|
|
107
|
+
module.exports.register = function (registry) {
|
|
108
|
+
const target = registry || this
|
|
109
|
+
if (typeof target.inlineMacro === 'function') {
|
|
110
|
+
registerAll(target)
|
|
111
|
+
} else if (!target.$docoutureAsciidocExtensionsRegistered) {
|
|
112
|
+
target.$docoutureAsciidocExtensionsRegistered = true
|
|
113
|
+
target.register(function () {
|
|
114
|
+
registerAll(this)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
package/lib/accordion.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { chain, chainAll, precomputeSubtree } = require('./async-compat')
|
|
4
|
+
const { escapeHtml, attr, stripTags } = require('./html')
|
|
5
|
+
const uniqueId = require('./unique-id')
|
|
6
|
+
const warn = require('./warn')
|
|
7
|
+
|
|
8
|
+
// Accordion grouping — GH-61 Part 2. Adds only what a native `[%collapsible]`
|
|
9
|
+
// cannot express on its own: group semantics (`role=group`, an accessible
|
|
10
|
+
// name) and, optionally, single-open behaviour. Part 1 (accordion.css,
|
|
11
|
+
// ui-bundle) already restyles every `[%collapsible]` as a DS accordion item,
|
|
12
|
+
// independent-mode by construction — this block is the thinnest possible
|
|
13
|
+
// wrapper around a run of them, never a reimplementation.
|
|
14
|
+
//
|
|
15
|
+
// SYNTAX
|
|
16
|
+
//
|
|
17
|
+
// An OPEN block (`--`/`--`), not an example block (`====`) like every other
|
|
18
|
+
// grouping extension in this package (`[cards]`, `[steps]`, `[feature-tabs]`
|
|
19
|
+
// all use `onContext('example')`). Deliberate: the children here are
|
|
20
|
+
// THEMSELVES example blocks — a `[%collapsible]` is `[%collapsible]====…====`
|
|
21
|
+
// — and nesting an example block inside another example block forces the
|
|
22
|
+
// child to `=====` (one more `=`), which is easy to get wrong and stops
|
|
23
|
+
// looking like an ordinary, standalone collapsible. An open block's own
|
|
24
|
+
// delimiter is `--`, so the children stay ordinary `====` blocks, unchanged,
|
|
25
|
+
// exactly as they would read outside a group:
|
|
26
|
+
//
|
|
27
|
+
// [accordion%single-open,aria-label="Frequently asked questions"]
|
|
28
|
+
// --
|
|
29
|
+
// .Can I use Weave.js with any UI framework?
|
|
30
|
+
// [%collapsible]
|
|
31
|
+
// ====
|
|
32
|
+
// Yes, Weave.js is framework-agnostic by design.
|
|
33
|
+
// ====
|
|
34
|
+
//
|
|
35
|
+
// .Can I deploy the frontend and backend on a single artifact?
|
|
36
|
+
// [%collapsible]
|
|
37
|
+
// ====
|
|
38
|
+
// Yes, you can bundle them together.
|
|
39
|
+
// ====
|
|
40
|
+
// --
|
|
41
|
+
//
|
|
42
|
+
// `single-open` is the DS component's own vocabulary (`AccordionProps.
|
|
43
|
+
// singleOpen`, `accordion.d.ts`), spelled as Asciidoctor's own option
|
|
44
|
+
// shorthand — the same `%name` mechanism `[%collapsible]` itself already
|
|
45
|
+
// uses (`Parser.parseStyleAttribute`, verified against the vendored parser:
|
|
46
|
+
// a style token can carry a style name AND `%option`s together, e.g. the
|
|
47
|
+
// `[%collapsible%open]` an author can already write today). Absent the
|
|
48
|
+
// option, items are independent — the same default the DS component's own
|
|
49
|
+
// `singleOpen` prop has.
|
|
50
|
+
//
|
|
51
|
+
// WHAT THIS EMITS
|
|
52
|
+
//
|
|
53
|
+
// `<div class="docouture-accordion-group" role="group" aria-label="…">` wrapping
|
|
54
|
+
// the children's OWN converted markup verbatim — nothing here re-renders a
|
|
55
|
+
// collapsible; `child.convert()` is Asciidoctor's own `convert_example`
|
|
56
|
+
// (`node.hasOption('collapsible')` branch), unchanged. `docouture-accordion-group`
|
|
57
|
+
// is not an invented DS class — the real `Accordion` component's own wrapper
|
|
58
|
+
// `<div>` (`accordion.js`) carries no class of its own beyond whatever the
|
|
59
|
+
// CALLER passes; there is nothing to match here, so this file names its own
|
|
60
|
+
// wrapper the same way every other non-DS wrapper in this package does
|
|
61
|
+
// (`docouture-cta`, `docouture-card-grid`, `docouture-feature-tabs`). Styled in
|
|
62
|
+
// `ui-bundle/src/css/accordion.css`, alongside the items it groups.
|
|
63
|
+
//
|
|
64
|
+
// Single-open is implemented with the native `<details name="…">` radio-group
|
|
65
|
+
// behaviour, `name` shared across every child in the group via
|
|
66
|
+
// `lib/unique-id.js` — regex-patched onto each child's own rendered
|
|
67
|
+
// `<details>` tag, the same "lift the converter's own tag, patch one
|
|
68
|
+
// attribute" technique `video-size.js`/`cta.js` already use. That makes
|
|
69
|
+
// single-open work with NO JavaScript in every browser new enough to
|
|
70
|
+
// support `<details name>` (Chrome 120+, Safari 17.2+, Firefox 130+) — a
|
|
71
|
+
// degradation to independent mode elsewhere, never a break. Real keyboard
|
|
72
|
+
// roving focus (arrow/Home/End across headers) is layered on top of this
|
|
73
|
+
// same markup by `ui-bundle/src/js/10-accordion.ts`; this file emits no
|
|
74
|
+
// script and no inline `style`.
|
|
75
|
+
|
|
76
|
+
/** The first `<details` of a child's own converted markup — see header. */
|
|
77
|
+
const DETAILS_OPEN_RX = /^<details\b/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Cross-major `hasOption` — the same split `first-positional.js`'s own
|
|
81
|
+
* header documents for a different method, here for this one: 2.2's JS API
|
|
82
|
+
* names it `isOption` (verified against the vendored dist,
|
|
83
|
+
* `AbstractNode.prototype.isOption`); 4.0 renamed it to `hasOption`
|
|
84
|
+
* (`abstract_node.js`'s own comment: "option? → hasOption"). Needed here,
|
|
85
|
+
* unlike `child.hasRole(...)` elsewhere in this package's other extensions —
|
|
86
|
+
* `hasRole` kept its name across both majors, `hasOption` did not.
|
|
87
|
+
*
|
|
88
|
+
* @param {Block} node
|
|
89
|
+
* @param {string} name
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function hasOption(node, name) {
|
|
93
|
+
if (typeof node.hasOption === 'function') return node.hasOption(name)
|
|
94
|
+
// 2.2's own JS API (Opal, real Antora builds) — absent from 4.0's
|
|
95
|
+
// `AbstractNode` type declarations, so a plain property access is a type
|
|
96
|
+
// error even though it exists at runtime; only reachable when the
|
|
97
|
+
// `hasOption` branch above is false, i.e. actually running 2.2.
|
|
98
|
+
return /** @type {{ isOption(name: string): boolean }} */ (/** @type {unknown} */ (node)).isOption(name)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An Asciidoctor block, in whichever major is running.
|
|
103
|
+
*
|
|
104
|
+
* @typedef {import('@asciidoctor/core').AbstractBlock} Block
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Escapes a value ALREADY produced by the converter (`getTitle()`) for
|
|
109
|
+
* placement inside a double-quoted HTML attribute. Deliberately not
|
|
110
|
+
* `lib/html.js`'s `escapeHtml` — that helper is for RAW block attributes
|
|
111
|
+
* Asciidoctor never substitutes; a block's title is the opposite case
|
|
112
|
+
* (`lib/html.js`'s own header: "anything from `getText()`/`getTitle()` is
|
|
113
|
+
* already converted HTML"), so `&`/`<`/`>` are already entities and escaping
|
|
114
|
+
* them again would double-encode (`&` → `&amp;`). Only the quote
|
|
115
|
+
* that would otherwise close the attribute early needs handling here.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} html - converted HTML, e.g. `wrapper.getTitle()`.
|
|
118
|
+
* @returns {string} the same text, safe inside `"…"`.
|
|
119
|
+
*/
|
|
120
|
+
function escapeConvertedAttribute(html) {
|
|
121
|
+
return html.replace(/"/g, '"')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The group's accessible name: the `aria-label=` attribute when given (a raw
|
|
126
|
+
* block attribute, so escaped normally), falling back to the wrapper's own
|
|
127
|
+
* block title with its markup stripped (converted HTML, so quote-escaped
|
|
128
|
+
* only — see {@link escapeConvertedAttribute}) — an ARIA attribute value
|
|
129
|
+
* cannot itself carry HTML, so any tags a titled `.Title` line converted to
|
|
130
|
+
* (a bold word, an `xref:`) are dropped rather than left as visible text.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} attrs - the block's own attributes.
|
|
133
|
+
* @param {Block} wrapper - the parsed `[accordion]` content, for its title.
|
|
134
|
+
* @returns {string} the label, or `''` when there is none.
|
|
135
|
+
*/
|
|
136
|
+
function ariaLabelFor(attrs, wrapper) {
|
|
137
|
+
const raw = attrs['aria-label']
|
|
138
|
+
if (raw) return escapeHtml(raw)
|
|
139
|
+
const title = wrapper.getTitle()
|
|
140
|
+
if (!title) return ''
|
|
141
|
+
return escapeConvertedAttribute(stripTags(String(title)))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The group's `[%collapsible]` children — anything else authored inside the
|
|
146
|
+
* open block (a stray paragraph, a non-collapsible example block) is left
|
|
147
|
+
* out silently, the same judgement `card-grid.js`/`feature-tabs.js` make for
|
|
148
|
+
* a stray child that isn't their own `[card]`/`[feature]` style.
|
|
149
|
+
*
|
|
150
|
+
* @param {Block} wrapper
|
|
151
|
+
* @returns {Block[]}
|
|
152
|
+
*/
|
|
153
|
+
function collapsibleChildren(wrapper) {
|
|
154
|
+
return wrapper.getBlocks().filter((block) => block.getContext() === 'example' && hasOption(block, 'collapsible'))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Assembles the block once every child has been rendered.
|
|
159
|
+
*
|
|
160
|
+
* @param {Block} parent - the block this replaces.
|
|
161
|
+
* @param {Block} wrapper - the parsed `[accordion]` content.
|
|
162
|
+
* @param {object} attrs - the block's own attributes.
|
|
163
|
+
* @param {{ createBlock: Function }} self
|
|
164
|
+
* @returns {object | Promise<object>}
|
|
165
|
+
*/
|
|
166
|
+
function finish(parent, wrapper, attrs, self) {
|
|
167
|
+
const children = collapsibleChildren(wrapper)
|
|
168
|
+
if (!children.length) {
|
|
169
|
+
warn(parent, '[accordion]', 'an accordion group with no `[%collapsible]` items in it')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ariaLabel = ariaLabelFor(attrs, wrapper)
|
|
173
|
+
if (!ariaLabel) {
|
|
174
|
+
warn(
|
|
175
|
+
parent,
|
|
176
|
+
'[accordion]',
|
|
177
|
+
'an accordion group has no `aria-label=` and no title; give it one — a `role=group` with no accessible name is a strongly-discouraged pattern for screen reader users'
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Shared across every child so native `<details name>` groups them into
|
|
182
|
+
// one mutually-exclusive set — `null` (and no `name=` patched in below)
|
|
183
|
+
// leaves every child independent, the DS's own `singleOpen` default.
|
|
184
|
+
const groupName = 'single-open-option' in attrs ? uniqueId(wrapper, 'accordion-group') : null
|
|
185
|
+
|
|
186
|
+
const rendered = children.map((child) => child.convert())
|
|
187
|
+
|
|
188
|
+
return chainAll(rendered, (htmls) => {
|
|
189
|
+
const itemsHtml = htmls
|
|
190
|
+
.map((html, index) => {
|
|
191
|
+
if (!groupName) return html
|
|
192
|
+
if (!DETAILS_OPEN_RX.test(html)) {
|
|
193
|
+
warn(
|
|
194
|
+
parent,
|
|
195
|
+
'[accordion]',
|
|
196
|
+
'child ' + (index + 1) + " didn't render as a <details>; single-open needs every item to be one"
|
|
197
|
+
)
|
|
198
|
+
return html
|
|
199
|
+
}
|
|
200
|
+
return html.replace(DETAILS_OPEN_RX, '<details' + attr('name', groupName))
|
|
201
|
+
})
|
|
202
|
+
.join('')
|
|
203
|
+
|
|
204
|
+
const html =
|
|
205
|
+
'<div class="docouture-accordion-group" role="group"' +
|
|
206
|
+
(ariaLabel ? ' aria-label="' + ariaLabel + '"' : '') +
|
|
207
|
+
'>' +
|
|
208
|
+
itemsHtml +
|
|
209
|
+
'</div>'
|
|
210
|
+
return self.createBlock(parent, 'pass', html, attrs)
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function accordionBlock() {
|
|
215
|
+
this.named('accordion')
|
|
216
|
+
this.onContext('open')
|
|
217
|
+
this.process((parent, reader, attrs) => {
|
|
218
|
+
// See steps.js's own comment: Opal (2.2) can hand this a bare JS `null`
|
|
219
|
+
// for a block with no attributes beyond its style, and `createBlock`
|
|
220
|
+
// crashes on that.
|
|
221
|
+
attrs = attrs || {}
|
|
222
|
+
// See steps.js's own comment: a literal JS `null` "source" crashes Opal
|
|
223
|
+
// (2.2) inside `Block#initialize`'s `.nil_or_empty?()` check.
|
|
224
|
+
const wrapper = this.createBlock(parent, 'open', '', attrs)
|
|
225
|
+
// See label-macro.js's own comment on this same pattern.
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
227
|
+
const self = this
|
|
228
|
+
// See async-compat.js's own header comment: parseContent is sync under
|
|
229
|
+
// 2.2 (Opal, real Antora builds) and Promise-returning under 4.0 (the
|
|
230
|
+
// ui-bundle preview harness) — chain() handles either without making
|
|
231
|
+
// this function `async` unconditionally. precomputeSubtree is what makes
|
|
232
|
+
// a `.Title` carrying inline markup (this block's own, or a child
|
|
233
|
+
// `[%collapsible]`'s) arrive converted rather than raw.
|
|
234
|
+
return chain(this.parseContent(wrapper, reader.getLines()), () =>
|
|
235
|
+
chain(precomputeSubtree(wrapper), () => finish(parent, wrapper, attrs, self))
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = function registerAccordion(registry) {
|
|
241
|
+
registry.block(accordionBlock)
|
|
242
|
+
}
|
|
243
|
+
module.exports.accordionBlock = accordionBlock
|