@inditextech/docouture-asciidoc-extensions 0.1.0-SNAPSHOT.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/index.js +117 -0
- package/lib/accordion.js +243 -0
- package/lib/async-compat.js +90 -0
- package/lib/card-grid.js +327 -0
- package/lib/cta.js +334 -0
- package/lib/feature-tabs.js +400 -0
- package/lib/first-positional.js +13 -0
- package/lib/html.js +121 -0
- package/lib/kroki-config.js +191 -0
- package/lib/kroki-instance.js +89 -0
- package/lib/kroki-mermaid-theme.js +93 -0
- package/lib/kroki.js +165 -0
- package/lib/label-macro.js +69 -0
- package/lib/mono-macro.js +41 -0
- package/lib/nowrap-cols.js +61 -0
- package/lib/shiki-config.js +94 -0
- package/lib/shiki-instance.js +64 -0
- package/lib/shiki-syntax-highlighter.js +137 -0
- package/lib/table-container.js +143 -0
- package/lib/table-width.js +70 -0
- package/lib/tabs.js +263 -0
- package/lib/unique-id.js +78 -0
- package/lib/video-size.js +89 -0
- package/lib/warn.js +75 -0
- package/package.json +33 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// `[cols="1,3,2",nowrap-cols="1,2"]` — an author-facing way to pin
|
|
4
|
+
// `white-space: nowrap` to specific columns (e.g. a mono-token column that
|
|
5
|
+
// should never break mid-value), without a new inline macro: `m|`/`mono:`
|
|
6
|
+
// already exist for "this cell is code" and neither carries a per-COLUMN
|
|
7
|
+
// concept, and the html5 converter hardcodes cell class to
|
|
8
|
+
// `tableblock halign-* valign-*` (see doc.css's own `td.tableblock`
|
|
9
|
+
// comment) — no room there either. A tree processor turning the attribute
|
|
10
|
+
// into roles is the only hook that reaches the table node itself before it
|
|
11
|
+
// converts to `<table class="tableblock …">`.
|
|
12
|
+
//
|
|
13
|
+
// `nowrap-cols="*"` (or any non-numeric token) is rejected per-token with a
|
|
14
|
+
// warning rather than silently doing nothing — a typo'd column index should
|
|
15
|
+
// fail loudly. Valid range is 1-12: doc.css only ever ships nth-child rules
|
|
16
|
+
// up to that ceiling (comfortably past any real `cols=` in this codebase),
|
|
17
|
+
// and a document needing more is almost certainly better served by
|
|
18
|
+
// `[cols=]`'s own per-column `.nowrap` role below instead (every column).
|
|
19
|
+
const MAX_COLS = 12
|
|
20
|
+
const NOWRAP_ALL_ATTR = 'nowrap' // `[cols=...,%nowrap]` shorthand — every column
|
|
21
|
+
|
|
22
|
+
function parseColumnIndexes(raw, doc, table) {
|
|
23
|
+
const logger = doc.getLogger()
|
|
24
|
+
const indexes = []
|
|
25
|
+
raw
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((token) => token.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.forEach((token) => {
|
|
30
|
+
const n = Number(token)
|
|
31
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_COLS) {
|
|
32
|
+
logger.warn(
|
|
33
|
+
'nowrap-cols="' +
|
|
34
|
+
raw +
|
|
35
|
+
'" on table "' +
|
|
36
|
+
(table.getTitle() || '(untitled)') +
|
|
37
|
+
'" — ignoring invalid column "' +
|
|
38
|
+
token +
|
|
39
|
+
'"; expected an integer between 1 and ' +
|
|
40
|
+
MAX_COLS
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
indexes.push(n)
|
|
45
|
+
})
|
|
46
|
+
return indexes
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = function registerNowrapCols(registry) {
|
|
50
|
+
registry.treeProcessor(function () {
|
|
51
|
+
this.process(function (doc) {
|
|
52
|
+
doc.findBy({ context: 'table' }).forEach((table) => {
|
|
53
|
+
if (table.hasAttribute(NOWRAP_ALL_ATTR)) table.addRole('nowrap')
|
|
54
|
+
const raw = table.getAttribute('nowrap-cols')
|
|
55
|
+
if (!raw) return
|
|
56
|
+
parseColumnIndexes(String(raw), doc, table).forEach((n) => table.addRole('nowrap-' + n))
|
|
57
|
+
})
|
|
58
|
+
return doc
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Shared config between the two halves of the Shiki (GH-89) syntax
|
|
4
|
+
// highlighter: `shiki-prewarm.js` (an @inditextech/docouture-antora-extensions
|
|
5
|
+
// pipeline extension, builds the one shared highlighter instance
|
|
6
|
+
// asynchronously before any page converts) and `shiki-syntax-highlighter.js`
|
|
7
|
+
// (the Asciidoctor SyntaxHighlighter adapter that reads it back
|
|
8
|
+
// synchronously, once per source block). Both need the exact same language
|
|
9
|
+
// and theme set — if the prewarm loads a language the highlighter never
|
|
10
|
+
// asks for, or vice versa, `getLoadedLanguages()` and the actual grammar set
|
|
11
|
+
// silently disagree.
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
// Shiki's "css variables" dual-theme mode (`defaultColor: false` — see
|
|
15
|
+
// shiki-syntax-highlighter.js) emits `--shiki-light`/`--shiki-dark` custom
|
|
16
|
+
// properties instead of literal colours; ui-bundle's doc.css switches
|
|
17
|
+
// between them on `.ids-theme-dark`, the same toggle the rest of the site
|
|
18
|
+
// themes off. Named themes, not hljs's theme FILES (GH-12) — @shikijs/themes
|
|
19
|
+
// ships them as data, no CSS import/PostCSS scoping step needed at all.
|
|
20
|
+
//
|
|
21
|
+
// Both slots are the SAME theme — deliberately, not an oversight. The
|
|
22
|
+
// code surface itself (doc.css) is black in EITHER site theme by design
|
|
23
|
+
// (GH-12 again: the design keeps a dark code surface regardless of
|
|
24
|
+
// light/dark site mode), so there is no separate "light" palette to
|
|
25
|
+
// switch to in the first place — a genuinely light-background theme
|
|
26
|
+
// (tried: 'github-light') renders near-illegible dark-grey text on that
|
|
27
|
+
// black surface (verified empirically). 'github-dark'/'github-dark-dimmed'
|
|
28
|
+
// (matching hljs's own old GH-12 pairing) and 'monokai' were tried next
|
|
29
|
+
// and both worked; settled on 'dark-plus' — Shiki's bundled name for VS
|
|
30
|
+
// Code's own default "Dark+" theme (GH-89 follow-up request) — for the
|
|
31
|
+
// familiarity of VS Code's own colour choices. One name in both slots
|
|
32
|
+
// means the code blocks look identical regardless of site theme —
|
|
33
|
+
// intentional, not a placeholder half-migration.
|
|
34
|
+
LIGHT_THEME: 'dark-plus',
|
|
35
|
+
DARK_THEME: 'dark-plus',
|
|
36
|
+
|
|
37
|
+
// @shikijs/langs module ids to bundle into the one synchronous highlighter
|
|
38
|
+
// instance — see shiki-prewarm.js's own header for why this must be a
|
|
39
|
+
// fixed, synchronously-loadable list rather than Shiki's normal on-demand
|
|
40
|
+
// dynamic import. Mirrors the language set
|
|
41
|
+
// ui-bundle/src/js/vendor/highlight.bundle.ts used to register under
|
|
42
|
+
// highlight.js, plus 'html', 'typescript' and 'tsx' — actually used by
|
|
43
|
+
// authored content (`[source,html]`, `[source,ts]`, `[source,tsx]`) but
|
|
44
|
+
// never registered under the old hljs bundle, so those three blocks were
|
|
45
|
+
// never really syntax-highlighted before this.
|
|
46
|
+
LANGS: [
|
|
47
|
+
'asciidoc',
|
|
48
|
+
'bash',
|
|
49
|
+
'clojure',
|
|
50
|
+
'cpp',
|
|
51
|
+
'csharp',
|
|
52
|
+
'css',
|
|
53
|
+
'diff',
|
|
54
|
+
'dockerfile',
|
|
55
|
+
'elixir',
|
|
56
|
+
'go',
|
|
57
|
+
'groovy',
|
|
58
|
+
'haskell',
|
|
59
|
+
'html',
|
|
60
|
+
'java',
|
|
61
|
+
'javascript',
|
|
62
|
+
'json',
|
|
63
|
+
'julia',
|
|
64
|
+
'kotlin',
|
|
65
|
+
'lua',
|
|
66
|
+
'markdown',
|
|
67
|
+
'nix',
|
|
68
|
+
'objective-c',
|
|
69
|
+
'perl',
|
|
70
|
+
'php',
|
|
71
|
+
'properties',
|
|
72
|
+
'puppet',
|
|
73
|
+
'python',
|
|
74
|
+
'ruby',
|
|
75
|
+
'rust',
|
|
76
|
+
'scala',
|
|
77
|
+
'shell',
|
|
78
|
+
'sql',
|
|
79
|
+
'swift',
|
|
80
|
+
'tsx',
|
|
81
|
+
'typescript',
|
|
82
|
+
'xml',
|
|
83
|
+
'yaml',
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
// hljs's own name for a grammar Shiki registers under a different id.
|
|
87
|
+
// highlight.js's objectivec.js self-registers as 'objectivec' with alias
|
|
88
|
+
// 'objc'; Shiki's equivalent grammar is 'objective-c' (aliased to 'objc',
|
|
89
|
+
// not 'objectivec') — so `[source,objectivec]`, if ever authored, needs an
|
|
90
|
+
// explicit redirect. 'ts'/'tsx'/'js' need no entry here: Shiki auto-registers
|
|
91
|
+
// a loaded grammar's own `aliases` (typescript's include 'ts', 'cts', 'mts'),
|
|
92
|
+
// so `getLoadedLanguages()` already reports them once 'typescript' loads.
|
|
93
|
+
LANG_ALIASES: { objectivec: 'objective-c' },
|
|
94
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Shared handle to the one Shiki highlighter instance for an entire Antora
|
|
4
|
+
// build.
|
|
5
|
+
//
|
|
6
|
+
// `shiki-prewarm.js` — an @inditextech/docouture-antora-extensions PIPELINE
|
|
7
|
+
// extension, hooking Antora's own generator lifecycle — creates it exactly
|
|
8
|
+
// once, asynchronously, before any page is converted. This file's
|
|
9
|
+
// `highlight()` counterpart in shiki-syntax-highlighter.js is an ASCIIDOCTOR
|
|
10
|
+
// extension instead: called synchronously, per source block, by
|
|
11
|
+
// @asciidoctor/core 2.2's fully-synchronous Opal conversion loop, which
|
|
12
|
+
// cannot itself await anything (see asciidoc-extensions/lib/async-compat.js
|
|
13
|
+
// for the first instance of that exact constraint). Splitting the async
|
|
14
|
+
// setup from the sync use is GH-89's whole answer to the issue's own "Key
|
|
15
|
+
// Risk / Spike Required" section — this module is the seam between the two
|
|
16
|
+
// halves, since they live in different packages and are wired into the
|
|
17
|
+
// build by different playbook keys (`antora.extensions` vs
|
|
18
|
+
// `asciidoc.extensions`).
|
|
19
|
+
//
|
|
20
|
+
// A plain module-level variable is enough: @antora/site-generator runs one
|
|
21
|
+
// build per process (see `just build-site`), so there is exactly one
|
|
22
|
+
// instance to hold, and both packages `require()` the exact same file (node's
|
|
23
|
+
// module cache, not a singleton class) — no DI container needed.
|
|
24
|
+
let state = null
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
/**
|
|
28
|
+
* Called once by shiki-prewarm.js, after its async Shiki setup resolves.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} highlighter - the synchronous-capable Shiki core
|
|
31
|
+
* instance (`createHighlighterCoreSync`, built from an
|
|
32
|
+
* already-instantiated engine — see that file). Typed loosely as
|
|
33
|
+
* `object`, not `import('shiki/core').HighlighterCore`: shiki ships ESM-
|
|
34
|
+
* only types, and importing an ESM type from this CommonJS package needs
|
|
35
|
+
* a `resolution-mode` assertion this tsconfig doesn't carry — not worth
|
|
36
|
+
* adding for one call site when `noImplicitAny`/`noImplicitThis` are
|
|
37
|
+
* already off package-wide (see tsconfig.json's own header).
|
|
38
|
+
* @param {string} rootStyle - the `style` attribute value Shiki itself puts
|
|
39
|
+
* on the `<pre>` it generates for a throwaway probe block — the
|
|
40
|
+
* `--shiki-light`/`--shiki-dark`/`-*-bg` custom property declarations
|
|
41
|
+
* that carry each theme's default foreground/background. Needed because
|
|
42
|
+
* `highlight()` returns only the INNER content of the `<code>`
|
|
43
|
+
* Asciidoctor wraps it in (see that file's own header for why) — those
|
|
44
|
+
* declarations, which normally live on the `<pre>` element Shiki
|
|
45
|
+
* generates itself, have nowhere to go, so `highlight()` re-declares them
|
|
46
|
+
* verbatim on its own outermost wrapper span instead of trying to parse
|
|
47
|
+
* them back out of markup it already discarded.
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
50
|
+
set(highlighter, rootStyle) {
|
|
51
|
+
state = { highlighter, rootStyle }
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @returns {{ highlighter: object, rootStyle: string } | null} `null`
|
|
56
|
+
* before the prewarm listener has run — e.g. the ui-bundle preview
|
|
57
|
+
* harness, which never registers `@inditextech/docouture-antora-extensions`
|
|
58
|
+
* at all (see extensions.md: it has no content catalog and no Antora
|
|
59
|
+
* pipeline). `highlight()` degrades to plain escaped text in that case.
|
|
60
|
+
*/
|
|
61
|
+
get() {
|
|
62
|
+
return state
|
|
63
|
+
},
|
|
64
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { escapeHtml } = require('./html')
|
|
4
|
+
const shikiInstance = require('./shiki-instance')
|
|
5
|
+
const { LIGHT_THEME, DARK_THEME, LANG_ALIASES } = require('./shiki-config')
|
|
6
|
+
|
|
7
|
+
// Registers a custom Asciidoctor SyntaxHighlighter adapter, 'shiki' (GH-89),
|
|
8
|
+
// against `@asciidoctor/core ~2.2` — the version actually used to convert
|
|
9
|
+
// pages, via `antora@3.1.15` → `@antora/asciidoc-loader` (see
|
|
10
|
+
// .opencode/skills/asciidoc/reference/extensions.md's version table).
|
|
11
|
+
//
|
|
12
|
+
// THIS IS NOT AN `asciidoc.extensions` REGISTRY EXTENSION. Every other
|
|
13
|
+
// module in this package (label-macro.js, card-grid.js, …) is registered
|
|
14
|
+
// through the per-page `registry` object Antora hands `index.js`'s
|
|
15
|
+
// `register(registry)` — see that file's own header. A syntax highlighter is
|
|
16
|
+
// different: Asciidoctor.js exposes it as a GLOBAL, STATIC registry —
|
|
17
|
+
// `Asciidoctor.SyntaxHighlighter.register(name, functions)` — that lives on
|
|
18
|
+
// the `@asciidoctor/core` module object itself, not on any one page's
|
|
19
|
+
// registry instance. Requiring THIS file (done once, at the top of
|
|
20
|
+
// index.js, not inside `registerAll`) performs that global registration
|
|
21
|
+
// exactly once per process, thanks to node's module cache — which is
|
|
22
|
+
// correct here because pnpm dedupes `@asciidoctor/core` to the identical
|
|
23
|
+
// installed `~2.2` package Antora's own require of it resolves to, so both
|
|
24
|
+
// requires return the very same module singleton.
|
|
25
|
+
//
|
|
26
|
+
// `require('@asciidoctor/core')` is a FACTORY in 2.2 (call it to get the
|
|
27
|
+
// real API object) — see extensions.md's "2.2 — factory export" example.
|
|
28
|
+
//
|
|
29
|
+
// Required under an ALIAS ('asciidoctor-core-2.2', see package.json's
|
|
30
|
+
// `dependencies`), not the bare package name: this package's
|
|
31
|
+
// `devDependencies` already pins `@asciidoctor/core` to `~4.0.8` — a
|
|
32
|
+
// TYPE-ONLY dependency the rest of this package's extensions (accordion.js,
|
|
33
|
+
// cta.js, …) annotate their JSDoc against (see tsconfig.json's own header).
|
|
34
|
+
// pnpm can only link ONE physical `node_modules/@asciidoctor/core` per
|
|
35
|
+
// package, so declaring the real `~2.2` runtime dependency under that same
|
|
36
|
+
// bare name would silently replace those files' 4.0 types with 2.2's —
|
|
37
|
+
// verified empirically: it broke typecheck on every `AbstractBlock`
|
|
38
|
+
// reference elsewhere in this package. `npm:` aliasing keeps both physically
|
|
39
|
+
// installed side by side under different names.
|
|
40
|
+
//
|
|
41
|
+
// `2.2`'s own `.d.ts` types the module's export as a namespace object with no
|
|
42
|
+
// call signature, even though the actual JS bridge IS a factory function
|
|
43
|
+
// (verified empirically — the shape 2.2's own README documents, and the same
|
|
44
|
+
// shape `extensions.md`'s version table describes). `/** @type {any} */`
|
|
45
|
+
// bridges that one gap between the shipped types and the real runtime API,
|
|
46
|
+
// deliberately, rather than fighting it with an incorrect type assertion.
|
|
47
|
+
const asciidoctor = /** @type {any} */ (require('asciidoctor-core-2.2'))()
|
|
48
|
+
|
|
49
|
+
// Shiki always wraps its own output in `<pre class="shiki ..." style="...">
|
|
50
|
+
// <code>...</code></pre>` — there is no lower-level call that returns just
|
|
51
|
+
// the inner tokens. `highlight()` is only allowed to return the STRING that
|
|
52
|
+
// becomes the block's `content` (Asciidoctor's own `format()` — inherited
|
|
53
|
+
// unmodified from `SyntaxHighlighter::Base` since this adapter doesn't
|
|
54
|
+
// override it — wraps that in ITS OWN `<pre class="shiki highlight">
|
|
55
|
+
// <code data-lang="…">…</code></pre>`, which is what ui-bundle's
|
|
56
|
+
// 06-copy-to-clipboard.ts and doc.css actually key off: `pre.highlight` and
|
|
57
|
+
// `code[data-lang]`). So the outer tags Shiki generated have to be stripped
|
|
58
|
+
// back off; this regex depends on the exact shape Shiki 4.x's HTML renderer
|
|
59
|
+
// produces (verified empirically — see this package's README for how to
|
|
60
|
+
// re-check it against a future Shiki upgrade).
|
|
61
|
+
const SHIKI_WRAPPER_RX = /^<pre[^>]*><code[^>]*>([\s\S]*)<\/code>\s*<\/pre>\s*$/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Asciidoctor passes `lang: 'none'` for an unannotated `[source]` block —
|
|
65
|
+
* the same case highlight.bundle.ts used to alias to its own 'plaintext'
|
|
66
|
+
* grammar. Shiki needs no grammar loaded at all for this: 'text' is a
|
|
67
|
+
* reserved id its core always understands, same as 'plain'/'txt' upstream.
|
|
68
|
+
*/
|
|
69
|
+
function resolveLang(lang) {
|
|
70
|
+
if (!lang || lang === 'none') return 'text'
|
|
71
|
+
return LANG_ALIASES[lang] || lang
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} source - raw source text, already run through Asciidoctor's
|
|
76
|
+
* own callout extraction (this is the value `highlight()` receives, NOT
|
|
77
|
+
* `node.getSource()` — using the passed-in `source` rather than reading the
|
|
78
|
+
* node directly is what keeps `<1>` callout bubbles in source blocks
|
|
79
|
+
* working: Asciidoctor re-inserts their markup into this function's return
|
|
80
|
+
* value by string position, matched against `opts.callouts`).
|
|
81
|
+
*/
|
|
82
|
+
function highlightSource(source, lang) {
|
|
83
|
+
const instance = shikiInstance.get()
|
|
84
|
+
if (!instance) {
|
|
85
|
+
// The pre-warm listener (@inditextech/docouture-antora-extensions'
|
|
86
|
+
// shiki-prewarm.js) never ran — the ui-bundle preview harness, which has
|
|
87
|
+
// no Antora pipeline at all (see extensions.md), or a future caller that
|
|
88
|
+
// forgets to list the extension. Degrade to plain escaped text rather
|
|
89
|
+
// than throwing: a build with `runtime.log.failure_level: warn` should
|
|
90
|
+
// fail loudly on a real authoring mistake, not on missing highlighting.
|
|
91
|
+
return escapeHtml(source)
|
|
92
|
+
}
|
|
93
|
+
const { highlighter, rootStyle } = instance
|
|
94
|
+
const resolved = resolveLang(lang)
|
|
95
|
+
const effective = highlighter.getLoadedLanguages().includes(resolved) ? resolved : 'text'
|
|
96
|
+
const html = highlighter.codeToHtml(source, {
|
|
97
|
+
lang: effective,
|
|
98
|
+
themes: { light: LIGHT_THEME, dark: DARK_THEME },
|
|
99
|
+
// "CSS variables" dual-theme mode: emits `--shiki-light`/`--shiki-dark`
|
|
100
|
+
// custom properties per token instead of a literal colour, so ONE build
|
|
101
|
+
// output serves both the site's light and dark theme — see
|
|
102
|
+
// ui-bundle/src/css/doc.css's own `--shiki-*` rules for the switch.
|
|
103
|
+
defaultColor: false,
|
|
104
|
+
})
|
|
105
|
+
const match = SHIKI_WRAPPER_RX.exec(html.trim())
|
|
106
|
+
const inner = match ? match[1] : escapeHtml(source)
|
|
107
|
+
// Re-declares the default-colour variables Shiki put on its own (now
|
|
108
|
+
// discarded) `<pre>` — see shiki-instance.js's own header for why this
|
|
109
|
+
// can't just read them back off `html` on every call instead.
|
|
110
|
+
return `<span style="${rootStyle}">${inner}</span>`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
asciidoctor.SyntaxHighlighter.register('shiki', {
|
|
114
|
+
handlesHighlighting() {
|
|
115
|
+
return true
|
|
116
|
+
},
|
|
117
|
+
highlight(node, source, lang) {
|
|
118
|
+
return highlightSource(source, lang)
|
|
119
|
+
},
|
|
120
|
+
// Overridden rather than left to `SyntaxHighlighter::Base`'s own default:
|
|
121
|
+
// that default reads `self.pre_class`, an instance variable ONLY a real
|
|
122
|
+
// Ruby subclass's `initialize` sets (to its own registered `name`) —
|
|
123
|
+
// registering a plain JS functions object the way this file does (per
|
|
124
|
+
// `.register`'s own contract, see the header above) never runs that
|
|
125
|
+
// Ruby-side `initialize`, so `pre_class` comes back JS `undefined` and
|
|
126
|
+
// Base's format would literally emit `class="undefined highlight"`
|
|
127
|
+
// (verified empirically). Reimplementing `format` here sidesteps that
|
|
128
|
+
// entirely, for one extra benefit: `node.getContent()` — Asciidoctor's own
|
|
129
|
+
// already-substituted, callout-restored content, i.e. exactly `highlight()`'s
|
|
130
|
+
// return value above — is read directly, rather than trusting an
|
|
131
|
+
// ivar-driven default this adapter doesn't fully participate in.
|
|
132
|
+
format(node, lang, opts) {
|
|
133
|
+
const preClass = opts && opts.nowrap ? 'shiki highlight nowrap' : 'shiki highlight'
|
|
134
|
+
const langAttr = lang ? ' data-lang="' + escapeHtml(lang) + '"' : ''
|
|
135
|
+
return `<pre class="${preClass}"><code${langAttr}>${node.getContent()}</code></pre>`
|
|
136
|
+
},
|
|
137
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// GH-14 table-sizing follow-up: `display: block` on `table.tableblock` (the
|
|
4
|
+
// old doc.css mechanism) never actually sized the table — it sizes the
|
|
5
|
+
// BLOCK BOX, and a native table lays out its own anonymous table box inside
|
|
6
|
+
// that with `width: auto` (shrink-to-fit), completely ignoring the parent's
|
|
7
|
+
// width. There is no CSS-only fix: the anonymous box isn't selectable, so a
|
|
8
|
+
// real wrapper element is required. This postprocessor is that wrapper.
|
|
9
|
+
//
|
|
10
|
+
// A postprocessor, not a converter override: Antora hard-sets its own HTML5
|
|
11
|
+
// converter (@antora/asciidoc-loader/lib/load-asciidoc.js), so a custom
|
|
12
|
+
// converter would only ever run in the ui-bundle preview harness, not real
|
|
13
|
+
// site builds. A registry-scoped postprocessor runs identically in both
|
|
14
|
+
// (verified against @asciidoctor/core 4.0.8, embedded/standalone: false).
|
|
15
|
+
//
|
|
16
|
+
// Markup emitted, for every `<table class="tableblock …">…</table>`:
|
|
17
|
+
//
|
|
18
|
+
// <div class="tableblock-wrap">
|
|
19
|
+
// <div class="title">…</div> <!-- hoisted, if present -->
|
|
20
|
+
// <div class="tablecontainer"><table class="tableblock …">…</table></div>
|
|
21
|
+
// </div>
|
|
22
|
+
//
|
|
23
|
+
// Two wrapping divs, not one: `.tableblock-wrap` carries the width budget
|
|
24
|
+
// (and the title), `.tablecontainer` is the horizontal-scroll port — kept
|
|
25
|
+
// separate so the title does not scroll away with the table body.
|
|
26
|
+
//
|
|
27
|
+
// The `<caption class="title">` Asciidoctor emits for a `.Table title` is
|
|
28
|
+
// removed and its content re-emitted as a plain `.title` div OUTSIDE the
|
|
29
|
+
// table, at the wrapper's own (budget) width, rather than the table's own
|
|
30
|
+
// (possibly much narrower — `[width=]`/`%autowidth`) width: a caption is
|
|
31
|
+
// laid out as part of the table box it belongs to, so leaving it in place
|
|
32
|
+
// would have it wrap at the table's width instead of the wrapper's.
|
|
33
|
+
//
|
|
34
|
+
// Table/caption detection is a plain regex scan rather than parsing the
|
|
35
|
+
// converted HTML into a DOM: the postprocessor only ever sees Asciidoctor's
|
|
36
|
+
// own machine-generated output, whose `<table class="tableblock …">` /
|
|
37
|
+
// `</table>` shape is fixed, so a depth counter is sufficient and avoids an
|
|
38
|
+
// HTML-parser dependency for a single, narrow substitution.
|
|
39
|
+
const TABLE_OPEN_RX = /<table class="tableblock\b[^"]*"[^>]*>/g
|
|
40
|
+
const TABLE_TAG_RX = /<(\/?)table\b/g
|
|
41
|
+
const CAPTION_RX = /^\n?<caption class="title">([\s\S]*?)<\/caption>\n?/
|
|
42
|
+
// Idempotency guard: if this table is already the direct child of a
|
|
43
|
+
// `.tablecontainer` this same function placed it in, re-running (verified
|
|
44
|
+
// to happen in practice — `gulp preview`'s watch server re-registers this
|
|
45
|
+
// extension globally on every rebuild without restarting the node process,
|
|
46
|
+
// see index.js's own comment) must leave it alone rather than nesting a
|
|
47
|
+
// second `.tableblock-wrap` around the first, each one re-applying (and
|
|
48
|
+
// compounding) the same width cap.
|
|
49
|
+
const ALREADY_WRAPPED_RX = /<div class="tablecontainer">$/
|
|
50
|
+
// `table-width.js`'s own stash key (doc.findBy-order array of per-table
|
|
51
|
+
// literal CSS widths, `null` where the author didn't set one) — read here
|
|
52
|
+
// by the SAME index, since both this scan and that tree processor's
|
|
53
|
+
// `doc.findBy({context:'table'})` walk the document in the same order
|
|
54
|
+
// (verified empirically).
|
|
55
|
+
const { STASH_KEY: WIDTHS_STASH_KEY } = require('./table-width')
|
|
56
|
+
|
|
57
|
+
function wrapTables(html, widths = []) {
|
|
58
|
+
let result = ''
|
|
59
|
+
let cursor = 0
|
|
60
|
+
let match
|
|
61
|
+
let tableIndex = -1
|
|
62
|
+
while ((match = TABLE_OPEN_RX.exec(html))) {
|
|
63
|
+
tableIndex += 1
|
|
64
|
+
if (ALREADY_WRAPPED_RX.test(html.slice(0, match.index))) continue
|
|
65
|
+
|
|
66
|
+
const openTagEnd = match.index + match[0].length
|
|
67
|
+
// Depth-count `<table` / `</table>` tags from just past the opening tag
|
|
68
|
+
// to find THIS table's own matching close tag, tolerating any table
|
|
69
|
+
// nested inside a cell (a legitimate, if rare, authoring case).
|
|
70
|
+
TABLE_TAG_RX.lastIndex = openTagEnd
|
|
71
|
+
let depth = 1
|
|
72
|
+
let tagMatch
|
|
73
|
+
let closeEnd = -1
|
|
74
|
+
while ((tagMatch = TABLE_TAG_RX.exec(html))) {
|
|
75
|
+
depth += tagMatch[1] ? -1 : 1
|
|
76
|
+
if (depth === 0) {
|
|
77
|
+
closeEnd = html.indexOf('>', TABLE_TAG_RX.lastIndex) + 1
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (closeEnd === -1 || closeEnd === 0) break // malformed input; leave the rest untouched
|
|
82
|
+
|
|
83
|
+
result += html.slice(cursor, match.index)
|
|
84
|
+
let inner = html.slice(match.index, closeEnd)
|
|
85
|
+
|
|
86
|
+
// `table-width.js`'s literal CSS width (e.g. `2000px`) — set as an
|
|
87
|
+
// inline `style`, which wins over `.stretch`/`.fit-content` (external
|
|
88
|
+
// stylesheet rules) unconditionally, regardless of specificity, no
|
|
89
|
+
// `!important` needed. Spliced into the OPEN TAG specifically (not the
|
|
90
|
+
// whole `inner`, which also contains the table body) so a `style="…"`
|
|
91
|
+
// the author's own raw HTML passthrough might already carry is
|
|
92
|
+
// extended rather than clobbered.
|
|
93
|
+
//
|
|
94
|
+
// `bodyStart` (where the caption search below begins) is recomputed
|
|
95
|
+
// from the SPLICED open tag's own new length, not the original
|
|
96
|
+
// `openTagEnd - match.index` — that offset is in the pre-splice
|
|
97
|
+
// string's coordinates, and injecting a `style="…"` attribute makes
|
|
98
|
+
// the tag longer, so re-using it pointed a few characters short of the
|
|
99
|
+
// real body and silently broke caption hoisting (verified: the caption
|
|
100
|
+
// regex, anchored at the very start of what it thinks is the body,
|
|
101
|
+
// stopped matching entirely once a table also had a `table-width`).
|
|
102
|
+
const width = widths[tableIndex]
|
|
103
|
+
let bodyStart = openTagEnd - match.index
|
|
104
|
+
if (width) {
|
|
105
|
+
const openTag = inner.slice(0, bodyStart)
|
|
106
|
+
const styledOpenTag = /\sstyle="/.test(openTag)
|
|
107
|
+
? openTag.replace(/\sstyle="/, ' style="width:' + width + ';')
|
|
108
|
+
: openTag.replace(/>$/, ' style="width:' + width + '">')
|
|
109
|
+
inner = styledOpenTag + inner.slice(bodyStart)
|
|
110
|
+
bodyStart = styledOpenTag.length
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let title = null
|
|
114
|
+
const afterOpenTag = inner.slice(bodyStart)
|
|
115
|
+
const captionMatch = afterOpenTag.match(CAPTION_RX)
|
|
116
|
+
if (captionMatch) {
|
|
117
|
+
title = captionMatch[1]
|
|
118
|
+
inner = inner.slice(0, bodyStart) + afterOpenTag.slice(captionMatch[0].length)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result +=
|
|
122
|
+
'<div class="tableblock-wrap">' +
|
|
123
|
+
(title !== null ? '<div class="title">' + title + '</div>' : '') +
|
|
124
|
+
'<div class="tablecontainer">' +
|
|
125
|
+
inner +
|
|
126
|
+
'</div></div>'
|
|
127
|
+
|
|
128
|
+
cursor = closeEnd
|
|
129
|
+
TABLE_OPEN_RX.lastIndex = cursor
|
|
130
|
+
}
|
|
131
|
+
result += html.slice(cursor)
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = function registerTableContainer(registry) {
|
|
136
|
+
registry.postprocessor(function () {
|
|
137
|
+
this.process(function (doc, output) {
|
|
138
|
+
return wrapTables(output, doc[WIDTHS_STASH_KEY])
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports.wrapTables = wrapTables
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// `[table-width=2000px,cols="1,1"]` — an explicit, literal CSS width for a
|
|
4
|
+
// table, bypassing Asciidoctor's own `width=` attribute entirely: that
|
|
5
|
+
// attribute is percentage-only and HARD-CLAMPED to 1-100 (verified against
|
|
6
|
+
// `@asciidoctor/core`'s own `table.js` — `width=2000px` parses to the
|
|
7
|
+
// numeric prefix 2000, sees it's `> 100`, and silently resets it to 100,
|
|
8
|
+
// i.e. exactly the unmarked default, `.stretch`). There is no core AsciiDoc
|
|
9
|
+
// syntax for an absolute width on a table; this is a separate, custom
|
|
10
|
+
// attribute for it.
|
|
11
|
+
//
|
|
12
|
+
// A tree processor is the only hook that can read the RAW attribute value
|
|
13
|
+
// before Asciidoctor's own Table constructor has already computed (and
|
|
14
|
+
// clamped) `tablepcwidth` from it — but the built-in html5 converter only
|
|
15
|
+
// ever emits `id`/`class`/the derived `width="N%"` for a `<table>` tag (see
|
|
16
|
+
// its own table-tag-building code), no generic passthrough for an arbitrary
|
|
17
|
+
// attribute to an inline `style`. So this doesn't touch Asciidoctor's own
|
|
18
|
+
// `width` attribute at all (an author can still combine `width=50%` — a
|
|
19
|
+
// PERCENTAGE OF THE inline style set here — with `table-width`, though
|
|
20
|
+
// that's an unusual thing to want) and instead hands the value to
|
|
21
|
+
// table-container.js's postprocessor, the other half of this mechanism,
|
|
22
|
+
// via a plain array stashed on the `doc` object itself: `doc.findBy` (here)
|
|
23
|
+
// and the postprocessor's own table-order HTML scan (table-container.js)
|
|
24
|
+
// walk the document in the same order (verified empirically), so entry `i`
|
|
25
|
+
// in the array is table `i`'s width, `null` where the attribute is absent.
|
|
26
|
+
//
|
|
27
|
+
// Verified in the CSS as already correct for a literal width with no
|
|
28
|
+
// further changes: `table.tableblock` carries no width rule of its own
|
|
29
|
+
// (only `.stretch`/`.fit-content`, both irrelevant once an inline `style`
|
|
30
|
+
// wins over them unconditionally), and `.tablecontainer`'s `overflow-x:
|
|
31
|
+
// auto` scrolls a table wider than `.tableblock-wrap`'s own 150%-capped
|
|
32
|
+
// budget rather than blowing out the page — exactly the "respect the
|
|
33
|
+
// explicit width, but the SURROUNDING box stays capped" behaviour the
|
|
34
|
+
// percentage form already has.
|
|
35
|
+
const ATTR_NAME = 'table-width'
|
|
36
|
+
const VALID_RX = /^\d+(?:\.\d+)?(?:px|rem|em|ch|vw|vh|%)?$/
|
|
37
|
+
const STASH_KEY = '$docoutureTableWidths'
|
|
38
|
+
|
|
39
|
+
function parseWidth(raw, doc, table) {
|
|
40
|
+
const trimmed = String(raw).trim()
|
|
41
|
+
if (!VALID_RX.test(trimmed)) {
|
|
42
|
+
doc
|
|
43
|
+
.getLogger()
|
|
44
|
+
.warn(
|
|
45
|
+
ATTR_NAME +
|
|
46
|
+
'="' +
|
|
47
|
+
raw +
|
|
48
|
+
'" on table "' +
|
|
49
|
+
(table.getTitle() || '(untitled)') +
|
|
50
|
+
'" — ignoring invalid length; expected a plain number (px assumed) or a number with one of px/rem/em/ch/vw/vh/%'
|
|
51
|
+
)
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return /\d$/.test(trimmed) ? trimmed + 'px' : trimmed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = function registerTableWidth(registry) {
|
|
58
|
+
registry.treeProcessor(function () {
|
|
59
|
+
this.process(function (doc) {
|
|
60
|
+
const widths = (doc[STASH_KEY] = [])
|
|
61
|
+
doc.findBy({ context: 'table' }).forEach((table) => {
|
|
62
|
+
const raw = table.getAttribute(ATTR_NAME)
|
|
63
|
+
widths.push(raw ? parseWidth(raw, doc, table) : null)
|
|
64
|
+
})
|
|
65
|
+
return doc
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports.STASH_KEY = STASH_KEY
|