@asciidoctor/core 3.0.4 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -10
- package/build/browser/index.js +24154 -0
- package/build/node/index.cjs +24735 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +53 -100
- package/src/abstract_block.js +849 -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 +1899 -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 +343 -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.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/document.js
ADDED
|
@@ -0,0 +1,2134 @@
|
|
|
1
|
+
/** @import { Reader } from './reader.js' */
|
|
2
|
+
|
|
3
|
+
// ESM conversion of document.rb
|
|
4
|
+
//
|
|
5
|
+
// Ruby-to-JavaScript notes:
|
|
6
|
+
// - Document extends AbstractBlock with super(self, 'document').
|
|
7
|
+
// - Ruby Struct → plain class with named properties.
|
|
8
|
+
// - Ruby `parse unless @parsed` → synchronous call since JS parse is synchronous.
|
|
9
|
+
// - Extensions / SyntaxHighlighter are optional; Extensions is statically imported,
|
|
10
|
+
// SyntaxHighlighter is loaded lazily by the converter pipeline.
|
|
11
|
+
// - File.write / process.env / Time.now have Node.js equivalents.
|
|
12
|
+
// - Mutex / thread-safety not applicable in single-threaded JS.
|
|
13
|
+
// - `instance_variable_get :@attribute_overrides` → direct property access.
|
|
14
|
+
|
|
15
|
+
import { AbstractBlock } from './abstract_block.js'
|
|
16
|
+
import { Section } from './section.js'
|
|
17
|
+
import { Inline } from './inline.js'
|
|
18
|
+
import { Callouts } from './callouts.js'
|
|
19
|
+
import { PathResolver } from './path_resolver.js'
|
|
20
|
+
import { Compliance } from './compliance.js'
|
|
21
|
+
import { extname, nextval } from './helpers.js'
|
|
22
|
+
import {
|
|
23
|
+
SafeMode,
|
|
24
|
+
DEFAULT_ATTRIBUTES,
|
|
25
|
+
DEFAULT_BACKEND,
|
|
26
|
+
DEFAULT_DOCTYPE,
|
|
27
|
+
BACKEND_ALIASES,
|
|
28
|
+
DEFAULT_PAGE_WIDTHS,
|
|
29
|
+
FLEXIBLE_ATTRIBUTES,
|
|
30
|
+
USER_HOME,
|
|
31
|
+
} from './constants.js'
|
|
32
|
+
import { Converter, CustomFactory, deriveBackendTraits } from './converter.js'
|
|
33
|
+
import { XmlSanitizeRx, AttributeEntryPassMacroRx } from './rx.js'
|
|
34
|
+
import { LF } from './constants.js'
|
|
35
|
+
import { applyLogging } from './logging.js'
|
|
36
|
+
import { SyntaxHighlighter, DefaultFactoryProxy } from './syntax_highlighter.js'
|
|
37
|
+
import { PreprocessorReader, Cursor } from './reader.js'
|
|
38
|
+
import { Parser } from './parser.js'
|
|
39
|
+
import { Extensions, Registry } from './extensions.js'
|
|
40
|
+
|
|
41
|
+
// ── Helper structs ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export class ImageReference {
|
|
44
|
+
constructor(target, imagesdir) {
|
|
45
|
+
this.target = target
|
|
46
|
+
this.imagesdir = imagesdir
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {string} the target image path or URI.
|
|
51
|
+
*/
|
|
52
|
+
getTarget() {
|
|
53
|
+
return this.target
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {string} the images directory.
|
|
58
|
+
*/
|
|
59
|
+
getImagesDirectory() {
|
|
60
|
+
return this.imagesdir
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toString() {
|
|
64
|
+
return this.target
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
import { Footnote } from './footnote.js'
|
|
69
|
+
export { Footnote }
|
|
70
|
+
|
|
71
|
+
import { AttributeEntry } from './attribute_entry.js'
|
|
72
|
+
export { AttributeEntry }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parsed and stores a partitioned title (title & subtitle).
|
|
76
|
+
*/
|
|
77
|
+
export class DocumentTitle {
|
|
78
|
+
constructor(val, opts = {}) {
|
|
79
|
+
this._sanitized = !!(opts.sanitize && val.includes('<'))
|
|
80
|
+
if (this._sanitized) {
|
|
81
|
+
val = val.replace(XmlSanitizeRx, '').replace(/ {2,}/g, ' ').trim()
|
|
82
|
+
}
|
|
83
|
+
const sep = opts.separator ?? ':'
|
|
84
|
+
const sepStr = sep ? `${sep} ` : null
|
|
85
|
+
if (!sepStr || !val.includes(sepStr)) {
|
|
86
|
+
this.main = val
|
|
87
|
+
this.subtitle = null
|
|
88
|
+
} else {
|
|
89
|
+
const idx = val.lastIndexOf(sepStr)
|
|
90
|
+
this.main = val.slice(0, idx)
|
|
91
|
+
this.subtitle = val.slice(idx + sepStr.length)
|
|
92
|
+
}
|
|
93
|
+
this.combined = val
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get title() {
|
|
97
|
+
return this.main
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isSanitized() {
|
|
101
|
+
return this._sanitized
|
|
102
|
+
}
|
|
103
|
+
hasSubtitle() {
|
|
104
|
+
return this.subtitle != null
|
|
105
|
+
}
|
|
106
|
+
getMain() {
|
|
107
|
+
return this.main
|
|
108
|
+
}
|
|
109
|
+
getCombined() {
|
|
110
|
+
return this.combined
|
|
111
|
+
}
|
|
112
|
+
getSubtitle() {
|
|
113
|
+
return this.subtitle
|
|
114
|
+
}
|
|
115
|
+
toString() {
|
|
116
|
+
return this.combined
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Represents an Author parsed from document attributes.
|
|
122
|
+
*/
|
|
123
|
+
export class Author {
|
|
124
|
+
constructor(name, firstname, middlename, lastname, initials, email) {
|
|
125
|
+
this.name = name
|
|
126
|
+
this.firstname = firstname
|
|
127
|
+
this.middlename = middlename
|
|
128
|
+
this.lastname = lastname
|
|
129
|
+
this.initials = initials
|
|
130
|
+
this.email = email
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getName() {
|
|
134
|
+
return this.name
|
|
135
|
+
}
|
|
136
|
+
getFirstName() {
|
|
137
|
+
return this.firstname
|
|
138
|
+
}
|
|
139
|
+
getMiddleName() {
|
|
140
|
+
return this.middlename
|
|
141
|
+
}
|
|
142
|
+
getLastName() {
|
|
143
|
+
return this.lastname
|
|
144
|
+
}
|
|
145
|
+
getInitials() {
|
|
146
|
+
return this.initials
|
|
147
|
+
}
|
|
148
|
+
getEmail() {
|
|
149
|
+
return this.email
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class RevisionInfo {
|
|
154
|
+
constructor(number, date, remark) {
|
|
155
|
+
this._number = number ?? null
|
|
156
|
+
this._date = date ?? null
|
|
157
|
+
this._remark = remark ?? null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
isEmpty() {
|
|
161
|
+
return !this._number && !this._date && !this._remark
|
|
162
|
+
}
|
|
163
|
+
getNumber() {
|
|
164
|
+
return this._number
|
|
165
|
+
}
|
|
166
|
+
getDate() {
|
|
167
|
+
return this._date
|
|
168
|
+
}
|
|
169
|
+
getRemark() {
|
|
170
|
+
return this._remark
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Document ──────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export class Document extends AbstractBlock {
|
|
177
|
+
/** @internal */
|
|
178
|
+
_converter
|
|
179
|
+
/** @internal */
|
|
180
|
+
_maxAttributeValueSize
|
|
181
|
+
/** @internal */
|
|
182
|
+
_docinfoProcessorExtensions
|
|
183
|
+
/** @internal */
|
|
184
|
+
_attributesModified
|
|
185
|
+
/** @internal */
|
|
186
|
+
_counters
|
|
187
|
+
/** @internal */
|
|
188
|
+
_headerAttributes
|
|
189
|
+
/** @internal */
|
|
190
|
+
_reftexts
|
|
191
|
+
/** @internal */
|
|
192
|
+
_parsed
|
|
193
|
+
/** @internal */
|
|
194
|
+
_inputMtime
|
|
195
|
+
/** @internal */
|
|
196
|
+
_parentDoctype
|
|
197
|
+
/** @internal */
|
|
198
|
+
_initializeExtensions
|
|
199
|
+
/** @internal */
|
|
200
|
+
_timings
|
|
201
|
+
/** @internal */
|
|
202
|
+
_attributeOverrides
|
|
203
|
+
/** @type {Reader} */
|
|
204
|
+
reader
|
|
205
|
+
/** @type {string} */
|
|
206
|
+
doctype
|
|
207
|
+
/** @type {string} */
|
|
208
|
+
baseDir
|
|
209
|
+
/** @type {string} */
|
|
210
|
+
backend
|
|
211
|
+
/** @type {number} */
|
|
212
|
+
safe
|
|
213
|
+
/** @type {boolean} */
|
|
214
|
+
compatMode
|
|
215
|
+
/** Override AbstractNode's getter so Document can own its converter directly. */
|
|
216
|
+
get converter() {
|
|
217
|
+
return this._converter
|
|
218
|
+
}
|
|
219
|
+
set converter(v) {
|
|
220
|
+
this._converter = v
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
constructor(data = null, options = {}) {
|
|
224
|
+
// Bootstrap: call super with a temporary placeholder — we'll fix parent ref below.
|
|
225
|
+
// AbstractBlock(parent, context, opts) — we pass `null` and patch afterward.
|
|
226
|
+
super(null, 'document', options)
|
|
227
|
+
// Document is its own parent/document (write _parent directly to avoid shadowing the accessor).
|
|
228
|
+
/** @internal */
|
|
229
|
+
this._parent = this
|
|
230
|
+
/** @internal */
|
|
231
|
+
this.document = this
|
|
232
|
+
|
|
233
|
+
const parentDoc = options.parent ?? null
|
|
234
|
+
delete options.parent
|
|
235
|
+
|
|
236
|
+
// ── Nested document setup ─────────────────────────────────────────────────
|
|
237
|
+
if (parentDoc) {
|
|
238
|
+
this.parentDocument = parentDoc
|
|
239
|
+
options.base_dir ??= parentDoc.baseDir
|
|
240
|
+
if (parentDoc.options.catalog_assets) options.catalog_assets = true
|
|
241
|
+
if (parentDoc.options.to_dir) options.to_dir = parentDoc.options.to_dir
|
|
242
|
+
|
|
243
|
+
this.catalog = { ...parentDoc.catalog, footnotes: [] }
|
|
244
|
+
|
|
245
|
+
// Clone parent's attribute overrides merged with parent attributes
|
|
246
|
+
this._attributeOverrides = {
|
|
247
|
+
...parentDoc._attributeOverrides,
|
|
248
|
+
...parentDoc.attributes,
|
|
249
|
+
}
|
|
250
|
+
const attrOverrides = this._attributeOverrides
|
|
251
|
+
delete attrOverrides['compat-mode']
|
|
252
|
+
const parentDoctype = attrOverrides.doctype
|
|
253
|
+
delete attrOverrides.doctype
|
|
254
|
+
delete attrOverrides.notitle
|
|
255
|
+
delete attrOverrides.showtitle
|
|
256
|
+
delete attrOverrides.toc
|
|
257
|
+
this.attributes['toc-placement'] =
|
|
258
|
+
attrOverrides['toc-placement'] ?? 'auto'
|
|
259
|
+
delete attrOverrides['toc-placement']
|
|
260
|
+
delete attrOverrides['toc-position']
|
|
261
|
+
|
|
262
|
+
this.safe = parentDoc.safe
|
|
263
|
+
this.compatMode = parentDoc.compatMode
|
|
264
|
+
if (this.compatMode) this.attributes['compat-mode'] = ''
|
|
265
|
+
this.outfilesuffix = parentDoc.outfilesuffix
|
|
266
|
+
this.sourcemap = parentDoc.sourcemap
|
|
267
|
+
this._timings = null
|
|
268
|
+
this.pathResolver = parentDoc.pathResolver
|
|
269
|
+
this.converter = parentDoc.converter
|
|
270
|
+
this.extensions = parentDoc.extensions
|
|
271
|
+
this.syntaxHighlighter = parentDoc.syntaxHighlighter
|
|
272
|
+
this._initializeExtensions = null
|
|
273
|
+
|
|
274
|
+
// For nested: re-use parent's @_parentDoctype
|
|
275
|
+
this._parentDoctype = parentDoctype
|
|
276
|
+
} else {
|
|
277
|
+
// ── Root document setup ───────────────────────────────────────────────
|
|
278
|
+
this.parentDocument = null
|
|
279
|
+
this.catalog = {
|
|
280
|
+
ids: {}, // deprecated
|
|
281
|
+
refs: {},
|
|
282
|
+
footnotes: [],
|
|
283
|
+
links: [],
|
|
284
|
+
images: [],
|
|
285
|
+
callouts: new Callouts(),
|
|
286
|
+
includes: {},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Process attribute overrides from options
|
|
290
|
+
this._attributeOverrides = {}
|
|
291
|
+
const attrOverrides = this._attributeOverrides
|
|
292
|
+
for (let [key, val] of Object.entries(options.attributes ?? {})) {
|
|
293
|
+
if (key.endsWith('@')) {
|
|
294
|
+
if (key.startsWith('!')) {
|
|
295
|
+
key = key.slice(1, -1)
|
|
296
|
+
val = false
|
|
297
|
+
} else if (key.endsWith('!@')) {
|
|
298
|
+
key = key.slice(0, -2)
|
|
299
|
+
val = false
|
|
300
|
+
} else {
|
|
301
|
+
key = key.slice(0, -1)
|
|
302
|
+
val = `${val}@`
|
|
303
|
+
}
|
|
304
|
+
} else if (key.startsWith('!')) {
|
|
305
|
+
key = key.slice(1)
|
|
306
|
+
val = val === '@' ? false : null
|
|
307
|
+
} else if (key.endsWith('!')) {
|
|
308
|
+
key = key.slice(0, -1)
|
|
309
|
+
val = val === '@' ? false : null
|
|
310
|
+
}
|
|
311
|
+
attrOverrides[key.toLowerCase()] = val
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof options.to_file === 'string') {
|
|
315
|
+
attrOverrides.outfilesuffix = extname(options.to_file)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Resolve safe mode
|
|
319
|
+
const safeMode = options.safe
|
|
320
|
+
if (!safeMode) {
|
|
321
|
+
this.safe = SafeMode.SECURE
|
|
322
|
+
} else if (typeof safeMode === 'number') {
|
|
323
|
+
this.safe = safeMode
|
|
324
|
+
} else {
|
|
325
|
+
this.safe = SafeMode.valueForName(safeMode) ?? SafeMode.SECURE
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this._inputMtime = options.input_mtime ?? null
|
|
329
|
+
delete options.input_mtime
|
|
330
|
+
this.compatMode = 'compat-mode' in attrOverrides
|
|
331
|
+
this.sourcemap = options.sourcemap ?? false
|
|
332
|
+
this._timings = options.timings ?? null
|
|
333
|
+
delete options.timings
|
|
334
|
+
this.pathResolver = new PathResolver()
|
|
335
|
+
if (options.extension_registry) {
|
|
336
|
+
this.extensions = options.extension_registry.activate(this)
|
|
337
|
+
} else {
|
|
338
|
+
this.extensions = null
|
|
339
|
+
// If no explicit registry but global extension groups are registered, activate them.
|
|
340
|
+
const globalGroups = Extensions.groups()
|
|
341
|
+
if (Object.keys(globalGroups).length > 0) {
|
|
342
|
+
this.extensions = new Registry()
|
|
343
|
+
this.extensions.activate(this)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
this.syntaxHighlighter = null
|
|
347
|
+
this._initializeExtensions = true // set to class if available
|
|
348
|
+
this._parentDoctype = null
|
|
349
|
+
|
|
350
|
+
// Normalize :header_footer → :standalone
|
|
351
|
+
if ('header_footer' in options && !('standalone' in options)) {
|
|
352
|
+
options.standalone = options.header_footer
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this._parsed = false
|
|
357
|
+
this._reftexts = null
|
|
358
|
+
this.header = null
|
|
359
|
+
this._headerAttributes = null
|
|
360
|
+
this._counters = {}
|
|
361
|
+
this._attributesModified = new Set()
|
|
362
|
+
this._docinfoProcessorExtensions = {}
|
|
363
|
+
const standalone = options.standalone ?? false
|
|
364
|
+
this.options = Object.freeze({ ...options })
|
|
365
|
+
|
|
366
|
+
const attrs = this.attributes
|
|
367
|
+
|
|
368
|
+
if (!parentDoc) {
|
|
369
|
+
attrs['attribute-undefined'] = Compliance.attributeUndefined
|
|
370
|
+
attrs['attribute-missing'] = Compliance.attributeMissing
|
|
371
|
+
Object.assign(attrs, DEFAULT_ATTRIBUTES)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (standalone) {
|
|
375
|
+
delete this._attributeOverrides.embedded
|
|
376
|
+
attrs.copycss = ''
|
|
377
|
+
attrs['iconfont-remote'] = ''
|
|
378
|
+
attrs.stylesheet = ''
|
|
379
|
+
attrs.webfonts = ''
|
|
380
|
+
} else {
|
|
381
|
+
this._attributeOverrides.embedded = ''
|
|
382
|
+
const ao = this._attributeOverrides
|
|
383
|
+
const showtitle = ao.showtitle
|
|
384
|
+
const notitle = ao.notitle
|
|
385
|
+
if (
|
|
386
|
+
'showtitle' in ao &&
|
|
387
|
+
['showtitle', 'notitle'].filter((k) => k in ao).pop() === 'showtitle'
|
|
388
|
+
) {
|
|
389
|
+
ao.notitle = { null: '', false: '@', '@': false }[showtitle]
|
|
390
|
+
} else if ('notitle' in ao) {
|
|
391
|
+
ao.showtitle = { null: '', false: '@', '@': false }[notitle]
|
|
392
|
+
} else {
|
|
393
|
+
attrs.notitle = ''
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const attrOverrides = this._attributeOverrides
|
|
398
|
+
attrOverrides.asciidoctor = ''
|
|
399
|
+
attrOverrides['asciidoctor-version'] = '3.0.0.dev' // matches Ruby VERSION
|
|
400
|
+
|
|
401
|
+
const safeModeName = SafeMode.nameForValue(this.safe)
|
|
402
|
+
attrOverrides['safe-mode-name'] = safeModeName
|
|
403
|
+
attrOverrides[`safe-mode-${safeModeName}`] = ''
|
|
404
|
+
attrOverrides['safe-mode-level'] = this.safe
|
|
405
|
+
attrOverrides['max-include-depth'] ??= 64
|
|
406
|
+
attrOverrides['allow-uri-read'] ??= null
|
|
407
|
+
|
|
408
|
+
// Remap legacy attributes
|
|
409
|
+
if ('numbered' in attrOverrides) {
|
|
410
|
+
const _v = attrOverrides.numbered
|
|
411
|
+
delete attrOverrides.numbered
|
|
412
|
+
attrOverrides.sectnums = _v
|
|
413
|
+
}
|
|
414
|
+
if ('hardbreaks' in attrOverrides) {
|
|
415
|
+
const _v = attrOverrides.hardbreaks
|
|
416
|
+
delete attrOverrides.hardbreaks
|
|
417
|
+
attrOverrides['hardbreaks-option'] = _v
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Resolve base_dir
|
|
421
|
+
if (options.base_dir) {
|
|
422
|
+
this.baseDir = attrOverrides.docdir = _expandPath(options.base_dir)
|
|
423
|
+
} else if (attrOverrides.docdir) {
|
|
424
|
+
this.baseDir = attrOverrides.docdir
|
|
425
|
+
} else {
|
|
426
|
+
this.baseDir = attrOverrides.docdir = _cwd()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (options.backend) attrOverrides.backend = String(options.backend)
|
|
430
|
+
if (options.doctype) attrOverrides.doctype = String(options.doctype)
|
|
431
|
+
|
|
432
|
+
if (this.safe >= SafeMode.SERVER) {
|
|
433
|
+
attrOverrides.copycss ??= null
|
|
434
|
+
attrOverrides['source-highlighter'] ??= null
|
|
435
|
+
attrOverrides.backend ??= DEFAULT_BACKEND
|
|
436
|
+
if (!parentDoc && 'docfile' in attrOverrides) {
|
|
437
|
+
const docdir = attrOverrides.docdir ?? ''
|
|
438
|
+
attrOverrides.docfile = attrOverrides.docfile.slice(docdir.length + 1)
|
|
439
|
+
}
|
|
440
|
+
attrOverrides.docdir = ''
|
|
441
|
+
attrOverrides['user-home'] ??= '.'
|
|
442
|
+
if (this.safe >= SafeMode.SECURE) {
|
|
443
|
+
if (!('max-attribute-value-size' in attrOverrides)) {
|
|
444
|
+
attrOverrides['max-attribute-value-size'] = 4096
|
|
445
|
+
}
|
|
446
|
+
attrOverrides.linkcss ??= ''
|
|
447
|
+
attrOverrides.icons ??= null
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
attrOverrides['user-home'] ??= USER_HOME
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const sizeAttr = (attrOverrides['max-attribute-value-size'] ??= null)
|
|
454
|
+
this._maxAttributeValueSize =
|
|
455
|
+
sizeAttr != null ? Math.abs(parseInt(sizeAttr, 10)) : null
|
|
456
|
+
|
|
457
|
+
// Apply attribute overrides — overrides that survive (non-soft) stay in attrOverrides.
|
|
458
|
+
const softKeys = []
|
|
459
|
+
for (const [key, val] of Object.entries(attrOverrides)) {
|
|
460
|
+
if (val != null && val !== false) {
|
|
461
|
+
let effective = val
|
|
462
|
+
let isSoft = false
|
|
463
|
+
if (typeof val === 'string' && val.endsWith('@')) {
|
|
464
|
+
effective = val.slice(0, -1)
|
|
465
|
+
isSoft = true
|
|
466
|
+
}
|
|
467
|
+
attrs[key] = effective
|
|
468
|
+
if (isSoft) softKeys.push(key)
|
|
469
|
+
} else {
|
|
470
|
+
delete attrs[key]
|
|
471
|
+
if (val === false) softKeys.push(key) // false = soft-lock delete; null = hard-lock absent (stays in overrides)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const key of softKeys) delete attrOverrides[key]
|
|
475
|
+
|
|
476
|
+
if (parentDoc) {
|
|
477
|
+
this.backend = attrs.backend
|
|
478
|
+
const parentDoctype = this._parentDoctype
|
|
479
|
+
if ((this.doctype = attrs.doctype = parentDoctype) !== DEFAULT_DOCTYPE) {
|
|
480
|
+
this._updateDoctypeAttributes(DEFAULT_DOCTYPE)
|
|
481
|
+
}
|
|
482
|
+
// Set up reader only — parsing is deferred to Document.create() / doc.parse().
|
|
483
|
+
this.reader = new PreprocessorReader(this, data, options.cursor)
|
|
484
|
+
if (this.sourcemap) this.sourceLocation = this.reader.cursor
|
|
485
|
+
} else {
|
|
486
|
+
this.backend = null
|
|
487
|
+
const initialBackend = attrs.backend || DEFAULT_BACKEND
|
|
488
|
+
if (initialBackend === 'manpage') {
|
|
489
|
+
this.doctype = attrs.doctype = attrOverrides.doctype = 'manpage'
|
|
490
|
+
} else {
|
|
491
|
+
this.doctype = attrs.doctype ??= DEFAULT_DOCTYPE
|
|
492
|
+
}
|
|
493
|
+
this._updateBackendAttributes(initialBackend, true)
|
|
494
|
+
|
|
495
|
+
attrs.stylesdir ??= '.'
|
|
496
|
+
attrs.iconsdir ??= `${attrs.imagesdir ?? './images'}/icons`
|
|
497
|
+
|
|
498
|
+
this._fillDatetimeAttributes(attrs, this._inputMtime)
|
|
499
|
+
|
|
500
|
+
// Extensions initialization deferred — handle in parse()
|
|
501
|
+
this.reader = new PreprocessorReader(
|
|
502
|
+
this,
|
|
503
|
+
data,
|
|
504
|
+
new Cursor(attrs.docfile ?? null, this.baseDir),
|
|
505
|
+
{ normalize: true }
|
|
506
|
+
)
|
|
507
|
+
if (this.sourcemap) this.sourceLocation = this.reader.cursor
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Alias catalog as references (backwards compat). */
|
|
512
|
+
get references() {
|
|
513
|
+
return this.catalog
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** @returns {boolean} True if this is a nested (child) document. */
|
|
517
|
+
nested() {
|
|
518
|
+
return !!this.parentDocument
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Factory — create and fully parse a Document asynchronously.
|
|
523
|
+
* @param {string|string[]|null} data - The AsciiDoc source.
|
|
524
|
+
* @param {Object} [options={}] - Processing options.
|
|
525
|
+
* @returns {Promise<Document>} The parsed Document.
|
|
526
|
+
*/
|
|
527
|
+
static async create(data, options = {}) {
|
|
528
|
+
const doc = new Document(data, options)
|
|
529
|
+
await doc.parse()
|
|
530
|
+
return doc
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Parse the AsciiDoc source and populate the document AST.
|
|
535
|
+
*
|
|
536
|
+
* This method is idempotent — repeated calls are no-ops once parsing is done.
|
|
537
|
+
* You rarely need to call it directly: prefer {@link Document.create} (factory) or
|
|
538
|
+
* the top-level {@link load} / {@link loadFile} functions, which call `parse()` for you.
|
|
539
|
+
*
|
|
540
|
+
* Call `parse()` explicitly only when you constructed `new Document(...)` by hand and
|
|
541
|
+
* need to defer the work, or when you want to supply a replacement `data` source.
|
|
542
|
+
*
|
|
543
|
+
* @param {string|string[]|null} [data=null] - Optional replacement source lines.
|
|
544
|
+
* When provided, replaces the data that was given to the constructor.
|
|
545
|
+
* @returns {Promise<Document>} This Document instance (allows chaining).
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* const doc = new Document('= Hello', {})
|
|
549
|
+
* await doc.parse()
|
|
550
|
+
* console.log(doc.getTitle()) // → 'Hello'
|
|
551
|
+
*/
|
|
552
|
+
async parse(data = null) {
|
|
553
|
+
if (this._parsed) return this
|
|
554
|
+
|
|
555
|
+
if (data) {
|
|
556
|
+
this.reader = new PreprocessorReader(
|
|
557
|
+
this,
|
|
558
|
+
data,
|
|
559
|
+
new Cursor(this.attributes.docfile ?? null, this.baseDir),
|
|
560
|
+
{ normalize: true }
|
|
561
|
+
)
|
|
562
|
+
if (this.sourcemap) this.sourceLocation = this.reader.cursor
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!this.parentDocument && this.extensions?.hasPreprocessors?.()) {
|
|
566
|
+
for (const ext of this.extensions.preprocessors()) {
|
|
567
|
+
this.reader = ext.processMethod(this, this.reader) ?? this.reader
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
await Parser.parse(this.reader, this, {
|
|
572
|
+
header_only: this.options.parse_header_only,
|
|
573
|
+
})
|
|
574
|
+
this.restoreAttributes()
|
|
575
|
+
|
|
576
|
+
if (!this.parentDocument && this.extensions?.hasTreeProcessors?.()) {
|
|
577
|
+
for (const ext of this.extensions.treeProcessors()) {
|
|
578
|
+
const result = ext.processMethod(this)
|
|
579
|
+
if (result instanceof Document && result !== this) {
|
|
580
|
+
return result
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Pre-compute all async text values (titles, list item text, cell text, reftexts)
|
|
586
|
+
// so that synchronous getters work correctly during conversion.
|
|
587
|
+
await this._resolveAllTexts(this)
|
|
588
|
+
// Reset the footnote counter so that body-content footnotes (processed during conversion)
|
|
589
|
+
// start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
|
|
590
|
+
// numbered during parsing via apply_title_subs, then the counter restarts for body content.
|
|
591
|
+
delete this.attributes['footnote-number']
|
|
592
|
+
delete this._counters['footnote-number']
|
|
593
|
+
// Pre-compute reftext for all registered inline anchor nodes.
|
|
594
|
+
for (const ref of Object.values(this.catalog.refs)) {
|
|
595
|
+
if (ref && typeof ref.precomputeReftext === 'function') {
|
|
596
|
+
await ref.precomputeReftext()
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Build the reftext→id lookup map so that resolveId() is synchronous.
|
|
600
|
+
await this._buildReftextsMap()
|
|
601
|
+
|
|
602
|
+
this._parsed = true
|
|
603
|
+
return this
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Return whether the document has been fully parsed.
|
|
608
|
+
* @returns {boolean}
|
|
609
|
+
*/
|
|
610
|
+
isParsed() {
|
|
611
|
+
return this._parsed
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Get the named counter and advance it by one step.
|
|
616
|
+
*
|
|
617
|
+
* Counters are document-scoped sequences used for automatic numbering (figures,
|
|
618
|
+
* tables, custom labels, …). Each call increments the sequence and returns the
|
|
619
|
+
* new value. Numeric counters increment by 1; alphabetic counters advance through
|
|
620
|
+
* the alphabet (`'a'` → `'b'` → … → `'z'`).
|
|
621
|
+
*
|
|
622
|
+
* When the counter does not yet exist:
|
|
623
|
+
* - If `seed` is a number (or a string that parses as an integer), the counter starts at `seed`.
|
|
624
|
+
* - If `seed` is a letter (`'a'`–`'z'`), the counter starts at that letter.
|
|
625
|
+
* - If `seed` is `null` (default), the counter starts at `1`.
|
|
626
|
+
*
|
|
627
|
+
* @param {string} name - Counter name (document-scoped key).
|
|
628
|
+
* @param {string|number|null} [seed=null] - Starting value for new counters.
|
|
629
|
+
* @returns {string|number} The new counter value after incrementing.
|
|
630
|
+
*
|
|
631
|
+
* @example <caption>Numeric counter (auto-starts at 1)</caption>
|
|
632
|
+
* doc.counter('figure-number') // → 1
|
|
633
|
+
* doc.counter('figure-number') // → 2
|
|
634
|
+
*
|
|
635
|
+
* @example <caption>Alphabetic counter</caption>
|
|
636
|
+
* doc.counter('appendix-number', 'A') // → 'A'
|
|
637
|
+
* doc.counter('appendix-number', 'A') // → 'B'
|
|
638
|
+
*
|
|
639
|
+
* @example <caption>Numeric counter with custom start</caption>
|
|
640
|
+
* doc.counter('example-number', 5) // → 5
|
|
641
|
+
* doc.counter('example-number', 5) // → 6
|
|
642
|
+
*/
|
|
643
|
+
counter(name, seed = null) {
|
|
644
|
+
if (this.parentDocument) return this.parentDocument.counter(name, seed)
|
|
645
|
+
const isLocked = this.isAttributeLocked(name)
|
|
646
|
+
let currVal = this._counters[name]
|
|
647
|
+
let nextVal
|
|
648
|
+
if (
|
|
649
|
+
(isLocked && currVal != null) ||
|
|
650
|
+
((currVal = this.attributes[name]) != null && currVal !== '')
|
|
651
|
+
) {
|
|
652
|
+
nextVal = this._counters[name] = nextval(currVal)
|
|
653
|
+
} else if (seed != null) {
|
|
654
|
+
nextVal = this._counters[name] =
|
|
655
|
+
String(seed) === String(parseInt(seed, 10)) ? parseInt(seed, 10) : seed
|
|
656
|
+
} else {
|
|
657
|
+
nextVal = this._counters[name] = 1
|
|
658
|
+
}
|
|
659
|
+
if (!isLocked) this.attributes[name] = nextVal
|
|
660
|
+
return nextVal
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Increment the specified counter and store it in the block's attributes.
|
|
665
|
+
* @param {string} counterName
|
|
666
|
+
* @param {Object} block
|
|
667
|
+
* @returns {string|number} The new counter value.
|
|
668
|
+
*/
|
|
669
|
+
incrementAndStoreCounter(counterName, block) {
|
|
670
|
+
return new AttributeEntry(counterName, this.counter(counterName)).saveTo(
|
|
671
|
+
block.attributes
|
|
672
|
+
).value
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** @deprecated Use incrementAndStoreCounter instead. */
|
|
676
|
+
counterIncrement(counterName, block) {
|
|
677
|
+
return this.incrementAndStoreCounter(counterName, block)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Register a reference in the document catalog.
|
|
682
|
+
* @param {string} type - Catalog type ('ids', 'refs', 'footnotes', 'links', 'images', 'callouts').
|
|
683
|
+
* @param {*} value - The value to register.
|
|
684
|
+
*/
|
|
685
|
+
register(type, value) {
|
|
686
|
+
switch (type) {
|
|
687
|
+
case 'ids': {
|
|
688
|
+
// deprecated
|
|
689
|
+
const id = value[0]
|
|
690
|
+
const ref = new Inline(this, 'anchor', value[1], { type: 'ref', id })
|
|
691
|
+
this.catalog.refs[id] ??= ref
|
|
692
|
+
// Keep _reftexts in sync if the map was already built (post-parse registration).
|
|
693
|
+
if (this._reftexts && value[1]) this._reftexts[value[1]] ??= id
|
|
694
|
+
return ref
|
|
695
|
+
}
|
|
696
|
+
case 'refs': {
|
|
697
|
+
const id = value[0]
|
|
698
|
+
if (id in this.catalog.refs) return false
|
|
699
|
+
this.catalog.refs[id] = value[1]
|
|
700
|
+
return true
|
|
701
|
+
}
|
|
702
|
+
case 'footnotes':
|
|
703
|
+
this.catalog.footnotes.push(value)
|
|
704
|
+
return
|
|
705
|
+
default:
|
|
706
|
+
if (this.options.catalog_assets) {
|
|
707
|
+
const entry =
|
|
708
|
+
type === 'images'
|
|
709
|
+
? new ImageReference(value, this.attributes.imagesdir)
|
|
710
|
+
: value
|
|
711
|
+
this.catalog[type]?.push(entry)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Find the first registered reference matching the given reftext.
|
|
718
|
+
* @param {string} text - The reftext to look up.
|
|
719
|
+
* @returns {string|null} The matching ID, or null.
|
|
720
|
+
*/
|
|
721
|
+
resolveId(text) {
|
|
722
|
+
if (this._reftexts) return this._reftexts[text] ?? null
|
|
723
|
+
// Fallback: scan refs synchronously (for documents not parsed via parse()).
|
|
724
|
+
for (const [id, ref] of Object.entries(this.catalog.refs)) {
|
|
725
|
+
const xreftext = ref.reftext ?? null
|
|
726
|
+
if (xreftext === text) return id
|
|
727
|
+
}
|
|
728
|
+
return null
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* @private
|
|
733
|
+
* @internal
|
|
734
|
+
* Build the reftext→id lookup map. Called at end of parse().
|
|
735
|
+
*/
|
|
736
|
+
async _buildReftextsMap() {
|
|
737
|
+
this._reftexts = {}
|
|
738
|
+
for (const [id, ref] of Object.entries(this.catalog.refs)) {
|
|
739
|
+
const xreftext = ref.xreftext ? await ref.xreftext() : null
|
|
740
|
+
if (xreftext != null) this._reftexts[xreftext] ??= id
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** @returns {boolean} True if this document has child Section objects. */
|
|
745
|
+
hasSections() {
|
|
746
|
+
return this._nextSectionIndex > 0
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
isMultipart() {
|
|
750
|
+
if (this.doctype !== 'book') return undefined
|
|
751
|
+
return this.blocks.some((b) => {
|
|
752
|
+
if (b.context !== 'section') return false
|
|
753
|
+
if (b.level === 0) return true
|
|
754
|
+
if (!b.special) return false // break in Ruby → but some() handles this
|
|
755
|
+
return false
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
hasFootnotes() {
|
|
760
|
+
return this.catalog.footnotes.length > 0
|
|
761
|
+
}
|
|
762
|
+
get footnotes() {
|
|
763
|
+
return this.catalog.footnotes
|
|
764
|
+
}
|
|
765
|
+
get callouts() {
|
|
766
|
+
return this.catalog.callouts
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
isNested() {
|
|
770
|
+
return this.parentDocument != null
|
|
771
|
+
}
|
|
772
|
+
isEmbedded() {
|
|
773
|
+
return 'embedded' in this.attributes
|
|
774
|
+
}
|
|
775
|
+
hasExtensions() {
|
|
776
|
+
return this.extensions != null
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
source() {
|
|
780
|
+
return this.reader?.source?.() ?? null
|
|
781
|
+
}
|
|
782
|
+
sourceLines() {
|
|
783
|
+
return this.reader?.sourceLines ?? null
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
basebackend(base) {
|
|
787
|
+
return this.attributes.basebackend === base
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** @returns {string|null} The document title. */
|
|
791
|
+
get title() {
|
|
792
|
+
return this.doctitle()
|
|
793
|
+
}
|
|
794
|
+
set title(val) {
|
|
795
|
+
let sect = this.header
|
|
796
|
+
if (!sect) {
|
|
797
|
+
sect = this.header = new Section(this, 0)
|
|
798
|
+
sect.sectname = 'header'
|
|
799
|
+
}
|
|
800
|
+
sect.title = val
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Resolve the primary title for the document.
|
|
805
|
+
* @param {Object} [opts={}]
|
|
806
|
+
* @param {boolean} [opts.use_fallback] - Use 'untitled-label' if no title found.
|
|
807
|
+
* @param {boolean|string} [opts.partition] - Return a DocumentTitle instead of a string.
|
|
808
|
+
* @param {boolean} [opts.sanitize] - Strip XML tags from the title.
|
|
809
|
+
* @returns {string|DocumentTitle|null}
|
|
810
|
+
*/
|
|
811
|
+
doctitle(opts = {}) {
|
|
812
|
+
let val = this.attributes.title
|
|
813
|
+
if (val == null) {
|
|
814
|
+
const sect = this.firstSection()
|
|
815
|
+
if (sect) {
|
|
816
|
+
val = sect.title
|
|
817
|
+
} else if (opts.use_fallback) {
|
|
818
|
+
val = this.attributes['untitled-label']
|
|
819
|
+
}
|
|
820
|
+
if (val == null) return null
|
|
821
|
+
}
|
|
822
|
+
if (opts.partition) {
|
|
823
|
+
const sep =
|
|
824
|
+
opts.partition === true
|
|
825
|
+
? this.attributes['title-separator']
|
|
826
|
+
: opts.partition
|
|
827
|
+
return new DocumentTitle(val, { ...opts, separator: sep })
|
|
828
|
+
}
|
|
829
|
+
if (opts.sanitize && val.includes('<')) {
|
|
830
|
+
return val.replace(XmlSanitizeRx, '').replace(/ {2,}/g, ' ').trim()
|
|
831
|
+
}
|
|
832
|
+
return val
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
get name() {
|
|
836
|
+
return this.doctitle()
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* @param {string|null} [_xrefstyle=null]
|
|
841
|
+
* @returns {Promise<string|null>}
|
|
842
|
+
*/
|
|
843
|
+
xreftext(_xrefstyle = null) {
|
|
844
|
+
const val = this.reftext
|
|
845
|
+
return val && val.length > 0 ? val : this.title
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
get author() {
|
|
849
|
+
return this.attributes.author ?? null
|
|
850
|
+
}
|
|
851
|
+
get revdate() {
|
|
852
|
+
return this.attributes.revdate ?? null
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
authors() {
|
|
856
|
+
const attrs = this.attributes
|
|
857
|
+
if (!('author' in attrs)) return []
|
|
858
|
+
const list = [
|
|
859
|
+
new Author(
|
|
860
|
+
attrs.author,
|
|
861
|
+
attrs.firstname,
|
|
862
|
+
attrs.middlename,
|
|
863
|
+
attrs.lastname,
|
|
864
|
+
attrs.authorinitials,
|
|
865
|
+
attrs.email
|
|
866
|
+
),
|
|
867
|
+
]
|
|
868
|
+
const numAuthors = parseInt(attrs.authorcount ?? '0', 10)
|
|
869
|
+
for (let idx = 2; idx <= numAuthors; idx++) {
|
|
870
|
+
list.push(
|
|
871
|
+
new Author(
|
|
872
|
+
attrs[`author_${idx}`],
|
|
873
|
+
attrs[`firstname_${idx}`],
|
|
874
|
+
attrs[`middlename_${idx}`],
|
|
875
|
+
attrs[`lastname_${idx}`],
|
|
876
|
+
attrs[`authorinitials_${idx}`],
|
|
877
|
+
attrs[`email_${idx}`]
|
|
878
|
+
)
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
return list
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
isNotitle() {
|
|
885
|
+
return 'notitle' in this.attributes
|
|
886
|
+
}
|
|
887
|
+
isNoheader() {
|
|
888
|
+
return 'noheader' in this.attributes
|
|
889
|
+
}
|
|
890
|
+
isNofooter() {
|
|
891
|
+
return 'nofooter' in this.attributes
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
firstSection() {
|
|
895
|
+
return (
|
|
896
|
+
this.header ?? this.blocks.find((b) => b.context === 'section') ?? null
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
hasHeader() {
|
|
901
|
+
return this.header != null
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Append a child Block, assigning index if it's a section.
|
|
906
|
+
* @param {Object} block
|
|
907
|
+
* @returns {Object} The appended block.
|
|
908
|
+
*/
|
|
909
|
+
append(block) {
|
|
910
|
+
if (block.context === 'section') this.assignNumeral(block)
|
|
911
|
+
return super.append(block)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @private
|
|
916
|
+
* Called by parser after parsing header, before parsing body.
|
|
917
|
+
*/
|
|
918
|
+
finalizeHeader(unrootedAttributes, headerValid = true) {
|
|
919
|
+
this._clearPlaybackAttributes(unrootedAttributes)
|
|
920
|
+
this._saveAttributes()
|
|
921
|
+
if (!headerValid) unrootedAttributes['invalid-header'] = true
|
|
922
|
+
return unrootedAttributes
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Replay attribute assignments from block attributes.
|
|
927
|
+
* @param {Object} blockAttributes
|
|
928
|
+
*/
|
|
929
|
+
playbackAttributes(blockAttributes) {
|
|
930
|
+
if (!('attribute_entries' in blockAttributes)) return
|
|
931
|
+
for (const entry of blockAttributes.attribute_entries) {
|
|
932
|
+
if (entry.negate) {
|
|
933
|
+
delete this.attributes[entry.name]
|
|
934
|
+
if (entry.name === 'compat-mode') this.compatMode = false
|
|
935
|
+
} else {
|
|
936
|
+
this.attributes[entry.name] = entry.value
|
|
937
|
+
if (entry.name === 'compat-mode') this.compatMode = true
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Set the specified attribute if not locked, applying attribute value substitutions.
|
|
944
|
+
* @param {string} name
|
|
945
|
+
* @param {string} [value='']
|
|
946
|
+
* @returns {string|null} The substituted value, or `null` if the attribute is locked.
|
|
947
|
+
*/
|
|
948
|
+
setAttribute(name, value = '') {
|
|
949
|
+
return this._setAttributeInternal(name, value, false)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Delete the specified attribute if not locked.
|
|
954
|
+
* @param {string} name
|
|
955
|
+
* @returns {boolean} True if deleted, false if locked.
|
|
956
|
+
*/
|
|
957
|
+
deleteAttribute(name) {
|
|
958
|
+
if (this.isAttributeLocked(name)) return false
|
|
959
|
+
delete this.attributes[name]
|
|
960
|
+
this._attributesModified.add(name)
|
|
961
|
+
return true
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Check if the attribute is locked (set via attribute overrides).
|
|
966
|
+
* @param {string} name
|
|
967
|
+
* @returns {boolean}
|
|
968
|
+
*/
|
|
969
|
+
isAttributeLocked(name) {
|
|
970
|
+
return name in this._attributeOverrides
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** @deprecated Use isAttributeLocked instead. */
|
|
974
|
+
attributeLocked(name) {
|
|
975
|
+
return this.isAttributeLocked(name)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Assign a value to the specified attribute in the document header.
|
|
980
|
+
* @param {string} name
|
|
981
|
+
* @param {string} [value='']
|
|
982
|
+
* @param {boolean} [overwrite=true]
|
|
983
|
+
* @returns {boolean} False if the attribute exists and overwrite is false.
|
|
984
|
+
*/
|
|
985
|
+
setHeaderAttribute(name, value = '', overwrite = true) {
|
|
986
|
+
const target = this._headerAttributes ?? this.attributes
|
|
987
|
+
if (!overwrite && name in target) return false
|
|
988
|
+
target[name] = value
|
|
989
|
+
return true
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Convert the parsed document to its output format (HTML5 by default).
|
|
994
|
+
*
|
|
995
|
+
* If `parse()` has not been called yet, it is called automatically.
|
|
996
|
+
*
|
|
997
|
+
* @param {Object} [opts={}] - Conversion options.
|
|
998
|
+
* @param {boolean} [opts.standalone] - When `true`, wraps output in a full
|
|
999
|
+
* document shell (html/head/body). Defaults to the `standalone` option
|
|
1000
|
+
* passed at load time (which itself defaults to `true`).
|
|
1001
|
+
* @param {string} [opts.outfile] - Path of the output file; stored as the
|
|
1002
|
+
* `outfile` document attribute during conversion.
|
|
1003
|
+
* @param {string} [opts.outdir] - Directory of the output file; stored as the
|
|
1004
|
+
* `outdir` document attribute during conversion.
|
|
1005
|
+
* @returns {Promise<string>} The converted output string.
|
|
1006
|
+
*
|
|
1007
|
+
* @example <caption>Embedded HTML (no html/head/body wrapper)</caption>
|
|
1008
|
+
* const doc = await Document.create('= Hello\nWorld', {})
|
|
1009
|
+
* const html = await doc.convert({ standalone: false })
|
|
1010
|
+
*
|
|
1011
|
+
* @example <caption>Full standalone HTML page</caption>
|
|
1012
|
+
* const html = await doc.convert({ standalone: true })
|
|
1013
|
+
*/
|
|
1014
|
+
async convert(opts = {}) {
|
|
1015
|
+
if (this._timings) this._timings.start('convert')
|
|
1016
|
+
await this.parse()
|
|
1017
|
+
// Pre-compute AsciiDoc table cell content now that parse is done:
|
|
1018
|
+
// callouts are rewound and all refs are registered.
|
|
1019
|
+
if (!this.parentDocument) await this._convertAsciiDocCells()
|
|
1020
|
+
if (this.safe < SafeMode.SERVER && Object.keys(opts).length > 0) {
|
|
1021
|
+
if (!opts.outfile) delete this.attributes.outfile
|
|
1022
|
+
else this.attributes.outfile = opts.outfile
|
|
1023
|
+
if (!opts.outdir) delete this.attributes.outdir
|
|
1024
|
+
else this.attributes.outdir = opts.outdir
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let output
|
|
1028
|
+
if (this.doctype === 'inline') {
|
|
1029
|
+
const block = this.blocks[0] ?? this.header
|
|
1030
|
+
if (block) {
|
|
1031
|
+
if (
|
|
1032
|
+
block.contentModel === 'compound' ||
|
|
1033
|
+
block.contentModel === 'empty'
|
|
1034
|
+
) {
|
|
1035
|
+
this.logger.warn(
|
|
1036
|
+
'no inline candidate; use the inline doctype to convert a single paragraph, verbatim, or raw block'
|
|
1037
|
+
)
|
|
1038
|
+
} else {
|
|
1039
|
+
output = await block.content()
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
} else {
|
|
1043
|
+
let transform
|
|
1044
|
+
if ('standalone' in opts) {
|
|
1045
|
+
transform = opts.standalone ? 'document' : 'embedded'
|
|
1046
|
+
} else if ('header_footer' in opts) {
|
|
1047
|
+
transform = opts.header_footer ? 'document' : 'embedded'
|
|
1048
|
+
} else {
|
|
1049
|
+
transform = this.options.standalone ? 'document' : 'embedded'
|
|
1050
|
+
}
|
|
1051
|
+
output = await this.converter.convert(this, transform)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!this.parentDocument && this.extensions?.hasPostprocessors?.()) {
|
|
1055
|
+
for (const ext of this.extensions.postprocessors()) {
|
|
1056
|
+
output = ext.processMethod(this, output)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (this._timings) this._timings.record('convert')
|
|
1061
|
+
return output
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** @deprecated Use convert instead. */
|
|
1065
|
+
render(opts = {}) {
|
|
1066
|
+
return this.convert(opts)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Write converted output to a file path or a writable stream.
|
|
1071
|
+
*
|
|
1072
|
+
* When `target` is a **string**, the output is written to that file path using
|
|
1073
|
+
* `node:fs/promises.writeFile`.
|
|
1074
|
+
* When `target` is a **writable stream** (has a `.write()` method), the output
|
|
1075
|
+
* is written to the stream in two chunks (content + newline).
|
|
1076
|
+
* When the converter itself implements `write()`, that method is called instead.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {string} output - The converted output string returned by {@link convert}.
|
|
1079
|
+
* @param {string|import('stream').Writable} target - File path or writable stream.
|
|
1080
|
+
* @returns {Promise<void>}
|
|
1081
|
+
*
|
|
1082
|
+
* @example <caption>Write to a file</caption>
|
|
1083
|
+
* const output = await doc.convert()
|
|
1084
|
+
* await doc.write(output, 'out/index.html')
|
|
1085
|
+
*
|
|
1086
|
+
* @example <caption>Write to a stream</caption>
|
|
1087
|
+
* await doc.write(output, process.stdout)
|
|
1088
|
+
*/
|
|
1089
|
+
async write(output, target) {
|
|
1090
|
+
if (this._timings) this._timings.start('write')
|
|
1091
|
+
if (typeof this.converter.write === 'function') {
|
|
1092
|
+
this.converter.write(output, target)
|
|
1093
|
+
} else {
|
|
1094
|
+
if (target && typeof target.write === 'function') {
|
|
1095
|
+
if (output && output.length > 0) {
|
|
1096
|
+
target.write(output.replace(/\n$/, ''))
|
|
1097
|
+
target.write(LF)
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
try {
|
|
1101
|
+
const { writeFile } = await import('node:fs/promises')
|
|
1102
|
+
await writeFile(target, output ?? '', 'utf8')
|
|
1103
|
+
} catch {}
|
|
1104
|
+
}
|
|
1105
|
+
if (
|
|
1106
|
+
this.backend === 'manpage' &&
|
|
1107
|
+
typeof target === 'string' &&
|
|
1108
|
+
typeof this.converter.constructor?.writeAlternatePages === 'function'
|
|
1109
|
+
) {
|
|
1110
|
+
this.converter.constructor.writeAlternatePages(
|
|
1111
|
+
this.attributes.mannames,
|
|
1112
|
+
this.attributes.manvolnum,
|
|
1113
|
+
target
|
|
1114
|
+
)
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (this._timings) this._timings.record('write')
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async content() {
|
|
1121
|
+
delete this.attributes.title
|
|
1122
|
+
return super.content()
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Read the docinfo file(s) for inclusion in the document template.
|
|
1127
|
+
* @param {string} [location='head'] - 'head' or 'footer'.
|
|
1128
|
+
* @param {string|null} [suffix=null] - File suffix override.
|
|
1129
|
+
* @returns {Promise<string>} Combined docinfo content.
|
|
1130
|
+
*/
|
|
1131
|
+
async docinfo(location = 'head', suffix = null) {
|
|
1132
|
+
let content = null
|
|
1133
|
+
if (this.safe < SafeMode.SECURE) {
|
|
1134
|
+
const qualifier = location !== 'head' ? `-${location}` : ''
|
|
1135
|
+
suffix ??= this.outfilesuffix
|
|
1136
|
+
|
|
1137
|
+
let docinfo = this.attributes.docinfo
|
|
1138
|
+
if (!docinfo) {
|
|
1139
|
+
if ('docinfo2' in this.attributes) {
|
|
1140
|
+
docinfo = ['private', 'shared']
|
|
1141
|
+
} else if ('docinfo1' in this.attributes) {
|
|
1142
|
+
docinfo = ['shared']
|
|
1143
|
+
} else {
|
|
1144
|
+
docinfo = docinfo != null ? ['private'] : null
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
docinfo = docinfo.split(',').map((k) => k.trim())
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (docinfo) {
|
|
1151
|
+
content = []
|
|
1152
|
+
const docinfoFile = `docinfo${qualifier}${suffix}`
|
|
1153
|
+
const docinfoDir = this.attributes.docinfodir
|
|
1154
|
+
const docinfoSubs = this._resolveDocinfoSubs()
|
|
1155
|
+
|
|
1156
|
+
const hasShared =
|
|
1157
|
+
docinfo.includes('shared') || docinfo.includes(`shared-${location}`)
|
|
1158
|
+
if (hasShared) {
|
|
1159
|
+
const path = this.normalizeSystemPath(docinfoFile, docinfoDir)
|
|
1160
|
+
const shared = await this.readAsset(path, { normalize: true })
|
|
1161
|
+
if (shared) content.push(await this.applySubs(shared, docinfoSubs))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const docname = this.attributes.docname
|
|
1165
|
+
const hasPrivate =
|
|
1166
|
+
docname &&
|
|
1167
|
+
(docinfo.includes('private') ||
|
|
1168
|
+
docinfo.includes(`private-${location}`))
|
|
1169
|
+
if (hasPrivate) {
|
|
1170
|
+
const path = this.normalizeSystemPath(
|
|
1171
|
+
`${docname}-${docinfoFile}`,
|
|
1172
|
+
docinfoDir
|
|
1173
|
+
)
|
|
1174
|
+
const priv = await this.readAsset(path, { normalize: true })
|
|
1175
|
+
if (priv) content.push(await this.applySubs(priv, docinfoSubs))
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (this.extensions && this.hasDocinfoProcessors(location)) {
|
|
1181
|
+
const extContent = this._docinfoProcessorExtensions[location]
|
|
1182
|
+
.map((ext) => ext.processMethod(this))
|
|
1183
|
+
.filter(Boolean)
|
|
1184
|
+
return (content ?? []).concat(extContent).join(LF)
|
|
1185
|
+
}
|
|
1186
|
+
return content ? content.join(LF) : ''
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* @param {string} [location='head'] A location for checking docinfo extensions at a given location (head or footer).
|
|
1191
|
+
* @returns {boolean} True if docinfo processors are registered for the given location.
|
|
1192
|
+
*/
|
|
1193
|
+
hasDocinfoProcessors(location = 'head') {
|
|
1194
|
+
if (location in this._docinfoProcessorExtensions) {
|
|
1195
|
+
return this._docinfoProcessorExtensions[location] !== false
|
|
1196
|
+
}
|
|
1197
|
+
if (this.extensions?.hasDocinfoProcessors?.(location)) {
|
|
1198
|
+
const exts = this.extensions.docinfoProcessors(location)
|
|
1199
|
+
this._docinfoProcessorExtensions[location] = exts || false
|
|
1200
|
+
return !!exts
|
|
1201
|
+
}
|
|
1202
|
+
this._docinfoProcessorExtensions[location] = false
|
|
1203
|
+
return false
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// ── JavaScript-style accessors ────────────────────────────────────────────────
|
|
1207
|
+
|
|
1208
|
+
/** @returns {string|null} The document title. */
|
|
1209
|
+
getTitle() {
|
|
1210
|
+
return this.title
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** @param {string} val */
|
|
1214
|
+
setTitle(val) {
|
|
1215
|
+
this.title = val
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* @deprecated Use {@link getDocumentTitle} instead.
|
|
1220
|
+
* @see getDocumentTitle
|
|
1221
|
+
*/
|
|
1222
|
+
getDoctitle(opts = {}) {
|
|
1223
|
+
return this.doctitle(opts)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Resolve the primary title for the document.
|
|
1228
|
+
*
|
|
1229
|
+
* Searches the following locations in order, returning the first non-empty value:
|
|
1230
|
+
* - document-level attribute named `title`
|
|
1231
|
+
* - header title (the document title)
|
|
1232
|
+
* - title of the first section
|
|
1233
|
+
* - document-level attribute named `untitled-label` (if `opts.use_fallback` is set)
|
|
1234
|
+
*
|
|
1235
|
+
* If no value can be resolved, `null` is returned.
|
|
1236
|
+
*
|
|
1237
|
+
* If `opts.partition` is specified, the value is parsed into a {@link DocumentTitle} object.
|
|
1238
|
+
* If `opts.sanitize` is specified, XML elements are removed from the value.
|
|
1239
|
+
* @param {Object} [opts={}]
|
|
1240
|
+
* @param {boolean} [opts.partition] - Parse the title into a {@link DocumentTitle} with main and subtitle parts.
|
|
1241
|
+
* @param {boolean} [opts.sanitize] - Strip XML/HTML elements from the resolved title.
|
|
1242
|
+
* @param {boolean} [opts.use_fallback] - Fall back to the `untitled-label` attribute if no title is found.
|
|
1243
|
+
* @returns {string|DocumentTitle|null} The resolved title, or null if none found.
|
|
1244
|
+
*/
|
|
1245
|
+
getDocumentTitle(opts = {}) {
|
|
1246
|
+
return this.doctitle(opts)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** @returns {string} The captioned title. */
|
|
1250
|
+
getCaptionedTitle() {
|
|
1251
|
+
return this.captionedTitle()
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** @returns {string} The document type (e.g. 'article', 'book'). */
|
|
1255
|
+
getDoctype() {
|
|
1256
|
+
return this.doctype
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** @returns {string} The backend name (e.g. 'html5', 'docbook5'). */
|
|
1260
|
+
getBackend() {
|
|
1261
|
+
return this.backend
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* @returns {number} The safe mode level as a numeric value.
|
|
1266
|
+
* Corresponds to {@link SafeMode}: unsafe (0), safe (1), server (10), secure (20).
|
|
1267
|
+
*/
|
|
1268
|
+
getSafe() {
|
|
1269
|
+
return this.safe
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Get the AsciiDoc compatibility mode flag.
|
|
1274
|
+
*
|
|
1275
|
+
* Enabling this attribute activates the following syntax changes:
|
|
1276
|
+
* - single quotes as constrained emphasis formatting marks
|
|
1277
|
+
* - single backticks parsed as inline literal, formatted as monospace
|
|
1278
|
+
* - single plus parsed as constrained, monospaced inline formatting
|
|
1279
|
+
* - double plus parsed as constrained, monospaced inline formatting
|
|
1280
|
+
* @returns {boolean} True if compat mode is enabled.
|
|
1281
|
+
*/
|
|
1282
|
+
getCompatMode() {
|
|
1283
|
+
return this.compatMode
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** @returns {boolean} True if sourcemap is enabled. */
|
|
1287
|
+
getSourcemap() {
|
|
1288
|
+
return this.sourcemap
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** @param {boolean} val */
|
|
1292
|
+
setSourcemap(val) {
|
|
1293
|
+
this.sourcemap = val
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** @returns {string} The output file suffix (e.g. '.html'). */
|
|
1297
|
+
getOutfilesuffix() {
|
|
1298
|
+
return this.outfilesuffix
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/** @returns {Object} The frozen options object. */
|
|
1302
|
+
getOptions() {
|
|
1303
|
+
return this.options
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** @returns {Object} The converter instance. */
|
|
1307
|
+
getConverter() {
|
|
1308
|
+
return this.converter
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Set the converter instance for this document.
|
|
1313
|
+
* @param {Object} converter - The converter instance.
|
|
1314
|
+
*/
|
|
1315
|
+
setConverter(converter) {
|
|
1316
|
+
this.converter = converter
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** @returns {string|null} The raw AsciiDoc source. */
|
|
1320
|
+
getSource() {
|
|
1321
|
+
return this.source()
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/** @returns {string[]|null} The source lines. */
|
|
1325
|
+
getSourceLines() {
|
|
1326
|
+
return this.sourceLines()
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** @returns {Object} The preprocessor reader. */
|
|
1330
|
+
getReader() {
|
|
1331
|
+
return this.reader
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** @returns {Footnote[]} The registered footnotes. */
|
|
1335
|
+
getFootnotes() {
|
|
1336
|
+
return this.footnotes
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** @returns {Object} The callouts registry. */
|
|
1340
|
+
getCallouts() {
|
|
1341
|
+
return this.callouts
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** @returns {Object} The asset catalog. */
|
|
1345
|
+
getCatalog() {
|
|
1346
|
+
return this.catalog
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** @returns {Object} The counters map. */
|
|
1350
|
+
getCounters() {
|
|
1351
|
+
return this._counters
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** @returns {string|null} The first author name. */
|
|
1355
|
+
getAuthor() {
|
|
1356
|
+
return this.author
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** @returns {Author[]} All document authors. */
|
|
1360
|
+
getAuthors() {
|
|
1361
|
+
return this.authors()
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** @returns {string} The base directory path. */
|
|
1365
|
+
getBaseDir() {
|
|
1366
|
+
return this.baseDir
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** @returns {RevisionInfo} The revision information. */
|
|
1370
|
+
getRevisionInfo() {
|
|
1371
|
+
const attrs = this.attributes
|
|
1372
|
+
return new RevisionInfo(
|
|
1373
|
+
attrs.revnumber ?? null,
|
|
1374
|
+
attrs.revdate ?? null,
|
|
1375
|
+
attrs.revremark ?? null
|
|
1376
|
+
)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** @returns {Object|null} The extensions registry. */
|
|
1380
|
+
getExtensions() {
|
|
1381
|
+
return this.extensions
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** @returns {Document|undefined} The parent document, or undefined for root documents. */
|
|
1385
|
+
getParentDocument() {
|
|
1386
|
+
return this.parentDocument ?? undefined
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Get the parent node of this node.
|
|
1391
|
+
* Always returns undefined for a root Document (Document is its own internal parent).
|
|
1392
|
+
* @returns {undefined}
|
|
1393
|
+
*/
|
|
1394
|
+
getParent() {
|
|
1395
|
+
return undefined
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** @returns {Object|null} The syntax highlighter instance. */
|
|
1399
|
+
getSyntaxHighlighter() {
|
|
1400
|
+
return this.syntaxHighlighter
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** @returns {Object} The id→node reference map. */
|
|
1404
|
+
getRefs() {
|
|
1405
|
+
return this.catalog.refs
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** @returns {ImageReference[]} The registered image references. */
|
|
1409
|
+
getImages() {
|
|
1410
|
+
return this.catalog.images
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** @returns {string[]} The registered links. */
|
|
1414
|
+
getLinks() {
|
|
1415
|
+
return this.catalog.links
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** @returns {Object|null} The level-0 Section (document header). */
|
|
1419
|
+
getHeader() {
|
|
1420
|
+
return this.header
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** @returns {boolean} True if the basebackend attribute is set. */
|
|
1424
|
+
isBasebackend() {
|
|
1425
|
+
return !!this.attributes.basebackend
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** @returns {Object} The asset catalog (alias for getCatalog). */
|
|
1429
|
+
getReferences() {
|
|
1430
|
+
return this.catalog
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** @returns {string|undefined} The revision date. */
|
|
1434
|
+
getRevisionDate() {
|
|
1435
|
+
return this.attributes.revdate ?? undefined
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** @returns {string|undefined} The revision date (alias for getRevisionDate). */
|
|
1439
|
+
getRevdate() {
|
|
1440
|
+
return this.attributes.revdate ?? undefined
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** @returns {string|undefined} The revision number. */
|
|
1444
|
+
getRevisionNumber() {
|
|
1445
|
+
return this.attributes.revnumber ?? undefined
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** @returns {string|undefined} The revision remark. */
|
|
1449
|
+
getRevisionRemark() {
|
|
1450
|
+
return this.attributes.revremark ?? undefined
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** @returns {boolean} True if any revision info is set. */
|
|
1454
|
+
hasRevisionInfo() {
|
|
1455
|
+
return !this.getRevisionInfo().isEmpty()
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** @returns {boolean} True if the notitle attribute is set. */
|
|
1459
|
+
getNotitle() {
|
|
1460
|
+
return this.isNotitle()
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** @returns {boolean} True if the noheader attribute is set. */
|
|
1464
|
+
getNoheader() {
|
|
1465
|
+
return this.isNoheader()
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** @returns {boolean} True if the nofooter attribute is set. */
|
|
1469
|
+
getNofooter() {
|
|
1470
|
+
return this.isNofooter()
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** Restore attributes to their saved header state. */
|
|
1474
|
+
restoreAttributes() {
|
|
1475
|
+
if (!this.parentDocument) this.catalog.callouts.rewind()
|
|
1476
|
+
const toRestore = this._headerAttributes
|
|
1477
|
+
if (toRestore) {
|
|
1478
|
+
for (const key of Object.keys(this.attributes)) {
|
|
1479
|
+
if (!(key in toRestore)) delete this.attributes[key]
|
|
1480
|
+
}
|
|
1481
|
+
Object.assign(this.attributes, toRestore)
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* @param {string} [location='head']
|
|
1487
|
+
* @param {string} [suffix]
|
|
1488
|
+
* @returns {Promise<string>}
|
|
1489
|
+
*/
|
|
1490
|
+
async getDocinfo(location = 'head', suffix = undefined) {
|
|
1491
|
+
return this.docinfo(location, suffix)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Delete the specified attribute if not locked.
|
|
1496
|
+
* @param {string} name - The attribute name to remove.
|
|
1497
|
+
* @returns {string|undefined} The previous value, or undefined if not present or locked.
|
|
1498
|
+
*/
|
|
1499
|
+
removeAttribute(name) {
|
|
1500
|
+
const prev = this.attributes[name]
|
|
1501
|
+
this.deleteAttribute(name)
|
|
1502
|
+
return prev
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
toString() {
|
|
1506
|
+
return `#<Document {doctype: '${this.doctype}', doctitle: ${JSON.stringify(this.header?.title ?? null)}, blocks: ${this.blocks.length}}>`
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ── Private methods ─────────────────────────────────────────────────────────
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* @private
|
|
1513
|
+
* @internal
|
|
1514
|
+
* Set the specified attribute without applying attribute value substitutions.
|
|
1515
|
+
* Used internally by the parser when the value is already resolved.
|
|
1516
|
+
* @param {string} name
|
|
1517
|
+
* @param {string} [value='']
|
|
1518
|
+
* @returns {string|null} The value as-is, or `null` if the attribute is locked.
|
|
1519
|
+
*/
|
|
1520
|
+
_setAttributeRaw(name, value = '') {
|
|
1521
|
+
return this._setAttributeInternal(name, value, true)
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* @private
|
|
1526
|
+
* @internal
|
|
1527
|
+
*/
|
|
1528
|
+
_setAttributeInternal(name, value, skipSubs) {
|
|
1529
|
+
if (this.isAttributeLocked(name)) return null
|
|
1530
|
+
if (!skipSubs && value && value !== '')
|
|
1531
|
+
value = this._applyAttributeValueSubs(value)
|
|
1532
|
+
if (this._headerAttributes) {
|
|
1533
|
+
// Beyond the document header; only update live attributes, not the header snapshot.
|
|
1534
|
+
this.attributes[name] = value
|
|
1535
|
+
} else {
|
|
1536
|
+
switch (name) {
|
|
1537
|
+
case 'backend':
|
|
1538
|
+
this._updateBackendAttributes(
|
|
1539
|
+
value,
|
|
1540
|
+
this._attributesModified.delete('htmlsyntax') &&
|
|
1541
|
+
value === this.backend
|
|
1542
|
+
)
|
|
1543
|
+
break
|
|
1544
|
+
case 'doctype':
|
|
1545
|
+
this._updateDoctypeAttributes(value)
|
|
1546
|
+
break
|
|
1547
|
+
default:
|
|
1548
|
+
this.attributes[name] = value
|
|
1549
|
+
}
|
|
1550
|
+
this._attributesModified.add(name)
|
|
1551
|
+
}
|
|
1552
|
+
return value
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* @private
|
|
1557
|
+
* @internal
|
|
1558
|
+
* Walk the block tree in document order and pre-compute the content of
|
|
1559
|
+
* every AsciiDoc-style table cell. Must be called AFTER parse() has finished so
|
|
1560
|
+
* that (a) callouts.rewind() has been called and (b) all cross-references from
|
|
1561
|
+
* the main document are already registered in the catalog.
|
|
1562
|
+
*/
|
|
1563
|
+
async _convertAsciiDocCells(block = this) {
|
|
1564
|
+
for (const child of block.blocks ?? []) {
|
|
1565
|
+
if (child.context === 'table') {
|
|
1566
|
+
for (const section of ['head', 'body', 'foot']) {
|
|
1567
|
+
for (const row of child.rows[section] ?? []) {
|
|
1568
|
+
for (const cell of row) {
|
|
1569
|
+
if (
|
|
1570
|
+
cell.style === 'asciidoc' &&
|
|
1571
|
+
cell._innerDocument &&
|
|
1572
|
+
cell._innerContent == null
|
|
1573
|
+
) {
|
|
1574
|
+
cell._innerContent = await cell._innerDocument.convert()
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
await this._convertAsciiDocCells(child)
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* @private
|
|
1587
|
+
* Sync version: applies only synchronous subs (specialcharacters, attributes, replacements).
|
|
1588
|
+
* Used by setAttribute() which must remain sync for the {set:...} inline directive path.
|
|
1589
|
+
* Async subs (quotes, macros, …) in pass macros are handled by _applyAttributeEntryValueSubs.
|
|
1590
|
+
*/
|
|
1591
|
+
_applyAttributeValueSubs(value) {
|
|
1592
|
+
const m = value.match(AttributeEntryPassMacroRx)
|
|
1593
|
+
if (m) {
|
|
1594
|
+
let result = m[2] ?? ''
|
|
1595
|
+
if (m[1]) {
|
|
1596
|
+
const subs = this.resolvePassSubs(m[1])
|
|
1597
|
+
if (subs) {
|
|
1598
|
+
for (const sub of subs) {
|
|
1599
|
+
if (sub === 'specialcharacters')
|
|
1600
|
+
result = this.subSpecialchars(result)
|
|
1601
|
+
else if (sub === 'attributes') result = this.subAttributes(result)
|
|
1602
|
+
else if (sub === 'replacements')
|
|
1603
|
+
result = this.subReplacements(result)
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return this._maxAttributeValueSize != null
|
|
1608
|
+
? _limitBytesize(result, this._maxAttributeValueSize)
|
|
1609
|
+
: result
|
|
1610
|
+
}
|
|
1611
|
+
const result = this.applyHeaderSubs(value)
|
|
1612
|
+
return this._maxAttributeValueSize != null
|
|
1613
|
+
? _limitBytesize(result, this._maxAttributeValueSize)
|
|
1614
|
+
: result
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* @private
|
|
1619
|
+
* Async version: applies all subs including async ones (quotes, macros, …).
|
|
1620
|
+
* Used by processAttributeEntry() which can await the result.
|
|
1621
|
+
*/
|
|
1622
|
+
async _applyAttributeEntryValueSubs(value) {
|
|
1623
|
+
const m = value.match(AttributeEntryPassMacroRx)
|
|
1624
|
+
if (m) {
|
|
1625
|
+
let result = m[2] ?? ''
|
|
1626
|
+
if (m[1]) {
|
|
1627
|
+
const subs = this.resolvePassSubs(m[1])
|
|
1628
|
+
if (subs) result = await this.applySubs(result, subs)
|
|
1629
|
+
}
|
|
1630
|
+
return this._maxAttributeValueSize != null
|
|
1631
|
+
? _limitBytesize(result, this._maxAttributeValueSize)
|
|
1632
|
+
: result
|
|
1633
|
+
}
|
|
1634
|
+
const result = this.applyHeaderSubs(value)
|
|
1635
|
+
return this._maxAttributeValueSize != null
|
|
1636
|
+
? _limitBytesize(result, this._maxAttributeValueSize)
|
|
1637
|
+
: result
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* @private
|
|
1642
|
+
* Resolve the list of substitutions to apply to docinfo files.
|
|
1643
|
+
*
|
|
1644
|
+
* Resolves subs from the `docinfosubs` document attribute if present,
|
|
1645
|
+
* otherwise returns `['attributes']` as the default.
|
|
1646
|
+
* @returns {string[]} The list of substitutions to apply.
|
|
1647
|
+
*/
|
|
1648
|
+
_resolveDocinfoSubs() {
|
|
1649
|
+
return 'docinfosubs' in this.attributes
|
|
1650
|
+
? this.resolveSubs(this.attributes.docinfosubs, 'block', null, 'docinfo')
|
|
1651
|
+
: ['attributes']
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* @private
|
|
1656
|
+
* Walk the block tree and pre-compute all async text values.
|
|
1657
|
+
* Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
|
|
1658
|
+
*/
|
|
1659
|
+
async _resolveAllTexts(block) {
|
|
1660
|
+
// Skip title pre-computation for blocks with an explicit empty id ([id=]).
|
|
1661
|
+
// In Ruby, apply_title_subs is lazy: it is never called during parsing for such
|
|
1662
|
+
// blocks because section.title is never accessed. An explicit empty id is
|
|
1663
|
+
// distinguished by block.attributes.id === '' (the AttributeList parser preserves it).
|
|
1664
|
+
if (block.attributes?.id !== '') {
|
|
1665
|
+
await block.precomputeTitle?.()
|
|
1666
|
+
}
|
|
1667
|
+
await block.precomputeReftext?.()
|
|
1668
|
+
const ctx = block.context
|
|
1669
|
+
if (ctx === 'dlist') {
|
|
1670
|
+
// dlist.blocks is an array of [[term, ...], item_or_null] pairs.
|
|
1671
|
+
for (const [terms, item] of block.blocks ?? []) {
|
|
1672
|
+
for (const term of terms ?? []) {
|
|
1673
|
+
await term.precomputeText?.()
|
|
1674
|
+
await this._resolveAllTexts(term)
|
|
1675
|
+
}
|
|
1676
|
+
if (item) {
|
|
1677
|
+
await item.precomputeText?.()
|
|
1678
|
+
await this._resolveAllTexts(item)
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
} else if (ctx === 'table') {
|
|
1682
|
+
for (const row of [
|
|
1683
|
+
...(block.rows?.head ?? []),
|
|
1684
|
+
...(block.rows?.body ?? []),
|
|
1685
|
+
...(block.rows?.foot ?? []),
|
|
1686
|
+
]) {
|
|
1687
|
+
for (const cell of row) {
|
|
1688
|
+
await cell.precomputeText?.()
|
|
1689
|
+
await cell.precomputeReftext?.()
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
for (const child of block.blocks ?? []) {
|
|
1694
|
+
await child.precomputeText?.()
|
|
1695
|
+
await this._resolveAllTexts(child)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* @private
|
|
1702
|
+
* Create and initialize an instance of the converter for this document.
|
|
1703
|
+
* @param {string} backend - The backend name (e.g. 'html5', 'docbook5').
|
|
1704
|
+
* @param {string} [delegateBackend] - An optional delegate backend to use when resolving the converter.
|
|
1705
|
+
*/
|
|
1706
|
+
_createConverter(backend, delegateBackend) {
|
|
1707
|
+
const opts = this.options
|
|
1708
|
+
if (!this.converter && opts._preCreatedConverter) {
|
|
1709
|
+
return opts._preCreatedConverter
|
|
1710
|
+
}
|
|
1711
|
+
const converterOpts = {
|
|
1712
|
+
document: this,
|
|
1713
|
+
htmlsyntax: this.attributes.htmlsyntax,
|
|
1714
|
+
}
|
|
1715
|
+
if (opts.template_dirs || opts.template_dir) {
|
|
1716
|
+
converterOpts.template_dirs = [].concat(
|
|
1717
|
+
opts.template_dirs ?? opts.template_dir
|
|
1718
|
+
)
|
|
1719
|
+
converterOpts.template_cache = opts.template_cache ?? true
|
|
1720
|
+
converterOpts.template_engine = opts.template_engine
|
|
1721
|
+
converterOpts.template_engine_options = opts.template_engine_options
|
|
1722
|
+
converterOpts.eruby = opts.eruby
|
|
1723
|
+
converterOpts.safe = this.safe
|
|
1724
|
+
if (delegateBackend) converterOpts.delegate_backend = delegateBackend
|
|
1725
|
+
}
|
|
1726
|
+
if (opts.converter) {
|
|
1727
|
+
return new CustomFactory({ [backend]: opts.converter }).createSync(
|
|
1728
|
+
backend,
|
|
1729
|
+
converterOpts
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
const factory = opts.converter_factory ?? Converter
|
|
1733
|
+
return factory.createSync(backend, converterOpts)
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* Delete any attributes stored for playback
|
|
1738
|
+
* @param attributes
|
|
1739
|
+
* @private
|
|
1740
|
+
* @internal
|
|
1741
|
+
*/
|
|
1742
|
+
_clearPlaybackAttributes(attributes) {
|
|
1743
|
+
delete attributes.attribute_entries
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Branch the attributes so that the original state can be restored
|
|
1748
|
+
* at a future time.
|
|
1749
|
+
* @returns the duplicated attributes, which will later be restored
|
|
1750
|
+
* @private
|
|
1751
|
+
* @internal
|
|
1752
|
+
*/
|
|
1753
|
+
_saveAttributes() {
|
|
1754
|
+
const attrs = this.attributes
|
|
1755
|
+
if (!('doctitle' in attrs)) {
|
|
1756
|
+
const dt = this.doctitle()
|
|
1757
|
+
if (dt) attrs.doctitle = dt
|
|
1758
|
+
}
|
|
1759
|
+
this.id ??= attrs['css-signature'] ?? null
|
|
1760
|
+
|
|
1761
|
+
// Handle toc / toc2
|
|
1762
|
+
// NOTE: delete toc/toc2 from attrs first; only re-add specific placement/position attrs
|
|
1763
|
+
let tocVal
|
|
1764
|
+
if ('toc2' in attrs) {
|
|
1765
|
+
delete attrs.toc2
|
|
1766
|
+
tocVal = 'left'
|
|
1767
|
+
} else if ('toc' in attrs) {
|
|
1768
|
+
tocVal = attrs.toc
|
|
1769
|
+
delete attrs.toc
|
|
1770
|
+
}
|
|
1771
|
+
if (tocVal != null) {
|
|
1772
|
+
const tocPlacementVal = attrs['toc-placement'] ?? 'macro'
|
|
1773
|
+
const tocPositionVal =
|
|
1774
|
+
tocPlacementVal && tocPlacementVal !== 'auto'
|
|
1775
|
+
? tocPlacementVal
|
|
1776
|
+
: attrs['toc-position']
|
|
1777
|
+
if (tocVal !== '' || tocPositionVal) {
|
|
1778
|
+
const defaultTocPosition = 'left'
|
|
1779
|
+
let defaultTocClass = 'toc2'
|
|
1780
|
+
const position = !tocPositionVal
|
|
1781
|
+
? tocVal || defaultTocPosition
|
|
1782
|
+
: tocPositionVal
|
|
1783
|
+
attrs['toc-placement'] = 'auto'
|
|
1784
|
+
switch (position) {
|
|
1785
|
+
case 'left':
|
|
1786
|
+
case '<':
|
|
1787
|
+
case '<':
|
|
1788
|
+
attrs['toc-position'] = 'left'
|
|
1789
|
+
break
|
|
1790
|
+
case 'right':
|
|
1791
|
+
case '>':
|
|
1792
|
+
case '>':
|
|
1793
|
+
attrs['toc-position'] = 'right'
|
|
1794
|
+
break
|
|
1795
|
+
case 'top':
|
|
1796
|
+
case '^':
|
|
1797
|
+
attrs['toc-position'] = 'top'
|
|
1798
|
+
break
|
|
1799
|
+
case 'bottom':
|
|
1800
|
+
case 'v':
|
|
1801
|
+
attrs['toc-position'] = 'bottom'
|
|
1802
|
+
break
|
|
1803
|
+
case 'preamble':
|
|
1804
|
+
case 'macro':
|
|
1805
|
+
attrs['toc-position'] = 'content'
|
|
1806
|
+
attrs['toc-placement'] = position
|
|
1807
|
+
defaultTocClass = null
|
|
1808
|
+
break
|
|
1809
|
+
default:
|
|
1810
|
+
delete attrs['toc-position']
|
|
1811
|
+
defaultTocClass = null
|
|
1812
|
+
}
|
|
1813
|
+
if (defaultTocClass) attrs['toc-class'] ??= defaultTocClass
|
|
1814
|
+
}
|
|
1815
|
+
attrs.toc = ''
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
const iconsVal = attrs.icons
|
|
1819
|
+
if (iconsVal != null && !('icontype' in attrs)) {
|
|
1820
|
+
if (iconsVal !== '' && iconsVal !== 'font') {
|
|
1821
|
+
attrs.icons = ''
|
|
1822
|
+
if (iconsVal !== 'image') attrs.icontype = iconsVal
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
this.compatMode = 'compat-mode' in attrs
|
|
1827
|
+
if (this.compatMode && 'language' in attrs) {
|
|
1828
|
+
attrs['source-language'] = attrs.language
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (!this.parentDocument) {
|
|
1832
|
+
const basebackend = attrs.basebackend
|
|
1833
|
+
if (basebackend === 'html') {
|
|
1834
|
+
const syntaxHlName = attrs['source-highlighter']
|
|
1835
|
+
if (syntaxHlName && !attrs[`${syntaxHlName}-unavailable`]) {
|
|
1836
|
+
// SyntaxHighlighter — optional integration, handle gracefully
|
|
1837
|
+
try {
|
|
1838
|
+
const factory = this.options.syntax_highlighter_factory
|
|
1839
|
+
const syntaxHls = this.options.syntax_highlighters
|
|
1840
|
+
if (factory) {
|
|
1841
|
+
this.syntaxHighlighter = factory.create(
|
|
1842
|
+
syntaxHlName,
|
|
1843
|
+
this.backend,
|
|
1844
|
+
{ document: this }
|
|
1845
|
+
)
|
|
1846
|
+
} else if (syntaxHls) {
|
|
1847
|
+
this.syntaxHighlighter = new DefaultFactoryProxy(
|
|
1848
|
+
syntaxHls,
|
|
1849
|
+
SyntaxHighlighter
|
|
1850
|
+
).create(syntaxHlName, this.backend, { document: this })
|
|
1851
|
+
} else {
|
|
1852
|
+
this.syntaxHighlighter = SyntaxHighlighter.create(
|
|
1853
|
+
syntaxHlName,
|
|
1854
|
+
this.backend,
|
|
1855
|
+
{ document: this }
|
|
1856
|
+
)
|
|
1857
|
+
}
|
|
1858
|
+
} catch {}
|
|
1859
|
+
}
|
|
1860
|
+
} else if (basebackend === 'docbook') {
|
|
1861
|
+
if (
|
|
1862
|
+
!this.isAttributeLocked('toc') &&
|
|
1863
|
+
!this._attributesModified.has('toc')
|
|
1864
|
+
) {
|
|
1865
|
+
attrs.toc = ''
|
|
1866
|
+
}
|
|
1867
|
+
if (
|
|
1868
|
+
!this.isAttributeLocked('sectnums') &&
|
|
1869
|
+
!this._attributesModified.has('sectnums')
|
|
1870
|
+
) {
|
|
1871
|
+
attrs.sectnums = ''
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
this.outfilesuffix = attrs.outfilesuffix ?? null
|
|
1875
|
+
|
|
1876
|
+
for (const name of FLEXIBLE_ATTRIBUTES) {
|
|
1877
|
+
const _fv = this._attributeOverrides[name]
|
|
1878
|
+
if (name in this._attributeOverrides && _fv != null && _fv !== false) {
|
|
1879
|
+
delete this._attributeOverrides[name]
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
this._headerAttributes = { ...attrs }
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Assign the local and document datetime attributes, which includes localdate, localyear, localtime,
|
|
1889
|
+
* localdatetime, docdate, docyear, doctime, and docdatetime. Honor the SOURCE_DATE_EPOCH environment variable, if set.
|
|
1890
|
+
* @param attrs
|
|
1891
|
+
* @param inputMtime
|
|
1892
|
+
* @private
|
|
1893
|
+
* @internal
|
|
1894
|
+
*/
|
|
1895
|
+
_fillDatetimeAttributes(attrs, inputMtime) {
|
|
1896
|
+
const sourceDateEpoch =
|
|
1897
|
+
typeof process !== 'undefined' ? process.env.SOURCE_DATE_EPOCH : null
|
|
1898
|
+
const now =
|
|
1899
|
+
sourceDateEpoch && sourceDateEpoch !== ''
|
|
1900
|
+
? new Date(parseInt(sourceDateEpoch, 10) * 1000)
|
|
1901
|
+
: new Date()
|
|
1902
|
+
|
|
1903
|
+
let localdate = attrs.localdate
|
|
1904
|
+
if (localdate) {
|
|
1905
|
+
attrs.localyear ??= localdate.length >= 4 ? localdate.slice(0, 4) : null
|
|
1906
|
+
} else {
|
|
1907
|
+
localdate = attrs.localdate = _formatDate(now)
|
|
1908
|
+
attrs.localyear ??= String(now.getFullYear())
|
|
1909
|
+
}
|
|
1910
|
+
const localtime = (attrs.localtime ??= _formatTime(now))
|
|
1911
|
+
attrs.localdatetime ??= `${localdate} ${localtime}`
|
|
1912
|
+
|
|
1913
|
+
const effectiveMtime =
|
|
1914
|
+
sourceDateEpoch && sourceDateEpoch !== ''
|
|
1915
|
+
? now
|
|
1916
|
+
: inputMtime instanceof Date
|
|
1917
|
+
? inputMtime
|
|
1918
|
+
: now
|
|
1919
|
+
|
|
1920
|
+
let docdate = attrs.docdate
|
|
1921
|
+
if (docdate) {
|
|
1922
|
+
attrs.docyear ??= docdate.length >= 4 ? docdate.slice(0, 4) : null
|
|
1923
|
+
} else {
|
|
1924
|
+
docdate = attrs.docdate = _formatDate(effectiveMtime)
|
|
1925
|
+
attrs.docyear ??= String(effectiveMtime.getFullYear())
|
|
1926
|
+
}
|
|
1927
|
+
const doctime = (attrs.doctime ??= _formatTime(effectiveMtime))
|
|
1928
|
+
attrs.docdatetime ??= `${docdate} ${doctime}`
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Update the backend attributes to reflect a change in the active backend.
|
|
1933
|
+
*
|
|
1934
|
+
* This method also handles updating the related doctype attributes if the
|
|
1935
|
+
* doctype attribute is assigned at the time this method is called.
|
|
1936
|
+
*
|
|
1937
|
+
* @param newBackend
|
|
1938
|
+
* @param init
|
|
1939
|
+
* @returns {undefined|*} the resolved String backend if updated, nothing otherwise.
|
|
1940
|
+
* @private
|
|
1941
|
+
* @internal
|
|
1942
|
+
*/
|
|
1943
|
+
_updateBackendAttributes(newBackend, init = false) {
|
|
1944
|
+
if (!init && newBackend === this.backend) return undefined
|
|
1945
|
+
const currentBackend = this.backend
|
|
1946
|
+
const attrs = this.attributes
|
|
1947
|
+
const currentBasebackend = attrs.basebackend
|
|
1948
|
+
const currentDoctype = this.doctype
|
|
1949
|
+
|
|
1950
|
+
let delegateBackend = null
|
|
1951
|
+
let actualBackend = null
|
|
1952
|
+
if (newBackend.includes(':')) {
|
|
1953
|
+
const parts = newBackend.split(':')
|
|
1954
|
+
actualBackend = parts[0]
|
|
1955
|
+
newBackend = parts[1]
|
|
1956
|
+
}
|
|
1957
|
+
if (newBackend.startsWith('xhtml')) {
|
|
1958
|
+
attrs.htmlsyntax = 'xml'
|
|
1959
|
+
newBackend = newBackend.slice(1)
|
|
1960
|
+
} else if (newBackend.startsWith('html')) {
|
|
1961
|
+
attrs.htmlsyntax ??= 'html'
|
|
1962
|
+
}
|
|
1963
|
+
newBackend = BACKEND_ALIASES[newBackend] ?? newBackend
|
|
1964
|
+
if (actualBackend) {
|
|
1965
|
+
delegateBackend = newBackend
|
|
1966
|
+
newBackend = actualBackend
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (currentDoctype) {
|
|
1970
|
+
if (currentBackend) {
|
|
1971
|
+
delete attrs[`backend-${currentBackend}`]
|
|
1972
|
+
delete attrs[`backend-${currentBackend}-doctype-${currentDoctype}`]
|
|
1973
|
+
}
|
|
1974
|
+
attrs[`backend-${newBackend}-doctype-${currentDoctype}`] = ''
|
|
1975
|
+
attrs[`doctype-${currentDoctype}`] = ''
|
|
1976
|
+
} else if (currentBackend) {
|
|
1977
|
+
delete attrs[`backend-${currentBackend}`]
|
|
1978
|
+
}
|
|
1979
|
+
attrs[`backend-${newBackend}`] = ''
|
|
1980
|
+
this.backend = attrs.backend = newBackend
|
|
1981
|
+
|
|
1982
|
+
// Create the converter (may be async in some environments; here synchronous)
|
|
1983
|
+
const converter = this._createConverter(newBackend, delegateBackend)
|
|
1984
|
+
let newBasebackend, newFiletype
|
|
1985
|
+
|
|
1986
|
+
if (converter && typeof converter._getBackendTraits === 'function') {
|
|
1987
|
+
newBasebackend = converter.basebackend()
|
|
1988
|
+
newFiletype = converter.filetype()
|
|
1989
|
+
const htmlsyntax = converter.htmlsyntax()
|
|
1990
|
+
if (htmlsyntax) attrs.htmlsyntax = htmlsyntax
|
|
1991
|
+
if (init) {
|
|
1992
|
+
attrs.outfilesuffix ??= converter.outfilesuffix()
|
|
1993
|
+
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
1994
|
+
attrs.outfilesuffix = converter.outfilesuffix()
|
|
1995
|
+
}
|
|
1996
|
+
} else if (converter) {
|
|
1997
|
+
const traits = deriveBackendTraits(newBackend)
|
|
1998
|
+
newBasebackend = traits.basebackend
|
|
1999
|
+
newFiletype = traits.filetype
|
|
2000
|
+
if (init) {
|
|
2001
|
+
attrs.outfilesuffix ??= traits.outfilesuffix
|
|
2002
|
+
} else if (!this.isAttributeLocked('outfilesuffix')) {
|
|
2003
|
+
attrs.outfilesuffix = traits.outfilesuffix
|
|
2004
|
+
}
|
|
2005
|
+
} else {
|
|
2006
|
+
throw new Error(
|
|
2007
|
+
`asciidoctor: FAILED: missing converter for backend '${newBackend}'. Processing aborted.`
|
|
2008
|
+
)
|
|
2009
|
+
}
|
|
2010
|
+
this.converter = converter
|
|
2011
|
+
|
|
2012
|
+
const currentFiletype = attrs.filetype
|
|
2013
|
+
if (currentFiletype) delete attrs[`filetype-${currentFiletype}`]
|
|
2014
|
+
attrs.filetype = newFiletype
|
|
2015
|
+
attrs[`filetype-${newFiletype}`] = ''
|
|
2016
|
+
|
|
2017
|
+
const pageWidth = DEFAULT_PAGE_WIDTHS[newBasebackend]
|
|
2018
|
+
if (pageWidth) {
|
|
2019
|
+
attrs.pagewidth = pageWidth
|
|
2020
|
+
} else {
|
|
2021
|
+
delete attrs.pagewidth
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
if (newBasebackend !== currentBasebackend) {
|
|
2025
|
+
if (currentDoctype) {
|
|
2026
|
+
if (currentBasebackend) {
|
|
2027
|
+
delete attrs[`basebackend-${currentBasebackend}`]
|
|
2028
|
+
delete attrs[
|
|
2029
|
+
`basebackend-${currentBasebackend}-doctype-${currentDoctype}`
|
|
2030
|
+
]
|
|
2031
|
+
}
|
|
2032
|
+
attrs[`basebackend-${newBasebackend}-doctype-${currentDoctype}`] = ''
|
|
2033
|
+
} else if (currentBasebackend) {
|
|
2034
|
+
delete attrs[`basebackend-${currentBasebackend}`]
|
|
2035
|
+
}
|
|
2036
|
+
attrs[`basebackend-${newBasebackend}`] = ''
|
|
2037
|
+
attrs.basebackend = newBasebackend
|
|
2038
|
+
}
|
|
2039
|
+
return newBackend
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Update the doctype and backend attributes to reflect a change in the active doctype.
|
|
2044
|
+
*
|
|
2045
|
+
* @param newDoctype
|
|
2046
|
+
* @returns {undefined|*} the String doctype if updated, nothing otherwise.
|
|
2047
|
+
* @private
|
|
2048
|
+
* @internal
|
|
2049
|
+
*/
|
|
2050
|
+
_updateDoctypeAttributes(newDoctype) {
|
|
2051
|
+
if (!newDoctype || newDoctype === this.doctype) return undefined
|
|
2052
|
+
const currentBackend = this.backend
|
|
2053
|
+
const attrs = this.attributes
|
|
2054
|
+
const currentBasebackend = attrs.basebackend
|
|
2055
|
+
const currentDoctype = this.doctype
|
|
2056
|
+
if (currentDoctype) {
|
|
2057
|
+
delete attrs[`doctype-${currentDoctype}`]
|
|
2058
|
+
if (currentBackend) {
|
|
2059
|
+
delete attrs[`backend-${currentBackend}-doctype-${currentDoctype}`]
|
|
2060
|
+
attrs[`backend-${currentBackend}-doctype-${newDoctype}`] = ''
|
|
2061
|
+
}
|
|
2062
|
+
if (currentBasebackend) {
|
|
2063
|
+
delete attrs[
|
|
2064
|
+
`basebackend-${currentBasebackend}-doctype-${currentDoctype}`
|
|
2065
|
+
]
|
|
2066
|
+
attrs[`basebackend-${currentBasebackend}-doctype-${newDoctype}`] = ''
|
|
2067
|
+
}
|
|
2068
|
+
} else {
|
|
2069
|
+
if (currentBackend)
|
|
2070
|
+
attrs[`backend-${currentBackend}-doctype-${newDoctype}`] = ''
|
|
2071
|
+
if (currentBasebackend)
|
|
2072
|
+
attrs[`basebackend-${currentBasebackend}-doctype-${newDoctype}`] = ''
|
|
2073
|
+
}
|
|
2074
|
+
attrs[`doctype-${newDoctype}`] = ''
|
|
2075
|
+
this.doctype = attrs.doctype = newDoctype
|
|
2076
|
+
return newDoctype
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
2081
|
+
|
|
2082
|
+
function _expandPath(p) {
|
|
2083
|
+
try {
|
|
2084
|
+
return require('node:path').resolve(p)
|
|
2085
|
+
} catch {
|
|
2086
|
+
return p
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function _cwd() {
|
|
2091
|
+
return typeof process !== 'undefined' ? process.cwd() : '/'
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function _pad2(n) {
|
|
2095
|
+
return String(n).padStart(2, '0')
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function _formatDate(d) {
|
|
2099
|
+
return `${d.getFullYear()}-${_pad2(d.getMonth() + 1)}-${_pad2(d.getDate())}`
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function _formatTime(d) {
|
|
2103
|
+
const offset = -d.getTimezoneOffset()
|
|
2104
|
+
const sign = offset >= 0 ? '+' : '-'
|
|
2105
|
+
const abs = Math.abs(offset)
|
|
2106
|
+
const hh = _pad2(Math.floor(abs / 60))
|
|
2107
|
+
const mm = _pad2(abs % 60)
|
|
2108
|
+
return `${_pad2(d.getHours())}:${_pad2(d.getMinutes())}:${_pad2(d.getSeconds())} ${offset === 0 ? 'UTC' : `${sign}${hh}${mm}`}`
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function _limitBytesize(str, max) {
|
|
2112
|
+
const encoded = new TextEncoder().encode(str)
|
|
2113
|
+
if (encoded.length <= max) return str
|
|
2114
|
+
// Walk back from max to find the last complete UTF-8 character boundary.
|
|
2115
|
+
let end = max
|
|
2116
|
+
// Back up past continuation bytes (0x80–0xBF).
|
|
2117
|
+
while (end > 0 && (encoded[end - 1] & 0xc0) === 0x80) end--
|
|
2118
|
+
// If the byte at end-1 is a multibyte start byte, check whether its full
|
|
2119
|
+
// sequence fits within max.
|
|
2120
|
+
if (end > 0 && (encoded[end - 1] & 0x80) !== 0) {
|
|
2121
|
+
const b = encoded[end - 1]
|
|
2122
|
+
const charLen = b >= 0xf0 ? 4 : b >= 0xe0 ? 3 : b >= 0xc0 ? 2 : 1
|
|
2123
|
+
if (end - 1 + charLen > max) {
|
|
2124
|
+
end-- // sequence extends past max → exclude this start byte
|
|
2125
|
+
} else {
|
|
2126
|
+
end = max // sequence fits entirely → restore max
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return new TextDecoder().decode(encoded.slice(0, end))
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
applyLogging(Document.prototype)
|
|
2133
|
+
|
|
2134
|
+
Document.Footnote = Footnote
|