@peaceroad/markdown-it-renderer-fence 0.7.0 → 0.9.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.
Files changed (36) hide show
  1. package/README.md +179 -470
  2. package/docs/README.md +37 -0
  3. package/docs/code-highlighting-design.md +321 -0
  4. package/docs/custom-highlight-styling-guide.md +210 -0
  5. package/package.json +25 -13
  6. package/src/custom-highlight/option-validator.js +5 -5
  7. package/src/custom-highlight/payload-utils.js +4 -9
  8. package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +249 -54
  9. package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +107 -99
  10. package/src/custom-highlight/shiki-theme-role.js +176 -0
  11. package/src/fence/line-notes.js +27 -9
  12. package/src/fence/render-api-constants.js +1 -1
  13. package/src/fence/render-api-provider.js +61 -53
  14. package/src/fence/render-api-renderer.js +26 -10
  15. package/src/fence/render-api-runtime.js +29 -10
  16. package/src/fence/render-api.js +20 -12
  17. package/src/fence/render-markup.js +34 -24
  18. package/src/fence/render-shared.js +150 -30
  19. package/src/utils/attr-utils.js +10 -0
  20. package/theme/_shared/rf-core.css +170 -0
  21. package/theme/line-notes.css +94 -0
  22. package/theme/line-number.css +64 -0
  23. package/theme/rf-basic/README.md +117 -0
  24. package/theme/rf-basic/api/highlightjs.css +71 -0
  25. package/theme/rf-basic/api/shiki.css +67 -0
  26. package/theme/rf-basic/base.css +5 -0
  27. package/theme/rf-basic/index.css +7 -0
  28. package/theme/rf-basic/index.js +164 -0
  29. package/theme/rf-basic/markup/highlightjs.css +77 -0
  30. package/theme/rf-basic/markup/shiki.css +11 -0
  31. package/theme/rf-monochrome/README.md +84 -0
  32. package/theme/rf-monochrome/base.css +5 -0
  33. package/theme/rf-monochrome/index.css +5 -0
  34. package/theme/rf-monochrome/index.js +71 -0
  35. package/theme/rf-monochrome/markup/highlightjs.css +150 -0
  36. package/theme/rf-monochrome/markup/shiki.css +38 -0
@@ -1,9 +1,10 @@
1
1
  import {
2
- applyKeywordV4Rules,
3
- applyShikiKeywordLegacyPostRules,
4
- } from './shiki-keyword-rules.js'
2
+ applyRoleV4Rules,
3
+ applyShikiRoleLegacyPostRules,
4
+ } from './shiki-role-rules.js'
5
+ import { resolveRendererFenceShikiThemeRoleBucket } from './shiki-theme-role.js'
5
6
 
6
- const shikiKeywordBucketScoreV3 = {
7
+ const shikiRoleBucketScore = {
7
8
  comment: 210,
8
9
  'meta-shebang': 206,
9
10
  'tag-delimiter': 202,
@@ -37,7 +38,7 @@ const shikiGlobalLiteralTokenSet = new Set(['true', 'false', 'null', 'undefined'
37
38
  const shikiShellKeywordTokenSet = new Set(['case', 'coproc', 'do', 'done', 'elif', 'else', 'esac', 'export', 'fi', 'for', 'function', 'if', 'in', 'local', 'readonly', 'select', 'then', 'time', 'typeset', 'until', 'while'])
38
39
  const shikiHclKeywordTokenSet = new Set(['terraform', 'resource', 'data', 'module', 'provider', 'variable', 'output', 'locals', 'backend', 'dynamic', 'for_each', 'count', 'provisioner', 'connection', 'if', 'for', 'in'])
39
40
 
40
- const shikiKeywordTokensByLang = {
41
+ const shikiLangKeywordTokens = {
41
42
  javascript: new Set(['await', 'async', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'from', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'of', 'return', 'static', 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']),
42
43
  typescript: new Set(['abstract', 'as', 'asserts', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'declare', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'finally', 'for', 'from', 'function', 'if', 'implements', 'import', 'in', 'infer', 'instanceof', 'interface', 'is', 'keyof', 'let', 'namespace', 'new', 'of', 'override', 'private', 'protected', 'public', 'readonly', 'return', 'satisfies', 'static', 'super', 'switch', 'this', 'throw', 'try', 'type', 'typeof', 'var', 'void', 'while', 'with', 'yield']),
43
44
  python: new Set(['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']),
@@ -61,26 +62,26 @@ const shikiNumberTokenReg = /^(?:0[xob][0-9a-f_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-
61
62
  const shikiPunctuationTokenReg = /^[()[\]{}<>.,;:!?~`'"@#$%^&*+=|/\\-]+$/
62
63
  const shikiSimpleIdentifierReg = /^[A-Za-z_$][\w$]*$/
63
64
  const shikiScopeLangReg = /(?:^|\.)(?:source|text)\.([A-Za-z0-9_#+-]+)/g
64
- const shikiKeywordLangSanitizeReg = /[^a-z0-9#+-]+/g
65
+ const shikiRoleLangSanitizeReg = /[^a-z0-9#+-]+/g
65
66
  const hyphenMultiReg = /-+/g
66
- const shikiScopeKeywordSingleV3Cache = new Map()
67
+ const shikiScopeRoleCache = new Map()
67
68
 
68
- const normalizeShikiKeywordLangKey = (lang) => {
69
+ const normalizeShikiRoleLangKey = (lang) => {
69
70
  let key = String(lang || '').trim().toLowerCase()
70
71
  if (!key) return ''
71
72
  if (key.startsWith('language-')) key = key.slice('language-'.length)
72
73
  key = key.replace(/\s+/g, '-').replace(/[./]+/g, '-').replace(/_+/g, '-')
73
- key = key.replace(shikiKeywordLangSanitizeReg, '')
74
+ key = key.replace(shikiRoleLangSanitizeReg, '')
74
75
  key = key.replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
75
76
  return key
76
77
  }
77
78
 
78
- const normalizeShikiKeywordLangAliasMap = (input) => {
79
+ const normalizeShikiRoleLangAliasMap = (input) => {
79
80
  if (!input || typeof input !== 'object') return null
80
81
  const map = {}
81
82
  for (const key of Object.keys(input)) {
82
- const from = normalizeShikiKeywordLangKey(key)
83
- const to = normalizeShikiKeywordLangKey(input[key])
83
+ const from = normalizeShikiRoleLangKey(key)
84
+ const to = normalizeShikiRoleLangKey(input[key])
84
85
  if (!from || !to) continue
85
86
  map[from] = to
86
87
  }
@@ -106,7 +107,7 @@ const getShikiRawScopeName = (tok) => {
106
107
  return null
107
108
  }
108
109
 
109
- const extractShikiKeywordLangCandidatesFromScope = (scope) => {
110
+ const extractShikiRoleLangCandidatesFromScope = (scope) => {
110
111
  const out = []
111
112
  const text = String(scope || '')
112
113
  if (!text) return out
@@ -177,58 +178,57 @@ const createScopePatternMatcher = (lowerScopeCandidates) => {
177
178
  }
178
179
  }
179
180
 
180
- const classifyShikiScopeKeywordSingleV3 = (scope) => {
181
+ const classifyShikiScopeRoleSingle = (scope) => {
181
182
  const s = String(scope || '').toLowerCase()
182
183
  if (!s) return 'text'
183
- const cached = shikiScopeKeywordSingleV3Cache.get(s)
184
+ const cached = shikiScopeRoleCache.get(s)
184
185
  if (cached) return cached
185
186
  let bucket = 'text'
186
- if (s.includes('comment')) return 'comment'
187
- if (s.includes('meta.shebang')) return 'meta-shebang'
188
- if (s.includes('punctuation.definition.tag.begin') || s.includes('punctuation.definition.tag.end') || s.includes('punctuation.separator.key-value.html')) return 'tag-delimiter'
189
- if (s.includes('entity.other.attribute-name') || s.includes('attribute-name')) return 'attribute'
190
- if (s.includes('entity.name.tag')) return 'tag'
191
- if (s.includes('string.unquoted')) return 'string-unquoted'
192
- if (s.includes('regexp') || s.includes('regex') || s.includes('string') || s.includes('punctuation.definition.string')) return 'string'
193
- if (s.includes('constant.other.color') || s.includes('support.constant.property-value')) return 'number'
194
- if (s.includes('constant.numeric') || s.includes('number')) return 'number'
195
- if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none')) return 'literal'
196
- if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace')) return 'namespace'
197
- if (s.includes('storage.type.function.arrow') || s.includes('keyword.operator')) return 'keyword'
198
- if (s.includes('variable.language.this')) return 'variable-this'
199
- if (s.includes('constant.other.php') || s.includes('variable.other.constant') || s.includes('variable.other.enummember')) return 'variable-const'
200
- if (s.includes('variable.object.property') || s.includes('variable.other.property') || s.includes('variable.other.member') || s.includes('variable.object.c') || s.includes('variable.ruby')) return 'variable-member'
201
- if (s.includes('variable.parameter') || s.includes('entity.name.variable.parameter')) return 'variable-parameter'
202
- if (s.includes('entity.name.variable.local') || s.includes('variable.other.readwrite') || s.includes('variable.other.normal') || s.includes('variable.other.php') || (s.includes('variable.other.') && !s.includes('variable.other.property') && !s.includes('variable.other.member'))) return 'variable-plain'
203
- if (s.includes('support.function.builtin') || s.includes('support.function.kernel') || s.includes('support.function.construct')) return 'title-function-builtin'
204
- if (s.includes('entity.name.function.macro')) return 'title-function'
205
- if (s.includes('meta.function') && !s.includes('entity.name.function') && !s.includes('support.function')) return 'meta'
206
- if (s.includes('entity.name.function') || s.includes('support.function')) return 'title-function'
207
- if (s.includes('entity.name.class') || s.includes('entity.name.type.class')) return 'title-class'
208
- if (s.includes('storage.type.built-in.primitive')) return 'type-primitive'
209
- if (s.includes('entity.name.type.namespace') || s.includes('entity.name.type') || s.includes('support.class')) return 'type-name'
210
- if (s.includes('support.type') || s.includes('storage.modifier') || s.includes('keyword.type')) return 'type'
211
- if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier')) return 'keyword'
212
- if (s.includes('variable')) return 'variable'
213
- if (s.includes('punctuation')) return 'punctuation'
214
- if (s.includes('source.')) bucket = 'text'
187
+ if (s.includes('comment')) bucket = 'comment'
188
+ else if (s.includes('meta.shebang')) bucket = 'meta-shebang'
189
+ else if (s.includes('punctuation.definition.tag.begin') || s.includes('punctuation.definition.tag.end') || s.includes('punctuation.separator.key-value.html')) bucket = 'tag-delimiter'
190
+ else if (s.includes('entity.other.attribute-name') || s.includes('attribute-name')) bucket = 'attribute'
191
+ else if (s.includes('entity.name.tag')) bucket = 'tag'
192
+ else if (s.includes('string.unquoted')) bucket = 'string-unquoted'
193
+ else if (s.includes('regexp') || s.includes('regex') || s.includes('string') || s.includes('punctuation.definition.string')) bucket = 'string'
194
+ else if (s.includes('constant.other.color')) bucket = 'number'
195
+ else if (s.includes('constant.numeric') || s.includes('number')) bucket = 'number'
196
+ else if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none')) bucket = 'literal'
197
+ else if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace')) bucket = 'namespace'
198
+ else if (s.includes('storage.type.function.arrow') || s.includes('keyword.operator')) bucket = 'keyword'
199
+ else if (s.includes('variable.language.this')) bucket = 'variable-this'
200
+ else if (s.includes('constant.other.php') || s.includes('variable.other.constant') || s.includes('variable.other.enummember')) bucket = 'variable-const'
201
+ else if (s.includes('variable.object.property') || s.includes('variable.other.property') || s.includes('variable.other.member') || s.includes('variable.object.c') || s.includes('variable.ruby')) bucket = 'variable-member'
202
+ else if (s.includes('variable.parameter') || s.includes('entity.name.variable.parameter')) bucket = 'variable-parameter'
203
+ else if (s.includes('entity.name.variable.local') || s.includes('variable.other.readwrite') || s.includes('variable.other.normal') || s.includes('variable.other.php') || (s.includes('variable.other.') && !s.includes('variable.other.property') && !s.includes('variable.other.member'))) bucket = 'variable-plain'
204
+ else if (s.includes('support.function.builtin') || s.includes('support.function.kernel') || s.includes('support.function.construct')) bucket = 'title-function-builtin'
205
+ else if (s.includes('entity.name.function.macro')) bucket = 'title-function'
206
+ else if (s.includes('meta.function') && !s.includes('entity.name.function') && !s.includes('support.function')) bucket = 'meta'
207
+ else if (s.includes('entity.name.function') || s.includes('support.function')) bucket = 'title-function'
208
+ else if (s.includes('entity.name.class') || s.includes('entity.name.type.class')) bucket = 'title-class'
209
+ else if (s.includes('storage.type.built-in.primitive')) bucket = 'type-primitive'
210
+ else if (s.includes('entity.name.type.namespace') || s.includes('entity.name.type') || s.includes('support.class')) bucket = 'type-name'
211
+ else if (s.includes('support.type')) bucket = 'type'
212
+ else if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier')) bucket = 'keyword'
213
+ else if (s.includes('variable')) bucket = 'variable'
214
+ else if (s.includes('punctuation')) bucket = 'punctuation'
215
215
  else if (s.includes('meta')) bucket = 'meta'
216
- shikiScopeKeywordSingleV3Cache.set(s, bucket)
217
- if (shikiScopeKeywordSingleV3Cache.size > 8192) shikiScopeKeywordSingleV3Cache.clear()
216
+ if (shikiScopeRoleCache.size >= 8192) shikiScopeRoleCache.clear()
217
+ shikiScopeRoleCache.set(s, bucket)
218
218
  return bucket
219
219
  }
220
220
 
221
- const hasJsonYamlPropertyNameScope = (scopeCandidates) => {
222
- if (!Array.isArray(scopeCandidates)) return false
223
- for (let i = 0; i < scopeCandidates.length; i++) {
224
- const s = String(scopeCandidates[i] || '').toLowerCase()
221
+ const hasJsonYamlPropertyNameScope = (lowerScopeCandidates) => {
222
+ if (!Array.isArray(lowerScopeCandidates)) return false
223
+ for (let i = 0; i < lowerScopeCandidates.length; i++) {
224
+ const s = lowerScopeCandidates[i]
225
225
  if (!s.includes('property-name') && !s.includes('dictionary-key')) continue
226
226
  if (s.includes('.json') || s.includes('-json') || s.includes('.yaml') || s.includes('-yaml')) return true
227
227
  }
228
228
  return false
229
229
  }
230
230
 
231
- const classifyShikiHclKeywordBucketV3 = (lowerScopeCandidates, hasAnyPattern) => {
231
+ const classifyShikiHclRoleBucket = (lowerScopeCandidates, hasAnyPattern) => {
232
232
  const hasAny = (typeof hasAnyPattern === 'function') ? hasAnyPattern : (patterns) => hasAnyScopePattern(lowerScopeCandidates, patterns)
233
233
  if (!Array.isArray(lowerScopeCandidates) || lowerScopeCandidates.length === 0) return null
234
234
  if (hasAny(['entity.name.type.hcl', 'entity-name-type-hcl'])) return 'title-class'
@@ -243,10 +243,10 @@ const classifyShikiHclKeywordBucketV3 = (lowerScopeCandidates, hasAnyPattern) =>
243
243
  return null
244
244
  }
245
245
 
246
- const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
246
+ const resolveShikiRoleLang = (lang, scopeCandidates, tok, opt) => {
247
247
  const queue = []
248
248
  const seen = new Set()
249
- const customAliasMap = opt && opt.shikiKeywordLangAliases
249
+ const customAliasMap = opt && opt.shikiRoleLangAliases
250
250
  const shikiInternalAliasMap = opt && opt._shikiInternalLangAliasMap
251
251
  const resolveAlias = (key) => {
252
252
  if (customAliasMap && customAliasMap[key]) return customAliasMap[key]
@@ -254,46 +254,46 @@ const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
254
254
  return ''
255
255
  }
256
256
  const pushCandidate = (value) => {
257
- const key = normalizeShikiKeywordLangKey(value)
257
+ const key = normalizeShikiRoleLangKey(value)
258
258
  if (!key || seen.has(key)) return
259
259
  seen.add(key)
260
260
  queue.push(key)
261
261
  }
262
- if (opt && typeof opt.shikiKeywordLangResolver === 'function') {
262
+ if (opt && typeof opt.shikiRoleLangResolver === 'function') {
263
263
  try {
264
- const resolved = opt.shikiKeywordLangResolver(lang, scopeCandidates, tok)
264
+ const resolved = opt.shikiRoleLangResolver(lang, scopeCandidates, tok)
265
265
  if (resolved != null && resolved !== '') pushCandidate(resolved)
266
266
  } catch (e) {}
267
267
  }
268
268
  pushCandidate(lang)
269
269
  if (Array.isArray(scopeCandidates)) {
270
270
  for (let i = 0; i < scopeCandidates.length; i++) {
271
- const candidates = extractShikiKeywordLangCandidatesFromScope(scopeCandidates[i])
271
+ const candidates = extractShikiRoleLangCandidatesFromScope(scopeCandidates[i])
272
272
  for (let j = 0; j < candidates.length; j++) pushCandidate(candidates[j])
273
273
  }
274
274
  }
275
275
  for (let i = 0; i < queue.length; i++) {
276
276
  const key = queue[i]
277
- if (shikiKeywordTokensByLang[key]) return key
277
+ if (shikiLangKeywordTokens[key]) return key
278
278
  const aliased = resolveAlias(key)
279
279
  if (aliased) {
280
- if (shikiKeywordTokensByLang[aliased]) return aliased
280
+ if (shikiLangKeywordTokens[aliased]) return aliased
281
281
  pushCandidate(aliased)
282
282
  }
283
283
  const compact = key.replace(/-/g, '')
284
- if (shikiKeywordTokensByLang[compact]) return compact
284
+ if (shikiLangKeywordTokens[compact]) return compact
285
285
  const compactAliased = resolveAlias(compact)
286
286
  if (compactAliased) {
287
- if (shikiKeywordTokensByLang[compactAliased]) return compactAliased
287
+ if (shikiLangKeywordTokens[compactAliased]) return compactAliased
288
288
  pushCandidate(compactAliased)
289
289
  }
290
290
  const parts = key.split('-')
291
291
  for (let n = parts.length - 1; n >= 1; n--) {
292
292
  const head = parts.slice(0, n).join('-')
293
- if (shikiKeywordTokensByLang[head]) return head
293
+ if (shikiLangKeywordTokens[head]) return head
294
294
  const headAliased = resolveAlias(head)
295
295
  if (headAliased) {
296
- if (shikiKeywordTokensByLang[headAliased]) return headAliased
296
+ if (shikiLangKeywordTokens[headAliased]) return headAliased
297
297
  pushCandidate(headAliased)
298
298
  }
299
299
  }
@@ -301,30 +301,37 @@ const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
301
301
  return ''
302
302
  }
303
303
 
304
- const classifyShikiKeywordByToken = (content, langKey) => {
304
+ const classifyShikiRoleByToken = (content, langKey, trimmedValue, lowerValue, punctuationValue) => {
305
305
  const text = String(content || '')
306
- const trimmed = text.trim()
306
+ const trimmed = typeof trimmedValue === 'string' ? trimmedValue : text.trim()
307
307
  if (!trimmed) return 'text'
308
- const lower = trimmed.toLowerCase().replace(/\s+/g, ' ')
308
+ const rawLower = typeof lowerValue === 'string' ? lowerValue : trimmed.toLowerCase()
309
+ const lower = rawLower.replace(/\s+/g, ' ')
309
310
  if (lower.startsWith('#!')) return 'meta'
310
311
  if (langKey !== 'sql' && shikiGlobalLiteralTokenSet.has(lower)) return 'literal'
311
312
  if (shikiNumberTokenReg.test(lower)) return 'number'
312
- const set = shikiKeywordTokensByLang[langKey]
313
+ const set = shikiLangKeywordTokens[langKey]
313
314
  if (set && set.has(lower)) return 'keyword'
314
315
  if (langKey === 'css' && lower.startsWith('@')) return 'keyword'
315
- if (shikiPunctuationTokenReg.test(trimmed)) return 'punctuation'
316
+ const isPunctuation = typeof punctuationValue === 'boolean'
317
+ ? punctuationValue
318
+ : shikiPunctuationTokenReg.test(trimmed)
319
+ if (isPunctuation) return 'punctuation'
316
320
  return null
317
321
  }
318
322
 
319
- const buildShikiKeywordContext = (rawScope, tok, lang, opt, preResolvedLangKey = '', tokenContentArg, tokenTrimArg, tokenLowerArg) => {
323
+ const buildShikiRoleContext = (rawScope, tok, lang, opt, preResolvedLangKey = '', tokenContentArg, tokenTrimArg, tokenLowerArg) => {
320
324
  const token = (tok && typeof tok === 'object') ? tok : {}
321
325
  const tokenContent = (typeof tokenContentArg === 'string') ? tokenContentArg : String(token.content || '')
322
326
  const tokenTrim = (typeof tokenTrimArg === 'string') ? tokenTrimArg : tokenContent.trim()
323
327
  const tokenLower = (typeof tokenLowerArg === 'string') ? tokenLowerArg : tokenTrim.toLowerCase()
324
328
  const scopeCandidates = getShikiScopeCandidates(token, rawScope)
325
- const langKey = preResolvedLangKey || resolveShikiKeywordLang(lang, scopeCandidates, token, opt)
329
+ const langKey = preResolvedLangKey || resolveShikiRoleLang(lang, scopeCandidates, token, opt)
326
330
  const lowerScopeCandidates = toLowerScopeCandidates(scopeCandidates)
327
331
  const hasAnyScopePatternCached = createScopePatternMatcher(lowerScopeCandidates)
332
+ const isIdentifier = shikiSimpleIdentifierReg.test(tokenTrim)
333
+ const isPunctuation = shikiPunctuationTokenReg.test(tokenTrim)
334
+ const tokenBucket = classifyShikiRoleByToken(tokenContent, langKey, tokenTrim, tokenLower, isPunctuation)
328
335
  return {
329
336
  rawScope,
330
337
  token,
@@ -335,18 +342,18 @@ const buildShikiKeywordContext = (rawScope, tok, lang, opt, preResolvedLangKey =
335
342
  tokenContent,
336
343
  tokenTrim,
337
344
  tokenLower,
338
- isIdentifier: shikiSimpleIdentifierReg.test(tokenTrim),
339
- isPunctuation: shikiPunctuationTokenReg.test(tokenTrim),
345
+ isIdentifier,
346
+ isPunctuation,
347
+ tokenBucket,
340
348
  hasAnyScopePattern: hasAnyScopePatternCached,
341
349
  hasScopePattern: (pattern) => hasAnyScopePatternCached([pattern]),
342
350
  }
343
351
  }
344
352
 
345
- const classifyShikiScopeKeywordBaseFromContext = (context) => {
353
+ const classifyShikiScopeRoleBaseFromContext = (context) => {
346
354
  if (!context || typeof context !== 'object') return 'text'
347
355
  let best = 'text'
348
- let bestScore = shikiKeywordBucketScoreV3.text
349
- const candidates = Array.isArray(context.scopeCandidates) ? context.scopeCandidates : []
356
+ let bestScore = shikiRoleBucketScore.text
350
357
  const lowerScopeCandidates = Array.isArray(context.lowerScopeCandidates) ? context.lowerScopeCandidates : []
351
358
  const hasAnyPattern = (context && typeof context.hasAnyScopePattern === 'function')
352
359
  ? context.hasAnyScopePattern
@@ -355,63 +362,64 @@ const classifyShikiScopeKeywordBaseFromContext = (context) => {
355
362
  ? context.hasScopePattern
356
363
  : (pattern) => hasAnyPattern([pattern])
357
364
  const langKey = String(context.langKey || '')
358
- const tokenContent = context.tokenContent
359
365
  if (langKey === 'hcl' || langKey === 'terraform') {
360
- const hclBucket = classifyShikiHclKeywordBucketV3(lowerScopeCandidates, hasAnyPattern)
366
+ const hclBucket = classifyShikiHclRoleBucket(lowerScopeCandidates, hasAnyPattern)
361
367
  if (hclBucket) return hclBucket
362
368
  }
363
- if (hasJsonYamlPropertyNameScope(candidates)) return 'type'
369
+ if (hasJsonYamlPropertyNameScope(lowerScopeCandidates)) return 'type'
364
370
  if (langKey === 'yaml' && hasScope('entity.name.tag.yaml')) return 'tag'
365
371
  for (let i = 0; i < lowerScopeCandidates.length; i++) {
366
- const bucket = classifyShikiScopeKeywordSingleV3(lowerScopeCandidates[i])
367
- const score = shikiKeywordBucketScoreV3[bucket] || shikiKeywordBucketScoreV3.text
372
+ const bucket = classifyShikiScopeRoleSingle(lowerScopeCandidates[i])
373
+ const score = shikiRoleBucketScore[bucket] || shikiRoleBucketScore.text
368
374
  if (score > bestScore) {
369
375
  best = bucket
370
376
  bestScore = score
371
377
  }
372
378
  }
373
- const tokenBucket = classifyShikiKeywordByToken(tokenContent, langKey)
379
+ if (bestScore <= shikiRoleBucketScore.text) {
380
+ const themeRoleBucket = resolveRendererFenceShikiThemeRoleBucket(lowerScopeCandidates)
381
+ if (themeRoleBucket) {
382
+ best = themeRoleBucket
383
+ bestScore = shikiRoleBucketScore.text + 1
384
+ }
385
+ }
386
+ const tokenBucket = context.tokenBucket
374
387
  if (tokenBucket) {
375
- const tokenScore = (shikiKeywordBucketScoreV3[tokenBucket] || shikiKeywordBucketScoreV3.text) + 1
376
- if (tokenScore > bestScore) best = tokenBucket
388
+ const tokenScore = (shikiRoleBucketScore[tokenBucket] || shikiRoleBucketScore.text) + 1
389
+ if (best !== 'string' && best !== 'string-unquoted' && tokenScore > bestScore) best = tokenBucket
377
390
  }
378
391
  return best
379
392
  }
380
393
 
381
- const resolveShikiKeywordLangForFence = (lang, opt) => {
382
- return resolveShikiKeywordLang(lang, null, null, opt)
394
+ const resolveShikiRoleLangForFence = (lang, opt) => {
395
+ return resolveShikiRoleLang(lang, null, null, opt)
383
396
  }
384
397
 
385
- const classifyShikiScopeKeyword = (rawScope, tok, lang, opt, preResolvedKeywordLang = '') => {
398
+ const classifyShikiScopeRole = (rawScope, tok, lang, opt, preResolvedRoleLang = '') => {
386
399
  const token = (tok && typeof tok === 'object') ? tok : {}
387
400
  const tokenContent = String(token.content || '')
388
401
  const tokenTrim = tokenContent.trim()
389
402
  if (!tokenTrim) return 'text'
390
403
  const tokenLower = tokenTrim.toLowerCase()
391
- const context = buildShikiKeywordContext(
404
+ const context = buildShikiRoleContext(
392
405
  rawScope,
393
406
  token,
394
407
  lang,
395
408
  opt,
396
- preResolvedKeywordLang,
409
+ preResolvedRoleLang,
397
410
  tokenContent,
398
411
  tokenTrim,
399
412
  tokenLower,
400
413
  )
401
- const baseBucket = classifyShikiScopeKeywordBaseFromContext(context)
402
- const postHelpers = {
403
- classifyToken: classifyShikiKeywordByToken,
404
- hasAnyScopePattern: context.hasAnyScopePattern,
405
- hasScopePattern: context.hasScopePattern,
406
- }
407
- const postBucket = applyShikiKeywordLegacyPostRules(baseBucket, context, postHelpers)
408
- const finalBucket = applyKeywordV4Rules(postBucket, context)
414
+ const baseBucket = classifyShikiScopeRoleBaseFromContext(context)
415
+ const postBucket = applyShikiRoleLegacyPostRules(baseBucket, context)
416
+ const finalBucket = applyRoleV4Rules(postBucket, context)
409
417
  return finalBucket || postBucket || baseBucket
410
418
  }
411
419
 
412
420
  export {
413
- classifyShikiScopeKeyword,
421
+ classifyShikiScopeRole,
414
422
  getShikiRawScopeName,
415
- normalizeShikiKeywordLangAliasMap,
416
- resolveShikiKeywordLangForFence,
423
+ normalizeShikiRoleLangAliasMap,
424
+ resolveShikiRoleLangForFence,
417
425
  }
@@ -0,0 +1,176 @@
1
+ const freezeScopeGroups = (groups) => Object.freeze(Object.fromEntries(
2
+ Object.entries(groups).map(([key, scopes]) => [key, Object.freeze(scopes.slice())]),
3
+ ))
4
+
5
+ const rendererFenceShikiThemeScopeGroups = freezeScopeGroups({
6
+ comment: [
7
+ 'comment',
8
+ 'punctuation.definition.comment',
9
+ 'string.comment',
10
+ ],
11
+ string: [
12
+ 'string',
13
+ 'constant.other.symbol',
14
+ 'constant.other.key',
15
+ ],
16
+ number: [
17
+ 'constant.numeric',
18
+ 'constant.numeric.integer',
19
+ 'constant.numeric.float',
20
+ 'constant.other.color',
21
+ ],
22
+ literal: [
23
+ 'constant.language',
24
+ 'constant.character',
25
+ 'support.constant',
26
+ 'variable.language',
27
+ ],
28
+ keyword: [
29
+ 'keyword',
30
+ 'storage.modifier',
31
+ 'storage.type',
32
+ 'storage.type.function',
33
+ ],
34
+ punctuation: [
35
+ // Keep this intentionally narrower than all of keyword.operator.
36
+ // Some grammars classify word-like tokens under keyword.operator; mapping the
37
+ // whole parent scope to punctuation can demote real keywords such as "new".
38
+ 'keyword.operator.accessor',
39
+ 'keyword.operator.arithmetic',
40
+ 'keyword.operator.assignment',
41
+ 'keyword.operator.bitwise',
42
+ 'keyword.operator.comparison',
43
+ 'keyword.operator.expression',
44
+ 'keyword.operator.logical',
45
+ 'keyword.operator.nullish',
46
+ 'keyword.operator.optional',
47
+ 'keyword.operator.spread',
48
+ 'keyword.operator.ternary',
49
+ 'punctuation',
50
+ 'meta.brace',
51
+ 'meta.delimiter',
52
+ ],
53
+ title: [
54
+ 'entity.name.function',
55
+ 'support.function',
56
+ ],
57
+ type: [
58
+ 'entity.name.class',
59
+ 'entity.name.type',
60
+ 'entity.name.namespace',
61
+ 'support.class',
62
+ 'support.type',
63
+ 'support.type.primitive',
64
+ 'meta.type',
65
+ ],
66
+ variable: [
67
+ 'variable',
68
+ 'variable.other',
69
+ 'variable.parameter',
70
+ 'meta.object-literal.key',
71
+ ],
72
+ tag: [
73
+ 'entity.name.tag',
74
+ 'meta.tag',
75
+ ],
76
+ attribute: [
77
+ 'entity.other.attribute-name',
78
+ 'meta.attribute',
79
+ ],
80
+ })
81
+
82
+ const rendererFenceShikiThemeRoleBuckets = Object.freeze({
83
+ comment: 'comment',
84
+ string: 'string',
85
+ number: 'number',
86
+ literal: 'literal',
87
+ keyword: 'keyword',
88
+ punctuation: 'punctuation',
89
+ title: 'title-function',
90
+ type: 'type',
91
+ variable: 'variable',
92
+ tag: 'tag',
93
+ attribute: 'attribute',
94
+ })
95
+
96
+ const rendererFenceShikiThemeRoleScores = Object.freeze({
97
+ comment: 210,
98
+ attribute: 200,
99
+ tag: 198,
100
+ keyword: 194,
101
+ type: 188,
102
+ number: 186,
103
+ literal: 184,
104
+ string: 182,
105
+ title: 172,
106
+ variable: 156,
107
+ punctuation: 140,
108
+ })
109
+
110
+ const shikiThemeRolePatternEntries = Object.freeze(
111
+ Object.entries(rendererFenceShikiThemeScopeGroups).flatMap(([role, patterns]) => {
112
+ const bucket = rendererFenceShikiThemeRoleBuckets[role] || role
113
+ const score = rendererFenceShikiThemeRoleScores[role] || 0
114
+ return patterns.map((pattern) => Object.freeze({
115
+ bucket,
116
+ pattern,
117
+ patternLength: pattern.length,
118
+ score,
119
+ }))
120
+ }),
121
+ )
122
+
123
+ const shikiThemeRoleNoHit = Object.freeze({
124
+ bucket: '',
125
+ patternLength: -1,
126
+ score: -1,
127
+ })
128
+
129
+ const shikiThemeRoleSingleCache = new Map()
130
+
131
+ const resolveRendererFenceShikiThemeRoleSingle = (scope) => {
132
+ const s = String(scope || '')
133
+ if (!s) return shikiThemeRoleNoHit
134
+ const cached = shikiThemeRoleSingleCache.get(s)
135
+ if (cached) return cached
136
+
137
+ let best = shikiThemeRoleNoHit
138
+ for (let i = 0; i < shikiThemeRolePatternEntries.length; i++) {
139
+ const entry = shikiThemeRolePatternEntries[i]
140
+ if (!s.includes(entry.pattern)) continue
141
+ if (
142
+ entry.score > best.score ||
143
+ (entry.score === best.score && entry.patternLength > best.patternLength)
144
+ ) {
145
+ best = entry
146
+ }
147
+ }
148
+ if (shikiThemeRoleSingleCache.size >= 8192) shikiThemeRoleSingleCache.clear()
149
+ shikiThemeRoleSingleCache.set(s, best)
150
+ return best
151
+ }
152
+
153
+ const resolveRendererFenceShikiThemeRoleBucket = (lowerScopeCandidates) => {
154
+ if (!Array.isArray(lowerScopeCandidates) || lowerScopeCandidates.length === 0) return null
155
+ let bestBucket = null
156
+ let bestScore = -1
157
+ let bestPatternLength = -1
158
+ for (let i = 0; i < lowerScopeCandidates.length; i++) {
159
+ const hit = resolveRendererFenceShikiThemeRoleSingle(lowerScopeCandidates[i])
160
+ if (!hit.bucket) continue
161
+ if (
162
+ hit.score > bestScore ||
163
+ (hit.score === bestScore && hit.patternLength > bestPatternLength)
164
+ ) {
165
+ bestBucket = hit.bucket
166
+ bestScore = hit.score
167
+ bestPatternLength = hit.patternLength
168
+ }
169
+ }
170
+ return bestBucket
171
+ }
172
+
173
+ export {
174
+ rendererFenceShikiThemeScopeGroups,
175
+ resolveRendererFenceShikiThemeRoleBucket,
176
+ }
@@ -5,17 +5,36 @@ import {
5
5
  const lineNotesRuleName = 'renderer_fence_line_notes'
6
6
  const lineNotesMetaKey = '__rendererFenceLineNotes'
7
7
  const lineNotesInstalledKey = '__rendererFenceLineNotesInstalled'
8
- const fenceInfoNameReg = /^([^{\s]*)/
9
8
  const lineNoteHeaderReg = /^\s*([1-9]\d*)(?:\s*-\s*([1-9]\d*))?\s*[::]\s*(.*)$/
10
9
  const lineNoteContinuationReg = /^(?: {2,}|\t)/
11
- const lineNoteFenceNameSet = new Set(['line-notes', 'notes'])
12
10
  const lineNoteAttrsReg = /^(.*?)(?:\s+\{([^{}]+)\})\s*$/
13
11
  const lineNoteAttrsOnlyReg = /^\{([^{}]+)\}\s*$/
14
12
  const lineNoteWidthReg = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|ch|vw|svw|lvw|dvw|%)$/
15
13
 
16
- const getFenceInfoName = (info) => {
17
- const match = String(info ?? '').trim().match(fenceInfoNameReg)
18
- return match ? match[1] : ''
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')
19
38
  }
20
39
 
21
40
  const stripContinuationIndent = (line) => {
@@ -155,6 +174,7 @@ const installLineNotesCoreRule = (md) => {
155
174
  if (!md || md[lineNotesInstalledKey]) return
156
175
  md[lineNotesInstalledKey] = true
157
176
  md.core.ruler.after('block', lineNotesRuleName, (state) => {
177
+ if (!canContainLineNotes(state)) return
158
178
  const tokens = state && state.tokens
159
179
  if (!Array.isArray(tokens) || tokens.length < 2) return
160
180
 
@@ -162,10 +182,8 @@ const installLineNotesCoreRule = (md) => {
162
182
  const token = tokens[i]
163
183
  const next = tokens[i + 1]
164
184
  if (!token || token.type !== 'fence' || !next || next.type !== 'fence') continue
165
- const nextName = getFenceInfoName(next.info)
166
- if (!lineNoteFenceNameSet.has(nextName)) continue
167
- const currentName = getFenceInfoName(token.info)
168
- if (lineNoteFenceNameSet.has(currentName)) continue
185
+ if (!isLineNoteFenceInfo(next.info)) continue
186
+ if (isLineNoteFenceInfo(token.info)) continue
169
187
  const notes = parseLineNotesContent(next.content)
170
188
  if (!notes || !notes.length) continue
171
189
  setTokenLineNotes(token, notes)
@@ -11,7 +11,7 @@ const customHighlightDataScriptId = 'pre-highlight-data'
11
11
  const customHighlightStyleTagId = 'pre-highlight-style'
12
12
  const customHighlightEnvInitRuleName = 'renderer_fence_custom_highlight_env_init'
13
13
 
14
- const runtimeFallbackReasonSet = new Set(['api-unsupported', 'apply-error'])
14
+ const runtimeFallbackReasonSet = new Set(['api-unsupported', 'range-invalid', 'apply-error'])
15
15
 
16
16
  const customHighlightPayloadSchemaVersion = 1
17
17
  const customHighlightPayloadSupportedVersions = [customHighlightPayloadSchemaVersion]