@asciidoctor/core 3.0.4 → 4.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24156 -0
  3. package/build/node/index.cjs +24737 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +55 -97
  6. package/src/abstract_block.js +857 -0
  7. package/src/abstract_node.js +954 -0
  8. package/src/attribute_entry.js +12 -0
  9. package/src/attribute_list.js +380 -0
  10. package/src/block.js +168 -0
  11. package/src/browser/asset.js +22 -0
  12. package/src/browser/reader.js +138 -0
  13. package/src/browser.js +121 -0
  14. package/src/callouts.js +85 -0
  15. package/src/compliance.js +54 -0
  16. package/src/constants.js +665 -0
  17. package/src/convert.js +370 -0
  18. package/src/converter/composite.js +83 -0
  19. package/src/converter/docbook5.js +1031 -0
  20. package/src/converter/html5.js +1893 -0
  21. package/src/converter/manpage.js +935 -0
  22. package/src/converter/template.js +459 -0
  23. package/src/converter.js +478 -0
  24. package/src/data/stylesheet-data.js +2 -0
  25. package/src/document.js +2134 -0
  26. package/src/extensions.js +1952 -0
  27. package/src/footnote.js +28 -0
  28. package/src/helpers.js +355 -0
  29. package/src/index.js +138 -0
  30. package/src/inline.js +158 -0
  31. package/src/list.js +240 -0
  32. package/src/load.js +276 -0
  33. package/src/logging.js +526 -0
  34. package/src/parser.js +3661 -0
  35. package/src/path_resolver.js +472 -0
  36. package/src/reader.js +1755 -0
  37. package/src/rx.js +829 -0
  38. package/src/section.js +354 -0
  39. package/src/stylesheets.js +30 -0
  40. package/src/substitutors.js +2241 -0
  41. package/src/syntaxHighlighter/highlightjs.js +90 -0
  42. package/src/syntaxHighlighter/html_pipeline.js +33 -0
  43. package/src/syntax_highlighter.js +304 -0
  44. package/src/table.js +952 -0
  45. package/src/timings.js +78 -0
  46. package/types/abstract_block.d.ts +346 -0
  47. package/types/abstract_node.d.ts +471 -0
  48. package/types/attribute_entry.d.ts +7 -0
  49. package/types/attribute_list.d.ts +52 -0
  50. package/types/block.d.ts +55 -0
  51. package/types/browser/asset.d.ts +7 -0
  52. package/types/browser/reader.d.ts +29 -0
  53. package/types/callouts.d.ts +36 -0
  54. package/types/compliance.d.ts +23 -0
  55. package/types/constants.d.ts +268 -0
  56. package/types/convert.d.ts +34 -0
  57. package/types/converter/composite.d.ts +20 -0
  58. package/types/converter/docbook5.d.ts +41 -0
  59. package/types/converter/html5.d.ts +51 -0
  60. package/types/converter/manpage.d.ts +59 -0
  61. package/types/converter/template.d.ts +83 -0
  62. package/types/converter.d.ts +150 -0
  63. package/types/data/stylesheet-data.d.ts +2 -0
  64. package/types/document.d.ts +495 -0
  65. package/types/extensions.d.ts +876 -0
  66. package/types/footnote.d.ts +18 -0
  67. package/types/helpers.d.ts +146 -0
  68. package/types/index.d.cts +75 -0
  69. package/types/index.d.ts +73 -3731
  70. package/types/inline.d.ts +69 -0
  71. package/types/list.d.ts +114 -0
  72. package/types/load.d.ts +39 -0
  73. package/types/logging.d.ts +187 -0
  74. package/types/parser.d.ts +114 -0
  75. package/types/path_resolver.d.ts +103 -0
  76. package/types/reader.d.ts +184 -0
  77. package/types/rx.d.ts +513 -0
  78. package/types/section.d.ts +122 -0
  79. package/types/stylesheets.d.ts +10 -0
  80. package/types/substitutors.d.ts +208 -0
  81. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  82. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  83. package/types/syntax_highlighter.d.ts +167 -0
  84. package/types/table.d.ts +231 -0
  85. package/types/timings.d.ts +25 -0
  86. package/types/tsconfig.json +9 -0
  87. package/LICENSE +0 -21
  88. package/dist/browser/asciidoctor.js +0 -47654
  89. package/dist/browser/asciidoctor.min.js +0 -1452
  90. package/dist/graalvm/asciidoctor.js +0 -47402
  91. package/dist/node/asciidoctor.cjs +0 -21567
  92. package/dist/node/asciidoctor.js +0 -23037
package/src/parser.js ADDED
@@ -0,0 +1,3661 @@
1
+ // ESM conversion of parser.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - All methods are static on the Parser class (Ruby class methods).
5
+ // - Ruby Struct BlockMatchData → plain object { context, masq, tip, terminator }.
6
+ // - Ruby's regex captures ($1, $2, …) → JS match array m[1], m[2], …
7
+ // - Ruby .nil_or_empty? → !val (or val == null || val === '')
8
+ // - Ruby .to_i → parseInt(val, 10) (returns 0 for nil/non-numeric)
9
+ // - ListContinuationMarker module → Symbol used for identity checks.
10
+ // - Logging mixin applied via applyLogging().
11
+
12
+ import { applyLogging } from './logging.js'
13
+ import { Block } from './block.js'
14
+ import { Section } from './section.js'
15
+ import { List, ListItem } from './list.js'
16
+ import { Table } from './table.js'
17
+ import { Inline } from './inline.js'
18
+ import { AttributeList } from './attribute_list.js'
19
+ import { AttributeEntry } from './attribute_entry.js'
20
+ import { Reader } from './reader.js'
21
+ import { Compliance } from './compliance.js'
22
+ import { basename, intToRoman, romanToInt } from './helpers.js'
23
+ import {
24
+ ADMONITION_STYLES,
25
+ ADMONITION_STYLE_HEADS,
26
+ PARAGRAPH_STYLES,
27
+ VERBATIM_STYLES,
28
+ DELIMITED_BLOCKS,
29
+ DELIMITED_BLOCK_HEADS,
30
+ DELIMITED_BLOCK_TAILS,
31
+ SETEXT_SECTION_LEVELS,
32
+ LAYOUT_BREAK_CHARS,
33
+ HYBRID_LAYOUT_BREAK_CHARS,
34
+ MARKDOWN_THEMATIC_BREAK_CHARS,
35
+ NESTABLE_LIST_CONTEXTS,
36
+ ORDERED_LIST_STYLES,
37
+ CAPTION_ATTRIBUTE_NAMES,
38
+ STEM_TYPE_ALIASES,
39
+ ATTR_REF_HEAD,
40
+ LIST_CONTINUATION,
41
+ LINE_CONTINUATION,
42
+ LINE_CONTINUATION_LEGACY,
43
+ LF,
44
+ } from './constants.js'
45
+ import {
46
+ BlockAnchorRx,
47
+ BlockAttributeLineRx,
48
+ BlockAttributeListRx,
49
+ BlockTitleRx,
50
+ AtxSectionTitleRx,
51
+ ExtAtxSectionTitleRx,
52
+ SetextSectionTitleRx,
53
+ SectionLevelStyleRx,
54
+ InlineSectionAnchorRx,
55
+ InlineAnchorScanRx,
56
+ ManpageTitleVolnumRx,
57
+ ManpageNamePurposeRx,
58
+ AnyListRx,
59
+ UnorderedListRx,
60
+ OrderedListRx,
61
+ DescriptionListRx,
62
+ DescriptionListSiblingRx,
63
+ CalloutListRx,
64
+ CalloutScanRx,
65
+ InlineBiblioAnchorRx,
66
+ AdmonitionParagraphRx,
67
+ BlockMediaMacroRx,
68
+ BlockTocMacroRx,
69
+ CustomBlockMacroRx,
70
+ ColumnSpecRx,
71
+ CellSpecStartRx,
72
+ CellSpecEndRx,
73
+ AuthorInfoLineRx,
74
+ AuthorDelimiterRx,
75
+ RevisionInfoLineRx,
76
+ AttributeEntryRx,
77
+ MarkdownThematicBreakRx,
78
+ ExtLayoutBreakRx,
79
+ LiteralParagraphRx,
80
+ InvalidAttributeNameCharsRx,
81
+ ListRxMap,
82
+ OrderedListMarkerRxMap,
83
+ LeadingInlineAnchorRx,
84
+ XmlSanitizeRx,
85
+ AttributeEntryPassMacroRx,
86
+ } from './rx.js'
87
+
88
+ // ── List continuation identity marker ────────────────────────────────────────
89
+ class ListContinuation extends String {}
90
+
91
+ function isListContinuation(v) {
92
+ return v instanceof ListContinuation
93
+ }
94
+
95
+ const ListContinuationPlaceholder = new ListContinuation('')
96
+ const ListContinuationString = new ListContinuation(LIST_CONTINUATION)
97
+
98
+ // Author attribute keys
99
+ const AuthorKeys = new Set([
100
+ 'author',
101
+ 'authorinitials',
102
+ 'firstname',
103
+ 'middlename',
104
+ 'lastname',
105
+ 'email',
106
+ ])
107
+
108
+ // Cell alignment and style maps
109
+ const TableCellHorzAlignments = { '<': 'left', '>': 'right', '^': 'center' }
110
+ const TableCellVertAlignments = { '<': 'top', '>': 'bottom', '^': 'middle' }
111
+ const TableCellStyles = {
112
+ d: 'none',
113
+ s: 'strong',
114
+ e: 'emphasis',
115
+ m: 'monospaced',
116
+ h: 'header',
117
+ l: 'literal',
118
+ a: 'asciidoc',
119
+ }
120
+
121
+ // ── Parser ────────────────────────────────────────────────────────────────────
122
+
123
+ export class Parser {
124
+ // Prevent instantiation
125
+ constructor() {
126
+ throw new Error('Parser cannot be instantiated')
127
+ }
128
+
129
+ /**
130
+ * Parse AsciiDoc source from reader into document.
131
+ * @param {Reader} reader
132
+ * @param {Document} document
133
+ * @param {{header_only?: boolean}} [options={}]
134
+ * @returns {Promise<Document>}
135
+ */
136
+ static async parse(reader, document, options = {}) {
137
+ const headerOnly = options.header_only ?? false
138
+ let blockAttributes = await Parser.parseDocumentHeader(
139
+ reader,
140
+ document,
141
+ headerOnly
142
+ )
143
+
144
+ if (!headerOnly) {
145
+ while (await reader.hasMoreLines()) {
146
+ const [newSection, attrs] = await Parser.nextSection(
147
+ reader,
148
+ document,
149
+ blockAttributes
150
+ )
151
+ blockAttributes = attrs
152
+ if (newSection) {
153
+ document.assignNumeral(newSection)
154
+ document.blocks.push(newSection)
155
+ }
156
+ }
157
+ }
158
+ return document
159
+ }
160
+
161
+ /**
162
+ * Parse the document header.
163
+ * @param {Reader} reader
164
+ * @param {Document} document
165
+ * @param {boolean} [headerOnly=false]
166
+ * @returns {Promise<Object>} Block attributes after the header.
167
+ */
168
+ static async parseDocumentHeader(reader, document, headerOnly = false) {
169
+ let blockAttrs =
170
+ (await reader.skipBlankLines()) != null
171
+ ? await Parser.parseBlockMetadataLines(reader, document)
172
+ : {}
173
+ const docAttrs = document.attributes
174
+
175
+ const implicitDoctitle = await Parser.isNextLineDoctitle(
176
+ reader,
177
+ blockAttrs,
178
+ docAttrs.leveloffset
179
+ )
180
+ if (implicitDoctitle && (blockAttrs.title || blockAttrs.style)) {
181
+ docAttrs.authorcount = 0
182
+ return document.finalizeHeader(blockAttrs, false)
183
+ }
184
+
185
+ let doctitleAttrVal = null
186
+ const existingDoctitle = docAttrs.doctitle
187
+ if (existingDoctitle && existingDoctitle !== '') {
188
+ document.title = doctitleAttrVal = existingDoctitle
189
+ }
190
+
191
+ if (implicitDoctitle) {
192
+ const sourceLocation = document.sourcemap ? reader.cursor : null
193
+ const [_sectId, , l0SectionTitle, , atx] = await Parser.parseSectionTitle(
194
+ reader,
195
+ document
196
+ )
197
+ let finalSectTitle = l0SectionTitle
198
+
199
+ if (doctitleAttrVal) {
200
+ finalSectTitle = null
201
+ } else {
202
+ document.title = finalSectTitle
203
+ let sanitized = document.subSpecialchars(finalSectTitle)
204
+ if (sanitized.includes(ATTR_REF_HEAD)) {
205
+ sanitized = document.subAttributes(sanitized, {
206
+ attribute_missing: 'skip',
207
+ })
208
+ }
209
+ docAttrs.doctitle = doctitleAttrVal = sanitized
210
+ }
211
+
212
+ if (sourceLocation && document.header) {
213
+ document.header.sourceLocation = sourceLocation
214
+ }
215
+
216
+ if (!atx && !document.isAttributeLocked('compat-mode')) {
217
+ docAttrs['compat-mode'] = ''
218
+ }
219
+ if (
220
+ blockAttrs.separator &&
221
+ !document.isAttributeLocked('title-separator')
222
+ ) {
223
+ docAttrs['title-separator'] = blockAttrs.separator
224
+ }
225
+ const docId = blockAttrs.id
226
+ if (docId) {
227
+ document.id = docId
228
+ }
229
+ if (blockAttrs.role) docAttrs.role = blockAttrs.role
230
+ if (blockAttrs.reftext) docAttrs.reftext = blockAttrs.reftext
231
+ blockAttrs = {}
232
+
233
+ const modifiedAttrs = document._attributesModified
234
+ modifiedAttrs.delete('doctitle')
235
+ await Parser.parseHeaderMetadata(reader, document, null)
236
+
237
+ if (modifiedAttrs.has('doctitle')) {
238
+ const val = docAttrs.doctitle
239
+ if (!val || val === '' || val === doctitleAttrVal) {
240
+ docAttrs.doctitle = doctitleAttrVal
241
+ } else {
242
+ document.title = val
243
+ }
244
+ } else if (!finalSectTitle) {
245
+ modifiedAttrs.add('doctitle')
246
+ }
247
+
248
+ if (docId) document.register('refs', [docId, document])
249
+ } else if (docAttrs.author) {
250
+ const authorMeta = Parser.processAuthors(docAttrs.author, true, false)
251
+ if (docAttrs.authorinitials) delete authorMeta.authorinitials
252
+ Object.assign(docAttrs, authorMeta)
253
+ } else if (docAttrs.authors) {
254
+ const authorMeta = Parser.processAuthors(docAttrs.authors, true)
255
+ Object.assign(docAttrs, authorMeta)
256
+ } else {
257
+ docAttrs.authorcount = 0
258
+ }
259
+
260
+ if (document.doctype === 'manpage') {
261
+ await Parser.parseManpageHeader(reader, document, blockAttrs, headerOnly)
262
+ }
263
+
264
+ return document.finalizeHeader(blockAttrs)
265
+ }
266
+
267
+ /**
268
+ * Parse manpage header.
269
+ * @param {Reader} reader
270
+ * @param {Document} document
271
+ * @param {Object} blockAttributes
272
+ * @param {boolean} [headerOnly=false]
273
+ * @returns {Promise<void>}
274
+ */
275
+ static async parseManpageHeader(
276
+ reader,
277
+ document,
278
+ blockAttributes,
279
+ headerOnly = false
280
+ ) {
281
+ const docAttrs = document.attributes
282
+ const doctitle = docAttrs.doctitle || ''
283
+ const m = doctitle.match(ManpageTitleVolnumRx)
284
+ let manvolnum
285
+ if (m) {
286
+ manvolnum = docAttrs.manvolnum = m[2]
287
+ let mantitle = m[1]
288
+ if (mantitle.includes(ATTR_REF_HEAD))
289
+ mantitle = document.subAttributes(mantitle)
290
+ docAttrs.mantitle = mantitle.toLowerCase()
291
+ } else {
292
+ Parser.logger.error(
293
+ Parser.messageWithContext('non-conforming manpage title', {
294
+ source_location: reader.cursorAtLine(1),
295
+ })
296
+ )
297
+ docAttrs.mantitle = doctitle || docAttrs.docname || 'command'
298
+ manvolnum = docAttrs.manvolnum = '1'
299
+ }
300
+
301
+ const manname = docAttrs.manname
302
+ if (manname && docAttrs.manpurpose) {
303
+ docAttrs['manname-title'] ??= 'Name'
304
+ docAttrs.mannames = [manname]
305
+ if (document.backend === 'manpage') {
306
+ docAttrs.docname = manname
307
+ docAttrs.outfilesuffix = `.${manvolnum}`
308
+ }
309
+ } else if (headerOnly) {
310
+ // done
311
+ } else {
312
+ await reader.skipBlankLines()
313
+ reader.save()
314
+ Object.assign(
315
+ blockAttributes,
316
+ await Parser.parseBlockMetadataLines(reader, document)
317
+ )
318
+ const nameSectionLevel = await Parser.isNextLineSection(reader, {})
319
+ if (nameSectionLevel !== null && nameSectionLevel !== undefined) {
320
+ if (nameSectionLevel === 1) {
321
+ const nameSection = await Parser.initializeSection(
322
+ reader,
323
+ document,
324
+ {}
325
+ )
326
+ const buffer = (
327
+ await reader.readLinesUntil({
328
+ break_on_blank_lines: true,
329
+ skip_line_comments: true,
330
+ })
331
+ )
332
+ .map((l) => l.trimStart())
333
+ .join(' ')
334
+ const nm = buffer.match(ManpageNamePurposeRx)
335
+ let errorMsg = null
336
+ if (nm) {
337
+ let mname = nm[1]
338
+ if (mname.includes(ATTR_REF_HEAD))
339
+ mname = document.subAttributes(mname)
340
+ let mannames
341
+ if (mname.includes(',')) {
342
+ mannames = mname.split(',').map((n) => n.trimStart())
343
+ mname = mannames[0]
344
+ } else {
345
+ mannames = [mname]
346
+ }
347
+ let manpurpose = nm[2]
348
+ if (manpurpose.includes(ATTR_REF_HEAD))
349
+ manpurpose = document.subAttributes(manpurpose)
350
+ docAttrs['manname-title'] ??= nameSection.title
351
+ if (nameSection.id) docAttrs['manname-id'] = nameSection.id
352
+ docAttrs.manname = mname
353
+ docAttrs.mannames = mannames
354
+ docAttrs.manpurpose = manpurpose
355
+ if (document.backend === 'manpage') {
356
+ docAttrs.docname = mname
357
+ docAttrs.outfilesuffix = `.${manvolnum}`
358
+ }
359
+ } else {
360
+ errorMsg = 'non-conforming name section body'
361
+ }
362
+ if (errorMsg) {
363
+ reader.restoreSave()
364
+ Parser.logger.error(
365
+ Parser.messageWithContext(errorMsg, {
366
+ source_location: reader.cursor,
367
+ })
368
+ )
369
+ const mn = docAttrs.docname || 'command'
370
+ docAttrs.manname = mn
371
+ docAttrs.mannames = [mn]
372
+ if (document.backend === 'manpage') {
373
+ docAttrs.docname = mn
374
+ docAttrs.outfilesuffix = `.${manvolnum}`
375
+ }
376
+ } else {
377
+ reader.discardSave()
378
+ }
379
+ } else {
380
+ reader.restoreSave()
381
+ Parser.logger.error(
382
+ Parser.messageWithContext('name section must be at level 1', {
383
+ source_location: reader.cursor,
384
+ })
385
+ )
386
+ }
387
+ } else {
388
+ reader.restoreSave()
389
+ Parser.logger.error(
390
+ Parser.messageWithContext('name section expected', {
391
+ source_location: reader.cursor,
392
+ })
393
+ )
394
+ const mn = docAttrs.docname || 'command'
395
+ docAttrs.manname = mn
396
+ docAttrs.mannames = [mn]
397
+ if (document.backend === 'manpage') {
398
+ docAttrs.docname = mn
399
+ docAttrs.outfilesuffix = `.${manvolnum}`
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Return the next section from the reader.
407
+ * @param {Reader} reader
408
+ * @param {Document|Section} parent
409
+ * @param {Object} [attributes={}]
410
+ * @returns {Promise<[Section|null, Object]>} Tuple of the new section (or null) and orphaned attributes.
411
+ */
412
+ static async nextSection(reader, parent, attributes = {}) {
413
+ let preamble = null,
414
+ intro = null,
415
+ part = false
416
+
417
+ const parentIsDocument = parent.context === 'document'
418
+ let section, currentLevel, expectedNextLevel, expectedNextLevelAlt
419
+ let book, document
420
+
421
+ if (
422
+ parentIsDocument &&
423
+ parent.blocks.length === 0 &&
424
+ (parent.hasHeader() ||
425
+ ('invalid-header' in attributes &&
426
+ !!attributes['invalid-header'] &&
427
+ delete attributes['invalid-header'] !== undefined) ||
428
+ typeof (await Parser.isNextLineSection(reader, attributes)) !==
429
+ 'number')
430
+ ) {
431
+ // We are at the start of document processing
432
+ document = parent
433
+ book = document.doctype === 'book'
434
+ if (parent.hasHeader() || (book && attributes[1] !== 'abstract')) {
435
+ preamble = intro = new Block(parent, 'preamble', {
436
+ content_model: 'compound',
437
+ })
438
+ if (book && parent.hasAttribute('preface-title')) {
439
+ preamble.title = parent.getAttribute('preface-title')
440
+ }
441
+ parent.blocks.push(preamble)
442
+ }
443
+ section = parent
444
+ currentLevel = 0
445
+ if ('fragment' in parent.attributes) {
446
+ expectedNextLevel = -1
447
+ } else if (book) {
448
+ expectedNextLevel = 1
449
+ expectedNextLevelAlt = 0
450
+ } else {
451
+ expectedNextLevel = 1
452
+ }
453
+ } else {
454
+ document = parent.document
455
+ book = document.doctype === 'book'
456
+ section = await Parser.initializeSection(reader, parent, attributes)
457
+ const title = attributes.title
458
+ attributes = title ? { title } : {}
459
+ currentLevel = section.level
460
+ expectedNextLevel = currentLevel + 1
461
+ if (currentLevel === 0) {
462
+ part = book
463
+ } else if (currentLevel === 1 && section.special) {
464
+ const sn = section.sectname
465
+ if (sn !== 'appendix' && sn !== 'preface' && sn !== 'abstract') {
466
+ expectedNextLevel = null
467
+ }
468
+ }
469
+ }
470
+
471
+ await reader.skipBlankLines()
472
+
473
+ while (await reader.hasMoreLines()) {
474
+ await Parser.parseBlockMetadataLines(reader, document, attributes)
475
+ let nextLevel = await Parser.isNextLineSection(reader, attributes)
476
+
477
+ if (
478
+ nextLevel !== null &&
479
+ nextLevel !== undefined &&
480
+ nextLevel !== false
481
+ ) {
482
+ const leveloffset = document.getAttribute('leveloffset')
483
+ if (leveloffset) {
484
+ nextLevel += parseInt(leveloffset, 10)
485
+ if (nextLevel < 0) nextLevel = 0
486
+ }
487
+
488
+ if (nextLevel > currentLevel) {
489
+ if (expectedNextLevel != null) {
490
+ if (
491
+ nextLevel !== expectedNextLevel &&
492
+ !(
493
+ expectedNextLevelAlt != null &&
494
+ nextLevel === expectedNextLevelAlt
495
+ ) &&
496
+ expectedNextLevel >= 0
497
+ ) {
498
+ const expectedCondition =
499
+ expectedNextLevelAlt != null
500
+ ? `expected levels ${expectedNextLevelAlt} or ${expectedNextLevel}`
501
+ : `expected level ${expectedNextLevel}`
502
+ Parser.logger.warn(
503
+ Parser.messageWithContext(
504
+ `section title out of sequence: ${expectedCondition}, got level ${nextLevel}`,
505
+ { source_location: reader.cursor }
506
+ )
507
+ )
508
+ }
509
+ } else {
510
+ Parser.logger.error(
511
+ Parser.messageWithContext(
512
+ `${section.sectname} sections do not support nested sections`,
513
+ { source_location: reader.cursor }
514
+ )
515
+ )
516
+ }
517
+ const [newSection, attrs] = await Parser.nextSection(
518
+ reader,
519
+ section,
520
+ attributes
521
+ )
522
+ attributes = attrs
523
+ section.assignNumeral(newSection)
524
+ section.blocks.push(newSection)
525
+ } else if (nextLevel === 0 && section === document) {
526
+ if (!book) {
527
+ Parser.logger.error(
528
+ Parser.messageWithContext(
529
+ 'level 0 sections can only be used when doctype is book',
530
+ { source_location: reader.cursor }
531
+ )
532
+ )
533
+ }
534
+ const [newSection, attrs] = await Parser.nextSection(
535
+ reader,
536
+ section,
537
+ attributes
538
+ )
539
+ attributes = attrs
540
+ section.assignNumeral(newSection)
541
+ section.blocks.push(newSection)
542
+ } else {
543
+ break
544
+ }
545
+ } else {
546
+ const blockCursor = reader.cursor
547
+ const newBlock = await Parser.nextBlock(
548
+ reader,
549
+ intro ?? section,
550
+ attributes,
551
+ { parse_metadata: false }
552
+ )
553
+ if (newBlock) {
554
+ if (part) {
555
+ if (!section.hasBlocks()) {
556
+ if (newBlock.style !== 'partintro') {
557
+ if (newBlock.style === 'open' && newBlock.context === 'open') {
558
+ newBlock.style = 'partintro'
559
+ } else {
560
+ newBlock.parent = intro = new Block(section, 'open', {
561
+ content_model: 'compound',
562
+ })
563
+ intro.style = 'partintro'
564
+ section.blocks.push(intro)
565
+ }
566
+ } else if (newBlock.contentModel === 'simple') {
567
+ newBlock.contentModel = 'compound'
568
+ newBlock.append(
569
+ new Block(newBlock, 'paragraph', {
570
+ source: newBlock.lines,
571
+ subs: newBlock.subs,
572
+ })
573
+ )
574
+ newBlock.lines.length = 0
575
+ newBlock.subs.length = 0
576
+ }
577
+ } else if (section.blocks.length === 1) {
578
+ const firstBlock = section.blocks[0]
579
+ if (!intro && firstBlock.contentModel === 'compound') {
580
+ Parser.logger.error(
581
+ Parser.messageWithContext(
582
+ 'illegal block content outside of partintro block',
583
+ { source_location: blockCursor }
584
+ )
585
+ )
586
+ } else if (firstBlock.contentModel !== 'compound') {
587
+ newBlock.parent = intro = new Block(section, 'open', {
588
+ content_model: 'compound',
589
+ })
590
+ if (firstBlock.style === (intro.style = 'partintro')) {
591
+ firstBlock.context = 'paragraph'
592
+ firstBlock.style = null
593
+ }
594
+ section.blocks.shift()
595
+ intro.append(firstBlock)
596
+ section.blocks.push(intro)
597
+ }
598
+ }
599
+ }
600
+ ;(intro ?? section).blocks.push(newBlock)
601
+ for (const key of Object.keys(attributes)) delete attributes[key]
602
+ }
603
+ }
604
+
605
+ if ((await reader.skipBlankLines()) == null) break
606
+ }
607
+
608
+ if (part) {
609
+ if (
610
+ !section.hasBlocks() ||
611
+ section.blocks[section.blocks.length - 1].context !== 'section'
612
+ ) {
613
+ Parser.logger.error(
614
+ Parser.messageWithContext(
615
+ 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)',
616
+ { source_location: reader.cursor }
617
+ )
618
+ )
619
+ }
620
+ } else if (preamble) {
621
+ if (preamble.hasBlocks()) {
622
+ if (
623
+ book ||
624
+ document.blocks[1] ||
625
+ !Compliance.unwrapStandalonePreamble
626
+ ) {
627
+ if (document.sourcemap)
628
+ preamble.sourceLocation = preamble.blocks[0].sourceLocation
629
+ } else {
630
+ document.blocks.shift()
631
+ while (preamble.blocks.length > 0) {
632
+ document.append(preamble.blocks.shift())
633
+ }
634
+ }
635
+ } else {
636
+ document.blocks.shift()
637
+ }
638
+ }
639
+
640
+ return [section === parent ? null : section, { ...attributes }]
641
+ }
642
+
643
+ /**
644
+ * Parse and return the next Block at the Reader's current location.
645
+ * @param {Reader} reader
646
+ * @param {AbstractBlock} parent
647
+ * @param {Object} [attributes={}]
648
+ * @param {Object} [options={}]
649
+ * @returns {Promise<Block|null>}
650
+ */
651
+ static async nextBlock(reader, parent, attributes = {}, options = {}) {
652
+ const skipped = await reader.skipBlankLines()
653
+ if (skipped == null) return null
654
+
655
+ let textOnly = options.text_only ?? null
656
+ if (textOnly && skipped > 0) {
657
+ delete options.text_only
658
+ textOnly = null
659
+ }
660
+
661
+ const document = parent.document
662
+ const parseMetadata = options.parse_metadata !== false
663
+
664
+ if (parseMetadata) {
665
+ while (
666
+ await Parser.parseBlockMetadataLine(
667
+ reader,
668
+ document,
669
+ attributes,
670
+ options
671
+ )
672
+ ) {
673
+ await reader.readLine()
674
+ if ((await reader.skipBlankLines()) == null) return null
675
+ }
676
+ }
677
+
678
+ const extensions = document.extensions
679
+ const blockExtensions = extensions?.hasBlocks?.()
680
+ const blockMacroExtensions = extensions?.hasBlockMacros?.()
681
+
682
+ reader.mark()
683
+ const thisLine = await reader.readLine()
684
+ if (thisLine === undefined) return null
685
+ const docAttrs = document.attributes
686
+ const style = attributes[1] ?? null
687
+ let block = null,
688
+ blockContext = null,
689
+ cloakedContext = null,
690
+ terminator = null
691
+
692
+ const delimitedBlock = Parser.isDelimitedBlock(thisLine, true)
693
+ if (delimitedBlock) {
694
+ blockContext = cloakedContext = delimitedBlock.context
695
+ terminator = delimitedBlock.terminator
696
+ if (style) {
697
+ if (style !== blockContext) {
698
+ if (delimitedBlock.masq.has(style)) {
699
+ blockContext = style
700
+ } else if (
701
+ delimitedBlock.masq.has('admonition') &&
702
+ ADMONITION_STYLES.has(style)
703
+ ) {
704
+ blockContext = 'admonition'
705
+ } else if (
706
+ blockExtensions &&
707
+ extensions.registeredForBlock(style, blockContext)
708
+ ) {
709
+ blockContext = style
710
+ } else {
711
+ // unknown style; revert to block context
712
+ if (Parser.logger.isDebug())
713
+ Parser.logger.debug(
714
+ Parser.messageWithContext(
715
+ `unknown style for ${blockContext} block: ${style}`,
716
+ { source_location: reader.cursor }
717
+ )
718
+ )
719
+ }
720
+ }
721
+ } else {
722
+ attributes.style = blockContext
723
+ }
724
+ }
725
+
726
+ if (!delimitedBlock) {
727
+ // Processed once (break used for flow control)
728
+ do {
729
+ // Verbatim style shortcut
730
+ if (
731
+ style &&
732
+ Compliance.strictVerbatimParagraphs &&
733
+ VERBATIM_STYLES.has(style)
734
+ ) {
735
+ blockContext = style
736
+ cloakedContext = 'paragraph'
737
+ reader.unshiftLine(thisLine)
738
+ break
739
+ }
740
+
741
+ let indented, ch0
742
+ const mdSyntax = Compliance.markdownSyntax
743
+
744
+ if (thisLine.startsWith(' ')) {
745
+ indented = true
746
+ ch0 = ' '
747
+ if (mdSyntax) {
748
+ const stripped = thisLine.trimStart()
749
+ const firstChar = stripped[0]
750
+ if (
751
+ MARKDOWN_THEMATIC_BREAK_CHARS[firstChar] &&
752
+ MarkdownThematicBreakRx.test(thisLine)
753
+ ) {
754
+ block = new Block(parent, 'thematic_break', {
755
+ content_model: 'empty',
756
+ })
757
+ break
758
+ }
759
+ }
760
+ } else if (thisLine.startsWith('\t')) {
761
+ indented = true
762
+ ch0 = '\t'
763
+ } else {
764
+ indented = false
765
+ ch0 = thisLine[0]
766
+ const layoutBreakChars = mdSyntax
767
+ ? HYBRID_LAYOUT_BREAK_CHARS
768
+ : LAYOUT_BREAK_CHARS
769
+
770
+ if (!textOnly && layoutBreakChars[ch0]) {
771
+ const ll = thisLine.length
772
+ if (
773
+ mdSyntax
774
+ ? ExtLayoutBreakRx.test(thisLine)
775
+ : _uniform(thisLine, ch0, ll) && ll > 2
776
+ ) {
777
+ block = new Block(parent, layoutBreakChars[ch0], {
778
+ content_model: 'empty',
779
+ })
780
+ break
781
+ }
782
+ }
783
+
784
+ if (thisLine.endsWith(']') && thisLine.includes('::')) {
785
+ // Block macro check
786
+ if (
787
+ ch0 === 'i' ||
788
+ thisLine.startsWith('video:') ||
789
+ thisLine.startsWith('audio:')
790
+ ) {
791
+ const mm = thisLine.match(BlockMediaMacroRx)
792
+ if (mm) {
793
+ const [, blkCtxStr, target0, blkAttrsStr] = mm
794
+ const blkCtx = blkCtxStr
795
+ block = new Block(parent, blkCtx, { content_model: 'empty' })
796
+ let target = target0
797
+ if (blkAttrsStr) {
798
+ let posattrs = []
799
+ if (blkCtx === 'video')
800
+ posattrs = ['poster', 'width', 'height']
801
+ else if (blkCtx === 'image')
802
+ posattrs = ['alt', 'width', 'height']
803
+ await block.parseAttributes(blkAttrsStr, posattrs, {
804
+ sub_input: true,
805
+ into: attributes,
806
+ })
807
+ }
808
+ delete attributes.style
809
+ if (target.includes(ATTR_REF_HEAD)) {
810
+ const expanded = block.subAttributes(target, {
811
+ returnDropSentinel: true,
812
+ })
813
+ if (expanded === null) {
814
+ // A missing attribute triggered drop-line; blank attributes (e.g. {blank})
815
+ // that resolve to '' are kept (expanded !== null for those).
816
+ for (const k of Object.keys(attributes))
817
+ delete attributes[k]
818
+ return null
819
+ }
820
+ target = expanded
821
+ }
822
+ if (blkCtx === 'image') {
823
+ document.register('images', target)
824
+ attributes.imagesdir ??= docAttrs.imagesdir
825
+ attributes.alt ??=
826
+ style ??
827
+ (attributes['default-alt'] = basename(target, true).replace(
828
+ /[_-]/g,
829
+ ' '
830
+ ))
831
+ let scaledwidth = attributes.scaledwidth
832
+ if (scaledwidth) {
833
+ delete attributes.scaledwidth
834
+ if (!scaledwidth.match(/\D/)) scaledwidth += '%'
835
+ attributes.scaledwidth = scaledwidth
836
+ }
837
+ if (attributes.title) {
838
+ block.title = attributes.title
839
+ delete attributes.title
840
+ block.assignCaption(attributes.caption, 'figure')
841
+ delete attributes.caption
842
+ }
843
+ }
844
+ attributes.target = target
845
+ break
846
+ }
847
+ }
848
+
849
+ if (ch0 === 't' && thisLine.startsWith('toc:')) {
850
+ const tocm = thisLine.match(BlockTocMacroRx)
851
+ if (tocm) {
852
+ block = new Block(parent, 'toc', { content_model: 'empty' })
853
+ if (tocm[1])
854
+ await block.parseAttributes(tocm[1], [], {
855
+ sub_input: true,
856
+ into: attributes,
857
+ })
858
+ break
859
+ }
860
+ }
861
+
862
+ if (blockMacroExtensions) {
863
+ const cbm = thisLine.match(CustomBlockMacroRx)
864
+ if (cbm) {
865
+ const extension = extensions.registeredForBlockMacro(cbm[1])
866
+ if (extension) {
867
+ let target = cbm[2]
868
+ const content = cbm[3]
869
+ if (target.includes(ATTR_REF_HEAD)) {
870
+ const expanded = parent.subAttributes(target, {
871
+ returnDropSentinel: true,
872
+ })
873
+ if (expanded === null) {
874
+ for (const k of Object.keys(attributes))
875
+ delete attributes[k]
876
+ return null
877
+ }
878
+ target = expanded
879
+ }
880
+ const extConfig = extension.config
881
+ if (extConfig.content_model === 'attributes') {
882
+ if (content)
883
+ await document.parseAttributes(
884
+ content,
885
+ extConfig.positional_attrs ?? extConfig.pos_attrs ?? [],
886
+ { sub_input: true, into: attributes }
887
+ )
888
+ } else {
889
+ attributes.text = content ?? ''
890
+ }
891
+ if (extConfig.default_attrs) {
892
+ for (const [k, v] of Object.entries(
893
+ extConfig.default_attrs
894
+ )) {
895
+ attributes[k] ??= v
896
+ }
897
+ }
898
+ const result = await extension.processMethod(
899
+ parent,
900
+ target,
901
+ attributes
902
+ )
903
+ if (result && result !== parent) {
904
+ Object.assign(attributes, result.attributes)
905
+ block = result
906
+ break
907
+ }
908
+ for (const k of Object.keys(attributes)) delete attributes[k]
909
+ return null
910
+ }
911
+ }
912
+ }
913
+ }
914
+ }
915
+
916
+ if (!indented && (ch0 ?? thisLine[0]) === '<') {
917
+ const clm = thisLine.match(CalloutListRx)
918
+ if (clm) {
919
+ reader.unshiftLine(thisLine)
920
+ block = await Parser.parseCalloutList(
921
+ reader,
922
+ clm,
923
+ parent,
924
+ document.callouts
925
+ )
926
+ attributes.style = 'arabic'
927
+ break
928
+ }
929
+ }
930
+
931
+ if (UnorderedListRx.test(thisLine)) {
932
+ reader.unshiftLine(thisLine)
933
+ if (
934
+ !style &&
935
+ parent instanceof Section &&
936
+ parent.sectname === 'bibliography'
937
+ ) {
938
+ attributes.style = 'bibliography'
939
+ }
940
+ block = await Parser.parseList(
941
+ reader,
942
+ 'ulist',
943
+ parent,
944
+ style ?? attributes.style ?? null
945
+ )
946
+ break
947
+ }
948
+
949
+ if (OrderedListRx.test(thisLine)) {
950
+ reader.unshiftLine(thisLine)
951
+ const start = 'start' in attributes ? attributes.start : null
952
+ delete attributes.start
953
+ block = await Parser.parseList(reader, 'olist', parent, style, {
954
+ start,
955
+ })
956
+ if (block.style) attributes.style = block.style
957
+ break
958
+ }
959
+
960
+ if (thisLine.includes('::') || thisLine.includes(';;')) {
961
+ const dlm = thisLine.match(DescriptionListRx)
962
+ if (dlm) {
963
+ reader.unshiftLine(thisLine)
964
+ block = await Parser.parseDescriptionList(reader, dlm, parent)
965
+ break
966
+ }
967
+ }
968
+
969
+ if (
970
+ (style === 'float' || style === 'discrete') &&
971
+ (Compliance.underlineStyleSectionTitles
972
+ ? Parser.isSectionTitle(thisLine, await reader.peekLine()) != null
973
+ : !indented && Parser.atxSectionTitle(thisLine) != null)
974
+ ) {
975
+ reader.unshiftLine(thisLine)
976
+ const [floatId, floatReftext, blockTitle, floatLevel] =
977
+ await Parser.parseSectionTitle(reader, document, attributes.id)
978
+ if (floatReftext) attributes.reftext = floatReftext
979
+ block = new Block(parent, 'floating_title', {
980
+ content_model: 'empty',
981
+ })
982
+ block.title = blockTitle
983
+ delete attributes.title
984
+ // Force title resolution while in scope to capture current attribute values (Ruby: parser.rb ~line 939)
985
+ if (blockTitle.includes(ATTR_REF_HEAD)) await block.precomputeTitle()
986
+ if (floatId) {
987
+ block.id = floatId
988
+ } else if ('sectids' in docAttrs) {
989
+ await block.precomputeTitle()
990
+ block.id = Section.generateId(block.title, document)
991
+ }
992
+ block.level = floatLevel
993
+ break
994
+ }
995
+
996
+ if (style && style !== 'normal') {
997
+ if (PARAGRAPH_STYLES.has(style)) {
998
+ blockContext = style
999
+ cloakedContext = 'paragraph'
1000
+ reader.unshiftLine(thisLine)
1001
+ break
1002
+ }
1003
+ if (ADMONITION_STYLES.has(style)) {
1004
+ blockContext = 'admonition'
1005
+ cloakedContext = 'paragraph'
1006
+ reader.unshiftLine(thisLine)
1007
+ break
1008
+ }
1009
+ if (
1010
+ blockExtensions &&
1011
+ extensions.registeredForBlock(style, 'paragraph')
1012
+ ) {
1013
+ blockContext = style
1014
+ cloakedContext = 'paragraph'
1015
+ reader.unshiftLine(thisLine)
1016
+ break
1017
+ }
1018
+ // unknown style; fall through
1019
+ if (style && Parser.logger.isDebug())
1020
+ Parser.logger.debug(
1021
+ Parser.messageWithContext(
1022
+ `unknown style for paragraph: ${style}`,
1023
+ { source_location: reader.cursor }
1024
+ )
1025
+ )
1026
+ }
1027
+
1028
+ reader.unshiftLine(thisLine)
1029
+
1030
+ if (indented && !style) {
1031
+ const contentAdjacent = skipped === 0 ? options.list_type : null
1032
+ const lines = await Parser.readParagraphLines(
1033
+ reader,
1034
+ contentAdjacent,
1035
+ { skip_line_comments: !!textOnly }
1036
+ )
1037
+ Parser.adjustIndentation(lines)
1038
+ if (textOnly || contentAdjacent === 'dlist') {
1039
+ block = new Block(parent, 'paragraph', {
1040
+ content_model: 'simple',
1041
+ source: lines,
1042
+ attributes,
1043
+ })
1044
+ } else {
1045
+ block = new Block(parent, 'literal', {
1046
+ content_model: 'verbatim',
1047
+ source: lines,
1048
+ attributes,
1049
+ })
1050
+ }
1051
+ } else {
1052
+ const lines = await Parser.readParagraphLines(
1053
+ reader,
1054
+ skipped === 0 && options.list_type,
1055
+ { skip_line_comments: true }
1056
+ )
1057
+ if (textOnly) {
1058
+ if (indented && style === 'normal') Parser.adjustIndentation(lines)
1059
+ block = new Block(parent, 'paragraph', {
1060
+ content_model: 'simple',
1061
+ source: lines,
1062
+ attributes,
1063
+ })
1064
+ } else if (
1065
+ ADMONITION_STYLE_HEADS.has(ch0) &&
1066
+ thisLine.includes(':')
1067
+ ) {
1068
+ const am = thisLine.match(AdmonitionParagraphRx)
1069
+ if (am) {
1070
+ lines[0] = thisLine.slice(am[0].length)
1071
+ const admName = am[1].toLowerCase()
1072
+ attributes.name = admName
1073
+ attributes.style = am[1]
1074
+ attributes.textlabel =
1075
+ attributes.caption ?? docAttrs[`${admName}-caption`]
1076
+ delete attributes.caption
1077
+ block = new Block(parent, 'admonition', {
1078
+ content_model: 'simple',
1079
+ source: lines,
1080
+ attributes,
1081
+ })
1082
+ } else {
1083
+ if (indented && style === 'normal')
1084
+ Parser.adjustIndentation(lines)
1085
+ block = new Block(parent, 'paragraph', {
1086
+ content_model: 'simple',
1087
+ source: lines,
1088
+ attributes,
1089
+ })
1090
+ }
1091
+ } else if (mdSyntax && ch0 === '>' && thisLine.startsWith('> ')) {
1092
+ const mapped = lines.map((line) => {
1093
+ if (line === '>') return line.slice(1)
1094
+ if (line.startsWith('> ')) return line.slice(2)
1095
+ return line
1096
+ })
1097
+ let creditLine = null
1098
+ if (mapped[mapped.length - 1]?.startsWith('-- ')) {
1099
+ creditLine = mapped.pop().slice(3)
1100
+ while (mapped.length > 0 && mapped[mapped.length - 1] === '')
1101
+ mapped.pop()
1102
+ }
1103
+ attributes.style = 'quote'
1104
+ const Rdr = Reader
1105
+ block = await Parser.buildBlock(
1106
+ 'quote',
1107
+ 'compound',
1108
+ false,
1109
+ parent,
1110
+ new Rdr(mapped),
1111
+ attributes
1112
+ )
1113
+ if (creditLine) {
1114
+ const subsApplied = await block.applySubs(creditLine, [
1115
+ 'specialcharacters',
1116
+ 'quotes',
1117
+ 'attributes',
1118
+ 'replacements',
1119
+ 'macros',
1120
+ 'post_replacements',
1121
+ ])
1122
+ const commaIdx = subsApplied.indexOf(', ')
1123
+ const attribution =
1124
+ commaIdx !== -1 ? subsApplied.slice(0, commaIdx) : subsApplied
1125
+ const citetitle =
1126
+ commaIdx !== -1 ? subsApplied.slice(commaIdx + 2) : null
1127
+ if (attribution) attributes.attribution = attribution
1128
+ if (citetitle) attributes.citetitle = citetitle
1129
+ }
1130
+ } else if (
1131
+ ch0 === '"' &&
1132
+ lines.length > 1 &&
1133
+ lines[lines.length - 1].startsWith('-- ') &&
1134
+ lines[lines.length - 2].endsWith('"')
1135
+ ) {
1136
+ lines[0] = thisLine.slice(1)
1137
+ const cred = lines.pop().slice(3)
1138
+ while (lines.length > 0 && lines[lines.length - 1] === '')
1139
+ lines.pop()
1140
+ lines[lines.length - 1] = lines[lines.length - 1].slice(0, -1)
1141
+ attributes.style = 'quote'
1142
+ block = new Block(parent, 'quote', {
1143
+ content_model: 'simple',
1144
+ source: lines,
1145
+ attributes,
1146
+ })
1147
+ const subsApplied = await block.applySubs(cred, [
1148
+ 'specialcharacters',
1149
+ 'quotes',
1150
+ 'attributes',
1151
+ 'replacements',
1152
+ 'macros',
1153
+ 'post_replacements',
1154
+ ])
1155
+ const commaIdx = subsApplied.indexOf(', ')
1156
+ const attribution =
1157
+ commaIdx !== -1 ? subsApplied.slice(0, commaIdx) : subsApplied
1158
+ const citetitle =
1159
+ commaIdx !== -1 ? subsApplied.slice(commaIdx + 2) : null
1160
+ if (attribution) attributes.attribution = attribution
1161
+ if (citetitle) attributes.citetitle = citetitle
1162
+ } else {
1163
+ if (indented && style === 'normal') Parser.adjustIndentation(lines)
1164
+ block = new Block(parent, 'paragraph', {
1165
+ content_model: 'simple',
1166
+ source: lines,
1167
+ attributes,
1168
+ })
1169
+ }
1170
+ Parser.catalogInlineAnchors(lines.join(LF), block, document, reader)
1171
+ }
1172
+ } while (false)
1173
+ }
1174
+
1175
+ // Delimited block or styled paragraph
1176
+ if (!block) {
1177
+ switch (blockContext) {
1178
+ case 'listing':
1179
+ case 'source': {
1180
+ const lang =
1181
+ blockContext !== 'source' && !attributes[1]
1182
+ ? (attributes[2] ?? docAttrs['source-language'])
1183
+ : null
1184
+ if (lang) {
1185
+ attributes.style = 'source'
1186
+ attributes.language = lang
1187
+ AttributeList.rekey(attributes, [null, null, 'linenums'])
1188
+ } else if (blockContext === 'source') {
1189
+ AttributeList.rekey(attributes, [null, 'language', 'linenums'])
1190
+ if ('source-language' in docAttrs && !('language' in attributes)) {
1191
+ attributes.language = docAttrs['source-language']
1192
+ }
1193
+ if (cloakedContext !== 'listing')
1194
+ attributes['cloaked-context'] = cloakedContext
1195
+ }
1196
+ if (
1197
+ !('linenums-option' in attributes) &&
1198
+ ('linenums' in attributes || 'source-linenums-option' in docAttrs)
1199
+ ) {
1200
+ attributes['linenums-option'] = ''
1201
+ }
1202
+ if (!('indent' in attributes) && 'source-indent' in docAttrs) {
1203
+ attributes.indent = docAttrs['source-indent']
1204
+ }
1205
+ block = await Parser.buildBlock(
1206
+ 'listing',
1207
+ 'verbatim',
1208
+ terminator,
1209
+ parent,
1210
+ reader,
1211
+ attributes
1212
+ )
1213
+ break
1214
+ }
1215
+ case 'fenced_code': {
1216
+ attributes.style = 'source'
1217
+ const ll = thisLine.length
1218
+ let language = null
1219
+ if (ll > 3) {
1220
+ const langPart = thisLine.slice(3)
1221
+ const commaIdx = langPart.indexOf(',')
1222
+ if (commaIdx >= 0) {
1223
+ if (commaIdx > 0) language = langPart.slice(0, commaIdx).trim()
1224
+ if (commaIdx < ll - 4) attributes.linenums = ''
1225
+ } else {
1226
+ language = langPart.trimStart()
1227
+ }
1228
+ }
1229
+ if (!language) {
1230
+ if ('source-language' in docAttrs)
1231
+ attributes.language = docAttrs['source-language']
1232
+ } else {
1233
+ attributes.language = language
1234
+ }
1235
+ attributes['cloaked-context'] = cloakedContext
1236
+ if (
1237
+ !('linenums-option' in attributes) &&
1238
+ ('linenums' in attributes || 'source-linenums-option' in docAttrs)
1239
+ ) {
1240
+ attributes['linenums-option'] = ''
1241
+ }
1242
+ if (!('indent' in attributes) && 'source-indent' in docAttrs)
1243
+ attributes.indent = docAttrs['source-indent']
1244
+ terminator = terminator.slice(0, 3)
1245
+ block = await Parser.buildBlock(
1246
+ 'listing',
1247
+ 'verbatim',
1248
+ terminator,
1249
+ parent,
1250
+ reader,
1251
+ attributes
1252
+ )
1253
+ break
1254
+ }
1255
+ case 'table': {
1256
+ const blockCursor = reader.cursor
1257
+ const Rdr = Reader
1258
+ const blockReader = new Rdr(
1259
+ await reader.readLinesUntil({
1260
+ terminator,
1261
+ skip_line_comments: true,
1262
+ context: 'table',
1263
+ cursor: 'at_mark',
1264
+ }),
1265
+ blockCursor
1266
+ )
1267
+ if (!terminator.startsWith('|') && !terminator.startsWith('!')) {
1268
+ attributes.format ??= terminator.startsWith(',') ? 'csv' : 'dsv'
1269
+ }
1270
+ block = await Parser.parseTable(blockReader, parent, attributes)
1271
+ break
1272
+ }
1273
+ case 'sidebar':
1274
+ block = await Parser.buildBlock(
1275
+ blockContext,
1276
+ 'compound',
1277
+ terminator,
1278
+ parent,
1279
+ reader,
1280
+ attributes
1281
+ )
1282
+ break
1283
+ case 'admonition': {
1284
+ const admStyle = attributes.style ?? blockContext
1285
+ attributes.name = admStyle.toLowerCase()
1286
+ attributes.textlabel =
1287
+ (attributes.caption && delete attributes.caption) ??
1288
+ docAttrs[`${attributes.name}-caption`]
1289
+ block = await Parser.buildBlock(
1290
+ blockContext,
1291
+ 'compound',
1292
+ terminator,
1293
+ parent,
1294
+ reader,
1295
+ attributes
1296
+ )
1297
+ break
1298
+ }
1299
+ case 'open':
1300
+ case 'abstract':
1301
+ case 'partintro':
1302
+ block = await Parser.buildBlock(
1303
+ 'open',
1304
+ 'compound',
1305
+ terminator,
1306
+ parent,
1307
+ reader,
1308
+ attributes
1309
+ )
1310
+ break
1311
+ case 'literal':
1312
+ block = await Parser.buildBlock(
1313
+ blockContext,
1314
+ 'verbatim',
1315
+ terminator,
1316
+ parent,
1317
+ reader,
1318
+ attributes
1319
+ )
1320
+ break
1321
+ case 'example':
1322
+ if ('collapsible-option' in attributes) attributes.caption ??= ''
1323
+ block = await Parser.buildBlock(
1324
+ blockContext,
1325
+ 'compound',
1326
+ terminator,
1327
+ parent,
1328
+ reader,
1329
+ attributes
1330
+ )
1331
+ break
1332
+ case 'quote':
1333
+ case 'verse':
1334
+ AttributeList.rekey(attributes, [null, 'attribution', 'citetitle'])
1335
+ block = await Parser.buildBlock(
1336
+ blockContext,
1337
+ blockContext === 'verse' ? 'verbatim' : 'compound',
1338
+ terminator,
1339
+ parent,
1340
+ reader,
1341
+ attributes
1342
+ )
1343
+ break
1344
+ case 'stem':
1345
+ case 'latexmath':
1346
+ case 'asciimath':
1347
+ if (blockContext === 'stem') {
1348
+ attributes.style = STEM_TYPE_ALIASES[attributes[2] ?? docAttrs.stem]
1349
+ }
1350
+ block = await Parser.buildBlock(
1351
+ 'stem',
1352
+ 'raw',
1353
+ terminator,
1354
+ parent,
1355
+ reader,
1356
+ attributes
1357
+ )
1358
+ break
1359
+ case 'pass':
1360
+ block = await Parser.buildBlock(
1361
+ blockContext,
1362
+ 'raw',
1363
+ terminator,
1364
+ parent,
1365
+ reader,
1366
+ attributes
1367
+ )
1368
+ break
1369
+ case 'comment':
1370
+ await Parser.buildBlock(
1371
+ blockContext,
1372
+ 'skip',
1373
+ terminator,
1374
+ parent,
1375
+ reader,
1376
+ attributes
1377
+ )
1378
+ for (const k of Object.keys(attributes)) delete attributes[k]
1379
+ return null
1380
+ default: {
1381
+ if (
1382
+ !blockExtensions ||
1383
+ !extensions.registeredForBlock(blockContext, cloakedContext)
1384
+ ) {
1385
+ throw new Error(
1386
+ `Unsupported block type ${blockContext} at ${reader.cursor}`
1387
+ )
1388
+ }
1389
+ const extension = extensions.registeredForBlock(
1390
+ blockContext,
1391
+ cloakedContext
1392
+ )
1393
+ const extConfig = extension.config
1394
+ const contentModel = extConfig.content_model
1395
+ if (contentModel !== 'skip') {
1396
+ const posAttrs = extConfig.positional_attrs ?? extConfig.pos_attrs
1397
+ if (posAttrs && posAttrs.length > 0) {
1398
+ AttributeList.rekey(attributes, [null, ...posAttrs])
1399
+ }
1400
+ if (extConfig.default_attrs) {
1401
+ for (const [k, v] of Object.entries(extConfig.default_attrs)) {
1402
+ attributes[k] ??= v
1403
+ }
1404
+ }
1405
+ attributes['cloaked-context'] = cloakedContext
1406
+ }
1407
+ block = await Parser.buildBlock(
1408
+ blockContext,
1409
+ contentModel,
1410
+ terminator,
1411
+ parent,
1412
+ reader,
1413
+ attributes,
1414
+ { extension }
1415
+ )
1416
+ if (!block) {
1417
+ for (const k of Object.keys(attributes)) delete attributes[k]
1418
+ return null
1419
+ }
1420
+ }
1421
+ }
1422
+ }
1423
+
1424
+ if (!block) return null
1425
+
1426
+ if (document.sourcemap) block.sourceLocation = reader.cursorAtMark()
1427
+ if (attributes.title) {
1428
+ const blockTitle = attributes.title
1429
+ block.title = blockTitle
1430
+ delete attributes.title
1431
+ // Force title resolution while in scope to capture current attribute values (Ruby: parser.rb ~line 939)
1432
+ if (blockTitle.includes(ATTR_REF_HEAD)) await block.precomputeTitle()
1433
+ if (CAPTION_ATTRIBUTE_NAMES[block.context]) {
1434
+ block.assignCaption(attributes.caption)
1435
+ delete attributes.caption
1436
+ }
1437
+ }
1438
+ block.style = attributes.style ?? null
1439
+
1440
+ const blockId = block.id ?? (block.id = attributes.id ?? null)
1441
+ if (blockId) {
1442
+ if (!document.register('refs', [blockId, block])) {
1443
+ Parser.logger.warn(
1444
+ Parser.messageWithContext(
1445
+ `id assigned to block already in use: ${blockId}`,
1446
+ { source_location: reader.cursorAtMark() }
1447
+ )
1448
+ )
1449
+ }
1450
+ }
1451
+
1452
+ if (Object.keys(attributes).length > 0) block.updateAttributes(attributes)
1453
+ block.commitSubs()
1454
+
1455
+ if (block.hasSub('callouts')) {
1456
+ if (!Parser.catalogCallouts(block.source, document))
1457
+ block.removeSub('callouts')
1458
+ }
1459
+
1460
+ return block
1461
+ }
1462
+
1463
+ /**
1464
+ * Build a block from reader lines.
1465
+ * @returns {Promise<Block|null>}
1466
+ * @internal
1467
+ */
1468
+ static async buildBlock(
1469
+ blockContext,
1470
+ contentModel,
1471
+ terminator,
1472
+ parent,
1473
+ reader,
1474
+ attributes,
1475
+ options = {}
1476
+ ) {
1477
+ let skipProcessing, parseAsContentModel
1478
+
1479
+ if (contentModel === 'skip') {
1480
+ skipProcessing = true
1481
+ parseAsContentModel = 'simple'
1482
+ } else if (contentModel === 'raw') {
1483
+ skipProcessing = false
1484
+ parseAsContentModel = 'simple'
1485
+ } else {
1486
+ skipProcessing = false
1487
+ parseAsContentModel = contentModel
1488
+ }
1489
+
1490
+ let lines = null,
1491
+ blockReader = null
1492
+
1493
+ if (terminator == null) {
1494
+ if (parseAsContentModel === 'verbatim') {
1495
+ lines = await reader.readLinesUntil({
1496
+ break_on_blank_lines: true,
1497
+ break_on_list_continuation: true,
1498
+ })
1499
+ } else {
1500
+ if (contentModel === 'compound') contentModel = 'simple'
1501
+ lines = await Parser.readParagraphLines(reader, false, {
1502
+ skip_line_comments: true,
1503
+ skip_processing: skipProcessing,
1504
+ })
1505
+ }
1506
+ } else if (parseAsContentModel !== 'compound') {
1507
+ lines = await reader.readLinesUntil({
1508
+ terminator,
1509
+ skip_processing: skipProcessing,
1510
+ context: blockContext,
1511
+ cursor: 'at_mark',
1512
+ })
1513
+ } else if (terminator === false) {
1514
+ blockReader = reader
1515
+ } else {
1516
+ const blockCursor = reader.cursor
1517
+ const Rdr = Reader
1518
+ blockReader = new Rdr(
1519
+ await reader.readLinesUntil({
1520
+ terminator,
1521
+ skip_processing: skipProcessing,
1522
+ context: blockContext,
1523
+ cursor: 'at_mark',
1524
+ }),
1525
+ blockCursor,
1526
+ { document: parent.document }
1527
+ )
1528
+ }
1529
+
1530
+ if (contentModel === 'verbatim') {
1531
+ const tabSize = parseInt(
1532
+ attributes.tabsize ?? parent.document.attributes.tabsize ?? '0',
1533
+ 10
1534
+ )
1535
+ const indent = attributes.indent
1536
+ if (indent != null) {
1537
+ Parser.adjustIndentation(lines, parseInt(indent, 10), tabSize)
1538
+ } else if (tabSize > 0) {
1539
+ Parser.adjustIndentation(lines, -1, tabSize)
1540
+ }
1541
+ } else if (contentModel === 'skip') {
1542
+ return null
1543
+ }
1544
+
1545
+ let block
1546
+ if (options.extension) {
1547
+ const extension = options.extension
1548
+ delete attributes.style
1549
+ const Rdr = Reader
1550
+ const result = await extension.processMethod(
1551
+ parent,
1552
+ blockReader ?? new Rdr(lines),
1553
+ { ...attributes }
1554
+ )
1555
+ if (!result || result === parent) return null
1556
+ block = result
1557
+ Object.assign(attributes, block.attributes)
1558
+ if (
1559
+ block.contentModel === 'compound' &&
1560
+ block instanceof Block &&
1561
+ block.lines.length > 0
1562
+ ) {
1563
+ contentModel = 'compound'
1564
+ blockReader = new Rdr(block.lines)
1565
+ }
1566
+ } else {
1567
+ block = new Block(parent, blockContext, {
1568
+ content_model: contentModel,
1569
+ source: lines,
1570
+ attributes,
1571
+ })
1572
+ }
1573
+
1574
+ if (contentModel === 'compound')
1575
+ await Parser.parseBlocks(blockReader, block)
1576
+
1577
+ return block
1578
+ }
1579
+
1580
+ /**
1581
+ * Parse blocks from reader until exhausted.
1582
+ * @param {Reader} reader
1583
+ * @param {AbstractBlock} parent
1584
+ * @param {Object|null} [attributes=null]
1585
+ * @returns {Promise<void>}
1586
+ */
1587
+ static async parseBlocks(reader, parent, attributes = null) {
1588
+ while (true) {
1589
+ const block = await Parser.nextBlock(
1590
+ reader,
1591
+ parent,
1592
+ attributes ? { ...attributes } : {}
1593
+ )
1594
+ if (block) parent.blocks.push(block)
1595
+ if (!(await reader.hasMoreLines())) break
1596
+ }
1597
+ }
1598
+
1599
+ /**
1600
+ * Parse an ordered or unordered list.
1601
+ * @returns {Promise<List>}
1602
+ * @internal
1603
+ */
1604
+ static async parseList(reader, listType, parent, style = null, opts = {}) {
1605
+ const start = opts.start != null ? parseInt(opts.start, 10) : null
1606
+ const listAttrs = start != null && start !== 1 ? { start } : null
1607
+ const listBlock = new List(
1608
+ parent,
1609
+ listType,
1610
+ listAttrs ? { attributes: listAttrs } : {}
1611
+ )
1612
+ const listRx = ListRxMap[listType]
1613
+
1614
+ while (
1615
+ (await reader.hasMoreLines()) &&
1616
+ listRx.test(await reader.peekLine())
1617
+ ) {
1618
+ const m = (await reader.peekLine()).match(listRx)
1619
+ const listItem = await Parser.parseListItem(
1620
+ reader,
1621
+ listBlock,
1622
+ m,
1623
+ m[1],
1624
+ style
1625
+ )
1626
+ if (listItem) listBlock.blocks.push(listItem)
1627
+ if ((await reader.skipBlankLines()) == null) break
1628
+ }
1629
+
1630
+ return listBlock
1631
+ }
1632
+
1633
+ /**
1634
+ * Catalog callouts in text.
1635
+ * @param {string} text
1636
+ * @param {Document} document
1637
+ * @returns {boolean} Whether any callouts were found.
1638
+ * @internal
1639
+ */
1640
+ static catalogCallouts(text, document) {
1641
+ if (!text.includes('<')) return false
1642
+ let found = false
1643
+ let autonum = 0
1644
+ const rx = new RegExp(CalloutScanRx.source, `${CalloutScanRx.flags}g`)
1645
+ let m
1646
+ while ((m = rx.exec(text)) !== null) {
1647
+ if (!m[0].startsWith('\\')) {
1648
+ document.callouts.register(m[2] === '.' ? String(++autonum) : m[2])
1649
+ }
1650
+ found = true
1651
+ }
1652
+ return found
1653
+ }
1654
+
1655
+ /**
1656
+ * Catalog a single inline anchor.
1657
+ * @internal
1658
+ */
1659
+ static catalogInlineAnchor(id, reftext, node, location, doc = node.document) {
1660
+ if (reftext?.includes(ATTR_REF_HEAD)) {
1661
+ reftext = doc.subAttributes(reftext)
1662
+ }
1663
+ const cursor = location?.cursor ? location.cursor : location
1664
+ if (
1665
+ !doc.register('refs', [
1666
+ id,
1667
+ new Inline(node, 'anchor', reftext, { type: 'ref', id }),
1668
+ ])
1669
+ ) {
1670
+ Parser.logger.warn(
1671
+ Parser.messageWithContext(
1672
+ `id assigned to anchor already in use: ${id}`,
1673
+ { source_location: cursor }
1674
+ )
1675
+ )
1676
+ }
1677
+ }
1678
+
1679
+ /**
1680
+ * Catalog all inline anchors in text.
1681
+ * @internal
1682
+ */
1683
+ static catalogInlineAnchors(text, block, document, reader) {
1684
+ if (!text.includes('[[') && !text.includes('anchor:')) return
1685
+
1686
+ let m
1687
+ // Reset lastIndex for global search
1688
+ InlineAnchorScanRx.lastIndex = 0
1689
+ const globalRx = new RegExp(InlineAnchorScanRx.source, 'gu')
1690
+ while ((m = globalRx.exec(text)) !== null) {
1691
+ let id, reftext
1692
+ if (m[1]) {
1693
+ id = m[1]
1694
+ reftext = m[2]
1695
+ if (reftext?.includes(ATTR_REF_HEAD)) {
1696
+ reftext = document.subAttributes(reftext)
1697
+ if (!reftext) continue
1698
+ }
1699
+ } else {
1700
+ id = m[3]
1701
+ reftext = m[4]
1702
+ if (reftext) {
1703
+ if (reftext.includes(']')) reftext = reftext.replace(/\\]/g, ']')
1704
+ if (reftext.includes(ATTR_REF_HEAD)) {
1705
+ reftext = document.subAttributes(reftext)
1706
+ if (!reftext) reftext = null
1707
+ }
1708
+ }
1709
+ }
1710
+ if (
1711
+ !document.register('refs', [
1712
+ id,
1713
+ new Inline(block, 'anchor', reftext, { type: 'ref', id }),
1714
+ ])
1715
+ ) {
1716
+ Parser.logger.warn(
1717
+ Parser.messageWithContext(
1718
+ `id assigned to anchor already in use: ${id}`,
1719
+ { source_location: reader.cursorAtMark() }
1720
+ )
1721
+ )
1722
+ }
1723
+ }
1724
+ }
1725
+
1726
+ /**
1727
+ * Catalog a bibliography inline anchor.
1728
+ * @internal
1729
+ */
1730
+ static catalogInlineBiblioAnchor(id, reftext, node, reader) {
1731
+ const displayReftext = reftext != null ? `[${reftext}]` : null
1732
+ if (
1733
+ !node.document.register('refs', [
1734
+ id,
1735
+ new Inline(node, 'anchor', displayReftext, { type: 'bibref', id }),
1736
+ ])
1737
+ ) {
1738
+ Parser.logger.warn(
1739
+ Parser.messageWithContext(
1740
+ `id assigned to bibliography anchor already in use: ${id}`,
1741
+ { source_location: reader.cursor }
1742
+ )
1743
+ )
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * Parse a description list.
1749
+ * @returns {Promise<List>}
1750
+ * @internal
1751
+ */
1752
+ static async parseDescriptionList(reader, match, parent) {
1753
+ const listBlock = new List(parent, 'dlist')
1754
+ const siblingPattern = DescriptionListSiblingRx[match[2]]
1755
+ let currentPair = await Parser.parseListItem(
1756
+ reader,
1757
+ listBlock,
1758
+ match,
1759
+ siblingPattern
1760
+ )
1761
+ listBlock.blocks.push(currentPair)
1762
+
1763
+ while (await reader.hasMoreLines()) {
1764
+ const pLine = await reader.peekLine()
1765
+ const nm = pLine.match(siblingPattern)
1766
+ if (!nm) break
1767
+ const nextPair = await Parser.parseListItem(
1768
+ reader,
1769
+ listBlock,
1770
+ nm,
1771
+ siblingPattern
1772
+ )
1773
+ if (currentPair[1]) {
1774
+ listBlock.blocks.push((currentPair = nextPair))
1775
+ } else {
1776
+ currentPair[0].push(nextPair[0][0])
1777
+ currentPair[1] = nextPair[1]
1778
+ }
1779
+ }
1780
+
1781
+ return listBlock
1782
+ }
1783
+
1784
+ /**
1785
+ * Parse a callout list.
1786
+ * @returns {Promise<List>}
1787
+ * @internal
1788
+ */
1789
+ static async parseCalloutList(reader, match, parent, callouts) {
1790
+ const listBlock = new List(parent, 'colist')
1791
+ let nextIndex = 1
1792
+ let autonum = 0
1793
+
1794
+ while (true) {
1795
+ if (!match) {
1796
+ const pLine = await reader.peekLine()
1797
+ if (!pLine) break
1798
+ const nm = pLine.match(CalloutListRx)
1799
+ if (!nm) break
1800
+ match = nm
1801
+ reader.mark()
1802
+ }
1803
+ let num = match[1]
1804
+ if (num === '.') num = String(++autonum)
1805
+ if (num !== String(nextIndex)) {
1806
+ Parser.logger.warn(
1807
+ Parser.messageWithContext(
1808
+ `callout list item index: expected ${nextIndex}, got ${num}`,
1809
+ { source_location: reader.cursorAtMark() }
1810
+ )
1811
+ )
1812
+ }
1813
+ const listItem = await Parser.parseListItem(
1814
+ reader,
1815
+ listBlock,
1816
+ match,
1817
+ '<1>'
1818
+ )
1819
+ if (listItem) {
1820
+ listBlock.blocks.push(listItem)
1821
+ const coids = callouts.getCalloutIds(listBlock.blocks.length)
1822
+ if (!coids) {
1823
+ Parser.logger.warn(
1824
+ Parser.messageWithContext(
1825
+ `no callout found for <${listBlock.blocks.length}>`,
1826
+ { source_location: reader.cursorAtMark() }
1827
+ )
1828
+ )
1829
+ } else {
1830
+ listItem.attributes.coids = coids
1831
+ }
1832
+ }
1833
+ nextIndex++
1834
+ match = null
1835
+ }
1836
+
1837
+ callouts.nextList()
1838
+ return listBlock
1839
+ }
1840
+
1841
+ /**
1842
+ * Parse a list item (ordered, unordered, callout, or description list).
1843
+ * @returns {Promise<ListItem|[ListItem[], ListItem|null]>}
1844
+ * @internal
1845
+ */
1846
+ static async parseListItem(
1847
+ reader,
1848
+ listBlock,
1849
+ match,
1850
+ siblingTrait,
1851
+ style = null
1852
+ ) {
1853
+ const listType = listBlock.context
1854
+ const dlist = listType === 'dlist'
1855
+ let listTerm, listItem, hasText, sourcemapAssignmentDeferred
1856
+
1857
+ if (dlist) {
1858
+ const termText = match[1]
1859
+ listTerm = new ListItem(listBlock, termText)
1860
+ if (termText.startsWith('[[')) {
1861
+ const am = termText.match(LeadingInlineAnchorRx)
1862
+ if (am)
1863
+ Parser.catalogInlineAnchor(
1864
+ am[1],
1865
+ am[2] ?? termText.slice(am[0].length).trimStart(),
1866
+ listTerm,
1867
+ reader
1868
+ )
1869
+ }
1870
+ const itemText = match[3] ?? null
1871
+ hasText = !!itemText
1872
+ listItem = new ListItem(listBlock, itemText)
1873
+ if (listBlock.document.sourcemap) {
1874
+ listTerm.sourceLocation = reader.cursor
1875
+ if (hasText) {
1876
+ listItem.sourceLocation = listTerm.sourceLocation
1877
+ } else {
1878
+ sourcemapAssignmentDeferred = true
1879
+ }
1880
+ }
1881
+ } else {
1882
+ hasText = true
1883
+ const itemText = match[2]
1884
+ listItem = new ListItem(listBlock, itemText)
1885
+ if (listBlock.document.sourcemap) listItem.sourceLocation = reader.cursor
1886
+
1887
+ if (listType === 'ulist') {
1888
+ listItem.marker = siblingTrait
1889
+ if (itemText.startsWith('[')) {
1890
+ if (style && style === 'bibliography') {
1891
+ const bm = itemText.match(InlineBiblioAnchorRx)
1892
+ if (bm)
1893
+ Parser.catalogInlineBiblioAnchor(bm[1], bm[2], listItem, reader)
1894
+ } else if (itemText.startsWith('[[')) {
1895
+ const am = itemText.match(LeadingInlineAnchorRx)
1896
+ if (am) Parser.catalogInlineAnchor(am[1], am[2], listItem, reader)
1897
+ } else if (
1898
+ itemText.startsWith('[ ] ') ||
1899
+ itemText.startsWith('[x] ') ||
1900
+ itemText.startsWith('[*] ')
1901
+ ) {
1902
+ listBlock.attributes['checklist-option'] = ''
1903
+ listItem.attributes.checkbox = ''
1904
+ if (!itemText.startsWith('[ ')) listItem.attributes.checked = ''
1905
+ listItem.setText(itemText.slice(4))
1906
+ }
1907
+ }
1908
+ } else if (listType === 'olist') {
1909
+ const ordinal = listBlock.blocks.length
1910
+ const isFirst = ordinal === 0
1911
+ let validate = true
1912
+ const startAttr = listBlock.attributes.start
1913
+ let effectiveOrdinal = ordinal
1914
+ if (startAttr != null) {
1915
+ effectiveOrdinal += parseInt(startAttr, 10) - 1
1916
+ } else if (isFirst) {
1917
+ const startNum = Parser.resolveOrderedListStart(siblingTrait)
1918
+ if (startNum !== 1) {
1919
+ listBlock.attributes.start = startNum
1920
+ effectiveOrdinal += startNum - 1
1921
+ validate = false
1922
+ }
1923
+ }
1924
+ const [resolvedMarker, implicitStyle] = Parser.resolveOrderedListMarker(
1925
+ siblingTrait,
1926
+ effectiveOrdinal,
1927
+ validate,
1928
+ reader
1929
+ )
1930
+ listItem.marker = resolvedMarker
1931
+ if (isFirst && !style) {
1932
+ listBlock.style =
1933
+ implicitStyle ??
1934
+ ORDERED_LIST_STYLES[resolvedMarker.length - 1] ??
1935
+ 'arabic'
1936
+ }
1937
+ if (itemText.startsWith('[[')) {
1938
+ const am = itemText.match(LeadingInlineAnchorRx)
1939
+ if (am) Parser.catalogInlineAnchor(am[1], am[2], listItem, reader)
1940
+ }
1941
+ } else {
1942
+ // colist
1943
+ listItem.marker = siblingTrait
1944
+ if (itemText.startsWith('[[')) {
1945
+ const am = itemText.match(LeadingInlineAnchorRx)
1946
+ if (am) Parser.catalogInlineAnchor(am[1], am[2], listItem, reader)
1947
+ }
1948
+ }
1949
+ }
1950
+
1951
+ await reader.readLine()
1952
+ const blockCursor = reader.cursor
1953
+ const Rdr = Reader
1954
+ const listItemLines = await Parser.readLinesForListItem(
1955
+ reader,
1956
+ listType,
1957
+ siblingTrait,
1958
+ hasText
1959
+ )
1960
+ const listItemReader = new Rdr(listItemLines, blockCursor)
1961
+
1962
+ if (await listItemReader.hasMoreLines()) {
1963
+ if (sourcemapAssignmentDeferred) listItem.sourceLocation = blockCursor
1964
+ const commentLines = await listItemReader.skipLineComments()
1965
+ const subsequentLine = await listItemReader.peekLine()
1966
+ if (subsequentLine != null) {
1967
+ if (commentLines.length > 0) listItemReader.unshiftLines(commentLines)
1968
+ let contentAdjacent = false
1969
+ if (String(subsequentLine) !== '') {
1970
+ contentAdjacent = true
1971
+ if (!dlist) hasText = null
1972
+ }
1973
+ const block = await Parser.nextBlock(
1974
+ listItemReader,
1975
+ listItem,
1976
+ {},
1977
+ { text_only: hasText ? null : true, list_type: listType }
1978
+ )
1979
+ if (block) listItem.blocks.push(block)
1980
+ while (await listItemReader.hasMoreLines()) {
1981
+ const b = await Parser.nextBlock(
1982
+ listItemReader,
1983
+ listItem,
1984
+ {},
1985
+ { list_type: listType }
1986
+ )
1987
+ if (b) listItem.blocks.push(b)
1988
+ }
1989
+ if (
1990
+ contentAdjacent &&
1991
+ listItem.blocks.length > 0 &&
1992
+ listItem.blocks[0].context === 'paragraph'
1993
+ ) {
1994
+ listItem.foldFirst()
1995
+ }
1996
+ }
1997
+ }
1998
+
1999
+ return dlist
2000
+ ? [
2001
+ [listTerm],
2002
+ listItem.hasText() || listItem.blocks.length > 0 ? listItem : null,
2003
+ ]
2004
+ : listItem
2005
+ }
2006
+
2007
+ /**
2008
+ * Collect lines belonging to the current list item.
2009
+ * @returns {Promise<string[]>}
2010
+ * @internal
2011
+ */
2012
+ static async readLinesForListItem(
2013
+ reader,
2014
+ listType,
2015
+ siblingTrait = null,
2016
+ hasText = true
2017
+ ) {
2018
+ const buffer = []
2019
+ let continuation = 'inactive'
2020
+ let withinNestedList = false
2021
+ let detachedContinuation = null
2022
+ const dlist = listType === 'dlist'
2023
+ let thisLine = null
2024
+
2025
+ while (await reader.hasMoreLines()) {
2026
+ thisLine = await reader.readLine()
2027
+
2028
+ if (Parser.isSiblingListItem(thisLine, listType, siblingTrait)) break
2029
+
2030
+ if (thisLine === LIST_CONTINUATION) thisLine = ListContinuationString
2031
+
2032
+ const prevLine = buffer.length > 0 ? buffer[buffer.length - 1] : null
2033
+
2034
+ if (isListContinuation(prevLine)) {
2035
+ if (continuation === 'inactive') {
2036
+ continuation = 'active'
2037
+ hasText = true
2038
+ if (!withinNestedList)
2039
+ buffer[buffer.length - 1] = ListContinuationPlaceholder
2040
+ }
2041
+ if (isListContinuation(thisLine)) {
2042
+ if (continuation !== 'frozen') {
2043
+ continuation = 'frozen'
2044
+ buffer.push(thisLine)
2045
+ }
2046
+ thisLine = null
2047
+ continue
2048
+ }
2049
+ }
2050
+
2051
+ const delimMatch = Parser.isDelimitedBlock(thisLine, true)
2052
+ if (delimMatch) {
2053
+ if (continuation !== 'active') break
2054
+ buffer.push(thisLine)
2055
+ const blockLines = await reader.readLinesUntil({
2056
+ terminator: delimMatch.terminator,
2057
+ read_last_line: true,
2058
+ context: delimMatch.context,
2059
+ })
2060
+ buffer.push(...blockLines)
2061
+ continuation = 'inactive'
2062
+ } else if (
2063
+ dlist &&
2064
+ continuation !== 'active' &&
2065
+ thisLine.startsWith('[') &&
2066
+ BlockAttributeLineRx.test(thisLine)
2067
+ ) {
2068
+ const blockAttributeLines = [thisLine]
2069
+ let interrupt = false
2070
+ while (true) {
2071
+ const nextLine = await reader.peekLine()
2072
+ if (nextLine == null) break
2073
+ if (Parser.isDelimitedBlock(nextLine)) {
2074
+ interrupt = true
2075
+ break
2076
+ }
2077
+ if (
2078
+ nextLine === '' ||
2079
+ (nextLine.startsWith('[') && BlockAttributeLineRx.test(nextLine))
2080
+ ) {
2081
+ blockAttributeLines.push(await reader.readLine())
2082
+ } else if (
2083
+ AnyListRx.test(nextLine) &&
2084
+ !Parser.isSiblingListItem(nextLine, listType, siblingTrait)
2085
+ ) {
2086
+ buffer.push(...blockAttributeLines)
2087
+ break
2088
+ } else {
2089
+ interrupt = true
2090
+ break
2091
+ }
2092
+ }
2093
+ if (interrupt) {
2094
+ thisLine = null
2095
+ reader.unshiftLines(blockAttributeLines)
2096
+ break
2097
+ }
2098
+ } else if (continuation === 'active' && thisLine !== '') {
2099
+ if (LiteralParagraphRx.test(thisLine)) {
2100
+ reader.unshiftLine(thisLine)
2101
+ if (dlist) {
2102
+ const lns = await reader.readLinesUntil(
2103
+ {
2104
+ preserve_last_line: true,
2105
+ break_on_blank_lines: true,
2106
+ break_on_list_continuation: true,
2107
+ },
2108
+ (line) => Parser.isSiblingListItem(line, listType, siblingTrait)
2109
+ )
2110
+ buffer.push(...lns)
2111
+ } else {
2112
+ const lns = await reader.readLinesUntil({
2113
+ preserve_last_line: true,
2114
+ break_on_blank_lines: true,
2115
+ break_on_list_continuation: true,
2116
+ })
2117
+ buffer.push(...lns)
2118
+ }
2119
+ continuation = 'inactive'
2120
+ } else if (
2121
+ (thisLine[0] === '.' && BlockTitleRx.test(thisLine)) ||
2122
+ (thisLine[0] === '[' && BlockAttributeLineRx.test(thisLine)) ||
2123
+ (thisLine[0] === ':' && AttributeEntryRx.test(thisLine))
2124
+ ) {
2125
+ buffer.push(thisLine)
2126
+ } else {
2127
+ if (!withinNestedList) {
2128
+ const nestedType = NESTABLE_LIST_CONTEXTS.find((ctx) =>
2129
+ ListRxMap[ctx].test(thisLine)
2130
+ )
2131
+ if (nestedType) {
2132
+ withinNestedList = true
2133
+ if (
2134
+ nestedType === 'dlist' &&
2135
+ !thisLine.match(DescriptionListRx)?.[3]
2136
+ ) {
2137
+ hasText = false
2138
+ }
2139
+ }
2140
+ }
2141
+ buffer.push(thisLine)
2142
+ continuation = 'inactive'
2143
+ }
2144
+ } else if (prevLine !== null && prevLine === '') {
2145
+ if (thisLine === '') {
2146
+ const skippedLine = await reader.skipBlankLines()
2147
+ if (skippedLine == null) {
2148
+ thisLine = null
2149
+ break
2150
+ }
2151
+ thisLine = await reader.readLine()
2152
+ if (thisLine == null) break
2153
+ if (Parser.isSiblingListItem(thisLine, listType, siblingTrait)) break
2154
+ }
2155
+ if (String(thisLine) === LIST_CONTINUATION) {
2156
+ detachedContinuation = buffer.length
2157
+ buffer.push(ListContinuationString)
2158
+ } else if (hasText) {
2159
+ if (Parser.isSiblingListItem(thisLine, listType, siblingTrait)) break
2160
+ const nestedType = NESTABLE_LIST_CONTEXTS.find((ctx) =>
2161
+ ListRxMap[ctx].test(thisLine)
2162
+ )
2163
+ if (nestedType) {
2164
+ buffer.push(thisLine)
2165
+ withinNestedList = true
2166
+ if (
2167
+ nestedType === 'dlist' &&
2168
+ !thisLine.match(DescriptionListRx)?.[3]
2169
+ )
2170
+ hasText = false
2171
+ } else if (LiteralParagraphRx.test(thisLine)) {
2172
+ reader.unshiftLine(thisLine)
2173
+ if (dlist) {
2174
+ const lns = await reader.readLinesUntil(
2175
+ {
2176
+ preserve_last_line: true,
2177
+ break_on_blank_lines: true,
2178
+ break_on_list_continuation: true,
2179
+ },
2180
+ (line) => Parser.isSiblingListItem(line, listType, siblingTrait)
2181
+ )
2182
+ buffer.push(...lns)
2183
+ } else {
2184
+ const lns = await reader.readLinesUntil({
2185
+ preserve_last_line: true,
2186
+ break_on_blank_lines: true,
2187
+ break_on_list_continuation: true,
2188
+ })
2189
+ buffer.push(...lns)
2190
+ }
2191
+ } else {
2192
+ break
2193
+ }
2194
+ } else {
2195
+ if (!withinNestedList) buffer.pop()
2196
+ buffer.push(thisLine)
2197
+ hasText = true
2198
+ }
2199
+ } else if (isListContinuation(thisLine)) {
2200
+ hasText = true
2201
+ buffer.push(thisLine)
2202
+ } else {
2203
+ if (thisLine !== '') {
2204
+ hasText = true
2205
+ const nestedType = (
2206
+ withinNestedList ? ['dlist'] : NESTABLE_LIST_CONTEXTS
2207
+ ).find((ctx) => ListRxMap[ctx].test(thisLine))
2208
+ if (nestedType) {
2209
+ withinNestedList = true
2210
+ if (
2211
+ nestedType === 'dlist' &&
2212
+ !thisLine.match(DescriptionListRx)?.[3]
2213
+ )
2214
+ hasText = false
2215
+ }
2216
+ }
2217
+ buffer.push(thisLine)
2218
+ }
2219
+ thisLine = null
2220
+ }
2221
+
2222
+ if (thisLine != null) reader.unshiftLine(thisLine)
2223
+ if (detachedContinuation != null)
2224
+ buffer[detachedContinuation] = ListContinuationPlaceholder
2225
+
2226
+ while (buffer.length > 0) {
2227
+ const last = buffer[buffer.length - 1]
2228
+ if (isListContinuation(last)) {
2229
+ buffer.pop()
2230
+ break
2231
+ }
2232
+ if (last === '') {
2233
+ buffer.pop()
2234
+ } else {
2235
+ break
2236
+ }
2237
+ }
2238
+
2239
+ return buffer
2240
+ }
2241
+
2242
+ /**
2243
+ * Initialize a Section from the current reader position.
2244
+ * @returns {Promise<Section>}
2245
+ * @internal
2246
+ */
2247
+ static async initializeSection(reader, parent, attributes = {}) {
2248
+ const document = parent.document
2249
+ const doctype = document.doctype
2250
+ const book = doctype === 'book'
2251
+ const sourceLocation = document.sourcemap ? reader.cursor : null
2252
+ const sectStyle = attributes[1] ?? null
2253
+
2254
+ const [sectId, sectReftext, sectTitle, rawSectLevel, sectAtx] =
2255
+ await Parser.parseSectionTitle(reader, document, attributes.id)
2256
+ let sectLevel = rawSectLevel
2257
+
2258
+ let sectName,
2259
+ sectSpecial = false,
2260
+ sectNumbered = false
2261
+ if (sectStyle) {
2262
+ if (book && sectStyle === 'abstract') {
2263
+ sectName = 'chapter'
2264
+ // sectLevel already 1 from parseSectionTitle typically
2265
+ } else if (
2266
+ sectStyle.startsWith('sect') &&
2267
+ SectionLevelStyleRx.test(sectStyle)
2268
+ ) {
2269
+ sectName = 'section'
2270
+ } else {
2271
+ sectName = sectStyle
2272
+ sectSpecial = true
2273
+ if (book && sectLevel === 0) sectLevel = 1
2274
+ sectNumbered = sectName === 'appendix'
2275
+ }
2276
+ } else if (book) {
2277
+ sectName =
2278
+ sectLevel === 0 ? 'part' : sectLevel > 1 ? 'section' : 'chapter'
2279
+ } else if (
2280
+ doctype === 'manpage' &&
2281
+ sectTitle.toLowerCase() === 'synopsis'
2282
+ ) {
2283
+ sectName = 'synopsis'
2284
+ sectSpecial = true
2285
+ } else {
2286
+ sectName = 'section'
2287
+ }
2288
+
2289
+ if (sectReftext) attributes.reftext = sectReftext
2290
+ const section = new Section(parent, sectLevel)
2291
+ section.id = sectId ?? null
2292
+ section.title = sectTitle
2293
+ section.sectname = sectName
2294
+ section.sourceLocation = sourceLocation
2295
+
2296
+ if (sectSpecial) {
2297
+ section.special = true
2298
+ if (sectNumbered) {
2299
+ section.numbered = true
2300
+ } else if (document.attributes.sectnums === 'all') {
2301
+ section.numbered = book && sectLevel === 1 ? 'chapter' : true
2302
+ }
2303
+ } else if ('sectnums' in document.attributes && sectLevel > 0) {
2304
+ section.numbered = section.special ? parent.numbered && true : true
2305
+ } else if (book && sectLevel === 0 && 'partnums' in document.attributes) {
2306
+ section.numbered = true
2307
+ }
2308
+
2309
+ let id = section.id
2310
+ if (id != null) {
2311
+ if (id === '') {
2312
+ section.id = id = null
2313
+ } else if (sectTitle.includes(ATTR_REF_HEAD)) {
2314
+ // Force title resolution while in scope, mirroring Ruby's lazy-memo access
2315
+ // (`section.title` triggers `@converted_title ||= apply_title_subs(@title)`).
2316
+ // Must happen before _restoreAttributes resets body-scoped attribute values.
2317
+ await section.precomputeTitle()
2318
+ }
2319
+ } else if ('sectids' in document.attributes) {
2320
+ // Match Ruby behaviour: section.title returns apply_title_subs(@title) (fully substituted HTML).
2321
+ // InvalidSectionIdCharsRx then strips the HTML tags, so inline anchors, icon macros and
2322
+ // URL macros are correctly excluded from the generated ID.
2323
+ // precomputeTitle() is idempotent (guarded by #convertedTitle == null), so calling it here
2324
+ // prevents a second substitution pass in _resolveAllTexts (avoids double-cataloging images,
2325
+ // footnotes, etc.).
2326
+ await section.precomputeTitle()
2327
+ section.id = id = Section.generateId(section.title, document)
2328
+ }
2329
+
2330
+ if (id && !document.register('refs', [id, section])) {
2331
+ const lineNo = reader.lineno - (sectAtx ? 1 : 2)
2332
+ Parser.logger.warn(
2333
+ Parser.messageWithContext(
2334
+ `id assigned to section already in use: ${id}`,
2335
+ { source_location: reader.cursorAtLine(lineNo) }
2336
+ )
2337
+ )
2338
+ }
2339
+
2340
+ section.updateAttributes(attributes)
2341
+ await reader.skipBlankLines()
2342
+
2343
+ return section
2344
+ }
2345
+
2346
+ /**
2347
+ * Check if the next line is a section title.
2348
+ * @returns {Promise<number|null>} The section level, or null.
2349
+ * @internal
2350
+ */
2351
+ static async isNextLineSection(reader, attributes) {
2352
+ const style = attributes[1]
2353
+ if (style && (style === 'discrete' || style === 'float')) return null
2354
+
2355
+ if (Compliance.underlineStyleSectionTitles) {
2356
+ const nextLines = await reader.peekLines(2, style && style === 'comment')
2357
+ return Parser.isSectionTitle(nextLines[0] ?? '', nextLines[1] ?? null)
2358
+ }
2359
+ return Parser.atxSectionTitle((await reader.peekLine()) ?? '')
2360
+ }
2361
+
2362
+ /**
2363
+ * Check if the next line is the document title.
2364
+ * @returns {Promise<boolean>}
2365
+ * @internal
2366
+ */
2367
+ static async isNextLineDoctitle(reader, attributes, leveloffset) {
2368
+ const sectLevel = await Parser.isNextLineSection(reader, attributes)
2369
+ if (sectLevel == null || sectLevel === false) return false
2370
+ if (leveloffset) {
2371
+ return sectLevel + parseInt(leveloffset, 10) === 0
2372
+ }
2373
+ return sectLevel === 0
2374
+ }
2375
+
2376
+ /**
2377
+ * Check if line1 (and optionally line2) form a section title.
2378
+ * @param {string} line1
2379
+ * @param {string|null} [line2=null]
2380
+ * @returns {number|null} The section level, or null.
2381
+ */
2382
+ static isSectionTitle(line1, line2 = null) {
2383
+ const atxLevel = Parser.atxSectionTitle(line1)
2384
+ if (atxLevel != null) return atxLevel
2385
+ if (!line2) return null
2386
+ return Parser.setextSectionTitle(line1, line2)
2387
+ }
2388
+
2389
+ /**
2390
+ * Check for ATX-style section title.
2391
+ * @param {string} line
2392
+ * @returns {number|null} The section level, or null.
2393
+ * @internal
2394
+ */
2395
+ static atxSectionTitle(line) {
2396
+ const rx = Compliance.markdownSyntax
2397
+ ? ExtAtxSectionTitleRx
2398
+ : AtxSectionTitleRx
2399
+ if (
2400
+ !(Compliance.markdownSyntax
2401
+ ? line.startsWith('=') || line.startsWith('#')
2402
+ : line.startsWith('='))
2403
+ )
2404
+ return null
2405
+ const m = line.match(rx)
2406
+ return m ? m[1].length - 1 : null
2407
+ }
2408
+
2409
+ /**
2410
+ * Check for setext-style section title.
2411
+ * @param {string} line1
2412
+ * @param {string} line2
2413
+ * @returns {number|null} The section level, or null.
2414
+ * @internal
2415
+ */
2416
+ static setextSectionTitle(line1, line2) {
2417
+ const ch0 = line2[0]
2418
+ const level = SETEXT_SECTION_LEVELS[ch0]
2419
+ if (level == null) return null
2420
+ if (!_uniform(line2, ch0, line2.length)) return null
2421
+ if (!SetextSectionTitleRx.test(line1)) return null
2422
+ if (Math.abs(line1.length - line2.length) >= 2) return null
2423
+ return level
2424
+ }
2425
+
2426
+ /**
2427
+ * Parse section title from reader.
2428
+ * @param {Reader} reader
2429
+ * @param {Document} document
2430
+ * @param {string|null} [sectId=null]
2431
+ * @returns {Promise<[string|null, string|null, string, number, boolean]>} Tuple of [id, reftext, title, level, atx].
2432
+ */
2433
+ static async parseSectionTitle(reader, document, sectId = null) {
2434
+ let sectReftext = null,
2435
+ sectTitle,
2436
+ sectLevel,
2437
+ atx
2438
+
2439
+ const line1 = await reader.readLine()
2440
+ const rx = Compliance.markdownSyntax
2441
+ ? ExtAtxSectionTitleRx
2442
+ : AtxSectionTitleRx
2443
+
2444
+ if (
2445
+ (Compliance.markdownSyntax
2446
+ ? line1.startsWith('=') || line1.startsWith('#')
2447
+ : line1.startsWith('=')) &&
2448
+ rx.test(line1)
2449
+ ) {
2450
+ const m = line1.match(rx)
2451
+ sectLevel = m[1].length - 1
2452
+ sectTitle = m[2]
2453
+ atx = true
2454
+ if (!sectId && sectTitle.endsWith(']]')) {
2455
+ const am = sectTitle.match(InlineSectionAnchorRx)
2456
+ if (am && !am[1]) {
2457
+ // not escaped
2458
+ sectTitle = sectTitle.slice(0, sectTitle.length - am[0].length)
2459
+ sectId = am[2]
2460
+ sectReftext = am[3] ?? null
2461
+ }
2462
+ }
2463
+ } else if (Compliance.underlineStyleSectionTitles) {
2464
+ const line2 = await reader.peekLine(true)
2465
+ if (line2) {
2466
+ const ch0 = line2[0]
2467
+ const level = SETEXT_SECTION_LEVELS[ch0]
2468
+ if (
2469
+ level != null &&
2470
+ _uniform(line2, ch0, line2.length) &&
2471
+ SetextSectionTitleRx.test(line1) &&
2472
+ Math.abs(line1.length - line2.length) < 2
2473
+ ) {
2474
+ sectLevel = level
2475
+ const m = line1.match(SetextSectionTitleRx)
2476
+ sectTitle = m ? m[1] : line1
2477
+ atx = false
2478
+ if (!sectId && sectTitle.endsWith(']]')) {
2479
+ const am = sectTitle.match(InlineSectionAnchorRx)
2480
+ if (am && !am[1]) {
2481
+ sectTitle = sectTitle.slice(0, sectTitle.length - am[0].length)
2482
+ sectId = am[2]
2483
+ sectReftext = am[3] ?? null
2484
+ }
2485
+ }
2486
+ await reader.readLine()
2487
+ }
2488
+ }
2489
+ }
2490
+
2491
+ if (sectTitle == null) {
2492
+ throw new Error(`Unrecognized section at ${reader.cursorAtPrevLine()}`)
2493
+ }
2494
+
2495
+ const leveloffset = document.getAttribute('leveloffset')
2496
+ if (leveloffset) {
2497
+ sectLevel += parseInt(leveloffset, 10)
2498
+ if (sectLevel < 0) sectLevel = 0
2499
+ }
2500
+
2501
+ return [sectId, sectReftext, sectTitle, sectLevel, atx]
2502
+ }
2503
+
2504
+ /**
2505
+ * Parse header metadata (author line and revision line).
2506
+ * @param {Reader} reader
2507
+ * @param {Document|null} [document=null]
2508
+ * @param {boolean} [retrieve=true]
2509
+ * @returns {Promise<Object|null>}
2510
+ */
2511
+ static async parseHeaderMetadata(reader, document = null, retrieve = true) {
2512
+ const docAttrs = document?.attributes
2513
+
2514
+ await Parser.processAttributeEntries(reader, document)
2515
+
2516
+ let implicitAuthorMetadata = {}
2517
+ let authorcount = null
2518
+ let implicitAuthor = null
2519
+ let implicitAuthorinitials = null
2520
+ let implicitAuthors = null
2521
+
2522
+ if ((await reader.hasMoreLines()) && !(await reader.isNextLineEmpty())) {
2523
+ const authorLine = await reader.readLine()
2524
+ const parsed = Parser.processAuthors(authorLine)
2525
+ authorcount = parsed.authorcount
2526
+ delete parsed.authorcount
2527
+ implicitAuthorMetadata = parsed
2528
+ implicitAuthorMetadata.authorcount = authorcount
2529
+
2530
+ if (document && docAttrs) {
2531
+ docAttrs.authorcount = authorcount
2532
+ if (authorcount > 0) {
2533
+ for (const [key, val] of Object.entries(parsed)) {
2534
+ if (!(key in docAttrs)) {
2535
+ docAttrs[key] = await document.applyHeaderSubs(val)
2536
+ }
2537
+ }
2538
+ implicitAuthor = docAttrs.author
2539
+ implicitAuthorinitials = docAttrs.authorinitials
2540
+ implicitAuthors = docAttrs.authors
2541
+ }
2542
+ }
2543
+
2544
+ await Parser.processAttributeEntries(reader, document)
2545
+
2546
+ if ((await reader.hasMoreLines()) && !(await reader.isNextLineEmpty())) {
2547
+ const revLine = await reader.readLine()
2548
+ const rm = revLine.match(RevisionInfoLineRx)
2549
+ if (rm) {
2550
+ const revMetadata = {}
2551
+ if (rm[1]) revMetadata.revnumber = rm[1].trimEnd()
2552
+ if (rm[2]) {
2553
+ const component = rm[2].trim()
2554
+ if (component !== '') {
2555
+ if (!rm[1] && component.startsWith('v')) {
2556
+ revMetadata.revnumber = component.slice(1)
2557
+ } else {
2558
+ revMetadata.revdate = component
2559
+ }
2560
+ }
2561
+ }
2562
+ if (rm[3]) revMetadata.revremark = rm[3].trimEnd()
2563
+ if (document && docAttrs && Object.keys(revMetadata).length > 0) {
2564
+ for (const [key, val] of Object.entries(revMetadata)) {
2565
+ if (!(key in docAttrs))
2566
+ docAttrs[key] = await document.applyHeaderSubs(val)
2567
+ }
2568
+ }
2569
+ Object.assign(implicitAuthorMetadata, revMetadata)
2570
+ } else {
2571
+ reader.unshiftLine(revLine)
2572
+ }
2573
+ }
2574
+
2575
+ await Parser.processAttributeEntries(reader, document)
2576
+ await reader.skipBlankLines()
2577
+ }
2578
+
2579
+ // Process author attribute entries that override (or stand in for) the implicit author line.
2580
+ let authorMetadata = null
2581
+ if (document) {
2582
+ if ('author' in docAttrs && docAttrs.author !== implicitAuthor) {
2583
+ // author attribute was set or overridden; re-parse as names only (no multiple)
2584
+ authorMetadata = Parser.processAuthors(docAttrs.author, true, false)
2585
+ if (docAttrs.authorinitials !== implicitAuthorinitials) {
2586
+ delete authorMetadata.authorinitials
2587
+ }
2588
+ } else if (
2589
+ 'authors' in docAttrs &&
2590
+ docAttrs.authors !== implicitAuthors
2591
+ ) {
2592
+ // authors attribute was set or overridden; re-parse as names only (allow multiple)
2593
+ authorMetadata = Parser.processAuthors(docAttrs.authors, true)
2594
+ } else {
2595
+ // check for individual author_N overrides
2596
+ const authors = []
2597
+ let authorIdx = 1
2598
+ let authorKey = 'author_1'
2599
+ let explicit = false
2600
+ let sparse = false
2601
+ while (authorKey in docAttrs) {
2602
+ const authorOverride = docAttrs[authorKey]
2603
+ if (authorOverride === implicitAuthorMetadata[authorKey]) {
2604
+ authors.push(null)
2605
+ sparse = true
2606
+ } else {
2607
+ authors.push(authorOverride)
2608
+ explicit = true
2609
+ }
2610
+ authorKey = `author_${++authorIdx}`
2611
+ }
2612
+ if (explicit) {
2613
+ if (sparse) {
2614
+ for (let idx = 0; idx < authors.length; idx++) {
2615
+ if (authors[idx] != null) continue
2616
+ const nameIdx = idx + 1
2617
+ const parts = [
2618
+ implicitAuthorMetadata[`firstname_${nameIdx}`],
2619
+ implicitAuthorMetadata[`middlename_${nameIdx}`],
2620
+ implicitAuthorMetadata[`lastname_${nameIdx}`],
2621
+ ]
2622
+ .filter(Boolean)
2623
+ .map((n) => n.replace(/ /g, '_'))
2624
+ authors[idx] = parts.join(' ')
2625
+ }
2626
+ }
2627
+ // process as names only (no multiple — each entry is already a single author)
2628
+ authorMetadata = Parser.processAuthors(authors, true, false)
2629
+ } else {
2630
+ authorMetadata = { authorcount: 0 }
2631
+ }
2632
+ }
2633
+
2634
+ if (authorMetadata.authorcount === 0) {
2635
+ if (authorcount != null) {
2636
+ authorMetadata = null
2637
+ } else {
2638
+ docAttrs.authorcount = 0
2639
+ }
2640
+ } else {
2641
+ Object.assign(docAttrs, authorMetadata)
2642
+ if (!('email' in docAttrs) && 'email_1' in docAttrs) {
2643
+ docAttrs.email = docAttrs.email_1
2644
+ }
2645
+ }
2646
+ }
2647
+
2648
+ if (!retrieve) return null
2649
+ return Object.assign({}, implicitAuthorMetadata, authorMetadata ?? {})
2650
+ }
2651
+
2652
+ /**
2653
+ * Parse the author line into a metadata object.
2654
+ * @returns {Object}
2655
+ * @internal
2656
+ */
2657
+ static processAuthors(authorLine, namesOnly = false, multiple = true) {
2658
+ const authorMetadata = {}
2659
+ let authorIdx = 0
2660
+ const entries =
2661
+ multiple && String(authorLine).includes(';')
2662
+ ? String(authorLine).split(AuthorDelimiterRx)
2663
+ : [].concat(authorLine)
2664
+
2665
+ for (const authorEntry of entries) {
2666
+ const entry = String(authorEntry)
2667
+ if (entry === '') continue
2668
+ authorIdx++
2669
+
2670
+ const keyMap = {}
2671
+ if (authorIdx === 1) {
2672
+ for (const key of AuthorKeys) keyMap[key] = key
2673
+ } else {
2674
+ for (const key of AuthorKeys) keyMap[key] = `${key}_${authorIdx}`
2675
+ }
2676
+
2677
+ let segments = null
2678
+ if (namesOnly) {
2679
+ let cleanEntry = entry
2680
+ if (entry.includes('<')) {
2681
+ authorMetadata[keyMap.author] = entry.replace(/_/g, ' ')
2682
+ cleanEntry = entry.replace(new RegExp(XmlSanitizeRx.source, 'g'), '')
2683
+ }
2684
+ // Ruby: split(nil, 3) — splits on whitespace, keeps remainder in 3rd element.
2685
+ // JS split with limit drops the remainder, so we split fully then cap at 3.
2686
+ const allParts = cleanEntry.split(/\s+/).filter(Boolean)
2687
+ const parts =
2688
+ allParts.length > 3
2689
+ ? [...allParts.slice(0, 2), allParts.slice(2).join(' ')]
2690
+ : allParts
2691
+ if (parts.length === 3) {
2692
+ const last = parts.pop()
2693
+ parts.push(last.replace(/ {2,}/g, ' '))
2694
+ }
2695
+ segments = parts
2696
+ } else {
2697
+ const m = entry.match(AuthorInfoLineRx)
2698
+ if (m) segments = m.slice(1)
2699
+ }
2700
+
2701
+ if (segments) {
2702
+ const fname = segments[0].replace(/_/g, ' ')
2703
+ authorMetadata[keyMap.firstname] = fname
2704
+ authorMetadata[keyMap.authorinitials] = fname[0]
2705
+ let author = fname
2706
+
2707
+ if (segments[1]) {
2708
+ if (segments[2]) {
2709
+ const mname = segments[1].replace(/_/g, ' ')
2710
+ const lname = segments[2].replace(/_/g, ' ')
2711
+ authorMetadata[keyMap.middlename] = mname
2712
+ authorMetadata[keyMap.lastname] = lname
2713
+ author = `${fname} ${mname} ${lname}`
2714
+ authorMetadata[keyMap.authorinitials] =
2715
+ `${fname[0]}${mname[0]}${lname[0]}`
2716
+ } else {
2717
+ const lname = segments[1].replace(/_/g, ' ')
2718
+ authorMetadata[keyMap.lastname] = lname
2719
+ author = `${fname} ${lname}`
2720
+ authorMetadata[keyMap.authorinitials] = `${fname[0]}${lname[0]}`
2721
+ }
2722
+ }
2723
+ authorMetadata[keyMap.author] ??= author
2724
+ if (!namesOnly && segments[3])
2725
+ authorMetadata[keyMap.email] = segments[3]
2726
+ } else {
2727
+ const author = entry.replace(/ {2,}/g, ' ').trim()
2728
+ authorMetadata[keyMap.author] = author
2729
+ authorMetadata[keyMap.firstname] = author
2730
+ authorMetadata[keyMap.authorinitials] = author[0]
2731
+ }
2732
+
2733
+ if (authorIdx === 1) {
2734
+ authorMetadata.authors = authorMetadata[keyMap.author]
2735
+ } else {
2736
+ if (authorIdx === 2) {
2737
+ for (const key of AuthorKeys) {
2738
+ if (key in authorMetadata)
2739
+ authorMetadata[`${key}_1`] = authorMetadata[key]
2740
+ }
2741
+ }
2742
+ authorMetadata.authors = `${authorMetadata.authors}, ${authorMetadata[keyMap.author]}`
2743
+ }
2744
+ }
2745
+
2746
+ authorMetadata.authorcount = authorIdx
2747
+ return authorMetadata
2748
+ }
2749
+
2750
+ /**
2751
+ * Parse block metadata lines.
2752
+ * @returns {Promise<Object>} Accumulated attributes.
2753
+ * @internal
2754
+ */
2755
+ static async parseBlockMetadataLines(
2756
+ reader,
2757
+ document,
2758
+ attributes = {},
2759
+ options = {}
2760
+ ) {
2761
+ while (
2762
+ await Parser.parseBlockMetadataLine(reader, document, attributes, options)
2763
+ ) {
2764
+ await reader.readLine()
2765
+ if ((await reader.skipBlankLines()) == null) break
2766
+ }
2767
+ return attributes
2768
+ }
2769
+
2770
+ /**
2771
+ * Parse the next line if it contains block metadata.
2772
+ * @returns {Promise<true|null>} True if the line is metadata, otherwise null.
2773
+ * @internal
2774
+ */
2775
+ static async parseBlockMetadataLine(
2776
+ reader,
2777
+ document,
2778
+ attributes,
2779
+ options = {}
2780
+ ) {
2781
+ const nextLine = await reader.peekLine()
2782
+ if (!nextLine) return null
2783
+
2784
+ const textOnly = options.text_only
2785
+ const normal =
2786
+ !textOnly &&
2787
+ (nextLine.startsWith('[') ||
2788
+ nextLine.startsWith('.') ||
2789
+ nextLine.startsWith('/') ||
2790
+ nextLine.startsWith(':'))
2791
+ const isAttrOrComment = textOnly
2792
+ ? nextLine.startsWith('[') || nextLine.startsWith('/')
2793
+ : normal
2794
+
2795
+ if (!isAttrOrComment) return null
2796
+
2797
+ if (nextLine.startsWith('[')) {
2798
+ if (nextLine.startsWith('[[')) {
2799
+ if (nextLine.endsWith(']]')) {
2800
+ const m = nextLine.match(BlockAnchorRx)
2801
+ if (m) {
2802
+ attributes.id = m[1]
2803
+ if (m[2]) {
2804
+ const reftext = m[2]
2805
+ attributes.reftext = reftext.includes(ATTR_REF_HEAD)
2806
+ ? document.subAttributes(reftext)
2807
+ : reftext
2808
+ }
2809
+ return true
2810
+ }
2811
+ }
2812
+ } else if (nextLine.endsWith(']')) {
2813
+ const m = nextLine.match(BlockAttributeListRx)
2814
+ if (m) {
2815
+ const currentStyle = attributes[1]
2816
+ const parsed = await document.parseAttributes(m[1], [], {
2817
+ sub_input: true,
2818
+ sub_result: true,
2819
+ into: attributes,
2820
+ })
2821
+ if (parsed[1]) {
2822
+ attributes[1] =
2823
+ Parser.parseStyleAttribute(attributes, reader) ?? currentStyle
2824
+ }
2825
+ return true
2826
+ }
2827
+ }
2828
+ } else if (normal && nextLine.startsWith('.')) {
2829
+ const m = nextLine.match(BlockTitleRx)
2830
+ if (m) {
2831
+ attributes.title = m[1]
2832
+ return true
2833
+ }
2834
+ } else if (!normal || nextLine.startsWith('/')) {
2835
+ if (nextLine === '//') return true
2836
+ if (
2837
+ normal &&
2838
+ nextLine.startsWith('//') &&
2839
+ _uniform(nextLine, '/', nextLine.length)
2840
+ ) {
2841
+ if (nextLine.length !== 3) {
2842
+ await reader.readLinesUntil({
2843
+ terminator: nextLine,
2844
+ skip_first_line: true,
2845
+ preserve_last_line: true,
2846
+ skip_processing: true,
2847
+ context: 'comment',
2848
+ })
2849
+ return true
2850
+ }
2851
+ } else if (nextLine.startsWith('//') && !nextLine.startsWith('///')) {
2852
+ return true
2853
+ }
2854
+ } else if (normal && nextLine.startsWith(':')) {
2855
+ const m = nextLine.match(AttributeEntryRx)
2856
+ if (m) {
2857
+ await Parser.processAttributeEntry(reader, document, attributes, m)
2858
+ return true
2859
+ }
2860
+ }
2861
+ return null
2862
+ }
2863
+
2864
+ /**
2865
+ * Process consecutive attribute entries.
2866
+ * @internal
2867
+ */
2868
+ static async processAttributeEntries(reader, document, attributes = null) {
2869
+ await reader.skipCommentLines()
2870
+ while (await Parser.processAttributeEntry(reader, document, attributes)) {
2871
+ await reader.readLine()
2872
+ await reader.skipCommentLines()
2873
+ }
2874
+ }
2875
+
2876
+ /**
2877
+ * Process a single attribute entry.
2878
+ * @returns {Promise<boolean>}
2879
+ * @internal
2880
+ */
2881
+ static async processAttributeEntry(
2882
+ reader,
2883
+ document,
2884
+ attributes = null,
2885
+ match = null
2886
+ ) {
2887
+ if (!match) {
2888
+ if (!(await reader.hasMoreLines())) return false
2889
+ const pLine = await reader.peekLine()
2890
+ const m = pLine ? pLine.match(AttributeEntryRx) : null
2891
+ if (!m) return false
2892
+ match = m
2893
+ }
2894
+
2895
+ let value = match[2] ?? ''
2896
+ if (value === '' || value == null) {
2897
+ value = ''
2898
+ } else if (
2899
+ value.endsWith(LINE_CONTINUATION) ||
2900
+ value.endsWith(LINE_CONTINUATION_LEGACY)
2901
+ ) {
2902
+ const conStr = value.slice(-2)
2903
+ value = value.slice(0, -2).trimEnd()
2904
+ while (await reader.advance()) {
2905
+ const nextLine = (await reader.peekLine()) ?? ''
2906
+ if (nextLine === '') break
2907
+ let next = nextLine.trimStart()
2908
+ const keepOpen = next.endsWith(conStr)
2909
+ if (keepOpen) next = next.slice(0, -2).trimEnd()
2910
+ value = `${value}${value.endsWith(' +') ? LF : ' '}${next}`
2911
+ if (!keepOpen) break
2912
+ }
2913
+ }
2914
+
2915
+ // Pre-process pass macros with full async subs (e.g. quotes) before storeAttribute.
2916
+ // The sync _applyAttributeValueSubs inside setAttribute cannot handle async subs.
2917
+ if (document && value !== '') {
2918
+ const passMatch = value.match(AttributeEntryPassMacroRx)
2919
+ if (passMatch) {
2920
+ value = await document._applyAttributeEntryValueSubs(value)
2921
+ Parser.storeAttribute(match[1], value, document, attributes, {
2922
+ skipSubs: true,
2923
+ })
2924
+ return true
2925
+ }
2926
+ }
2927
+
2928
+ Parser.storeAttribute(match[1], value, document, attributes)
2929
+ return true
2930
+ }
2931
+
2932
+ /**
2933
+ * Store the attribute in the document.
2934
+ * @param {string} name
2935
+ * @param {string} value
2936
+ * @param {Document|null} [doc=null]
2937
+ * @param {Object|null} [attrs=null]
2938
+ * @param {Object} [opts={}]
2939
+ * @returns {[string, string|null]} Tuple of the resolved name and value.
2940
+ */
2941
+ static storeAttribute(name, value, doc = null, attrs = null, opts = {}) {
2942
+ if (name.endsWith('!')) {
2943
+ name = name.slice(0, -1)
2944
+ value = null
2945
+ } else if (name.startsWith('!')) {
2946
+ name = name.slice(1)
2947
+ value = null
2948
+ }
2949
+
2950
+ name = Parser.sanitizeAttributeName(name)
2951
+
2952
+ if (name === 'numbered') name = 'sectnums'
2953
+ else if (name === 'hardbreaks') name = 'hardbreaks-option'
2954
+ else if (name === 'showtitle') {
2955
+ // Ruby: '' is truthy so `value ? nil : ''` unsets notitle when showtitle is set.
2956
+ // In JS, '' is falsy, so we test value !== null instead.
2957
+ Parser.storeAttribute('notitle', value !== null ? null : '', doc, attrs)
2958
+ }
2959
+
2960
+ if (doc) {
2961
+ if (value != null) {
2962
+ if (value !== '') {
2963
+ if (name === 'leveloffset') {
2964
+ const current =
2965
+ parseInt(doc.getAttribute('leveloffset', 0), 10) || 0
2966
+ if (value.startsWith('+'))
2967
+ value = String(current + parseInt(value.slice(1), 10))
2968
+ else if (value.startsWith('-'))
2969
+ value = String(current - parseInt(value.slice(1), 10))
2970
+ }
2971
+ }
2972
+ // value === '' means set to empty string (Ruby: '' is truthy → setAttribute path)
2973
+ const resolvedValue = opts.skipSubs
2974
+ ? doc._setAttributeRaw(name, value)
2975
+ : doc.setAttribute(name, value)
2976
+ if (resolvedValue != null) {
2977
+ value = resolvedValue
2978
+ if (attrs) new AttributeEntry(name, value).saveTo(attrs)
2979
+ }
2980
+ } else if (doc.deleteAttribute(name) && attrs) {
2981
+ new AttributeEntry(name, value).saveTo(attrs)
2982
+ }
2983
+ } else if (attrs) {
2984
+ new AttributeEntry(name, value).saveTo(attrs)
2985
+ }
2986
+
2987
+ return [name, value]
2988
+ }
2989
+
2990
+ /**
2991
+ * Read paragraph lines.
2992
+ * @returns {Promise<string[]>}
2993
+ * @internal
2994
+ */
2995
+ static async readParagraphLines(reader, breakAtList, opts = {}) {
2996
+ opts.break_on_blank_lines = true
2997
+ opts.break_on_list_continuation = true
2998
+ opts.preserve_last_line = true
2999
+
3000
+ // isPlaceholder matches only ListContinuationPlaceholder (empty boxed String), not ListContinuationString ('+').
3001
+ // We must not fire on ListContinuationString here because it would be preserved back to the reader,
3002
+ // and skipBlankLines would not consume it (String('+') ≠ ''), causing an infinite loop.
3003
+ const isPlaceholder = (l) => isListContinuation(l) && String(l) === ''
3004
+
3005
+ let breakCondition = null
3006
+ if (breakAtList) {
3007
+ breakCondition = Compliance.blockTerminatesParagraph
3008
+ ? (l) =>
3009
+ isPlaceholder(l) ||
3010
+ Parser.isDelimitedBlock(l) ||
3011
+ (l.startsWith('[') && BlockAttributeLineRx.test(l)) ||
3012
+ AnyListRx.test(l)
3013
+ : (l) => isPlaceholder(l) || AnyListRx.test(l)
3014
+ } else if (Compliance.blockTerminatesParagraph) {
3015
+ breakCondition = (l) =>
3016
+ isPlaceholder(l) ||
3017
+ (l.startsWith('[') && BlockAttributeLineRx.test(l)) ||
3018
+ Parser.isDelimitedBlock(l)
3019
+ } else {
3020
+ breakCondition = (l) => isPlaceholder(l)
3021
+ }
3022
+
3023
+ return await reader.readLinesUntil(opts, breakCondition)
3024
+ }
3025
+
3026
+ /**
3027
+ * Check if line is the start of a delimited block.
3028
+ * @param {string} line
3029
+ * @param {boolean} [returnMatchData=false]
3030
+ * @returns {{context: string, masq: string[], tip: string, terminator: string}|true|null}
3031
+ * BlockMatchData object if returnMatchData is true, true/null otherwise.
3032
+ */
3033
+ static isDelimitedBlock(line, returnMatchData = false) {
3034
+ let lineLen = line.length
3035
+ if (lineLen < 2 || !DELIMITED_BLOCK_HEADS[line.slice(0, 2)]) return null
3036
+
3037
+ let tip, tipLen
3038
+
3039
+ if (lineLen === 2) {
3040
+ tip = line
3041
+ tipLen = 2
3042
+ } else {
3043
+ tipLen = lineLen < 5 ? lineLen : 4
3044
+ tip = line.slice(0, tipLen)
3045
+
3046
+ // Fenced code special case
3047
+ if (Compliance.markdownSyntax && tip.startsWith('`')) {
3048
+ if (tipLen === 4) {
3049
+ if (tip === '````') return null
3050
+ tip = tip.slice(0, 3)
3051
+ if (tip !== '```') return null
3052
+ // Mirror Ruby: line = tip; line_len = tip_len = 3
3053
+ // This ensures the returned terminator is '```', not the full opener line
3054
+ // (e.g. '```ruby'), so that readLinesUntil finds the correct closing delimiter.
3055
+ line = tip
3056
+ lineLen = tipLen = 3
3057
+ } else if (tip !== '```') {
3058
+ return null
3059
+ }
3060
+ } else if (tipLen === 3) {
3061
+ return null
3062
+ }
3063
+ }
3064
+
3065
+ const entry = DELIMITED_BLOCKS[tip]
3066
+ if (!entry) return null
3067
+ const [context, masq] = entry
3068
+
3069
+ const isMatch =
3070
+ lineLen === tipLen ||
3071
+ (DELIMITED_BLOCK_TAILS[tip] != null &&
3072
+ _uniform(line.slice(1), DELIMITED_BLOCK_TAILS[tip], lineLen - 1))
3073
+ if (!isMatch) return null
3074
+
3075
+ return returnMatchData ? { context, masq, tip, terminator: line } : true
3076
+ }
3077
+
3078
+ /**
3079
+ * Resolve the list marker for a list item.
3080
+ * @returns {string}
3081
+ * @internal
3082
+ */
3083
+ static resolveListMarker(listType, marker) {
3084
+ if (listType === 'ulist') return marker
3085
+ if (listType === 'olist') return Parser.resolveOrderedListMarker(marker)[0]
3086
+ return '<1>'
3087
+ }
3088
+
3089
+ /**
3090
+ * Resolve the normalized ordered list marker.
3091
+ * @returns {[string]|[string, string]} Tuple of [normalizedMarker] or [normalizedMarker, style].
3092
+ * @internal
3093
+ */
3094
+ static resolveOrderedListMarker(
3095
+ marker,
3096
+ ordinal = null,
3097
+ validate = false,
3098
+ reader = null
3099
+ ) {
3100
+ if (marker.startsWith('.')) return [marker]
3101
+
3102
+ const style = ORDERED_LIST_STYLES.find((s) =>
3103
+ OrderedListMarkerRxMap[s].test(marker)
3104
+ )
3105
+ let normalizedMarker, expected, actual
3106
+
3107
+ switch (style) {
3108
+ case 'arabic':
3109
+ if (validate) {
3110
+ expected = String(ordinal + 1)
3111
+ actual = String(parseInt(marker, 10))
3112
+ }
3113
+ normalizedMarker = '1.'
3114
+ break
3115
+ case 'loweralpha':
3116
+ if (validate) {
3117
+ expected = String.fromCharCode(97 + ordinal)
3118
+ actual = marker.slice(0, -1)
3119
+ }
3120
+ normalizedMarker = 'a.'
3121
+ break
3122
+ case 'upperalpha':
3123
+ if (validate) {
3124
+ expected = String.fromCharCode(65 + ordinal)
3125
+ actual = marker.slice(0, -1)
3126
+ }
3127
+ normalizedMarker = 'A.'
3128
+ break
3129
+ case 'lowerroman':
3130
+ if (validate) {
3131
+ expected = intToRoman(ordinal + 1).toLowerCase()
3132
+ actual = marker.slice(0, -1)
3133
+ }
3134
+ normalizedMarker = 'i)'
3135
+ break
3136
+ case 'upperroman':
3137
+ if (validate) {
3138
+ expected = intToRoman(ordinal + 1)
3139
+ actual = marker.slice(0, -1)
3140
+ }
3141
+ normalizedMarker = 'I)'
3142
+ break
3143
+ default:
3144
+ normalizedMarker = marker
3145
+ }
3146
+
3147
+ if (ordinal != null) {
3148
+ if (validate && expected !== actual) {
3149
+ Parser.logger.warn(
3150
+ Parser.messageWithContext(
3151
+ `list item index: expected ${expected}, got ${actual}`,
3152
+ { source_location: reader?.cursor }
3153
+ )
3154
+ )
3155
+ }
3156
+ return [normalizedMarker, style]
3157
+ }
3158
+ return [normalizedMarker]
3159
+ }
3160
+
3161
+ /**
3162
+ * Resolve the start value for an ordered list.
3163
+ * @param {string} marker
3164
+ * @returns {number}
3165
+ * @internal
3166
+ */
3167
+ static resolveOrderedListStart(marker) {
3168
+ if (marker.startsWith('.')) return 1
3169
+ const style = ORDERED_LIST_STYLES.find((s) =>
3170
+ OrderedListMarkerRxMap[s].test(marker)
3171
+ )
3172
+ switch (style) {
3173
+ case 'arabic':
3174
+ return parseInt(marker, 10)
3175
+ case 'loweralpha':
3176
+ return marker.slice(0, -1).charCodeAt(0) - 96
3177
+ case 'upperalpha':
3178
+ return marker.slice(0, -1).charCodeAt(0) - 64
3179
+ case 'lowerroman':
3180
+ return romanToInt(marker.slice(0, -1).toUpperCase())
3181
+ case 'upperroman':
3182
+ return romanToInt(marker.slice(0, -1))
3183
+ default:
3184
+ return 1
3185
+ }
3186
+ }
3187
+
3188
+ /**
3189
+ * Check if this line is a sibling list item.
3190
+ * @returns {boolean}
3191
+ * @internal
3192
+ */
3193
+ static isSiblingListItem(line, listType, siblingTrait) {
3194
+ if (siblingTrait instanceof RegExp) return siblingTrait.test(line)
3195
+ const m = line.match(ListRxMap[listType])
3196
+ if (!m) return false
3197
+ const resolvedSibling = Parser.resolveListMarker(listType, siblingTrait)
3198
+ return resolvedSibling === Parser.resolveListMarker(listType, m[1])
3199
+ }
3200
+
3201
+ /**
3202
+ * Parse a table.
3203
+ * @returns {Promise<Table>}
3204
+ * @internal
3205
+ */
3206
+ static async parseTable(tableReader, parent, attributes) {
3207
+ const table = new Table(parent, attributes)
3208
+
3209
+ let explicitColspecs = false
3210
+ if ('cols' in attributes) {
3211
+ const colspecs = Parser.parseColspecs(attributes.cols)
3212
+ if (colspecs.length > 0) {
3213
+ table.createColumns(colspecs)
3214
+ explicitColspecs = true
3215
+ }
3216
+ }
3217
+
3218
+ const skipped = (await tableReader.skipBlankLines()) ?? 0
3219
+ if ('header-option' in attributes) {
3220
+ table.hasHeaderOption = true
3221
+ } else if (skipped === 0 && !('noheader-option' in attributes)) {
3222
+ table.hasHeaderOption = 'implicit'
3223
+ }
3224
+ let implicitHeader = table.hasHeaderOption === 'implicit'
3225
+
3226
+ const parserCtx = new Table.ParserContext(tableReader, table, attributes)
3227
+ const format = parserCtx.format
3228
+ let loopIdx = -1
3229
+ let implicitHeaderBoundary = null
3230
+
3231
+ while (true) {
3232
+ let line = await tableReader.readLine()
3233
+ if (line == null) break
3234
+
3235
+ const beyondFirst = ++loopIdx > 0
3236
+ if (beyondFirst && line === '') {
3237
+ line = null
3238
+ if (implicitHeaderBoundary != null) implicitHeaderBoundary++
3239
+ } else if (format === 'psv') {
3240
+ if (parserCtx.startsWith(line)) {
3241
+ line = line.slice(1)
3242
+ await parserCtx.closeOpenCell()
3243
+ if (implicitHeaderBoundary != null) implicitHeaderBoundary = null
3244
+ } else {
3245
+ const [nextCellspec, rest] = Parser.parseCellspec(
3246
+ line,
3247
+ 'start',
3248
+ parserCtx.delimiter
3249
+ )
3250
+ if (nextCellspec != null) {
3251
+ await parserCtx.closeOpenCell(nextCellspec)
3252
+ if (implicitHeaderBoundary != null) implicitHeaderBoundary = null
3253
+ } else if (
3254
+ implicitHeaderBoundary != null &&
3255
+ implicitHeaderBoundary === loopIdx
3256
+ ) {
3257
+ table.hasHeaderOption =
3258
+ implicitHeader =
3259
+ implicitHeaderBoundary =
3260
+ null
3261
+ }
3262
+ line = rest
3263
+ }
3264
+ }
3265
+
3266
+ if (!beyondFirst) {
3267
+ tableReader.mark()
3268
+ if (implicitHeader) {
3269
+ if (
3270
+ (await tableReader.hasMoreLines()) &&
3271
+ (await tableReader.peekLine()) === ''
3272
+ ) {
3273
+ implicitHeaderBoundary = 1
3274
+ } else {
3275
+ table.hasHeaderOption = implicitHeader = null
3276
+ }
3277
+ }
3278
+ }
3279
+
3280
+ // Inner loop for cell delimiter processing
3281
+ while (true) {
3282
+ if (line != null) {
3283
+ const m = line.match(parserCtx.delimiterRe)
3284
+ if (m) {
3285
+ const preMatch = line.slice(0, m.index)
3286
+ const postMatch = line.slice(m.index + m[0].length)
3287
+ if (format === 'csv') {
3288
+ if (parserCtx.bufferHasUnclosedQuotes(preMatch)) {
3289
+ parserCtx.skipPastDelimiter(preMatch)
3290
+ line = postMatch
3291
+ if (line === '') break
3292
+ continue
3293
+ }
3294
+ parserCtx.buffer += preMatch
3295
+ } else if (format === 'dsv') {
3296
+ if (preMatch.endsWith('\\')) {
3297
+ parserCtx.skipPastEscapedDelimiter(preMatch)
3298
+ if (postMatch === '') {
3299
+ parserCtx.buffer += LF
3300
+ parserCtx.keepCellOpen()
3301
+ break
3302
+ }
3303
+ line = postMatch
3304
+ continue
3305
+ }
3306
+ parserCtx.buffer += preMatch
3307
+ } else {
3308
+ if (preMatch.endsWith('\\')) {
3309
+ parserCtx.skipPastEscapedDelimiter(preMatch)
3310
+ if (postMatch === '') {
3311
+ parserCtx.buffer += LF
3312
+ parserCtx.keepCellOpen()
3313
+ break
3314
+ }
3315
+ line = postMatch
3316
+ continue
3317
+ }
3318
+ const [nextSpec, cellText] = Parser.parseCellspec(preMatch)
3319
+ parserCtx.pushCellspec(nextSpec)
3320
+ parserCtx.buffer += cellText
3321
+ }
3322
+ line = postMatch || null
3323
+ await parserCtx.closeCell()
3324
+ if (postMatch === '') {
3325
+ if (format === 'csv' || format === 'dsv') {
3326
+ await parserCtx.closeCell(true)
3327
+ } else if (format === 'psv') {
3328
+ parserCtx.keepCellOpen()
3329
+ }
3330
+ }
3331
+ } else {
3332
+ parserCtx.buffer += line + LF
3333
+ if (format === 'csv') {
3334
+ if (parserCtx.bufferHasUnclosedQuotes()) {
3335
+ if (implicitHeaderBoundary != null && loopIdx === 0) {
3336
+ table.hasHeaderOption =
3337
+ implicitHeader =
3338
+ implicitHeaderBoundary =
3339
+ null
3340
+ }
3341
+ parserCtx.keepCellOpen()
3342
+ } else {
3343
+ await parserCtx.closeCell(true)
3344
+ }
3345
+ } else if (format === 'dsv') {
3346
+ await parserCtx.closeCell(true)
3347
+ } else {
3348
+ parserCtx.keepCellOpen()
3349
+ }
3350
+ break
3351
+ }
3352
+ } else {
3353
+ // null line = blank line; preserve in buffer so multi-paragraph cells are detected
3354
+ if (format === 'psv' && parserCtx.buffer !== '') {
3355
+ parserCtx.buffer += LF
3356
+ parserCtx.keepCellOpen()
3357
+ } else if (format === 'csv' && parserCtx.isCellOpen()) {
3358
+ parserCtx.buffer += LF
3359
+ }
3360
+ break
3361
+ }
3362
+ }
3363
+
3364
+ if (parserCtx.isCellOpen()) {
3365
+ if (!(await tableReader.hasMoreLines())) await parserCtx.closeCell(true)
3366
+ } else {
3367
+ if ((await tableReader.skipBlankLines()) == null) break
3368
+ }
3369
+ }
3370
+
3371
+ await parserCtx.closeTable()
3372
+ if (
3373
+ (table.attributes.colcount ??= table.columns.length) !== 0 &&
3374
+ !explicitColspecs
3375
+ ) {
3376
+ table.assignColumnWidths()
3377
+ }
3378
+ if (implicitHeader) table.hasHeaderOption = true
3379
+ await table.partitionHeaderFooter(attributes)
3380
+
3381
+ return table
3382
+ }
3383
+
3384
+ /**
3385
+ * Parse column specs.
3386
+ * @param {string} records
3387
+ * @returns {Object[]}
3388
+ * @internal
3389
+ */
3390
+ static parseColspecs(records) {
3391
+ records = records.replace(/ /g, '')
3392
+ if (!records) return []
3393
+ if (records === String(parseInt(records, 10))) {
3394
+ return Array.from({ length: parseInt(records, 10) }, () => ({ width: 1 }))
3395
+ }
3396
+ const specs = []
3397
+ const parts = records.includes(',')
3398
+ ? records.split(',')
3399
+ : records.split(';')
3400
+ for (const record of parts) {
3401
+ if (record === '') {
3402
+ specs.push({ width: 1 })
3403
+ } else {
3404
+ const m = record.match(ColumnSpecRx)
3405
+ if (!m) continue
3406
+ const spec = {}
3407
+ if (m[2]) {
3408
+ const [colspec, rowspec] = m[2].split('.')
3409
+ if (colspec && TableCellHorzAlignments[colspec])
3410
+ spec.halign = TableCellHorzAlignments[colspec]
3411
+ if (rowspec && TableCellVertAlignments[rowspec])
3412
+ spec.valign = TableCellVertAlignments[rowspec]
3413
+ }
3414
+ spec.width = m[3] ? (m[3] === '~' ? -1 : parseInt(m[3], 10)) : 1
3415
+ if (m[4] && TableCellStyles[m[4]]) spec.style = TableCellStyles[m[4]]
3416
+ const repeat = m[1] ? parseInt(m[1], 10) : 1
3417
+ for (let i = 0; i < repeat; i++) specs.push({ ...spec })
3418
+ }
3419
+ }
3420
+ return specs
3421
+ }
3422
+
3423
+ /**
3424
+ * Parse cell test from line.
3425
+ * @param {string} line
3426
+ * @param {'start'|'end'} [pos='end']
3427
+ * @param {string|null} [delimiter=null]
3428
+ * @returns {[Object|null, string]} Tuple of [test, rest].
3429
+ * @internal
3430
+ */
3431
+ static parseCellspec(line, pos = 'end', delimiter = null) {
3432
+ let m,
3433
+ rest = ''
3434
+
3435
+ if (pos === 'start') {
3436
+ if (!line.includes(delimiter)) return [null, line]
3437
+ const delimIdx = line.indexOf(delimiter)
3438
+ const specPart = line.slice(0, delimIdx)
3439
+ rest = line.slice(delimIdx + delimiter.length)
3440
+ m = specPart.match(CellSpecStartRx)
3441
+ if (!m) return [null, line]
3442
+ if (m[0] === '') return [{}, rest]
3443
+ if (specPart.trim() === '') return [null, line]
3444
+ } else {
3445
+ m = line.match(CellSpecEndRx)
3446
+ if (!m) return [{}, line]
3447
+ if (m[0].trimStart() === '') return [{}, line.trimEnd()]
3448
+ rest = line.slice(0, m.index)
3449
+ }
3450
+
3451
+ const spec = {}
3452
+ if (m[1]) {
3453
+ const [colspec, rowspec] = m[1].split('.')
3454
+ const cs = colspec ? parseInt(colspec, 10) : 1
3455
+ const rs = rowspec ? parseInt(rowspec, 10) : 1
3456
+ if (m[2] === '+') {
3457
+ if (cs !== 1) spec.colspan = cs
3458
+ if (rs !== 1) spec.rowspan = rs
3459
+ } else if (m[2] === '*') {
3460
+ if (cs !== 1) spec.repeatcol = cs
3461
+ }
3462
+ }
3463
+ if (m[3]) {
3464
+ const [colspec, rowspec] = m[3].split('.')
3465
+ if (colspec && TableCellHorzAlignments[colspec])
3466
+ spec.halign = TableCellHorzAlignments[colspec]
3467
+ if (rowspec && TableCellVertAlignments[rowspec])
3468
+ spec.valign = TableCellVertAlignments[rowspec]
3469
+ }
3470
+ if (m[4] && TableCellStyles[m[4]]) spec.style = TableCellStyles[m[4]]
3471
+
3472
+ return [spec, rest]
3473
+ }
3474
+
3475
+ /**
3476
+ * Parse the first positional attribute for style, role, id, and options.
3477
+ * @param {Object} attributes
3478
+ * @param {Reader|null} [reader=null]
3479
+ * @returns {string|null} The resolved style value.
3480
+ */
3481
+ static parseStyleAttribute(attributes, reader = null) {
3482
+ const rawStyle = attributes[1]
3483
+ if (
3484
+ !rawStyle ||
3485
+ rawStyle.includes(' ') ||
3486
+ !Compliance.shorthandPropertySyntax
3487
+ ) {
3488
+ return (attributes.style = rawStyle)
3489
+ }
3490
+
3491
+ let name = null
3492
+ let accum = ''
3493
+ const parsed = {}
3494
+
3495
+ for (const c of rawStyle) {
3496
+ if (c === '.') {
3497
+ Parser._yieldBufferedAttribute(parsed, name, accum, reader)
3498
+ accum = ''
3499
+ name = 'role'
3500
+ } else if (c === '#') {
3501
+ Parser._yieldBufferedAttribute(parsed, name, accum, reader)
3502
+ accum = ''
3503
+ name = 'id'
3504
+ } else if (c === '%') {
3505
+ Parser._yieldBufferedAttribute(parsed, name, accum, reader)
3506
+ accum = ''
3507
+ name = 'option'
3508
+ } else {
3509
+ accum += c
3510
+ }
3511
+ }
3512
+
3513
+ if (name) {
3514
+ Parser._yieldBufferedAttribute(parsed, name, accum, reader)
3515
+ if (parsed.style) attributes.style = parsed.style
3516
+ if ('id' in parsed) attributes.id = parsed.id
3517
+ if ('role' in parsed) {
3518
+ const existing = attributes.role
3519
+ attributes.role =
3520
+ !existing || existing === ''
3521
+ ? parsed.role.join(' ')
3522
+ : `${existing} ${parsed.role.join(' ')}`
3523
+ }
3524
+ if ('option' in parsed) {
3525
+ for (const opt of parsed.option) attributes[`${opt}-option`] = ''
3526
+ }
3527
+ return parsed.style ?? null
3528
+ }
3529
+ return (attributes.style = rawStyle)
3530
+ }
3531
+
3532
+ static _yieldBufferedAttribute(attrs, name, value, reader) {
3533
+ if (name) {
3534
+ if (value === '') {
3535
+ const msg = `invalid empty ${name} detected in style attribute`
3536
+ if (reader)
3537
+ Parser.logger.warn(
3538
+ Parser.messageWithContext(msg, {
3539
+ source_location: reader.cursorAtPrevLine(),
3540
+ })
3541
+ )
3542
+ else Parser.logger.warn(msg)
3543
+ } else if (name === 'id') {
3544
+ if ('id' in attrs) {
3545
+ const msg = 'multiple ids detected in style attribute'
3546
+ if (reader)
3547
+ Parser.logger.warn(
3548
+ Parser.messageWithContext(msg, {
3549
+ source_location: reader.cursorAtPrevLine(),
3550
+ })
3551
+ )
3552
+ else Parser.logger.warn(msg)
3553
+ }
3554
+ attrs.id = value
3555
+ } else {
3556
+ ;(attrs[name] ??= []).push(value)
3557
+ }
3558
+ } else if (value !== '') {
3559
+ attrs.style = value
3560
+ }
3561
+ }
3562
+
3563
+ /**
3564
+ * Remove block indentation and optionally expand tabs.
3565
+ * @param {string[]} lines - Modified in place.
3566
+ * @param {number} [indentSize=0]
3567
+ * @param {number} [tabSize=0]
3568
+ * @internal
3569
+ */
3570
+ static adjustIndentation(lines, indentSize = 0, tabSize = 0) {
3571
+ if (!lines || lines.length === 0) return
3572
+
3573
+ if (tabSize > 0 && lines.some((l) => l.includes('\t'))) {
3574
+ for (let i = 0; i < lines.length; i++) {
3575
+ const line = lines[i]
3576
+ if (line === '' || !line.includes('\t')) continue
3577
+ let result = ''
3578
+ let spacesAdded = 0
3579
+ let idx = 0
3580
+ for (const c of line) {
3581
+ if (c === '\t') {
3582
+ const offset = idx + spacesAdded
3583
+ const spaces = tabSize - (offset % tabSize) || tabSize
3584
+ spacesAdded += spaces - 1
3585
+ result += ' '.repeat(spaces)
3586
+ } else {
3587
+ result += c
3588
+ }
3589
+ idx++
3590
+ }
3591
+ lines[i] = result
3592
+ }
3593
+ }
3594
+
3595
+ if (indentSize < 0) return
3596
+
3597
+ let blockIndent = null
3598
+ for (const line of lines) {
3599
+ if (String(line) === '') continue
3600
+ const lineIndent = line.length - line.trimStart().length
3601
+ if (lineIndent === 0) {
3602
+ blockIndent = null
3603
+ break
3604
+ }
3605
+ if (blockIndent == null || lineIndent < blockIndent)
3606
+ blockIndent = lineIndent
3607
+ }
3608
+
3609
+ if (indentSize === 0) {
3610
+ if (blockIndent) {
3611
+ for (let i = 0; i < lines.length; i++) {
3612
+ if (String(lines[i]) !== '' && !isListContinuation(lines[i]))
3613
+ lines[i] = lines[i].slice(blockIndent)
3614
+ }
3615
+ }
3616
+ } else {
3617
+ const newIndent = ' '.repeat(indentSize)
3618
+ for (let i = 0; i < lines.length; i++) {
3619
+ if (String(lines[i]) !== '' && !isListContinuation(lines[i])) {
3620
+ lines[i] =
3621
+ newIndent + (blockIndent ? lines[i].slice(blockIndent) : lines[i])
3622
+ }
3623
+ }
3624
+ }
3625
+ }
3626
+
3627
+ /**
3628
+ * Check if string is uniform (all same character).
3629
+ * @param {string} str
3630
+ * @param {string} chr
3631
+ * @param {number} len
3632
+ * @returns {boolean}
3633
+ * @internal
3634
+ */
3635
+ static uniform(str, chr, len) {
3636
+ if (str.length !== len) return false
3637
+ for (const c of str) if (c !== chr) return false
3638
+ return true
3639
+ }
3640
+
3641
+ /**
3642
+ * Convert an attribute name to a legal form.
3643
+ * @param {string} name
3644
+ * @returns {string}
3645
+ * @internal
3646
+ */
3647
+ static sanitizeAttributeName(name) {
3648
+ return name
3649
+ .replace(new RegExp(InvalidAttributeNameCharsRx.source, 'gu'), '')
3650
+ .toLowerCase()
3651
+ }
3652
+ }
3653
+
3654
+ // Apply logging mixin to the Parser class itself (static methods use it via singleton)
3655
+ applyLogging(Parser)
3656
+
3657
+ // ── Module-level helpers ──────────────────────────────────────────────────────
3658
+
3659
+ function _uniform(str, chr, len) {
3660
+ return Parser.uniform(str, chr, len)
3661
+ }