@asciidoctor/core 3.0.4 → 4.0.0-alpha.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.
Files changed (92) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24156 -0
  3. package/build/node/index.cjs +24737 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +55 -97
  6. package/src/abstract_block.js +857 -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 +1893 -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 +346 -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.cts +75 -0
  69. package/types/index.d.ts +73 -3731
  70. package/types/inline.d.ts +69 -0
  71. package/types/list.d.ts +114 -0
  72. package/types/load.d.ts +39 -0
  73. package/types/logging.d.ts +187 -0
  74. package/types/parser.d.ts +114 -0
  75. package/types/path_resolver.d.ts +103 -0
  76. package/types/reader.d.ts +184 -0
  77. package/types/rx.d.ts +513 -0
  78. package/types/section.d.ts +122 -0
  79. package/types/stylesheets.d.ts +10 -0
  80. package/types/substitutors.d.ts +208 -0
  81. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  82. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  83. package/types/syntax_highlighter.d.ts +167 -0
  84. package/types/table.d.ts +231 -0
  85. package/types/timings.d.ts +25 -0
  86. package/types/tsconfig.json +9 -0
  87. package/LICENSE +0 -21
  88. package/dist/browser/asciidoctor.js +0 -47654
  89. package/dist/browser/asciidoctor.min.js +0 -1452
  90. package/dist/graalvm/asciidoctor.js +0 -47402
  91. package/dist/node/asciidoctor.cjs +0 -21567
  92. package/dist/node/asciidoctor.js +0 -23037
@@ -0,0 +1,12 @@
1
+ export class AttributeEntry {
2
+ constructor(name, value, negate = null) {
3
+ this.name = name
4
+ this.value = value
5
+ this.negate = negate == null ? value == null : negate
6
+ }
7
+
8
+ saveTo(blockAttributes) {
9
+ ;(blockAttributes.attribute_entries ??= []).push(this)
10
+ return this
11
+ }
12
+ }
@@ -0,0 +1,380 @@
1
+ // ESM conversion of attribute_list.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - Ruby's StringScanner is reimplemented as the module-private StringScanner
5
+ // class using JS sticky regexes (flag 'y'). The scanner caches a sticky
6
+ // version of each RegExp on first use to avoid repeated RegExp construction.
7
+ // - scan() returns null (not nil) on no-match; getByte() returns undefined at EOS.
8
+ // - Ruby's boolean `false` return from parse_attribute (bare `return`) is
9
+ // represented as `return false`.
10
+ // - The `continue` local variable is renamed `shouldContinue` because `continue`
11
+ // is a reserved word in JS.
12
+ // - snake_case method names are converted to camelCase.
13
+ // - Private methods/fields use the JS # prefix.
14
+ // - block.apply_subs → block.applySubs (matches Substitutors mixin naming).
15
+
16
+ import { CG_WORD, CC_WORD } from './rx.js'
17
+
18
+ // ── Constants ─────────────────────────────────────────────────────────────────
19
+ const APOS = "'"
20
+ const BACKSLASH = '\\'
21
+ const QUOT = '"'
22
+
23
+ // Regular expressions for detecting the boundary of a value.
24
+ // These are passed to StringScanner which converts them to sticky variants.
25
+ const BoundaryRx = {
26
+ [QUOT]: /.*?[^\\](?=")/,
27
+ [APOS]: /.*?[^\\](?=')/,
28
+ ',': /.*?(?=[ \t]*(,|$))/,
29
+ }
30
+
31
+ // Regular expressions for unescaping quoted characters.
32
+ const EscapedQuotes = {
33
+ [QUOT]: '\\"',
34
+ [APOS]: "\\'",
35
+ }
36
+
37
+ // Regular expressions for skipping delimiters.
38
+ const SkipRx = {
39
+ ',': /[ \t]*(,|$)/,
40
+ }
41
+
42
+ // Attribute name: starts with a word character, followed by word chars or hyphens.
43
+ // Constructed with the 'u' flag so \p{…} Unicode properties work.
44
+ const NameRx = new RegExp(`${CG_WORD}[${CC_WORD}\\-]*`, 'u')
45
+
46
+ // Matches one or more horizontal whitespace characters.
47
+ const BlankRx = /[ \t]+/
48
+
49
+ // ── StringScanner ─────────────────────────────────────────────────────────────
50
+ /**
51
+ * A minimal port of Ruby's StringScanner, sufficient for AttributeList parsing.
52
+ *
53
+ * Differences from Ruby's StringScanner:
54
+ * - getByte() returns undefined (not nil) at end of string.
55
+ * - scan/skip return null/0 (not nil) on no match.
56
+ * - Regexes are anchored at the current position via the sticky ('y') flag.
57
+ * A sticky copy is created once per regex and cached for reuse.
58
+ * - unscan() reverts only the most recent getByte / scan / skip advance.
59
+ */
60
+ class StringScanner {
61
+ #source
62
+ #pos = 0
63
+ #lastMatchLen = 0
64
+ #stickyCache = new Map()
65
+
66
+ constructor(source) {
67
+ this.#source = source
68
+ }
69
+
70
+ /** @returns {string} The original source string (equivalent to Ruby scanner.string). */
71
+ get source() {
72
+ return this.#source
73
+ }
74
+
75
+ /** @returns {boolean} true when the scan pointer is at or past the end of the string. */
76
+ eos() {
77
+ return this.#pos >= this.#source.length
78
+ }
79
+
80
+ /**
81
+ * @param {number} n
82
+ * @returns {string} The next n characters without advancing the scan pointer.
83
+ */
84
+ peek(n) {
85
+ return this.#source.slice(this.#pos, this.#pos + n)
86
+ }
87
+
88
+ /** @returns {string|undefined} The next character, or undefined at EOS. */
89
+ getByte() {
90
+ if (this.#pos >= this.#source.length) {
91
+ this.#lastMatchLen = 0
92
+ return undefined
93
+ }
94
+ this.#lastMatchLen = 1
95
+ return this.#source[this.#pos++]
96
+ }
97
+
98
+ /** Reverts the most recent getByte / scan / skip advance. */
99
+ unscan() {
100
+ this.#pos -= this.#lastMatchLen
101
+ this.#lastMatchLen = 0
102
+ }
103
+
104
+ /**
105
+ * Advances past rx at the current position.
106
+ * @param {RegExp} rx
107
+ * @returns {number} The number of characters skipped, or 0 on no match.
108
+ */
109
+ skip(rx) {
110
+ const m = this.#exec(rx)
111
+ return m ? m[0].length : 0
112
+ }
113
+
114
+ /**
115
+ * @param {RegExp} rx
116
+ * @returns {string|null} The matched string at the current position, or null on no match.
117
+ */
118
+ scan(rx) {
119
+ const m = this.#exec(rx)
120
+ return m ? m[0] : null
121
+ }
122
+
123
+ /**
124
+ * @param {RegExp} rx
125
+ * @returns {RegExpExecArray|null}
126
+ */
127
+ #exec(rx) {
128
+ let sticky = this.#stickyCache.get(rx)
129
+ if (!sticky) {
130
+ const flags = rx.flags.includes('y') ? rx.flags : `${rx.flags}y`
131
+ sticky = new RegExp(rx.source, flags)
132
+ this.#stickyCache.set(rx, sticky)
133
+ }
134
+ sticky.lastIndex = this.#pos
135
+ const m = sticky.exec(this.#source)
136
+ if (!m) {
137
+ this.#lastMatchLen = 0
138
+ return null
139
+ }
140
+ this.#lastMatchLen = m[0].length
141
+ this.#pos += m[0].length
142
+ return m
143
+ }
144
+ }
145
+
146
+ // ── AttributeList ─────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Handles parsing AsciiDoc attribute lists into a plain object of key/value pairs.
150
+ * By default, attributes must each be separated by a comma and quotes may be used
151
+ * around the value. If a key is not detected, the value is assigned to a 1-based
152
+ * positional key. Positional attributes can be "rekeyed" when given a positionalAttrs
153
+ * array either during parsing or after.
154
+ *
155
+ * @example
156
+ * const attrlist = new AttributeList('astyle')
157
+ * await attrlist.parse()
158
+ * // => { 1: 'astyle' }
159
+ *
160
+ * attrlist.rekey(['style'])
161
+ * // => { 1: 'astyle', style: 'astyle' }
162
+ *
163
+ * @example
164
+ * const attrlist2 = new AttributeList('quote, Famous Person, Famous Book (2001)')
165
+ * await attrlist2.parse(['style', 'attribution', 'citetitle'])
166
+ * // => { 1: 'quote', style: 'quote', 2: 'Famous Person', attribution: 'Famous Person',
167
+ * // 3: 'Famous Book (2001)', citetitle: 'Famous Book (2001)' }
168
+ */
169
+ export class AttributeList {
170
+ #scanner
171
+ #block
172
+ #delimiter
173
+ #delimiterSkipPattern
174
+ #delimiterBoundaryPattern
175
+ #attributes = null
176
+
177
+ constructor(source, block = null, delimiter = ',') {
178
+ this.#scanner = new StringScanner(source)
179
+ this.#block = block
180
+ this.#delimiter = delimiter
181
+ this.#delimiterSkipPattern = SkipRx[delimiter]
182
+ this.#delimiterBoundaryPattern = BoundaryRx[delimiter]
183
+ }
184
+
185
+ /**
186
+ * Parse the attribute list and merge the result into the given object.
187
+ * @param {Object} attributes - The target plain object to update.
188
+ * @param {string[]} [positionalAttrs=[]] - An array of keys to assign to positional values.
189
+ * @returns {Promise<Object>} The updated attributes object.
190
+ */
191
+ async parseInto(attributes, positionalAttrs = []) {
192
+ return Object.assign(attributes, await this.parse(positionalAttrs))
193
+ }
194
+
195
+ /**
196
+ * Parse the attribute list and return a plain object of key/value pairs.
197
+ * Subsequent calls return the already-parsed result without re-parsing.
198
+ * @param {string[]} [positionalAttrs=[]] - An array of keys to assign to positional values.
199
+ * @returns {Promise<Object>} A plain object of parsed attributes.
200
+ */
201
+ async parse(positionalAttrs = []) {
202
+ if (this.#attributes) return this.#attributes
203
+ this.#attributes = {}
204
+ let index = 0
205
+ while (await this.#parseAttribute(index, positionalAttrs)) {
206
+ if (this.#scanner.eos()) break
207
+ this.#skipDelimiter()
208
+ index++
209
+ }
210
+ return this.#attributes
211
+ }
212
+
213
+ /**
214
+ * Rekey the parsed positional attributes using the given key names.
215
+ * @param {string[]} positionalAttrs - An array of keys to assign to positional values.
216
+ * @returns {Object} The updated attributes object.
217
+ */
218
+ rekey(positionalAttrs) {
219
+ return AttributeList.rekey(this.#attributes, positionalAttrs)
220
+ }
221
+
222
+ /**
223
+ * Assign string keys to the positional (numeric-keyed) values of the given attributes object.
224
+ * @param {Object} attributes - A plain object produced by parse().
225
+ * @param {Array<string|null>} positionalAttrs - Keys to assign (null entries are skipped).
226
+ * @returns {Object} The updated attributes object.
227
+ */
228
+ static rekey(attributes, positionalAttrs) {
229
+ for (let i = 0; i < positionalAttrs.length; i++) {
230
+ const key = positionalAttrs[i]
231
+ if (key) {
232
+ const val = attributes[i + 1]
233
+ if (val != null) attributes[key] = val
234
+ }
235
+ }
236
+ return attributes
237
+ }
238
+
239
+ /**
240
+ * @param {number} index
241
+ * @param {string[]} positionalAttrs
242
+ * @returns {Promise<boolean>} true to continue parsing, false to stop.
243
+ */
244
+ async #parseAttribute(index, positionalAttrs) {
245
+ let shouldContinue = true
246
+ this.#skipBlank()
247
+ const peeked = this.#scanner.peek(1)
248
+ let name, value, singleQuoted
249
+
250
+ if (peeked === QUOT) {
251
+ // example: "quote" || "foo
252
+ name = this.#parseAttributeValue(this.#scanner.getByte())
253
+ } else if (peeked === APOS) {
254
+ // example: 'quote' || 'foo
255
+ name = this.#parseAttributeValue(this.#scanner.getByte())
256
+ if (!name.startsWith(APOS)) singleQuoted = true
257
+ } else {
258
+ name = this.#scanName()
259
+ const skipped = (name !== null && this.#skipBlank()) || 0
260
+
261
+ if (this.#scanner.eos()) {
262
+ // Stop unless we have a name or the source ends with the delimiter
263
+ if (!name && !this.#scanner.source.trimEnd().endsWith(this.#delimiter))
264
+ return false
265
+ // example: quote (at eos)
266
+ shouldContinue = false
267
+ } else {
268
+ const c = this.#scanner.getByte()
269
+ if (c === this.#delimiter) {
270
+ // example: quote,
271
+ this.#scanner.unscan()
272
+ } else if (name) {
273
+ if (c === '=') {
274
+ // example: foo=...
275
+ this.#skipBlank()
276
+ const c2 = this.#scanner.getByte()
277
+ if (c2 === QUOT) {
278
+ // example: foo="bar" || foo="ba\"zaar" || foo="bar
279
+ value = this.#parseAttributeValue(c2)
280
+ } else if (c2 === APOS) {
281
+ // example: foo='bar' || foo='ba\'zaar' || foo='ba"zaar' || foo='bar
282
+ value = this.#parseAttributeValue(c2)
283
+ if (!value.startsWith(APOS)) singleQuoted = true
284
+ } else if (c2 === this.#delimiter) {
285
+ // example: foo=,
286
+ value = ''
287
+ this.#scanner.unscan()
288
+ } else if (c2 === undefined) {
289
+ // example: foo= (at eos)
290
+ value = ''
291
+ } else {
292
+ // example: foo=bar || foo=None
293
+ value = `${c2}${this.#scanToDelimiter() ?? ''}`
294
+ if (value === 'None') return true
295
+ }
296
+ } else {
297
+ // example: foo bar
298
+ name = `${name}${' '.repeat(skipped)}${c}${this.#scanToDelimiter() ?? ''}`
299
+ }
300
+ } else {
301
+ // example: =foo= || !foo
302
+ name = `${c}${this.#scanToDelimiter() ?? ''}`
303
+ }
304
+ }
305
+ }
306
+
307
+ if (value !== undefined) {
308
+ // Named attribute
309
+ if (name === 'options' || name === 'opts') {
310
+ // example: options="opt1,opt2,opt3" || opts="opt1,opt2,opt3"
311
+ if (value.includes(',')) {
312
+ if (value.includes(' ')) value = value.replace(/ /g, '')
313
+ for (const opt of value.split(',')) {
314
+ if (opt) this.#attributes[`${opt}-option`] = ''
315
+ }
316
+ } else if (value) {
317
+ this.#attributes[`${value}-option`] = ''
318
+ }
319
+ } else if (singleQuoted && this.#block) {
320
+ if (name === 'title' || name === 'reftext') {
321
+ this.#attributes[name] = value
322
+ } else {
323
+ this.#attributes[name] = await this.#block.applySubs(value)
324
+ }
325
+ } else {
326
+ this.#attributes[name] = value
327
+ }
328
+ } else {
329
+ // Positional attribute
330
+ if (singleQuoted && this.#block) {
331
+ name = await this.#block.applySubs(name)
332
+ }
333
+ const positionalAttrName = positionalAttrs[index]
334
+ if (positionalAttrName && name != null) {
335
+ this.#attributes[positionalAttrName] = name
336
+ }
337
+ // QUESTION should we assign the positional key even when claimed by a positional attribute?
338
+ this.#attributes[index + 1] = name
339
+ }
340
+
341
+ return shouldContinue
342
+ }
343
+
344
+ /**
345
+ * @param {string} quote - The quote character that opened this value (QUOT or APOS).
346
+ * @returns {string} The parsed value (unescaped, without surrounding quotes).
347
+ */
348
+ #parseAttributeValue(quote) {
349
+ // empty quoted value: "" or ''
350
+ if (this.#scanner.peek(1) === quote) {
351
+ this.#scanner.getByte()
352
+ return ''
353
+ }
354
+ const value = this.#scanToQuote(quote)
355
+ if (value !== null) {
356
+ this.#scanner.getByte() // consume closing quote
357
+ return value.includes(BACKSLASH)
358
+ ? value.replaceAll(EscapedQuotes[quote], quote)
359
+ : value
360
+ }
361
+ // no closing quote found – treat opening quote as part of the value
362
+ return `${quote}${this.#scanToDelimiter() ?? ''}`
363
+ }
364
+
365
+ #skipBlank() {
366
+ return this.#scanner.skip(BlankRx)
367
+ }
368
+ #skipDelimiter() {
369
+ return this.#scanner.skip(this.#delimiterSkipPattern)
370
+ }
371
+ #scanName() {
372
+ return this.#scanner.scan(NameRx)
373
+ }
374
+ #scanToDelimiter() {
375
+ return this.#scanner.scan(this.#delimiterBoundaryPattern)
376
+ }
377
+ #scanToQuote(quote) {
378
+ return this.#scanner.scan(BoundaryRx[quote])
379
+ }
380
+ }
package/src/block.js ADDED
@@ -0,0 +1,168 @@
1
+ // ESM conversion of block.rb
2
+
3
+ import { AbstractBlock } from './abstract_block.js'
4
+ import { prepareSourceString } from './helpers.js'
5
+ import { LF } from './constants.js'
6
+
7
+ /**
8
+ * Maps block context strings to their default content model.
9
+ * Any context not listed defaults to 'simple'.
10
+ * @type {Object<string, string>}
11
+ */
12
+ export const DEFAULT_CONTENT_MODEL = new Proxy(
13
+ {
14
+ audio: 'empty',
15
+ image: 'empty',
16
+ listing: 'verbatim',
17
+ literal: 'verbatim',
18
+ stem: 'raw',
19
+ open: 'compound',
20
+ page_break: 'empty',
21
+ pass: 'raw',
22
+ thematic_break: 'empty',
23
+ video: 'empty',
24
+ },
25
+ {
26
+ get: (target, key) => (Object.hasOwn(target, key) ? target[key] : 'simple'),
27
+ }
28
+ )
29
+
30
+ /**
31
+ * Methods for managing AsciiDoc content blocks.
32
+ */
33
+ export class Block extends AbstractBlock {
34
+ /** @type {string[]} */
35
+ lines
36
+ /** @type {string[]|null} */
37
+ defaultSubs
38
+
39
+ /**
40
+ * Factory method — mirrors the core Block.create(parent, context, opts) API.
41
+ * @param {AbstractBlock} parent
42
+ * @param {string} context
43
+ * @param {Object} [opts={}]
44
+ * @returns {Block}
45
+ */
46
+ static create(parent, context, opts = {}) {
47
+ return new Block(parent, context, opts)
48
+ }
49
+
50
+ /**
51
+ * Initialize an Asciidoctor::Block object.
52
+ * @param {AbstractBlock} parent - The parent AbstractBlock.
53
+ * @param {string} context - The context name (e.g. 'paragraph', 'listing').
54
+ * @param {Object} [opts={}]
55
+ * @param {'compound'|'simple'|'verbatim'|'raw'|'empty'} [opts.content_model] - Defaults to lookup from DEFAULT_CONTENT_MODEL.
56
+ * @param {Object} [opts.attributes] - Attributes to merge in.
57
+ * @param {string|string[]} [opts.source] - Raw source string or lines.
58
+ * @param {'default'|string[]|string|null} [opts.subs]
59
+ * @param {string[]} [opts.default_subs] - Override for default subs (used with subs: 'default').
60
+ */
61
+ constructor(parent, context, opts = {}) {
62
+ super(parent, context, opts)
63
+ this.contentModel = opts.content_model ?? DEFAULT_CONTENT_MODEL[context]
64
+
65
+ if ('subs' in opts) {
66
+ const subs = opts.subs
67
+ if (subs) {
68
+ if (subs === 'default') {
69
+ // subs attribute is honored; falls back to opts.default_subs then built-in defaults
70
+ this.defaultSubs = opts.default_subs ?? null
71
+ } else if (Array.isArray(subs)) {
72
+ // subs attribute is not honored; use provided array directly
73
+ this.defaultSubs = [...subs]
74
+ delete this.attributes.subs
75
+ } else {
76
+ // e.g. subs: 'normal' — subs attribute is not honored
77
+ this.defaultSubs = null
78
+ this.attributes.subs = String(subs)
79
+ }
80
+ // Resolve subs eagerly when subs option is specified
81
+ this.commitSubs()
82
+ } else {
83
+ // subs: null/[] — lock subs as empty; subsequent commitSubs() calls are no-ops
84
+ this.defaultSubs = []
85
+ delete this.attributes.subs
86
+ }
87
+ } else {
88
+ // Defer subs resolution; subs attribute will be honored later
89
+ this.defaultSubs = null
90
+ }
91
+
92
+ const rawSource = opts.source
93
+ if (!rawSource && rawSource !== 0) {
94
+ this.lines = []
95
+ } else if (typeof rawSource === 'string') {
96
+ this.lines = prepareSourceString(rawSource)
97
+ } else {
98
+ this.lines = [...rawSource]
99
+ }
100
+ }
101
+
102
+ /** @returns {string} Alias for context — consistent with AsciiDoc terminology. */
103
+ get blockname() {
104
+ return this.context
105
+ }
106
+
107
+ /**
108
+ * Get the converted result appropriate to this block's content model.
109
+ * @returns {Promise<string|null>}
110
+ */
111
+ async content() {
112
+ switch (this.contentModel) {
113
+ case 'compound':
114
+ return super.content()
115
+ case 'simple':
116
+ return this.applySubs(this.lines.join(LF), this.subs)
117
+ case 'verbatim':
118
+ case 'raw': {
119
+ const result = await this.applySubs(this.lines, this.subs)
120
+ if (result.length < 2) return result[0] ?? ''
121
+ while (result.length > 0 && result[0].trimEnd() === '') result.shift()
122
+ while (result.length > 0 && result[result.length - 1].trimEnd() === '')
123
+ result.pop()
124
+ return result.join(LF)
125
+ }
126
+ default:
127
+ if (this.contentModel !== 'empty') {
128
+ this.logger.warn(
129
+ `unknown content model '${this.contentModel}' for block: ${this}`
130
+ )
131
+ }
132
+ return null
133
+ }
134
+ }
135
+
136
+ /** @returns {string[]} The source lines for this block (matches the core API). */
137
+ getSourceLines() {
138
+ return this.lines
139
+ }
140
+
141
+ /** @returns {string} The preprocessed source of this block as a single String. */
142
+ get source() {
143
+ return this.lines.join(LF)
144
+ }
145
+
146
+ /** @returns {string} The source as a single String (alias for the source getter). */
147
+ getSource() {
148
+ return this.source
149
+ }
150
+
151
+ // ── JavaScript-style accessors ────────────────────────────────────────────────
152
+
153
+ /**
154
+ * Get the block name (alias for context).
155
+ * @returns {string}
156
+ */
157
+ getBlockName() {
158
+ return this.blockname
159
+ }
160
+
161
+ toString() {
162
+ const contentSummary =
163
+ this.contentModel === 'compound'
164
+ ? `blocks: ${this.blocks.length}`
165
+ : `lines: ${this.lines.length}`
166
+ return `#<Block {context: '${this.context}', content_model: '${this.contentModel}', style: ${JSON.stringify(this.style ?? null)}, ${contentSummary}}>`
167
+ }
168
+ }
@@ -0,0 +1,22 @@
1
+ // Browser-specific asset reading via Fetch API.
2
+ //
3
+ // In a browser environment the document base directory is resolved as an HTTP URL,
4
+ // so "local" assets are served over HTTP rather than from the filesystem.
5
+ // This module provides a fetch-based fallback used by readContents when the
6
+ // resolved path is an HTTP/HTTPS URI (i.e. docdir was set to a browser URL).
7
+
8
+ /**
9
+ * Fetch the text content of a URI.
10
+ *
11
+ * @param {string} uri - The URI to fetch.
12
+ * @returns {Promise<string|null>} the response text, or null on failure.
13
+ */
14
+ export async function readBrowserAsset(uri) {
15
+ try {
16
+ const response = await fetch(uri)
17
+ if (!response.ok) return null
18
+ return response.text()
19
+ } catch {
20
+ return null
21
+ }
22
+ }