@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/lib/tabs.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { chain, chainAll, precomputeSubtree } = require('./async-compat')
|
|
4
|
+
const { escapeHtml, attr } = require('./html')
|
|
5
|
+
const uniqueId = require('./unique-id')
|
|
6
|
+
const warn = require('./warn')
|
|
7
|
+
|
|
8
|
+
// Content tabs — GH-45. IDS Tabs (`Kind=Underlined`, Figma 2735:69397 for
|
|
9
|
+
// light, 2735:69401 for dark), used for the one real case the migration
|
|
10
|
+
// found: `main/quickstart.adoc`'s package-manager command blocks
|
|
11
|
+
// (`code/tools/fumadocs-migrate`'s README, "Degraded" section). Both frames
|
|
12
|
+
// are the same component with only the ink swapped (`#000000` / `#FFFFFF`,
|
|
13
|
+
// which is `--ids-color-content-default` in each theme) — dark costs
|
|
14
|
+
// tabs.css nothing, same reasoning as card-grid.js's own header.
|
|
15
|
+
//
|
|
16
|
+
// Not to be confused with `feature-tabs.js` (GH-22, the landing's "Key
|
|
17
|
+
// features" switcher): that block's slides are a fixed composition — media,
|
|
18
|
+
// prose, one call to action, in the design's own order regardless of
|
|
19
|
+
// authoring order. A tab here is arbitrary CONTENT — prose, tables, images,
|
|
20
|
+
// admonitions, titled blocks, a nested `[cards]` — so this holds authored
|
|
21
|
+
// blocks in authored order and never re-sorts anything.
|
|
22
|
+
//
|
|
23
|
+
// SYNTAX
|
|
24
|
+
//
|
|
25
|
+
// An OPEN block (`--`/`--`), not an example block (`====`) like every other
|
|
26
|
+
// grouping extension in this package (`[cards]`, `[steps]`, `[feature-tabs]`
|
|
27
|
+
// all use `onContext('example')`). Deliberate, for the opposite reason
|
|
28
|
+
// accordion.js gives for the same choice: the real use site nests this
|
|
29
|
+
// INSIDE `[steps]`'s own `====`, and if `[tabs]` were also an example block,
|
|
30
|
+
// that nesting would force it to `=====` (one more `=`) — easy to get wrong
|
|
31
|
+
// and a pattern with no precedent anywhere in this corpus. An open block's
|
|
32
|
+
// delimiter never collides with the enclosing `====`, so `[tabs]` reads the
|
|
33
|
+
// same whether it sits at a page's top level or inside a step.
|
|
34
|
+
//
|
|
35
|
+
// Each child is `[tab,label="…"]` on its OWN SIDEBAR block (`****`), not an
|
|
36
|
+
// example block and not title-grouped the way `[steps]` groups its
|
|
37
|
+
// children. Two reasons, not one:
|
|
38
|
+
//
|
|
39
|
+
// - a tab can hold prose, tables, a titled source block, even another
|
|
40
|
+
// titled block entirely, and title-grouping (steps.js's own
|
|
41
|
+
// `groupBySteps`) would silently start a new tab at the first titled
|
|
42
|
+
// child it found. An explicit per-tab delimiter has no such trap.
|
|
43
|
+
// - it CANNOT be `====` either, despite `[tabs]` itself being an open
|
|
44
|
+
// block: AsciiDoc's delimited-block matching is a plain stack keyed on
|
|
45
|
+
// the exact delimiter string, with no regard for what sits between two
|
|
46
|
+
// fences of the same kind. A `[tab]` example block nested inside
|
|
47
|
+
// `[tabs]`'s `--`, itself nested inside `[steps]`'s `====`, closes the
|
|
48
|
+
// OUTER `[steps]` block the instant Asciidoctor meets the tab's own
|
|
49
|
+
// closing `====` — verified empirically (the outer step's remaining
|
|
50
|
+
// content and the whole rest of the page fell out of the timeline and
|
|
51
|
+
// rendered as a bare, un-stepped `====` example instead). `****` never
|
|
52
|
+
// collides with `====` at any nesting depth, which is the only property
|
|
53
|
+
// that matters here.
|
|
54
|
+
//
|
|
55
|
+
// One more line per tab than an example block would need, in exchange for
|
|
56
|
+
// never having to reason about ambient nesting depth:
|
|
57
|
+
//
|
|
58
|
+
// [tabs]
|
|
59
|
+
// --
|
|
60
|
+
// [tab,label="pnpm"]
|
|
61
|
+
// ****
|
|
62
|
+
// [source,bash]
|
|
63
|
+
// ----
|
|
64
|
+
// pnpm create weave-backend-app
|
|
65
|
+
// ----
|
|
66
|
+
// ****
|
|
67
|
+
//
|
|
68
|
+
// [tab,label="npm"]
|
|
69
|
+
// ****
|
|
70
|
+
// [source,bash]
|
|
71
|
+
// ----
|
|
72
|
+
// npm create weave-backend-app
|
|
73
|
+
// ----
|
|
74
|
+
// ****
|
|
75
|
+
// --
|
|
76
|
+
//
|
|
77
|
+
// `label=` is required on every tab — a block style attribute, not a
|
|
78
|
+
// `.Title` (unlike `[card]`/`[feature]`, whose titles are already-converted
|
|
79
|
+
// links or headings the label reuses): a tab's label is never itself
|
|
80
|
+
// content, and reading it off a `.Title` line would make an author's first
|
|
81
|
+
// line of prose double as the tab strip by accident. A `[tabs]` with fewer
|
|
82
|
+
// than two `[tab]` children is a build-fatal warning — a single tab is not a
|
|
83
|
+
// choice.
|
|
84
|
+
//
|
|
85
|
+
// Every `[tabs]` block is independent. There is no cross-block linking (no
|
|
86
|
+
// `sync=`, nothing shared in `localStorage`): an earlier revision of this
|
|
87
|
+
// file had exactly that, and it was wrong on two counts — picking a tab in
|
|
88
|
+
// one block is not supposed to change an unrelated block elsewhere on the
|
|
89
|
+
// page, full stop, and the mechanism used to make GROUPS switch together
|
|
90
|
+
// (real `<a href="#panel-id">` tabs) collided with
|
|
91
|
+
// `ui-bundle/src/js/03-fragment-jumper.ts`, which globally rewrites
|
|
92
|
+
// `window.location.hash` for every `a[href^="#"]` on the page regardless of
|
|
93
|
+
// this file's own `click` handler and its `preventDefault()`. Two problems,
|
|
94
|
+
// one fix: tabs are BUTTONS now (see below), and every block only ever
|
|
95
|
+
// touches its own DOM.
|
|
96
|
+
//
|
|
97
|
+
// WHAT THIS EMITS
|
|
98
|
+
//
|
|
99
|
+
// One `<button type="button">` per tab (never an anchor — see above),
|
|
100
|
+
// `role=tab`/`aria-selected`/`aria-controls` set here directly rather than
|
|
101
|
+
// layered on by script: unlike `feature-tabs.js`, there is no readable
|
|
102
|
+
// "plain document" state this degrades to first — exactly one panel is
|
|
103
|
+
// ever visible (`ui-bundle/src/js/11-tabs.ts` only handles switching which
|
|
104
|
+
// one, never whether more than one shows), so the tab semantics are true
|
|
105
|
+
// from the first paint, script or not. A reader without JavaScript sees the
|
|
106
|
+
// first tab's content and a strip of inert (but clearly labelled) buttons —
|
|
107
|
+
// a real limitation of a widget that fundamentally requires script to
|
|
108
|
+
// switch state, same trade-off any tab component makes.
|
|
109
|
+
//
|
|
110
|
+
// CASE SENSITIVITY (GH-45's own requirement)
|
|
111
|
+
//
|
|
112
|
+
// Labels render in the author's own case — tabs.css does not force
|
|
113
|
+
// uppercase (a deliberate deviation from the Figma text styles it is
|
|
114
|
+
// otherwise built from; see that file's header) — and are matched
|
|
115
|
+
// CASE-SENSITIVELY everywhere: `data-tab-value` and the ARIA relationships
|
|
116
|
+
// this file builds both use the author's label byte for byte, and an exact
|
|
117
|
+
// duplicate or a same-except-case pair (`"pnpm"` vs `"Pnpm"`) both warn,
|
|
118
|
+
// since two tabs that differ only by case are almost always a typo rather
|
|
119
|
+
// than an intentional pair.
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* An Asciidoctor block, in whichever major is running.
|
|
123
|
+
*
|
|
124
|
+
* @typedef {import('@asciidoctor/core').AbstractBlock} Block
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One tab: its strip button, and its panel.
|
|
129
|
+
*
|
|
130
|
+
* @param {Block} block - the `[tab]` sidebar block.
|
|
131
|
+
* @param {number} index - its position, zero-based.
|
|
132
|
+
* @param {string} groupId - the unique id shared by every tab/panel pair in this block.
|
|
133
|
+
* @param {string[]} seenLabels - every raw label rendered so far in this block, for duplicate detection.
|
|
134
|
+
* @param {Block} parent - the enclosing block, for warnings.
|
|
135
|
+
* @returns {{ tab: string, panel: string } | Promise<{ tab: string, panel: string }>}
|
|
136
|
+
*/
|
|
137
|
+
function renderTab(block, index, groupId, seenLabels, parent) {
|
|
138
|
+
// A block style attribute, so raw and unsubstituted — escaped, per
|
|
139
|
+
// lib/html.js's own table (row "block style attribute").
|
|
140
|
+
const label = block.getAttribute('label')
|
|
141
|
+
if (!label) {
|
|
142
|
+
warn(parent, '[tab]', 'a tab has no `label=`; give it one, e.g. `[tab,label="pnpm"]`')
|
|
143
|
+
return { tab: '', panel: '' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (seenLabels.includes(label)) {
|
|
147
|
+
warn(parent, '[tab,label="' + label + '"]', 'a tab set already has a tab labelled "' + label + '"')
|
|
148
|
+
} else if (seenLabels.some((seen) => seen.toLowerCase() === label.toLowerCase())) {
|
|
149
|
+
warn(
|
|
150
|
+
parent,
|
|
151
|
+
'[tab,label="' + label + '"]',
|
|
152
|
+
'a tab set already has a tab labelled "' +
|
|
153
|
+
seenLabels.find((seen) => seen.toLowerCase() === label.toLowerCase()) +
|
|
154
|
+
'"; labels are matched case-sensitively, but two that differ only by case are ' +
|
|
155
|
+
'almost always a typo rather than an intentional pair'
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
seenLabels.push(label)
|
|
159
|
+
|
|
160
|
+
const tabId = groupId + '-tab-' + (index + 1)
|
|
161
|
+
const panelId = groupId + '-panel-' + (index + 1)
|
|
162
|
+
const selected = index === 0
|
|
163
|
+
|
|
164
|
+
const bodies = block.getBlocks().map((child) => child.convert())
|
|
165
|
+
|
|
166
|
+
return chainAll(bodies, (parts) => {
|
|
167
|
+
const tab =
|
|
168
|
+
'<li class="docouture-tabs__item" role="presentation">' +
|
|
169
|
+
'<button type="button" class="ids-tabs-item docouture-tabs__tab' +
|
|
170
|
+
(selected ? ' ids-tabs-item--selected' : '') +
|
|
171
|
+
'"' +
|
|
172
|
+
attr('id', tabId) +
|
|
173
|
+
attr('role', 'tab') +
|
|
174
|
+
attr('aria-selected', String(selected)) +
|
|
175
|
+
attr('aria-controls', panelId) +
|
|
176
|
+
attr('tabindex', selected ? '0' : '-1') +
|
|
177
|
+
attr('data-tab-value', label) +
|
|
178
|
+
'>' +
|
|
179
|
+
'<span class="ids-tabs-item__label">' +
|
|
180
|
+
escapeHtml(label) +
|
|
181
|
+
'</span>' +
|
|
182
|
+
'</button>' +
|
|
183
|
+
'</li>'
|
|
184
|
+
|
|
185
|
+
const panel =
|
|
186
|
+
'<section class="docouture-tabs__panel' +
|
|
187
|
+
(selected ? ' is-selected' : '') +
|
|
188
|
+
'"' +
|
|
189
|
+
attr('id', panelId) +
|
|
190
|
+
attr('role', 'tabpanel') +
|
|
191
|
+
attr('aria-labelledby', tabId) +
|
|
192
|
+
attr('tabindex', '0') +
|
|
193
|
+
attr('data-tab-value', label) +
|
|
194
|
+
'>' +
|
|
195
|
+
parts.join('\n') +
|
|
196
|
+
'</section>'
|
|
197
|
+
|
|
198
|
+
return { tab, panel }
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Assembles the block once every tab has been rendered.
|
|
204
|
+
*
|
|
205
|
+
* @param {Block} parent - the block this replaces.
|
|
206
|
+
* @param {Block} wrapper - the parsed `[tabs]` content.
|
|
207
|
+
* @param {Record<string, unknown>} attrs - the block's own attributes.
|
|
208
|
+
* @param {{ createBlock: Function }} self - the block processor.
|
|
209
|
+
* @returns {object | Promise<object>} the `pass` block carrying the markup.
|
|
210
|
+
*/
|
|
211
|
+
function finish(parent, wrapper, attrs, self) {
|
|
212
|
+
const tabBlocks = wrapper.getBlocks().filter((block) => block.getStyle() === 'tab')
|
|
213
|
+
if (tabBlocks.length < 2) {
|
|
214
|
+
warn(parent, '[tabs]', tabBlocks.length + ' `[tab]` block(s) found; a tab set needs at least two')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const groupId = uniqueId(parent, 'tabs')
|
|
218
|
+
/** @type {string[]} */
|
|
219
|
+
const seenLabels = []
|
|
220
|
+
const rendered = tabBlocks.map((block, index) => renderTab(block, index, groupId, seenLabels, parent))
|
|
221
|
+
|
|
222
|
+
return chainAll(rendered, (tabs) => {
|
|
223
|
+
const html =
|
|
224
|
+
'<div class="docouture-tabs" data-tabs>' +
|
|
225
|
+
'<ul class="docouture-tabs__list" role="tablist">' +
|
|
226
|
+
tabs.map((tab) => tab.tab).join('') +
|
|
227
|
+
'</ul>' +
|
|
228
|
+
'<div class="docouture-tabs__panels">' +
|
|
229
|
+
tabs.map((tab) => tab.panel).join('') +
|
|
230
|
+
'</div>' +
|
|
231
|
+
'</div>'
|
|
232
|
+
return self.createBlock(parent, 'pass', html, attrs)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function tabsBlock() {
|
|
237
|
+
this.named('tabs')
|
|
238
|
+
this.onContext('open')
|
|
239
|
+
this.process((parent, reader, attrs) => {
|
|
240
|
+
// See steps.js's own comment: Opal (2.2) can hand this a bare JS `null`
|
|
241
|
+
// for a block with no attributes beyond its style, and `createBlock`
|
|
242
|
+
// crashes on that.
|
|
243
|
+
attrs = attrs || {}
|
|
244
|
+
// See steps.js's own comment: a literal JS `null` "source" crashes Opal
|
|
245
|
+
// (2.2) inside `Block#initialize`'s `.nil_or_empty?()` check.
|
|
246
|
+
const wrapper = this.createBlock(parent, 'open', '', attrs)
|
|
247
|
+
// See label-macro.js's own comment on this same pattern.
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
249
|
+
const self = this
|
|
250
|
+
// See async-compat.js's own header comment: parseContent is sync under
|
|
251
|
+
// 2.2 (Opal, real Antora builds) and Promise-returning under 4.0 (the
|
|
252
|
+
// ui-bundle preview harness) — chain() handles either without making
|
|
253
|
+
// this function `async` unconditionally.
|
|
254
|
+
return chain(this.parseContent(wrapper, reader.getLines()), () =>
|
|
255
|
+
chain(precomputeSubtree(wrapper), () => finish(parent, wrapper, attrs, self))
|
|
256
|
+
)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = function registerTabs(registry) {
|
|
261
|
+
registry.block(tabsBlock)
|
|
262
|
+
}
|
|
263
|
+
module.exports.tabsBlock = tabsBlock
|
package/lib/unique-id.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Document-scoped id generation, for blocks that have to wire one element to
|
|
4
|
+
// another through an id — ARIA relationships (`aria-controls`,
|
|
5
|
+
// `aria-labelledby`, `aria-describedby`), `<label for>`, and in-page anchors.
|
|
6
|
+
//
|
|
7
|
+
// Why this cannot be a plain module-level counter: extensions are registered
|
|
8
|
+
// ONCE per process in the ui-bundle preview harness (see index.js's own
|
|
9
|
+
// header comment on the global registration path), and that harness converts
|
|
10
|
+
// every preview page in the same process. A module-level counter would keep
|
|
11
|
+
// climbing across pages, so the same block would get different ids depending
|
|
12
|
+
// on which page happened to be converted first — output that changes with
|
|
13
|
+
// build order rather than with content. Antora's own per-page registry does
|
|
14
|
+
// not have that problem, but the harness does, and the same extension code
|
|
15
|
+
// runs under both.
|
|
16
|
+
//
|
|
17
|
+
// Counting per DOCUMENT instead makes the ids a function of the page's own
|
|
18
|
+
// content and nothing else: page A always produces the same ids whether it is
|
|
19
|
+
// converted first or last. The counter is stashed on the document object,
|
|
20
|
+
// which Asciidoctor discards along with the page.
|
|
21
|
+
//
|
|
22
|
+
// The ids are NOT globally unique across a site, and do not need to be — an
|
|
23
|
+
// id only has to be unique within the page it appears on.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Property name for the per-document counter map. A `$`-prefixed, package-
|
|
27
|
+
* scoped name, matching the convention index.js already uses for its own
|
|
28
|
+
* registration guard, so it cannot collide with anything Asciidoctor or Antora
|
|
29
|
+
* puts on the same object.
|
|
30
|
+
*/
|
|
31
|
+
const COUNTER_KEY = '$docoutureUniqueIdCounters'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Anything with a document behind it: the `parent` handed to a block
|
|
35
|
+
* processor, or a Document itself.
|
|
36
|
+
*
|
|
37
|
+
* @typedef {{ getDocument?: () => object }} DocumentLike
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the object the counter is stashed on. A block processor receives
|
|
42
|
+
* `parent`, which is a block, not the document — but two blocks on one page
|
|
43
|
+
* must share a counter, so the document is what the count has to hang off.
|
|
44
|
+
*
|
|
45
|
+
* @param {DocumentLike} node - a block, or a document.
|
|
46
|
+
* @returns {object} the document, or `node` itself if it has none (a detached
|
|
47
|
+
* node, only reachable in tests).
|
|
48
|
+
*/
|
|
49
|
+
function documentOf(node) {
|
|
50
|
+
return (typeof node.getDocument === 'function' && node.getDocument()) || node
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the next id for `prefix` on this node's document.
|
|
55
|
+
*
|
|
56
|
+
* ```js
|
|
57
|
+
* const panelId = uniqueId(parent, 'feature-tabs-panel') // feature-tabs-panel-1
|
|
58
|
+
* const labelId = uniqueId(parent, 'feature-tabs-label') // feature-tabs-label-1
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* Each prefix counts independently, so related elements can be paired by
|
|
62
|
+
* generating both from the same iteration rather than by parsing an id apart.
|
|
63
|
+
*
|
|
64
|
+
* @param {DocumentLike} node - the block processor's `parent`, or a document.
|
|
65
|
+
* @param {string} prefix - a stable, extension-specific prefix. Include the
|
|
66
|
+
* block's own name so two extensions on one page cannot collide.
|
|
67
|
+
* @returns {string} `${prefix}-${n}`, with `n` starting at 1.
|
|
68
|
+
*/
|
|
69
|
+
function uniqueId(node, prefix) {
|
|
70
|
+
const doc = /** @type {Record<string, Record<string, number>>} */ (documentOf(node))
|
|
71
|
+
const counters = doc[COUNTER_KEY] || (doc[COUNTER_KEY] = {})
|
|
72
|
+
const next = (counters[prefix] || 0) + 1
|
|
73
|
+
counters[prefix] = next
|
|
74
|
+
return prefix + '-' + next
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = uniqueId
|
|
78
|
+
module.exports.COUNTER_KEY = COUNTER_KEY
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// GH-15 (A9) follow-up: `video::target[youtube,640,360]`'s own `width`/
|
|
4
|
+
// `height` land as plain HTML attributes on the `<iframe>`/`<video>` tag
|
|
5
|
+
// (@asciidoctor/core's html5 converter, convert_video — verified against
|
|
6
|
+
// its own source: `width="${node.getAttribute('width')}"`, no unit, no
|
|
7
|
+
// clamping unlike a table's `width=` percentage). A browser does NOT derive
|
|
8
|
+
// `aspect-ratio` from an iframe's `width`/`height` attributes the way it
|
|
9
|
+
// does for `<img>`, so a CSS `max-width` alone would leave the element's
|
|
10
|
+
// height fixed in pixels regardless of the viewport, distorting the embed
|
|
11
|
+
// at any width narrower than its own attribute. There is no CSS-only fix —
|
|
12
|
+
// `aspect-ratio: attr(width) / attr(height)` is real syntax but Chrome-only
|
|
13
|
+
// (133+) and unsupported everywhere else as of writing — so this moves both
|
|
14
|
+
// numbers into an inline `style` as custom properties doc.css consumes,
|
|
15
|
+
// same mechanism as `table-width.js` + `table-container.js` one file over:
|
|
16
|
+
// a postprocessor scanning the ALREADY-CONVERTED html5 output, since the
|
|
17
|
+
// converter (hard-set by Antora, @antora/asciidoc-loader/lib/load-asciidoc.js)
|
|
18
|
+
// has no passthrough for an arbitrary attribute into an inline style.
|
|
19
|
+
//
|
|
20
|
+
// No separate tree processor needed here, unlike the table case: a table's
|
|
21
|
+
// own `width=` attribute gets CLAMPED by Asciidoctor's Table constructor
|
|
22
|
+
// before a postprocessor ever runs, so table-width.js has to read the RAW
|
|
23
|
+
// value at tree-processor time, before that happens. A video block's
|
|
24
|
+
// `width`/`height` are never touched by any such constructor — `doc`
|
|
25
|
+
// (handed to every postprocessor by @asciidoctor/core) is still fully
|
|
26
|
+
// queryable then, so `doc.findBy({ context: 'video' })` at postprocess time
|
|
27
|
+
// already returns the same un-clamped values a tree processor would.
|
|
28
|
+
//
|
|
29
|
+
// Author-facing effect: `video::x[youtube]` (no size) gets neither custom
|
|
30
|
+
// property, so doc.css's own fallback applies (full width, 16:9).
|
|
31
|
+
// `video::x[youtube,640,360]` caps the block at 640px and locks it to a
|
|
32
|
+
// correct 640:360 ratio at any narrower viewport; `video::x[youtube,640]`
|
|
33
|
+
// (width only) caps the width and leaves the ratio at doc.css's 16:9
|
|
34
|
+
// fallback.
|
|
35
|
+
const VIDEO_OPEN_RX = /<div((?:\s+id="[^"]*")?\s+class="videoblock\b[^"]*")>/g
|
|
36
|
+
const VALID_RX = /^\d+(?:\.\d+)?$/
|
|
37
|
+
|
|
38
|
+
function parseDimension(raw) {
|
|
39
|
+
const trimmed = raw == null ? '' : String(raw).trim()
|
|
40
|
+
return VALID_RX.test(trimmed) ? trimmed : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sizeVideos(html, sizes = []) {
|
|
44
|
+
let result = ''
|
|
45
|
+
let cursor = 0
|
|
46
|
+
let match
|
|
47
|
+
let videoIndex = -1
|
|
48
|
+
while ((match = VIDEO_OPEN_RX.exec(html))) {
|
|
49
|
+
videoIndex += 1
|
|
50
|
+
const size = sizes[videoIndex]
|
|
51
|
+
if (!size || (!size.width && !size.height)) continue
|
|
52
|
+
|
|
53
|
+
const declarations = []
|
|
54
|
+
if (size.width) declarations.push(`--video-max-width:${size.width}px`)
|
|
55
|
+
if (size.width && size.height) {
|
|
56
|
+
declarations.push(`--video-aspect-ratio:${size.width}/${size.height}`)
|
|
57
|
+
}
|
|
58
|
+
if (!declarations.length) continue
|
|
59
|
+
|
|
60
|
+
result += html.slice(cursor, match.index)
|
|
61
|
+
// Capture group 1 is not optional in VIDEO_OPEN_RX, so a match always
|
|
62
|
+
// carries it — but `noUncheckedIndexedAccess` types every group past 0 as
|
|
63
|
+
// possibly undefined, since it cannot know that. Cast rather than add a
|
|
64
|
+
// runtime guard for a branch that cannot be reached.
|
|
65
|
+
const attrs = /** @type {string} */ (match[1])
|
|
66
|
+
const styledAttrs = /\sstyle="/.test(attrs)
|
|
67
|
+
? attrs.replace(/\sstyle="/, ` style="${declarations.join(';')};`)
|
|
68
|
+
: `${attrs} style="${declarations.join(';')}"`
|
|
69
|
+
result += `<div${styledAttrs}>`
|
|
70
|
+
cursor = match.index + match[0].length
|
|
71
|
+
VIDEO_OPEN_RX.lastIndex = cursor
|
|
72
|
+
}
|
|
73
|
+
result += html.slice(cursor)
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = function registerVideoSize(registry) {
|
|
78
|
+
registry.postprocessor(function () {
|
|
79
|
+
this.process(function (doc, output) {
|
|
80
|
+
const sizes = doc.findBy({ context: 'video' }).map((video) => ({
|
|
81
|
+
width: parseDimension(video.getAttribute('width')),
|
|
82
|
+
height: parseDimension(video.getAttribute('height')),
|
|
83
|
+
}))
|
|
84
|
+
return sizeVideos(output, sizes)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports.sizeVideos = sizeVideos
|
package/lib/warn.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Reporting an authoring mistake.
|
|
4
|
+
//
|
|
5
|
+
// Both site playbooks set `runtime.log.failure_level: warn`, so a warning
|
|
6
|
+
// logged here does not merely print — it FAILS THE BUILD. That is the intended
|
|
7
|
+
// severity for this package: an unknown IDS Label colour, a `[cards]` block
|
|
8
|
+
// with no cards in it, a tab set with no panels. Each of those renders as
|
|
9
|
+
// something plausible-looking but wrong (unstyled markup, an empty region, a
|
|
10
|
+
// silently dropped section), and a documentation site that ships those is worse
|
|
11
|
+
// than one that fails to build.
|
|
12
|
+
//
|
|
13
|
+
// So the choice this helper encodes is: authoring mistakes are warnings,
|
|
14
|
+
// because warnings are fatal here. Anything that should NOT stop a build does
|
|
15
|
+
// not belong at this level.
|
|
16
|
+
//
|
|
17
|
+
// It exists mostly to get the logger lookup right in one place. Reaching the
|
|
18
|
+
// logger means walking `node -> document -> logger`, and the node an extension
|
|
19
|
+
// holds varies by extension point — a block processor's `parent`, a
|
|
20
|
+
// postprocessor's `doc`. Getting that wrong throws a TypeError from inside the
|
|
21
|
+
// extension, which surfaces as an opaque conversion failure rather than as the
|
|
22
|
+
// authoring error it was trying to report.
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Anything a warning can be raised from: the `parent` handed to a block or
|
|
26
|
+
* macro processor, or a Document.
|
|
27
|
+
*
|
|
28
|
+
* @typedef {{ getDocument?: () => { getLogger?: () => Logger }, getLogger?: () => Logger }} WarnSource
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The subset of the Asciidoctor logger this uses. Typed structurally rather
|
|
33
|
+
* than imported from `@asciidoctor/core`, because the object at runtime comes
|
|
34
|
+
* from 2.2 under Antora and 4.0 under the preview harness.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {{ warn: (message: string) => void }} Logger
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Logs a warning against the document, failing the build under both site
|
|
41
|
+
* playbooks' `failure_level: warn`.
|
|
42
|
+
*
|
|
43
|
+
* ```js
|
|
44
|
+
* warn(parent, 'label:' + target + '[]', 'unknown IDS Label variant "' + variant + '"', VARIANTS)
|
|
45
|
+
* // → label:mauve[] — unknown IDS Label variant "mauve"; expected one of white, grey, ...
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* The message is assembled here so every extension reports in the same shape:
|
|
49
|
+
* what the author wrote, what is wrong with it, and — when there is a closed
|
|
50
|
+
* set — what was expected instead. The author gets told how to fix it, not just
|
|
51
|
+
* that something is broken.
|
|
52
|
+
*
|
|
53
|
+
* A missing logger is tolerated silently. Asciidoctor always provides one in
|
|
54
|
+
* both majors; a detached node constructed in a test may not, and losing a
|
|
55
|
+
* warning is a better failure mode there than masking the real assertion with a
|
|
56
|
+
* TypeError.
|
|
57
|
+
*
|
|
58
|
+
* @param {WarnSource} node - the block processor's `parent`, or a document.
|
|
59
|
+
* @param {string} source - what the author wrote, verbatim enough to find:
|
|
60
|
+
* `label:mauve[]`, `[cards]`, `cta::[]`.
|
|
61
|
+
* @param {string} problem - what is wrong, in lower case, no trailing period.
|
|
62
|
+
* @param {readonly string[]} [expected] - the permitted values, when the
|
|
63
|
+
* mistake is a value outside a closed set. Appended as
|
|
64
|
+
* `; expected one of a, b, c`.
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
function warn(node, source, problem, expected) {
|
|
68
|
+
const doc = (typeof node.getDocument === 'function' && node.getDocument()) || node
|
|
69
|
+
const logger = typeof doc.getLogger === 'function' ? doc.getLogger() : undefined
|
|
70
|
+
if (!logger || typeof logger.warn !== 'function') return
|
|
71
|
+
const suffix = expected && expected.length ? '; expected one of ' + expected.join(', ') : ''
|
|
72
|
+
logger.warn(source + ' — ' + problem + suffix)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = warn
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inditextech/docouture-asciidoc-extensions",
|
|
3
|
+
"version": "0.1.0-SNAPSHOT.40.1",
|
|
4
|
+
"description": "Asciidoctor extensions shared by docouture documentation sites — authored content that has no AsciiDoc equivalent, rendered as IOP DS markup",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/InditexTech/test-antoradocs.git",
|
|
8
|
+
"directory": "code/packages/asciidoc-extensions"
|
|
9
|
+
},
|
|
10
|
+
"license": "MPL-2.0",
|
|
11
|
+
"main": "index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"lib",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@asciidoctor/core": "~4.0.8",
|
|
22
|
+
"typescript": "6.0.3"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"asciidoctor-core-2.2": "npm:@asciidoctor/core@~2.2",
|
|
26
|
+
"shiki": "~4.4.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix"
|
|
32
|
+
}
|
|
33
|
+
}
|