@asciidoctor/core 4.0.0 → 4.0.2
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/build/browser/index.js +230 -81
- package/build/node/index.cjs +277 -93
- package/package.json +2 -2
- package/src/abstract_node.js +6 -3
- package/src/browser/reader.js +2 -2
- package/src/browser.js +8 -1
- package/src/converter/html5.js +39 -9
- package/src/converter/manpage.js +1 -1
- package/src/converter/template.js +47 -13
- package/src/converter.js +22 -9
- package/src/document.js +71 -21
- package/src/index.js +8 -1
- package/src/logging.js +41 -10
- package/src/parser.js +13 -2
- package/src/reader.js +25 -13
- package/src/substitutors.js +9 -9
- package/types/abstract_node.d.ts +4 -3
- package/types/document.d.ts +16 -0
- package/types/index.d.cts +2 -1
- package/types/index.d.ts +2 -1
- package/types/logging.d.ts +49 -5
- package/types/reader.d.ts +2 -2
- package/types/substitutors.d.ts +18 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asciidoctor/core",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Asciidoctor.js: AsciiDoc in JavaScript powered by Asciidoctor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/node/index.cjs",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"homepage": "https://github.com/asciidoctor/asciidoctor.js",
|
|
68
68
|
"volta": {
|
|
69
|
-
"node": "24.
|
|
69
|
+
"node": "24.18.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@biomejs/biome": "^2.4.13",
|
package/src/abstract_node.js
CHANGED
|
@@ -461,9 +461,10 @@ export class AbstractNode {
|
|
|
461
461
|
* Construct a URI reference or data URI to the target image.
|
|
462
462
|
*
|
|
463
463
|
* If the target image is already a URI it is left untouched (unless data-uri
|
|
464
|
-
* conversion is requested).
|
|
465
|
-
*
|
|
466
|
-
* the
|
|
464
|
+
* conversion is requested). If the target image is a data URI, then it is
|
|
465
|
+
* already an embedded image, so it is returned as-is. The image is resolved
|
|
466
|
+
* relative to the directory named by assetDirKey. When data-uri is enabled and
|
|
467
|
+
* the safe level permits, the image is embedded as a Base64 data URI.
|
|
467
468
|
*
|
|
468
469
|
* NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
|
|
469
470
|
* and the resolved image URL is a remote URI, this method returns a Promise
|
|
@@ -474,6 +475,8 @@ export class AbstractNode {
|
|
|
474
475
|
* @returns {Promise<string>} a Promise resolving to a String reference or data URI.
|
|
475
476
|
*/
|
|
476
477
|
async imageUri(targetImage, assetDirKey = 'imagesdir') {
|
|
478
|
+
// A data URI is already an embedded image, so use it as-is rather than reading or re-encoding it.
|
|
479
|
+
if (targetImage.startsWith('data:')) return targetImage
|
|
477
480
|
const doc = this.document
|
|
478
481
|
if (doc.safe < SafeMode.SECURE && doc.hasAttribute('data-uri')) {
|
|
479
482
|
let imagesBase
|
package/src/browser/reader.js
CHANGED
|
@@ -92,7 +92,7 @@ export function resolveBrowserIncludePath(reader, target, attrlist) {
|
|
|
92
92
|
// ── Rule 2: target is an absolute URL (http:// / https:// / …) ───────────
|
|
93
93
|
} else if (isUriish(pTarget)) {
|
|
94
94
|
const descends = pathResolver.descendsFrom(pTarget, baseDir)
|
|
95
|
-
if (descends === false && !doc.
|
|
95
|
+
if (descends === false && !doc.hasAttribute('allow-uri-read')) {
|
|
96
96
|
return reader.replaceNextLine(_linkReplacement(reader, target, attrlist))
|
|
97
97
|
}
|
|
98
98
|
incPath = relpath = pTarget
|
|
@@ -126,7 +126,7 @@ export function resolveBrowserIncludePath(reader, target, attrlist) {
|
|
|
126
126
|
} else {
|
|
127
127
|
// Nested include: context dir is an absolute URL.
|
|
128
128
|
const ctxDescends = pathResolver.descendsFrom(ctxDir, baseDir)
|
|
129
|
-
if (ctxDescends !== false || doc.
|
|
129
|
+
if (ctxDescends !== false || doc.hasAttribute('allow-uri-read')) {
|
|
130
130
|
incPath = `${ctxDir}/${pTarget}`
|
|
131
131
|
relpath = ctxDescends !== false ? incPath.slice(ctxDescends) : pTarget
|
|
132
132
|
} else {
|
package/src/browser.js
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
ImageReference,
|
|
10
10
|
RevisionInfo,
|
|
11
11
|
} from './document.js'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Logger,
|
|
14
|
+
LoggerManager,
|
|
15
|
+
LogMessage,
|
|
16
|
+
MemoryLogger,
|
|
17
|
+
NullLogger,
|
|
18
|
+
} from './logging.js'
|
|
13
19
|
import { HttpCache, MemoryHttpCache, HttpCacheManager } from './http_cache.js'
|
|
14
20
|
import { SafeMode, ContentModel } from './constants.js'
|
|
15
21
|
import { Timings } from './timings.js'
|
|
@@ -110,6 +116,7 @@ export {
|
|
|
110
116
|
Reader,
|
|
111
117
|
SyntaxHighlighterBase,
|
|
112
118
|
LoggerManager,
|
|
119
|
+
LogMessage,
|
|
113
120
|
MemoryLogger,
|
|
114
121
|
NullLogger,
|
|
115
122
|
HttpCache,
|
package/src/converter/html5.js
CHANGED
|
@@ -116,7 +116,7 @@ export default class Html5Converter extends ConverterBase {
|
|
|
116
116
|
let reproducible
|
|
117
117
|
if (!(reproducible = node.hasAttribute('reproducible'))) {
|
|
118
118
|
result.push(
|
|
119
|
-
`<meta name="generator" content="Asciidoctor ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
119
|
+
`<meta name="generator" content="Asciidoctor.js ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
120
120
|
)
|
|
121
121
|
}
|
|
122
122
|
if (node.hasAttribute('app-name')) {
|
|
@@ -833,7 +833,9 @@ ${await node.content()}
|
|
|
833
833
|
const slash = this._voidSlash
|
|
834
834
|
let img, src
|
|
835
835
|
if (
|
|
836
|
-
(node.hasAttribute('format', 'svg') ||
|
|
836
|
+
(node.hasAttribute('format', 'svg') ||
|
|
837
|
+
target.includes('.svg') ||
|
|
838
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
837
839
|
node.document.safe < SafeMode.SECURE
|
|
838
840
|
) {
|
|
839
841
|
if (node.hasOption('inline')) {
|
|
@@ -1674,7 +1676,9 @@ Your browser does not support the video tag.
|
|
|
1674
1676
|
if (node.hasAttribute('title'))
|
|
1675
1677
|
attrs += ` title="${node.getAttribute('title')}"`
|
|
1676
1678
|
if (
|
|
1677
|
-
(node.hasAttribute('format', 'svg') ||
|
|
1679
|
+
(node.hasAttribute('format', 'svg') ||
|
|
1680
|
+
target.includes('.svg') ||
|
|
1681
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
1678
1682
|
node.document.safe < SafeMode.SECURE
|
|
1679
1683
|
) {
|
|
1680
1684
|
if (node.hasOption('inline')) {
|
|
@@ -1767,12 +1771,17 @@ Your browser does not support the video tag.
|
|
|
1767
1771
|
|
|
1768
1772
|
// NOTE expose readSvgContents for Bespoke converter
|
|
1769
1773
|
async readSvgContents(node, target) {
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1774
|
+
// A data-URI carries the SVG in the target itself (e.g. an embedded diagram
|
|
1775
|
+
// produced with `:data-uri:`), so decode it directly rather than trying to
|
|
1776
|
+
// read it as a file or remote URI.
|
|
1777
|
+
let svg = target.startsWith('data:')
|
|
1778
|
+
? this._decodeDataUri(target)
|
|
1779
|
+
: await node.readContents(target, {
|
|
1780
|
+
start: node.document.getAttribute('imagesdir'),
|
|
1781
|
+
normalize: true,
|
|
1782
|
+
label: 'SVG',
|
|
1783
|
+
warnIfEmpty: true,
|
|
1784
|
+
})
|
|
1776
1785
|
if (!svg) return null
|
|
1777
1786
|
if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '')
|
|
1778
1787
|
// Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
|
|
@@ -1804,6 +1813,27 @@ Your browser does not support the video tag.
|
|
|
1804
1813
|
|
|
1805
1814
|
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
1806
1815
|
|
|
1816
|
+
/**
|
|
1817
|
+
* Decode an inline `data:` URI to its text contents (e.g. an SVG document) so
|
|
1818
|
+
* an image whose target is a data-URI can be embedded inline. Supports both
|
|
1819
|
+
* Base64 (`;base64,`) and percent-encoded payloads. Returns null when the
|
|
1820
|
+
* payload is missing.
|
|
1821
|
+
*
|
|
1822
|
+
* @internal
|
|
1823
|
+
* @private
|
|
1824
|
+
*/
|
|
1825
|
+
_decodeDataUri(target) {
|
|
1826
|
+
const comma = target.indexOf(',')
|
|
1827
|
+
if (comma === -1) return null
|
|
1828
|
+
const meta = target.slice('data:'.length, comma)
|
|
1829
|
+
const data = target.slice(comma + 1)
|
|
1830
|
+
if (/;base64\b/i.test(meta)) {
|
|
1831
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0))
|
|
1832
|
+
return new TextDecoder('utf-8').decode(bytes)
|
|
1833
|
+
}
|
|
1834
|
+
return decodeURIComponent(data)
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1807
1837
|
/**
|
|
1808
1838
|
* @internal
|
|
1809
1839
|
* @private
|
package/src/converter/manpage.js
CHANGED
|
@@ -108,7 +108,7 @@ export default class ManPageConverter extends ConverterBase {
|
|
|
108
108
|
`'\\" t
|
|
109
109
|
.\\" Title: ${mantitle}
|
|
110
110
|
.\\" Author: ${node.hasAttribute('authors') ? node.getAttribute('authors') : '[see the "AUTHOR(S)" section]'}
|
|
111
|
-
.\\" Generator: Asciidoctor ${node.getAttribute('asciidoctor-version')}`,
|
|
111
|
+
.\\" Generator: Asciidoctor.js ${node.getAttribute('asciidoctor-version')}`,
|
|
112
112
|
]
|
|
113
113
|
|
|
114
114
|
if (docdate) result.push(`.\\" Date: ${docdate}`)
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// - Ruby File.directory?/File.file? → fsp.stat().isDirectory()/isFile() (async).
|
|
9
9
|
// - Ruby File.basename / File.expand_path → node:path basename / resolve.
|
|
10
10
|
// - PathResolver.system_path → pathResolver.systemPath().
|
|
11
|
-
// - template.render(node, opts) → template.render({node, opts, helpers}).
|
|
12
|
-
// - Helpers loaded from helpers.js/helpers.cjs; can export configure(enginesContext).
|
|
11
|
+
// - template.render(node, opts) → template.render({node, opts, helpers}).// - Helpers loaded from helpers.js/helpers.cjs/helpers.mjs; can export configure(enginesContext).
|
|
13
12
|
// - Custom engines registered via TemplateConverter.TemplateEngine.register(ext, adapter).
|
|
14
13
|
// - Thread safety / Mutex → not needed (single-threaded JS).
|
|
15
14
|
// - load_eruby / eRuby support → not applicable in JS environment.
|
|
@@ -19,6 +18,7 @@
|
|
|
19
18
|
import { ConverterBase } from '../converter.js'
|
|
20
19
|
import { PathResolver } from '../path_resolver.js'
|
|
21
20
|
import { createRequire } from 'node:module'
|
|
21
|
+
import { pathToFileURL } from 'node:url'
|
|
22
22
|
import { promises as fsp } from 'node:fs'
|
|
23
23
|
import path from 'node:path'
|
|
24
24
|
|
|
@@ -288,7 +288,11 @@ export class TemplateConverter extends ConverterBase {
|
|
|
288
288
|
if (!(await _isFile(file))) continue
|
|
289
289
|
|
|
290
290
|
// Collect helpers separately; process after all templates.
|
|
291
|
-
if (
|
|
291
|
+
if (
|
|
292
|
+
basename === 'helpers.js' ||
|
|
293
|
+
basename === 'helpers.cjs' ||
|
|
294
|
+
basename === 'helpers.mjs'
|
|
295
|
+
) {
|
|
292
296
|
helpersFile = file
|
|
293
297
|
continue
|
|
294
298
|
}
|
|
@@ -356,8 +360,8 @@ export class TemplateConverter extends ConverterBase {
|
|
|
356
360
|
const pugOpts = { ...(this.engineOptions.pug ?? {}), filename: file }
|
|
357
361
|
const renderFn = pug.compileFile(file, pugOpts)
|
|
358
362
|
template = { render: renderFn, file }
|
|
359
|
-
} else if (ext === 'js' || ext === 'cjs') {
|
|
360
|
-
const renderFn =
|
|
363
|
+
} else if (ext === 'js' || ext === 'cjs' || ext === 'mjs') {
|
|
364
|
+
const renderFn = await _loadModule(file, ext)
|
|
361
365
|
template = { render: renderFn, file }
|
|
362
366
|
} else {
|
|
363
367
|
// Fall back to custom TemplateEngine registry.
|
|
@@ -372,16 +376,20 @@ export class TemplateConverter extends ConverterBase {
|
|
|
372
376
|
result[name] = template
|
|
373
377
|
}
|
|
374
378
|
|
|
375
|
-
// Load helpers if found (or if a helpers
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Load helpers if found (or if a helpers file exists at the top of the dir).
|
|
380
|
+
if (!helpersFile) {
|
|
381
|
+
for (const ext of ['js', 'cjs', 'mjs']) {
|
|
382
|
+
const fallback = path.join(templateDir, `helpers.${ext}`)
|
|
383
|
+
if (await _isFile(fallback)) {
|
|
384
|
+
helpersFile = fallback
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
382
389
|
|
|
383
390
|
if (helpersFile) {
|
|
384
|
-
const
|
|
391
|
+
const helpersExt = helpersFile.slice(helpersFile.lastIndexOf('.') + 1)
|
|
392
|
+
const ctx = await _loadModule(helpersFile, helpersExt)
|
|
385
393
|
if (typeof ctx.configure === 'function') ctx.configure(enginesCtx)
|
|
386
394
|
result['helpers.js'] = { file: helpersFile, ctx }
|
|
387
395
|
}
|
|
@@ -409,6 +417,32 @@ export class TemplateConverter extends ConverterBase {
|
|
|
409
417
|
|
|
410
418
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
411
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Load a JavaScript module (template render function or helpers object) by
|
|
422
|
+
* file extension.
|
|
423
|
+
*
|
|
424
|
+
* - `.cjs` files are always CommonJS: require() returns `module.exports`.
|
|
425
|
+
* - `.js` and `.mjs` files are loaded with a dynamic `import()`, which Node
|
|
426
|
+
* resolves as either ES modules or CommonJS. The export is taken from the
|
|
427
|
+
* module's default export (`export default …` in ESM, `module.exports = …`
|
|
428
|
+
* in CommonJS via interop); when there is no default export the module
|
|
429
|
+
* namespace is returned instead, so ESM helpers exposing named exports
|
|
430
|
+
* (`export function configure() {}`) work too.
|
|
431
|
+
* @param {string} file - absolute path to the module file
|
|
432
|
+
* @param {string} ext - file extension without the leading dot ('js' | 'cjs' | 'mjs')
|
|
433
|
+
* @returns {Promise<*>} the module's default export, or its namespace
|
|
434
|
+
* @internal
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
async function _loadModule(file, ext) {
|
|
438
|
+
if (ext === 'cjs') {
|
|
439
|
+
const mod = _require(file)
|
|
440
|
+
return mod?.default ? mod.default : mod
|
|
441
|
+
}
|
|
442
|
+
const mod = await import(pathToFileURL(file).href)
|
|
443
|
+
return mod.default ?? mod
|
|
444
|
+
}
|
|
445
|
+
|
|
412
446
|
/**
|
|
413
447
|
* @internal
|
|
414
448
|
* @private
|
package/src/converter.js
CHANGED
|
@@ -26,7 +26,18 @@ import { TrailingDigitsRx } from './rx.js'
|
|
|
26
26
|
export function applyBackendTraits(instance) {
|
|
27
27
|
instance._backendTraits = null
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Install a Ruby-style trait accessor method, but never clobber a flat string
|
|
30
|
+
// property the converter already declared (convention #2). Overwriting e.g.
|
|
31
|
+
// `converter.outfilesuffix = '.html'` with a method would silently turn the
|
|
32
|
+
// author's string into a function; the backend traits stay reachable through
|
|
33
|
+
// `_getBackendTraits()` instead.
|
|
34
|
+
const defineTraitAccessor = (name, fn) => {
|
|
35
|
+
const existing = Object.getOwnPropertyDescriptor(instance, name)
|
|
36
|
+
if (existing && typeof existing.value !== 'function') return
|
|
37
|
+
instance[name] = fn
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
defineTraitAccessor('basebackend', function (value = null) {
|
|
30
41
|
if (value) {
|
|
31
42
|
const traits = (this._backendTraits ??= {})
|
|
32
43
|
traits.basebackend = value
|
|
@@ -40,19 +51,19 @@ export function applyBackendTraits(instance) {
|
|
|
40
51
|
return value
|
|
41
52
|
}
|
|
42
53
|
return this._getBackendTraits().basebackend
|
|
43
|
-
}
|
|
44
|
-
|
|
54
|
+
})
|
|
55
|
+
defineTraitAccessor('filetype', function (value = null) {
|
|
45
56
|
if (value) return (this._getBackendTraits().filetype = value)
|
|
46
57
|
return this._getBackendTraits().filetype
|
|
47
|
-
}
|
|
48
|
-
|
|
58
|
+
})
|
|
59
|
+
defineTraitAccessor('htmlsyntax', function (value = null) {
|
|
49
60
|
if (value) return (this._getBackendTraits().htmlsyntax = value)
|
|
50
61
|
return this._getBackendTraits().htmlsyntax
|
|
51
|
-
}
|
|
52
|
-
|
|
62
|
+
})
|
|
63
|
+
defineTraitAccessor('outfilesuffix', function (value = null) {
|
|
53
64
|
if (value) return (this._getBackendTraits().outfilesuffix = value)
|
|
54
65
|
return this._getBackendTraits().outfilesuffix
|
|
55
|
-
}
|
|
66
|
+
})
|
|
56
67
|
instance.supportsTemplates = function (value = true) {
|
|
57
68
|
this._getBackendTraits().supportsTemplates = value
|
|
58
69
|
}
|
|
@@ -136,7 +147,9 @@ export function normalizeConverter(converter, backend) {
|
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
// Apply the BackendTraits mixin so Document can
|
|
150
|
+
// Apply the BackendTraits mixin so Document can read traits via
|
|
151
|
+
// _getBackendTraits(). Flat string properties (convention #2) are preserved:
|
|
152
|
+
// applyBackendTraits does not overwrite an existing same-named data property.
|
|
140
153
|
applyBackendTraits(converter)
|
|
141
154
|
if (traits) {
|
|
142
155
|
converter._backendTraits = traits
|
package/src/document.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// - Mutex / thread-safety not applicable in single-threaded JS.
|
|
13
13
|
// - `instance_variable_get :@attribute_overrides` → direct property access.
|
|
14
14
|
|
|
15
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
15
16
|
import { AbstractBlock } from './abstract_block.js'
|
|
16
17
|
import { Section } from './section.js'
|
|
17
18
|
import { Inline } from './inline.js'
|
|
@@ -400,7 +401,7 @@ export class Document extends AbstractBlock {
|
|
|
400
401
|
|
|
401
402
|
const attrOverrides = this._attributeOverrides
|
|
402
403
|
attrOverrides.asciidoctor = ''
|
|
403
|
-
attrOverrides['asciidoctor-version'] =
|
|
404
|
+
attrOverrides['asciidoctor-version'] = packageJson.version
|
|
404
405
|
|
|
405
406
|
const safeModeName = SafeMode.nameForValue(this.safe)
|
|
406
407
|
attrOverrides['safe-mode-name'] = safeModeName
|
|
@@ -587,13 +588,21 @@ export class Document extends AbstractBlock {
|
|
|
587
588
|
}
|
|
588
589
|
|
|
589
590
|
// Pre-compute all async text values (titles, list item text, cell text, reftexts)
|
|
590
|
-
// so that synchronous getters work correctly during conversion.
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
591
|
+
// so that synchronous getters work correctly during conversion. _resolveAllTexts
|
|
592
|
+
// replays attribute entries in document order (mirroring conversion) so body-level
|
|
593
|
+
// attribute (re)assignments are in scope; snapshot and restore the document
|
|
594
|
+
// attributes so the downstream steps and conversion still start from the restored
|
|
595
|
+
// (header) state, matching Ruby's restore_attributes-before-convert invariant.
|
|
596
|
+
//
|
|
597
|
+
// This runs in two passes because list item / table cell / dlist text may contain
|
|
598
|
+
// natural cross-references (e.g. <<Some section title>>) that resolve against the
|
|
599
|
+
// reftext→id map. Pass 1 substitutes titles and reftexts only, so every section
|
|
600
|
+
// reftext is known; the map is then built; pass 2 substitutes the block content
|
|
601
|
+
// text, where resolveId() can now match those natural references.
|
|
602
|
+
const attributesSnapshot = { ...this.attributes }
|
|
603
|
+
// Pass 1: titles + reftexts (no block content text yet).
|
|
604
|
+
await this._resolveAllTexts(this, false)
|
|
605
|
+
this._restoreAttributeSnapshot(attributesSnapshot)
|
|
597
606
|
// Pre-compute reftext for all registered inline anchor nodes.
|
|
598
607
|
for (const ref of Object.values(this.catalog.refs)) {
|
|
599
608
|
if (ref && typeof ref.precomputeReftext === 'function') {
|
|
@@ -602,6 +611,15 @@ export class Document extends AbstractBlock {
|
|
|
602
611
|
}
|
|
603
612
|
// Build the reftext→id lookup map so that resolveId() is synchronous.
|
|
604
613
|
await this._buildReftextsMap()
|
|
614
|
+
// Pass 2: list item / table cell / dlist text, now that natural cross-references
|
|
615
|
+
// can be resolved against the reftext→id map.
|
|
616
|
+
await this._resolveAllTexts(this, true)
|
|
617
|
+
this._restoreAttributeSnapshot(attributesSnapshot)
|
|
618
|
+
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
619
|
+
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
620
|
+
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
621
|
+
delete this.attributes['footnote-number']
|
|
622
|
+
delete this._counters['footnote-number']
|
|
605
623
|
|
|
606
624
|
this._parsed = true
|
|
607
625
|
return this
|
|
@@ -1656,12 +1674,39 @@ export class Document extends AbstractBlock {
|
|
|
1656
1674
|
: ['attributes']
|
|
1657
1675
|
}
|
|
1658
1676
|
|
|
1677
|
+
/**
|
|
1678
|
+
* @private
|
|
1679
|
+
* Restore the document attributes to a previously captured snapshot, discarding any
|
|
1680
|
+
* body-level (re)assignments replayed while pre-computing text. Mirrors Ruby's
|
|
1681
|
+
* restore_attributes-before-convert invariant.
|
|
1682
|
+
* @param {Object} snapshot - The attributes snapshot to restore.
|
|
1683
|
+
*/
|
|
1684
|
+
_restoreAttributeSnapshot(snapshot) {
|
|
1685
|
+
for (const key of Reflect.ownKeys(this.attributes))
|
|
1686
|
+
delete this.attributes[key]
|
|
1687
|
+
Object.assign(this.attributes, snapshot)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1659
1690
|
/**
|
|
1660
1691
|
* @private
|
|
1661
1692
|
* Walk the block tree and pre-compute all async text values.
|
|
1662
1693
|
* Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
|
|
1694
|
+
*
|
|
1695
|
+
* Runs in two passes (see {@link parse}): with `resolveContent` false only titles and
|
|
1696
|
+
* reftexts are substituted (so the reftext→id map can be built); with `resolveContent`
|
|
1697
|
+
* true the list item / table cell / dlist text is substituted, resolving any natural
|
|
1698
|
+
* cross-references against the now-complete map. Title/reftext pre-computation is
|
|
1699
|
+
* idempotent (results are cached), so running it in both passes is a no-op the second time.
|
|
1700
|
+
* @param {AbstractBlock} block - The block to resolve.
|
|
1701
|
+
* @param {boolean} resolveContent - Whether to substitute list item / cell / dlist text.
|
|
1663
1702
|
*/
|
|
1664
|
-
async _resolveAllTexts(block) {
|
|
1703
|
+
async _resolveAllTexts(block, resolveContent) {
|
|
1704
|
+
// Replay this block's attribute entries (in document order, since the walk is
|
|
1705
|
+
// depth-first pre-order like conversion) so that body-level attribute assignments
|
|
1706
|
+
// and reassignments are in scope when the block's — and its descendants' and later
|
|
1707
|
+
// siblings' — title / list item / table cell / reftext values are substituted.
|
|
1708
|
+
// Mirrors AbstractBlock#convert, which calls playbackAttributes before converting.
|
|
1709
|
+
this.playbackAttributes(block.attributes)
|
|
1665
1710
|
// The header section lives outside document.blocks; pre-compute its title here so
|
|
1666
1711
|
// that doc.doctitle() returns the fully-substituted title (with replacements applied,
|
|
1667
1712
|
// e.g. ' → ’) rather than the header-subs-only fallback.
|
|
@@ -1681,12 +1726,12 @@ export class Document extends AbstractBlock {
|
|
|
1681
1726
|
// dlist.blocks is an array of [[term, ...], item_or_null] pairs.
|
|
1682
1727
|
for (const [terms, item] of block.blocks ?? []) {
|
|
1683
1728
|
for (const term of terms ?? []) {
|
|
1684
|
-
await term.precomputeText?.()
|
|
1685
|
-
await this._resolveAllTexts(term)
|
|
1729
|
+
if (resolveContent) await term.precomputeText?.()
|
|
1730
|
+
await this._resolveAllTexts(term, resolveContent)
|
|
1686
1731
|
}
|
|
1687
1732
|
if (item) {
|
|
1688
|
-
await item.precomputeText?.()
|
|
1689
|
-
await this._resolveAllTexts(item)
|
|
1733
|
+
if (resolveContent) await item.precomputeText?.()
|
|
1734
|
+
await this._resolveAllTexts(item, resolveContent)
|
|
1690
1735
|
}
|
|
1691
1736
|
}
|
|
1692
1737
|
} else if (ctx === 'table') {
|
|
@@ -1696,14 +1741,14 @@ export class Document extends AbstractBlock {
|
|
|
1696
1741
|
...(block.rows?.foot ?? []),
|
|
1697
1742
|
]) {
|
|
1698
1743
|
for (const cell of row) {
|
|
1699
|
-
await cell.precomputeText?.()
|
|
1744
|
+
if (resolveContent) await cell.precomputeText?.()
|
|
1700
1745
|
await cell.precomputeReftext?.()
|
|
1701
1746
|
}
|
|
1702
1747
|
}
|
|
1703
1748
|
} else {
|
|
1704
1749
|
for (const child of block.blocks ?? []) {
|
|
1705
|
-
await child.precomputeText?.()
|
|
1706
|
-
await this._resolveAllTexts(child)
|
|
1750
|
+
if (resolveContent) await child.precomputeText?.()
|
|
1751
|
+
await this._resolveAllTexts(child, resolveContent)
|
|
1707
1752
|
}
|
|
1708
1753
|
}
|
|
1709
1754
|
}
|
|
@@ -1995,14 +2040,19 @@ export class Document extends AbstractBlock {
|
|
|
1995
2040
|
let newBasebackend, newFiletype
|
|
1996
2041
|
|
|
1997
2042
|
if (converter && typeof converter._getBackendTraits === 'function') {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2043
|
+
// Read the traits object directly rather than the same-named accessor
|
|
2044
|
+
// methods. A user converter that declares flat string properties
|
|
2045
|
+
// (`converter.outfilesuffix = '.html'`, convention #2) keeps them intact,
|
|
2046
|
+
// so callers reading `converter.outfilesuffix` still see the string.
|
|
2047
|
+
const traits = converter._getBackendTraits()
|
|
2048
|
+
newBasebackend = traits.basebackend
|
|
2049
|
+
newFiletype = traits.filetype
|
|
2050
|
+
const htmlsyntax = traits.htmlsyntax
|
|
2001
2051
|
if (htmlsyntax) attrs.htmlsyntax = htmlsyntax
|
|
2002
2052
|
if (init) {
|
|
2003
|
-
attrs.outfilesuffix ??=
|
|
2053
|
+
attrs.outfilesuffix ??= traits.outfilesuffix
|
|
2004
2054
|
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
2005
|
-
attrs.outfilesuffix =
|
|
2055
|
+
attrs.outfilesuffix = traits.outfilesuffix
|
|
2006
2056
|
}
|
|
2007
2057
|
} else if (converter) {
|
|
2008
2058
|
const traits = deriveBackendTraits(newBackend)
|
package/src/index.js
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
ImageReference,
|
|
10
10
|
RevisionInfo,
|
|
11
11
|
} from './document.js'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
Logger,
|
|
14
|
+
LoggerManager,
|
|
15
|
+
LogMessage,
|
|
16
|
+
MemoryLogger,
|
|
17
|
+
NullLogger,
|
|
18
|
+
} from './logging.js'
|
|
13
19
|
import { HttpCache, MemoryHttpCache, HttpCacheManager } from './http_cache.js'
|
|
14
20
|
import { SafeMode, ContentModel } from './constants.js'
|
|
15
21
|
import { Timings } from './timings.js'
|
|
@@ -122,6 +128,7 @@ export {
|
|
|
122
128
|
Reader,
|
|
123
129
|
SyntaxHighlighterBase,
|
|
124
130
|
LoggerManager,
|
|
131
|
+
LogMessage,
|
|
125
132
|
MemoryLogger,
|
|
126
133
|
NullLogger,
|
|
127
134
|
HttpCache,
|
package/src/logging.js
CHANGED
|
@@ -277,14 +277,23 @@ Logger.BasicFormatter = class {
|
|
|
277
277
|
|
|
278
278
|
Logger.AutoFormattingMessage = {
|
|
279
279
|
/**
|
|
280
|
-
* Attach auto-formatting to any plain object carrying
|
|
281
|
-
*
|
|
280
|
+
* Attach auto-formatting to any plain object carrying
|
|
281
|
+
* { text, source_location, include_location }.
|
|
282
|
+
*
|
|
283
|
+
* The location(s) are rendered only by inspect()/toString() (used when a
|
|
284
|
+
* stderr Logger formats the line); the structured `source_location` /
|
|
285
|
+
* `include_location` remain on the object so a MemoryLogger can record them
|
|
286
|
+
* on the resulting LogMessage without duplicating them inside `text`.
|
|
287
|
+
* @param {{text: string, source_location?: any, include_location?: any}} obj
|
|
282
288
|
* @returns {typeof obj} The same object with inspect() and toString() added.
|
|
283
289
|
*/
|
|
284
290
|
attach(obj) {
|
|
285
291
|
obj.inspect = function () {
|
|
286
292
|
const sloc = this.source_location
|
|
287
|
-
|
|
293
|
+
const iloc = this.include_location
|
|
294
|
+
let text = sloc ? `${sloc}: ${this.text}` : this.text
|
|
295
|
+
if (iloc) text += ` (${iloc})`
|
|
296
|
+
return text
|
|
288
297
|
}
|
|
289
298
|
obj.toString = obj.inspect
|
|
290
299
|
return obj
|
|
@@ -294,28 +303,46 @@ Logger.AutoFormattingMessage = {
|
|
|
294
303
|
// ── LogMessage ────────────────────────────────────────────────────────────────
|
|
295
304
|
|
|
296
305
|
/** Wrapper stored by MemoryLogger; provides getSeverity/getText/getSourceLocation. */
|
|
297
|
-
class LogMessage {
|
|
306
|
+
export class LogMessage {
|
|
307
|
+
/**
|
|
308
|
+
* @param {string} severity - Severity label, e.g. 'ERROR'.
|
|
309
|
+
* @param {string|{text: string, source_location?: import('./reader.js').Cursor}|null} message
|
|
310
|
+
*/
|
|
298
311
|
constructor(severity, message) {
|
|
299
312
|
this.message = message
|
|
313
|
+
/** @type {string} */
|
|
300
314
|
this.severity = severity // string label, e.g. 'ERROR'
|
|
301
315
|
// AutoFormattingMessage objects carry { text, source_location }
|
|
302
316
|
if (message !== null && typeof message === 'object' && 'text' in message) {
|
|
303
|
-
|
|
304
|
-
this.
|
|
317
|
+
/** @type {string} */
|
|
318
|
+
this.text = message.text
|
|
319
|
+
/** @type {import('./reader.js').Cursor|null} */
|
|
320
|
+
this.sourceLocation = message.source_location ?? null
|
|
305
321
|
} else {
|
|
306
|
-
this.
|
|
307
|
-
this.
|
|
322
|
+
this.text = message != null ? String(message) : ''
|
|
323
|
+
this.sourceLocation = null
|
|
308
324
|
}
|
|
309
325
|
}
|
|
310
326
|
|
|
327
|
+
/**
|
|
328
|
+
* @returns {string} The severity label, e.g. 'ERROR'.
|
|
329
|
+
*/
|
|
311
330
|
getSeverity() {
|
|
312
331
|
return this.severity
|
|
313
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @returns {string} The message text.
|
|
336
|
+
*/
|
|
314
337
|
getText() {
|
|
315
|
-
return this.
|
|
338
|
+
return this.text
|
|
316
339
|
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @returns {import('./reader.js').Cursor|undefined} The source location, if any.
|
|
343
|
+
*/
|
|
317
344
|
getSourceLocation() {
|
|
318
|
-
return this.
|
|
345
|
+
return this.sourceLocation ?? undefined
|
|
319
346
|
}
|
|
320
347
|
}
|
|
321
348
|
|
|
@@ -328,6 +355,7 @@ export class MemoryLogger {
|
|
|
328
355
|
// matching Ruby's MemoryLogger (level: UNKNOWN). The add() method stores all
|
|
329
356
|
// messages unconditionally — level is only used by the isDebug() guard.
|
|
330
357
|
this.level = Severity.UNKNOWN
|
|
358
|
+
/** @type {LogMessage[]} */
|
|
331
359
|
this.messages = []
|
|
332
360
|
}
|
|
333
361
|
|
|
@@ -335,6 +363,9 @@ export class MemoryLogger {
|
|
|
335
363
|
return new MemoryLogger()
|
|
336
364
|
}
|
|
337
365
|
|
|
366
|
+
/**
|
|
367
|
+
* @returns {LogMessage[]} The log messages recorded so far, in order.
|
|
368
|
+
*/
|
|
338
369
|
getMessages() {
|
|
339
370
|
return this.messages
|
|
340
371
|
}
|
package/src/parser.js
CHANGED
|
@@ -598,7 +598,12 @@ export class Parser {
|
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
;(intro ?? section).blocks.push(newBlock)
|
|
601
|
-
|
|
601
|
+
// Reset the shared attributes object for the next block. Use Reflect.ownKeys
|
|
602
|
+
// (not Object.keys) so the Symbol-keyed attribute entries (ATTR_ENTRIES_KEY)
|
|
603
|
+
// are cleared too; otherwise the array of AttributeEntry objects leaks and
|
|
604
|
+
// accumulates across blocks, causing reassigned attributes (e.g. a body-level
|
|
605
|
+
// `:name:` redefined later) to all resolve to the final value at playback time.
|
|
606
|
+
for (const key of Reflect.ownKeys(attributes)) delete attributes[key]
|
|
602
607
|
}
|
|
603
608
|
}
|
|
604
609
|
|
|
@@ -1449,7 +1454,13 @@ export class Parser {
|
|
|
1449
1454
|
}
|
|
1450
1455
|
}
|
|
1451
1456
|
|
|
1452
|
-
|
|
1457
|
+
// Reflect.ownKeys (not Object.keys) so a block carrying only Symbol-keyed
|
|
1458
|
+
// attribute entries (ATTR_ENTRIES_KEY) — e.g. an `:attr:` entry immediately
|
|
1459
|
+
// preceding a list or table — still receives them, matching Ruby's
|
|
1460
|
+
// `attributes.empty?` where the `:attribute_entries` key is counted. Without this
|
|
1461
|
+
// the entries are dropped and the attribute is not played back for that block.
|
|
1462
|
+
if (Reflect.ownKeys(attributes).length > 0)
|
|
1463
|
+
block.updateAttributes(attributes)
|
|
1453
1464
|
block.commitSubs()
|
|
1454
1465
|
|
|
1455
1466
|
if (block.hasSub('callouts')) {
|