@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
package/src/reader.js
ADDED
|
@@ -0,0 +1,1755 @@
|
|
|
1
|
+
// ESM conversion of reader.rb
|
|
2
|
+
//
|
|
3
|
+
// Ruby-to-JavaScript notes:
|
|
4
|
+
// - @lines is an Array used as a reversed stack: @lines[-1] is the next line.
|
|
5
|
+
// In JS: this._lines[this._lines.length - 1] / this._lines.pop() / push().
|
|
6
|
+
// - Ruby private methods called by subclasses (shift, unshift, unshift_all,
|
|
7
|
+
// process_line, prepare_lines) use the _ prefix convention rather than JS
|
|
8
|
+
// # private, because PreprocessorReader must be able to override/call them.
|
|
9
|
+
// - JS # private fields are used only for data that is truly inaccessible to
|
|
10
|
+
// subclasses and external callers (none here, to keep inheritance clean).
|
|
11
|
+
// - PreprocessorReader overrides _shift() to strip the backslash from escaped
|
|
12
|
+
// directives, mirroring the Ruby `def shift` override.
|
|
13
|
+
// - PreprocessorReader overrides _prepareLines() to add front-matter handling
|
|
14
|
+
// and indentation adjustment (mirrors `def prepare_lines`).
|
|
15
|
+
// - The Logging mixin is implemented with inline helper methods; the logger
|
|
16
|
+
// defaults to this._document?.logger ?? console.
|
|
17
|
+
// - File I/O uses node:fs/promises async APIs (unavailable in browsers).
|
|
18
|
+
// - URI-based includes use the async Fetch API.
|
|
19
|
+
// - Compliance.attribute_missing defaults to 'skip' until compliance.js exists.
|
|
20
|
+
// - Parser.adjustIndentation is referenced but forwarded as a TODO.
|
|
21
|
+
// - RUBY_ENGINE_OPAL branches are omitted.
|
|
22
|
+
// - JRuby-specific unshift_all variant is omitted; the standard branch is used.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
LF,
|
|
26
|
+
MAX_INT,
|
|
27
|
+
ATTR_REF_HEAD,
|
|
28
|
+
LIST_CONTINUATION,
|
|
29
|
+
ASCIIDOC_EXTENSIONS,
|
|
30
|
+
SafeMode,
|
|
31
|
+
} from './constants.js'
|
|
32
|
+
import {
|
|
33
|
+
ConditionalDirectiveRx,
|
|
34
|
+
IncludeDirectiveRx,
|
|
35
|
+
TagDirectiveRx,
|
|
36
|
+
EvalExpressionRx,
|
|
37
|
+
} from './rx.js'
|
|
38
|
+
import {
|
|
39
|
+
prepareSourceArray,
|
|
40
|
+
prepareSourceString,
|
|
41
|
+
rootname,
|
|
42
|
+
isUriish,
|
|
43
|
+
} from './helpers.js'
|
|
44
|
+
import { LoggerManager, Logger } from './logging.js'
|
|
45
|
+
import { Compliance } from './compliance.js'
|
|
46
|
+
import { resolveBrowserIncludePath } from './browser/reader.js'
|
|
47
|
+
|
|
48
|
+
// ── Node.js fs (lazy, optional) ───────────────────────────────────────────────
|
|
49
|
+
// Loaded on first use in Node.js; silently absent in browser/WebWorker environments.
|
|
50
|
+
let _fsp // undefined = not tried, null = unavailable, object = available
|
|
51
|
+
let _fsConstants // node:fs constants (F_OK etc.) — not on node:fs/promises
|
|
52
|
+
|
|
53
|
+
async function _requireFsp() {
|
|
54
|
+
if (_fsp !== undefined) return
|
|
55
|
+
try {
|
|
56
|
+
_fsp = await import('node:fs/promises')
|
|
57
|
+
_fsConstants = (await import('node:fs')).constants
|
|
58
|
+
} catch {
|
|
59
|
+
_fsp = null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── path helpers (no node:path dependency) ───────────────────────────────────
|
|
64
|
+
function fsdirname(p) {
|
|
65
|
+
if (!p) return '.'
|
|
66
|
+
const idx = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'))
|
|
67
|
+
return idx < 0 ? '.' : idx === 0 ? '/' : p.slice(0, idx)
|
|
68
|
+
}
|
|
69
|
+
function fsbasename(p) {
|
|
70
|
+
const idx = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'))
|
|
71
|
+
return p ? p.slice(idx + 1) : ''
|
|
72
|
+
}
|
|
73
|
+
async function fileExists(path) {
|
|
74
|
+
await _requireFsp()
|
|
75
|
+
if (!_fsp) return false
|
|
76
|
+
try {
|
|
77
|
+
await _fsp.access(path, _fsConstants.F_OK)
|
|
78
|
+
return true
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── adjustIndentation ─────────────────────────────────────────────────────────
|
|
85
|
+
// Port of Parser.adjust_indentation! from Ruby.
|
|
86
|
+
// Mutates `lines` in place to remove block indent, then optionally re-indent.
|
|
87
|
+
function _adjustIndentation(lines, indentSize, tabSize = 0) {
|
|
88
|
+
if (lines.length === 0) return
|
|
89
|
+
// Determine block indent (minimum leading spaces of non-blank lines)
|
|
90
|
+
let blockIndent = null
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (line === '') continue
|
|
93
|
+
const lineIndent = line.length - line.trimStart().length
|
|
94
|
+
if (lineIndent === 0) {
|
|
95
|
+
blockIndent = null
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
if (blockIndent === null || lineIndent < blockIndent)
|
|
99
|
+
blockIndent = lineIndent
|
|
100
|
+
}
|
|
101
|
+
if (indentSize === 0) {
|
|
102
|
+
if (blockIndent) {
|
|
103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
104
|
+
if (lines[i] !== '') lines[i] = lines[i].slice(blockIndent)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
const newIndent = ' '.repeat(indentSize)
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
if (lines[i] !== '') {
|
|
111
|
+
lines[i] = blockIndent
|
|
112
|
+
? newIndent + lines[i].slice(blockIndent)
|
|
113
|
+
: newIndent + lines[i]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Cursor ────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export class Cursor {
|
|
122
|
+
constructor(file, dir = null, path = null, lineno = 1) {
|
|
123
|
+
this.file = file
|
|
124
|
+
this.dir = dir
|
|
125
|
+
this.path = path
|
|
126
|
+
this.lineno = lineno
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
advance(num) {
|
|
130
|
+
this.lineno += num
|
|
131
|
+
}
|
|
132
|
+
get lineInfo() {
|
|
133
|
+
return `${this.path}: line ${this.lineno}`
|
|
134
|
+
}
|
|
135
|
+
toString() {
|
|
136
|
+
return this.lineInfo
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Public API (mirrors Ruby Asciidoctor::Reader::Cursor)
|
|
140
|
+
getLineNumber() {
|
|
141
|
+
return this.lineno
|
|
142
|
+
}
|
|
143
|
+
getFile() {
|
|
144
|
+
return this.file ?? undefined
|
|
145
|
+
}
|
|
146
|
+
getDirectory() {
|
|
147
|
+
return this.dir
|
|
148
|
+
}
|
|
149
|
+
getPath() {
|
|
150
|
+
return this.path
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the line info string for this cursor (e.g. "path/to/file.adoc: line 42").
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
getLineInfo() {
|
|
158
|
+
return this.lineInfo
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Reader ────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export class Reader {
|
|
165
|
+
constructor(data = null, cursor = null, opts = {}) {
|
|
166
|
+
if (!cursor) {
|
|
167
|
+
this.file = null
|
|
168
|
+
this._dir = '.'
|
|
169
|
+
this.path = '<stdin>'
|
|
170
|
+
this.lineno = 1
|
|
171
|
+
} else if (typeof cursor === 'string') {
|
|
172
|
+
this.file = cursor
|
|
173
|
+
this._dir = fsdirname(cursor)
|
|
174
|
+
this.path = fsbasename(cursor)
|
|
175
|
+
this.lineno = 1
|
|
176
|
+
} else {
|
|
177
|
+
if ((this.file = cursor.file)) {
|
|
178
|
+
this._dir = cursor.dir || fsdirname(this.file)
|
|
179
|
+
this.path = cursor.path || fsbasename(this.file)
|
|
180
|
+
} else {
|
|
181
|
+
this._dir = cursor.dir || '.'
|
|
182
|
+
this.path = cursor.path || '<stdin>'
|
|
183
|
+
}
|
|
184
|
+
this.lineno = cursor.lineno || 1
|
|
185
|
+
}
|
|
186
|
+
if (opts.document) this._document = opts.document
|
|
187
|
+
this.sourceLines = this._prepareLines(data, opts)
|
|
188
|
+
this._lines = this.sourceLines.slice().reverse()
|
|
189
|
+
this._mark = null
|
|
190
|
+
this._lookAhead = 0
|
|
191
|
+
this.processLines = true
|
|
192
|
+
this._unescapeNextLine = false
|
|
193
|
+
this.unterminated = null
|
|
194
|
+
this._saved = null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** @returns {boolean | Promise<boolean>} */
|
|
200
|
+
hasMoreLines() {
|
|
201
|
+
if (this._lines.length === 0) {
|
|
202
|
+
this._lookAhead = 0
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @returns {boolean | Promise<boolean>} */
|
|
209
|
+
empty() {
|
|
210
|
+
if (this._lines.length === 0) {
|
|
211
|
+
this._lookAhead = 0
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** @returns {boolean | Promise<boolean>} */
|
|
218
|
+
eof() {
|
|
219
|
+
return this.empty()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async nextLineEmpty() {
|
|
223
|
+
const l = await this.peekLine()
|
|
224
|
+
return !l
|
|
225
|
+
}
|
|
226
|
+
async isNextLineEmpty() {
|
|
227
|
+
return await this.nextLineEmpty()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Peek at the next line without consuming it.
|
|
232
|
+
* @param {boolean} [direct=false] - When true, bypass processLine and return the raw stack top.
|
|
233
|
+
* @returns {Promise<string|undefined>} The next line, or undefined if there are no more lines.
|
|
234
|
+
*/
|
|
235
|
+
async peekLine(direct = false) {
|
|
236
|
+
while (true) {
|
|
237
|
+
const nextLine = this._lines[this._lines.length - 1]
|
|
238
|
+
if (direct || this._lookAhead > 0) {
|
|
239
|
+
return this._unescapeNextLine ? nextLine.slice(1) : nextLine
|
|
240
|
+
}
|
|
241
|
+
if (nextLine !== undefined) {
|
|
242
|
+
const line = await this.processLine(nextLine)
|
|
243
|
+
if (line !== null && line !== undefined) return line
|
|
244
|
+
} else {
|
|
245
|
+
this._lookAhead = 0
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Peek at the next num lines without consuming them.
|
|
253
|
+
* @param {number|null} [num=null]
|
|
254
|
+
* @param {boolean} [direct=false]
|
|
255
|
+
* @returns {Promise<string[]>}
|
|
256
|
+
*/
|
|
257
|
+
async peekLines(num = null, direct = false) {
|
|
258
|
+
const oldLookAhead = this._lookAhead
|
|
259
|
+
const result = []
|
|
260
|
+
const limit = num != null ? num : MAX_INT
|
|
261
|
+
for (let i = 0; i < limit; i++) {
|
|
262
|
+
const line = direct ? this._shift() : await this.readLine()
|
|
263
|
+
if (line !== undefined) {
|
|
264
|
+
result.push(line)
|
|
265
|
+
} else {
|
|
266
|
+
if (direct) this.lineno--
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (result.length > 0) {
|
|
271
|
+
this._unshiftAll(result)
|
|
272
|
+
if (direct) this._lookAhead = oldLookAhead
|
|
273
|
+
}
|
|
274
|
+
return result
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async readLine() {
|
|
278
|
+
return this._lookAhead > 0 || (await this.hasMoreLines())
|
|
279
|
+
? this._shift()
|
|
280
|
+
: undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async readLines() {
|
|
284
|
+
const lines = []
|
|
285
|
+
while (await this.hasMoreLines()) lines.push(this._shift())
|
|
286
|
+
return lines
|
|
287
|
+
}
|
|
288
|
+
async readlines() {
|
|
289
|
+
return await this.readLines()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async read() {
|
|
293
|
+
return (await this.readLines()).join(LF)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async advance() {
|
|
297
|
+
return this._shift() !== undefined
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
unshiftLine(lineToRestore) {
|
|
301
|
+
this._unshift(lineToRestore)
|
|
302
|
+
}
|
|
303
|
+
restoreLine(lineToRestore) {
|
|
304
|
+
this._unshift(lineToRestore)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
unshiftLines(linesToRestore) {
|
|
308
|
+
this._unshiftAll(linesToRestore)
|
|
309
|
+
}
|
|
310
|
+
restoreLines(linesToRestore) {
|
|
311
|
+
this._unshiftAll(linesToRestore)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
replaceNextLine(replacement) {
|
|
315
|
+
this._shift()
|
|
316
|
+
this._unshift(replacement)
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
replaceLine(replacement) {
|
|
320
|
+
return this.replaceNextLine(replacement)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async skipBlankLines() {
|
|
324
|
+
if (await this.empty()) return undefined
|
|
325
|
+
let numSkipped = 0
|
|
326
|
+
let nextLine
|
|
327
|
+
while ((nextLine = await this.peekLine()) !== undefined) {
|
|
328
|
+
if (String(nextLine) !== '') return numSkipped
|
|
329
|
+
this._shift()
|
|
330
|
+
numSkipped++
|
|
331
|
+
}
|
|
332
|
+
return undefined
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async skipCommentLines() {
|
|
336
|
+
if (await this.empty()) return
|
|
337
|
+
let nextLine
|
|
338
|
+
while (
|
|
339
|
+
(nextLine = await this.peekLine()) !== undefined &&
|
|
340
|
+
nextLine !== ''
|
|
341
|
+
) {
|
|
342
|
+
if (!nextLine.startsWith('//')) break
|
|
343
|
+
if (nextLine.startsWith('///')) {
|
|
344
|
+
const ll = nextLine.length
|
|
345
|
+
if (!(ll > 3 && nextLine === '/'.repeat(ll))) break
|
|
346
|
+
await this.readLinesUntil({
|
|
347
|
+
terminator: nextLine,
|
|
348
|
+
skipFirstLine: true,
|
|
349
|
+
readLastLine: true,
|
|
350
|
+
skipProcessing: true,
|
|
351
|
+
context: 'comment',
|
|
352
|
+
})
|
|
353
|
+
} else {
|
|
354
|
+
this._shift()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async skipLineComments() {
|
|
360
|
+
if (await this.empty()) return []
|
|
361
|
+
const commentLines = []
|
|
362
|
+
let nextLine
|
|
363
|
+
while (
|
|
364
|
+
(nextLine = await this.peekLine()) !== undefined &&
|
|
365
|
+
nextLine !== ''
|
|
366
|
+
) {
|
|
367
|
+
if (!nextLine.startsWith('//')) break
|
|
368
|
+
commentLines.push(this._shift())
|
|
369
|
+
}
|
|
370
|
+
return commentLines
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
terminate() {
|
|
374
|
+
this.lineno += this._lines.length
|
|
375
|
+
this._lines.length = 0
|
|
376
|
+
this._lookAhead = 0
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Read lines until a termination condition is met.
|
|
381
|
+
* @param {Object} [options={}]
|
|
382
|
+
* @param {string} [options.terminator] - Line at which to stop.
|
|
383
|
+
* @param {boolean} [options.breakOnBlankLines] - Stop on blank lines.
|
|
384
|
+
* @param {boolean} [options.breakOnListContinuation] - Stop on a list continuation (+).
|
|
385
|
+
* @param {boolean} [options.skipFirstLine] - Skip the first line before scanning.
|
|
386
|
+
* @param {boolean} [options.preserveLastLine] - Push the terminating line back.
|
|
387
|
+
* @param {boolean} [options.readLastLine] - Include the terminating line in result.
|
|
388
|
+
* @param {boolean} [options.skipLineComments] - Skip line comments.
|
|
389
|
+
* @param {boolean} [options.skipProcessing] - Disable line preprocessing for this call.
|
|
390
|
+
* @param {string} [options.context] - Name used in unterminated-block warnings.
|
|
391
|
+
* @param {Cursor} [options.cursor] - Starting cursor for unterminated-block warnings.
|
|
392
|
+
* @param {Function|null} [filter=null] - Optional function(line) returning true to break.
|
|
393
|
+
* @returns {Promise<string[]>}
|
|
394
|
+
*/
|
|
395
|
+
async readLinesUntil(options = {}, filter = null) {
|
|
396
|
+
const result = []
|
|
397
|
+
let restoreProcessLines = false
|
|
398
|
+
if (
|
|
399
|
+
this.processLines &&
|
|
400
|
+
(options.skipProcessing || options.skip_processing)
|
|
401
|
+
) {
|
|
402
|
+
this.processLines = false
|
|
403
|
+
restoreProcessLines = true
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const terminator = options.terminator ?? null
|
|
407
|
+
let startCursor, breakOnBlankLines, breakOnListContinuation
|
|
408
|
+
if (terminator) {
|
|
409
|
+
startCursor = options.cursor || this.cursor
|
|
410
|
+
breakOnBlankLines = false
|
|
411
|
+
breakOnListContinuation = false
|
|
412
|
+
} else {
|
|
413
|
+
breakOnBlankLines =
|
|
414
|
+
options.breakOnBlankLines || options.break_on_blank_lines || false
|
|
415
|
+
breakOnListContinuation =
|
|
416
|
+
options.breakOnListContinuation ||
|
|
417
|
+
options.break_on_list_continuation ||
|
|
418
|
+
false
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const skipComments =
|
|
422
|
+
options.skipLineComments || options.skip_line_comments || false
|
|
423
|
+
let lineRead = false
|
|
424
|
+
let lineRestored = false
|
|
425
|
+
let line
|
|
426
|
+
|
|
427
|
+
if (options.skipFirstLine || options.skip_first_line) this._shift()
|
|
428
|
+
|
|
429
|
+
while ((line = await this.readLine()) !== undefined) {
|
|
430
|
+
let shouldBreak = false
|
|
431
|
+
if (terminator) {
|
|
432
|
+
shouldBreak = line === terminator
|
|
433
|
+
} else {
|
|
434
|
+
if (breakOnBlankLines && line === '') {
|
|
435
|
+
shouldBreak = true
|
|
436
|
+
} else if (
|
|
437
|
+
breakOnListContinuation &&
|
|
438
|
+
lineRead &&
|
|
439
|
+
line === LIST_CONTINUATION
|
|
440
|
+
) {
|
|
441
|
+
options.preserveLastLine = options.preserve_last_line = true
|
|
442
|
+
shouldBreak = true
|
|
443
|
+
} else if (filter?.(line)) {
|
|
444
|
+
shouldBreak = true
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (shouldBreak) {
|
|
449
|
+
if (options.readLastLine || options.read_last_line) result.push(line)
|
|
450
|
+
if (options.preserveLastLine || options.preserve_last_line) {
|
|
451
|
+
this._unshift(line)
|
|
452
|
+
lineRestored = true
|
|
453
|
+
}
|
|
454
|
+
break
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!(skipComments && line.startsWith('//') && !line.startsWith('///'))) {
|
|
458
|
+
result.push(line)
|
|
459
|
+
lineRead = true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (restoreProcessLines) {
|
|
464
|
+
this.processLines = true
|
|
465
|
+
if (lineRestored && !terminator) this._lookAhead--
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (terminator && terminator !== line) {
|
|
469
|
+
const context = 'context' in options ? options.context : terminator
|
|
470
|
+
if (context) {
|
|
471
|
+
const sc = startCursor === 'at_mark' ? this.cursorAtMark() : startCursor
|
|
472
|
+
this._logWarn(`unterminated ${context} block`, { sourceLocation: sc })
|
|
473
|
+
this.unterminated = true
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return result
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Cursor helpers ──────────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
get cursor() {
|
|
483
|
+
return new Cursor(this.file, this._dir, this.path, this.lineno)
|
|
484
|
+
}
|
|
485
|
+
cursorAtLine(lineno) {
|
|
486
|
+
return new Cursor(this.file, this._dir, this.path, lineno)
|
|
487
|
+
}
|
|
488
|
+
cursorAtMark() {
|
|
489
|
+
return this._mark ? new Cursor(...this._mark) : this.cursor
|
|
490
|
+
}
|
|
491
|
+
cursorBeforeMark() {
|
|
492
|
+
if (this._mark) {
|
|
493
|
+
const [mFile, mDir, mPath, mLineno] = this._mark
|
|
494
|
+
return new Cursor(mFile, mDir, mPath, mLineno - 1)
|
|
495
|
+
}
|
|
496
|
+
return new Cursor(this.file, this._dir, this.path, this.lineno - 1)
|
|
497
|
+
}
|
|
498
|
+
cursorAtPrevLine() {
|
|
499
|
+
return new Cursor(this.file, this._dir, this.path, this.lineno - 1)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
mark() {
|
|
503
|
+
this._mark = [this.file, this._dir, this.path, this.lineno]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
lineInfo() {
|
|
507
|
+
return `${this.path}: line ${this.lineno}`
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Returns the remaining lines in forward order (first remaining line at index 0).
|
|
512
|
+
* The returned object is a mutable proxy so that element assignments like
|
|
513
|
+
* `reader.lines[i] = newValue` are reflected back into the internal reversed stack.
|
|
514
|
+
* @returns {string[]}
|
|
515
|
+
*/
|
|
516
|
+
get lines() {
|
|
517
|
+
const _l = this._lines
|
|
518
|
+
const fwd = _l.slice().reverse()
|
|
519
|
+
return new Proxy(fwd, {
|
|
520
|
+
set(target, prop, value) {
|
|
521
|
+
target[prop] = value
|
|
522
|
+
const idx = parseInt(prop, 10)
|
|
523
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < _l.length) {
|
|
524
|
+
_l[_l.length - 1 - idx] = value
|
|
525
|
+
}
|
|
526
|
+
return true
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
string() {
|
|
532
|
+
return this._lines.slice().reverse().join(LF)
|
|
533
|
+
}
|
|
534
|
+
source() {
|
|
535
|
+
return this.sourceLines.join(LF)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ── Save / restore ──────────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
save() {
|
|
541
|
+
this._saved = {
|
|
542
|
+
file: this.file,
|
|
543
|
+
dir: this._dir,
|
|
544
|
+
path: this.path,
|
|
545
|
+
lineno: this.lineno,
|
|
546
|
+
lines: [...this._lines],
|
|
547
|
+
mark: this._mark,
|
|
548
|
+
lookAhead: this._lookAhead,
|
|
549
|
+
processLines: this.processLines,
|
|
550
|
+
unescapeNextLine: this._unescapeNextLine,
|
|
551
|
+
unterminated: this.unterminated,
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
restoreSave() {
|
|
556
|
+
if (!this._saved) return
|
|
557
|
+
const s = this._saved
|
|
558
|
+
this.file = s.file
|
|
559
|
+
this._dir = s.dir
|
|
560
|
+
this.path = s.path
|
|
561
|
+
this.lineno = s.lineno
|
|
562
|
+
this._lines = s.lines
|
|
563
|
+
this._mark = s.mark
|
|
564
|
+
this._lookAhead = s.lookAhead
|
|
565
|
+
this.processLines = s.processLines
|
|
566
|
+
this._unescapeNextLine = s.unescapeNextLine
|
|
567
|
+
this.unterminated = s.unterminated
|
|
568
|
+
this._saved = null
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
discardSave() {
|
|
572
|
+
this._saved = null
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
toString() {
|
|
576
|
+
return `#<Reader {path: ${JSON.stringify(this.path)}, line: ${this.lineno}}>`
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Internal (inheritable) ──────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Shift the top line off the stack and increment lineno.
|
|
583
|
+
* Subclasses may override to post-process consumed lines (see PreprocessorReader).
|
|
584
|
+
* @returns {string|undefined}
|
|
585
|
+
* @internal
|
|
586
|
+
*/
|
|
587
|
+
_shift() {
|
|
588
|
+
this.lineno++
|
|
589
|
+
if (this._lookAhead > 0) this._lookAhead--
|
|
590
|
+
return this._lines.pop()
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Push a line onto the stack and decrement lineno.
|
|
595
|
+
* @param {string} line
|
|
596
|
+
* @internal
|
|
597
|
+
*/
|
|
598
|
+
_unshift(line) {
|
|
599
|
+
this.lineno--
|
|
600
|
+
this._lookAhead++
|
|
601
|
+
this._lines.push(line)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Restore multiple lines onto the stack.
|
|
606
|
+
* @param {string[]} linesToRestore
|
|
607
|
+
* @internal
|
|
608
|
+
*/
|
|
609
|
+
_unshiftAll(linesToRestore) {
|
|
610
|
+
this.lineno -= linesToRestore.length
|
|
611
|
+
this._lookAhead += linesToRestore.length
|
|
612
|
+
this._lines.push(...linesToRestore.slice().reverse())
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Process a line on first visit. Returns the line unmodified by default;
|
|
617
|
+
* subclasses override to evaluate preprocessor directives.
|
|
618
|
+
* @param {string} line
|
|
619
|
+
* @returns {string}
|
|
620
|
+
* @internal
|
|
621
|
+
*/
|
|
622
|
+
processLine(line) {
|
|
623
|
+
if (this.processLines) this._lookAhead++
|
|
624
|
+
return line
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Prepare the source data into a String Array.
|
|
629
|
+
* Subclasses override to add front-matter / indentation handling.
|
|
630
|
+
* @param {string|string[]|null} data
|
|
631
|
+
* @param {Object} [opts={}]
|
|
632
|
+
* @returns {string[]}
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
_prepareLines(data, opts = {}) {
|
|
636
|
+
const normalize = opts.normalize
|
|
637
|
+
if (normalize) {
|
|
638
|
+
const trimEnd = normalize !== 'chomp'
|
|
639
|
+
return Array.isArray(data)
|
|
640
|
+
? prepareSourceArray(data, trimEnd)
|
|
641
|
+
: prepareSourceString(data != null ? String(data) : '', trimEnd)
|
|
642
|
+
}
|
|
643
|
+
if (Array.isArray(data)) return [...data]
|
|
644
|
+
if (data != null) {
|
|
645
|
+
let s = String(data)
|
|
646
|
+
if (s.includes('\r')) s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
647
|
+
return s.replace(/\n$/, '').split('\n')
|
|
648
|
+
}
|
|
649
|
+
return []
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Public API (mirrors Ruby Asciidoctor::Reader) ───────────────────────────
|
|
653
|
+
|
|
654
|
+
getCursor() {
|
|
655
|
+
return this.cursor
|
|
656
|
+
}
|
|
657
|
+
getLines() {
|
|
658
|
+
return this.sourceLines
|
|
659
|
+
}
|
|
660
|
+
getString() {
|
|
661
|
+
return this.source()
|
|
662
|
+
}
|
|
663
|
+
getLogger() {
|
|
664
|
+
return LoggerManager.logger
|
|
665
|
+
}
|
|
666
|
+
createLogMessage(text, context = {}) {
|
|
667
|
+
return Logger.AutoFormattingMessage.attach({ text, ...context })
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── Logging helpers ─────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
get logger() {
|
|
673
|
+
return this._document?.logger ?? console
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** @param {string} msg @param {{ sourceLocation?: any, includeLocation?: any }} [opts] */
|
|
677
|
+
_logWarn(msg, { sourceLocation, includeLocation } = {}) {
|
|
678
|
+
let text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg
|
|
679
|
+
if (includeLocation) text += ` (${includeLocation.lineInfo})`
|
|
680
|
+
this.logger.warn(text)
|
|
681
|
+
}
|
|
682
|
+
_logError(msg, opts = {}) {
|
|
683
|
+
let text = opts.sourceLocation
|
|
684
|
+
? `${opts.sourceLocation.lineInfo}: ${msg}`
|
|
685
|
+
: msg
|
|
686
|
+
if (opts.includeLocation) text += ` (${opts.includeLocation.lineInfo})`
|
|
687
|
+
this.logger.error(text)
|
|
688
|
+
}
|
|
689
|
+
/** @param {string} msg @param {{ sourceLocation?: any }} [opts] */
|
|
690
|
+
_logInfo(msg, { sourceLocation } = {}) {
|
|
691
|
+
const text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg
|
|
692
|
+
this.logger.info(text)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── PreprocessorReader ────────────────────────────────────────────────────────
|
|
697
|
+
|
|
698
|
+
export class PreprocessorReader extends Reader {
|
|
699
|
+
constructor(document, data = null, cursor = null, opts = {}) {
|
|
700
|
+
if (
|
|
701
|
+
'skip-front-matter' in document.attributes &&
|
|
702
|
+
!('skipFrontMatter' in opts)
|
|
703
|
+
) {
|
|
704
|
+
opts = { ...opts, skipFrontMatter: true }
|
|
705
|
+
}
|
|
706
|
+
// Pass document in opts so that _prepareLines (called from super) can access it.
|
|
707
|
+
if (!opts.document) opts = { ...opts, document }
|
|
708
|
+
super(data, cursor, opts)
|
|
709
|
+
this._document = document
|
|
710
|
+
this._sourcemap = document.sourcemap
|
|
711
|
+
const defaultDepth = parseInt(
|
|
712
|
+
document.attributes['max-include-depth'] ?? 64,
|
|
713
|
+
10
|
|
714
|
+
)
|
|
715
|
+
this._maxdepth =
|
|
716
|
+
defaultDepth > 0
|
|
717
|
+
? { abs: defaultDepth, curr: defaultDepth, rel: defaultDepth }
|
|
718
|
+
: null
|
|
719
|
+
this.includeStack = []
|
|
720
|
+
this._includes = document.catalog.includes
|
|
721
|
+
this._skipping = false
|
|
722
|
+
this._conditionalStack = []
|
|
723
|
+
this._includeProcessorExtensions = null
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
get logger() {
|
|
727
|
+
return this._document?.logger ?? console
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Drain conditional stack at EOS; treat blank lines as lines (not as EOF).
|
|
732
|
+
* `peekLine()` returns undefined only at true EOF; '' for blank lines.
|
|
733
|
+
* @returns {Promise<boolean>}
|
|
734
|
+
*/
|
|
735
|
+
async hasMoreLines() {
|
|
736
|
+
return (await this.peekLine()) !== undefined
|
|
737
|
+
}
|
|
738
|
+
async empty() {
|
|
739
|
+
return (await this.peekLine()) === undefined
|
|
740
|
+
}
|
|
741
|
+
async eof() {
|
|
742
|
+
return await this.empty()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async peekLine(direct = false) {
|
|
746
|
+
const line = await super.peekLine(direct)
|
|
747
|
+
if (line !== undefined) return line
|
|
748
|
+
if (this.includeStack.length === 0) {
|
|
749
|
+
let endCursor = null
|
|
750
|
+
this._conditionalStack = this._conditionalStack.filter((cond) => {
|
|
751
|
+
const loc =
|
|
752
|
+
cond.sourceLocation || (endCursor ??= this.cursorAtPrevLine())
|
|
753
|
+
this._logError(
|
|
754
|
+
`detected unterminated preprocessor conditional directive: ${cond.name}::${cond.target || ''}[${cond.expr || ''}]`,
|
|
755
|
+
{ sourceLocation: loc }
|
|
756
|
+
)
|
|
757
|
+
return false
|
|
758
|
+
})
|
|
759
|
+
return undefined
|
|
760
|
+
}
|
|
761
|
+
this._popInclude()
|
|
762
|
+
return await this.peekLine(direct)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Strip leading backslash from escaped directives.
|
|
767
|
+
* @returns {string|undefined}
|
|
768
|
+
*/
|
|
769
|
+
_shift() {
|
|
770
|
+
if (this._unescapeNextLine) {
|
|
771
|
+
this._unescapeNextLine = false
|
|
772
|
+
const line = super._shift()
|
|
773
|
+
return line.slice(1)
|
|
774
|
+
}
|
|
775
|
+
return super._shift()
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Push new source onto the reader, switching the include context.
|
|
780
|
+
* @param {string|string[]} data
|
|
781
|
+
* @param {string|null} [file=null]
|
|
782
|
+
* @param {string|null} [path=null]
|
|
783
|
+
* @param {number} [lineno=1]
|
|
784
|
+
* @param {Object} [attributes={}]
|
|
785
|
+
* @returns {this}
|
|
786
|
+
*/
|
|
787
|
+
pushInclude(data, file = null, path = null, lineno = 1, attributes = {}) {
|
|
788
|
+
this.includeStack.push([
|
|
789
|
+
this._lines,
|
|
790
|
+
this.file,
|
|
791
|
+
this._dir,
|
|
792
|
+
this.path,
|
|
793
|
+
this.lineno,
|
|
794
|
+
this._maxdepth,
|
|
795
|
+
this.processLines,
|
|
796
|
+
])
|
|
797
|
+
|
|
798
|
+
if ((this.file = file)) {
|
|
799
|
+
this._dir = fsdirname(String(file))
|
|
800
|
+
this.path = path || fsbasename(String(file))
|
|
801
|
+
const fileStr = String(file)
|
|
802
|
+
if (
|
|
803
|
+
(this.processLines = Object.keys(ASCIIDOC_EXTENSIONS).some((ext) =>
|
|
804
|
+
fileStr.endsWith(ext)
|
|
805
|
+
))
|
|
806
|
+
) {
|
|
807
|
+
const key = this.path.slice(0, this.path.lastIndexOf('.'))
|
|
808
|
+
this._includes[key] ??= 'partial-option' in attributes ? null : true
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
this._dir = '.'
|
|
812
|
+
this.processLines = true
|
|
813
|
+
if ((this.path = path)) {
|
|
814
|
+
this._includes[rootname(this.path)] ??=
|
|
815
|
+
'partial-option' in attributes ? null : true
|
|
816
|
+
} else {
|
|
817
|
+
this.path = '<stdin>'
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
this.lineno = lineno
|
|
822
|
+
|
|
823
|
+
if (this._maxdepth && 'depth' in attributes) {
|
|
824
|
+
const relMaxdepth = parseInt(attributes.depth, 10)
|
|
825
|
+
if (relMaxdepth > 0) {
|
|
826
|
+
const absMaxdepth = this._maxdepth.abs
|
|
827
|
+
let currMaxdepth = this.includeStack.length + relMaxdepth
|
|
828
|
+
let effRel = relMaxdepth
|
|
829
|
+
if (currMaxdepth > absMaxdepth) currMaxdepth = effRel = absMaxdepth
|
|
830
|
+
this._maxdepth = { abs: absMaxdepth, curr: currMaxdepth, rel: effRel }
|
|
831
|
+
} else {
|
|
832
|
+
this._maxdepth = {
|
|
833
|
+
abs: this._maxdepth.abs,
|
|
834
|
+
curr: this.includeStack.length,
|
|
835
|
+
rel: 0,
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
this._lines = this._prepareLines(data, {
|
|
841
|
+
include: true,
|
|
842
|
+
normalize: this.processLines || 'chomp',
|
|
843
|
+
indent: attributes.indent,
|
|
844
|
+
skipFrontMatter: 'skip-front-matter-option' in attributes,
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
if (this._lines.length === 0) {
|
|
848
|
+
this._popInclude()
|
|
849
|
+
} else if ('leveloffset' in attributes) {
|
|
850
|
+
const leveloffset = this._document.getAttribute('leveloffset')
|
|
851
|
+
const resetLine = leveloffset
|
|
852
|
+
? `:leveloffset: ${leveloffset}`
|
|
853
|
+
: ':leveloffset!:'
|
|
854
|
+
const setLine = `:leveloffset: ${attributes.leveloffset}`
|
|
855
|
+
// Build stack-order array: setLine at end (read first), resetLine at start (read last)
|
|
856
|
+
this._lines = [
|
|
857
|
+
resetLine,
|
|
858
|
+
'',
|
|
859
|
+
...this._lines.slice().reverse(),
|
|
860
|
+
'',
|
|
861
|
+
setLine,
|
|
862
|
+
]
|
|
863
|
+
this.lineno -= 2
|
|
864
|
+
} else {
|
|
865
|
+
this._lines.reverse()
|
|
866
|
+
}
|
|
867
|
+
this._lookAhead = 0
|
|
868
|
+
return this
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
get includeDepth() {
|
|
872
|
+
return this.includeStack.length
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
exceedsMaxDepth() {
|
|
876
|
+
return (
|
|
877
|
+
this._maxdepth &&
|
|
878
|
+
this.includeStack.length >= this._maxdepth.curr &&
|
|
879
|
+
this._maxdepth.rel
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
exceededMaxDepth() {
|
|
883
|
+
return this.exceedsMaxDepth()
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
hasIncludeProcessors() {
|
|
887
|
+
if (this._includeProcessorExtensions === null) {
|
|
888
|
+
const exts = this._document.extensions
|
|
889
|
+
if (
|
|
890
|
+
exts &&
|
|
891
|
+
(this._includeProcessorExtensions = exts.includeProcessors?.())
|
|
892
|
+
)
|
|
893
|
+
return true
|
|
894
|
+
this._includeProcessorExtensions = false
|
|
895
|
+
}
|
|
896
|
+
return this._includeProcessorExtensions !== false
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
createIncludeCursor(file, path, lineno) {
|
|
900
|
+
return new Cursor(String(file), fsdirname(String(file)), path, lineno)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
toString() {
|
|
904
|
+
return `#<PreprocessorReader {path: ${JSON.stringify(this.path)}, line: ${this.lineno}, include depth: ${this.includeStack.length}}>`
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** Save PreprocessorReader-specific fields in addition to Reader fields. */
|
|
908
|
+
save() {
|
|
909
|
+
super.save()
|
|
910
|
+
Object.assign(this._saved, {
|
|
911
|
+
maxdepth: this._maxdepth,
|
|
912
|
+
skipping: this._skipping,
|
|
913
|
+
conditionalStack: this._conditionalStack.map((e) => ({ ...e })),
|
|
914
|
+
includeStack: [...this.includeStack],
|
|
915
|
+
})
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Also restore PreprocessorReader-specific fields. */
|
|
919
|
+
restoreSave() {
|
|
920
|
+
if (!this._saved) return
|
|
921
|
+
this._maxdepth = this._saved.maxdepth
|
|
922
|
+
this._skipping = this._saved.skipping
|
|
923
|
+
this._conditionalStack = this._saved.conditionalStack
|
|
924
|
+
this.includeStack = this._saved.includeStack
|
|
925
|
+
super.restoreSave()
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Add front-matter stripping and indentation adjustment.
|
|
930
|
+
* @param {string|string[]|null} data
|
|
931
|
+
* @param {Object} [opts={}]
|
|
932
|
+
* @returns {string[]}
|
|
933
|
+
*/
|
|
934
|
+
_prepareLines(data, opts = {}) {
|
|
935
|
+
const result = super._prepareLines(data, opts)
|
|
936
|
+
|
|
937
|
+
if (opts.skipFrontMatter) {
|
|
938
|
+
const frontMatter = this._skipFrontMatter(result)
|
|
939
|
+
if (frontMatter !== null && !opts.include) {
|
|
940
|
+
this._document.attributes['front-matter'] = frontMatter.join(LF)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (opts.include) {
|
|
945
|
+
if (opts.indent != null) {
|
|
946
|
+
const indentVal = parseInt(opts.indent, 10) || 0
|
|
947
|
+
const tabsize = parseInt(
|
|
948
|
+
this._document.getAttribute('tabsize') ?? 0,
|
|
949
|
+
10
|
|
950
|
+
)
|
|
951
|
+
_adjustIndentation(result, indentVal, tabsize)
|
|
952
|
+
}
|
|
953
|
+
} else {
|
|
954
|
+
while (result.length > 0 && result[result.length - 1] === '') result.pop()
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return result
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Evaluate preprocessor directives as lines are visited.
|
|
962
|
+
* @param {string} line
|
|
963
|
+
* @returns {Promise<string|undefined>}
|
|
964
|
+
*/
|
|
965
|
+
async processLine(line) {
|
|
966
|
+
if (!this.processLines) return line
|
|
967
|
+
|
|
968
|
+
if (line === '') {
|
|
969
|
+
if (this._skipping) {
|
|
970
|
+
super._shift()
|
|
971
|
+
return undefined
|
|
972
|
+
}
|
|
973
|
+
this._lookAhead++
|
|
974
|
+
return line
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (line.endsWith(']') && !line.startsWith('[') && line.includes('::')) {
|
|
978
|
+
if (line.includes('if')) {
|
|
979
|
+
const m = ConditionalDirectiveRx.exec(line)
|
|
980
|
+
if (m) {
|
|
981
|
+
const [, esc, name, target, delimiter, text] = m
|
|
982
|
+
if (esc === '\\') {
|
|
983
|
+
this._unescapeNextLine = true
|
|
984
|
+
this._lookAhead++
|
|
985
|
+
return line.slice(1)
|
|
986
|
+
}
|
|
987
|
+
if (
|
|
988
|
+
this._preprocessConditionalDirective(
|
|
989
|
+
name,
|
|
990
|
+
target || '',
|
|
991
|
+
delimiter || null,
|
|
992
|
+
text || null
|
|
993
|
+
)
|
|
994
|
+
) {
|
|
995
|
+
super._shift()
|
|
996
|
+
return undefined
|
|
997
|
+
}
|
|
998
|
+
this._lookAhead++
|
|
999
|
+
return line
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (this._skipping) {
|
|
1003
|
+
super._shift()
|
|
1004
|
+
return undefined
|
|
1005
|
+
}
|
|
1006
|
+
if (line.startsWith('inc') || line.startsWith('\\inc')) {
|
|
1007
|
+
const m = IncludeDirectiveRx.exec(line)
|
|
1008
|
+
if (m) {
|
|
1009
|
+
const [, esc, target, attrlist] = m
|
|
1010
|
+
if (esc === '\\') {
|
|
1011
|
+
this._unescapeNextLine = true
|
|
1012
|
+
this._lookAhead++
|
|
1013
|
+
return line.slice(1)
|
|
1014
|
+
}
|
|
1015
|
+
if (await this._preprocessIncludeDirective(target, attrlist ?? null))
|
|
1016
|
+
return undefined
|
|
1017
|
+
this._lookAhead++
|
|
1018
|
+
return line
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
this._lookAhead++
|
|
1022
|
+
return line
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (this._skipping) {
|
|
1026
|
+
super._shift()
|
|
1027
|
+
return undefined
|
|
1028
|
+
}
|
|
1029
|
+
this._lookAhead++
|
|
1030
|
+
return line
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ── Private preprocessor logic ──────────────────────────────────────────────
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Evaluate a conditional directive (ifdef/ifndef/ifeval/endif).
|
|
1037
|
+
* @param {string} name
|
|
1038
|
+
* @param {string} target
|
|
1039
|
+
* @param {string|null} delimiter
|
|
1040
|
+
* @param {string|null} text
|
|
1041
|
+
* @returns {boolean} True if the cursor should advance past this line.
|
|
1042
|
+
* @internal
|
|
1043
|
+
*/
|
|
1044
|
+
_preprocessConditionalDirective(name, target, delimiter, text) {
|
|
1045
|
+
const noTarget = target === ''
|
|
1046
|
+
if (!noTarget) target = target.toLowerCase()
|
|
1047
|
+
|
|
1048
|
+
if (name === 'endif') {
|
|
1049
|
+
if (text) {
|
|
1050
|
+
this._logError(
|
|
1051
|
+
`malformed preprocessor directive - text not permitted: endif::${target}[${text}]`,
|
|
1052
|
+
{ sourceLocation: this.cursor }
|
|
1053
|
+
)
|
|
1054
|
+
} else if (this._conditionalStack.length === 0) {
|
|
1055
|
+
this._logError(`unmatched preprocessor directive: endif::${target}[]`, {
|
|
1056
|
+
sourceLocation: this.cursor,
|
|
1057
|
+
})
|
|
1058
|
+
} else {
|
|
1059
|
+
const top = this._conditionalStack[this._conditionalStack.length - 1]
|
|
1060
|
+
if (noTarget || target === top.target) {
|
|
1061
|
+
this._conditionalStack.pop()
|
|
1062
|
+
this._skipping =
|
|
1063
|
+
this._conditionalStack.length === 0
|
|
1064
|
+
? false
|
|
1065
|
+
: this._conditionalStack[this._conditionalStack.length - 1]
|
|
1066
|
+
.skipping
|
|
1067
|
+
} else {
|
|
1068
|
+
this._logError(
|
|
1069
|
+
`mismatched preprocessor directive: endif::${target}[], expected endif::${top.target || ''}[]`,
|
|
1070
|
+
{ sourceLocation: this.cursor }
|
|
1071
|
+
)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return true
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
let skip
|
|
1078
|
+
if (this._skipping) {
|
|
1079
|
+
if (name === 'ifeval') {
|
|
1080
|
+
if (!(noTarget && text && EvalExpressionRx.test(text.trim())))
|
|
1081
|
+
return true
|
|
1082
|
+
} else if (noTarget) {
|
|
1083
|
+
return true
|
|
1084
|
+
}
|
|
1085
|
+
skip = false
|
|
1086
|
+
} else {
|
|
1087
|
+
const attrs = this._document.attributes
|
|
1088
|
+
if (name === 'ifdef') {
|
|
1089
|
+
if (noTarget) {
|
|
1090
|
+
this._logError(
|
|
1091
|
+
`malformed preprocessor directive - missing target: ifdef::[${text}]`,
|
|
1092
|
+
{ sourceLocation: this.cursor }
|
|
1093
|
+
)
|
|
1094
|
+
return true
|
|
1095
|
+
}
|
|
1096
|
+
skip =
|
|
1097
|
+
delimiter === ','
|
|
1098
|
+
? !target.split(',').some((a) => a in attrs)
|
|
1099
|
+
: delimiter === '+'
|
|
1100
|
+
? target.split('+').some((a) => !(a in attrs))
|
|
1101
|
+
: !(target in attrs)
|
|
1102
|
+
} else if (name === 'ifndef') {
|
|
1103
|
+
if (noTarget) {
|
|
1104
|
+
this._logError(
|
|
1105
|
+
`malformed preprocessor directive - missing target: ifndef::[${text}]`,
|
|
1106
|
+
{ sourceLocation: this.cursor }
|
|
1107
|
+
)
|
|
1108
|
+
return true
|
|
1109
|
+
}
|
|
1110
|
+
skip =
|
|
1111
|
+
delimiter === ','
|
|
1112
|
+
? target.split(',').some((a) => a in attrs)
|
|
1113
|
+
: delimiter === '+'
|
|
1114
|
+
? target.split('+').every((a) => a in attrs)
|
|
1115
|
+
: target in attrs
|
|
1116
|
+
} else if (name === 'ifeval') {
|
|
1117
|
+
if (!noTarget) {
|
|
1118
|
+
this._logError(
|
|
1119
|
+
`malformed preprocessor directive - target not permitted: ifeval::${target}[${text}]`,
|
|
1120
|
+
{ sourceLocation: this.cursor }
|
|
1121
|
+
)
|
|
1122
|
+
return true
|
|
1123
|
+
}
|
|
1124
|
+
const m = text && EvalExpressionRx.exec(text.trim())
|
|
1125
|
+
if (m) {
|
|
1126
|
+
try {
|
|
1127
|
+
skip = !this._evalOp(
|
|
1128
|
+
this._resolveExprVal(m[1]),
|
|
1129
|
+
m[2],
|
|
1130
|
+
this._resolveExprVal(m[3])
|
|
1131
|
+
)
|
|
1132
|
+
} catch {
|
|
1133
|
+
skip = true
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
this._logError(
|
|
1137
|
+
`malformed preprocessor directive - ${text ? 'invalid expression' : 'missing expression'}: ifeval::[${text}]`,
|
|
1138
|
+
{ sourceLocation: this.cursor }
|
|
1139
|
+
)
|
|
1140
|
+
return true
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (name === 'ifeval') {
|
|
1146
|
+
if (skip) this._skipping = true
|
|
1147
|
+
this._conditionalStack.push({
|
|
1148
|
+
name,
|
|
1149
|
+
expr: text,
|
|
1150
|
+
skip,
|
|
1151
|
+
skipping: this._skipping,
|
|
1152
|
+
sourceLocation: this._sourcemap ? this.cursor : null,
|
|
1153
|
+
})
|
|
1154
|
+
} else if (text) {
|
|
1155
|
+
if (!this._skipping && !skip) {
|
|
1156
|
+
this.replaceNextLine(text.trimEnd())
|
|
1157
|
+
// Push a dummy line to stand in for the opening conditional directive
|
|
1158
|
+
this._lines.push('')
|
|
1159
|
+
if (text.startsWith('include::')) this._lookAhead--
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
if (skip) this._skipping = true
|
|
1163
|
+
this._conditionalStack.push({
|
|
1164
|
+
name,
|
|
1165
|
+
target,
|
|
1166
|
+
skip,
|
|
1167
|
+
skipping: this._skipping,
|
|
1168
|
+
sourceLocation: this._sourcemap ? this.cursor : null,
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return true
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Evaluate a conditional include directive.
|
|
1177
|
+
* @param {string} target
|
|
1178
|
+
* @param {string|null} attrlist
|
|
1179
|
+
* @returns {Promise<boolean|undefined>} True if the line under the cursor was consumed or changed.
|
|
1180
|
+
* @internal
|
|
1181
|
+
*/
|
|
1182
|
+
async _preprocessIncludeDirective(target, attrlist) {
|
|
1183
|
+
await _requireFsp()
|
|
1184
|
+
const doc = this._document
|
|
1185
|
+
let expandedTarget = target
|
|
1186
|
+
|
|
1187
|
+
if (expandedTarget.includes(ATTR_REF_HEAD)) {
|
|
1188
|
+
const attrMissing =
|
|
1189
|
+
doc.attributes['attribute-missing'] || Compliance.attribute_missing
|
|
1190
|
+
expandedTarget = doc.subAttributes(target, {
|
|
1191
|
+
attributeMissing: attrMissing === 'warn' ? 'drop-line' : attrMissing,
|
|
1192
|
+
})
|
|
1193
|
+
if (expandedTarget === '') {
|
|
1194
|
+
const parsedAttrs = attrlist
|
|
1195
|
+
? await doc.parseAttributes(attrlist, [], { subInput: true })
|
|
1196
|
+
: {}
|
|
1197
|
+
if ('optional-option' in parsedAttrs) {
|
|
1198
|
+
this._logInfo(
|
|
1199
|
+
`optional include dropped because resolved target is blank: include::${target}[${attrlist ?? ''}]`,
|
|
1200
|
+
{ sourceLocation: this.cursor }
|
|
1201
|
+
)
|
|
1202
|
+
super._shift()
|
|
1203
|
+
return true
|
|
1204
|
+
}
|
|
1205
|
+
if (attrMissing === 'drop-line') {
|
|
1206
|
+
this._logInfo(
|
|
1207
|
+
`include dropped due to missing attribute: include::${target}[${attrlist ?? ''}]`,
|
|
1208
|
+
{ sourceLocation: this.cursor }
|
|
1209
|
+
)
|
|
1210
|
+
super._shift()
|
|
1211
|
+
return true
|
|
1212
|
+
}
|
|
1213
|
+
this._logWarn(
|
|
1214
|
+
`include dropped because resolved target is blank: include::${target}[${attrlist ?? ''}]`,
|
|
1215
|
+
{ sourceLocation: this.cursor }
|
|
1216
|
+
)
|
|
1217
|
+
return this.replaceNextLine(
|
|
1218
|
+
`Unresolved directive in ${this.path} - include::${target}[${attrlist ?? ''}]`
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (this.hasIncludeProcessors()) {
|
|
1224
|
+
const ext = this._includeProcessorExtensions.find((c) =>
|
|
1225
|
+
c.instance.handles(doc, expandedTarget)
|
|
1226
|
+
)
|
|
1227
|
+
if (ext) {
|
|
1228
|
+
super._shift()
|
|
1229
|
+
const pa = attrlist
|
|
1230
|
+
? await doc.parseAttributes(attrlist, [], { subInput: true })
|
|
1231
|
+
: {}
|
|
1232
|
+
await ext.processMethod(doc, this, expandedTarget, pa)
|
|
1233
|
+
return true
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (doc.safe >= SafeMode.SECURE) {
|
|
1238
|
+
const lt = expandedTarget.includes(' ')
|
|
1239
|
+
? `pass:c[${expandedTarget}]`
|
|
1240
|
+
: expandedTarget
|
|
1241
|
+
const la = doc.hasAttribute('compat-mode')
|
|
1242
|
+
? (attrlist ?? '')
|
|
1243
|
+
: `role=include${attrlist ? `,${attrlist}` : ''}`
|
|
1244
|
+
return this.replaceNextLine(`link:${lt}[${la}]`)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (!this._maxdepth) return undefined
|
|
1248
|
+
|
|
1249
|
+
if (this.includeStack.length >= this._maxdepth.curr) {
|
|
1250
|
+
this._logError(
|
|
1251
|
+
`maximum include depth of ${this._maxdepth.rel} exceeded`,
|
|
1252
|
+
{ sourceLocation: this.cursor }
|
|
1253
|
+
)
|
|
1254
|
+
return undefined
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const parsedAttrs = attrlist
|
|
1258
|
+
? await doc.parseAttributes(attrlist, [], { subInput: true })
|
|
1259
|
+
: {}
|
|
1260
|
+
const resolution = await this._resolveIncludePath(
|
|
1261
|
+
expandedTarget,
|
|
1262
|
+
attrlist,
|
|
1263
|
+
parsedAttrs
|
|
1264
|
+
)
|
|
1265
|
+
if (!Array.isArray(resolution)) return resolution
|
|
1266
|
+
const [incPath, targetType, relpath] = resolution
|
|
1267
|
+
|
|
1268
|
+
let incLinenos = null
|
|
1269
|
+
let incTags = null
|
|
1270
|
+
if (attrlist) {
|
|
1271
|
+
if ('lines' in parsedAttrs && parsedAttrs.lines !== '') {
|
|
1272
|
+
incLinenos = []
|
|
1273
|
+
for (const ld of this._splitDelimitedValue(parsedAttrs.lines)) {
|
|
1274
|
+
if (ld.includes('..')) {
|
|
1275
|
+
const sep = ld.indexOf('..')
|
|
1276
|
+
const from = parseInt(ld.slice(0, sep), 10)
|
|
1277
|
+
const toStr = ld.slice(sep + 2)
|
|
1278
|
+
if (toStr === '' || parseInt(toStr, 10) < 0) {
|
|
1279
|
+
incLinenos.push(from, Infinity)
|
|
1280
|
+
} else {
|
|
1281
|
+
const to = parseInt(toStr, 10)
|
|
1282
|
+
for (let i = from; i <= to; i++) incLinenos.push(i)
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
incLinenos.push(parseInt(ld, 10))
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
incLinenos =
|
|
1289
|
+
incLinenos.length > 0
|
|
1290
|
+
? [...new Set(incLinenos)].sort((a, b) => a - b)
|
|
1291
|
+
: null
|
|
1292
|
+
} else if ('tag' in parsedAttrs) {
|
|
1293
|
+
const tag = parsedAttrs.tag
|
|
1294
|
+
if (tag && tag !== '!')
|
|
1295
|
+
incTags = tag.startsWith('!')
|
|
1296
|
+
? { [tag.slice(1)]: false }
|
|
1297
|
+
: { [tag]: true }
|
|
1298
|
+
} else if ('tags' in parsedAttrs) {
|
|
1299
|
+
incTags = {}
|
|
1300
|
+
for (const td of this._splitDelimitedValue(parsedAttrs.tags)) {
|
|
1301
|
+
if (td && td !== '!') {
|
|
1302
|
+
incTags[td.startsWith('!') ? td.slice(1) : td] = !td.startsWith('!')
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (Object.keys(incTags).length === 0) incTags = null
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (targetType === 'uri') {
|
|
1310
|
+
let uriContent
|
|
1311
|
+
try {
|
|
1312
|
+
const response = await fetch(incPath)
|
|
1313
|
+
if (!response.ok)
|
|
1314
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`)
|
|
1315
|
+
uriContent = await response.text()
|
|
1316
|
+
super._shift()
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
if ('optional-option' in parsedAttrs) {
|
|
1319
|
+
this._logInfo(
|
|
1320
|
+
`optional include dropped because include URI not readable: ${incPath}`,
|
|
1321
|
+
{ sourceLocation: this.cursor }
|
|
1322
|
+
)
|
|
1323
|
+
super._shift()
|
|
1324
|
+
return true
|
|
1325
|
+
}
|
|
1326
|
+
this._logError(
|
|
1327
|
+
`include URI not readable: ${incPath} (${err.message})`,
|
|
1328
|
+
{ sourceLocation: this.cursor }
|
|
1329
|
+
)
|
|
1330
|
+
return this.replaceNextLine(
|
|
1331
|
+
`Unresolved directive in ${this.path} - include::${expandedTarget}[${attrlist ?? ''}]`
|
|
1332
|
+
)
|
|
1333
|
+
}
|
|
1334
|
+
if (incLinenos) {
|
|
1335
|
+
const { incLines, incOffset } = this._filterLinesByLinenos(
|
|
1336
|
+
uriContent.split('\n'),
|
|
1337
|
+
incLinenos
|
|
1338
|
+
)
|
|
1339
|
+
if (incOffset !== null) {
|
|
1340
|
+
parsedAttrs['partial-option'] = ''
|
|
1341
|
+
this.pushInclude(incLines, incPath, relpath, incOffset, parsedAttrs)
|
|
1342
|
+
}
|
|
1343
|
+
} else if (incTags) {
|
|
1344
|
+
const { incLines, incOffset } = this._filterLinesByTags(
|
|
1345
|
+
uriContent.split('\n'),
|
|
1346
|
+
incPath,
|
|
1347
|
+
expandedTarget,
|
|
1348
|
+
targetType,
|
|
1349
|
+
incTags,
|
|
1350
|
+
parsedAttrs
|
|
1351
|
+
)
|
|
1352
|
+
if (incOffset !== null)
|
|
1353
|
+
this.pushInclude(incLines, incPath, relpath, incOffset, parsedAttrs)
|
|
1354
|
+
} else {
|
|
1355
|
+
this.pushInclude(uriContent, incPath, relpath, 1, parsedAttrs)
|
|
1356
|
+
}
|
|
1357
|
+
return true
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
try {
|
|
1361
|
+
if (incLinenos) {
|
|
1362
|
+
const fileLines = (await _fsp.readFile(incPath, 'utf8')).split('\n')
|
|
1363
|
+
super._shift()
|
|
1364
|
+
const { incLines, incOffset } = this._filterLinesByLinenos(
|
|
1365
|
+
fileLines,
|
|
1366
|
+
incLinenos
|
|
1367
|
+
)
|
|
1368
|
+
if (incOffset !== null) {
|
|
1369
|
+
parsedAttrs['partial-option'] = ''
|
|
1370
|
+
this.pushInclude(incLines, incPath, relpath, incOffset, parsedAttrs)
|
|
1371
|
+
}
|
|
1372
|
+
} else if (incTags) {
|
|
1373
|
+
const fileLines = (await _fsp.readFile(incPath, 'utf8')).split('\n')
|
|
1374
|
+
super._shift()
|
|
1375
|
+
const { incLines, incOffset } = this._filterLinesByTags(
|
|
1376
|
+
fileLines,
|
|
1377
|
+
incPath,
|
|
1378
|
+
expandedTarget,
|
|
1379
|
+
targetType,
|
|
1380
|
+
incTags,
|
|
1381
|
+
parsedAttrs
|
|
1382
|
+
)
|
|
1383
|
+
if (incOffset !== null)
|
|
1384
|
+
this.pushInclude(incLines, incPath, relpath, incOffset, parsedAttrs)
|
|
1385
|
+
} else {
|
|
1386
|
+
let incContent
|
|
1387
|
+
try {
|
|
1388
|
+
incContent = await _fsp.readFile(incPath, 'utf8')
|
|
1389
|
+
super._shift()
|
|
1390
|
+
} catch {
|
|
1391
|
+
this._logError(`include ${targetType} not readable: ${incPath}`, {
|
|
1392
|
+
sourceLocation: this.cursor,
|
|
1393
|
+
})
|
|
1394
|
+
return this.replaceNextLine(
|
|
1395
|
+
`Unresolved directive in ${this.path} - include::${expandedTarget}[${attrlist ?? ''}]`
|
|
1396
|
+
)
|
|
1397
|
+
}
|
|
1398
|
+
this.pushInclude(incContent, incPath, relpath, 1, parsedAttrs)
|
|
1399
|
+
}
|
|
1400
|
+
} catch {
|
|
1401
|
+
this._logError(`include ${targetType} not readable: ${incPath}`, {
|
|
1402
|
+
sourceLocation: this.cursor,
|
|
1403
|
+
})
|
|
1404
|
+
return this.replaceNextLine(
|
|
1405
|
+
`Unresolved directive in ${this.path} - include::${expandedTarget}[${attrlist ?? ''}]`
|
|
1406
|
+
)
|
|
1407
|
+
}
|
|
1408
|
+
return true
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Check whether the current context requires browser-mode include resolution.
|
|
1413
|
+
* Browser mode applies when there is no Node.js fs (true browser environment) or when
|
|
1414
|
+
* the document base_dir is a URI (file:// or http(s)://), even in Node.js.
|
|
1415
|
+
* @returns {boolean}
|
|
1416
|
+
* @internal
|
|
1417
|
+
*/
|
|
1418
|
+
_isBrowserMode() {
|
|
1419
|
+
if (!_fsp) return true
|
|
1420
|
+
const baseDir = this._document.baseDir
|
|
1421
|
+
return (
|
|
1422
|
+
!!baseDir &&
|
|
1423
|
+
baseDir !== '.' &&
|
|
1424
|
+
(baseDir.startsWith('file://') || isUriish(baseDir))
|
|
1425
|
+
)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Resolve the include target to [incPath, targetType, relpath] or a Boolean.
|
|
1430
|
+
* @param {string} target
|
|
1431
|
+
* @param {string|null} attrlist
|
|
1432
|
+
* @param {Object} attributes
|
|
1433
|
+
* @returns {Promise<[string, string, string]|boolean|undefined>}
|
|
1434
|
+
* @internal
|
|
1435
|
+
*/
|
|
1436
|
+
async _resolveIncludePath(target, attrlist, attributes) {
|
|
1437
|
+
const doc = this._document
|
|
1438
|
+
|
|
1439
|
+
// Delegate to browser-specific resolution when in a URI-based or browserless environment.
|
|
1440
|
+
// This handles file://, http(s)://, and relative targets resolved against a URI base_dir.
|
|
1441
|
+
// See src/browser/reader.js for the full specification.
|
|
1442
|
+
if (this._isBrowserMode()) {
|
|
1443
|
+
const resolution = resolveBrowserIncludePath(this, target, attrlist)
|
|
1444
|
+
if (!Array.isArray(resolution)) return resolution
|
|
1445
|
+
const [incPath, relpath] = resolution
|
|
1446
|
+
return [incPath, 'uri', relpath]
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (isUriish(target) || typeof this._dir !== 'string') {
|
|
1450
|
+
if (!doc.getAttribute('allow-uri-read')) {
|
|
1451
|
+
this._logWarn(
|
|
1452
|
+
`cannot include contents of URI: ${target} (allow-uri-read attribute not enabled)`,
|
|
1453
|
+
{ sourceLocation: this.cursor }
|
|
1454
|
+
)
|
|
1455
|
+
const lt = target.includes(' ') ? `pass:c[${target}]` : target
|
|
1456
|
+
const la = doc.hasAttribute('compat-mode')
|
|
1457
|
+
? (attrlist ?? '')
|
|
1458
|
+
: `role=include${attrlist ? `,${attrlist}` : ''}`
|
|
1459
|
+
return this.replaceNextLine(`link:${lt}[${la}]`)
|
|
1460
|
+
}
|
|
1461
|
+
return [target, 'uri', target]
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const incPath = doc.normalizeSystemPath(target, this._dir, null, {
|
|
1465
|
+
targetName: 'include file',
|
|
1466
|
+
})
|
|
1467
|
+
if (!(await fileExists(incPath))) {
|
|
1468
|
+
if ('optional-option' in attributes) {
|
|
1469
|
+
this._logInfo(
|
|
1470
|
+
`optional include dropped because include file not found: ${incPath}`,
|
|
1471
|
+
{ sourceLocation: this.cursor }
|
|
1472
|
+
)
|
|
1473
|
+
super._shift()
|
|
1474
|
+
return true
|
|
1475
|
+
}
|
|
1476
|
+
this._logError(`include file not found: ${incPath}`, {
|
|
1477
|
+
sourceLocation: this.cursor,
|
|
1478
|
+
})
|
|
1479
|
+
return this.replaceNextLine(
|
|
1480
|
+
`Unresolved directive in ${this.path} - include::${target}[${attrlist ?? ''}]`
|
|
1481
|
+
)
|
|
1482
|
+
}
|
|
1483
|
+
const relpath = doc.pathResolver.relativePath(incPath, doc.baseDir)
|
|
1484
|
+
return [incPath, 'file', relpath]
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Pop the top include context and restore state.
|
|
1489
|
+
* @internal
|
|
1490
|
+
*/
|
|
1491
|
+
_popInclude() {
|
|
1492
|
+
if (this.includeStack.length === 0) return
|
|
1493
|
+
;[
|
|
1494
|
+
this._lines,
|
|
1495
|
+
this.file,
|
|
1496
|
+
this._dir,
|
|
1497
|
+
this.path,
|
|
1498
|
+
this.lineno,
|
|
1499
|
+
this._maxdepth,
|
|
1500
|
+
this.processLines,
|
|
1501
|
+
] = this.includeStack.pop()
|
|
1502
|
+
this._lookAhead = 0
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Read lines filtered by line-number ranges.
|
|
1507
|
+
* @param {string[]} fileLines
|
|
1508
|
+
* @param {number[]} incLinenos
|
|
1509
|
+
* @returns {{incLines: string[], incOffset: number|null}}
|
|
1510
|
+
* @internal
|
|
1511
|
+
*/
|
|
1512
|
+
_filterLinesByLinenos(fileLines, incLinenos) {
|
|
1513
|
+
const remaining = [...incLinenos]
|
|
1514
|
+
const incLines = []
|
|
1515
|
+
let incOffset = null
|
|
1516
|
+
let selectRemaining = false
|
|
1517
|
+
for (let idx = 0; idx < fileLines.length; idx++) {
|
|
1518
|
+
const incLineno = idx + 1
|
|
1519
|
+
const l = fileLines[idx] + (idx < fileLines.length - 1 ? '\n' : '')
|
|
1520
|
+
if (
|
|
1521
|
+
selectRemaining ||
|
|
1522
|
+
(remaining[0] === Infinity && (selectRemaining = true))
|
|
1523
|
+
) {
|
|
1524
|
+
incOffset ??= incLineno
|
|
1525
|
+
incLines.push(l)
|
|
1526
|
+
} else if (remaining[0] === incLineno) {
|
|
1527
|
+
incOffset ??= incLineno
|
|
1528
|
+
incLines.push(l)
|
|
1529
|
+
remaining.shift()
|
|
1530
|
+
if (remaining.length === 0) break
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return { incLines, incOffset }
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Filter lines by tag directives.
|
|
1538
|
+
* @param {string[]} fileLines
|
|
1539
|
+
* @param {string} incPath
|
|
1540
|
+
* @param {string} expandedTarget
|
|
1541
|
+
* @param {string} targetType
|
|
1542
|
+
* @param {Object} incTagsIn
|
|
1543
|
+
* @param {Object} parsedAttrs
|
|
1544
|
+
* @returns {{incLines: string[], incOffset: number|null}}
|
|
1545
|
+
* @internal
|
|
1546
|
+
*/
|
|
1547
|
+
_filterLinesByTags(
|
|
1548
|
+
fileLines,
|
|
1549
|
+
incPath,
|
|
1550
|
+
expandedTarget,
|
|
1551
|
+
targetType,
|
|
1552
|
+
incTagsIn,
|
|
1553
|
+
parsedAttrs
|
|
1554
|
+
) {
|
|
1555
|
+
const tags = { ...incTagsIn }
|
|
1556
|
+
let select, baseSelect, wildcard
|
|
1557
|
+
if ('**' in tags) {
|
|
1558
|
+
select = baseSelect = tags['**']
|
|
1559
|
+
delete tags['**']
|
|
1560
|
+
if ('*' in tags) {
|
|
1561
|
+
wildcard = tags['*']
|
|
1562
|
+
delete tags['*']
|
|
1563
|
+
} else if (!select && Object.values(tags)[0] === false) wildcard = true
|
|
1564
|
+
} else if ('*' in tags) {
|
|
1565
|
+
if (Object.keys(tags)[0] === '*') {
|
|
1566
|
+
select = baseSelect = !(wildcard = tags['*'])
|
|
1567
|
+
} else {
|
|
1568
|
+
select = baseSelect = false
|
|
1569
|
+
wildcard = tags['*']
|
|
1570
|
+
}
|
|
1571
|
+
delete tags['*']
|
|
1572
|
+
} else {
|
|
1573
|
+
select = baseSelect = !Object.values(tags).includes(true)
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const incLines = []
|
|
1577
|
+
let incOffset = null
|
|
1578
|
+
const tagStack = []
|
|
1579
|
+
const tagsSelected = new Set()
|
|
1580
|
+
let activeTag = null
|
|
1581
|
+
|
|
1582
|
+
for (let idx = 0; idx < fileLines.length; idx++) {
|
|
1583
|
+
const incLineno = idx + 1
|
|
1584
|
+
const l = fileLines[idx] + (idx < fileLines.length - 1 ? '\n' : '')
|
|
1585
|
+
if (l.includes('::') && l.includes('[]')) {
|
|
1586
|
+
const m = TagDirectiveRx.exec(l)
|
|
1587
|
+
if (m) {
|
|
1588
|
+
const [, isEnd, thisTag] = m
|
|
1589
|
+
if (isEnd) {
|
|
1590
|
+
if (thisTag === activeTag) {
|
|
1591
|
+
tagStack.pop()
|
|
1592
|
+
;[activeTag, select] =
|
|
1593
|
+
tagStack.length === 0
|
|
1594
|
+
? [null, baseSelect]
|
|
1595
|
+
: tagStack[tagStack.length - 1]
|
|
1596
|
+
} else if (thisTag in tags) {
|
|
1597
|
+
const ic = this.createIncludeCursor(
|
|
1598
|
+
incPath,
|
|
1599
|
+
expandedTarget,
|
|
1600
|
+
incLineno
|
|
1601
|
+
)
|
|
1602
|
+
const si = tagStack.findLastIndex(([k]) => k === thisTag)
|
|
1603
|
+
if (si >= 0) {
|
|
1604
|
+
tagStack.splice(si, 1)
|
|
1605
|
+
this._logWarn(
|
|
1606
|
+
`mismatched end tag (expected '${activeTag}' but found '${thisTag}') at line ${incLineno} of include ${targetType}: ${incPath}`,
|
|
1607
|
+
{ sourceLocation: this.cursor, includeLocation: ic }
|
|
1608
|
+
)
|
|
1609
|
+
} else {
|
|
1610
|
+
this._logWarn(
|
|
1611
|
+
`unexpected end tag '${thisTag}' at line ${incLineno} of include ${targetType}: ${incPath}`,
|
|
1612
|
+
{ sourceLocation: this.cursor, includeLocation: ic }
|
|
1613
|
+
)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
} else if (thisTag in tags) {
|
|
1617
|
+
if ((select = tags[thisTag])) tagsSelected.add(thisTag)
|
|
1618
|
+
tagStack.push([(activeTag = thisTag), select, incLineno])
|
|
1619
|
+
} else if (wildcard !== undefined) {
|
|
1620
|
+
select = activeTag && !select ? false : wildcard
|
|
1621
|
+
tagStack.push([(activeTag = thisTag), select, incLineno])
|
|
1622
|
+
}
|
|
1623
|
+
continue
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (select) {
|
|
1627
|
+
incOffset ??= incLineno
|
|
1628
|
+
incLines.push(l)
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
for (const [tagName, , tagLineno] of tagStack) {
|
|
1633
|
+
const ic = this.createIncludeCursor(incPath, expandedTarget, tagLineno)
|
|
1634
|
+
this._logWarn(
|
|
1635
|
+
`detected unclosed tag '${tagName}' starting at line ${tagLineno} of include ${targetType}: ${incPath}`,
|
|
1636
|
+
{ sourceLocation: this.cursor, includeLocation: ic }
|
|
1637
|
+
)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const missingTags = Object.entries(tags)
|
|
1641
|
+
.filter(([, v]) => v)
|
|
1642
|
+
.map(([k]) => k)
|
|
1643
|
+
.filter((k) => !tagsSelected.has(k))
|
|
1644
|
+
if (missingTags.length > 0) {
|
|
1645
|
+
this._logWarn(
|
|
1646
|
+
`tag${missingTags.length > 1 ? 's' : ''} '${missingTags.join(', ')}' not found in include ${targetType}: ${incPath}`,
|
|
1647
|
+
{ sourceLocation: this.cursor }
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (!baseSelect || wildcard === false || Object.keys(tags).length > 0) {
|
|
1652
|
+
parsedAttrs['partial-option'] = ''
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
return { incLines, incOffset }
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Strip YAML/TOML front matter from the data Array (in-place).
|
|
1660
|
+
* @param {string[]} data
|
|
1661
|
+
* @param {boolean} [incrementLinenos=true]
|
|
1662
|
+
* @returns {string[]|null} The front-matter lines, or null if no front matter was found.
|
|
1663
|
+
* @internal
|
|
1664
|
+
*/
|
|
1665
|
+
_skipFrontMatter(data, incrementLinenos = true) {
|
|
1666
|
+
const delim = data[0]
|
|
1667
|
+
if (delim !== '---' && delim !== '+++') return null
|
|
1668
|
+
const original = [...data]
|
|
1669
|
+
data.shift()
|
|
1670
|
+
const frontMatter = []
|
|
1671
|
+
if (incrementLinenos) this.lineno++
|
|
1672
|
+
let eof = false
|
|
1673
|
+
while (!(eof = data.length === 0) && data[0] !== delim) {
|
|
1674
|
+
frontMatter.push(data.shift())
|
|
1675
|
+
if (incrementLinenos) this.lineno++
|
|
1676
|
+
}
|
|
1677
|
+
if (eof) {
|
|
1678
|
+
data.length = 0
|
|
1679
|
+
data.push(...original)
|
|
1680
|
+
if (incrementLinenos) this.lineno -= original.length
|
|
1681
|
+
return null
|
|
1682
|
+
}
|
|
1683
|
+
data.shift()
|
|
1684
|
+
if (incrementLinenos) this.lineno++
|
|
1685
|
+
return frontMatter
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Resolve the value of one side of an ifeval expression.
|
|
1690
|
+
* @param {string} val
|
|
1691
|
+
* @returns {string|number|boolean|null}
|
|
1692
|
+
* @internal
|
|
1693
|
+
*/
|
|
1694
|
+
_resolveExprVal(val) {
|
|
1695
|
+
let quoted = false
|
|
1696
|
+
if (
|
|
1697
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
1698
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
1699
|
+
) {
|
|
1700
|
+
quoted = true
|
|
1701
|
+
val = val.slice(1, val.length - 1)
|
|
1702
|
+
}
|
|
1703
|
+
if (val.includes(ATTR_REF_HEAD)) {
|
|
1704
|
+
val = this._document.subAttributes(val, { attributeMissing: 'drop' })
|
|
1705
|
+
}
|
|
1706
|
+
if (quoted) return val
|
|
1707
|
+
if (val === '') return null
|
|
1708
|
+
if (val === 'true') return true
|
|
1709
|
+
if (val === 'false') return false
|
|
1710
|
+
if (val.trimEnd() === '') return ' '
|
|
1711
|
+
if (val.includes('.')) return parseFloat(val)
|
|
1712
|
+
return parseInt(val, 10)
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Evaluate a binary comparison.
|
|
1717
|
+
* @param {*} lhs
|
|
1718
|
+
* @param {string} op
|
|
1719
|
+
* @param {*} rhs
|
|
1720
|
+
* @returns {boolean}
|
|
1721
|
+
* @internal
|
|
1722
|
+
*/
|
|
1723
|
+
_evalOp(lhs, op, rhs) {
|
|
1724
|
+
// Reject comparisons that mix boolean with non-boolean (invalid in Ruby — throws TypeError).
|
|
1725
|
+
if ((typeof lhs === 'boolean') !== (typeof rhs === 'boolean'))
|
|
1726
|
+
throw new TypeError('incompatible operand types')
|
|
1727
|
+
if (op === '==') return lhs === rhs
|
|
1728
|
+
if (op === '!=') return lhs !== rhs
|
|
1729
|
+
if (op === '<') return lhs < rhs
|
|
1730
|
+
if (op === '>') return lhs > rhs
|
|
1731
|
+
if (op === '<=') return lhs <= rhs
|
|
1732
|
+
if (op === '>=') return lhs >= rhs
|
|
1733
|
+
return false
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* Split a delimited value on comma (if present), otherwise semicolon.
|
|
1738
|
+
* @param {string} val
|
|
1739
|
+
* @returns {string[]}
|
|
1740
|
+
* @internal
|
|
1741
|
+
*/
|
|
1742
|
+
_splitDelimitedValue(val) {
|
|
1743
|
+
return val.includes(',') ? val.split(',') : val.split(';')
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// ── JavaScript-style accessors ────────────────────────────────────────────────
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Get the current include depth (number of nested includes).
|
|
1750
|
+
* @returns {number}
|
|
1751
|
+
*/
|
|
1752
|
+
getIncludeDepth() {
|
|
1753
|
+
return this.includeDepth
|
|
1754
|
+
}
|
|
1755
|
+
}
|