@peaceroad/markdown-it-renderer-fence 0.6.1 → 0.8.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 +183 -430
- package/docs/README.md +37 -0
- package/docs/code-highlighting-design.md +317 -0
- package/docs/custom-highlight-styling-guide.md +204 -0
- package/package.json +22 -13
- package/src/custom-highlight/option-validator.js +19 -10
- package/src/custom-highlight/scope-name.js +23 -0
- package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +229 -44
- package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +64 -56
- package/src/custom-highlight/shiki-theme-role.js +176 -0
- package/src/entry/markup-highlight.js +4 -0
- package/src/fence/line-notes.js +199 -0
- package/src/fence/render-api-provider.js +62 -62
- package/src/fence/render-api-renderer.js +26 -11
- package/src/fence/render-api-runtime.js +5 -13
- package/src/fence/render-api.js +24 -12
- package/src/fence/render-markup.js +24 -16
- package/src/fence/render-shared.js +232 -20
- package/theme/_shared/rf-core.css +164 -0
- package/theme/rf-basic/README.md +91 -0
- package/theme/rf-basic/api/highlightjs.css +71 -0
- package/theme/rf-basic/api/shiki.css +67 -0
- package/theme/rf-basic/base.css +3 -0
- package/theme/rf-basic/index.css +7 -0
- package/theme/rf-basic/index.js +155 -0
- package/theme/rf-basic/markup/highlightjs.css +77 -0
- package/theme/rf-basic/markup/shiki.css +11 -0
- package/theme/rf-monochrome/README.md +71 -0
- package/theme/rf-monochrome/base.css +3 -0
- package/theme/rf-monochrome/index.css +5 -0
- package/theme/rf-monochrome/index.js +72 -0
- package/theme/rf-monochrome/markup/highlightjs.css +134 -0
- package/theme/rf-monochrome/markup/shiki.css +27 -0
- package/THIRD_PARTY_NOTICES.md +0 -56
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getInfoAttr,
|
|
3
|
+
} from '../utils/attr-utils.js'
|
|
4
|
+
|
|
5
|
+
const lineNotesRuleName = 'renderer_fence_line_notes'
|
|
6
|
+
const lineNotesMetaKey = '__rendererFenceLineNotes'
|
|
7
|
+
const lineNotesInstalledKey = '__rendererFenceLineNotesInstalled'
|
|
8
|
+
const lineNoteHeaderReg = /^\s*([1-9]\d*)(?:\s*-\s*([1-9]\d*))?\s*[::]\s*(.*)$/
|
|
9
|
+
const lineNoteContinuationReg = /^(?: {2,}|\t)/
|
|
10
|
+
const lineNoteAttrsReg = /^(.*?)(?:\s+\{([^{}]+)\})\s*$/
|
|
11
|
+
const lineNoteAttrsOnlyReg = /^\{([^{}]+)\}\s*$/
|
|
12
|
+
const lineNoteWidthReg = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|ch|vw|svw|lvw|dvw|%)$/
|
|
13
|
+
|
|
14
|
+
const canContainLineNotes = (state) => {
|
|
15
|
+
const src = state && state.src
|
|
16
|
+
return typeof src === 'string' && src.indexOf('notes') !== -1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isInfoWhitespaceCode = (code) => {
|
|
20
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 12
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isInfoNameEnd = (str, pos) => {
|
|
24
|
+
if (pos >= str.length) return true
|
|
25
|
+
const code = str.charCodeAt(pos)
|
|
26
|
+
return code === 123 || isInfoWhitespaceCode(code)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const infoNameStartsWith = (info, name) => {
|
|
30
|
+
const str = String(info ?? '')
|
|
31
|
+
let pos = 0
|
|
32
|
+
while (pos < str.length && isInfoWhitespaceCode(str.charCodeAt(pos))) pos++
|
|
33
|
+
return str.startsWith(name, pos) && isInfoNameEnd(str, pos + name.length)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isLineNoteFenceInfo = (info) => {
|
|
37
|
+
return infoNameStartsWith(info, 'line-notes') || infoNameStartsWith(info, 'notes')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stripContinuationIndent = (line) => {
|
|
41
|
+
if (!line) return ''
|
|
42
|
+
if (line[0] === '\t') return line.slice(1)
|
|
43
|
+
if (line.startsWith(' ')) return line.slice(2)
|
|
44
|
+
return line
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parseValidatedLineNoteWidth = (attrText) => {
|
|
48
|
+
if (!attrText || attrText.indexOf('width') === -1) return ''
|
|
49
|
+
const attrs = getInfoAttr(attrText)
|
|
50
|
+
if (!attrs.length) return ''
|
|
51
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
52
|
+
const name = attrs[i][0]
|
|
53
|
+
const value = String(attrs[i][1] || '').trim()
|
|
54
|
+
if (name !== 'width') continue
|
|
55
|
+
if (lineNoteWidthReg.test(value)) return value
|
|
56
|
+
}
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parseLineNoteHeaderText = (text) => {
|
|
61
|
+
const str = String(text ?? '')
|
|
62
|
+
if (str.indexOf('{') === -1 || str.indexOf('}') === -1) {
|
|
63
|
+
return { text: str, width: '' }
|
|
64
|
+
}
|
|
65
|
+
const match = str.match(lineNoteAttrsReg)
|
|
66
|
+
if (!match) return { text: str, width: '' }
|
|
67
|
+
return {
|
|
68
|
+
text: match[1].trimEnd(),
|
|
69
|
+
width: parseValidatedLineNoteWidth(match[2]),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parseLineNoteAttrsOnlyText = (text) => {
|
|
74
|
+
const str = String(text ?? '').trim()
|
|
75
|
+
if (str[0] !== '{' || str[str.length - 1] !== '}') return null
|
|
76
|
+
const match = str.match(lineNoteAttrsOnlyReg)
|
|
77
|
+
if (!match) return null
|
|
78
|
+
const width = parseValidatedLineNoteWidth(match[1])
|
|
79
|
+
return width ? { width } : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parseLineNotesContent = (content) => {
|
|
83
|
+
const raw = String(content ?? '')
|
|
84
|
+
if (!raw || (raw.indexOf(':') === -1 && raw.indexOf(':') === -1)) return []
|
|
85
|
+
const lines = raw.split('\n')
|
|
86
|
+
const notes = []
|
|
87
|
+
const seenStarts = new Set()
|
|
88
|
+
let current = null
|
|
89
|
+
|
|
90
|
+
const pushCurrent = () => {
|
|
91
|
+
if (!current) return true
|
|
92
|
+
if (!current.lines.length) return false
|
|
93
|
+
notes.push({
|
|
94
|
+
from: current.from,
|
|
95
|
+
to: current.to,
|
|
96
|
+
text: current.lines.join('\n'),
|
|
97
|
+
lineCount: current.lines.length,
|
|
98
|
+
width: current.width || '',
|
|
99
|
+
})
|
|
100
|
+
seenStarts.add(current.from)
|
|
101
|
+
current = null
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
const line = lines[i]
|
|
107
|
+
const header = line.match(lineNoteHeaderReg)
|
|
108
|
+
if (header) {
|
|
109
|
+
if (!pushCurrent()) return null
|
|
110
|
+
let from = Number(header[1])
|
|
111
|
+
let to = header[2] ? Number(header[2]) : from
|
|
112
|
+
if (from > to) {
|
|
113
|
+
const swap = from
|
|
114
|
+
from = to
|
|
115
|
+
to = swap
|
|
116
|
+
}
|
|
117
|
+
if (seenStarts.has(from)) return null
|
|
118
|
+
current = {
|
|
119
|
+
from,
|
|
120
|
+
to,
|
|
121
|
+
lines: [],
|
|
122
|
+
width: '',
|
|
123
|
+
}
|
|
124
|
+
const parsed = parseLineNoteHeaderText(header[3])
|
|
125
|
+
current.width = parsed.width
|
|
126
|
+
if (parsed.text) current.lines.push(parsed.text)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (current && lineNoteContinuationReg.test(line)) {
|
|
131
|
+
const stripped = stripContinuationIndent(line)
|
|
132
|
+
const attrsOnly = parseLineNoteAttrsOnlyText(stripped)
|
|
133
|
+
if (attrsOnly) {
|
|
134
|
+
if (attrsOnly.width) current.width = attrsOnly.width
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
current.lines.push(stripped)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!line.trim()) {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!pushCurrent()) return null
|
|
149
|
+
return notes
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const getTokenLineNotes = (token) => {
|
|
153
|
+
if (!token || !token.meta || typeof token.meta !== 'object') return null
|
|
154
|
+
const notes = token.meta[lineNotesMetaKey]
|
|
155
|
+
return Array.isArray(notes) && notes.length ? notes : null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const setTokenLineNotes = (token, notes) => {
|
|
159
|
+
if (!token || !notes || !notes.length) return
|
|
160
|
+
if (!token.meta || typeof token.meta !== 'object') token.meta = {}
|
|
161
|
+
token.meta[lineNotesMetaKey] = notes
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const extendTokenMapWithImmediateFollower = (token, next) => {
|
|
165
|
+
if (!token || !Array.isArray(token.map) || token.map.length !== 2) return
|
|
166
|
+
if (!next || !Array.isArray(next.map) || next.map.length !== 2) return
|
|
167
|
+
const start = token.map[0]
|
|
168
|
+
const end = next.map[1]
|
|
169
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)) return
|
|
170
|
+
if (end > token.map[1]) token.map = [start, end]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const installLineNotesCoreRule = (md) => {
|
|
174
|
+
if (!md || md[lineNotesInstalledKey]) return
|
|
175
|
+
md[lineNotesInstalledKey] = true
|
|
176
|
+
md.core.ruler.after('block', lineNotesRuleName, (state) => {
|
|
177
|
+
if (!canContainLineNotes(state)) return
|
|
178
|
+
const tokens = state && state.tokens
|
|
179
|
+
if (!Array.isArray(tokens) || tokens.length < 2) return
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
182
|
+
const token = tokens[i]
|
|
183
|
+
const next = tokens[i + 1]
|
|
184
|
+
if (!token || token.type !== 'fence' || !next || next.type !== 'fence') continue
|
|
185
|
+
if (!isLineNoteFenceInfo(next.info)) continue
|
|
186
|
+
if (isLineNoteFenceInfo(token.info)) continue
|
|
187
|
+
const notes = parseLineNotesContent(next.content)
|
|
188
|
+
if (!notes || !notes.length) continue
|
|
189
|
+
setTokenLineNotes(token, notes)
|
|
190
|
+
extendTokenMapWithImmediateFollower(token, next)
|
|
191
|
+
tokens.splice(i + 1, 1)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
getTokenLineNotes,
|
|
198
|
+
installLineNotesCoreRule,
|
|
199
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
classifyShikiScopeRole,
|
|
3
3
|
getShikiRawScopeName,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '../custom-highlight/shiki-
|
|
4
|
+
normalizeShikiRoleLangAliasMap,
|
|
5
|
+
resolveShikiRoleLangForFence,
|
|
6
|
+
} from '../custom-highlight/shiki-role.js'
|
|
7
7
|
import {
|
|
8
8
|
escapeHtmlAttr,
|
|
9
9
|
getCustomHighlightPayloadMap,
|
|
@@ -15,8 +15,12 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
validateCustomHighlightOptions,
|
|
17
17
|
} from '../custom-highlight/option-validator.js'
|
|
18
|
+
import {
|
|
19
|
+
sanitizeHighlightName,
|
|
20
|
+
} from '../custom-highlight/scope-name.js'
|
|
18
21
|
import {
|
|
19
22
|
commentLineClass,
|
|
23
|
+
getCommentLineRanges,
|
|
20
24
|
normalizeEmphasisRanges,
|
|
21
25
|
} from './render-shared.js'
|
|
22
26
|
import {
|
|
@@ -31,7 +35,6 @@ import {
|
|
|
31
35
|
} from './render-api-constants.js'
|
|
32
36
|
|
|
33
37
|
const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
|
|
34
|
-
const hyphenMultiReg = /-+/g
|
|
35
38
|
|
|
36
39
|
const defaultCustomHighlightOpt = {
|
|
37
40
|
provider: 'shiki',
|
|
@@ -44,9 +47,9 @@ const defaultCustomHighlightOpt = {
|
|
|
44
47
|
scopePrefix: 'hl',
|
|
45
48
|
includeScopeStyles: true,
|
|
46
49
|
shikiScopeMode: 'auto',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
shikiRoleClassifier: null,
|
|
51
|
+
shikiRoleLangResolver: null,
|
|
52
|
+
shikiRoleLangAliases: null,
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const normalizeCustomHighlightProvider = (provider) => {
|
|
@@ -58,7 +61,7 @@ const normalizeCustomHighlightProvider = (provider) => {
|
|
|
58
61
|
const normalizeShikiScopeMode = (mode) => {
|
|
59
62
|
const key = String(mode || '').trim().toLowerCase()
|
|
60
63
|
if (!key) return null
|
|
61
|
-
if (key === '
|
|
64
|
+
if (key === 'role') return 'role'
|
|
62
65
|
if (key === 'semantic') return 'semantic'
|
|
63
66
|
if (key === 'color') return 'color'
|
|
64
67
|
if (key === 'auto') return 'auto'
|
|
@@ -133,7 +136,7 @@ const buildShikiInternalLangAliasMap = (highlighter) => {
|
|
|
133
136
|
} catch (e) {}
|
|
134
137
|
rawMap[rawName] = canonical
|
|
135
138
|
}
|
|
136
|
-
return
|
|
139
|
+
return normalizeShikiRoleLangAliasMap(rawMap)
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
const normalizeCustomHighlightOpt = (opt = {}) => {
|
|
@@ -160,11 +163,11 @@ const normalizeCustomHighlightOpt = (opt = {}) => {
|
|
|
160
163
|
} else {
|
|
161
164
|
next.theme = normalizedTheme.singleTheme || null
|
|
162
165
|
}
|
|
163
|
-
if (typeof next.
|
|
164
|
-
if (typeof next.
|
|
165
|
-
next.
|
|
166
|
+
if (typeof next.shikiRoleClassifier !== 'function') next.shikiRoleClassifier = null
|
|
167
|
+
if (typeof next.shikiRoleLangResolver !== 'function') next.shikiRoleLangResolver = null
|
|
168
|
+
next.shikiRoleLangAliases = normalizeShikiRoleLangAliasMap(next.shikiRoleLangAliases)
|
|
166
169
|
next._shikiInternalLangAliasMap =
|
|
167
|
-
(next.provider === 'shiki' && next.shikiScopeMode === '
|
|
170
|
+
(next.provider === 'shiki' && next.shikiScopeMode === 'role')
|
|
168
171
|
? buildShikiInternalLangAliasMap(next.highlighter)
|
|
169
172
|
: null
|
|
170
173
|
const fallbackOn = Array.isArray(next.fallbackOn) ? next.fallbackOn : fallbackOnDefault
|
|
@@ -183,16 +186,6 @@ const shouldApplyApiFallbackForReason = (chOpt, reason) => {
|
|
|
183
186
|
return chOpt._fallbackOnSet.has(reason)
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
const sanitizeHighlightName = (name, prefix = '') => {
|
|
187
|
-
const prefixBase = String(prefix == null ? '' : prefix).replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
|
|
188
|
-
const raw = String(name || '')
|
|
189
|
-
let safe = raw.replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
|
|
190
|
-
if (!safe) safe = 'scope'
|
|
191
|
-
if (/^[0-9]/.test(safe)) safe = 'x-' + safe
|
|
192
|
-
if (safe.startsWith('--')) safe = safe.slice(2) || 'scope'
|
|
193
|
-
return prefixBase ? `${prefixBase}-${safe}` : safe
|
|
194
|
-
}
|
|
195
|
-
|
|
196
189
|
const uniqueHighlightName = (base, usedMap) => {
|
|
197
190
|
if (!usedMap.has(base)) {
|
|
198
191
|
usedMap.set(base, 1)
|
|
@@ -280,19 +273,20 @@ const sameScopeStyles = (a, b) => {
|
|
|
280
273
|
return true
|
|
281
274
|
}
|
|
282
275
|
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
276
|
+
const getLogicalLineOffsets = (text) => {
|
|
277
|
+
const str = String(text ?? '')
|
|
278
|
+
const offsets = []
|
|
279
|
+
if (!str) return offsets
|
|
280
|
+
let lineStart = 0
|
|
281
|
+
for (let i = 0; i < str.length; i++) {
|
|
282
|
+
const code = str.charCodeAt(i)
|
|
283
|
+
if (code !== 10 && code !== 13) continue
|
|
284
|
+
offsets.push([lineStart, i])
|
|
285
|
+
if (code === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
|
|
286
|
+
lineStart = i + 1
|
|
294
287
|
}
|
|
295
|
-
|
|
288
|
+
if (lineStart < str.length) offsets.push([lineStart, str.length])
|
|
289
|
+
return offsets
|
|
296
290
|
}
|
|
297
291
|
|
|
298
292
|
const getShikiTokenStyle = (token) => {
|
|
@@ -467,21 +461,21 @@ const hasShikiOffsets = (tokenLines) => {
|
|
|
467
461
|
return false
|
|
468
462
|
}
|
|
469
463
|
|
|
470
|
-
const getShikiScopeName = (tok, lang, style, opt,
|
|
464
|
+
const getShikiScopeName = (tok, lang, style, opt, preResolvedRoleLang = '') => {
|
|
471
465
|
const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
|
|
472
466
|
const rawScope = getShikiRawScopeName(tok)
|
|
473
|
-
let
|
|
474
|
-
const
|
|
475
|
-
if (
|
|
467
|
+
let customRoleName = null
|
|
468
|
+
const isRoleScopeMode = scopeMode === 'role'
|
|
469
|
+
if (isRoleScopeMode && opt && typeof opt.shikiRoleClassifier === 'function') {
|
|
476
470
|
try {
|
|
477
|
-
const customName = opt.
|
|
478
|
-
if (customName != null && customName !== '')
|
|
471
|
+
const customName = opt.shikiRoleClassifier(rawScope, tok, { lang, style, scopeMode })
|
|
472
|
+
if (customName != null && customName !== '') customRoleName = String(customName)
|
|
479
473
|
} catch (e) {}
|
|
480
474
|
}
|
|
481
|
-
if (
|
|
482
|
-
if (
|
|
483
|
-
const bucket =
|
|
484
|
-
return 'shiki-' + bucket
|
|
475
|
+
if (isRoleScopeMode) {
|
|
476
|
+
if (customRoleName) return 'shiki-role-' + customRoleName
|
|
477
|
+
const bucket = classifyShikiScopeRole(rawScope, tok, lang, opt, preResolvedRoleLang)
|
|
478
|
+
return 'shiki-role-' + bucket
|
|
485
479
|
}
|
|
486
480
|
if (scopeMode === 'semantic') {
|
|
487
481
|
if (rawScope) return 'shiki-' + rawScope
|
|
@@ -523,11 +517,11 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
523
517
|
scopeMode === 'auto' ||
|
|
524
518
|
scopeMode === 'color' ||
|
|
525
519
|
scopeMode === 'semantic' ||
|
|
526
|
-
(scopeMode === '
|
|
520
|
+
(scopeMode === 'role' && !!(opt && typeof opt.shikiRoleClassifier === 'function'))
|
|
527
521
|
const needTokenStyle = includeScopeStyles || needStyleForScopeName
|
|
528
|
-
let
|
|
529
|
-
if (scopeMode === '
|
|
530
|
-
|
|
522
|
+
let preResolvedRoleLang = ''
|
|
523
|
+
if (scopeMode === 'role' && (!opt || typeof opt.shikiRoleLangResolver !== 'function')) {
|
|
524
|
+
preResolvedRoleLang = resolveShikiRoleLangForFence(lang, opt)
|
|
531
525
|
}
|
|
532
526
|
let cursor = 0
|
|
533
527
|
for (let i = 0; i < tokenLines.length; i++) {
|
|
@@ -542,7 +536,7 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
|
|
|
542
536
|
const start = hasOffset ? tok.offset : cursor
|
|
543
537
|
const end = start + content.length
|
|
544
538
|
const style = needTokenStyle ? getShikiTokenStyle(tok) : null
|
|
545
|
-
const scope = getShikiScopeName(tok, lang, style, opt,
|
|
539
|
+
const scope = getShikiScopeName(tok, lang, style, opt, preResolvedRoleLang)
|
|
546
540
|
entries.push({ scope, start, end, style })
|
|
547
541
|
cursor = end
|
|
548
542
|
}
|
|
@@ -559,6 +553,7 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
559
553
|
const entries = []
|
|
560
554
|
for (const range of source.ranges) {
|
|
561
555
|
let scope
|
|
556
|
+
let scopeIndex = null
|
|
562
557
|
let start
|
|
563
558
|
let end
|
|
564
559
|
let style
|
|
@@ -575,10 +570,11 @@ const normalizeCustomProviderRanges = (result) => {
|
|
|
575
570
|
} else {
|
|
576
571
|
continue
|
|
577
572
|
}
|
|
578
|
-
if (
|
|
573
|
+
if (Number.isSafeInteger(scope)) scopeIndex = scope
|
|
574
|
+
if (scopeIndex != null && scopes && scopes[scopeIndex] != null) scope = scopes[scopeIndex]
|
|
579
575
|
if (scope == null) continue
|
|
580
576
|
if (!style && scopeStyles) {
|
|
581
|
-
if (
|
|
577
|
+
if (scopeIndex != null && Array.isArray(scopeStyles)) style = scopeStyles[scopeIndex]
|
|
582
578
|
else style = scopeStyles[String(scope)]
|
|
583
579
|
}
|
|
584
580
|
entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
|
|
@@ -617,7 +613,12 @@ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeS
|
|
|
617
613
|
}
|
|
618
614
|
scopeKeyToIndex.set(key, scopeIndex)
|
|
619
615
|
}
|
|
620
|
-
ranges.
|
|
616
|
+
const previousRange = ranges.length ? ranges[ranges.length - 1] : null
|
|
617
|
+
if (previousRange && previousRange[0] === scopeIndex && previousRange[2] === start) {
|
|
618
|
+
previousRange[2] = end
|
|
619
|
+
} else {
|
|
620
|
+
ranges.push([scopeIndex, start, end])
|
|
621
|
+
}
|
|
621
622
|
}
|
|
622
623
|
const payload = {
|
|
623
624
|
v: customHighlightPayloadSchemaVersion,
|
|
@@ -637,8 +638,8 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
637
638
|
const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
|
|
638
639
|
const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
|
|
639
640
|
if (!needsEmphasis && !needsCommentScan) return
|
|
640
|
-
const
|
|
641
|
-
const maxLine =
|
|
641
|
+
const offsets = needsEmphasis ? getLogicalLineOffsets(content) : null
|
|
642
|
+
const maxLine = offsets ? offsets.length : 0
|
|
642
643
|
if (needsEmphasis) {
|
|
643
644
|
const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
|
|
644
645
|
for (const [s, e] of normalized) {
|
|
@@ -648,11 +649,11 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
|
|
|
648
649
|
}
|
|
649
650
|
}
|
|
650
651
|
if (needsCommentScan) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
652
|
+
const ranges = getCommentLineRanges(content, commentMarkValue)
|
|
653
|
+
if (!ranges) return
|
|
654
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
655
|
+
const [start, end] = ranges[i]
|
|
656
|
+
entries.push({ scope: commentLineClass, start, end })
|
|
656
657
|
}
|
|
657
658
|
}
|
|
658
659
|
}
|
|
@@ -805,6 +806,5 @@ export {
|
|
|
805
806
|
normalizeCustomHighlightOpt,
|
|
806
807
|
renderCustomHighlightPayloadScript,
|
|
807
808
|
renderCustomHighlightScopeStyleTag,
|
|
808
|
-
sanitizeHighlightName,
|
|
809
809
|
shouldApplyApiFallbackForReason,
|
|
810
810
|
}
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
orderTokenAttrs,
|
|
12
12
|
preWrapStyle,
|
|
13
13
|
resolveAdvancedLineNumberPlan,
|
|
14
|
+
resolveLineNotesPlan,
|
|
14
15
|
splitFenceBlockToLines,
|
|
16
|
+
wrapFencePreWithLineNotes,
|
|
15
17
|
} from './render-shared.js'
|
|
16
18
|
import {
|
|
17
19
|
createApiPayloadForFence,
|
|
@@ -26,6 +28,8 @@ import {
|
|
|
26
28
|
renderFenceMarkup,
|
|
27
29
|
} from './render-markup.js'
|
|
28
30
|
|
|
31
|
+
const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes'])
|
|
32
|
+
|
|
29
33
|
let fallbackCustomHighlightSeq = 0
|
|
30
34
|
|
|
31
35
|
const nextCustomHighlightId = (env, prefix) => {
|
|
@@ -45,23 +49,29 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
|
|
|
45
49
|
return preAttrs
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, includePayload, timings) => {
|
|
49
|
-
const isSamp = opt._sampReg.test(lang)
|
|
52
|
+
const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
|
|
50
53
|
const tag = isSamp ? 'samp' : 'code'
|
|
51
54
|
let content = md.utils.escapeHtml(token.content)
|
|
52
55
|
const lineStrategy = opt.customHighlight.lineFeatureStrategy
|
|
53
56
|
const needLineNumber = lineStrategy === 'hybrid' && opt.setLineNumber && startNumber >= 0
|
|
54
57
|
const needEndSpan = lineStrategy === 'hybrid' && opt.lineEndSpanThreshold > 0
|
|
58
|
+
const needAdvancedLineNumberPlan = needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)
|
|
59
|
+
const needLineNotes = lineStrategy !== 'disable' && !!(lineNotes && lineNotes.length)
|
|
60
|
+
const logicalLineCount = (needAdvancedLineNumberPlan || needLineNotes)
|
|
61
|
+
? getLogicalLineCount(token.content)
|
|
62
|
+
: -1
|
|
55
63
|
let lineNumberPlan = null
|
|
56
|
-
if (
|
|
57
|
-
const logicalLineCount = getLogicalLineCount(token.content)
|
|
64
|
+
if (needAdvancedLineNumberPlan) {
|
|
58
65
|
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, logicalLineCount, logicalLineCount)
|
|
59
66
|
}
|
|
60
|
-
|
|
67
|
+
const lineNotePlan = needLineNotes
|
|
68
|
+
? resolveLineNotesPlan(lineNotes, logicalLineCount, logicalLineCount, lineNoteIdPrefix)
|
|
69
|
+
: null
|
|
70
|
+
if (needLineNumber || needEndSpan || lineNotePlan) {
|
|
61
71
|
const splitStartedAt = timings ? getNowMs() : 0
|
|
62
72
|
const nlIndex = content.indexOf('\n')
|
|
63
73
|
const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
|
|
64
|
-
content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass, lineNumberPlan)
|
|
74
|
+
content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass, lineNumberPlan, lineNotePlan)
|
|
65
75
|
if (timings) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -88,7 +98,8 @@ const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emph
|
|
|
88
98
|
|
|
89
99
|
if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
|
|
90
100
|
const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
|
|
91
|
-
const
|
|
101
|
+
const preHtml = `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>`
|
|
102
|
+
const html = wrapFencePreWithLineNotes(preHtml, lineNotePlan)
|
|
92
103
|
return inlinePayloadScript ? html + inlinePayloadScript : html
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -105,6 +116,9 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
105
116
|
const wrapEnabled = context.wrapEnabled
|
|
106
117
|
const preWrapValue = context.preWrapValue
|
|
107
118
|
const commentMarkValue = context.commentMarkValue
|
|
119
|
+
const lineNotes = context.lineNotes
|
|
120
|
+
const lineNoteIdPrefix = context.lineNoteIdPrefix
|
|
121
|
+
const isSamp = context.isSamp
|
|
108
122
|
|
|
109
123
|
const apiDecisionBase = {
|
|
110
124
|
renderer: 'api',
|
|
@@ -112,16 +126,17 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
112
126
|
fallbackUsed: false,
|
|
113
127
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
114
128
|
disabledFeatures: opt.customHighlight.lineFeatureStrategy === 'disable'
|
|
115
|
-
?
|
|
129
|
+
? apiDisableLineFeatureList
|
|
116
130
|
: [],
|
|
117
131
|
}
|
|
118
132
|
try {
|
|
119
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, true, timings)
|
|
133
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
|
|
120
134
|
if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
121
135
|
emitFenceDecision(opt, apiDecisionBase)
|
|
122
136
|
return html
|
|
123
137
|
} catch (err) {
|
|
124
|
-
if (
|
|
138
|
+
if (!shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) throw err
|
|
139
|
+
if (opt.customHighlight.fallback === 'plain') {
|
|
125
140
|
const fallbackDecision = {
|
|
126
141
|
renderer: 'api',
|
|
127
142
|
includePayload: false,
|
|
@@ -130,7 +145,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
130
145
|
reason: 'provider-error',
|
|
131
146
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
132
147
|
}
|
|
133
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, false, timings)
|
|
148
|
+
const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
|
|
134
149
|
if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
135
150
|
emitFenceDecision(opt, fallbackDecision)
|
|
136
151
|
return html
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
getPayloadMapFromScript,
|
|
5
5
|
styleToHighlightCss,
|
|
6
6
|
} from '../custom-highlight/runtime-utils.js'
|
|
7
|
+
import {
|
|
8
|
+
sanitizeHighlightName,
|
|
9
|
+
} from '../custom-highlight/scope-name.js'
|
|
7
10
|
import {
|
|
8
11
|
customHighlightAppliedAttr,
|
|
9
12
|
customHighlightAppliedSelector,
|
|
@@ -19,17 +22,6 @@ const globalRuntimeInsertedScopeStyles = new Map()
|
|
|
19
22
|
const runtimeInsertedScopeStylesByDoc = new WeakMap()
|
|
20
23
|
const runtimeApplyStateByRoot = new WeakMap()
|
|
21
24
|
const validColorSchemeSet = new Set(['light', 'dark', 'auto'])
|
|
22
|
-
const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
|
|
23
|
-
const hyphenMultiReg = /-+/g
|
|
24
|
-
|
|
25
|
-
const sanitizeHighlightName = (name) => {
|
|
26
|
-
const raw = String(name || '')
|
|
27
|
-
let safe = raw.replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
|
|
28
|
-
if (!safe) safe = 'scope'
|
|
29
|
-
if (/^[0-9]/.test(safe)) safe = 'x-' + safe
|
|
30
|
-
if (safe.startsWith('--')) safe = safe.slice(2) || 'scope'
|
|
31
|
-
return safe
|
|
32
|
-
}
|
|
33
25
|
|
|
34
26
|
const getCustomHighlightBlockId = (node) => {
|
|
35
27
|
if (!node || typeof node.getAttribute !== 'function') return null
|
|
@@ -133,10 +125,10 @@ const addMediaQueryChangeListener = (queryList, listener) => {
|
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
const getRuntimeSupportedPayloadVersions = (options = {}) => {
|
|
136
|
-
const set = new Set()
|
|
137
128
|
if (options.strictVersion === true) {
|
|
138
|
-
|
|
129
|
+
return new Set(customHighlightPayloadSupportedVersions)
|
|
139
130
|
}
|
|
131
|
+
const set = new Set()
|
|
140
132
|
if (Number.isSafeInteger(options.supportedVersion)) set.add(options.supportedVersion)
|
|
141
133
|
if (Array.isArray(options.supportedVersions)) {
|
|
142
134
|
for (const v of options.supportedVersions) {
|
package/src/fence/render-api.js
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
finalizeCommonFenceOption,
|
|
8
8
|
prepareFenceRenderContext,
|
|
9
9
|
} from './render-shared.js'
|
|
10
|
+
import {
|
|
11
|
+
installLineNotesCoreRule,
|
|
12
|
+
} from './line-notes.js'
|
|
10
13
|
import {
|
|
11
14
|
customHighlightDataEnvKey,
|
|
12
15
|
customHighlightEnvInitRuleName,
|
|
@@ -37,6 +40,9 @@ const shouldRuntimeFallback = (reason, opt = {}) => {
|
|
|
37
40
|
return shouldApplyApiFallbackForReason(chOpt, reason)
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
const customHighlightEnvInitInstalledKey = '__rendererFenceCustomHighlightEnvInitInstalled'
|
|
44
|
+
const customHighlightEnvInitOptionKey = '__rendererFenceCustomHighlightEnvInitOption'
|
|
45
|
+
|
|
40
46
|
const mditRendererFenceCustomHighlight = (md, option) => {
|
|
41
47
|
const opt = {
|
|
42
48
|
...createCommonFenceOptionDefaults(md),
|
|
@@ -66,25 +72,31 @@ const mditRendererFenceCustomHighlight = (md, option) => {
|
|
|
66
72
|
opt.customHighlight._hasShikiHighlighter = !!(opt.customHighlight.highlighter && typeof opt.customHighlight.highlighter.codeToTokens === 'function')
|
|
67
73
|
const tokenOptionBase = {}
|
|
68
74
|
if (opt.customHighlight._singleTheme) tokenOptionBase.theme = opt.customHighlight._singleTheme
|
|
69
|
-
if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === '
|
|
75
|
+
if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === 'role') {
|
|
70
76
|
tokenOptionBase.includeExplanation = 'scopeName'
|
|
71
77
|
}
|
|
72
78
|
opt.customHighlight._shikiTokenOptionBase = Object.keys(tokenOptionBase).length ? tokenOptionBase : null
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
finalizeCommonFenceOption(opt)
|
|
82
|
+
installLineNotesCoreRule(md)
|
|
76
83
|
|
|
77
|
-
md
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
env
|
|
82
|
-
env
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
md[customHighlightEnvInitOptionKey] = opt.customHighlight
|
|
85
|
+
if (!md[customHighlightEnvInitInstalledKey]) {
|
|
86
|
+
md[customHighlightEnvInitInstalledKey] = true
|
|
87
|
+
md.core.ruler.before('block', customHighlightEnvInitRuleName, (state) => {
|
|
88
|
+
const env = state && state.env
|
|
89
|
+
if (!env || typeof env !== 'object') return
|
|
90
|
+
const chOpt = md[customHighlightEnvInitOptionKey]
|
|
91
|
+
if (chOpt && chOpt.transport === 'env') {
|
|
92
|
+
env[customHighlightDataEnvKey] = {}
|
|
93
|
+
env[customHighlightSeqEnvKey] = 0
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
delete env[customHighlightDataEnvKey]
|
|
97
|
+
delete env[customHighlightSeqEnvKey]
|
|
98
|
+
})
|
|
99
|
+
}
|
|
88
100
|
|
|
89
101
|
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
|
90
102
|
const context = prepareFenceRenderContext(tokens, idx, opt)
|