@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.
- package/README.md +42 -10
- package/build/browser/index.js +24156 -0
- package/build/node/index.cjs +24737 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +55 -97
- package/src/abstract_block.js +857 -0
- package/src/abstract_node.js +954 -0
- package/src/attribute_entry.js +12 -0
- package/src/attribute_list.js +380 -0
- package/src/block.js +168 -0
- package/src/browser/asset.js +22 -0
- package/src/browser/reader.js +138 -0
- package/src/browser.js +121 -0
- package/src/callouts.js +85 -0
- package/src/compliance.js +54 -0
- package/src/constants.js +665 -0
- package/src/convert.js +370 -0
- package/src/converter/composite.js +83 -0
- package/src/converter/docbook5.js +1031 -0
- package/src/converter/html5.js +1893 -0
- package/src/converter/manpage.js +935 -0
- package/src/converter/template.js +459 -0
- package/src/converter.js +478 -0
- package/src/data/stylesheet-data.js +2 -0
- package/src/document.js +2134 -0
- package/src/extensions.js +1952 -0
- package/src/footnote.js +28 -0
- package/src/helpers.js +355 -0
- package/src/index.js +138 -0
- package/src/inline.js +158 -0
- package/src/list.js +240 -0
- package/src/load.js +276 -0
- package/src/logging.js +526 -0
- package/src/parser.js +3661 -0
- package/src/path_resolver.js +472 -0
- package/src/reader.js +1755 -0
- package/src/rx.js +829 -0
- package/src/section.js +354 -0
- package/src/stylesheets.js +30 -0
- package/src/substitutors.js +2241 -0
- package/src/syntaxHighlighter/highlightjs.js +90 -0
- package/src/syntaxHighlighter/html_pipeline.js +33 -0
- package/src/syntax_highlighter.js +304 -0
- package/src/table.js +952 -0
- package/src/timings.js +78 -0
- package/types/abstract_block.d.ts +346 -0
- package/types/abstract_node.d.ts +471 -0
- package/types/attribute_entry.d.ts +7 -0
- package/types/attribute_list.d.ts +52 -0
- package/types/block.d.ts +55 -0
- package/types/browser/asset.d.ts +7 -0
- package/types/browser/reader.d.ts +29 -0
- package/types/callouts.d.ts +36 -0
- package/types/compliance.d.ts +23 -0
- package/types/constants.d.ts +268 -0
- package/types/convert.d.ts +34 -0
- package/types/converter/composite.d.ts +20 -0
- package/types/converter/docbook5.d.ts +41 -0
- package/types/converter/html5.d.ts +51 -0
- package/types/converter/manpage.d.ts +59 -0
- package/types/converter/template.d.ts +83 -0
- package/types/converter.d.ts +150 -0
- package/types/data/stylesheet-data.d.ts +2 -0
- package/types/document.d.ts +495 -0
- package/types/extensions.d.ts +876 -0
- package/types/footnote.d.ts +18 -0
- package/types/helpers.d.ts +146 -0
- package/types/index.d.cts +75 -0
- package/types/index.d.ts +73 -3731
- package/types/inline.d.ts +69 -0
- package/types/list.d.ts +114 -0
- package/types/load.d.ts +39 -0
- package/types/logging.d.ts +187 -0
- package/types/parser.d.ts +114 -0
- package/types/path_resolver.d.ts +103 -0
- package/types/reader.d.ts +184 -0
- package/types/rx.d.ts +513 -0
- package/types/section.d.ts +122 -0
- package/types/stylesheets.d.ts +10 -0
- package/types/substitutors.d.ts +208 -0
- package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
- package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
- package/types/syntax_highlighter.d.ts +167 -0
- package/types/table.d.ts +231 -0
- package/types/timings.d.ts +25 -0
- package/types/tsconfig.json +9 -0
- package/LICENSE +0 -21
- package/dist/browser/asciidoctor.js +0 -47654
- package/dist/browser/asciidoctor.min.js +0 -1452
- package/dist/graalvm/asciidoctor.js +0 -47402
- package/dist/node/asciidoctor.cjs +0 -21567
- 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
|
+
}
|