@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.
- package/README.md +440 -117
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/index.js +33 -503
- package/package.json +28 -4
- package/src/custom-highlight/option-validator.js +152 -0
- package/src/custom-highlight/payload-utils.js +43 -0
- package/src/custom-highlight/runtime-utils.js +83 -0
- package/src/custom-highlight/shiki-keyword-rules.js +407 -0
- package/src/custom-highlight/shiki-keyword.js +417 -0
- package/src/entry/custom-highlight-runtime.js +11 -0
- package/src/entry/custom-highlight.js +25 -0
- package/src/entry/markup-highlight.js +40 -0
- package/src/fence/render-api-constants.js +34 -0
- package/src/fence/render-api-provider.js +810 -0
- package/src/fence/render-api-renderer.js +136 -0
- package/src/fence/render-api-runtime.js +712 -0
- package/src/fence/render-api.js +117 -0
- package/src/fence/render-markup.js +161 -0
- package/src/fence/render-shared.js +428 -0
- package/src/utils/attr-utils.js +120 -0
- package/src/utils/pre-code-wrapper-parser.js +114 -0
|
@@ -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
|
+
}
|