@peaceroad/markdown-it-renderer-fence 0.7.0 → 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.
@@ -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,10 +178,10 @@ 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
187
  if (s.includes('comment')) return 'comment'
@@ -190,7 +191,7 @@ const classifyShikiScopeKeywordSingleV3 = (scope) => {
190
191
  if (s.includes('entity.name.tag')) return 'tag'
191
192
  if (s.includes('string.unquoted')) return 'string-unquoted'
192
193
  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.other.color')) return 'number'
194
195
  if (s.includes('constant.numeric') || s.includes('number')) return 'number'
195
196
  if (s.includes('constant.language') || s.includes('boolean') || s.includes('null') || s.includes('undefined') || s.includes('none')) return 'literal'
196
197
  if (s.includes('entity.name.scope-resolution') || s.includes('entity.name.namespace')) return 'namespace'
@@ -207,14 +208,14 @@ const classifyShikiScopeKeywordSingleV3 = (scope) => {
207
208
  if (s.includes('entity.name.class') || s.includes('entity.name.type.class')) return 'title-class'
208
209
  if (s.includes('storage.type.built-in.primitive')) return 'type-primitive'
209
210
  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('support.type')) return 'type'
211
212
  if (s.includes('storage') || s.includes('keyword') || s.includes('operator') || s.includes('control') || s.includes('modifier')) return 'keyword'
212
213
  if (s.includes('variable')) return 'variable'
213
214
  if (s.includes('punctuation')) return 'punctuation'
214
215
  if (s.includes('source.')) bucket = 'text'
215
216
  else if (s.includes('meta')) bucket = 'meta'
216
- shikiScopeKeywordSingleV3Cache.set(s, bucket)
217
- if (shikiScopeKeywordSingleV3Cache.size > 8192) shikiScopeKeywordSingleV3Cache.clear()
217
+ shikiScopeRoleCache.set(s, bucket)
218
+ if (shikiScopeRoleCache.size > 8192) shikiScopeRoleCache.clear()
218
219
  return bucket
219
220
  }
220
221
 
@@ -228,7 +229,7 @@ const hasJsonYamlPropertyNameScope = (scopeCandidates) => {
228
229
  return false
229
230
  }
230
231
 
231
- const classifyShikiHclKeywordBucketV3 = (lowerScopeCandidates, hasAnyPattern) => {
232
+ const classifyShikiHclRoleBucket = (lowerScopeCandidates, hasAnyPattern) => {
232
233
  const hasAny = (typeof hasAnyPattern === 'function') ? hasAnyPattern : (patterns) => hasAnyScopePattern(lowerScopeCandidates, patterns)
233
234
  if (!Array.isArray(lowerScopeCandidates) || lowerScopeCandidates.length === 0) return null
234
235
  if (hasAny(['entity.name.type.hcl', 'entity-name-type-hcl'])) return 'title-class'
@@ -243,10 +244,10 @@ const classifyShikiHclKeywordBucketV3 = (lowerScopeCandidates, hasAnyPattern) =>
243
244
  return null
244
245
  }
245
246
 
246
- const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
247
+ const resolveShikiRoleLang = (lang, scopeCandidates, tok, opt) => {
247
248
  const queue = []
248
249
  const seen = new Set()
249
- const customAliasMap = opt && opt.shikiKeywordLangAliases
250
+ const customAliasMap = opt && opt.shikiRoleLangAliases
250
251
  const shikiInternalAliasMap = opt && opt._shikiInternalLangAliasMap
251
252
  const resolveAlias = (key) => {
252
253
  if (customAliasMap && customAliasMap[key]) return customAliasMap[key]
@@ -254,46 +255,46 @@ const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
254
255
  return ''
255
256
  }
256
257
  const pushCandidate = (value) => {
257
- const key = normalizeShikiKeywordLangKey(value)
258
+ const key = normalizeShikiRoleLangKey(value)
258
259
  if (!key || seen.has(key)) return
259
260
  seen.add(key)
260
261
  queue.push(key)
261
262
  }
262
- if (opt && typeof opt.shikiKeywordLangResolver === 'function') {
263
+ if (opt && typeof opt.shikiRoleLangResolver === 'function') {
263
264
  try {
264
- const resolved = opt.shikiKeywordLangResolver(lang, scopeCandidates, tok)
265
+ const resolved = opt.shikiRoleLangResolver(lang, scopeCandidates, tok)
265
266
  if (resolved != null && resolved !== '') pushCandidate(resolved)
266
267
  } catch (e) {}
267
268
  }
268
269
  pushCandidate(lang)
269
270
  if (Array.isArray(scopeCandidates)) {
270
271
  for (let i = 0; i < scopeCandidates.length; i++) {
271
- const candidates = extractShikiKeywordLangCandidatesFromScope(scopeCandidates[i])
272
+ const candidates = extractShikiRoleLangCandidatesFromScope(scopeCandidates[i])
272
273
  for (let j = 0; j < candidates.length; j++) pushCandidate(candidates[j])
273
274
  }
274
275
  }
275
276
  for (let i = 0; i < queue.length; i++) {
276
277
  const key = queue[i]
277
- if (shikiKeywordTokensByLang[key]) return key
278
+ if (shikiLangKeywordTokens[key]) return key
278
279
  const aliased = resolveAlias(key)
279
280
  if (aliased) {
280
- if (shikiKeywordTokensByLang[aliased]) return aliased
281
+ if (shikiLangKeywordTokens[aliased]) return aliased
281
282
  pushCandidate(aliased)
282
283
  }
283
284
  const compact = key.replace(/-/g, '')
284
- if (shikiKeywordTokensByLang[compact]) return compact
285
+ if (shikiLangKeywordTokens[compact]) return compact
285
286
  const compactAliased = resolveAlias(compact)
286
287
  if (compactAliased) {
287
- if (shikiKeywordTokensByLang[compactAliased]) return compactAliased
288
+ if (shikiLangKeywordTokens[compactAliased]) return compactAliased
288
289
  pushCandidate(compactAliased)
289
290
  }
290
291
  const parts = key.split('-')
291
292
  for (let n = parts.length - 1; n >= 1; n--) {
292
293
  const head = parts.slice(0, n).join('-')
293
- if (shikiKeywordTokensByLang[head]) return head
294
+ if (shikiLangKeywordTokens[head]) return head
294
295
  const headAliased = resolveAlias(head)
295
296
  if (headAliased) {
296
- if (shikiKeywordTokensByLang[headAliased]) return headAliased
297
+ if (shikiLangKeywordTokens[headAliased]) return headAliased
297
298
  pushCandidate(headAliased)
298
299
  }
299
300
  }
@@ -301,7 +302,7 @@ const resolveShikiKeywordLang = (lang, scopeCandidates, tok, opt) => {
301
302
  return ''
302
303
  }
303
304
 
304
- const classifyShikiKeywordByToken = (content, langKey) => {
305
+ const classifyShikiRoleByToken = (content, langKey) => {
305
306
  const text = String(content || '')
306
307
  const trimmed = text.trim()
307
308
  if (!trimmed) return 'text'
@@ -309,20 +310,20 @@ const classifyShikiKeywordByToken = (content, langKey) => {
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
316
  if (shikiPunctuationTokenReg.test(trimmed)) return 'punctuation'
316
317
  return null
317
318
  }
318
319
 
319
- const buildShikiKeywordContext = (rawScope, tok, lang, opt, preResolvedLangKey = '', tokenContentArg, tokenTrimArg, tokenLowerArg) => {
320
+ const buildShikiRoleContext = (rawScope, tok, lang, opt, preResolvedLangKey = '', tokenContentArg, tokenTrimArg, tokenLowerArg) => {
320
321
  const token = (tok && typeof tok === 'object') ? tok : {}
321
322
  const tokenContent = (typeof tokenContentArg === 'string') ? tokenContentArg : String(token.content || '')
322
323
  const tokenTrim = (typeof tokenTrimArg === 'string') ? tokenTrimArg : tokenContent.trim()
323
324
  const tokenLower = (typeof tokenLowerArg === 'string') ? tokenLowerArg : tokenTrim.toLowerCase()
324
325
  const scopeCandidates = getShikiScopeCandidates(token, rawScope)
325
- const langKey = preResolvedLangKey || resolveShikiKeywordLang(lang, scopeCandidates, token, opt)
326
+ const langKey = preResolvedLangKey || resolveShikiRoleLang(lang, scopeCandidates, token, opt)
326
327
  const lowerScopeCandidates = toLowerScopeCandidates(scopeCandidates)
327
328
  const hasAnyScopePatternCached = createScopePatternMatcher(lowerScopeCandidates)
328
329
  return {
@@ -342,10 +343,10 @@ const buildShikiKeywordContext = (rawScope, tok, lang, opt, preResolvedLangKey =
342
343
  }
343
344
  }
344
345
 
345
- const classifyShikiScopeKeywordBaseFromContext = (context) => {
346
+ const classifyShikiScopeRoleBaseFromContext = (context) => {
346
347
  if (!context || typeof context !== 'object') return 'text'
347
348
  let best = 'text'
348
- let bestScore = shikiKeywordBucketScoreV3.text
349
+ let bestScore = shikiRoleBucketScore.text
349
350
  const candidates = Array.isArray(context.scopeCandidates) ? context.scopeCandidates : []
350
351
  const lowerScopeCandidates = Array.isArray(context.lowerScopeCandidates) ? context.lowerScopeCandidates : []
351
352
  const hasAnyPattern = (context && typeof context.hasAnyScopePattern === 'function')
@@ -357,61 +358,68 @@ const classifyShikiScopeKeywordBaseFromContext = (context) => {
357
358
  const langKey = String(context.langKey || '')
358
359
  const tokenContent = context.tokenContent
359
360
  if (langKey === 'hcl' || langKey === 'terraform') {
360
- const hclBucket = classifyShikiHclKeywordBucketV3(lowerScopeCandidates, hasAnyPattern)
361
+ const hclBucket = classifyShikiHclRoleBucket(lowerScopeCandidates, hasAnyPattern)
361
362
  if (hclBucket) return hclBucket
362
363
  }
363
364
  if (hasJsonYamlPropertyNameScope(candidates)) return 'type'
364
365
  if (langKey === 'yaml' && hasScope('entity.name.tag.yaml')) return 'tag'
365
366
  for (let i = 0; i < lowerScopeCandidates.length; i++) {
366
- const bucket = classifyShikiScopeKeywordSingleV3(lowerScopeCandidates[i])
367
- const score = shikiKeywordBucketScoreV3[bucket] || shikiKeywordBucketScoreV3.text
367
+ const bucket = classifyShikiScopeRoleSingle(lowerScopeCandidates[i])
368
+ const score = shikiRoleBucketScore[bucket] || shikiRoleBucketScore.text
368
369
  if (score > bestScore) {
369
370
  best = bucket
370
371
  bestScore = score
371
372
  }
372
373
  }
373
- const tokenBucket = classifyShikiKeywordByToken(tokenContent, langKey)
374
+ if (bestScore <= shikiRoleBucketScore.text) {
375
+ const themeRoleBucket = resolveRendererFenceShikiThemeRoleBucket(lowerScopeCandidates)
376
+ if (themeRoleBucket) {
377
+ best = themeRoleBucket
378
+ bestScore = shikiRoleBucketScore.text + 1
379
+ }
380
+ }
381
+ const tokenBucket = classifyShikiRoleByToken(tokenContent, langKey)
374
382
  if (tokenBucket) {
375
- const tokenScore = (shikiKeywordBucketScoreV3[tokenBucket] || shikiKeywordBucketScoreV3.text) + 1
376
- if (tokenScore > bestScore) best = tokenBucket
383
+ const tokenScore = (shikiRoleBucketScore[tokenBucket] || shikiRoleBucketScore.text) + 1
384
+ if (best !== 'string' && best !== 'string-unquoted' && tokenScore > bestScore) best = tokenBucket
377
385
  }
378
386
  return best
379
387
  }
380
388
 
381
- const resolveShikiKeywordLangForFence = (lang, opt) => {
382
- return resolveShikiKeywordLang(lang, null, null, opt)
389
+ const resolveShikiRoleLangForFence = (lang, opt) => {
390
+ return resolveShikiRoleLang(lang, null, null, opt)
383
391
  }
384
392
 
385
- const classifyShikiScopeKeyword = (rawScope, tok, lang, opt, preResolvedKeywordLang = '') => {
393
+ const classifyShikiScopeRole = (rawScope, tok, lang, opt, preResolvedRoleLang = '') => {
386
394
  const token = (tok && typeof tok === 'object') ? tok : {}
387
395
  const tokenContent = String(token.content || '')
388
396
  const tokenTrim = tokenContent.trim()
389
397
  if (!tokenTrim) return 'text'
390
398
  const tokenLower = tokenTrim.toLowerCase()
391
- const context = buildShikiKeywordContext(
399
+ const context = buildShikiRoleContext(
392
400
  rawScope,
393
401
  token,
394
402
  lang,
395
403
  opt,
396
- preResolvedKeywordLang,
404
+ preResolvedRoleLang,
397
405
  tokenContent,
398
406
  tokenTrim,
399
407
  tokenLower,
400
408
  )
401
- const baseBucket = classifyShikiScopeKeywordBaseFromContext(context)
409
+ const baseBucket = classifyShikiScopeRoleBaseFromContext(context)
402
410
  const postHelpers = {
403
- classifyToken: classifyShikiKeywordByToken,
411
+ classifyToken: classifyShikiRoleByToken,
404
412
  hasAnyScopePattern: context.hasAnyScopePattern,
405
413
  hasScopePattern: context.hasScopePattern,
406
414
  }
407
- const postBucket = applyShikiKeywordLegacyPostRules(baseBucket, context, postHelpers)
408
- const finalBucket = applyKeywordV4Rules(postBucket, context)
415
+ const postBucket = applyShikiRoleLegacyPostRules(baseBucket, context, postHelpers)
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)