@asciidoctor/core 3.0.3 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -10
- package/build/browser/index.js +24154 -0
- package/build/node/index.cjs +24735 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +53 -100
- package/src/abstract_block.js +849 -0
- package/src/abstract_node.js +954 -0
- package/src/attribute_entry.js +12 -0
- package/src/attribute_list.js +380 -0
- package/src/block.js +168 -0
- package/src/browser/asset.js +22 -0
- package/src/browser/reader.js +138 -0
- package/src/browser.js +121 -0
- package/src/callouts.js +85 -0
- package/src/compliance.js +54 -0
- package/src/constants.js +665 -0
- package/src/convert.js +370 -0
- package/src/converter/composite.js +83 -0
- package/src/converter/docbook5.js +1031 -0
- package/src/converter/html5.js +1899 -0
- package/src/converter/manpage.js +935 -0
- package/src/converter/template.js +459 -0
- package/src/converter.js +478 -0
- package/src/data/stylesheet-data.js +2 -0
- package/src/document.js +2134 -0
- package/src/extensions.js +1952 -0
- package/src/footnote.js +28 -0
- package/src/helpers.js +355 -0
- package/src/index.js +138 -0
- package/src/inline.js +158 -0
- package/src/list.js +240 -0
- package/src/load.js +276 -0
- package/src/logging.js +526 -0
- package/src/parser.js +3661 -0
- package/src/path_resolver.js +472 -0
- package/src/reader.js +1755 -0
- package/src/rx.js +829 -0
- package/src/section.js +354 -0
- package/src/stylesheets.js +30 -0
- package/src/substitutors.js +2241 -0
- package/src/syntaxHighlighter/highlightjs.js +90 -0
- package/src/syntaxHighlighter/html_pipeline.js +33 -0
- package/src/syntax_highlighter.js +304 -0
- package/src/table.js +952 -0
- package/src/timings.js +78 -0
- package/types/abstract_block.d.ts +343 -0
- package/types/abstract_node.d.ts +471 -0
- package/types/attribute_entry.d.ts +7 -0
- package/types/attribute_list.d.ts +52 -0
- package/types/block.d.ts +55 -0
- package/types/browser/asset.d.ts +7 -0
- package/types/browser/reader.d.ts +29 -0
- package/types/callouts.d.ts +36 -0
- package/types/compliance.d.ts +23 -0
- package/types/constants.d.ts +268 -0
- package/types/convert.d.ts +34 -0
- package/types/converter/composite.d.ts +20 -0
- package/types/converter/docbook5.d.ts +41 -0
- package/types/converter/html5.d.ts +51 -0
- package/types/converter/manpage.d.ts +59 -0
- package/types/converter/template.d.ts +83 -0
- package/types/converter.d.ts +150 -0
- package/types/data/stylesheet-data.d.ts +2 -0
- package/types/document.d.ts +495 -0
- package/types/extensions.d.ts +876 -0
- package/types/footnote.d.ts +18 -0
- package/types/helpers.d.ts +146 -0
- package/types/index.d.ts +73 -3731
- package/types/inline.d.ts +69 -0
- package/types/list.d.ts +114 -0
- package/types/load.d.ts +39 -0
- package/types/logging.d.ts +187 -0
- package/types/parser.d.ts +114 -0
- package/types/path_resolver.d.ts +103 -0
- package/types/reader.d.ts +184 -0
- package/types/rx.d.ts +513 -0
- package/types/section.d.ts +122 -0
- package/types/stylesheets.d.ts +10 -0
- package/types/substitutors.d.ts +208 -0
- package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
- package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
- package/types/syntax_highlighter.d.ts +167 -0
- package/types/table.d.ts +231 -0
- package/types/timings.d.ts +25 -0
- package/types/tsconfig.json +9 -0
- package/LICENSE +0 -21
- package/dist/browser/asciidoctor.js +0 -47654
- package/dist/browser/asciidoctor.min.js +0 -1452
- package/dist/graalvm/asciidoctor.js +0 -47402
- package/dist/node/asciidoctor.cjs +0 -21567
- package/dist/node/asciidoctor.js +0 -23037
package/src/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
|
+
}
|