@asciidoctor/core 3.0.4 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24154 -0
  3. package/build/node/index.cjs +24735 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +53 -100
  6. package/src/abstract_block.js +849 -0
  7. package/src/abstract_node.js +954 -0
  8. package/src/attribute_entry.js +12 -0
  9. package/src/attribute_list.js +380 -0
  10. package/src/block.js +168 -0
  11. package/src/browser/asset.js +22 -0
  12. package/src/browser/reader.js +138 -0
  13. package/src/browser.js +121 -0
  14. package/src/callouts.js +85 -0
  15. package/src/compliance.js +54 -0
  16. package/src/constants.js +665 -0
  17. package/src/convert.js +370 -0
  18. package/src/converter/composite.js +83 -0
  19. package/src/converter/docbook5.js +1031 -0
  20. package/src/converter/html5.js +1899 -0
  21. package/src/converter/manpage.js +935 -0
  22. package/src/converter/template.js +459 -0
  23. package/src/converter.js +478 -0
  24. package/src/data/stylesheet-data.js +2 -0
  25. package/src/document.js +2134 -0
  26. package/src/extensions.js +1952 -0
  27. package/src/footnote.js +28 -0
  28. package/src/helpers.js +355 -0
  29. package/src/index.js +138 -0
  30. package/src/inline.js +158 -0
  31. package/src/list.js +240 -0
  32. package/src/load.js +276 -0
  33. package/src/logging.js +526 -0
  34. package/src/parser.js +3661 -0
  35. package/src/path_resolver.js +472 -0
  36. package/src/reader.js +1755 -0
  37. package/src/rx.js +829 -0
  38. package/src/section.js +354 -0
  39. package/src/stylesheets.js +30 -0
  40. package/src/substitutors.js +2241 -0
  41. package/src/syntaxHighlighter/highlightjs.js +90 -0
  42. package/src/syntaxHighlighter/html_pipeline.js +33 -0
  43. package/src/syntax_highlighter.js +304 -0
  44. package/src/table.js +952 -0
  45. package/src/timings.js +78 -0
  46. package/types/abstract_block.d.ts +343 -0
  47. package/types/abstract_node.d.ts +471 -0
  48. package/types/attribute_entry.d.ts +7 -0
  49. package/types/attribute_list.d.ts +52 -0
  50. package/types/block.d.ts +55 -0
  51. package/types/browser/asset.d.ts +7 -0
  52. package/types/browser/reader.d.ts +29 -0
  53. package/types/callouts.d.ts +36 -0
  54. package/types/compliance.d.ts +23 -0
  55. package/types/constants.d.ts +268 -0
  56. package/types/convert.d.ts +34 -0
  57. package/types/converter/composite.d.ts +20 -0
  58. package/types/converter/docbook5.d.ts +41 -0
  59. package/types/converter/html5.d.ts +51 -0
  60. package/types/converter/manpage.d.ts +59 -0
  61. package/types/converter/template.d.ts +83 -0
  62. package/types/converter.d.ts +150 -0
  63. package/types/data/stylesheet-data.d.ts +2 -0
  64. package/types/document.d.ts +495 -0
  65. package/types/extensions.d.ts +876 -0
  66. package/types/footnote.d.ts +18 -0
  67. package/types/helpers.d.ts +146 -0
  68. package/types/index.d.ts +73 -3731
  69. package/types/inline.d.ts +69 -0
  70. package/types/list.d.ts +114 -0
  71. package/types/load.d.ts +39 -0
  72. package/types/logging.d.ts +187 -0
  73. package/types/parser.d.ts +114 -0
  74. package/types/path_resolver.d.ts +103 -0
  75. package/types/reader.d.ts +184 -0
  76. package/types/rx.d.ts +513 -0
  77. package/types/section.d.ts +122 -0
  78. package/types/stylesheets.d.ts +10 -0
  79. package/types/substitutors.d.ts +208 -0
  80. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  81. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  82. package/types/syntax_highlighter.d.ts +167 -0
  83. package/types/table.d.ts +231 -0
  84. package/types/timings.d.ts +25 -0
  85. package/types/tsconfig.json +9 -0
  86. package/LICENSE +0 -21
  87. package/dist/browser/asciidoctor.js +0 -47654
  88. package/dist/browser/asciidoctor.min.js +0 -1452
  89. package/dist/graalvm/asciidoctor.js +0 -47402
  90. package/dist/node/asciidoctor.cjs +0 -21567
  91. package/dist/node/asciidoctor.js +0 -23037
@@ -0,0 +1,954 @@
1
+ // ESM conversion of abstract_node.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - Ruby symbols (:document, :context) are represented as plain strings.
5
+ // - attr_reader / attr_accessor are implemented as plain instance properties;
6
+ // cases where the setter has side effects use JS get/set pairs.
7
+ // - Ruby methods ending in ? are renamed: attr? → hasAttr, block? → isBlock,
8
+ // inline? → isInline, role? → hasRoleAttribute, has_role? → hasRole,
9
+ // option? → hasOption, reftext? → hasReftext.
10
+ // - Ruby methods ending in = that have side effects use JS set accessors:
11
+ // parent= → set parent(), role= → set role().
12
+ // - snake_case method/property names are converted to camelCase:
13
+ // node_name → nodeName, set_attr → setAttr, etc.
14
+ // - The Logging mixin (logger getter) is provided as a default on AbstractNode;
15
+ // it falls back to the document's logger or the global console.
16
+ // - The Substitutors mixin is applied via Object.assign(AbstractNode.prototype, Substitutors)
17
+ // after both modules are loaded (see the bottom of substitutors.js).
18
+ // - File I/O in generateDataUri / readAsset uses node:fs/promises async APIs.
19
+ // When the resolved path is an HTTP URI (browser: docdir is a URL), readAsset
20
+ // delegates to browser/asset.js (Fetch API) instead of using the filesystem.
21
+ // - generateDataUriFromUri and readContents use the Fetch API and are async;
22
+ // imageUri and readContents must be awaited when the data-uri + allow-uri-read
23
+ // combination is active.
24
+ // - Ruby's Set is represented as a JavaScript Set.
25
+
26
+ import { SafeMode, LF } from './constants.js'
27
+ import {
28
+ isUriish,
29
+ encodeSpacesInUri,
30
+ isExtname,
31
+ extname,
32
+ prepareSourceString,
33
+ } from './helpers.js'
34
+
35
+ // ── Node.js fs (lazy, optional) ───────────────────────────────────────────────
36
+ // Loaded on first use in Node.js; silently absent in browser/WebWorker environments.
37
+ let _fsp // undefined = not tried, null = unavailable, object = available
38
+ let _fsConstants // node:fs constants (R_OK etc.) — not on node:fs/promises
39
+
40
+ async function _requireFsp() {
41
+ if (_fsp !== undefined) return
42
+ try {
43
+ _fsp = await import('node:fs/promises')
44
+ _fsConstants = (await import('node:fs')).constants
45
+ } catch {
46
+ _fsp = null
47
+ }
48
+ }
49
+
50
+ async function isReadable(path) {
51
+ await _requireFsp()
52
+ if (!_fsp) return false
53
+ try {
54
+ await _fsp.access(path, _fsConstants.R_OK)
55
+ return true
56
+ } catch {
57
+ return false
58
+ }
59
+ }
60
+
61
+ /**
62
+ * An abstract base class that provides state and methods for managing a node of AsciiDoc content.
63
+ * The state and methods on this class are common to all content segments in an AsciiDoc document.
64
+ * @abstract
65
+ */
66
+ export class AbstractNode {
67
+ /**
68
+ * @type {string|null|undefined}
69
+ * @internal
70
+ */
71
+ _convertedReftext
72
+
73
+ /**
74
+ * @param {AbstractNode} parent
75
+ * @param {string} context
76
+ * @param {object} [opts={}]
77
+ */
78
+ constructor(parent, context, opts = {}) {
79
+ // document is a special case – should refer to itself
80
+ if (context === 'document') {
81
+ /** @type {Document} */
82
+ this.document = /** @type {Document} */ this
83
+ } else if (parent) {
84
+ /** @internal */
85
+ this._parent = parent
86
+ /** @type {Document} */
87
+ this.document = parent.document
88
+ }
89
+ this.context = context
90
+ this.nodeName = String(context)
91
+ this.id = null
92
+ // NOTE the value of the attributes option may be undefined on an Inline node
93
+ const attrs = opts.attributes
94
+ this.attributes = attrs ? { ...attrs } : {}
95
+ this.passthroughs = []
96
+ }
97
+
98
+ /**
99
+ * Alias for {@link getParent}.
100
+ * @see {getParent}
101
+ */
102
+ get parent() {
103
+ return this._parent
104
+ }
105
+
106
+ /**
107
+ * Alias for {@link setParent}.
108
+ * @see {setParent}
109
+ */
110
+ set parent(parent) {
111
+ this._parent = parent
112
+ this.document = parent.document
113
+ }
114
+
115
+ /**
116
+ * Alias for {@link getRole}.
117
+ * @see {getRole}
118
+ */
119
+ get role() {
120
+ return this.attributes.role
121
+ }
122
+
123
+ /**
124
+ * Set the value of the role attribute on this node.
125
+ *
126
+ * Accepts a single role name, a space-separated String, or an Array.
127
+ *
128
+ * @param {string|string[]} names - A single role name, a space-separated String, or an Array.
129
+ */
130
+ set role(names) {
131
+ this.attributes.role = Array.isArray(names) ? names.join(' ') : names
132
+ }
133
+
134
+ /**
135
+ * Alias for {@link getRoles}.
136
+ * @see {getRoles}
137
+ */
138
+ get roles() {
139
+ const val = this.attributes.role
140
+ return val ? val.split(' ') : []
141
+ }
142
+
143
+ /**
144
+ * @returns {boolean} true if this AbstractNode is an instance of Block.
145
+ * @throws {Error} Subclasses must override this method.
146
+ */
147
+ isBlock() {
148
+ throw new Error('NotImplementedError')
149
+ }
150
+
151
+ /**
152
+ * @returns {boolean} true if this AbstractNode is an instance of Inline.
153
+ * @throws {Error} Subclasses must override this method.
154
+ */
155
+ isInline() {
156
+ throw new Error('NotImplementedError')
157
+ }
158
+
159
+ /**
160
+ * Alias for {@link getConverter}.
161
+ * @see {getConverter}
162
+ * @returns {object} the converter instance.
163
+ */
164
+ get converter() {
165
+ return this.document.converter
166
+ }
167
+
168
+ /**
169
+ * Get the value of the specified attribute.
170
+ *
171
+ * Looks for the attribute on this node first. If not found and `fallbackName` is
172
+ * set, and this node is not the Document node, look for that attribute on the
173
+ * Document node. Otherwise, return `defaultValue`.
174
+ *
175
+ * @param {string} name - The attribute name to resolve.
176
+ * @param {*} [defaultValue=null] - The value to return if the attribute is not found.
177
+ * @param {string|boolean|null} [fallbackName=null] - When truthy, also checks the Document's
178
+ * attributes. Pass `true` to fall back using the same name, or a string to use a different name.
179
+ * @returns {*} the attribute value or defaultValue.
180
+ *
181
+ * @example <caption>Simple lookup</caption>
182
+ * block.getAttribute('language') // → 'ruby' or null
183
+ *
184
+ * @example <caption>With default</caption>
185
+ * block.getAttribute('linenums', false) // → false if not set
186
+ *
187
+ * @example <caption>Inherit from document if absent on block</caption>
188
+ * block.getAttribute('source-highlighter', null, true) // → falls back to doc attribute of same name
189
+ * block.getAttribute('linenums', null, 'source-linenums') // → falls back to 'source-linenums' on doc
190
+ */
191
+ getAttribute(name, defaultValue = null, fallbackName = null) {
192
+ const key = String(name)
193
+ const val = this.attributes[key]
194
+ if (val != null) return val
195
+ if (fallbackName && this._parent) {
196
+ const fallbackKey = String(fallbackName === true ? name : fallbackName)
197
+ const docVal = this.document.attributes[fallbackKey]
198
+ if (docVal != null) return docVal
199
+ }
200
+ return defaultValue
201
+ }
202
+
203
+ /**
204
+ * Check if the specified attribute is defined on this node, with optional
205
+ * value match and document-level fallback.
206
+ *
207
+ * @param {string} name - The attribute name.
208
+ * @param {*} [expectedValue=null] - When truthy, also checks that the resolved value equals this.
209
+ * @param {string|boolean|null} [fallbackName=null] - When truthy, also checks the Document's
210
+ * attributes. Pass `true` to use the same name, or a string for a different fallback name.
211
+ * @returns {boolean}
212
+ *
213
+ * @example <caption>Presence check</caption>
214
+ * block.hasAttribute('linenums') // → true/false
215
+ *
216
+ * @example <caption>Value match</caption>
217
+ * block.hasAttribute('language', 'ruby') // → true only when language === 'ruby'
218
+ *
219
+ * @example <caption>Inherit presence from document</caption>
220
+ * block.hasAttribute('source-highlighter', null, true) // → also checks doc-level attribute
221
+ */
222
+ hasAttribute(name, expectedValue = null, fallbackName = null) {
223
+ const key = String(name)
224
+ if (expectedValue) {
225
+ const val =
226
+ this.attributes[key] ??
227
+ (fallbackName && this._parent
228
+ ? this.document.attributes[
229
+ String(fallbackName === true ? name : fallbackName)
230
+ ]
231
+ : null)
232
+ return expectedValue === val
233
+ }
234
+ return (
235
+ key in this.attributes ||
236
+ !!(
237
+ fallbackName &&
238
+ this._parent &&
239
+ String(fallbackName === true ? name : fallbackName) in
240
+ this.document.attributes
241
+ )
242
+ )
243
+ }
244
+
245
+ /**
246
+ * Set the value of the specified attribute on this node.
247
+ *
248
+ * @param {string} name - The attribute name to assign.
249
+ * @param {*} [value=''] - The value to assign.
250
+ * @param {boolean} [overwrite=true] - When `false`, does nothing if the attribute already exists.
251
+ * @returns {string|boolean|null} `true` if the attribute was set, `false` if blocked by `overwrite=false`.
252
+ * Subclasses (e.g. `Document`) may return the resolved value string or `null` when the attribute is locked.
253
+ */
254
+ setAttribute(name, value = '', overwrite = true) {
255
+ if (overwrite === false && name in this.attributes) return false
256
+ this.attributes[name] = value
257
+ return true
258
+ }
259
+
260
+ /**
261
+ * Check if the specified attribute is defined with an optional value match.
262
+ * Alias for {@link hasAttribute}.
263
+ * @see {hasAttribute}
264
+ */
265
+ isAttribute(name, expectedValue = null) {
266
+ if (expectedValue != null) return this.getAttribute(name) === expectedValue
267
+ return name in this.attributes
268
+ }
269
+
270
+ /**
271
+ * Remove the attribute from this node.
272
+ *
273
+ * @param {string} name - The attribute name to remove.
274
+ * @returns {*} the previous value, or `undefined` if the attribute was not present.
275
+ */
276
+ removeAttribute(name) {
277
+ const val = this.attributes[name]
278
+ delete this.attributes[name]
279
+ return val
280
+ }
281
+
282
+ /**
283
+ * Check if the specified option attribute is enabled on this node.
284
+ * This method checks whether the `<name>-option` attribute is set.
285
+ *
286
+ * @param {string} name - The String or Symbol name of the option.
287
+ * @returns {boolean} true if the option is enabled, false otherwise.
288
+ */
289
+ hasOption(name) {
290
+ return `${name}-option` in this.attributes
291
+ }
292
+
293
+ /**
294
+ * Set the specified option on this node by setting the `<name>-option` attribute.
295
+ *
296
+ * @param {string} name - The String name of the option.
297
+ */
298
+ setOption(name) {
299
+ this.attributes[`${name}-option`] = ''
300
+ }
301
+
302
+ /**
303
+ * Retrieve the Set of option names that are enabled on this node.
304
+ *
305
+ * @returns {Set<string>} a Set of option name strings.
306
+ */
307
+ enabledOptions() {
308
+ const result = new Set()
309
+ for (const k of Object.keys(this.attributes)) {
310
+ if (k.endsWith('-option')) result.add(k.slice(0, k.length - 7))
311
+ }
312
+ return result
313
+ }
314
+
315
+ /**
316
+ * Update the attributes of this node with the new values.
317
+ *
318
+ * @param {Object} newAttributes - A plain object of additional attributes to assign.
319
+ * @returns {Object} the updated attributes object on this node.
320
+ */
321
+ updateAttributes(newAttributes) {
322
+ return Object.assign(this.attributes, newAttributes)
323
+ }
324
+
325
+ /**
326
+ * Check if the `role` attribute is set on this node, optionally matching an exact value.
327
+ *
328
+ * Unlike {@link hasRole}, which checks for an individual role name within a
329
+ * space-separated list, this method tests the raw `role` attribute string as a whole.
330
+ *
331
+ * @param {string|null} [expectedValue=null] - When provided, checks that the `role`
332
+ * attribute equals this string exactly.
333
+ * @returns {boolean}
334
+ *
335
+ * @example
336
+ * node.hasRoleAttribute() // → true if role attribute is set at all
337
+ * node.hasRoleAttribute('lead') // → true only when role === 'lead' (not 'lead primary')
338
+ */
339
+ hasRoleAttribute(expectedValue = null) {
340
+ if (expectedValue != null) return expectedValue === this.attributes.role
341
+ return 'role' in this.attributes
342
+ }
343
+
344
+ /**
345
+ * Check if the specified role name is present in this node's role list.
346
+ *
347
+ * @param {string} name - The String role name to find.
348
+ * @returns {boolean}
349
+ */
350
+ hasRole(name) {
351
+ const val = this.attributes.role
352
+ return val ? ` ${val} `.includes(` ${name} `) : false
353
+ }
354
+
355
+ /**
356
+ * Add the given role directly to this node.
357
+ *
358
+ * @param {string} name - The String role name to add.
359
+ * @returns {boolean} true if the role was added, false if it was already present.
360
+ */
361
+ addRole(name) {
362
+ const val = this.attributes.role
363
+ if (val) {
364
+ if (` ${val} `.includes(` ${name} `)) return false
365
+ this.attributes.role = `${val} ${name}`
366
+ return true
367
+ }
368
+ this.attributes.role = name
369
+ return true
370
+ }
371
+
372
+ /**
373
+ * Remove the given role directly from this node.
374
+ *
375
+ * @param {string} name - The String role name to remove.
376
+ * @returns {boolean} true if the role was removed, false if it was not present.
377
+ */
378
+ removeRole(name) {
379
+ const val = this.attributes.role
380
+ if (!val) return false
381
+ const roles = val.split(' ')
382
+ const idx = roles.indexOf(name)
383
+ if (idx < 0) return false
384
+ roles.splice(idx, 1)
385
+ if (roles.length === 0) {
386
+ delete this.attributes.role
387
+ } else {
388
+ this.attributes.role = roles.join(' ')
389
+ }
390
+ return true
391
+ }
392
+
393
+ /**
394
+ * Get the value of the reftext attribute with substitutions applied.
395
+ * The result is pre-computed during Document.parse() via {@link precomputeReftext}.
396
+ * Falls back to the raw reftext attribute if precomputeReftext() has not been called yet.
397
+ *
398
+ * @returns {string|null} the String reftext or null if not set.
399
+ */
400
+ get reftext() {
401
+ if (this._convertedReftext !== undefined) return this._convertedReftext
402
+ const val = this.attributes.reftext
403
+ return val ?? null
404
+ }
405
+
406
+ /**
407
+ * Pre-compute the reftext with substitutions applied asynchronously.
408
+ * Called during Document.parse() so the synchronous getter works during conversion.
409
+ *
410
+ * @returns {Promise<void>}
411
+ */
412
+ async precomputeReftext() {
413
+ const val = this.attributes.reftext
414
+ this._convertedReftext =
415
+ val != null ? await this.applyReftextSubs(val) : null
416
+ }
417
+
418
+ /**
419
+ * Check if the reftext attribute is defined.
420
+ *
421
+ * @returns {boolean}
422
+ */
423
+ hasReftext() {
424
+ return 'reftext' in this.attributes
425
+ }
426
+
427
+ /**
428
+ * Check whether this node has reftext — either an explicit 'reftext' attribute
429
+ * or a title that can serve as the cross-reference text.
430
+ * Mirrors Ruby's AbstractNode#reftext?
431
+ * @returns {boolean}
432
+ */
433
+ isReftext() {
434
+ return this.hasAttribute('reftext') || !!this.title
435
+ }
436
+
437
+ /**
438
+ * Construct a reference or data URI to an icon image for the given name.
439
+ *
440
+ * If the 'icon' attribute is set on this node the name is ignored and the
441
+ * attribute value is used as the target path. Otherwise the icon path is built
442
+ * from 'iconsdir', the name, and 'icontype' (default: 'png').
443
+ *
444
+ * @param {string} name - The String name of the icon.
445
+ * @returns {Promise<string>} a Promise resolving to a String reference or data URI for the icon image.
446
+ */
447
+ async iconUri(name) {
448
+ let icon
449
+ if (this.hasAttribute('icon')) {
450
+ icon = this.getAttribute('icon')
451
+ if (!isExtname(icon))
452
+ icon = `${icon}.${this.document.getAttribute('icontype', 'png')}`
453
+ } else {
454
+ icon = `${name}.${this.document.getAttribute('icontype', 'png')}`
455
+ }
456
+ return this.imageUri(icon, 'iconsdir')
457
+ }
458
+
459
+ /**
460
+ * Construct a URI reference or data URI to the target image.
461
+ *
462
+ * If the target image is already a URI it is left untouched (unless data-uri
463
+ * conversion is requested). The image is resolved relative to the directory
464
+ * named by assetDirKey. When data-uri is enabled and the safe level permits,
465
+ * the image is embedded as a Base64 data URI.
466
+ *
467
+ * NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
468
+ * and the resolved image URL is a remote URI, this method returns a Promise
469
+ * rather than a String. Await the result when that combination may be active.
470
+ *
471
+ * @param {string} targetImage - A String path to the target image.
472
+ * @param {string} [assetDirKey='imagesdir'] - The String attribute key for the image directory.
473
+ * @returns {Promise<string>} a Promise resolving to a String reference or data URI.
474
+ */
475
+ async imageUri(targetImage, assetDirKey = 'imagesdir') {
476
+ const doc = this.document
477
+ if (doc.safe < SafeMode.SECURE && doc.hasAttribute('data-uri')) {
478
+ let imagesBase
479
+ if (
480
+ (isUriish(targetImage) &&
481
+ (targetImage = encodeSpacesInUri(targetImage))) ||
482
+ (assetDirKey &&
483
+ (imagesBase = this.getAttribute(assetDirKey, null, true)) &&
484
+ isUriish(imagesBase) &&
485
+ (targetImage = this.normalizeWebPath(targetImage, imagesBase, false)))
486
+ ) {
487
+ return doc.hasAttribute('allow-uri-read')
488
+ ? this.generateDataUriFromUri(
489
+ targetImage,
490
+ doc.hasAttribute('cache-uri')
491
+ )
492
+ : targetImage
493
+ }
494
+ return this.generateDataUri(targetImage, assetDirKey)
495
+ }
496
+ return this.normalizeWebPath(
497
+ targetImage,
498
+ assetDirKey ? this.getAttribute(assetDirKey, null, true) : null
499
+ )
500
+ }
501
+
502
+ /**
503
+ * Construct a URI reference to the target media.
504
+ *
505
+ * @param {string} target - A String reference to the target media.
506
+ * @param {string} [assetDirKey='imagesdir'] - The String attribute key for the media directory.
507
+ * @returns {string} a String reference for the target media.
508
+ */
509
+ mediaUri(target, assetDirKey = 'imagesdir') {
510
+ return this.normalizeWebPath(
511
+ target,
512
+ assetDirKey ? this.getAttribute(assetDirKey, null, true) : null
513
+ )
514
+ }
515
+
516
+ /**
517
+ * Generate a data URI that embeds the image at the given local path.
518
+ *
519
+ * The image path is cleaned to prevent access outside the jail when the
520
+ * document safe level is SafeMode.SAFE or higher. The image data is read
521
+ * and Base64-encoded. In non-Node environments this method returns an empty
522
+ * data URI with a warning.
523
+ *
524
+ * @param {string} targetImage - A String path to the target image.
525
+ * @param {string|null} [assetDirKey=null] - The String attribute key for the image directory.
526
+ * @returns {Promise<string>} a Promise resolving to a String data URI.
527
+ */
528
+ async generateDataUri(targetImage, assetDirKey = null) {
529
+ const ext = extname(targetImage, null)
530
+ const mimetype = ext
531
+ ? ext === '.svg'
532
+ ? 'image/svg+xml'
533
+ : `image/${ext.slice(1)}`
534
+ : 'application/octet-stream'
535
+ const imagePath = assetDirKey
536
+ ? this.normalizeSystemPath(
537
+ targetImage,
538
+ this.getAttribute(assetDirKey, null, true),
539
+ null,
540
+ { targetName: 'image' }
541
+ )
542
+ : this.normalizeSystemPath(targetImage)
543
+ if (isUriish(imagePath)) {
544
+ return await this.generateDataUriFromUri(
545
+ imagePath,
546
+ this.document.hasAttribute('cache-uri')
547
+ )
548
+ }
549
+ if (await isReadable(imagePath)) {
550
+ const data = await _fsp.readFile(imagePath)
551
+ return `data:${mimetype};base64,${data.toString('base64')}`
552
+ }
553
+ this.logger.warn(`image to embed not found or not readable: ${imagePath}`)
554
+ return `data:${mimetype};base64,`
555
+ }
556
+
557
+ /**
558
+ * Read the image data from the specified URI and generate a data URI.
559
+ *
560
+ * The image data is fetched and Base64-encoded. The MIME type is taken from
561
+ * the Content-Type response header.
562
+ *
563
+ * NOTE: This method is async in JS (the Fetch API is async). When called from
564
+ * imageUri, the caller must await the returned Promise.
565
+ *
566
+ * @param {string} imageUri - The URI from which to read the image data (http/https/ftp).
567
+ * @param {boolean} [cacheUri=false] - A Boolean to control caching (not yet supported in JS).
568
+ * @returns {Promise<string>} a Promise resolving to a String data URI.
569
+ */
570
+ async generateDataUriFromUri(imageUri, cacheUri = false) {
571
+ try {
572
+ const response = await fetch(imageUri)
573
+ if (response.ok) {
574
+ const mimetype = (
575
+ response.headers.get('content-type') || 'application/octet-stream'
576
+ )
577
+ .split(';')[0]
578
+ .trim()
579
+ const buffer = await response.arrayBuffer()
580
+ const base64 = btoa(
581
+ Array.from(new Uint8Array(buffer), (b) =>
582
+ String.fromCharCode(b)
583
+ ).join('')
584
+ )
585
+ return `data:${mimetype};base64,${base64}`
586
+ } else {
587
+ const ext = extname(imageUri, null)
588
+ const mimetype = ext
589
+ ? ext === '.svg'
590
+ ? 'image/svg+xml'
591
+ : `image/${ext.slice(1)}`
592
+ : 'application/octet-stream'
593
+ this.logger.warn(
594
+ `image to embed not found or not readable: ${imageUri}`
595
+ )
596
+ return `data:${mimetype};base64,`
597
+ }
598
+ } catch {
599
+ this.logger.warn(`could not retrieve image data from URI: ${imageUri}`)
600
+ return imageUri
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Normalize the asset file or directory to a concrete and rinsed path.
606
+ *
607
+ * Delegates to {@link normalizeSystemPath} with start set to document.baseDir.
608
+ *
609
+ * @param {string} assetRef - The String asset reference to normalize.
610
+ * @param {string} [assetName='path'] - The String label for the asset used in messages.
611
+ * @param {boolean} [autocorrect=true] - A Boolean indicating whether to recover from an illegal path.
612
+ * @returns {string} the normalized String path.
613
+ */
614
+ normalizeAssetPath(assetRef, assetName = 'path', autocorrect = true) {
615
+ return this.normalizeSystemPath(assetRef, this.document.baseDir, null, {
616
+ targetName: assetName,
617
+ recover: autocorrect,
618
+ })
619
+ }
620
+
621
+ /**
622
+ * Resolve and normalize a secure path from the target and start paths.
623
+ *
624
+ * Prevents resolving a path outside the jail (defaulting to document.baseDir)
625
+ * when the document safe level is SafeMode.SAFE or higher.
626
+ *
627
+ * @param {string} target - The String target path.
628
+ * @param {string|null} [start=null] - The String start (parent) path.
629
+ * @param {string|null} [jail=null] - The String jail path.
630
+ * @param {Object} [opts={}] - A plain object of options:
631
+ * - `recover` {boolean} - Whether to automatically recover for illegal paths.
632
+ * - `targetName` {string} - Label used in messages for the path being resolved.
633
+ * @throws {Error} if a jail is specified and the resolved path is outside it.
634
+ * @returns {string} the resolved String path.
635
+ */
636
+ normalizeSystemPath(target, start = null, jail = null, opts = {}) {
637
+ const doc = this.document
638
+ if (doc.safe < SafeMode.SAFE) {
639
+ if (start) {
640
+ if (!doc.pathResolver.root(start)) start = `${doc.baseDir}/${start}`
641
+ } else {
642
+ start = doc.baseDir
643
+ }
644
+ } else {
645
+ start = start ?? doc.baseDir
646
+ jail = jail ?? doc.baseDir
647
+ }
648
+ return doc.pathResolver.systemPath(target, start, jail, opts)
649
+ }
650
+
651
+ /**
652
+ * Normalize the web path using the PathResolver.
653
+ *
654
+ * @param {string} target - The String target path.
655
+ * @param {string|null} [start=null] - The String start (parent) path.
656
+ * @param {boolean} [preserveUriTarget=true] - Whether a URI target should be preserved as-is.
657
+ * @returns {string} the resolved String path.
658
+ */
659
+ normalizeWebPath(target, start = null, preserveUriTarget = true) {
660
+ if (preserveUriTarget && isUriish(target)) return encodeSpacesInUri(target)
661
+ return this.document.pathResolver.webPath(target, start)
662
+ }
663
+
664
+ /**
665
+ * Read the contents of the file at the specified path.
666
+ *
667
+ * This method checks that the file is readable before attempting to read it.
668
+ *
669
+ * @param {string} path - The String path from which to read the contents.
670
+ * @param {Object} [opts={}] - A plain object of options:
671
+ * - `warnOnFailure` {boolean} - Whether a warning is issued when the file cannot be read (default: false).
672
+ * - `normalize` {boolean} - Whether lines are normalized and coerced to UTF-8 (default: false).
673
+ * - `label` {string} - Label for the file used in warning messages.
674
+ * @returns {Promise<string|null>} a Promise resolving to the file content, or null if not readable.
675
+ */
676
+ async readAsset(path, opts = {}) {
677
+ // remap opts for backwards compatibility (boolean shorthand)
678
+ if (typeof opts !== 'object' || opts === null)
679
+ opts = { warnOnFailure: opts !== false }
680
+ if (isUriish(path)) {
681
+ // Browser: docdir is a URL so the resolved path is an HTTP URI; use fetch instead of fs.
682
+ const { readBrowserAsset } = await import('./browser/asset.js')
683
+ const text = await readBrowserAsset(path)
684
+ if (text != null)
685
+ return opts.normalize ? prepareSourceString(text).join(LF) : text
686
+ if (opts.warnOnFailure) {
687
+ const docfile = this.getAttribute('docfile') || '<stdin>'
688
+ const label = opts.label || 'file'
689
+ this.logger.warn(
690
+ `${docfile}: ${label} does not exist or cannot be read: ${path}`
691
+ )
692
+ }
693
+ return null
694
+ }
695
+ if (await isReadable(path)) {
696
+ if (opts.normalize) {
697
+ return prepareSourceString(await _fsp.readFile(path, 'utf8')).join(LF)
698
+ }
699
+ return _fsp.readFile(path, 'utf8')
700
+ }
701
+ if (opts.warnOnFailure) {
702
+ const docfile = this.getAttribute('docfile') || '<stdin>'
703
+ const label = opts.label || 'file'
704
+ this.logger.warn(
705
+ `${docfile}: ${label} does not exist or cannot be read: ${path}`
706
+ )
707
+ }
708
+ return null
709
+ }
710
+
711
+ /**
712
+ * Resolve the URI or system path to the target, then read and return its contents.
713
+ *
714
+ * When the resolved path is a URI and allow-uri-read is enabled, the content is
715
+ * fetched via the Fetch API (async). When it is a local path, the file is read
716
+ * via {@link readAsset}.
717
+ *
718
+ * @param {string} target - The URI or local path String from which to read the data.
719
+ * @param {Object} [opts={}] - A plain object of options:
720
+ * - `label` {string} - Label used in warning messages (default: 'asset').
721
+ * - `normalize` {boolean} - Whether the data should be normalized (default: false).
722
+ * - `start` {string} - Relative base path for resolving the target.
723
+ * - `warnOnFailure` {boolean} - Whether warnings are issued on failure (default: true).
724
+ * - `warnIfEmpty` {boolean} - Whether a warning is issued when the target contents are empty (default: false).
725
+ * @returns {Promise<string|null>} a Promise resolving to the content, or null on failure.
726
+ */
727
+ async readContents(target, opts = {}) {
728
+ const doc = this.document
729
+ const label = opts.label || 'asset'
730
+ let contents
731
+ let resolvedTarget = target
732
+ const start = opts.start
733
+ const warnOnFailure = opts.warnOnFailure !== false
734
+
735
+ if (
736
+ isUriish(target) ||
737
+ (start &&
738
+ isUriish(start) &&
739
+ (resolvedTarget = doc.pathResolver.webPath(target, start)))
740
+ ) {
741
+ if (doc.hasAttribute('allow-uri-read')) {
742
+ try {
743
+ const response = await fetch(resolvedTarget)
744
+ const text = await response.text()
745
+ contents = opts.normalize ? prepareSourceString(text).join(LF) : text
746
+ } catch {
747
+ if (warnOnFailure)
748
+ this.logger.warn(
749
+ `could not retrieve contents of ${label} at URI: ${resolvedTarget}`
750
+ )
751
+ }
752
+ } else if (warnOnFailure) {
753
+ this.logger.warn(
754
+ `cannot retrieve contents of ${label} at URI: ${resolvedTarget} (allow-uri-read attribute not enabled)`
755
+ )
756
+ }
757
+ } else {
758
+ resolvedTarget = this.normalizeSystemPath(target, opts.start, null, {
759
+ targetName: label,
760
+ })
761
+ contents = await this.readAsset(resolvedTarget, {
762
+ normalize: opts.normalize,
763
+ warnOnFailure,
764
+ label,
765
+ })
766
+ }
767
+
768
+ if (contents && opts.warnIfEmpty && contents.length === 0) {
769
+ this.logger.warn(`contents of ${label} is empty: ${resolvedTarget}`)
770
+ }
771
+ return contents
772
+ }
773
+
774
+ /**
775
+ * @deprecated Use `isUriish` from helpers.js instead.
776
+ * @param {string} str
777
+ * @returns {boolean}
778
+ */
779
+ isUri(str) {
780
+ return isUriish(str)
781
+ }
782
+
783
+ /**
784
+ * Provide a default logger.
785
+ * The Logging mixin (logging.js) overrides this getter on the prototype.
786
+ */
787
+ get logger() {
788
+ return this.document?.logger ?? console
789
+ }
790
+
791
+ // ── JavaScript-style accessors ────────────────────────────────────────────────
792
+
793
+ /**
794
+ * Get the logger for this node.
795
+ * @returns {object} the logger instance.
796
+ */
797
+ getLogger() {
798
+ return this.logger
799
+ }
800
+
801
+ /**
802
+ * Retrieve the space-separated String role for this node.
803
+ *
804
+ * @returns {string|undefined} the role as a space-separated String.
805
+ */
806
+ getRole() {
807
+ return this.role
808
+ }
809
+
810
+ /**
811
+ * Set the value of the role attribute on this node.
812
+ *
813
+ * Accepts a single role name, a space-separated String, an Array, or spread arguments.
814
+ *
815
+ * @param {...string|string[]} names - A single role name, a space-separated String, an Array,
816
+ * or multiple role names as spread arguments.
817
+ * @returns {string} the value of the role attribute.
818
+ */
819
+ setRole(...names) {
820
+ this.role = names.length === 1 ? names[0] : names
821
+ return this.attributes.role
822
+ }
823
+
824
+ /**
825
+ * Retrieve the String role names for this node as an Array.
826
+ *
827
+ * @returns {string[]} the role names as a String Array, empty if the role attribute is absent.
828
+ */
829
+ getRoles() {
830
+ return this.roles
831
+ }
832
+
833
+ /**
834
+ * Get the attributes hash for this node.
835
+ *
836
+ * @returns {Object} a plain Object of attributes.
837
+ */
838
+ getAttributes() {
839
+ return this.attributes
840
+ }
841
+
842
+ /**
843
+ * Get the document to which this node belongs.
844
+ *
845
+ * @returns {Document} the Document.
846
+ */
847
+ getDocument() {
848
+ return this.document
849
+ }
850
+
851
+ /**
852
+ * Get the parent node of this node.
853
+ *
854
+ * @returns {AbstractNode|undefined} the parent AbstractNode, or undefined for the root document.
855
+ */
856
+ getParent() {
857
+ return this._parent
858
+ }
859
+
860
+ /**
861
+ * Set the parent of this node.
862
+ * Also updates the document reference.
863
+ */
864
+ setParent(parent) {
865
+ this._parent = parent
866
+ this.document = parent.document
867
+ }
868
+
869
+ /**
870
+ * Get the String name of this node.
871
+ *
872
+ * @returns {string} the node name.
873
+ */
874
+ getNodeName() {
875
+ return this.nodeName
876
+ }
877
+
878
+ /**
879
+ * Get the String id for this node.
880
+ *
881
+ * @returns {string|undefined} the id, or undefined if not set.
882
+ */
883
+ getId() {
884
+ return this.id ?? undefined
885
+ }
886
+
887
+ /**
888
+ * Set the String id for this node.
889
+ *
890
+ * @param {string} id - The String id to assign.
891
+ */
892
+ setId(id) {
893
+ this.id = id
894
+ }
895
+
896
+ /**
897
+ * Get the context name for this node.
898
+ *
899
+ * @returns {string} the context name.
900
+ */
901
+ getContext() {
902
+ return this.context
903
+ }
904
+
905
+ /**
906
+ * Get the {Converter} instance being used to convert the current {Document}.
907
+ *
908
+ * @returns {object} the converter instance.
909
+ */
910
+ getConverter() {
911
+ return this.converter
912
+ }
913
+
914
+ /**
915
+ * Get the icon URI for the named icon.
916
+ *
917
+ * @param {string} name - The String icon name.
918
+ * @returns {Promise<string>} a Promise resolving to a String URI.
919
+ */
920
+ getIconUri(name) {
921
+ return this.iconUri(name)
922
+ }
923
+
924
+ /**
925
+ * Get the media URI for the target.
926
+ *
927
+ * @param {string} target - The String target path or URL.
928
+ * @param {string} [assetDirKey='imagesdir'] - The String asset directory attribute key.
929
+ * @returns {string} a String URI.
930
+ */
931
+ getMediaUri(target, assetDirKey = 'imagesdir') {
932
+ return this.mediaUri(target, assetDirKey)
933
+ }
934
+
935
+ /**
936
+ * Get the image URI for the target image.
937
+ *
938
+ * @param {string} targetImage - The String target image path or URL.
939
+ * @param {string|null} [assetDirKey=null] - The String asset directory attribute key.
940
+ * @returns {Promise<string>} a Promise resolving to a String URI.
941
+ */
942
+ getImageUri(targetImage, assetDirKey = null) {
943
+ return this.imageUri(targetImage, assetDirKey)
944
+ }
945
+
946
+ /**
947
+ * Get the value of the reftext attribute with substitutions applied.
948
+ *
949
+ * @returns {string|undefined} the reftext value, or undefined if not set.
950
+ */
951
+ getReftext() {
952
+ return this.reftext ?? undefined
953
+ }
954
+ }