@asciidoctor/core 3.0.4 → 4.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -10
- package/build/browser/index.js +24156 -0
- package/build/node/index.cjs +24737 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +55 -97
- package/src/abstract_block.js +857 -0
- package/src/abstract_node.js +954 -0
- package/src/attribute_entry.js +12 -0
- package/src/attribute_list.js +380 -0
- package/src/block.js +168 -0
- package/src/browser/asset.js +22 -0
- package/src/browser/reader.js +138 -0
- package/src/browser.js +121 -0
- package/src/callouts.js +85 -0
- package/src/compliance.js +54 -0
- package/src/constants.js +665 -0
- package/src/convert.js +370 -0
- package/src/converter/composite.js +83 -0
- package/src/converter/docbook5.js +1031 -0
- package/src/converter/html5.js +1893 -0
- package/src/converter/manpage.js +935 -0
- package/src/converter/template.js +459 -0
- package/src/converter.js +478 -0
- package/src/data/stylesheet-data.js +2 -0
- package/src/document.js +2134 -0
- package/src/extensions.js +1952 -0
- package/src/footnote.js +28 -0
- package/src/helpers.js +355 -0
- package/src/index.js +138 -0
- package/src/inline.js +158 -0
- package/src/list.js +240 -0
- package/src/load.js +276 -0
- package/src/logging.js +526 -0
- package/src/parser.js +3661 -0
- package/src/path_resolver.js +472 -0
- package/src/reader.js +1755 -0
- package/src/rx.js +829 -0
- package/src/section.js +354 -0
- package/src/stylesheets.js +30 -0
- package/src/substitutors.js +2241 -0
- package/src/syntaxHighlighter/highlightjs.js +90 -0
- package/src/syntaxHighlighter/html_pipeline.js +33 -0
- package/src/syntax_highlighter.js +304 -0
- package/src/table.js +952 -0
- package/src/timings.js +78 -0
- package/types/abstract_block.d.ts +346 -0
- package/types/abstract_node.d.ts +471 -0
- package/types/attribute_entry.d.ts +7 -0
- package/types/attribute_list.d.ts +52 -0
- package/types/block.d.ts +55 -0
- package/types/browser/asset.d.ts +7 -0
- package/types/browser/reader.d.ts +29 -0
- package/types/callouts.d.ts +36 -0
- package/types/compliance.d.ts +23 -0
- package/types/constants.d.ts +268 -0
- package/types/convert.d.ts +34 -0
- package/types/converter/composite.d.ts +20 -0
- package/types/converter/docbook5.d.ts +41 -0
- package/types/converter/html5.d.ts +51 -0
- package/types/converter/manpage.d.ts +59 -0
- package/types/converter/template.d.ts +83 -0
- package/types/converter.d.ts +150 -0
- package/types/data/stylesheet-data.d.ts +2 -0
- package/types/document.d.ts +495 -0
- package/types/extensions.d.ts +876 -0
- package/types/footnote.d.ts +18 -0
- package/types/helpers.d.ts +146 -0
- package/types/index.d.cts +75 -0
- package/types/index.d.ts +73 -3731
- package/types/inline.d.ts +69 -0
- package/types/list.d.ts +114 -0
- package/types/load.d.ts +39 -0
- package/types/logging.d.ts +187 -0
- package/types/parser.d.ts +114 -0
- package/types/path_resolver.d.ts +103 -0
- package/types/reader.d.ts +184 -0
- package/types/rx.d.ts +513 -0
- package/types/section.d.ts +122 -0
- package/types/stylesheets.d.ts +10 -0
- package/types/substitutors.d.ts +208 -0
- package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
- package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
- package/types/syntax_highlighter.d.ts +167 -0
- package/types/table.d.ts +231 -0
- package/types/timings.d.ts +25 -0
- package/types/tsconfig.json +9 -0
- package/LICENSE +0 -21
- package/dist/browser/asciidoctor.js +0 -47654
- package/dist/browser/asciidoctor.min.js +0 -1452
- package/dist/graalvm/asciidoctor.js +0 -47402
- package/dist/node/asciidoctor.cjs +0 -21567
- package/dist/node/asciidoctor.js +0 -23037
|
@@ -0,0 +1,2241 @@
|
|
|
1
|
+
// ESM conversion of substitutors.rb
|
|
2
|
+
// This module is intended to be mixed into Section and Block via Object.assign(Target.prototype, Substitutors)
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
LF,
|
|
6
|
+
ATTR_REF_HEAD,
|
|
7
|
+
HARD_LINE_BREAK,
|
|
8
|
+
INTRINSIC_ATTRIBUTES,
|
|
9
|
+
REPLACEMENTS,
|
|
10
|
+
ReplaceableTextRx,
|
|
11
|
+
QUOTE_SUBS,
|
|
12
|
+
InlinePassMacroRx,
|
|
13
|
+
InlinePassRx,
|
|
14
|
+
InlineStemMacroRx,
|
|
15
|
+
STEM_TYPE_ALIASES,
|
|
16
|
+
InlineKbdBtnMacroRx,
|
|
17
|
+
InlineMenuMacroRx,
|
|
18
|
+
InlineMenuRx,
|
|
19
|
+
InlineImageMacroRx,
|
|
20
|
+
InlineIndextermMacroRx,
|
|
21
|
+
InlineLinkRx,
|
|
22
|
+
UriSniffRx,
|
|
23
|
+
InlineLinkMacroRx,
|
|
24
|
+
InlineEmailRx,
|
|
25
|
+
InlineBiblioAnchorRx,
|
|
26
|
+
InlineAnchorRx,
|
|
27
|
+
InlineXrefMacroRx,
|
|
28
|
+
InlineFootnoteMacroRx,
|
|
29
|
+
HardLineBreakRx,
|
|
30
|
+
CalloutSourceRxMap,
|
|
31
|
+
CalloutSourceRx,
|
|
32
|
+
CalloutExtractRxMap,
|
|
33
|
+
CalloutExtractRx,
|
|
34
|
+
SubModifierSniffRx,
|
|
35
|
+
ASCIIDOC_EXTENSIONS,
|
|
36
|
+
AttributeReferenceRx,
|
|
37
|
+
BASIC_SUBS,
|
|
38
|
+
NORMAL_SUBS,
|
|
39
|
+
} from './constants.js'
|
|
40
|
+
import { Compliance } from './compliance.js'
|
|
41
|
+
import { Parser } from './parser.js'
|
|
42
|
+
import {
|
|
43
|
+
asyncReplace,
|
|
44
|
+
basename,
|
|
45
|
+
encodeUriComponent,
|
|
46
|
+
isExtname,
|
|
47
|
+
} from './helpers.js'
|
|
48
|
+
import { Inline } from './inline.js'
|
|
49
|
+
import { AttributeList } from './attribute_list.js'
|
|
50
|
+
import { Footnote } from './footnote.js'
|
|
51
|
+
|
|
52
|
+
// ── Module-level constants ────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const SPECIAL_CHARS_RX = /[<&>]/g
|
|
55
|
+
const SPECIAL_CHARS_TR = { '>': '>', '<': '<', '&': '&' }
|
|
56
|
+
|
|
57
|
+
// Detects if text is a possible candidate for the quotes substitution.
|
|
58
|
+
const QUOTED_TEXT_SNIFF_RX = {
|
|
59
|
+
false: /[*_`#^~]/,
|
|
60
|
+
true: /[*'_+#^~]/,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const HEADER_SUBS = Object.freeze(['specialcharacters', 'attributes'])
|
|
64
|
+
const NO_SUBS = Object.freeze([])
|
|
65
|
+
const REFTEXT_SUBS = Object.freeze([
|
|
66
|
+
'specialcharacters',
|
|
67
|
+
'quotes',
|
|
68
|
+
'replacements',
|
|
69
|
+
])
|
|
70
|
+
const VERBATIM_SUBS = Object.freeze(['specialcharacters', 'callouts'])
|
|
71
|
+
|
|
72
|
+
const SUB_GROUPS = {
|
|
73
|
+
none: NO_SUBS,
|
|
74
|
+
normal: NORMAL_SUBS,
|
|
75
|
+
verbatim: VERBATIM_SUBS,
|
|
76
|
+
specialchars: BASIC_SUBS,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SUB_HINTS = {
|
|
80
|
+
a: 'attributes',
|
|
81
|
+
m: 'macros',
|
|
82
|
+
n: 'normal',
|
|
83
|
+
p: 'post_replacements',
|
|
84
|
+
q: 'quotes',
|
|
85
|
+
r: 'replacements',
|
|
86
|
+
c: 'specialcharacters',
|
|
87
|
+
v: 'verbatim',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const SUB_OPTIONS = {
|
|
91
|
+
block: [...Object.keys(SUB_GROUPS), ...NORMAL_SUBS, 'callouts'],
|
|
92
|
+
inline: [...Object.keys(SUB_GROUPS), ...NORMAL_SUBS],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// control characters used as placeholders
|
|
96
|
+
const CAN = '\u0018'
|
|
97
|
+
const DEL = '\u007f'
|
|
98
|
+
|
|
99
|
+
// SPA, start of guarded protected area (\u0096)
|
|
100
|
+
const PASS_START = '\u0096'
|
|
101
|
+
|
|
102
|
+
// EPA, end of guarded protected area (\u0097)
|
|
103
|
+
const PASS_END = '\u0097'
|
|
104
|
+
|
|
105
|
+
// match passthrough slot
|
|
106
|
+
const PASS_SLOT_RX = new RegExp(`${PASS_START}(\\d+)${PASS_END}`, 'g')
|
|
107
|
+
|
|
108
|
+
// fix passthrough slot after syntax highlighting
|
|
109
|
+
const HIGHLIGHTED_PASS_SLOT_RX = new RegExp(
|
|
110
|
+
`<span\\b[^>]*>${PASS_START}</span>[^\\d]*(\\d+)[^\\d]*<span\\b[^>]*>${PASS_END}</span>`,
|
|
111
|
+
'g'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const RS = '\\'
|
|
115
|
+
const R_SB = ']'
|
|
116
|
+
const ESC_R_SB = '\\]'
|
|
117
|
+
const PLUS = '+'
|
|
118
|
+
|
|
119
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ruby `str.partition(delim)` → `[before, delim, after]` (first occurrence).
|
|
123
|
+
* Returns `[str, '', '']` when delim is not found.
|
|
124
|
+
*/
|
|
125
|
+
function partition(str, delim) {
|
|
126
|
+
const idx = str.indexOf(delim)
|
|
127
|
+
if (idx === -1) return [str, '', '']
|
|
128
|
+
return [str.slice(0, idx), delim, str.slice(idx + delim.length)]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Array union (Ruby `arr | other`).
|
|
133
|
+
*/
|
|
134
|
+
function arrayUnion(a, b) {
|
|
135
|
+
const set = new Set(a)
|
|
136
|
+
for (const v of b) set.add(v)
|
|
137
|
+
return [...set]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Array intersection (Ruby `arr & other`): elements of a that appear in b, deduplicated,
|
|
142
|
+
* preserving the order from a with first occurrence winning.
|
|
143
|
+
*/
|
|
144
|
+
function arrayIntersect(a, b) {
|
|
145
|
+
const allowed = new Set(b)
|
|
146
|
+
const seen = new Set()
|
|
147
|
+
return a.filter((v) => {
|
|
148
|
+
if (!allowed.has(v) || seen.has(v)) return false
|
|
149
|
+
seen.add(v)
|
|
150
|
+
return true
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Array difference (Ruby `arr - other`).
|
|
156
|
+
*/
|
|
157
|
+
function arrayDiff(a, b) {
|
|
158
|
+
const set = new Set(b)
|
|
159
|
+
return a.filter((v) => !set.has(v))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Make a regex global if it isn't already.
|
|
164
|
+
*/
|
|
165
|
+
function globalRx(rx) {
|
|
166
|
+
return rx.global ? rx : new RegExp(rx.source, `${rx.flags}g`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Substitutors mixin ────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export const Substitutors = {
|
|
172
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Apply the specified substitutions to the text.
|
|
176
|
+
*
|
|
177
|
+
* @param {string|string[]} text - The text to process; must not be null.
|
|
178
|
+
* @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
|
|
179
|
+
* @returns {string|string[]} Text with substitutions applied.
|
|
180
|
+
*/
|
|
181
|
+
async applySubs(text, subs = NORMAL_SUBS) {
|
|
182
|
+
const isEmpty = Array.isArray(text) ? text.length === 0 : text.length === 0
|
|
183
|
+
if (isEmpty || !subs || subs.length === 0) return text
|
|
184
|
+
|
|
185
|
+
const isMultiline = Array.isArray(text)
|
|
186
|
+
if (isMultiline) {
|
|
187
|
+
text = text.length > 1 ? text.join(LF) : text[0]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let passthrus
|
|
191
|
+
let clearPassthrus = false
|
|
192
|
+
|
|
193
|
+
if (subs.includes('macros')) {
|
|
194
|
+
text = this.extractPassthroughs(text)
|
|
195
|
+
if (this.passthroughs.length > 0) {
|
|
196
|
+
passthrus = this.passthroughs
|
|
197
|
+
// placeholders can move around; only clear in the outermost substitution call
|
|
198
|
+
if (!this.passthroughsLocked) {
|
|
199
|
+
this.passthroughsLocked = true
|
|
200
|
+
clearPassthrus = true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const type of subs) {
|
|
206
|
+
switch (type) {
|
|
207
|
+
case 'specialcharacters':
|
|
208
|
+
text = this.subSpecialchars(text)
|
|
209
|
+
break
|
|
210
|
+
case 'quotes':
|
|
211
|
+
text = await this.subQuotes(text)
|
|
212
|
+
break
|
|
213
|
+
case 'attributes':
|
|
214
|
+
if (text.includes(ATTR_REF_HEAD)) text = this.subAttributes(text)
|
|
215
|
+
break
|
|
216
|
+
case 'replacements':
|
|
217
|
+
text = this.subReplacements(text)
|
|
218
|
+
break
|
|
219
|
+
case 'macros':
|
|
220
|
+
text = await this.subMacros(text)
|
|
221
|
+
break
|
|
222
|
+
case 'highlight':
|
|
223
|
+
text = await this.highlightSource(text, subs.includes('callouts'))
|
|
224
|
+
break
|
|
225
|
+
case 'callouts':
|
|
226
|
+
if (!subs.includes('highlight')) text = await this.subCallouts(text)
|
|
227
|
+
break
|
|
228
|
+
case 'post_replacements':
|
|
229
|
+
text = await this.subPostReplacements(text)
|
|
230
|
+
break
|
|
231
|
+
default:
|
|
232
|
+
this.logger.warn(`unknown substitution type ${type}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (passthrus) {
|
|
237
|
+
text = await this.restorePassthroughs(text)
|
|
238
|
+
if (clearPassthrus) {
|
|
239
|
+
passthrus.length = 0
|
|
240
|
+
this.passthroughsLocked = null
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return isMultiline ? text.split(LF) : text
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/** Apply normal substitutions (alias for applySubs with default args). */
|
|
248
|
+
async applyNormalSubs(text) {
|
|
249
|
+
return this.applySubs(text, NORMAL_SUBS)
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/** Apply substitutions for header metadata and attribute assignments.
|
|
253
|
+
* Header subs are 'specialcharacters' + 'attributes', both of which are
|
|
254
|
+
* purely synchronous operations — so this method is intentionally sync
|
|
255
|
+
* to allow it to be called from synchronous contexts such as setAttribute()
|
|
256
|
+
* and the {set:...} directive inside subAttributes(). */
|
|
257
|
+
applyHeaderSubs(text) {
|
|
258
|
+
return this.subAttributes(this.subSpecialchars(text))
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/** Apply substitutions for titles (alias for applySubs). */
|
|
262
|
+
async applyTitleSubs(text, subs = NORMAL_SUBS) {
|
|
263
|
+
return this.applySubs(text, subs)
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/** Apply substitutions for reftext. */
|
|
267
|
+
async applyReftextSubs(text) {
|
|
268
|
+
return this.applySubs(text, REFTEXT_SUBS)
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Substitute special characters (encode XML entities).
|
|
273
|
+
*
|
|
274
|
+
* @param {string} text
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
subSpecialchars(text) {
|
|
278
|
+
if (text.includes('>') || text.includes('&') || text.includes('<')) {
|
|
279
|
+
return text.replace(SPECIAL_CHARS_RX, (ch) => SPECIAL_CHARS_TR[ch])
|
|
280
|
+
}
|
|
281
|
+
return text
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/** Alias for subSpecialchars. */
|
|
285
|
+
subSpecialcharacters(text) {
|
|
286
|
+
return this.subSpecialchars(text)
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Substitute quoted text (emphasis, strong, monospaced, etc.)
|
|
291
|
+
*
|
|
292
|
+
* @param {string} text
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
async subQuotes(text) {
|
|
296
|
+
const compat = this.document.compatMode
|
|
297
|
+
if (QUOTED_TEXT_SNIFF_RX[compat].test(text)) {
|
|
298
|
+
for (const [type, scope, pattern] of QUOTE_SUBS[compat]) {
|
|
299
|
+
text = await asyncReplace(text, globalRx(pattern), async (...args) => {
|
|
300
|
+
return this.convertQuotedText(args, type, scope)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return text
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Substitute attribute references in the specified text.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} text
|
|
311
|
+
* @param {Object} [opts={}]
|
|
312
|
+
* @returns {string}
|
|
313
|
+
*/
|
|
314
|
+
subAttributes(text, opts = {}) {
|
|
315
|
+
const docAttrs = this.document.attributes
|
|
316
|
+
let drop = false
|
|
317
|
+
let dropLine = false
|
|
318
|
+
let dropLineSeverity = null
|
|
319
|
+
let dropEmptyLine = false
|
|
320
|
+
let attributeUndefined = null
|
|
321
|
+
let attributeMissing = null
|
|
322
|
+
|
|
323
|
+
text = text.replace(
|
|
324
|
+
globalRx(AttributeReferenceRx),
|
|
325
|
+
(match, p1, p2, p3, p4) => {
|
|
326
|
+
// escaped attribute → return unescaped
|
|
327
|
+
if (p1 === RS || p4 === RS) {
|
|
328
|
+
return `{${p2}}`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (p3) {
|
|
332
|
+
const args = p2.split(':', 3)
|
|
333
|
+
const directive = args.shift()
|
|
334
|
+
if (directive === 'set') {
|
|
335
|
+
const [, value] = Parser.storeAttribute(
|
|
336
|
+
args[0],
|
|
337
|
+
args[1] || '',
|
|
338
|
+
this.document
|
|
339
|
+
)
|
|
340
|
+
if (
|
|
341
|
+
(value !== null && value !== undefined) ||
|
|
342
|
+
(attributeUndefined ||=
|
|
343
|
+
docAttrs['attribute-undefined'] ||
|
|
344
|
+
Compliance.attributeUndefined) !== 'drop-line'
|
|
345
|
+
) {
|
|
346
|
+
drop = true
|
|
347
|
+
dropEmptyLine = true
|
|
348
|
+
return DEL
|
|
349
|
+
} else {
|
|
350
|
+
drop = true
|
|
351
|
+
dropLine = true
|
|
352
|
+
return CAN
|
|
353
|
+
}
|
|
354
|
+
} else if (directive === 'counter2') {
|
|
355
|
+
this.document.counter(...args)
|
|
356
|
+
drop = true
|
|
357
|
+
dropEmptyLine = true
|
|
358
|
+
return DEL
|
|
359
|
+
} else {
|
|
360
|
+
// 'counter'
|
|
361
|
+
return this.document.counter(...args)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const key = p2.toLowerCase()
|
|
366
|
+
if (Object.hasOwn(docAttrs, key)) {
|
|
367
|
+
return docAttrs[key]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const intrinsicValue = INTRINSIC_ATTRIBUTES[key]
|
|
371
|
+
if (intrinsicValue !== undefined) return intrinsicValue
|
|
372
|
+
|
|
373
|
+
switch (
|
|
374
|
+
(attributeMissing ||=
|
|
375
|
+
opts.attributeMissing ||
|
|
376
|
+
docAttrs['attribute-missing'] ||
|
|
377
|
+
Compliance.attributeMissing)
|
|
378
|
+
) {
|
|
379
|
+
case 'drop':
|
|
380
|
+
drop = true
|
|
381
|
+
dropEmptyLine = true
|
|
382
|
+
return DEL
|
|
383
|
+
case 'drop-line':
|
|
384
|
+
dropLineSeverity ||= opts.dropLineSeverity || 'info'
|
|
385
|
+
if (dropLineSeverity === 'info') {
|
|
386
|
+
this.logger.info(
|
|
387
|
+
`dropping line containing reference to missing attribute: ${key}`
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
drop = true
|
|
391
|
+
dropLine = true
|
|
392
|
+
return CAN
|
|
393
|
+
case 'warn':
|
|
394
|
+
this.logger.warn(`skipping reference to missing attribute: ${key}`)
|
|
395
|
+
return match
|
|
396
|
+
default: // 'skip'
|
|
397
|
+
return match
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if (drop) {
|
|
403
|
+
if (dropEmptyLine) {
|
|
404
|
+
const lines = text.replace(new RegExp(`${DEL}+`, 'g'), DEL).split(LF)
|
|
405
|
+
if (dropLine) {
|
|
406
|
+
return lines
|
|
407
|
+
.filter(
|
|
408
|
+
(line) =>
|
|
409
|
+
line !== DEL &&
|
|
410
|
+
line !== CAN &&
|
|
411
|
+
!line.startsWith(CAN) &&
|
|
412
|
+
!line.includes(CAN)
|
|
413
|
+
)
|
|
414
|
+
.join(LF)
|
|
415
|
+
.split(DEL)
|
|
416
|
+
.join('')
|
|
417
|
+
} else {
|
|
418
|
+
return lines
|
|
419
|
+
.filter((line) => line !== DEL)
|
|
420
|
+
.join(LF)
|
|
421
|
+
.split(DEL)
|
|
422
|
+
.join('')
|
|
423
|
+
}
|
|
424
|
+
} else if (text.includes(LF)) {
|
|
425
|
+
return text
|
|
426
|
+
.split(LF)
|
|
427
|
+
.filter(
|
|
428
|
+
(line) =>
|
|
429
|
+
line !== CAN && !line.startsWith(CAN) && !line.includes(CAN)
|
|
430
|
+
)
|
|
431
|
+
.join(LF)
|
|
432
|
+
} else {
|
|
433
|
+
// When the caller sets returnDropSentinel, return null to signal that the line was
|
|
434
|
+
// dropped due to a *missing* attribute (as opposed to an attribute that simply has a
|
|
435
|
+
// blank value). This lets callers distinguish the two cases without changing the
|
|
436
|
+
// general contract of subAttributes for every other call-site.
|
|
437
|
+
return opts.returnDropSentinel ? null : ''
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return text
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Substitute replacement characters (copyright, trademark, etc.)
|
|
446
|
+
*
|
|
447
|
+
* @param {string} text
|
|
448
|
+
* @returns {string}
|
|
449
|
+
*/
|
|
450
|
+
subReplacements(text) {
|
|
451
|
+
if (ReplaceableTextRx.test(text)) {
|
|
452
|
+
for (const [pattern, replacement, restore] of REPLACEMENTS) {
|
|
453
|
+
text = text.replace(globalRx(pattern), (...args) => {
|
|
454
|
+
return this.doReplacement(args, replacement, restore)
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return text
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Substitute inline macros (links, images, etc.)
|
|
463
|
+
*
|
|
464
|
+
* @param {string} text
|
|
465
|
+
* @returns {string}
|
|
466
|
+
*/
|
|
467
|
+
async subMacros(text) {
|
|
468
|
+
const foundSquareBracket = text.includes('[')
|
|
469
|
+
const foundColon = text.includes(':')
|
|
470
|
+
const foundMacroish = foundSquareBracket && foundColon
|
|
471
|
+
const foundMacroishShort = foundMacroish && text.includes(':[')
|
|
472
|
+
const doc = this.document
|
|
473
|
+
const docAttrs = doc.attributes
|
|
474
|
+
|
|
475
|
+
// Extension inline macros
|
|
476
|
+
const extensions = doc.extensions
|
|
477
|
+
if (extensions?.inlineMacros()) {
|
|
478
|
+
for (const extension of extensions.inlineMacros()) {
|
|
479
|
+
text = await asyncReplace(
|
|
480
|
+
text,
|
|
481
|
+
globalRx(extension.instance.regexp),
|
|
482
|
+
async (...args) => {
|
|
483
|
+
const match = args[0]
|
|
484
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
485
|
+
|
|
486
|
+
const groups =
|
|
487
|
+
typeof args[args.length - 1] === 'object' &&
|
|
488
|
+
args[args.length - 1] !== null
|
|
489
|
+
? args[args.length - 1]
|
|
490
|
+
: null
|
|
491
|
+
let target, content
|
|
492
|
+
if (!groups || Object.keys(groups).length === 0) {
|
|
493
|
+
target = args[1]
|
|
494
|
+
content = args[2]
|
|
495
|
+
} else {
|
|
496
|
+
target = groups.target ?? null
|
|
497
|
+
content = groups.content ?? null
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const extConfig = extension.config
|
|
501
|
+
const defaultAttrs = extConfig.defaultAttrs
|
|
502
|
+
const attributes = defaultAttrs ? { ...defaultAttrs } : {}
|
|
503
|
+
|
|
504
|
+
if (content !== null && content !== undefined) {
|
|
505
|
+
if (!content) {
|
|
506
|
+
if (extConfig.contentModel !== 'attributes')
|
|
507
|
+
attributes.text = content
|
|
508
|
+
} else {
|
|
509
|
+
content = this.normalizeText(content, true, true)
|
|
510
|
+
if (extConfig.contentModel === 'attributes') {
|
|
511
|
+
await this.parseAttributes(
|
|
512
|
+
content,
|
|
513
|
+
extConfig.positionalAttrs || extConfig.posAttrs || [],
|
|
514
|
+
{ into: attributes }
|
|
515
|
+
)
|
|
516
|
+
} else {
|
|
517
|
+
attributes.text = content
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
target = target ?? (extConfig.format === 'short' ? content : null)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const replacement = extension.processMethod(
|
|
524
|
+
this,
|
|
525
|
+
target,
|
|
526
|
+
attributes
|
|
527
|
+
)
|
|
528
|
+
if (replacement instanceof Inline) {
|
|
529
|
+
const inlineSubs = replacement.attributes.subs
|
|
530
|
+
if (inlineSubs) {
|
|
531
|
+
const expandedSubs = this.expandSubs(
|
|
532
|
+
inlineSubs,
|
|
533
|
+
'custom inline macro'
|
|
534
|
+
)
|
|
535
|
+
if (expandedSubs)
|
|
536
|
+
replacement.text = await this.applySubs(
|
|
537
|
+
replacement.text,
|
|
538
|
+
expandedSubs
|
|
539
|
+
)
|
|
540
|
+
delete replacement.attributes.subs
|
|
541
|
+
}
|
|
542
|
+
return replacement.convert()
|
|
543
|
+
} else if (replacement) {
|
|
544
|
+
this.logger.info(
|
|
545
|
+
`expected substitution value for custom inline macro to be of type Inline; got ${replacement.constructor.name}: ${match}`
|
|
546
|
+
)
|
|
547
|
+
return replacement
|
|
548
|
+
}
|
|
549
|
+
return ''
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// kbd / btn macros (experimental)
|
|
556
|
+
if (docAttrs.experimental !== undefined) {
|
|
557
|
+
if (
|
|
558
|
+
foundMacroishShort &&
|
|
559
|
+
(text.includes('kbd:') || text.includes('btn:'))
|
|
560
|
+
) {
|
|
561
|
+
text = await asyncReplace(
|
|
562
|
+
text,
|
|
563
|
+
globalRx(InlineKbdBtnMacroRx),
|
|
564
|
+
async (match, p1, p2, p3) => {
|
|
565
|
+
if (p1) return match.slice(1)
|
|
566
|
+
if (p2 === 'kbd') {
|
|
567
|
+
let keys = p3.trim()
|
|
568
|
+
if (keys.includes(R_SB)) keys = keys.split(ESC_R_SB).join(R_SB)
|
|
569
|
+
if (keys.length > 1) {
|
|
570
|
+
let delimIdx = keys.indexOf(',', 1)
|
|
571
|
+
const plusIdx = keys.indexOf('+', 1)
|
|
572
|
+
if (delimIdx !== -1 && plusIdx !== -1)
|
|
573
|
+
delimIdx = Math.min(delimIdx, plusIdx)
|
|
574
|
+
else if (delimIdx === -1) delimIdx = plusIdx
|
|
575
|
+
|
|
576
|
+
if (delimIdx !== -1) {
|
|
577
|
+
const delim = keys.charAt(delimIdx)
|
|
578
|
+
if (keys.endsWith(delim)) {
|
|
579
|
+
keys = keys
|
|
580
|
+
.slice(0, -1)
|
|
581
|
+
.split(delim)
|
|
582
|
+
.map((k) => k.trim())
|
|
583
|
+
keys[keys.length - 1] += delim
|
|
584
|
+
} else {
|
|
585
|
+
keys = keys.split(delim).map((k) => k.trim())
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
keys = [keys]
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
keys = [keys]
|
|
592
|
+
}
|
|
593
|
+
return new Inline(this, 'kbd', null, {
|
|
594
|
+
attributes: { keys },
|
|
595
|
+
}).convert()
|
|
596
|
+
} else {
|
|
597
|
+
// btn
|
|
598
|
+
return new Inline(
|
|
599
|
+
this,
|
|
600
|
+
'button',
|
|
601
|
+
this.normalizeText(p3, true, true)
|
|
602
|
+
).convert()
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (foundMacroish && text.includes('menu:')) {
|
|
609
|
+
text = await asyncReplace(
|
|
610
|
+
text,
|
|
611
|
+
globalRx(InlineMenuMacroRx),
|
|
612
|
+
async (match, p1, p2) => {
|
|
613
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
614
|
+
const menu = p1
|
|
615
|
+
let submenus, menuitem
|
|
616
|
+
if (p2) {
|
|
617
|
+
const items = p2.includes(R_SB)
|
|
618
|
+
? p2.split(ESC_R_SB).join(R_SB)
|
|
619
|
+
: p2
|
|
620
|
+
let delim = null
|
|
621
|
+
if (items.includes('>')) delim = '>'
|
|
622
|
+
else if (items.includes(',')) delim = ','
|
|
623
|
+
if (delim) {
|
|
624
|
+
const parts = items.split(delim).map((item) => item.trim())
|
|
625
|
+
menuitem = parts.pop()
|
|
626
|
+
submenus = parts
|
|
627
|
+
} else {
|
|
628
|
+
submenus = []
|
|
629
|
+
menuitem = items.trimEnd()
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
submenus = []
|
|
633
|
+
menuitem = null
|
|
634
|
+
}
|
|
635
|
+
return new Inline(this, 'menu', null, {
|
|
636
|
+
attributes: { menu, submenus, menuitem },
|
|
637
|
+
}).convert()
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (text.includes('"') && text.includes('>')) {
|
|
643
|
+
text = await asyncReplace(
|
|
644
|
+
text,
|
|
645
|
+
globalRx(InlineMenuRx),
|
|
646
|
+
async (match, p1) => {
|
|
647
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
648
|
+
const parts = p1.split('>').map((item) => item.trim())
|
|
649
|
+
const menu = parts.shift()
|
|
650
|
+
const menuitem = parts.pop() ?? null
|
|
651
|
+
const submenus = parts
|
|
652
|
+
return new Inline(this, 'menu', null, {
|
|
653
|
+
attributes: { menu, submenus, menuitem },
|
|
654
|
+
}).convert()
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// image / icon macros
|
|
661
|
+
if (foundMacroish && (text.includes('image:') || text.includes('icon:'))) {
|
|
662
|
+
text = await asyncReplace(
|
|
663
|
+
text,
|
|
664
|
+
globalRx(InlineImageMacroRx),
|
|
665
|
+
async (match, p1, p2) => {
|
|
666
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
667
|
+
let type, posattrs
|
|
668
|
+
if (match.startsWith('icon:')) {
|
|
669
|
+
type = 'icon'
|
|
670
|
+
posattrs = ['size']
|
|
671
|
+
} else {
|
|
672
|
+
type = 'image'
|
|
673
|
+
posattrs = ['alt', 'width', 'height']
|
|
674
|
+
}
|
|
675
|
+
const target = p1
|
|
676
|
+
const attrs = await this.parseAttributes(p2, posattrs, {
|
|
677
|
+
unescapeInput: true,
|
|
678
|
+
})
|
|
679
|
+
let id
|
|
680
|
+
if (type !== 'icon') {
|
|
681
|
+
id = attrs.id
|
|
682
|
+
doc.register('images', target)
|
|
683
|
+
attrs.imagesdir = attrs.imagesdir ?? docAttrs.imagesdir
|
|
684
|
+
}
|
|
685
|
+
attrs.alt =
|
|
686
|
+
attrs.alt ??
|
|
687
|
+
(attrs['default-alt'] = basename(target, true).replace(
|
|
688
|
+
/[_-]/g,
|
|
689
|
+
' '
|
|
690
|
+
))
|
|
691
|
+
return new Inline(this, 'image', null, {
|
|
692
|
+
type,
|
|
693
|
+
target,
|
|
694
|
+
id,
|
|
695
|
+
attributes: attrs,
|
|
696
|
+
}).convert()
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// index terms
|
|
702
|
+
if (
|
|
703
|
+
(text.includes('((') && text.includes('))')) ||
|
|
704
|
+
(foundMacroishShort && text.includes('dexterm'))
|
|
705
|
+
) {
|
|
706
|
+
text = await asyncReplace(
|
|
707
|
+
text,
|
|
708
|
+
globalRx(InlineIndextermMacroRx),
|
|
709
|
+
async (match, p1, p2, p3) => {
|
|
710
|
+
switch (p1) {
|
|
711
|
+
case 'indexterm': {
|
|
712
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
713
|
+
const attrlist = this.normalizeText(p2, true, true)
|
|
714
|
+
let attrs
|
|
715
|
+
if (attrlist.includes('=')) {
|
|
716
|
+
const parsed = await new AttributeList(attrlist, this).parse()
|
|
717
|
+
const primary = parsed[1]
|
|
718
|
+
if (primary) {
|
|
719
|
+
const terms = [primary]
|
|
720
|
+
const secondary = parsed[2]
|
|
721
|
+
if (secondary) {
|
|
722
|
+
terms.push(secondary)
|
|
723
|
+
const tertiary = parsed[3]
|
|
724
|
+
if (tertiary) terms.push(tertiary)
|
|
725
|
+
}
|
|
726
|
+
attrs = { ...parsed, terms }
|
|
727
|
+
if (attrs['see-also']) {
|
|
728
|
+
const seeAlso = attrs['see-also']
|
|
729
|
+
attrs['see-also'] = seeAlso.includes(',')
|
|
730
|
+
? seeAlso.split(',').map((s) => s.trimStart())
|
|
731
|
+
: [seeAlso]
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
attrs = { terms: attrlist }
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
attrs = { terms: this.splitSimpleCsv(attrlist) }
|
|
738
|
+
}
|
|
739
|
+
return new Inline(this, 'indexterm', null, {
|
|
740
|
+
attributes: attrs,
|
|
741
|
+
}).convert()
|
|
742
|
+
}
|
|
743
|
+
case 'indexterm2': {
|
|
744
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
745
|
+
let term = this.normalizeText(p2, true, true)
|
|
746
|
+
let attrs = null
|
|
747
|
+
if (term.includes('=')) {
|
|
748
|
+
const parsed = await new AttributeList(term, this).parse()
|
|
749
|
+
term = parsed[1] || term
|
|
750
|
+
if (parsed[1]) {
|
|
751
|
+
attrs = parsed
|
|
752
|
+
if (attrs['see-also']) {
|
|
753
|
+
attrs['see-also'] = attrs['see-also'].includes(',')
|
|
754
|
+
? attrs['see-also'].split(',').map((s) => s.trimStart())
|
|
755
|
+
: [attrs['see-also']]
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
attrs = null
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return new Inline(this, 'indexterm', term, {
|
|
762
|
+
attributes: attrs,
|
|
763
|
+
type: 'visible',
|
|
764
|
+
}).convert()
|
|
765
|
+
}
|
|
766
|
+
default: {
|
|
767
|
+
let enclText = p3
|
|
768
|
+
let visible = true,
|
|
769
|
+
before = null,
|
|
770
|
+
after = null
|
|
771
|
+
if (match.startsWith(RS)) {
|
|
772
|
+
if (enclText.startsWith('(') && enclText.endsWith(')')) {
|
|
773
|
+
enclText = enclText.slice(1, -1)
|
|
774
|
+
visible = true
|
|
775
|
+
before = '('
|
|
776
|
+
after = ')'
|
|
777
|
+
} else {
|
|
778
|
+
return match.slice(1)
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
if (enclText.startsWith('(')) {
|
|
782
|
+
if (enclText.endsWith(')')) {
|
|
783
|
+
enclText = enclText.slice(1, -1)
|
|
784
|
+
visible = false
|
|
785
|
+
} else {
|
|
786
|
+
enclText = enclText.slice(1)
|
|
787
|
+
before = '('
|
|
788
|
+
after = ''
|
|
789
|
+
}
|
|
790
|
+
} else if (enclText.endsWith(')')) {
|
|
791
|
+
enclText = enclText.slice(0, -1)
|
|
792
|
+
before = ''
|
|
793
|
+
after = ')'
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
let subbed_term
|
|
797
|
+
if (visible) {
|
|
798
|
+
let term = this.normalizeText(enclText, true)
|
|
799
|
+
let attrs = null
|
|
800
|
+
if (term.includes(';&')) {
|
|
801
|
+
if (term.includes(' >> ')) {
|
|
802
|
+
const [t, , see] = partition(term, ' >> ')
|
|
803
|
+
term = t
|
|
804
|
+
attrs = { see }
|
|
805
|
+
} else if (term.includes(' &> ')) {
|
|
806
|
+
const parts = term.split(' &> ')
|
|
807
|
+
term = parts.shift()
|
|
808
|
+
attrs = { 'see-also': parts }
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
subbed_term = await new Inline(this, 'indexterm', term, {
|
|
812
|
+
attributes: attrs,
|
|
813
|
+
type: 'visible',
|
|
814
|
+
}).convert()
|
|
815
|
+
} else {
|
|
816
|
+
const attrs = {}
|
|
817
|
+
let terms = this.normalizeText(enclText, true)
|
|
818
|
+
if (terms.includes(';&')) {
|
|
819
|
+
if (terms.includes(' >> ')) {
|
|
820
|
+
const [t, , see] = partition(terms, ' >> ')
|
|
821
|
+
terms = t
|
|
822
|
+
attrs.see = see
|
|
823
|
+
} else if (terms.includes(' &> ')) {
|
|
824
|
+
const parts = terms.split(' &> ')
|
|
825
|
+
terms = parts.shift()
|
|
826
|
+
attrs['see-also'] = parts
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
attrs.terms = this.splitSimpleCsv(terms)
|
|
830
|
+
subbed_term = await new Inline(this, 'indexterm', null, {
|
|
831
|
+
attributes: attrs,
|
|
832
|
+
}).convert()
|
|
833
|
+
}
|
|
834
|
+
return before !== null
|
|
835
|
+
? `${before}${subbed_term}${after}`
|
|
836
|
+
: subbed_term
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// inline URLs
|
|
844
|
+
if (foundColon && text.includes('://')) {
|
|
845
|
+
text = await asyncReplace(
|
|
846
|
+
text,
|
|
847
|
+
globalRx(InlineLinkRx),
|
|
848
|
+
async (match, p1, p2, p3, p4, p5, p6, p7, p8) => {
|
|
849
|
+
if (p2 && p5 == null) {
|
|
850
|
+
if (p1.startsWith(RS)) return match.slice(1)
|
|
851
|
+
if (p3.startsWith(RS)) return p1 + match.slice(p1.length + 1)
|
|
852
|
+
if (!p6) return match
|
|
853
|
+
const target = p3 + p6
|
|
854
|
+
doc.register('links', target)
|
|
855
|
+
const linkText =
|
|
856
|
+
docAttrs['hide-uri-scheme'] !== undefined
|
|
857
|
+
? target.replace(UriSniffRx, '')
|
|
858
|
+
: target
|
|
859
|
+
return new Inline(this, 'anchor', linkText, {
|
|
860
|
+
type: 'link',
|
|
861
|
+
target,
|
|
862
|
+
attributes: { role: 'bare' },
|
|
863
|
+
}).convert()
|
|
864
|
+
} else {
|
|
865
|
+
if (p3.startsWith(RS)) return p1 + match.slice(p1.length + 1)
|
|
866
|
+
let prefix = p1
|
|
867
|
+
let target = p3 + (p4 || p7 || '')
|
|
868
|
+
let suffix = ''
|
|
869
|
+
|
|
870
|
+
let link_text
|
|
871
|
+
if (p5 != null) {
|
|
872
|
+
if (prefix === 'link:') prefix = ''
|
|
873
|
+
const rawLinkText = p5
|
|
874
|
+
link_text = rawLinkText || null
|
|
875
|
+
} else {
|
|
876
|
+
switch (prefix) {
|
|
877
|
+
case 'link:':
|
|
878
|
+
case '"':
|
|
879
|
+
case "'":
|
|
880
|
+
return match
|
|
881
|
+
}
|
|
882
|
+
switch (p8) {
|
|
883
|
+
case ';':
|
|
884
|
+
target = target.slice(0, -1)
|
|
885
|
+
if (target.endsWith(')')) {
|
|
886
|
+
target = target.slice(0, -1)
|
|
887
|
+
suffix = ');'
|
|
888
|
+
} else {
|
|
889
|
+
suffix = ';'
|
|
890
|
+
}
|
|
891
|
+
if (target === p3) return match
|
|
892
|
+
break
|
|
893
|
+
case ':':
|
|
894
|
+
target = target.slice(0, -1)
|
|
895
|
+
if (target.endsWith(')')) {
|
|
896
|
+
target = target.slice(0, -1)
|
|
897
|
+
suffix = '):'
|
|
898
|
+
} else {
|
|
899
|
+
suffix = ':'
|
|
900
|
+
}
|
|
901
|
+
if (target === p3) return match
|
|
902
|
+
break
|
|
903
|
+
}
|
|
904
|
+
link_text = null
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const linkOpts = { type: 'link' }
|
|
908
|
+
let attrs = null
|
|
909
|
+
let bare = false
|
|
910
|
+
|
|
911
|
+
if (link_text !== null) {
|
|
912
|
+
let newLinkText = link_text.includes(R_SB)
|
|
913
|
+
? link_text.split(ESC_R_SB).join(R_SB)
|
|
914
|
+
: link_text
|
|
915
|
+
link_text = newLinkText
|
|
916
|
+
|
|
917
|
+
if (!doc.compatMode && link_text.includes('=')) {
|
|
918
|
+
const [extractedText, extractedAttrs] =
|
|
919
|
+
await this.extractAttributesFromText(link_text, '')
|
|
920
|
+
link_text = extractedText
|
|
921
|
+
newLinkText = extractedText
|
|
922
|
+
attrs = extractedAttrs
|
|
923
|
+
linkOpts.id = attrs?.id
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (link_text.endsWith('^')) {
|
|
927
|
+
newLinkText = link_text = link_text.slice(0, -1)
|
|
928
|
+
if (attrs) {
|
|
929
|
+
attrs.window = attrs.window ?? '_blank'
|
|
930
|
+
} else {
|
|
931
|
+
attrs = { window: '_blank' }
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (
|
|
936
|
+
newLinkText !== null &&
|
|
937
|
+
newLinkText !== undefined &&
|
|
938
|
+
newLinkText === ''
|
|
939
|
+
) {
|
|
940
|
+
link_text =
|
|
941
|
+
docAttrs['hide-uri-scheme'] !== undefined
|
|
942
|
+
? target.replace(UriSniffRx, '')
|
|
943
|
+
: target
|
|
944
|
+
bare = true
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
link_text =
|
|
948
|
+
docAttrs['hide-uri-scheme'] !== undefined
|
|
949
|
+
? target.replace(UriSniffRx, '')
|
|
950
|
+
: target
|
|
951
|
+
bare = true
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (bare) {
|
|
955
|
+
if (attrs) {
|
|
956
|
+
attrs.role =
|
|
957
|
+
'role' in (attrs || {}) ? `bare ${attrs.role}` : 'bare'
|
|
958
|
+
} else {
|
|
959
|
+
attrs = { role: 'bare' }
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
linkOpts.target = target
|
|
964
|
+
doc.register('links', target)
|
|
965
|
+
if (attrs) linkOpts.attributes = attrs
|
|
966
|
+
return `${prefix}${await new Inline(this, 'anchor', link_text, linkOpts).convert()}${suffix}`
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// link: and mailto: macros
|
|
973
|
+
if (foundMacroish && (text.includes('link:') || text.includes('ilto:'))) {
|
|
974
|
+
text = await asyncReplace(
|
|
975
|
+
text,
|
|
976
|
+
globalRx(InlineLinkMacroRx),
|
|
977
|
+
async (match, p1, p2, p3) => {
|
|
978
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
979
|
+
let target, mailtoText
|
|
980
|
+
if (p1) {
|
|
981
|
+
mailtoText = p2
|
|
982
|
+
target = `mailto:${mailtoText}`
|
|
983
|
+
} else {
|
|
984
|
+
target = p2
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let attrs = null
|
|
988
|
+
const linkOpts = { type: 'link' }
|
|
989
|
+
let linkText = p3
|
|
990
|
+
|
|
991
|
+
if (linkText) {
|
|
992
|
+
linkText = linkText.includes(R_SB)
|
|
993
|
+
? linkText.split(ESC_R_SB).join(R_SB)
|
|
994
|
+
: linkText
|
|
995
|
+
if (p1) {
|
|
996
|
+
if (!doc.compatMode && linkText.includes(',')) {
|
|
997
|
+
const [extractedText, extractedAttrs] =
|
|
998
|
+
await this.extractAttributesFromText(linkText, '')
|
|
999
|
+
linkText = extractedText
|
|
1000
|
+
attrs = extractedAttrs
|
|
1001
|
+
linkOpts.id = attrs?.id
|
|
1002
|
+
if (attrs?.[2]) {
|
|
1003
|
+
if (attrs[3]) {
|
|
1004
|
+
target = `${target}?subject=${encodeUriComponent(attrs[2])}&body=${encodeUriComponent(attrs[3])}`
|
|
1005
|
+
} else {
|
|
1006
|
+
target = `${target}?subject=${encodeUriComponent(attrs[2])}`
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} else if (!doc.compatMode && linkText.includes('=')) {
|
|
1011
|
+
const [extractedText, extractedAttrs] =
|
|
1012
|
+
await this.extractAttributesFromText(linkText, '')
|
|
1013
|
+
linkText = extractedText
|
|
1014
|
+
attrs = extractedAttrs
|
|
1015
|
+
linkOpts.id = attrs?.id
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (linkText.endsWith('^')) {
|
|
1019
|
+
linkText = linkText.slice(0, -1)
|
|
1020
|
+
if (attrs) {
|
|
1021
|
+
attrs.window = attrs.window ?? '_blank'
|
|
1022
|
+
} else {
|
|
1023
|
+
attrs = { window: '_blank' }
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (!linkText) {
|
|
1029
|
+
if (p1) {
|
|
1030
|
+
linkText = mailtoText
|
|
1031
|
+
} else {
|
|
1032
|
+
if (docAttrs['hide-uri-scheme'] !== undefined) {
|
|
1033
|
+
linkText = target.replace(UriSniffRx, '') || target
|
|
1034
|
+
} else {
|
|
1035
|
+
linkText = target
|
|
1036
|
+
}
|
|
1037
|
+
if (attrs) {
|
|
1038
|
+
attrs.role = 'role' in attrs ? `bare ${attrs.role}` : 'bare'
|
|
1039
|
+
} else {
|
|
1040
|
+
attrs = { role: 'bare' }
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
linkOpts.target = target
|
|
1046
|
+
doc.register('links', target)
|
|
1047
|
+
if (attrs) linkOpts.attributes = attrs
|
|
1048
|
+
return new Inline(this, 'anchor', linkText, linkOpts).convert()
|
|
1049
|
+
}
|
|
1050
|
+
)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// bare email addresses
|
|
1054
|
+
if (text.includes('@')) {
|
|
1055
|
+
text = await asyncReplace(
|
|
1056
|
+
text,
|
|
1057
|
+
globalRx(InlineEmailRx),
|
|
1058
|
+
async (match, p1) => {
|
|
1059
|
+
if (p1) return p1 === RS ? match.slice(1) : match
|
|
1060
|
+
const address = match
|
|
1061
|
+
const target = `mailto:${address}`
|
|
1062
|
+
doc.register('links', target)
|
|
1063
|
+
return new Inline(this, 'anchor', address, {
|
|
1064
|
+
type: 'link',
|
|
1065
|
+
target,
|
|
1066
|
+
}).convert()
|
|
1067
|
+
}
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// bibliography anchor
|
|
1072
|
+
if (
|
|
1073
|
+
foundSquareBracket &&
|
|
1074
|
+
this.context === 'list_item' &&
|
|
1075
|
+
this.getParent().style === 'bibliography'
|
|
1076
|
+
) {
|
|
1077
|
+
text = await asyncReplace(
|
|
1078
|
+
text,
|
|
1079
|
+
InlineBiblioAnchorRx,
|
|
1080
|
+
async (match, p1, p2) => {
|
|
1081
|
+
return new Inline(this, 'anchor', p2, {
|
|
1082
|
+
type: 'bibref',
|
|
1083
|
+
id: p1,
|
|
1084
|
+
}).convert()
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// inline anchors
|
|
1090
|
+
if (
|
|
1091
|
+
(foundSquareBracket && text.includes('[[')) ||
|
|
1092
|
+
(foundMacroish && text.includes('or:'))
|
|
1093
|
+
) {
|
|
1094
|
+
text = await asyncReplace(
|
|
1095
|
+
text,
|
|
1096
|
+
globalRx(InlineAnchorRx),
|
|
1097
|
+
async (match, p1, p2, p3, p4, p5) => {
|
|
1098
|
+
if (p1) return match.slice(1)
|
|
1099
|
+
let id, reftext
|
|
1100
|
+
if (p2) {
|
|
1101
|
+
id = p2
|
|
1102
|
+
reftext = p3
|
|
1103
|
+
} else {
|
|
1104
|
+
id = p4
|
|
1105
|
+
reftext = p5
|
|
1106
|
+
? p5.includes(R_SB)
|
|
1107
|
+
? p5.split(ESC_R_SB).join(R_SB)
|
|
1108
|
+
: p5
|
|
1109
|
+
: null
|
|
1110
|
+
}
|
|
1111
|
+
return new Inline(this, 'anchor', reftext, {
|
|
1112
|
+
type: 'ref',
|
|
1113
|
+
id,
|
|
1114
|
+
}).convert()
|
|
1115
|
+
}
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// xref macros
|
|
1120
|
+
if (
|
|
1121
|
+
(text.includes('&') && text.includes(';&l')) ||
|
|
1122
|
+
(foundMacroish && text.includes('xref:'))
|
|
1123
|
+
) {
|
|
1124
|
+
text = await asyncReplace(
|
|
1125
|
+
text,
|
|
1126
|
+
globalRx(InlineXrefMacroRx),
|
|
1127
|
+
async (match, p1, p2, p3) => {
|
|
1128
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
1129
|
+
const attrs = {}
|
|
1130
|
+
let refid, linkText, macro, path, fragment, target, src2src
|
|
1131
|
+
|
|
1132
|
+
if (p1) {
|
|
1133
|
+
refid = p1
|
|
1134
|
+
if (refid.includes(',')) {
|
|
1135
|
+
const commaIdx = refid.indexOf(',')
|
|
1136
|
+
const rawLinkText = refid.slice(commaIdx + 1).trimStart()
|
|
1137
|
+
refid = refid.slice(0, commaIdx)
|
|
1138
|
+
linkText = rawLinkText || null
|
|
1139
|
+
}
|
|
1140
|
+
} else {
|
|
1141
|
+
macro = true
|
|
1142
|
+
refid = p2
|
|
1143
|
+
if (p3) {
|
|
1144
|
+
linkText = p3.includes(R_SB) ? p3.split(ESC_R_SB).join(R_SB) : p3
|
|
1145
|
+
if (!doc.compatMode && linkText.includes('=')) {
|
|
1146
|
+
const [extractedText, extractedAttrs] =
|
|
1147
|
+
await this.extractAttributesFromText(linkText)
|
|
1148
|
+
linkText = extractedText
|
|
1149
|
+
Object.assign(attrs, extractedAttrs)
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (doc.compatMode) {
|
|
1155
|
+
fragment = refid
|
|
1156
|
+
} else {
|
|
1157
|
+
const hashIdx = refid.indexOf('#')
|
|
1158
|
+
if (
|
|
1159
|
+
hashIdx !== -1 &&
|
|
1160
|
+
(hashIdx === 0 || refid[hashIdx - 1] !== '&')
|
|
1161
|
+
) {
|
|
1162
|
+
if (hashIdx > 0) {
|
|
1163
|
+
const fragmentLen = refid.length - 1 - hashIdx
|
|
1164
|
+
if (fragmentLen > 0) {
|
|
1165
|
+
path = refid.slice(0, hashIdx)
|
|
1166
|
+
fragment = refid.slice(hashIdx + 1)
|
|
1167
|
+
} else {
|
|
1168
|
+
path = refid.slice(0, -1)
|
|
1169
|
+
}
|
|
1170
|
+
if (macro) {
|
|
1171
|
+
if (path.endsWith('.adoc')) {
|
|
1172
|
+
src2src = path = path.slice(0, -5)
|
|
1173
|
+
} else if (!isExtname(path)) {
|
|
1174
|
+
src2src = path
|
|
1175
|
+
}
|
|
1176
|
+
} else if (
|
|
1177
|
+
Object.keys(ASCIIDOC_EXTENSIONS).some((ext) =>
|
|
1178
|
+
path.endsWith(ext)
|
|
1179
|
+
)
|
|
1180
|
+
) {
|
|
1181
|
+
src2src = path = path.slice(0, path.lastIndexOf('.'))
|
|
1182
|
+
} else {
|
|
1183
|
+
src2src = path
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
target = refid
|
|
1187
|
+
fragment = refid.slice(1)
|
|
1188
|
+
}
|
|
1189
|
+
} else if (macro) {
|
|
1190
|
+
if (refid.endsWith('.adoc')) {
|
|
1191
|
+
src2src = path = refid.slice(0, -5)
|
|
1192
|
+
} else if (isExtname(refid)) {
|
|
1193
|
+
path = refid
|
|
1194
|
+
} else {
|
|
1195
|
+
fragment = refid
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
fragment = refid
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (target) {
|
|
1203
|
+
// handles: #id
|
|
1204
|
+
refid = fragment
|
|
1205
|
+
if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
|
|
1206
|
+
this.logger.info(`possible invalid reference: ${refid}`)
|
|
1207
|
+
}
|
|
1208
|
+
} else if (path) {
|
|
1209
|
+
if (
|
|
1210
|
+
src2src &&
|
|
1211
|
+
(doc.attributes.docname === path || doc.catalog.includes[path])
|
|
1212
|
+
) {
|
|
1213
|
+
if (fragment) {
|
|
1214
|
+
refid = fragment
|
|
1215
|
+
path = null
|
|
1216
|
+
target = `#${fragment}`
|
|
1217
|
+
if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
|
|
1218
|
+
this.logger.info(`possible invalid reference: ${refid}`)
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
refid = null
|
|
1222
|
+
path = null
|
|
1223
|
+
target = '#'
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
const relfileprefix = doc.attributes.relfileprefix || ''
|
|
1227
|
+
const relfilesuffix = src2src
|
|
1228
|
+
? (doc.attributes.relfilesuffix ?? doc.outfilesuffix)
|
|
1229
|
+
: ''
|
|
1230
|
+
const resolvedPath = `${relfileprefix}${path}${relfilesuffix}`
|
|
1231
|
+
refid = path
|
|
1232
|
+
path = resolvedPath
|
|
1233
|
+
if (fragment) {
|
|
1234
|
+
refid = `${refid}#${fragment}`
|
|
1235
|
+
target = `${path}#${fragment}`
|
|
1236
|
+
} else {
|
|
1237
|
+
target = path
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
} else if (doc.compatMode || !Compliance.naturalXrefs) {
|
|
1241
|
+
refid = fragment
|
|
1242
|
+
target = `#${fragment}`
|
|
1243
|
+
if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
|
|
1244
|
+
this.logger.info(`possible invalid reference: ${refid}`)
|
|
1245
|
+
}
|
|
1246
|
+
} else if (doc.catalog.refs[fragment]) {
|
|
1247
|
+
refid = fragment
|
|
1248
|
+
target = `#${fragment}`
|
|
1249
|
+
} else if (
|
|
1250
|
+
(fragment.includes(' ') || fragment.toLowerCase() !== fragment) &&
|
|
1251
|
+
(refid = await doc.resolveId(fragment))
|
|
1252
|
+
) {
|
|
1253
|
+
fragment = refid
|
|
1254
|
+
target = `#${refid}`
|
|
1255
|
+
} else {
|
|
1256
|
+
refid = fragment
|
|
1257
|
+
target = `#${fragment}`
|
|
1258
|
+
if (this.logger.isInfo?.())
|
|
1259
|
+
this.logger.info(`possible invalid reference: ${refid}`)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (path != null) attrs.path = path
|
|
1263
|
+
if (fragment != null) attrs.fragment = fragment
|
|
1264
|
+
attrs.refid = refid
|
|
1265
|
+
return new Inline(this, 'anchor', linkText, {
|
|
1266
|
+
type: 'xref',
|
|
1267
|
+
target,
|
|
1268
|
+
attributes: attrs,
|
|
1269
|
+
}).convert()
|
|
1270
|
+
}
|
|
1271
|
+
)
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// footnote macros
|
|
1275
|
+
if (foundMacroish && text.includes('tnote')) {
|
|
1276
|
+
text = await asyncReplace(
|
|
1277
|
+
text,
|
|
1278
|
+
globalRx(InlineFootnoteMacroRx),
|
|
1279
|
+
async (match, p1, p2, p3) => {
|
|
1280
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
1281
|
+
|
|
1282
|
+
let id, content, type, target
|
|
1283
|
+
if (p1) {
|
|
1284
|
+
// footnoteref
|
|
1285
|
+
if (p3) {
|
|
1286
|
+
const commaIdx = p3.indexOf(',')
|
|
1287
|
+
if (commaIdx >= 0) {
|
|
1288
|
+
id = p3.slice(0, commaIdx)
|
|
1289
|
+
content = p3.slice(commaIdx + 1)
|
|
1290
|
+
} else {
|
|
1291
|
+
// reference only (no text), e.g. footnoteref:[id]
|
|
1292
|
+
id = p3
|
|
1293
|
+
}
|
|
1294
|
+
if (!doc.compatMode) {
|
|
1295
|
+
this.logger.warn(
|
|
1296
|
+
`found deprecated footnoteref macro: ${match}; use footnote macro with target instead`
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
return match
|
|
1301
|
+
}
|
|
1302
|
+
} else {
|
|
1303
|
+
id = p2
|
|
1304
|
+
content = p3
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
let index
|
|
1308
|
+
if (id) {
|
|
1309
|
+
const footnote = doc.footnotes.find((f) => f.id === id)
|
|
1310
|
+
if (footnote) {
|
|
1311
|
+
index = footnote.index
|
|
1312
|
+
content = footnote.text
|
|
1313
|
+
type = 'xref'
|
|
1314
|
+
target = id
|
|
1315
|
+
id = null
|
|
1316
|
+
} else if (content) {
|
|
1317
|
+
content = await this.restorePassthroughs(
|
|
1318
|
+
this.normalizeText(content, true, true)
|
|
1319
|
+
)
|
|
1320
|
+
index = doc.counter('footnote-number')
|
|
1321
|
+
doc.register('footnotes', new Footnote(index, id, content))
|
|
1322
|
+
type = 'ref'
|
|
1323
|
+
target = null
|
|
1324
|
+
} else {
|
|
1325
|
+
this.logger.warn(`invalid footnote reference: ${id}`)
|
|
1326
|
+
type = 'xref'
|
|
1327
|
+
target = id
|
|
1328
|
+
content = id
|
|
1329
|
+
id = null
|
|
1330
|
+
}
|
|
1331
|
+
} else if (content) {
|
|
1332
|
+
content = await this.restorePassthroughs(
|
|
1333
|
+
this.normalizeText(content, true, true)
|
|
1334
|
+
)
|
|
1335
|
+
index = doc.counter('footnote-number')
|
|
1336
|
+
doc.register('footnotes', new Footnote(index, id, content))
|
|
1337
|
+
type = null
|
|
1338
|
+
target = null
|
|
1339
|
+
} else {
|
|
1340
|
+
return match
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return new Inline(this, 'footnote', content, {
|
|
1344
|
+
attributes: { index },
|
|
1345
|
+
id,
|
|
1346
|
+
target,
|
|
1347
|
+
type,
|
|
1348
|
+
}).convert()
|
|
1349
|
+
}
|
|
1350
|
+
)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return text
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Substitute post replacements (hard line breaks).
|
|
1358
|
+
*
|
|
1359
|
+
* @param {string} text
|
|
1360
|
+
* @returns {string}
|
|
1361
|
+
*/
|
|
1362
|
+
async subPostReplacements(text) {
|
|
1363
|
+
if (
|
|
1364
|
+
'hardbreaks-option' in this.attributes ||
|
|
1365
|
+
'hardbreaks-option' in this.document.attributes
|
|
1366
|
+
) {
|
|
1367
|
+
const lines = text.split(LF)
|
|
1368
|
+
if (lines.length < 2) return text
|
|
1369
|
+
const last = lines.pop()
|
|
1370
|
+
const converted = await Promise.all(
|
|
1371
|
+
lines.map((line) =>
|
|
1372
|
+
new Inline(
|
|
1373
|
+
this,
|
|
1374
|
+
'break',
|
|
1375
|
+
line.endsWith(HARD_LINE_BREAK) ? line.slice(0, -2) : line,
|
|
1376
|
+
{ type: 'line' }
|
|
1377
|
+
).convert()
|
|
1378
|
+
)
|
|
1379
|
+
)
|
|
1380
|
+
return [...converted, last].join(LF)
|
|
1381
|
+
} else if (text.includes(PLUS) && text.includes(HARD_LINE_BREAK)) {
|
|
1382
|
+
return asyncReplace(
|
|
1383
|
+
text,
|
|
1384
|
+
globalRx(HardLineBreakRx),
|
|
1385
|
+
async (match, p1) => {
|
|
1386
|
+
return new Inline(this, 'break', p1, { type: 'line' }).convert()
|
|
1387
|
+
}
|
|
1388
|
+
)
|
|
1389
|
+
}
|
|
1390
|
+
return text
|
|
1391
|
+
},
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Apply verbatim substitutions on source.
|
|
1395
|
+
*
|
|
1396
|
+
* @param {string} source
|
|
1397
|
+
* @param {boolean} processCallouts
|
|
1398
|
+
* @returns {string}
|
|
1399
|
+
*/
|
|
1400
|
+
async subSource(source, processCallouts) {
|
|
1401
|
+
return processCallouts
|
|
1402
|
+
? await this.subCallouts(this.subSpecialchars(source))
|
|
1403
|
+
: this.subSpecialchars(source)
|
|
1404
|
+
},
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Substitute callout source references.
|
|
1408
|
+
*
|
|
1409
|
+
* @param {string} text
|
|
1410
|
+
* @returns {string}
|
|
1411
|
+
*/
|
|
1412
|
+
async subCallouts(text) {
|
|
1413
|
+
const calloutRx = this.hasAttribute('line-comment')
|
|
1414
|
+
? CalloutSourceRxMap[this.getAttribute('line-comment')]
|
|
1415
|
+
: CalloutSourceRx
|
|
1416
|
+
let autonum = 0
|
|
1417
|
+
return asyncReplace(
|
|
1418
|
+
text,
|
|
1419
|
+
globalRx(calloutRx),
|
|
1420
|
+
async (match, p1, p2, p3, p4) => {
|
|
1421
|
+
if (p2) {
|
|
1422
|
+
return match.replace(RS, '')
|
|
1423
|
+
}
|
|
1424
|
+
const guard = p1 || (p3 === '--' ? ['<!--', '-->'] : null)
|
|
1425
|
+
const numeral = p4 === '.' ? String(++autonum) : p4
|
|
1426
|
+
return new Inline(this, 'callout', numeral, {
|
|
1427
|
+
id: this.document.callouts.readNextId(),
|
|
1428
|
+
attributes: { guard },
|
|
1429
|
+
}).convert()
|
|
1430
|
+
}
|
|
1431
|
+
)
|
|
1432
|
+
},
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Highlight (colorize) the source code using a syntax highlighter.
|
|
1436
|
+
*
|
|
1437
|
+
* @param {string} source
|
|
1438
|
+
* @param {boolean} processCallouts
|
|
1439
|
+
* @returns {string}
|
|
1440
|
+
*/
|
|
1441
|
+
async highlightSource(source, processCallouts) {
|
|
1442
|
+
const syntaxHl = this.document.syntaxHighlighter
|
|
1443
|
+
if (!syntaxHl?.handlesHighlighting()) {
|
|
1444
|
+
return this.subSource(source, processCallouts)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
let calloutMarks
|
|
1448
|
+
if (processCallouts) {
|
|
1449
|
+
;[source, calloutMarks] = this.extractCallouts(source)
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const docAttrs = this.document.attributes
|
|
1453
|
+
const syntaxHlName = syntaxHl.name
|
|
1454
|
+
let linenumsMode = null
|
|
1455
|
+
let startLineNumber = null
|
|
1456
|
+
if (this.hasOption('linenums')) {
|
|
1457
|
+
linenumsMode = docAttrs[`${syntaxHlName}-linenums-mode`] || 'table'
|
|
1458
|
+
startLineNumber = parseInt(this.getAttribute('start', 1), 10)
|
|
1459
|
+
if (startLineNumber < 1) startLineNumber = 1
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
let highlightLines = null
|
|
1463
|
+
if (this.hasAttribute('highlight')) {
|
|
1464
|
+
highlightLines = this.resolveLinesToHighlight(
|
|
1465
|
+
source,
|
|
1466
|
+
this.getAttribute('highlight'),
|
|
1467
|
+
startLineNumber
|
|
1468
|
+
)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const hlResult = syntaxHl.highlight(
|
|
1472
|
+
this,
|
|
1473
|
+
source,
|
|
1474
|
+
this.getAttribute('language'),
|
|
1475
|
+
{
|
|
1476
|
+
callouts: calloutMarks,
|
|
1477
|
+
cssMode: docAttrs[`${syntaxHlName}-css`] || 'class',
|
|
1478
|
+
highlightLines,
|
|
1479
|
+
numberLines: linenumsMode,
|
|
1480
|
+
startLineNumber,
|
|
1481
|
+
style: docAttrs[`${syntaxHlName}-style`],
|
|
1482
|
+
}
|
|
1483
|
+
)
|
|
1484
|
+
const [highlighted, sourceOffset] = Array.isArray(hlResult)
|
|
1485
|
+
? hlResult
|
|
1486
|
+
: [hlResult, undefined]
|
|
1487
|
+
|
|
1488
|
+
let result = highlighted
|
|
1489
|
+
if (this.passthroughs.length > 0) {
|
|
1490
|
+
result = result.replace(
|
|
1491
|
+
globalRx(HIGHLIGHTED_PASS_SLOT_RX),
|
|
1492
|
+
`${PASS_START}$1${PASS_END}`
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (!calloutMarks || Object.keys(calloutMarks).length === 0) {
|
|
1497
|
+
return result
|
|
1498
|
+
}
|
|
1499
|
+
return await this.restoreCallouts(result, calloutMarks, sourceOffset)
|
|
1500
|
+
},
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Resolve line numbers to highlight from a test string.
|
|
1504
|
+
*
|
|
1505
|
+
* @param {string} source
|
|
1506
|
+
* @param {string} spec - e.g. "1-5, !2, 10" or "1..5;!2;10"
|
|
1507
|
+
* @param {number|null} [start=null]
|
|
1508
|
+
* @returns {number[]}
|
|
1509
|
+
*/
|
|
1510
|
+
resolveLinesToHighlight(source, spec, start = null) {
|
|
1511
|
+
let lines = []
|
|
1512
|
+
if (spec.includes(' ')) spec = spec.split(' ').join('')
|
|
1513
|
+
const entries = spec.includes(',') ? spec.split(',') : spec.split(';')
|
|
1514
|
+
|
|
1515
|
+
for (let entry of entries) {
|
|
1516
|
+
let negate = false
|
|
1517
|
+
if (entry.startsWith('!')) {
|
|
1518
|
+
entry = entry.slice(1)
|
|
1519
|
+
negate = true
|
|
1520
|
+
}
|
|
1521
|
+
const delim = entry.includes('..')
|
|
1522
|
+
? '..'
|
|
1523
|
+
: entry.includes('-')
|
|
1524
|
+
? '-'
|
|
1525
|
+
: null
|
|
1526
|
+
if (delim) {
|
|
1527
|
+
const [fromStr, , toStr] = partition(entry, delim)
|
|
1528
|
+
const from = parseInt(fromStr, 10)
|
|
1529
|
+
let to
|
|
1530
|
+
if (!toStr || (to = parseInt(toStr, 10)) < 0) {
|
|
1531
|
+
to = source.split(LF).length + 1
|
|
1532
|
+
}
|
|
1533
|
+
const range = Array.from({ length: to - from + 1 }, (_, i) => from + i)
|
|
1534
|
+
if (negate) {
|
|
1535
|
+
lines = arrayDiff(lines, range)
|
|
1536
|
+
} else {
|
|
1537
|
+
lines = arrayUnion(lines, range)
|
|
1538
|
+
}
|
|
1539
|
+
} else if (negate) {
|
|
1540
|
+
const val = parseInt(entry, 10)
|
|
1541
|
+
lines = lines.filter((l) => l !== val)
|
|
1542
|
+
} else {
|
|
1543
|
+
const line = parseInt(entry, 10)
|
|
1544
|
+
if (!lines.includes(line)) lines.push(line)
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (start) {
|
|
1549
|
+
const shift = start - 1
|
|
1550
|
+
if (shift !== 0) lines = lines.map((l) => l - shift)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return lines.sort((a, b) => a - b)
|
|
1554
|
+
},
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Extract passthrough text for reinsertion after processing.
|
|
1558
|
+
*
|
|
1559
|
+
* @param {string} text
|
|
1560
|
+
* @returns {string} Text with passthrough regions replaced by placeholders.
|
|
1561
|
+
*/
|
|
1562
|
+
extractPassthroughs(text) {
|
|
1563
|
+
const compatMode = this.document.compatMode
|
|
1564
|
+
const passthrus = this.passthroughs
|
|
1565
|
+
|
|
1566
|
+
if (text.includes('++') || text.includes('$$') || text.includes('ss:')) {
|
|
1567
|
+
text = text.replace(
|
|
1568
|
+
globalRx(InlinePassMacroRx),
|
|
1569
|
+
(match, p1, p2, p3, p4, p5, p6, p7, p8) => {
|
|
1570
|
+
const boundary = p4 // $$, ++, or +++
|
|
1571
|
+
if (boundary) {
|
|
1572
|
+
// skip ++ in compat mode
|
|
1573
|
+
if (compatMode && boundary === '++') {
|
|
1574
|
+
const prefix = p2 ? `${p1}[${p2}]${p3}` : `${p1}${p3}`
|
|
1575
|
+
return `${prefix}++${this.extractPassthroughs(p5)}++`
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
let attributes, oldBehavior, preceding
|
|
1579
|
+
if (p2) {
|
|
1580
|
+
const attrlist = p2
|
|
1581
|
+
const escapeCount = p3.length
|
|
1582
|
+
if (escapeCount > 0) {
|
|
1583
|
+
return `${p1}[${attrlist}]${RS.repeat(escapeCount - 1)}${boundary}${p5}${boundary}`
|
|
1584
|
+
} else if (p1 === RS) {
|
|
1585
|
+
preceding = `[${attrlist}]`
|
|
1586
|
+
} else if (boundary === '++') {
|
|
1587
|
+
if (attrlist === 'x-') {
|
|
1588
|
+
oldBehavior = true
|
|
1589
|
+
attributes = {}
|
|
1590
|
+
} else if (attrlist.endsWith(' x-')) {
|
|
1591
|
+
oldBehavior = true
|
|
1592
|
+
attributes = this.parseQuotedTextAttributes(
|
|
1593
|
+
attrlist.slice(0, -3)
|
|
1594
|
+
)
|
|
1595
|
+
} else {
|
|
1596
|
+
attributes = this.parseQuotedTextAttributes(attrlist)
|
|
1597
|
+
}
|
|
1598
|
+
} else {
|
|
1599
|
+
attributes = this.parseQuotedTextAttributes(attrlist)
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
const escapeCount = p3.length
|
|
1603
|
+
if (escapeCount > 0) {
|
|
1604
|
+
return `${RS.repeat(escapeCount - 1)}${boundary}${p5}${boundary}`
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const subs = boundary === '+++' ? [] : [...BASIC_SUBS]
|
|
1609
|
+
let passthruKey
|
|
1610
|
+
if (attributes) {
|
|
1611
|
+
if (oldBehavior) {
|
|
1612
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1613
|
+
text: p5,
|
|
1614
|
+
subs: NORMAL_SUBS,
|
|
1615
|
+
type: 'monospaced',
|
|
1616
|
+
attributes,
|
|
1617
|
+
}
|
|
1618
|
+
} else {
|
|
1619
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1620
|
+
text: p5,
|
|
1621
|
+
subs,
|
|
1622
|
+
type: 'unquoted',
|
|
1623
|
+
attributes,
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
} else {
|
|
1627
|
+
passthrus[(passthruKey = passthrus.length)] = { text: p5, subs }
|
|
1628
|
+
}
|
|
1629
|
+
return `${preceding || ''}${PASS_START}${passthruKey}${PASS_END}`
|
|
1630
|
+
} else {
|
|
1631
|
+
// pass:[]
|
|
1632
|
+
if (p6 === RS) return match.slice(1)
|
|
1633
|
+
let passthruKey
|
|
1634
|
+
if (p7) {
|
|
1635
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1636
|
+
text: this.normalizeText(p8, null, true),
|
|
1637
|
+
subs: this.resolvePassSubs(p7),
|
|
1638
|
+
}
|
|
1639
|
+
} else {
|
|
1640
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1641
|
+
text: this.normalizeText(p8, null, true),
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return `${PASS_START}${passthruKey}${PASS_END}`
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
)
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const [passInlineChar1, passInlineChar2, passInlineRx] =
|
|
1651
|
+
InlinePassRx[compatMode]
|
|
1652
|
+
|
|
1653
|
+
if (
|
|
1654
|
+
text.includes(passInlineChar1) ||
|
|
1655
|
+
(passInlineChar2 && text.includes(passInlineChar2))
|
|
1656
|
+
) {
|
|
1657
|
+
text = text.replace(
|
|
1658
|
+
globalRx(passInlineRx),
|
|
1659
|
+
(match, p1, p2, p3, p4, p5, p6, p7, p8) => {
|
|
1660
|
+
const preceding = p1
|
|
1661
|
+
const attrlist = p4 || p3
|
|
1662
|
+
const escaped = !!p5
|
|
1663
|
+
const quotedText = p6
|
|
1664
|
+
const formatMark = p7
|
|
1665
|
+
const content = p8
|
|
1666
|
+
|
|
1667
|
+
let oldBehavior, oldBehaviorForced, attributes
|
|
1668
|
+
|
|
1669
|
+
if (compatMode) {
|
|
1670
|
+
oldBehavior = true
|
|
1671
|
+
} else if (
|
|
1672
|
+
attrlist &&
|
|
1673
|
+
(attrlist === 'x-' || attrlist.endsWith(' x-'))
|
|
1674
|
+
) {
|
|
1675
|
+
oldBehavior = true
|
|
1676
|
+
oldBehaviorForced = true
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (attrlist) {
|
|
1680
|
+
if (escaped) {
|
|
1681
|
+
return `${preceding}[${attrlist}]${quotedText.slice(1)}`
|
|
1682
|
+
} else if (preceding === RS) {
|
|
1683
|
+
if (oldBehaviorForced && formatMark === '`') {
|
|
1684
|
+
return `${preceding}[${attrlist}]${quotedText}`
|
|
1685
|
+
}
|
|
1686
|
+
if (compatMode && formatMark === '`') {
|
|
1687
|
+
// escaped role in compat-mode: role becomes literal text, backtick span still processed as monospaced
|
|
1688
|
+
let passthruKey
|
|
1689
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1690
|
+
text: content,
|
|
1691
|
+
subs: BASIC_SUBS,
|
|
1692
|
+
type: 'monospaced',
|
|
1693
|
+
}
|
|
1694
|
+
return `[${attrlist}]${PASS_START}${passthruKey}${PASS_END}`
|
|
1695
|
+
}
|
|
1696
|
+
return `[${attrlist}]${quotedText}` // preceding replaced by attrlist form
|
|
1697
|
+
} else if (oldBehaviorForced) {
|
|
1698
|
+
attributes =
|
|
1699
|
+
attrlist === 'x-'
|
|
1700
|
+
? {}
|
|
1701
|
+
: this.parseQuotedTextAttributes(attrlist.slice(0, -3))
|
|
1702
|
+
} else {
|
|
1703
|
+
attributes = this.parseQuotedTextAttributes(attrlist)
|
|
1704
|
+
}
|
|
1705
|
+
} else if (escaped) {
|
|
1706
|
+
return `${preceding}${quotedText.slice(1)}`
|
|
1707
|
+
} else if (compatMode && preceding === RS) {
|
|
1708
|
+
return quotedText
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
let passthruKey
|
|
1712
|
+
if (compatMode) {
|
|
1713
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1714
|
+
text: content,
|
|
1715
|
+
subs: BASIC_SUBS,
|
|
1716
|
+
attributes,
|
|
1717
|
+
type: 'monospaced',
|
|
1718
|
+
}
|
|
1719
|
+
} else if (attributes) {
|
|
1720
|
+
if (oldBehavior) {
|
|
1721
|
+
const subs = formatMark === '`' ? BASIC_SUBS : NORMAL_SUBS
|
|
1722
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1723
|
+
text: content,
|
|
1724
|
+
subs,
|
|
1725
|
+
attributes,
|
|
1726
|
+
type: 'monospaced',
|
|
1727
|
+
}
|
|
1728
|
+
} else {
|
|
1729
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1730
|
+
text: content,
|
|
1731
|
+
subs: BASIC_SUBS,
|
|
1732
|
+
attributes,
|
|
1733
|
+
type: 'unquoted',
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
passthrus[(passthruKey = passthrus.length)] = {
|
|
1738
|
+
text: content,
|
|
1739
|
+
subs: BASIC_SUBS,
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return `${preceding || ''}${PASS_START}${passthruKey}${PASS_END}`
|
|
1744
|
+
}
|
|
1745
|
+
)
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// stem macros (in a subsequent step to allow escaping by the former)
|
|
1749
|
+
if (
|
|
1750
|
+
text.includes(':') &&
|
|
1751
|
+
(text.includes('stem:') || text.includes('math:'))
|
|
1752
|
+
) {
|
|
1753
|
+
text = text.replace(globalRx(InlineStemMacroRx), (match, p1, p2, p3) => {
|
|
1754
|
+
if (match.startsWith(RS)) return match.slice(1)
|
|
1755
|
+
let type = p1
|
|
1756
|
+
if (type === 'stem') {
|
|
1757
|
+
type = STEM_TYPE_ALIASES[this.document.attributes.stem]
|
|
1758
|
+
}
|
|
1759
|
+
let content = this.normalizeText(p3, null, true)
|
|
1760
|
+
if (
|
|
1761
|
+
type === 'latexmath' &&
|
|
1762
|
+
content.startsWith('$') &&
|
|
1763
|
+
content.endsWith('$')
|
|
1764
|
+
) {
|
|
1765
|
+
content = content.slice(1, -1)
|
|
1766
|
+
}
|
|
1767
|
+
const subs = p2
|
|
1768
|
+
? this.resolvePassSubs(p2, 'stem macro')
|
|
1769
|
+
: this.document.basebackend('html')
|
|
1770
|
+
? BASIC_SUBS
|
|
1771
|
+
: null
|
|
1772
|
+
const passthruKey = passthrus.length
|
|
1773
|
+
passthrus[passthruKey] = { text: content, subs, type }
|
|
1774
|
+
return `${PASS_START}${passthruKey}${PASS_END}`
|
|
1775
|
+
})
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
return text
|
|
1779
|
+
},
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Restore passthrough text by reinserting into placeholder positions.
|
|
1783
|
+
*
|
|
1784
|
+
* @param {string} text
|
|
1785
|
+
* @returns {string}
|
|
1786
|
+
*/
|
|
1787
|
+
async restorePassthroughs(text) {
|
|
1788
|
+
if (!text.includes(PASS_START)) return text
|
|
1789
|
+
const passthrus = this.passthroughs
|
|
1790
|
+
return asyncReplace(text, globalRx(PASS_SLOT_RX), async (match, p1) => {
|
|
1791
|
+
const pass = passthrus[parseInt(p1, 10)]
|
|
1792
|
+
if (pass) {
|
|
1793
|
+
let subbedText = await this.applySubs(pass.text, pass.subs)
|
|
1794
|
+
const type = pass.type
|
|
1795
|
+
if (type) {
|
|
1796
|
+
const attributes = pass.attributes
|
|
1797
|
+
const id = attributes?.id
|
|
1798
|
+
subbedText = await new Inline(this, 'quoted', subbedText, {
|
|
1799
|
+
type,
|
|
1800
|
+
id,
|
|
1801
|
+
attributes,
|
|
1802
|
+
}).convert()
|
|
1803
|
+
}
|
|
1804
|
+
return subbedText.includes(PASS_START)
|
|
1805
|
+
? this.restorePassthroughs(subbedText)
|
|
1806
|
+
: subbedText
|
|
1807
|
+
} else {
|
|
1808
|
+
this.logger.error(`unresolved passthrough detected: ${text}`)
|
|
1809
|
+
return '??pass??'
|
|
1810
|
+
}
|
|
1811
|
+
})
|
|
1812
|
+
},
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Resolve the list of comma-delimited subs against the possible options.
|
|
1816
|
+
*
|
|
1817
|
+
* @param {string} subs
|
|
1818
|
+
* @param {'block'|'inline'} [type='block']
|
|
1819
|
+
* @param {string[]|null} [defaults=null]
|
|
1820
|
+
* @param {string|null} [subject=null]
|
|
1821
|
+
* @returns {string[]|undefined}
|
|
1822
|
+
*/
|
|
1823
|
+
resolveSubs(subs, type = 'block', defaults = null, subject = null) {
|
|
1824
|
+
if (!subs || subs.length === 0) return undefined
|
|
1825
|
+
let candidates = null
|
|
1826
|
+
if (subs.includes(' ')) subs = subs.split(' ').join('')
|
|
1827
|
+
const modifiersPresent = SubModifierSniffRx.test(subs)
|
|
1828
|
+
|
|
1829
|
+
for (let key of subs.split(',')) {
|
|
1830
|
+
let modifierOperation = null
|
|
1831
|
+
if (modifiersPresent) {
|
|
1832
|
+
const first = key.charAt(0)
|
|
1833
|
+
if (first === '+') {
|
|
1834
|
+
modifierOperation = 'append'
|
|
1835
|
+
key = key.slice(1)
|
|
1836
|
+
} else if (first === '-') {
|
|
1837
|
+
modifierOperation = 'remove'
|
|
1838
|
+
key = key.slice(1)
|
|
1839
|
+
} else if (key.endsWith('+')) {
|
|
1840
|
+
modifierOperation = 'prepend'
|
|
1841
|
+
key = key.slice(0, -1)
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
let resolvedKeys
|
|
1846
|
+
if (type === 'inline' && (key === 'verbatim' || key === 'v')) {
|
|
1847
|
+
resolvedKeys = BASIC_SUBS
|
|
1848
|
+
} else if (key in SUB_GROUPS) {
|
|
1849
|
+
resolvedKeys = SUB_GROUPS[key]
|
|
1850
|
+
} else if (type === 'inline' && key.length === 1 && key in SUB_HINTS) {
|
|
1851
|
+
const resolvedKey = SUB_HINTS[key]
|
|
1852
|
+
resolvedKeys = SUB_GROUPS[resolvedKey] || [resolvedKey]
|
|
1853
|
+
} else {
|
|
1854
|
+
resolvedKeys = [key]
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (modifierOperation) {
|
|
1858
|
+
candidates = candidates ?? (defaults ? [...defaults] : [])
|
|
1859
|
+
switch (modifierOperation) {
|
|
1860
|
+
case 'append':
|
|
1861
|
+
candidates = [...candidates, ...resolvedKeys]
|
|
1862
|
+
break
|
|
1863
|
+
case 'prepend':
|
|
1864
|
+
candidates = [...resolvedKeys, ...candidates]
|
|
1865
|
+
break
|
|
1866
|
+
case 'remove':
|
|
1867
|
+
candidates = arrayDiff(candidates, resolvedKeys)
|
|
1868
|
+
break
|
|
1869
|
+
}
|
|
1870
|
+
} else {
|
|
1871
|
+
candidates = candidates ?? []
|
|
1872
|
+
candidates = [...candidates, ...resolvedKeys]
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (!candidates) return undefined
|
|
1877
|
+
|
|
1878
|
+
// weed out invalid options and remove duplicates (order preserved; first occurrence wins)
|
|
1879
|
+
const resolved = arrayIntersect(candidates, SUB_OPTIONS[type])
|
|
1880
|
+
const invalid = arrayDiff(candidates, resolved)
|
|
1881
|
+
if (invalid.length > 0) {
|
|
1882
|
+
this.logger.warn(
|
|
1883
|
+
`invalid substitution type${invalid.length > 1 ? 's' : ''}${subject ? ' for ' : ''}${subject || ''}: ${invalid.join(', ')}`
|
|
1884
|
+
)
|
|
1885
|
+
}
|
|
1886
|
+
return resolved
|
|
1887
|
+
},
|
|
1888
|
+
|
|
1889
|
+
/** Call resolveSubs for the 'block' type. */
|
|
1890
|
+
resolveBlockSubs(subs, defaults, subject) {
|
|
1891
|
+
return this.resolveSubs(subs, 'block', defaults, subject)
|
|
1892
|
+
},
|
|
1893
|
+
|
|
1894
|
+
/** Call resolveSubs for the 'inline' type with subject set as passthrough macro. */
|
|
1895
|
+
resolvePassSubs(subs, subject = 'passthrough macro') {
|
|
1896
|
+
return this.resolveSubs(subs, 'inline', null, subject)
|
|
1897
|
+
},
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Expand all groups in the subs list and return.
|
|
1901
|
+
*
|
|
1902
|
+
* @param {string|string[]} subs
|
|
1903
|
+
* @param {string|null} [subject=null]
|
|
1904
|
+
* @returns {string[]|null}
|
|
1905
|
+
*/
|
|
1906
|
+
expandSubs(subs, subject = null) {
|
|
1907
|
+
if (typeof subs === 'string') {
|
|
1908
|
+
// subs is a single key name
|
|
1909
|
+
if (subs === 'none') return null
|
|
1910
|
+
return SUB_GROUPS[subs] || [subs]
|
|
1911
|
+
} else if (Array.isArray(subs)) {
|
|
1912
|
+
const expandedSubs = []
|
|
1913
|
+
for (const key of subs) {
|
|
1914
|
+
if (key !== 'none') {
|
|
1915
|
+
const subGroup = SUB_GROUPS[key]
|
|
1916
|
+
if (subGroup) {
|
|
1917
|
+
expandedSubs.push(...subGroup)
|
|
1918
|
+
} else {
|
|
1919
|
+
expandedSubs.push(key)
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return expandedSubs.length === 0 ? null : expandedSubs
|
|
1924
|
+
} else {
|
|
1925
|
+
return this.resolveSubs(subs, 'inline', null, subject)
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* Commit the requested substitutions to this block.
|
|
1931
|
+
* Looks for an attribute named "subs". If present, resolves substitutions.
|
|
1932
|
+
*/
|
|
1933
|
+
commitSubs() {
|
|
1934
|
+
let defaultSubs = this.defaultSubs
|
|
1935
|
+
if (!defaultSubs) {
|
|
1936
|
+
switch (this.contentModel) {
|
|
1937
|
+
case 'simple':
|
|
1938
|
+
defaultSubs = NORMAL_SUBS
|
|
1939
|
+
break
|
|
1940
|
+
case 'verbatim':
|
|
1941
|
+
defaultSubs = this.context === 'verse' ? NORMAL_SUBS : VERBATIM_SUBS
|
|
1942
|
+
break
|
|
1943
|
+
case 'raw':
|
|
1944
|
+
defaultSubs = this.context === 'stem' ? BASIC_SUBS : NO_SUBS
|
|
1945
|
+
break
|
|
1946
|
+
default:
|
|
1947
|
+
return this.subs
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const customSubs = this.attributes.subs
|
|
1952
|
+
if (customSubs) {
|
|
1953
|
+
this.subs =
|
|
1954
|
+
this.resolveBlockSubs(customSubs, defaultSubs, this.context) || []
|
|
1955
|
+
} else {
|
|
1956
|
+
this.subs = [...defaultSubs]
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
if (
|
|
1960
|
+
this.context === 'listing' &&
|
|
1961
|
+
this.style === 'source' &&
|
|
1962
|
+
this.document.syntaxHighlighter?.handlesHighlighting()
|
|
1963
|
+
) {
|
|
1964
|
+
const idx = this.subs.indexOf('specialcharacters')
|
|
1965
|
+
if (idx !== -1) this.subs[idx] = 'highlight'
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
return null
|
|
1969
|
+
},
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Parse attributes in name or name=value format from a comma-separated String.
|
|
1973
|
+
*
|
|
1974
|
+
* @param {string} attrlist
|
|
1975
|
+
* @param {string[]} [posattrs=[]]
|
|
1976
|
+
* @param {Object} [opts={}]
|
|
1977
|
+
* @returns {Object}
|
|
1978
|
+
*/
|
|
1979
|
+
async parseAttributes(attrlist, posattrs = [], opts = {}) {
|
|
1980
|
+
if (!attrlist || attrlist.length === 0) return {}
|
|
1981
|
+
if (opts.unescapeInput) attrlist = this.normalizeText(attrlist, true, true)
|
|
1982
|
+
if ((opts.subInput || opts.sub_input) && attrlist.includes(ATTR_REF_HEAD)) {
|
|
1983
|
+
attrlist = this.document.subAttributes(attrlist)
|
|
1984
|
+
}
|
|
1985
|
+
const block = opts.subResult || opts.sub_result ? this : null
|
|
1986
|
+
const al = new AttributeList(attrlist, block)
|
|
1987
|
+
if (opts.into) {
|
|
1988
|
+
return al.parseInto(opts.into, posattrs)
|
|
1989
|
+
}
|
|
1990
|
+
return al.parse(posattrs)
|
|
1991
|
+
},
|
|
1992
|
+
|
|
1993
|
+
// ── Private methods ────────────────────────────────────────────────────────
|
|
1994
|
+
|
|
1995
|
+
async extractAttributesFromText(text, defaultText = null) {
|
|
1996
|
+
const attrlist = text.includes(LF) ? text.split(LF).join(' ') : text
|
|
1997
|
+
const attrs = await new AttributeList(attrlist, this).parse()
|
|
1998
|
+
const resolvedText = attrs[1]
|
|
1999
|
+
if (resolvedText != null) {
|
|
2000
|
+
if (resolvedText === attrlist) {
|
|
2001
|
+
Object.keys(attrs).forEach((k) => {
|
|
2002
|
+
delete attrs[k]
|
|
2003
|
+
})
|
|
2004
|
+
return [text, attrs]
|
|
2005
|
+
}
|
|
2006
|
+
return [resolvedText, attrs]
|
|
2007
|
+
}
|
|
2008
|
+
return [defaultText, attrs]
|
|
2009
|
+
},
|
|
2010
|
+
|
|
2011
|
+
extractCallouts(source) {
|
|
2012
|
+
const calloutMarks = {}
|
|
2013
|
+
let autonum = 0
|
|
2014
|
+
let lineno = 0
|
|
2015
|
+
let lastLineno = null
|
|
2016
|
+
const calloutRx = this.hasAttribute('line-comment')
|
|
2017
|
+
? CalloutExtractRxMap[this.getAttribute('line-comment')]
|
|
2018
|
+
: CalloutExtractRx
|
|
2019
|
+
|
|
2020
|
+
const lines = source.split(LF).map((line) => {
|
|
2021
|
+
lineno++
|
|
2022
|
+
return line.replace(globalRx(calloutRx), (match, p1, p2, p3, p4) => {
|
|
2023
|
+
if (p2) {
|
|
2024
|
+
return match.replace(RS, '')
|
|
2025
|
+
}
|
|
2026
|
+
const guard = p1 || (p3 === '--' ? ['<!--', '-->'] : null)
|
|
2027
|
+
const numeral = p4 === '.' ? String(++autonum) : p4
|
|
2028
|
+
;(calloutMarks[lineno] = calloutMarks[lineno] || []).push([
|
|
2029
|
+
guard,
|
|
2030
|
+
numeral,
|
|
2031
|
+
])
|
|
2032
|
+
lastLineno = lineno
|
|
2033
|
+
return ''
|
|
2034
|
+
})
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
let result = lines.join(LF)
|
|
2038
|
+
if (lastLineno !== null) {
|
|
2039
|
+
if (lastLineno === lineno) result = `${result}${LF}`
|
|
2040
|
+
} else {
|
|
2041
|
+
return [result, null]
|
|
2042
|
+
}
|
|
2043
|
+
return [result, calloutMarks]
|
|
2044
|
+
},
|
|
2045
|
+
|
|
2046
|
+
async restoreCallouts(source, calloutMarks, sourceOffset = null) {
|
|
2047
|
+
let preamble = ''
|
|
2048
|
+
if (sourceOffset !== null) {
|
|
2049
|
+
preamble = source.slice(0, sourceOffset)
|
|
2050
|
+
source = source.slice(sourceOffset)
|
|
2051
|
+
}
|
|
2052
|
+
let lineno = 0
|
|
2053
|
+
const result = await Promise.all(
|
|
2054
|
+
source.split(LF).map(async (line) => {
|
|
2055
|
+
const conums = calloutMarks[++lineno]
|
|
2056
|
+
if (conums) {
|
|
2057
|
+
delete calloutMarks[lineno]
|
|
2058
|
+
if (conums.length === 1) {
|
|
2059
|
+
const [guard, numeral] = conums[0]
|
|
2060
|
+
return `${line}${await new Inline(this, 'callout', numeral, {
|
|
2061
|
+
id: this.document.callouts.readNextId(),
|
|
2062
|
+
attributes: { guard },
|
|
2063
|
+
}).convert()}`
|
|
2064
|
+
} else {
|
|
2065
|
+
const converted = await Promise.all(
|
|
2066
|
+
conums.map(([guard, numeral]) =>
|
|
2067
|
+
new Inline(this, 'callout', numeral, {
|
|
2068
|
+
id: this.document.callouts.readNextId(),
|
|
2069
|
+
attributes: { guard },
|
|
2070
|
+
}).convert()
|
|
2071
|
+
)
|
|
2072
|
+
)
|
|
2073
|
+
return `${line}${converted.join(' ')}`
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return line
|
|
2077
|
+
})
|
|
2078
|
+
)
|
|
2079
|
+
return preamble + result.join(LF)
|
|
2080
|
+
},
|
|
2081
|
+
|
|
2082
|
+
async convertQuotedText(args, type, scope) {
|
|
2083
|
+
// args: [fullMatch, group1, group2, ...]
|
|
2084
|
+
const fullMatch = args[0]
|
|
2085
|
+
if (fullMatch.startsWith(RS)) {
|
|
2086
|
+
if (scope === 'constrained') {
|
|
2087
|
+
const attrs = args[2]
|
|
2088
|
+
if (attrs) {
|
|
2089
|
+
return `[${attrs}]${await new Inline(this, 'quoted', args[3], { type }).convert()}`
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return fullMatch.slice(1)
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
if (scope === 'constrained') {
|
|
2096
|
+
const attrlist = args[2]
|
|
2097
|
+
let id, attributes
|
|
2098
|
+
if (attrlist) {
|
|
2099
|
+
attributes = this.parseQuotedTextAttributes(attrlist)
|
|
2100
|
+
id = attributes.id
|
|
2101
|
+
if (type === 'mark') type = 'unquoted'
|
|
2102
|
+
}
|
|
2103
|
+
return `${args[1] || ''}${await new Inline(this, 'quoted', args[3], { type, id, attributes }).convert()}`
|
|
2104
|
+
} else {
|
|
2105
|
+
const attrlist = args[1]
|
|
2106
|
+
let id, attributes
|
|
2107
|
+
if (attrlist) {
|
|
2108
|
+
attributes = this.parseQuotedTextAttributes(attrlist)
|
|
2109
|
+
id = attributes.id
|
|
2110
|
+
if (type === 'mark') type = 'unquoted'
|
|
2111
|
+
}
|
|
2112
|
+
return new Inline(this, 'quoted', args[2], {
|
|
2113
|
+
type,
|
|
2114
|
+
id,
|
|
2115
|
+
attributes,
|
|
2116
|
+
}).convert()
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
|
|
2120
|
+
doReplacement(match, replacement, restore) {
|
|
2121
|
+
const captured = match[0]
|
|
2122
|
+
if (captured.includes(RS)) {
|
|
2123
|
+
return captured.replace(RS, '')
|
|
2124
|
+
}
|
|
2125
|
+
switch (restore) {
|
|
2126
|
+
case 'none':
|
|
2127
|
+
return replacement
|
|
2128
|
+
case 'bounding':
|
|
2129
|
+
return match[1] + replacement + match[2]
|
|
2130
|
+
default: // 'leading'
|
|
2131
|
+
return match[1] + replacement
|
|
2132
|
+
}
|
|
2133
|
+
},
|
|
2134
|
+
|
|
2135
|
+
/** Inserts text into a formatted text enclosure (sprintf). */
|
|
2136
|
+
subPlaceholder(format, ...args) {
|
|
2137
|
+
let i = 0
|
|
2138
|
+
return format.replace(/%s/g, () => String(args[i++] ?? ''))
|
|
2139
|
+
},
|
|
2140
|
+
|
|
2141
|
+
parseQuotedTextAttributes(str) {
|
|
2142
|
+
if (str.includes(ATTR_REF_HEAD)) str = this.subAttributes(str)
|
|
2143
|
+
// for compliance, only consider first positional attribute
|
|
2144
|
+
if (str.includes(',')) str = str.slice(0, str.indexOf(','))
|
|
2145
|
+
str = str.trim()
|
|
2146
|
+
if (!str) return {}
|
|
2147
|
+
if (
|
|
2148
|
+
(str.startsWith('.') || str.startsWith('#')) &&
|
|
2149
|
+
Compliance.shorthandPropertySyntax
|
|
2150
|
+
) {
|
|
2151
|
+
const [before, , after] = partition(str, '#')
|
|
2152
|
+
const attrs = {}
|
|
2153
|
+
if (!after) {
|
|
2154
|
+
if (before.length > 1)
|
|
2155
|
+
attrs.role = before.slice(1).split('.').join(' ').trimStart()
|
|
2156
|
+
} else {
|
|
2157
|
+
const [id, , roles] = partition(after, '.')
|
|
2158
|
+
if (id) attrs.id = id
|
|
2159
|
+
if (!roles) {
|
|
2160
|
+
if (before.length > 1)
|
|
2161
|
+
attrs.role = before.slice(1).split('.').join(' ').trimStart()
|
|
2162
|
+
} else if (before.length > 1) {
|
|
2163
|
+
attrs.role = `${before}.${roles}`
|
|
2164
|
+
.slice(1)
|
|
2165
|
+
.split('.')
|
|
2166
|
+
.join(' ')
|
|
2167
|
+
.trimStart()
|
|
2168
|
+
} else {
|
|
2169
|
+
attrs.role = roles.split('.').join(' ')
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return attrs
|
|
2173
|
+
}
|
|
2174
|
+
return { role: str }
|
|
2175
|
+
},
|
|
2176
|
+
|
|
2177
|
+
normalizeText(
|
|
2178
|
+
text,
|
|
2179
|
+
normalizeWhitespace = null,
|
|
2180
|
+
unescapeClosingSquareBrackets = null
|
|
2181
|
+
) {
|
|
2182
|
+
if (text && text.length > 0) {
|
|
2183
|
+
if (normalizeWhitespace) text = text.trim().split(LF).join(' ')
|
|
2184
|
+
if (unescapeClosingSquareBrackets && text.includes(R_SB)) {
|
|
2185
|
+
text = text.split(ESC_R_SB).join(R_SB)
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return text
|
|
2189
|
+
},
|
|
2190
|
+
|
|
2191
|
+
splitSimpleCsv(str) {
|
|
2192
|
+
if (!str || str.length === 0) return []
|
|
2193
|
+
if (str.includes('"')) {
|
|
2194
|
+
const values = []
|
|
2195
|
+
let accum = ''
|
|
2196
|
+
let quoteOpen = false
|
|
2197
|
+
for (const c of str) {
|
|
2198
|
+
if (c === ',') {
|
|
2199
|
+
if (quoteOpen) {
|
|
2200
|
+
accum += c
|
|
2201
|
+
} else {
|
|
2202
|
+
values.push(accum.trim())
|
|
2203
|
+
accum = ''
|
|
2204
|
+
}
|
|
2205
|
+
} else if (c === '"') {
|
|
2206
|
+
quoteOpen = !quoteOpen
|
|
2207
|
+
} else {
|
|
2208
|
+
accum += c
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
values.push(accum.trim())
|
|
2212
|
+
return values
|
|
2213
|
+
}
|
|
2214
|
+
return str.split(',').map((item) => item.trim())
|
|
2215
|
+
},
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
export {
|
|
2219
|
+
BASIC_SUBS,
|
|
2220
|
+
HEADER_SUBS,
|
|
2221
|
+
NO_SUBS,
|
|
2222
|
+
NORMAL_SUBS,
|
|
2223
|
+
REFTEXT_SUBS,
|
|
2224
|
+
VERBATIM_SUBS,
|
|
2225
|
+
SUB_GROUPS,
|
|
2226
|
+
SUB_HINTS,
|
|
2227
|
+
SUB_OPTIONS,
|
|
2228
|
+
CAN,
|
|
2229
|
+
DEL,
|
|
2230
|
+
PASS_START,
|
|
2231
|
+
PASS_END,
|
|
2232
|
+
PASS_SLOT_RX,
|
|
2233
|
+
HIGHLIGHTED_PASS_SLOT_RX,
|
|
2234
|
+
RS,
|
|
2235
|
+
R_SB,
|
|
2236
|
+
ESC_R_SB,
|
|
2237
|
+
PLUS,
|
|
2238
|
+
SPECIAL_CHARS_RX,
|
|
2239
|
+
SPECIAL_CHARS_TR,
|
|
2240
|
+
QUOTED_TEXT_SNIFF_RX,
|
|
2241
|
+
}
|