@asciidoctor/core 4.0.1 → 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 +128 -47
- package/build/node/index.cjs +175 -60
- package/package.json +1 -1
- package/src/abstract_node.js +6 -3
- 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 +59 -23
- package/types/abstract_node.d.ts +4 -3
- package/types/document.d.ts +16 -0
package/build/browser/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var version = "4.0.
|
|
1
|
+
var version = "4.0.2";
|
|
2
2
|
const packageJson = {
|
|
3
3
|
version: version};
|
|
4
4
|
|
|
@@ -3032,9 +3032,10 @@ class AbstractNode {
|
|
|
3032
3032
|
* Construct a URI reference or data URI to the target image.
|
|
3033
3033
|
*
|
|
3034
3034
|
* If the target image is already a URI it is left untouched (unless data-uri
|
|
3035
|
-
* conversion is requested).
|
|
3036
|
-
*
|
|
3037
|
-
* the
|
|
3035
|
+
* conversion is requested). If the target image is a data URI, then it is
|
|
3036
|
+
* already an embedded image, so it is returned as-is. The image is resolved
|
|
3037
|
+
* relative to the directory named by assetDirKey. When data-uri is enabled and
|
|
3038
|
+
* the safe level permits, the image is embedded as a Base64 data URI.
|
|
3038
3039
|
*
|
|
3039
3040
|
* NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
|
|
3040
3041
|
* and the resolved image URL is a remote URI, this method returns a Promise
|
|
@@ -3045,6 +3046,8 @@ class AbstractNode {
|
|
|
3045
3046
|
* @returns {Promise<string>} a Promise resolving to a String reference or data URI.
|
|
3046
3047
|
*/
|
|
3047
3048
|
async imageUri(targetImage, assetDirKey = 'imagesdir') {
|
|
3049
|
+
// A data URI is already an embedded image, so use it as-is rather than reading or re-encoding it.
|
|
3050
|
+
if (targetImage.startsWith('data:')) return targetImage
|
|
3048
3051
|
const doc = this.document;
|
|
3049
3052
|
if (doc.safe < SafeMode.SECURE && doc.hasAttribute('data-uri')) {
|
|
3050
3053
|
let imagesBase;
|
|
@@ -5452,7 +5455,18 @@ class SecurityError extends Error {
|
|
|
5452
5455
|
function applyBackendTraits(instance) {
|
|
5453
5456
|
instance._backendTraits = null;
|
|
5454
5457
|
|
|
5455
|
-
|
|
5458
|
+
// Install a Ruby-style trait accessor method, but never clobber a flat string
|
|
5459
|
+
// property the converter already declared (convention #2). Overwriting e.g.
|
|
5460
|
+
// `converter.outfilesuffix = '.html'` with a method would silently turn the
|
|
5461
|
+
// author's string into a function; the backend traits stay reachable through
|
|
5462
|
+
// `_getBackendTraits()` instead.
|
|
5463
|
+
const defineTraitAccessor = (name, fn) => {
|
|
5464
|
+
const existing = Object.getOwnPropertyDescriptor(instance, name);
|
|
5465
|
+
if (existing && typeof existing.value !== 'function') return
|
|
5466
|
+
instance[name] = fn;
|
|
5467
|
+
};
|
|
5468
|
+
|
|
5469
|
+
defineTraitAccessor('basebackend', function (value = null) {
|
|
5456
5470
|
if (value) {
|
|
5457
5471
|
const traits = (this._backendTraits ??= {});
|
|
5458
5472
|
traits.basebackend = value;
|
|
@@ -5466,19 +5480,19 @@ function applyBackendTraits(instance) {
|
|
|
5466
5480
|
return value
|
|
5467
5481
|
}
|
|
5468
5482
|
return this._getBackendTraits().basebackend
|
|
5469
|
-
};
|
|
5470
|
-
|
|
5483
|
+
});
|
|
5484
|
+
defineTraitAccessor('filetype', function (value = null) {
|
|
5471
5485
|
if (value) return (this._getBackendTraits().filetype = value)
|
|
5472
5486
|
return this._getBackendTraits().filetype
|
|
5473
|
-
};
|
|
5474
|
-
|
|
5487
|
+
});
|
|
5488
|
+
defineTraitAccessor('htmlsyntax', function (value = null) {
|
|
5475
5489
|
if (value) return (this._getBackendTraits().htmlsyntax = value)
|
|
5476
5490
|
return this._getBackendTraits().htmlsyntax
|
|
5477
|
-
};
|
|
5478
|
-
|
|
5491
|
+
});
|
|
5492
|
+
defineTraitAccessor('outfilesuffix', function (value = null) {
|
|
5479
5493
|
if (value) return (this._getBackendTraits().outfilesuffix = value)
|
|
5480
5494
|
return this._getBackendTraits().outfilesuffix
|
|
5481
|
-
};
|
|
5495
|
+
});
|
|
5482
5496
|
instance.supportsTemplates = function (value = true) {
|
|
5483
5497
|
this._getBackendTraits().supportsTemplates = value;
|
|
5484
5498
|
};
|
|
@@ -5562,7 +5576,9 @@ function normalizeConverter(converter, backend) {
|
|
|
5562
5576
|
}
|
|
5563
5577
|
}
|
|
5564
5578
|
|
|
5565
|
-
// Apply the BackendTraits mixin so Document can
|
|
5579
|
+
// Apply the BackendTraits mixin so Document can read traits via
|
|
5580
|
+
// _getBackendTraits(). Flat string properties (convention #2) are preserved:
|
|
5581
|
+
// applyBackendTraits does not overwrite an existing same-named data property.
|
|
5566
5582
|
applyBackendTraits(converter);
|
|
5567
5583
|
if (traits) {
|
|
5568
5584
|
converter._backendTraits = traits;
|
|
@@ -16091,7 +16107,7 @@ class Document extends AbstractBlock {
|
|
|
16091
16107
|
|
|
16092
16108
|
const attrOverrides = this._attributeOverrides;
|
|
16093
16109
|
attrOverrides.asciidoctor = '';
|
|
16094
|
-
attrOverrides['asciidoctor-version'] =
|
|
16110
|
+
attrOverrides['asciidoctor-version'] = packageJson.version;
|
|
16095
16111
|
|
|
16096
16112
|
const safeModeName = SafeMode.nameForValue(this.safe);
|
|
16097
16113
|
attrOverrides['safe-mode-name'] = safeModeName;
|
|
@@ -16283,16 +16299,16 @@ class Document extends AbstractBlock {
|
|
|
16283
16299
|
// attribute (re)assignments are in scope; snapshot and restore the document
|
|
16284
16300
|
// attributes so the downstream steps and conversion still start from the restored
|
|
16285
16301
|
// (header) state, matching Ruby's restore_attributes-before-convert invariant.
|
|
16302
|
+
//
|
|
16303
|
+
// This runs in two passes because list item / table cell / dlist text may contain
|
|
16304
|
+
// natural cross-references (e.g. <<Some section title>>) that resolve against the
|
|
16305
|
+
// reftext→id map. Pass 1 substitutes titles and reftexts only, so every section
|
|
16306
|
+
// reftext is known; the map is then built; pass 2 substitutes the block content
|
|
16307
|
+
// text, where resolveId() can now match those natural references.
|
|
16286
16308
|
const attributesSnapshot = { ...this.attributes };
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
Object.assign(this.attributes, attributesSnapshot);
|
|
16291
|
-
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
16292
|
-
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
16293
|
-
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
16294
|
-
delete this.attributes['footnote-number'];
|
|
16295
|
-
delete this._counters['footnote-number'];
|
|
16309
|
+
// Pass 1: titles + reftexts (no block content text yet).
|
|
16310
|
+
await this._resolveAllTexts(this, false);
|
|
16311
|
+
this._restoreAttributeSnapshot(attributesSnapshot);
|
|
16296
16312
|
// Pre-compute reftext for all registered inline anchor nodes.
|
|
16297
16313
|
for (const ref of Object.values(this.catalog.refs)) {
|
|
16298
16314
|
if (ref && typeof ref.precomputeReftext === 'function') {
|
|
@@ -16301,6 +16317,15 @@ class Document extends AbstractBlock {
|
|
|
16301
16317
|
}
|
|
16302
16318
|
// Build the reftext→id lookup map so that resolveId() is synchronous.
|
|
16303
16319
|
await this._buildReftextsMap();
|
|
16320
|
+
// Pass 2: list item / table cell / dlist text, now that natural cross-references
|
|
16321
|
+
// can be resolved against the reftext→id map.
|
|
16322
|
+
await this._resolveAllTexts(this, true);
|
|
16323
|
+
this._restoreAttributeSnapshot(attributesSnapshot);
|
|
16324
|
+
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
16325
|
+
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
16326
|
+
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
16327
|
+
delete this.attributes['footnote-number'];
|
|
16328
|
+
delete this._counters['footnote-number'];
|
|
16304
16329
|
|
|
16305
16330
|
this._parsed = true;
|
|
16306
16331
|
return this
|
|
@@ -17355,12 +17380,33 @@ class Document extends AbstractBlock {
|
|
|
17355
17380
|
: ['attributes']
|
|
17356
17381
|
}
|
|
17357
17382
|
|
|
17383
|
+
/**
|
|
17384
|
+
* @private
|
|
17385
|
+
* Restore the document attributes to a previously captured snapshot, discarding any
|
|
17386
|
+
* body-level (re)assignments replayed while pre-computing text. Mirrors Ruby's
|
|
17387
|
+
* restore_attributes-before-convert invariant.
|
|
17388
|
+
* @param {Object} snapshot - The attributes snapshot to restore.
|
|
17389
|
+
*/
|
|
17390
|
+
_restoreAttributeSnapshot(snapshot) {
|
|
17391
|
+
for (const key of Reflect.ownKeys(this.attributes))
|
|
17392
|
+
delete this.attributes[key];
|
|
17393
|
+
Object.assign(this.attributes, snapshot);
|
|
17394
|
+
}
|
|
17395
|
+
|
|
17358
17396
|
/**
|
|
17359
17397
|
* @private
|
|
17360
17398
|
* Walk the block tree and pre-compute all async text values.
|
|
17361
17399
|
* Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
|
|
17362
|
-
|
|
17363
|
-
|
|
17400
|
+
*
|
|
17401
|
+
* Runs in two passes (see {@link parse}): with `resolveContent` false only titles and
|
|
17402
|
+
* reftexts are substituted (so the reftext→id map can be built); with `resolveContent`
|
|
17403
|
+
* true the list item / table cell / dlist text is substituted, resolving any natural
|
|
17404
|
+
* cross-references against the now-complete map. Title/reftext pre-computation is
|
|
17405
|
+
* idempotent (results are cached), so running it in both passes is a no-op the second time.
|
|
17406
|
+
* @param {AbstractBlock} block - The block to resolve.
|
|
17407
|
+
* @param {boolean} resolveContent - Whether to substitute list item / cell / dlist text.
|
|
17408
|
+
*/
|
|
17409
|
+
async _resolveAllTexts(block, resolveContent) {
|
|
17364
17410
|
// Replay this block's attribute entries (in document order, since the walk is
|
|
17365
17411
|
// depth-first pre-order like conversion) so that body-level attribute assignments
|
|
17366
17412
|
// and reassignments are in scope when the block's — and its descendants' and later
|
|
@@ -17386,12 +17432,12 @@ class Document extends AbstractBlock {
|
|
|
17386
17432
|
// dlist.blocks is an array of [[term, ...], item_or_null] pairs.
|
|
17387
17433
|
for (const [terms, item] of block.blocks ?? []) {
|
|
17388
17434
|
for (const term of terms ?? []) {
|
|
17389
|
-
await term.precomputeText?.();
|
|
17390
|
-
await this._resolveAllTexts(term);
|
|
17435
|
+
if (resolveContent) await term.precomputeText?.();
|
|
17436
|
+
await this._resolveAllTexts(term, resolveContent);
|
|
17391
17437
|
}
|
|
17392
17438
|
if (item) {
|
|
17393
|
-
await item.precomputeText?.();
|
|
17394
|
-
await this._resolveAllTexts(item);
|
|
17439
|
+
if (resolveContent) await item.precomputeText?.();
|
|
17440
|
+
await this._resolveAllTexts(item, resolveContent);
|
|
17395
17441
|
}
|
|
17396
17442
|
}
|
|
17397
17443
|
} else if (ctx === 'table') {
|
|
@@ -17401,14 +17447,14 @@ class Document extends AbstractBlock {
|
|
|
17401
17447
|
...(block.rows?.foot ?? []),
|
|
17402
17448
|
]) {
|
|
17403
17449
|
for (const cell of row) {
|
|
17404
|
-
await cell.precomputeText?.();
|
|
17450
|
+
if (resolveContent) await cell.precomputeText?.();
|
|
17405
17451
|
await cell.precomputeReftext?.();
|
|
17406
17452
|
}
|
|
17407
17453
|
}
|
|
17408
17454
|
} else {
|
|
17409
17455
|
for (const child of block.blocks ?? []) {
|
|
17410
|
-
await child.precomputeText?.();
|
|
17411
|
-
await this._resolveAllTexts(child);
|
|
17456
|
+
if (resolveContent) await child.precomputeText?.();
|
|
17457
|
+
await this._resolveAllTexts(child, resolveContent);
|
|
17412
17458
|
}
|
|
17413
17459
|
}
|
|
17414
17460
|
}
|
|
@@ -17700,14 +17746,19 @@ class Document extends AbstractBlock {
|
|
|
17700
17746
|
let newBasebackend, newFiletype;
|
|
17701
17747
|
|
|
17702
17748
|
if (converter && typeof converter._getBackendTraits === 'function') {
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17749
|
+
// Read the traits object directly rather than the same-named accessor
|
|
17750
|
+
// methods. A user converter that declares flat string properties
|
|
17751
|
+
// (`converter.outfilesuffix = '.html'`, convention #2) keeps them intact,
|
|
17752
|
+
// so callers reading `converter.outfilesuffix` still see the string.
|
|
17753
|
+
const traits = converter._getBackendTraits();
|
|
17754
|
+
newBasebackend = traits.basebackend;
|
|
17755
|
+
newFiletype = traits.filetype;
|
|
17756
|
+
const htmlsyntax = traits.htmlsyntax;
|
|
17706
17757
|
if (htmlsyntax) attrs.htmlsyntax = htmlsyntax;
|
|
17707
17758
|
if (init) {
|
|
17708
|
-
attrs.outfilesuffix ??=
|
|
17759
|
+
attrs.outfilesuffix ??= traits.outfilesuffix;
|
|
17709
17760
|
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
17710
|
-
attrs.outfilesuffix =
|
|
17761
|
+
attrs.outfilesuffix = traits.outfilesuffix;
|
|
17711
17762
|
}
|
|
17712
17763
|
} else if (converter) {
|
|
17713
17764
|
const traits = deriveBackendTraits(newBackend);
|
|
@@ -20954,7 +21005,7 @@ class Html5Converter extends ConverterBase {
|
|
|
20954
21005
|
let reproducible;
|
|
20955
21006
|
if (!(reproducible = node.hasAttribute('reproducible'))) {
|
|
20956
21007
|
result.push(
|
|
20957
|
-
`<meta name="generator" content="Asciidoctor ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
21008
|
+
`<meta name="generator" content="Asciidoctor.js ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
20958
21009
|
);
|
|
20959
21010
|
}
|
|
20960
21011
|
if (node.hasAttribute('app-name')) {
|
|
@@ -21671,7 +21722,9 @@ ${await node.content()}
|
|
|
21671
21722
|
const slash = this._voidSlash;
|
|
21672
21723
|
let img, src;
|
|
21673
21724
|
if (
|
|
21674
|
-
(node.hasAttribute('format', 'svg') ||
|
|
21725
|
+
(node.hasAttribute('format', 'svg') ||
|
|
21726
|
+
target.includes('.svg') ||
|
|
21727
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
21675
21728
|
node.document.safe < SafeMode.SECURE
|
|
21676
21729
|
) {
|
|
21677
21730
|
if (node.hasOption('inline')) {
|
|
@@ -22512,7 +22565,9 @@ Your browser does not support the video tag.
|
|
|
22512
22565
|
if (node.hasAttribute('title'))
|
|
22513
22566
|
attrs += ` title="${node.getAttribute('title')}"`;
|
|
22514
22567
|
if (
|
|
22515
|
-
(node.hasAttribute('format', 'svg') ||
|
|
22568
|
+
(node.hasAttribute('format', 'svg') ||
|
|
22569
|
+
target.includes('.svg') ||
|
|
22570
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
22516
22571
|
node.document.safe < SafeMode.SECURE
|
|
22517
22572
|
) {
|
|
22518
22573
|
if (node.hasOption('inline')) {
|
|
@@ -22605,12 +22660,17 @@ Your browser does not support the video tag.
|
|
|
22605
22660
|
|
|
22606
22661
|
// NOTE expose readSvgContents for Bespoke converter
|
|
22607
22662
|
async readSvgContents(node, target) {
|
|
22608
|
-
|
|
22609
|
-
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22663
|
+
// A data-URI carries the SVG in the target itself (e.g. an embedded diagram
|
|
22664
|
+
// produced with `:data-uri:`), so decode it directly rather than trying to
|
|
22665
|
+
// read it as a file or remote URI.
|
|
22666
|
+
let svg = target.startsWith('data:')
|
|
22667
|
+
? this._decodeDataUri(target)
|
|
22668
|
+
: await node.readContents(target, {
|
|
22669
|
+
start: node.document.getAttribute('imagesdir'),
|
|
22670
|
+
normalize: true,
|
|
22671
|
+
label: 'SVG',
|
|
22672
|
+
warnIfEmpty: true,
|
|
22673
|
+
});
|
|
22614
22674
|
if (!svg) return null
|
|
22615
22675
|
if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '');
|
|
22616
22676
|
// Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
|
|
@@ -22642,6 +22702,27 @@ Your browser does not support the video tag.
|
|
|
22642
22702
|
|
|
22643
22703
|
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
22644
22704
|
|
|
22705
|
+
/**
|
|
22706
|
+
* Decode an inline `data:` URI to its text contents (e.g. an SVG document) so
|
|
22707
|
+
* an image whose target is a data-URI can be embedded inline. Supports both
|
|
22708
|
+
* Base64 (`;base64,`) and percent-encoded payloads. Returns null when the
|
|
22709
|
+
* payload is missing.
|
|
22710
|
+
*
|
|
22711
|
+
* @internal
|
|
22712
|
+
* @private
|
|
22713
|
+
*/
|
|
22714
|
+
_decodeDataUri(target) {
|
|
22715
|
+
const comma = target.indexOf(',');
|
|
22716
|
+
if (comma === -1) return null
|
|
22717
|
+
const meta = target.slice('data:'.length, comma);
|
|
22718
|
+
const data = target.slice(comma + 1);
|
|
22719
|
+
if (/;base64\b/i.test(meta)) {
|
|
22720
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
22721
|
+
return new TextDecoder('utf-8').decode(bytes)
|
|
22722
|
+
}
|
|
22723
|
+
return decodeURIComponent(data)
|
|
22724
|
+
}
|
|
22725
|
+
|
|
22645
22726
|
/**
|
|
22646
22727
|
* @internal
|
|
22647
22728
|
* @private
|
|
@@ -24006,7 +24087,7 @@ class ManPageConverter extends ConverterBase {
|
|
|
24006
24087
|
`'\\" t
|
|
24007
24088
|
.\\" Title: ${mantitle}
|
|
24008
24089
|
.\\" Author: ${node.hasAttribute('authors') ? node.getAttribute('authors') : '[see the "AUTHOR(S)" section]'}
|
|
24009
|
-
.\\" Generator: Asciidoctor ${node.getAttribute('asciidoctor-version')}`,
|
|
24090
|
+
.\\" Generator: Asciidoctor.js ${node.getAttribute('asciidoctor-version')}`,
|
|
24010
24091
|
];
|
|
24011
24092
|
|
|
24012
24093
|
if (docdate) result.push(`.\\" Date: ${docdate}`);
|
package/build/node/index.cjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const node_module = require('node:module');
|
|
4
|
+
const node_url = require('node:url');
|
|
4
5
|
const node_fs = require('node:fs');
|
|
5
6
|
const path = require('node:path');
|
|
6
7
|
|
|
7
8
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
|
-
var version = "4.0.
|
|
9
|
+
var version = "4.0.2";
|
|
9
10
|
const packageJson = {
|
|
10
11
|
version: version};
|
|
11
12
|
|
|
@@ -3039,9 +3040,10 @@ class AbstractNode {
|
|
|
3039
3040
|
* Construct a URI reference or data URI to the target image.
|
|
3040
3041
|
*
|
|
3041
3042
|
* If the target image is already a URI it is left untouched (unless data-uri
|
|
3042
|
-
* conversion is requested).
|
|
3043
|
-
*
|
|
3044
|
-
* the
|
|
3043
|
+
* conversion is requested). If the target image is a data URI, then it is
|
|
3044
|
+
* already an embedded image, so it is returned as-is. The image is resolved
|
|
3045
|
+
* relative to the directory named by assetDirKey. When data-uri is enabled and
|
|
3046
|
+
* the safe level permits, the image is embedded as a Base64 data URI.
|
|
3045
3047
|
*
|
|
3046
3048
|
* NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
|
|
3047
3049
|
* and the resolved image URL is a remote URI, this method returns a Promise
|
|
@@ -3052,6 +3054,8 @@ class AbstractNode {
|
|
|
3052
3054
|
* @returns {Promise<string>} a Promise resolving to a String reference or data URI.
|
|
3053
3055
|
*/
|
|
3054
3056
|
async imageUri(targetImage, assetDirKey = 'imagesdir') {
|
|
3057
|
+
// A data URI is already an embedded image, so use it as-is rather than reading or re-encoding it.
|
|
3058
|
+
if (targetImage.startsWith('data:')) return targetImage
|
|
3055
3059
|
const doc = this.document;
|
|
3056
3060
|
if (doc.safe < SafeMode.SECURE && doc.hasAttribute('data-uri')) {
|
|
3057
3061
|
let imagesBase;
|
|
@@ -5459,7 +5463,18 @@ class SecurityError extends Error {
|
|
|
5459
5463
|
function applyBackendTraits(instance) {
|
|
5460
5464
|
instance._backendTraits = null;
|
|
5461
5465
|
|
|
5462
|
-
|
|
5466
|
+
// Install a Ruby-style trait accessor method, but never clobber a flat string
|
|
5467
|
+
// property the converter already declared (convention #2). Overwriting e.g.
|
|
5468
|
+
// `converter.outfilesuffix = '.html'` with a method would silently turn the
|
|
5469
|
+
// author's string into a function; the backend traits stay reachable through
|
|
5470
|
+
// `_getBackendTraits()` instead.
|
|
5471
|
+
const defineTraitAccessor = (name, fn) => {
|
|
5472
|
+
const existing = Object.getOwnPropertyDescriptor(instance, name);
|
|
5473
|
+
if (existing && typeof existing.value !== 'function') return
|
|
5474
|
+
instance[name] = fn;
|
|
5475
|
+
};
|
|
5476
|
+
|
|
5477
|
+
defineTraitAccessor('basebackend', function (value = null) {
|
|
5463
5478
|
if (value) {
|
|
5464
5479
|
const traits = (this._backendTraits ??= {});
|
|
5465
5480
|
traits.basebackend = value;
|
|
@@ -5473,19 +5488,19 @@ function applyBackendTraits(instance) {
|
|
|
5473
5488
|
return value
|
|
5474
5489
|
}
|
|
5475
5490
|
return this._getBackendTraits().basebackend
|
|
5476
|
-
};
|
|
5477
|
-
|
|
5491
|
+
});
|
|
5492
|
+
defineTraitAccessor('filetype', function (value = null) {
|
|
5478
5493
|
if (value) return (this._getBackendTraits().filetype = value)
|
|
5479
5494
|
return this._getBackendTraits().filetype
|
|
5480
|
-
};
|
|
5481
|
-
|
|
5495
|
+
});
|
|
5496
|
+
defineTraitAccessor('htmlsyntax', function (value = null) {
|
|
5482
5497
|
if (value) return (this._getBackendTraits().htmlsyntax = value)
|
|
5483
5498
|
return this._getBackendTraits().htmlsyntax
|
|
5484
|
-
};
|
|
5485
|
-
|
|
5499
|
+
});
|
|
5500
|
+
defineTraitAccessor('outfilesuffix', function (value = null) {
|
|
5486
5501
|
if (value) return (this._getBackendTraits().outfilesuffix = value)
|
|
5487
5502
|
return this._getBackendTraits().outfilesuffix
|
|
5488
|
-
};
|
|
5503
|
+
});
|
|
5489
5504
|
instance.supportsTemplates = function (value = true) {
|
|
5490
5505
|
this._getBackendTraits().supportsTemplates = value;
|
|
5491
5506
|
};
|
|
@@ -5569,7 +5584,9 @@ function normalizeConverter(converter, backend) {
|
|
|
5569
5584
|
}
|
|
5570
5585
|
}
|
|
5571
5586
|
|
|
5572
|
-
// Apply the BackendTraits mixin so Document can
|
|
5587
|
+
// Apply the BackendTraits mixin so Document can read traits via
|
|
5588
|
+
// _getBackendTraits(). Flat string properties (convention #2) are preserved:
|
|
5589
|
+
// applyBackendTraits does not overwrite an existing same-named data property.
|
|
5573
5590
|
applyBackendTraits(converter);
|
|
5574
5591
|
if (traits) {
|
|
5575
5592
|
converter._backendTraits = traits;
|
|
@@ -16098,7 +16115,7 @@ class Document extends AbstractBlock {
|
|
|
16098
16115
|
|
|
16099
16116
|
const attrOverrides = this._attributeOverrides;
|
|
16100
16117
|
attrOverrides.asciidoctor = '';
|
|
16101
|
-
attrOverrides['asciidoctor-version'] =
|
|
16118
|
+
attrOverrides['asciidoctor-version'] = packageJson.version;
|
|
16102
16119
|
|
|
16103
16120
|
const safeModeName = SafeMode.nameForValue(this.safe);
|
|
16104
16121
|
attrOverrides['safe-mode-name'] = safeModeName;
|
|
@@ -16290,16 +16307,16 @@ class Document extends AbstractBlock {
|
|
|
16290
16307
|
// attribute (re)assignments are in scope; snapshot and restore the document
|
|
16291
16308
|
// attributes so the downstream steps and conversion still start from the restored
|
|
16292
16309
|
// (header) state, matching Ruby's restore_attributes-before-convert invariant.
|
|
16310
|
+
//
|
|
16311
|
+
// This runs in two passes because list item / table cell / dlist text may contain
|
|
16312
|
+
// natural cross-references (e.g. <<Some section title>>) that resolve against the
|
|
16313
|
+
// reftext→id map. Pass 1 substitutes titles and reftexts only, so every section
|
|
16314
|
+
// reftext is known; the map is then built; pass 2 substitutes the block content
|
|
16315
|
+
// text, where resolveId() can now match those natural references.
|
|
16293
16316
|
const attributesSnapshot = { ...this.attributes };
|
|
16294
|
-
|
|
16295
|
-
|
|
16296
|
-
|
|
16297
|
-
Object.assign(this.attributes, attributesSnapshot);
|
|
16298
|
-
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
16299
|
-
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
16300
|
-
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
16301
|
-
delete this.attributes['footnote-number'];
|
|
16302
|
-
delete this._counters['footnote-number'];
|
|
16317
|
+
// Pass 1: titles + reftexts (no block content text yet).
|
|
16318
|
+
await this._resolveAllTexts(this, false);
|
|
16319
|
+
this._restoreAttributeSnapshot(attributesSnapshot);
|
|
16303
16320
|
// Pre-compute reftext for all registered inline anchor nodes.
|
|
16304
16321
|
for (const ref of Object.values(this.catalog.refs)) {
|
|
16305
16322
|
if (ref && typeof ref.precomputeReftext === 'function') {
|
|
@@ -16308,6 +16325,15 @@ class Document extends AbstractBlock {
|
|
|
16308
16325
|
}
|
|
16309
16326
|
// Build the reftext→id lookup map so that resolveId() is synchronous.
|
|
16310
16327
|
await this._buildReftextsMap();
|
|
16328
|
+
// Pass 2: list item / table cell / dlist text, now that natural cross-references
|
|
16329
|
+
// can be resolved against the reftext→id map.
|
|
16330
|
+
await this._resolveAllTexts(this, true);
|
|
16331
|
+
this._restoreAttributeSnapshot(attributesSnapshot);
|
|
16332
|
+
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
16333
|
+
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
16334
|
+
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
16335
|
+
delete this.attributes['footnote-number'];
|
|
16336
|
+
delete this._counters['footnote-number'];
|
|
16311
16337
|
|
|
16312
16338
|
this._parsed = true;
|
|
16313
16339
|
return this
|
|
@@ -17362,12 +17388,33 @@ class Document extends AbstractBlock {
|
|
|
17362
17388
|
: ['attributes']
|
|
17363
17389
|
}
|
|
17364
17390
|
|
|
17391
|
+
/**
|
|
17392
|
+
* @private
|
|
17393
|
+
* Restore the document attributes to a previously captured snapshot, discarding any
|
|
17394
|
+
* body-level (re)assignments replayed while pre-computing text. Mirrors Ruby's
|
|
17395
|
+
* restore_attributes-before-convert invariant.
|
|
17396
|
+
* @param {Object} snapshot - The attributes snapshot to restore.
|
|
17397
|
+
*/
|
|
17398
|
+
_restoreAttributeSnapshot(snapshot) {
|
|
17399
|
+
for (const key of Reflect.ownKeys(this.attributes))
|
|
17400
|
+
delete this.attributes[key];
|
|
17401
|
+
Object.assign(this.attributes, snapshot);
|
|
17402
|
+
}
|
|
17403
|
+
|
|
17365
17404
|
/**
|
|
17366
17405
|
* @private
|
|
17367
17406
|
* Walk the block tree and pre-compute all async text values.
|
|
17368
17407
|
* Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
|
|
17369
|
-
|
|
17370
|
-
|
|
17408
|
+
*
|
|
17409
|
+
* Runs in two passes (see {@link parse}): with `resolveContent` false only titles and
|
|
17410
|
+
* reftexts are substituted (so the reftext→id map can be built); with `resolveContent`
|
|
17411
|
+
* true the list item / table cell / dlist text is substituted, resolving any natural
|
|
17412
|
+
* cross-references against the now-complete map. Title/reftext pre-computation is
|
|
17413
|
+
* idempotent (results are cached), so running it in both passes is a no-op the second time.
|
|
17414
|
+
* @param {AbstractBlock} block - The block to resolve.
|
|
17415
|
+
* @param {boolean} resolveContent - Whether to substitute list item / cell / dlist text.
|
|
17416
|
+
*/
|
|
17417
|
+
async _resolveAllTexts(block, resolveContent) {
|
|
17371
17418
|
// Replay this block's attribute entries (in document order, since the walk is
|
|
17372
17419
|
// depth-first pre-order like conversion) so that body-level attribute assignments
|
|
17373
17420
|
// and reassignments are in scope when the block's — and its descendants' and later
|
|
@@ -17393,12 +17440,12 @@ class Document extends AbstractBlock {
|
|
|
17393
17440
|
// dlist.blocks is an array of [[term, ...], item_or_null] pairs.
|
|
17394
17441
|
for (const [terms, item] of block.blocks ?? []) {
|
|
17395
17442
|
for (const term of terms ?? []) {
|
|
17396
|
-
await term.precomputeText?.();
|
|
17397
|
-
await this._resolveAllTexts(term);
|
|
17443
|
+
if (resolveContent) await term.precomputeText?.();
|
|
17444
|
+
await this._resolveAllTexts(term, resolveContent);
|
|
17398
17445
|
}
|
|
17399
17446
|
if (item) {
|
|
17400
|
-
await item.precomputeText?.();
|
|
17401
|
-
await this._resolveAllTexts(item);
|
|
17447
|
+
if (resolveContent) await item.precomputeText?.();
|
|
17448
|
+
await this._resolveAllTexts(item, resolveContent);
|
|
17402
17449
|
}
|
|
17403
17450
|
}
|
|
17404
17451
|
} else if (ctx === 'table') {
|
|
@@ -17408,14 +17455,14 @@ class Document extends AbstractBlock {
|
|
|
17408
17455
|
...(block.rows?.foot ?? []),
|
|
17409
17456
|
]) {
|
|
17410
17457
|
for (const cell of row) {
|
|
17411
|
-
await cell.precomputeText?.();
|
|
17458
|
+
if (resolveContent) await cell.precomputeText?.();
|
|
17412
17459
|
await cell.precomputeReftext?.();
|
|
17413
17460
|
}
|
|
17414
17461
|
}
|
|
17415
17462
|
} else {
|
|
17416
17463
|
for (const child of block.blocks ?? []) {
|
|
17417
|
-
await child.precomputeText?.();
|
|
17418
|
-
await this._resolveAllTexts(child);
|
|
17464
|
+
if (resolveContent) await child.precomputeText?.();
|
|
17465
|
+
await this._resolveAllTexts(child, resolveContent);
|
|
17419
17466
|
}
|
|
17420
17467
|
}
|
|
17421
17468
|
}
|
|
@@ -17707,14 +17754,19 @@ class Document extends AbstractBlock {
|
|
|
17707
17754
|
let newBasebackend, newFiletype;
|
|
17708
17755
|
|
|
17709
17756
|
if (converter && typeof converter._getBackendTraits === 'function') {
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17757
|
+
// Read the traits object directly rather than the same-named accessor
|
|
17758
|
+
// methods. A user converter that declares flat string properties
|
|
17759
|
+
// (`converter.outfilesuffix = '.html'`, convention #2) keeps them intact,
|
|
17760
|
+
// so callers reading `converter.outfilesuffix` still see the string.
|
|
17761
|
+
const traits = converter._getBackendTraits();
|
|
17762
|
+
newBasebackend = traits.basebackend;
|
|
17763
|
+
newFiletype = traits.filetype;
|
|
17764
|
+
const htmlsyntax = traits.htmlsyntax;
|
|
17713
17765
|
if (htmlsyntax) attrs.htmlsyntax = htmlsyntax;
|
|
17714
17766
|
if (init) {
|
|
17715
|
-
attrs.outfilesuffix ??=
|
|
17767
|
+
attrs.outfilesuffix ??= traits.outfilesuffix;
|
|
17716
17768
|
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
17717
|
-
attrs.outfilesuffix =
|
|
17769
|
+
attrs.outfilesuffix = traits.outfilesuffix;
|
|
17718
17770
|
}
|
|
17719
17771
|
} else if (converter) {
|
|
17720
17772
|
const traits = deriveBackendTraits(newBackend);
|
|
@@ -21025,7 +21077,7 @@ class Html5Converter extends ConverterBase {
|
|
|
21025
21077
|
let reproducible;
|
|
21026
21078
|
if (!(reproducible = node.hasAttribute('reproducible'))) {
|
|
21027
21079
|
result.push(
|
|
21028
|
-
`<meta name="generator" content="Asciidoctor ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
21080
|
+
`<meta name="generator" content="Asciidoctor.js ${node.getAttribute('asciidoctor-version')}"${slash}>`
|
|
21029
21081
|
);
|
|
21030
21082
|
}
|
|
21031
21083
|
if (node.hasAttribute('app-name')) {
|
|
@@ -21742,7 +21794,9 @@ ${await node.content()}
|
|
|
21742
21794
|
const slash = this._voidSlash;
|
|
21743
21795
|
let img, src;
|
|
21744
21796
|
if (
|
|
21745
|
-
(node.hasAttribute('format', 'svg') ||
|
|
21797
|
+
(node.hasAttribute('format', 'svg') ||
|
|
21798
|
+
target.includes('.svg') ||
|
|
21799
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
21746
21800
|
node.document.safe < SafeMode.SECURE
|
|
21747
21801
|
) {
|
|
21748
21802
|
if (node.hasOption('inline')) {
|
|
@@ -22583,7 +22637,9 @@ Your browser does not support the video tag.
|
|
|
22583
22637
|
if (node.hasAttribute('title'))
|
|
22584
22638
|
attrs += ` title="${node.getAttribute('title')}"`;
|
|
22585
22639
|
if (
|
|
22586
|
-
(node.hasAttribute('format', 'svg') ||
|
|
22640
|
+
(node.hasAttribute('format', 'svg') ||
|
|
22641
|
+
target.includes('.svg') ||
|
|
22642
|
+
target.startsWith('data:image/svg+xml')) &&
|
|
22587
22643
|
node.document.safe < SafeMode.SECURE
|
|
22588
22644
|
) {
|
|
22589
22645
|
if (node.hasOption('inline')) {
|
|
@@ -22676,12 +22732,17 @@ Your browser does not support the video tag.
|
|
|
22676
22732
|
|
|
22677
22733
|
// NOTE expose readSvgContents for Bespoke converter
|
|
22678
22734
|
async readSvgContents(node, target) {
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22735
|
+
// A data-URI carries the SVG in the target itself (e.g. an embedded diagram
|
|
22736
|
+
// produced with `:data-uri:`), so decode it directly rather than trying to
|
|
22737
|
+
// read it as a file or remote URI.
|
|
22738
|
+
let svg = target.startsWith('data:')
|
|
22739
|
+
? this._decodeDataUri(target)
|
|
22740
|
+
: await node.readContents(target, {
|
|
22741
|
+
start: node.document.getAttribute('imagesdir'),
|
|
22742
|
+
normalize: true,
|
|
22743
|
+
label: 'SVG',
|
|
22744
|
+
warnIfEmpty: true,
|
|
22745
|
+
});
|
|
22685
22746
|
if (!svg) return null
|
|
22686
22747
|
if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '');
|
|
22687
22748
|
// Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
|
|
@@ -22713,6 +22774,27 @@ Your browser does not support the video tag.
|
|
|
22713
22774
|
|
|
22714
22775
|
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
22715
22776
|
|
|
22777
|
+
/**
|
|
22778
|
+
* Decode an inline `data:` URI to its text contents (e.g. an SVG document) so
|
|
22779
|
+
* an image whose target is a data-URI can be embedded inline. Supports both
|
|
22780
|
+
* Base64 (`;base64,`) and percent-encoded payloads. Returns null when the
|
|
22781
|
+
* payload is missing.
|
|
22782
|
+
*
|
|
22783
|
+
* @internal
|
|
22784
|
+
* @private
|
|
22785
|
+
*/
|
|
22786
|
+
_decodeDataUri(target) {
|
|
22787
|
+
const comma = target.indexOf(',');
|
|
22788
|
+
if (comma === -1) return null
|
|
22789
|
+
const meta = target.slice('data:'.length, comma);
|
|
22790
|
+
const data = target.slice(comma + 1);
|
|
22791
|
+
if (/;base64\b/i.test(meta)) {
|
|
22792
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
22793
|
+
return new TextDecoder('utf-8').decode(bytes)
|
|
22794
|
+
}
|
|
22795
|
+
return decodeURIComponent(data)
|
|
22796
|
+
}
|
|
22797
|
+
|
|
22716
22798
|
/**
|
|
22717
22799
|
* @internal
|
|
22718
22800
|
* @private
|
|
@@ -22955,8 +23037,7 @@ const composite = /*#__PURE__*/Object.freeze({
|
|
|
22955
23037
|
// - Ruby File.directory?/File.file? → fsp.stat().isDirectory()/isFile() (async).
|
|
22956
23038
|
// - Ruby File.basename / File.expand_path → node:path basename / resolve.
|
|
22957
23039
|
// - PathResolver.system_path → pathResolver.systemPath().
|
|
22958
|
-
// - template.render(node, opts) → template.render({node, opts, helpers}).
|
|
22959
|
-
// - Helpers loaded from helpers.js/helpers.cjs; can export configure(enginesContext).
|
|
23040
|
+
// - template.render(node, opts) → template.render({node, opts, helpers}).// - Helpers loaded from helpers.js/helpers.cjs/helpers.mjs; can export configure(enginesContext).
|
|
22960
23041
|
// - Custom engines registered via TemplateConverter.TemplateEngine.register(ext, adapter).
|
|
22961
23042
|
// - Thread safety / Mutex → not needed (single-threaded JS).
|
|
22962
23043
|
// - load_eruby / eRuby support → not applicable in JS environment.
|
|
@@ -23230,7 +23311,11 @@ class TemplateConverter extends ConverterBase {
|
|
|
23230
23311
|
if (!(await _isFile(file))) continue
|
|
23231
23312
|
|
|
23232
23313
|
// Collect helpers separately; process after all templates.
|
|
23233
|
-
if (
|
|
23314
|
+
if (
|
|
23315
|
+
basename === 'helpers.js' ||
|
|
23316
|
+
basename === 'helpers.cjs' ||
|
|
23317
|
+
basename === 'helpers.mjs'
|
|
23318
|
+
) {
|
|
23234
23319
|
helpersFile = file;
|
|
23235
23320
|
continue
|
|
23236
23321
|
}
|
|
@@ -23298,8 +23383,8 @@ class TemplateConverter extends ConverterBase {
|
|
|
23298
23383
|
const pugOpts = { ...(this.engineOptions.pug ?? {}), filename: file };
|
|
23299
23384
|
const renderFn = pug.compileFile(file, pugOpts);
|
|
23300
23385
|
template = { render: renderFn, file };
|
|
23301
|
-
} else if (ext === 'js' || ext === 'cjs') {
|
|
23302
|
-
const renderFn =
|
|
23386
|
+
} else if (ext === 'js' || ext === 'cjs' || ext === 'mjs') {
|
|
23387
|
+
const renderFn = await _loadModule(file, ext);
|
|
23303
23388
|
template = { render: renderFn, file };
|
|
23304
23389
|
} else {
|
|
23305
23390
|
// Fall back to custom TemplateEngine registry.
|
|
@@ -23314,16 +23399,20 @@ class TemplateConverter extends ConverterBase {
|
|
|
23314
23399
|
result[name] = template;
|
|
23315
23400
|
}
|
|
23316
23401
|
|
|
23317
|
-
// Load helpers if found (or if a helpers
|
|
23318
|
-
|
|
23319
|
-
|
|
23320
|
-
|
|
23321
|
-
|
|
23322
|
-
|
|
23323
|
-
|
|
23402
|
+
// Load helpers if found (or if a helpers file exists at the top of the dir).
|
|
23403
|
+
if (!helpersFile) {
|
|
23404
|
+
for (const ext of ['js', 'cjs', 'mjs']) {
|
|
23405
|
+
const fallback = path.join(templateDir, `helpers.${ext}`);
|
|
23406
|
+
if (await _isFile(fallback)) {
|
|
23407
|
+
helpersFile = fallback;
|
|
23408
|
+
break
|
|
23409
|
+
}
|
|
23410
|
+
}
|
|
23411
|
+
}
|
|
23324
23412
|
|
|
23325
23413
|
if (helpersFile) {
|
|
23326
|
-
const
|
|
23414
|
+
const helpersExt = helpersFile.slice(helpersFile.lastIndexOf('.') + 1);
|
|
23415
|
+
const ctx = await _loadModule(helpersFile, helpersExt);
|
|
23327
23416
|
if (typeof ctx.configure === 'function') ctx.configure(enginesCtx);
|
|
23328
23417
|
result['helpers.js'] = { file: helpersFile, ctx };
|
|
23329
23418
|
}
|
|
@@ -23351,6 +23440,32 @@ class TemplateConverter extends ConverterBase {
|
|
|
23351
23440
|
|
|
23352
23441
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
23353
23442
|
|
|
23443
|
+
/**
|
|
23444
|
+
* Load a JavaScript module (template render function or helpers object) by
|
|
23445
|
+
* file extension.
|
|
23446
|
+
*
|
|
23447
|
+
* - `.cjs` files are always CommonJS: require() returns `module.exports`.
|
|
23448
|
+
* - `.js` and `.mjs` files are loaded with a dynamic `import()`, which Node
|
|
23449
|
+
* resolves as either ES modules or CommonJS. The export is taken from the
|
|
23450
|
+
* module's default export (`export default …` in ESM, `module.exports = …`
|
|
23451
|
+
* in CommonJS via interop); when there is no default export the module
|
|
23452
|
+
* namespace is returned instead, so ESM helpers exposing named exports
|
|
23453
|
+
* (`export function configure() {}`) work too.
|
|
23454
|
+
* @param {string} file - absolute path to the module file
|
|
23455
|
+
* @param {string} ext - file extension without the leading dot ('js' | 'cjs' | 'mjs')
|
|
23456
|
+
* @returns {Promise<*>} the module's default export, or its namespace
|
|
23457
|
+
* @internal
|
|
23458
|
+
* @private
|
|
23459
|
+
*/
|
|
23460
|
+
async function _loadModule(file, ext) {
|
|
23461
|
+
if (ext === 'cjs') {
|
|
23462
|
+
const mod = _require(file);
|
|
23463
|
+
return mod?.default ? mod.default : mod
|
|
23464
|
+
}
|
|
23465
|
+
const mod = await import(node_url.pathToFileURL(file).href);
|
|
23466
|
+
return mod.default ?? mod
|
|
23467
|
+
}
|
|
23468
|
+
|
|
23354
23469
|
/**
|
|
23355
23470
|
* @internal
|
|
23356
23471
|
* @private
|
|
@@ -24539,7 +24654,7 @@ class ManPageConverter extends ConverterBase {
|
|
|
24539
24654
|
`'\\" t
|
|
24540
24655
|
.\\" Title: ${mantitle}
|
|
24541
24656
|
.\\" Author: ${node.hasAttribute('authors') ? node.getAttribute('authors') : '[see the "AUTHOR(S)" section]'}
|
|
24542
|
-
.\\" Generator: Asciidoctor ${node.getAttribute('asciidoctor-version')}`,
|
|
24657
|
+
.\\" Generator: Asciidoctor.js ${node.getAttribute('asciidoctor-version')}`,
|
|
24543
24658
|
];
|
|
24544
24659
|
|
|
24545
24660
|
if (docdate) result.push(`.\\" Date: ${docdate}`);
|
package/package.json
CHANGED
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/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
|
|
@@ -592,16 +593,16 @@ export class Document extends AbstractBlock {
|
|
|
592
593
|
// attribute (re)assignments are in scope; snapshot and restore the document
|
|
593
594
|
// attributes so the downstream steps and conversion still start from the restored
|
|
594
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.
|
|
595
602
|
const attributesSnapshot = { ...this.attributes }
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
Object.assign(this.attributes, attributesSnapshot)
|
|
600
|
-
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
601
|
-
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
602
|
-
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
603
|
-
delete this.attributes['footnote-number']
|
|
604
|
-
delete this._counters['footnote-number']
|
|
603
|
+
// Pass 1: titles + reftexts (no block content text yet).
|
|
604
|
+
await this._resolveAllTexts(this, false)
|
|
605
|
+
this._restoreAttributeSnapshot(attributesSnapshot)
|
|
605
606
|
// Pre-compute reftext for all registered inline anchor nodes.
|
|
606
607
|
for (const ref of Object.values(this.catalog.refs)) {
|
|
607
608
|
if (ref && typeof ref.precomputeReftext === 'function') {
|
|
@@ -610,6 +611,15 @@ export class Document extends AbstractBlock {
|
|
|
610
611
|
}
|
|
611
612
|
// Build the reftext→id lookup map so that resolveId() is synchronous.
|
|
612
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']
|
|
613
623
|
|
|
614
624
|
this._parsed = true
|
|
615
625
|
return this
|
|
@@ -1664,12 +1674,33 @@ export class Document extends AbstractBlock {
|
|
|
1664
1674
|
: ['attributes']
|
|
1665
1675
|
}
|
|
1666
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
|
+
|
|
1667
1690
|
/**
|
|
1668
1691
|
* @private
|
|
1669
1692
|
* Walk the block tree and pre-compute all async text values.
|
|
1670
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.
|
|
1671
1702
|
*/
|
|
1672
|
-
async _resolveAllTexts(block) {
|
|
1703
|
+
async _resolveAllTexts(block, resolveContent) {
|
|
1673
1704
|
// Replay this block's attribute entries (in document order, since the walk is
|
|
1674
1705
|
// depth-first pre-order like conversion) so that body-level attribute assignments
|
|
1675
1706
|
// and reassignments are in scope when the block's — and its descendants' and later
|
|
@@ -1695,12 +1726,12 @@ export class Document extends AbstractBlock {
|
|
|
1695
1726
|
// dlist.blocks is an array of [[term, ...], item_or_null] pairs.
|
|
1696
1727
|
for (const [terms, item] of block.blocks ?? []) {
|
|
1697
1728
|
for (const term of terms ?? []) {
|
|
1698
|
-
await term.precomputeText?.()
|
|
1699
|
-
await this._resolveAllTexts(term)
|
|
1729
|
+
if (resolveContent) await term.precomputeText?.()
|
|
1730
|
+
await this._resolveAllTexts(term, resolveContent)
|
|
1700
1731
|
}
|
|
1701
1732
|
if (item) {
|
|
1702
|
-
await item.precomputeText?.()
|
|
1703
|
-
await this._resolveAllTexts(item)
|
|
1733
|
+
if (resolveContent) await item.precomputeText?.()
|
|
1734
|
+
await this._resolveAllTexts(item, resolveContent)
|
|
1704
1735
|
}
|
|
1705
1736
|
}
|
|
1706
1737
|
} else if (ctx === 'table') {
|
|
@@ -1710,14 +1741,14 @@ export class Document extends AbstractBlock {
|
|
|
1710
1741
|
...(block.rows?.foot ?? []),
|
|
1711
1742
|
]) {
|
|
1712
1743
|
for (const cell of row) {
|
|
1713
|
-
await cell.precomputeText?.()
|
|
1744
|
+
if (resolveContent) await cell.precomputeText?.()
|
|
1714
1745
|
await cell.precomputeReftext?.()
|
|
1715
1746
|
}
|
|
1716
1747
|
}
|
|
1717
1748
|
} else {
|
|
1718
1749
|
for (const child of block.blocks ?? []) {
|
|
1719
|
-
await child.precomputeText?.()
|
|
1720
|
-
await this._resolveAllTexts(child)
|
|
1750
|
+
if (resolveContent) await child.precomputeText?.()
|
|
1751
|
+
await this._resolveAllTexts(child, resolveContent)
|
|
1721
1752
|
}
|
|
1722
1753
|
}
|
|
1723
1754
|
}
|
|
@@ -2009,14 +2040,19 @@ export class Document extends AbstractBlock {
|
|
|
2009
2040
|
let newBasebackend, newFiletype
|
|
2010
2041
|
|
|
2011
2042
|
if (converter && typeof converter._getBackendTraits === 'function') {
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
|
2015
2051
|
if (htmlsyntax) attrs.htmlsyntax = htmlsyntax
|
|
2016
2052
|
if (init) {
|
|
2017
|
-
attrs.outfilesuffix ??=
|
|
2053
|
+
attrs.outfilesuffix ??= traits.outfilesuffix
|
|
2018
2054
|
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
2019
|
-
attrs.outfilesuffix =
|
|
2055
|
+
attrs.outfilesuffix = traits.outfilesuffix
|
|
2020
2056
|
}
|
|
2021
2057
|
} else if (converter) {
|
|
2022
2058
|
const traits = deriveBackendTraits(newBackend)
|
package/types/abstract_node.d.ts
CHANGED
|
@@ -234,9 +234,10 @@ export abstract class AbstractNode {
|
|
|
234
234
|
* Construct a URI reference or data URI to the target image.
|
|
235
235
|
*
|
|
236
236
|
* If the target image is already a URI it is left untouched (unless data-uri
|
|
237
|
-
* conversion is requested).
|
|
238
|
-
*
|
|
239
|
-
* the
|
|
237
|
+
* conversion is requested). If the target image is a data URI, then it is
|
|
238
|
+
* already an embedded image, so it is returned as-is. The image is resolved
|
|
239
|
+
* relative to the directory named by assetDirKey. When data-uri is enabled and
|
|
240
|
+
* the safe level permits, the image is embedded as a Base64 data URI.
|
|
240
241
|
*
|
|
241
242
|
* NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
|
|
242
243
|
* and the resolved image URL is a remote URI, this method returns a Promise
|
package/types/document.d.ts
CHANGED
|
@@ -469,10 +469,26 @@ export class Document extends AbstractBlock<string> {
|
|
|
469
469
|
* @returns {string[]} The list of substitutions to apply.
|
|
470
470
|
*/
|
|
471
471
|
private _resolveDocinfoSubs;
|
|
472
|
+
/**
|
|
473
|
+
* @private
|
|
474
|
+
* Restore the document attributes to a previously captured snapshot, discarding any
|
|
475
|
+
* body-level (re)assignments replayed while pre-computing text. Mirrors Ruby's
|
|
476
|
+
* restore_attributes-before-convert invariant.
|
|
477
|
+
* @param {Object} snapshot - The attributes snapshot to restore.
|
|
478
|
+
*/
|
|
479
|
+
private _restoreAttributeSnapshot;
|
|
472
480
|
/**
|
|
473
481
|
* @private
|
|
474
482
|
* Walk the block tree and pre-compute all async text values.
|
|
475
483
|
* Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
|
|
484
|
+
*
|
|
485
|
+
* Runs in two passes (see {@link parse}): with `resolveContent` false only titles and
|
|
486
|
+
* reftexts are substituted (so the reftext→id map can be built); with `resolveContent`
|
|
487
|
+
* true the list item / table cell / dlist text is substituted, resolving any natural
|
|
488
|
+
* cross-references against the now-complete map. Title/reftext pre-computation is
|
|
489
|
+
* idempotent (results are cached), so running it in both passes is a no-op the second time.
|
|
490
|
+
* @param {AbstractBlock} block - The block to resolve.
|
|
491
|
+
* @param {boolean} resolveContent - Whether to substitute list item / cell / dlist text.
|
|
476
492
|
*/
|
|
477
493
|
private _resolveAllTexts;
|
|
478
494
|
/**
|