@peaceroad/markdown-it-renderer-fence 0.4.1 → 0.5.0

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.
@@ -0,0 +1,117 @@
1
+ import {
2
+ getCustomHighlightPayloadMap,
3
+ } from '../custom-highlight/payload-utils.js'
4
+ import {
5
+ applyLineEndAlias,
6
+ finalizeCommonFenceOption,
7
+ prepareFenceRenderContext,
8
+ } from './render-shared.js'
9
+ import {
10
+ customHighlightDataEnvKey,
11
+ customHighlightEnvInitRuleName,
12
+ customHighlightPayloadSchemaVersion,
13
+ customHighlightPayloadSupportedVersions,
14
+ runtimeFallbackReasonSet,
15
+ customHighlightSeqEnvKey,
16
+ } from './render-api-constants.js'
17
+ import {
18
+ isNormalizedCustomHighlightOpt,
19
+ normalizeCustomHighlightOpt,
20
+ renderCustomHighlightPayloadScript,
21
+ renderCustomHighlightScopeStyleTag,
22
+ shouldApplyApiFallbackForReason,
23
+ } from './render-api-provider.js'
24
+ import {
25
+ getFenceHtml,
26
+ } from './render-api-renderer.js'
27
+ import {
28
+ applyCustomHighlights,
29
+ clearCustomHighlights,
30
+ observeCustomHighlights,
31
+ } from './render-api-runtime.js'
32
+
33
+ const shouldRuntimeFallback = (reason, opt = {}) => {
34
+ const chOpt = isNormalizedCustomHighlightOpt(opt) ? opt : normalizeCustomHighlightOpt(opt)
35
+ if (!runtimeFallbackReasonSet.has(reason)) return false
36
+ return shouldApplyApiFallbackForReason(chOpt, reason)
37
+ }
38
+
39
+ const mditRendererFenceCustomHighlight = (md, option) => {
40
+ const opt = {
41
+ attrsOrder: ['class', 'id', 'data-*', 'style'],
42
+ setHighlight: true,
43
+ setLineNumber: true,
44
+ setEmphasizeLines: true,
45
+ lineEndSpanThreshold: 0,
46
+ lineEndSpanClass: 'pre-lineend-spacer',
47
+ setPreWrapStyle: true,
48
+ useHighlightPre: false,
49
+ onFenceDecision: null,
50
+ onFenceDecisionTiming: false,
51
+ sampLang: 'shell,console',
52
+ langPrefix: md.options.langPrefix || 'language-',
53
+ customHighlight: null,
54
+ }
55
+ if (option) {
56
+ Object.assign(opt, option)
57
+ applyLineEndAlias(opt, option)
58
+ }
59
+
60
+ const rawCustomHighlightOpt = (opt.customHighlight && typeof opt.customHighlight === 'object')
61
+ ? opt.customHighlight
62
+ : {}
63
+ opt.customHighlight = normalizeCustomHighlightOpt(rawCustomHighlightOpt)
64
+ if (opt.customHighlight.provider === 'custom') {
65
+ opt.customHighlight._customGetRanges = (typeof opt.customHighlight.getRanges === 'function')
66
+ ? opt.customHighlight.getRanges
67
+ : null
68
+ } else if (opt.customHighlight.provider === 'hljs') {
69
+ opt.customHighlight._hljsHighlightFn =
70
+ (typeof opt.customHighlight.hljsHighlight === 'function')
71
+ ? opt.customHighlight.hljsHighlight
72
+ : ((typeof opt.customHighlight.highlight === 'function')
73
+ ? opt.customHighlight.highlight
74
+ : ((md && md.options && typeof md.options.highlight === 'function') ? md.options.highlight : null))
75
+ } else if (opt.customHighlight.provider === 'shiki') {
76
+ opt.customHighlight._hasShikiHighlighter = !!(opt.customHighlight.highlighter && typeof opt.customHighlight.highlighter.codeToTokens === 'function')
77
+ const tokenOptionBase = {}
78
+ if (opt.customHighlight._singleTheme) tokenOptionBase.theme = opt.customHighlight._singleTheme
79
+ if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === 'keyword') {
80
+ tokenOptionBase.includeExplanation = 'scopeName'
81
+ }
82
+ opt.customHighlight._shikiTokenOptionBase = Object.keys(tokenOptionBase).length ? tokenOptionBase : null
83
+ }
84
+
85
+ finalizeCommonFenceOption(opt)
86
+
87
+ md.core.ruler.before('block', customHighlightEnvInitRuleName, (state) => {
88
+ const env = state && state.env
89
+ if (!env || typeof env !== 'object') return
90
+ if (opt.customHighlight.transport === 'env') {
91
+ env[customHighlightDataEnvKey] = {}
92
+ env[customHighlightSeqEnvKey] = 0
93
+ return
94
+ }
95
+ delete env[customHighlightDataEnvKey]
96
+ delete env[customHighlightSeqEnvKey]
97
+ })
98
+
99
+ md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
100
+ const context = prepareFenceRenderContext(tokens, idx, opt)
101
+ return getFenceHtml(context, md, opt, slf, env)
102
+ }
103
+ }
104
+
105
+ export {
106
+ applyCustomHighlights,
107
+ clearCustomHighlights,
108
+ customHighlightPayloadSchemaVersion,
109
+ customHighlightPayloadSupportedVersions,
110
+ getCustomHighlightPayloadMap,
111
+ observeCustomHighlights,
112
+ renderCustomHighlightPayloadScript,
113
+ renderCustomHighlightScopeStyleTag,
114
+ shouldRuntimeFallback,
115
+ }
116
+
117
+ export default mditRendererFenceCustomHighlight
@@ -0,0 +1,161 @@
1
+ import {
2
+ appendStyleValue,
3
+ mergeAttrSets,
4
+ parseHtmlAttrs,
5
+ } from '../utils/attr-utils.js'
6
+ import {
7
+ addTimingMs,
8
+ commentLineClass,
9
+ emitFenceDecision,
10
+ finalizeFenceTimings,
11
+ getLogicalLineCount,
12
+ getNowMs,
13
+ normalizeEmphasisRanges,
14
+ orderTokenAttrs,
15
+ preWrapStyle,
16
+ splitFenceBlockToLines,
17
+ } from './render-shared.js'
18
+ import {
19
+ parsePreCodeWrapper,
20
+ } from '../utils/pre-code-wrapper-parser.js'
21
+
22
+ const renderFenceMarkup = (context, md, opt, slf) => {
23
+ const token = context.token
24
+ const lang = context.lang
25
+ const timingEnabled = context.timingEnabled
26
+ const timings = context.timings
27
+ const fenceStartedAt = context.fenceStartedAt
28
+ const startNumber = context.startNumber
29
+ const emphasizeLines = context.emphasizeLines
30
+ const wrapEnabled = context.wrapEnabled
31
+ const preWrapValue = context.preWrapValue
32
+ const commentMarkValue = context.commentMarkValue
33
+
34
+ const isSamp = opt._sampReg.test(lang)
35
+ const sourceContent = token.content
36
+ let content = sourceContent
37
+ let commentLines
38
+ let needComment = false
39
+
40
+ if (opt.setHighlight && md.options.highlight) {
41
+ const highlightStartedAt = timingEnabled ? getNowMs() : 0
42
+ if (lang && lang !== 'samp') {
43
+ content = md.options.highlight(content, lang)
44
+ } else {
45
+ content = md.utils.escapeHtml(sourceContent)
46
+ }
47
+ if (timingEnabled) addTimingMs(timings, 'highlightMs', getNowMs() - highlightStartedAt)
48
+ } else {
49
+ content = md.utils.escapeHtml(sourceContent)
50
+ }
51
+
52
+ let preAttrsFromHighlight
53
+ let hasHighlightPre = false
54
+ const hasPreTag = (content.indexOf('<pre') !== -1 || content.indexOf('<PRE') !== -1)
55
+ if (hasPreTag) {
56
+ const preMatch = parsePreCodeWrapper(content)
57
+ if (preMatch) {
58
+ hasHighlightPre = true
59
+ preAttrsFromHighlight = parseHtmlAttrs(preMatch.preAttrsText)
60
+ const codeAttrsFromHighlight = parseHtmlAttrs(preMatch.codeAttrsText)
61
+ content = preMatch.content
62
+ if (codeAttrsFromHighlight.length) {
63
+ if (!token.attrs) token.attrs = []
64
+ mergeAttrSets(token.attrs, codeAttrsFromHighlight)
65
+ }
66
+ }
67
+ }
68
+
69
+ if (opt.useHighlightPre && hasPreTag && !hasHighlightPre) {
70
+ const decision = {
71
+ renderer: 'markup',
72
+ useHighlightPre: true,
73
+ passthrough: true,
74
+ passthroughReason: 'pre-code-parse-failed',
75
+ hasHighlightPre: false,
76
+ disabledFeatures: ['setLineNumber', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'],
77
+ }
78
+ if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
79
+ emitFenceDecision(opt, decision)
80
+ return content.endsWith('\n') ? content : content + '\n'
81
+ }
82
+
83
+ orderTokenAttrs(token, opt)
84
+
85
+ const preAttrs = preAttrsFromHighlight ? preAttrsFromHighlight.slice() : []
86
+ if (preWrapValue !== undefined) {
87
+ const idx = preAttrs.findIndex((attr) => attr[0] === 'data-pre-wrap')
88
+ if (idx === -1) {
89
+ preAttrs.push(['data-pre-wrap', preWrapValue])
90
+ } else {
91
+ preAttrs[idx][1] = preWrapValue
92
+ }
93
+ }
94
+ if (wrapEnabled && opt.setPreWrapStyle !== false) {
95
+ const idx = preAttrs.findIndex((attr) => attr[0] === 'style')
96
+ if (idx === -1) {
97
+ preAttrs.push(['style', preWrapStyle])
98
+ } else {
99
+ preAttrs[idx][1] = appendStyleValue(preAttrs[idx][1], preWrapStyle)
100
+ }
101
+ }
102
+ if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
103
+ const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
104
+
105
+ const needLineNumber = opt.setLineNumber && startNumber >= 0
106
+ const normalizedEmphasis = (opt.setEmphasizeLines && emphasizeLines.length > 0)
107
+ ? normalizeEmphasisRanges(emphasizeLines, getLogicalLineCount(content))
108
+ : []
109
+ const needEmphasis = normalizedEmphasis.length > 0
110
+ const needEndSpan = opt.lineEndSpanThreshold > 0
111
+ const useHighlightPre = opt.useHighlightPre && hasHighlightPre
112
+
113
+ if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
114
+ const sourceLogicalLineCount = getLogicalLineCount(sourceContent)
115
+ const highlightedLogicalLineCount = getLogicalLineCount(content)
116
+ if (highlightedLogicalLineCount === sourceLogicalLineCount) {
117
+ const rawLines = sourceContent.split('\n')
118
+ for (let i = 0; i < rawLines.length; i++) {
119
+ if (rawLines[i].trimStart().startsWith(commentMarkValue)) {
120
+ if (!commentLines) commentLines = []
121
+ commentLines[i] = true
122
+ needComment = true
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
129
+ const splitStartedAt = timingEnabled ? getNowMs() : 0
130
+ const nlIndex = content.indexOf('\n')
131
+ const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
132
+ content = splitFenceBlockToLines(
133
+ content,
134
+ normalizedEmphasis,
135
+ needLineNumber,
136
+ needEmphasis,
137
+ needEndSpan,
138
+ opt.lineEndSpanThreshold,
139
+ opt.lineEndSpanClass,
140
+ br,
141
+ commentLines,
142
+ commentLineClass,
143
+ )
144
+ if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
145
+ }
146
+
147
+ const tag = isSamp ? 'samp' : 'code'
148
+ const decision = {
149
+ renderer: 'markup',
150
+ useHighlightPre,
151
+ hasHighlightPre,
152
+ disabledFeatures: useHighlightPre ? ['setLineNumber', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'] : [],
153
+ }
154
+ if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
155
+ emitFenceDecision(opt, decision)
156
+ return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
157
+ }
158
+
159
+ export {
160
+ renderFenceMarkup,
161
+ }
@@ -0,0 +1,428 @@
1
+ import {
2
+ appendStyleValue,
3
+ createAttrOrderIndexGetter,
4
+ getInfoAttr,
5
+ getLangFromClassAttr,
6
+ } from '../utils/attr-utils.js'
7
+
8
+ const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
9
+ const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
10
+ const preLineTag = '<span class="pre-line">'
11
+ const emphOpenTag = '<span class="pre-lines-emphasis">'
12
+ const commentLineClass = 'pre-line-comment'
13
+ const closeTag = '</span>'
14
+ const closeTagLen = closeTag.length
15
+ const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
16
+ const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
17
+ const nonNegativeIntReg = /^\d+$/
18
+
19
+ const getLineVisualLengthIgnoringTags = (line, threshold) => {
20
+ let len = 0
21
+ let inTag = false
22
+ for (let i = 0; i < line.length; i++) {
23
+ const code = line.charCodeAt(i)
24
+ if (inTag) {
25
+ if (code === 62) inTag = false // >
26
+ continue
27
+ }
28
+ if (code === 60) { // <
29
+ inTag = true
30
+ continue
31
+ }
32
+ len += code > 255 ? 2 : 1
33
+ if (len >= threshold) return len
34
+ }
35
+ return len
36
+ }
37
+
38
+ const getNowMs = () => {
39
+ if (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') return performance.now()
40
+ return Date.now()
41
+ }
42
+
43
+ const roundTimingMs = (value) => {
44
+ return Math.round(value * 1000) / 1000
45
+ }
46
+
47
+ const addTimingMs = (timings, key, value) => {
48
+ if (!timings || !key || !Number.isFinite(value) || value <= 0) return
49
+ timings[key] = (timings[key] || 0) + value
50
+ }
51
+
52
+ const finalizeFenceTimings = (timings, startedAtMs) => {
53
+ if (!timings || !Number.isFinite(startedAtMs)) return null
54
+ const totalMs = getNowMs() - startedAtMs
55
+ const out = {}
56
+ if (timings.attrNormalizeMs) out.attrNormalizeMs = roundTimingMs(timings.attrNormalizeMs)
57
+ if (timings.highlightMs) out.highlightMs = roundTimingMs(timings.highlightMs)
58
+ if (timings.providerMs) out.providerMs = roundTimingMs(timings.providerMs)
59
+ if (timings.lineSplitMs) out.lineSplitMs = roundTimingMs(timings.lineSplitMs)
60
+ out.totalMs = roundTimingMs(totalMs > 0 ? totalMs : 0)
61
+ return out
62
+ }
63
+
64
+ const applyLineEndAlias = (opt, sourceOption) => {
65
+ if (sourceOption && sourceOption.lineEndSpanThreshold == null && sourceOption.setLineEndSpan != null) {
66
+ opt.lineEndSpanThreshold = sourceOption.setLineEndSpan
67
+ }
68
+ }
69
+
70
+ const finalizeCommonFenceOption = (opt) => {
71
+ opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
72
+ opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
73
+ return opt
74
+ }
75
+
76
+ const emitFenceDecision = (opt, data) => {
77
+ if (!opt || typeof opt.onFenceDecision !== 'function') return
78
+ try {
79
+ opt.onFenceDecision(data)
80
+ } catch (e) {}
81
+ }
82
+
83
+ const parseStartNumber = (val) => {
84
+ const str = String(val ?? '')
85
+ if (!nonNegativeIntReg.test(str)) return null
86
+ const num = Number(str)
87
+ if (!Number.isSafeInteger(num) || num < 0) return null
88
+ return num
89
+ }
90
+
91
+ const getLogicalLineCount = (text) => {
92
+ const str = String(text ?? '')
93
+ if (!str) return 0
94
+ let count = 1
95
+ for (let i = 0; i < str.length; i++) {
96
+ const ch = str.charCodeAt(i)
97
+ if (ch !== 10 && ch !== 13) continue
98
+ if (ch === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
99
+ count++
100
+ }
101
+ const last = str.charCodeAt(str.length - 1)
102
+ if (last === 10 || last === 13) count--
103
+ return count
104
+ }
105
+
106
+ const getEmphasizeLines = (attrVal) => {
107
+ const str = String(attrVal ?? '')
108
+ if (!str) return []
109
+ const result = []
110
+ for (const partRaw of str.split(',')) {
111
+ const part = partRaw.trim()
112
+ if (!part) continue
113
+ const hyphen = part.indexOf('-')
114
+ if (hyphen === -1) {
115
+ const n = Number(part)
116
+ if (Number.isFinite(n) && n > 0) result.push([n, n])
117
+ continue
118
+ }
119
+ const start = part.slice(0, hyphen).trim()
120
+ const end = part.slice(hyphen + 1).trim()
121
+ const s = start ? Number(start) : null
122
+ const e = end ? Number(end) : null
123
+ if (s == null && e == null) continue
124
+ if (s != null && (!Number.isFinite(s) || s <= 0)) continue
125
+ if (e != null && (!Number.isFinite(e) || e <= 0)) continue
126
+ result.push([s, e])
127
+ }
128
+ return result
129
+ }
130
+
131
+ const normalizeEmphasisRanges = (emphasizeLines, maxLine) => {
132
+ const normalized = []
133
+ if (!emphasizeLines || !emphasizeLines.length || maxLine <= 0) return normalized
134
+ for (const range of emphasizeLines) {
135
+ let s = range[0]
136
+ let e = range[1]
137
+ if (s == null) s = 1
138
+ if (e == null) e = maxLine
139
+ if (!Number.isFinite(s) || !Number.isFinite(e) || s <= 0 || e <= 0) continue
140
+ if (s > e) {
141
+ const tmp = s
142
+ s = e
143
+ e = tmp
144
+ }
145
+ if (s > maxLine || e < 1) continue
146
+ if (e > maxLine) e = maxLine
147
+ normalized.push([s, e])
148
+ }
149
+ return normalized
150
+ }
151
+
152
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass) => {
153
+ const lines = content.split(br)
154
+ const max = lines.length
155
+ let emIdx = 0
156
+ let emStart = -1
157
+ let emEnd = -1
158
+ if (needEmphasis && emphasizeLines && emphasizeLines.length) {
159
+ emStart = emphasizeLines[0][0]
160
+ emEnd = emphasizeLines[0][1]
161
+ } else {
162
+ needEmphasis = false
163
+ }
164
+
165
+ const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
166
+
167
+ for (let n = 0; n < max; n++) {
168
+ let line = lines[n]
169
+ const notLastLine = n < max - 1
170
+ const doComment = commentLines && commentLines[n]
171
+ let hasLt = false
172
+ let hasLtChecked = false
173
+
174
+ if (needEndSpan && threshold > 0) {
175
+ if (!hasLtChecked) {
176
+ hasLt = line.indexOf('<') !== -1
177
+ hasLtChecked = true
178
+ }
179
+ let lineLen = 0
180
+ if (!hasLt) {
181
+ lineLen = line.length
182
+ } else {
183
+ lineLen = getLineVisualLengthIgnoringTags(line, threshold)
184
+ }
185
+ if (lineLen >= threshold) {
186
+ if (line.endsWith(closeTag)) {
187
+ line = line.slice(0, -closeTagLen) + endSpanTag + closeTag
188
+ } else {
189
+ line = line + endSpanTag
190
+ }
191
+ }
192
+ }
193
+
194
+ if (needLineNumber && notLastLine) {
195
+ if (!hasLtChecked) {
196
+ hasLt = line.indexOf('<') !== -1
197
+ hasLtChecked = true
198
+ }
199
+ if (hasLt && line.indexOf('>') !== -1) {
200
+ const tagStack = []
201
+ tagReg.lastIndex = 0
202
+ let match
203
+ while ((match = tagReg.exec(line)) !== null) {
204
+ const fullMatch = match[0]
205
+ const tagNameLower = match[1].toLowerCase()
206
+ if (fullMatch.startsWith('</')) {
207
+ if (tagStack[tagStack.length - 1] === tagNameLower) tagStack.pop()
208
+ } else if (!fullMatch.endsWith('/>') && !voidTags.has(tagNameLower)) {
209
+ tagStack.push(tagNameLower)
210
+ }
211
+ }
212
+ for (let i = tagStack.length - 1; i >= 0; i--) {
213
+ const tagName = tagStack[i]
214
+ line += `</${tagName}>`
215
+ lines[n + 1] = `<${tagName}>` + lines[n + 1]
216
+ }
217
+ }
218
+ }
219
+
220
+ if (doComment) {
221
+ line = `<span class="${commentClass}">` + line + closeTag
222
+ }
223
+
224
+ if (needLineNumber && notLastLine) {
225
+ line = preLineTag + line + closeTag
226
+ }
227
+
228
+ if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
229
+ line = emphOpenTag + line
230
+ }
231
+ if (needEmphasis && emIdx < emphasizeLines.length && emEnd === n) {
232
+ line = closeTag + line
233
+ emIdx++
234
+ const nextEmphasis = emphasizeLines[emIdx] || []
235
+ emStart = nextEmphasis[0]
236
+ emEnd = nextEmphasis[1]
237
+ }
238
+
239
+ lines[n] = line
240
+ }
241
+ return lines.join(br)
242
+ }
243
+
244
+ const orderTokenAttrs = (token, opt) => {
245
+ const attrs = token.attrs
246
+ if (!attrs || attrs.length < 2) return
247
+ const rankCache = new Map()
248
+ const getRank = (name) => {
249
+ let rank = rankCache.get(name)
250
+ if (rank === undefined) {
251
+ rank = opt._attrOrderIndex(name)
252
+ rankCache.set(name, rank)
253
+ }
254
+ return rank
255
+ }
256
+ attrs.sort((a, b) => getRank(a[0]) - getRank(b[0]))
257
+ }
258
+
259
+ const prepareFenceRenderContext = (tokens, idx, opt) => {
260
+ const token = tokens[idx]
261
+ const match = token.info.trim().match(infoReg)
262
+ let lang = match ? match[1] : ''
263
+ const timingEnabled = !!(opt.onFenceDecisionTiming && typeof opt.onFenceDecision === 'function')
264
+ const fenceStartedAt = timingEnabled ? getNowMs() : 0
265
+ const timings = timingEnabled ? {} : null
266
+
267
+ if (match && match[2]) {
268
+ const infoAttrs = getInfoAttr(match[2])
269
+ for (let i = 0; i < infoAttrs.length; i++) {
270
+ const attr = infoAttrs[i]
271
+ token.attrJoin(attr[0], attr[1])
272
+ }
273
+ }
274
+ if (lang && lang !== 'samp') {
275
+ const langClass = opt.langPrefix + lang
276
+ const existingClass = token.attrGet('class')
277
+ token.attrSet('class', existingClass ? langClass + ' ' + existingClass : langClass)
278
+ }
279
+
280
+ let startNumber = -1
281
+ let emphasizeLines = []
282
+ let wrapEnabled = false
283
+ let preWrapValue
284
+ let commentMarkValue
285
+ const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
286
+
287
+ if (token.attrs) {
288
+ const newAttrs = []
289
+ let dataPreStartIndex = -1
290
+ let dataPreEmphasisIndex = -1
291
+ let styleIndex = -1
292
+ let dataPreCommentIndex = -1
293
+ let startValue
294
+ let emphasisValue
295
+ let styleValue
296
+ let sawCommentMark = false
297
+ let sawStartAttr = false
298
+ let sawEmphasisAttr = false
299
+ const appendOrder = []
300
+
301
+ for (const attr of token.attrs) {
302
+ const name = attr[0]
303
+ const val = attr[1]
304
+
305
+ switch (name) {
306
+ case 'class': {
307
+ const classLang = getLangFromClassAttr(val, opt.langPrefix)
308
+ if (classLang) lang = classLang
309
+ newAttrs.push(attr)
310
+ break
311
+ }
312
+ case 'style':
313
+ styleIndex = newAttrs.length
314
+ styleValue = val
315
+ newAttrs.push(attr)
316
+ break
317
+ case 'data-pre-start':
318
+ startNumber = parseStartNumber(val) ?? -1
319
+ startValue = val
320
+ dataPreStartIndex = newAttrs.length
321
+ newAttrs.push(attr)
322
+ break
323
+ case 'start':
324
+ case 'pre-start':
325
+ startNumber = parseStartNumber(val) ?? -1
326
+ startValue = val
327
+ if (!sawStartAttr) {
328
+ appendOrder.push('start')
329
+ sawStartAttr = true
330
+ }
331
+ break
332
+ case 'data-pre-emphasis':
333
+ dataPreEmphasisIndex = newAttrs.length
334
+ newAttrs.push(attr)
335
+ break
336
+ case 'data-pre-comment-mark':
337
+ dataPreCommentIndex = newAttrs.length
338
+ commentMarkValue = val
339
+ newAttrs.push(attr)
340
+ break
341
+ case 'em-lines':
342
+ case 'emphasize-lines':
343
+ if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
344
+ emphasisValue = val
345
+ if (!sawEmphasisAttr) {
346
+ appendOrder.push('emphasis')
347
+ sawEmphasisAttr = true
348
+ }
349
+ break
350
+ case 'comment-mark':
351
+ commentMarkValue = val
352
+ if (!sawCommentMark) {
353
+ appendOrder.push('comment')
354
+ sawCommentMark = true
355
+ }
356
+ break
357
+ case 'data-pre-wrap':
358
+ preWrapValue = val
359
+ if (val === '' || val === 'true') wrapEnabled = true
360
+ break
361
+ case 'wrap':
362
+ case 'pre-wrap':
363
+ if (val === '' || val === 'true') wrapEnabled = true
364
+ break
365
+ default:
366
+ newAttrs.push(attr)
367
+ }
368
+ }
369
+
370
+ if (startValue !== undefined && dataPreStartIndex >= 0) newAttrs[dataPreStartIndex][1] = startValue
371
+ if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) newAttrs[dataPreEmphasisIndex][1] = emphasisValue
372
+ if (commentMarkValue !== undefined && dataPreCommentIndex >= 0) newAttrs[dataPreCommentIndex][1] = commentMarkValue
373
+
374
+ for (const kind of appendOrder) {
375
+ if (kind === 'start' && dataPreStartIndex === -1 && startValue !== undefined) {
376
+ newAttrs.push(['data-pre-start', startValue])
377
+ } else if (kind === 'emphasis' && dataPreEmphasisIndex === -1 && emphasisValue !== undefined) {
378
+ newAttrs.push(['data-pre-emphasis', emphasisValue])
379
+ } else if (kind === 'comment' && dataPreCommentIndex === -1 && commentMarkValue !== undefined) {
380
+ newAttrs.push(['data-pre-comment-mark', commentMarkValue])
381
+ }
382
+ }
383
+
384
+ if (startNumber !== -1) {
385
+ styleValue = appendStyleValue(styleValue, 'counter-set:pre-line-number ' + startNumber + ';')
386
+ }
387
+ if (styleIndex >= 0) {
388
+ if (styleValue !== undefined) newAttrs[styleIndex][1] = styleValue
389
+ } else if (styleValue) {
390
+ newAttrs.push(['style', styleValue])
391
+ }
392
+
393
+ if (wrapEnabled) preWrapValue = 'true'
394
+ token.attrs = newAttrs
395
+ }
396
+
397
+ if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
398
+
399
+ return {
400
+ token,
401
+ lang,
402
+ startNumber,
403
+ emphasizeLines,
404
+ wrapEnabled,
405
+ preWrapValue,
406
+ commentMarkValue,
407
+ timingEnabled,
408
+ timings,
409
+ fenceStartedAt,
410
+ }
411
+ }
412
+
413
+ export {
414
+ addTimingMs,
415
+ applyLineEndAlias,
416
+ commentLineClass,
417
+ emitFenceDecision,
418
+ finalizeCommonFenceOption,
419
+ finalizeFenceTimings,
420
+ getLogicalLineCount,
421
+ getNowMs,
422
+ getEmphasizeLines,
423
+ normalizeEmphasisRanges,
424
+ orderTokenAttrs,
425
+ preWrapStyle,
426
+ prepareFenceRenderContext,
427
+ splitFenceBlockToLines,
428
+ }