@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/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
@@ -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
+ }