@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,9 @@
1
1
  import {
2
- classifyShikiScopeKeyword,
2
+ classifyShikiScopeRole,
3
3
  getShikiRawScopeName,
4
- normalizeShikiKeywordLangAliasMap,
5
- resolveShikiKeywordLangForFence,
6
- } from '../custom-highlight/shiki-keyword.js'
4
+ normalizeShikiRoleLangAliasMap,
5
+ resolveShikiRoleLangForFence,
6
+ } from '../custom-highlight/shiki-role.js'
7
7
  import {
8
8
  escapeHtmlAttr,
9
9
  getCustomHighlightPayloadMap,
@@ -20,6 +20,7 @@ import {
20
20
  } from '../custom-highlight/scope-name.js'
21
21
  import {
22
22
  commentLineClass,
23
+ getCommentLineRanges,
23
24
  normalizeEmphasisRanges,
24
25
  } from './render-shared.js'
25
26
  import {
@@ -46,9 +47,9 @@ const defaultCustomHighlightOpt = {
46
47
  scopePrefix: 'hl',
47
48
  includeScopeStyles: true,
48
49
  shikiScopeMode: 'auto',
49
- shikiKeywordClassifier: null,
50
- shikiKeywordLangResolver: null,
51
- shikiKeywordLangAliases: null,
50
+ shikiRoleClassifier: null,
51
+ shikiRoleLangResolver: null,
52
+ shikiRoleLangAliases: null,
52
53
  }
53
54
 
54
55
  const normalizeCustomHighlightProvider = (provider) => {
@@ -60,7 +61,7 @@ const normalizeCustomHighlightProvider = (provider) => {
60
61
  const normalizeShikiScopeMode = (mode) => {
61
62
  const key = String(mode || '').trim().toLowerCase()
62
63
  if (!key) return null
63
- if (key === 'keyword') return 'keyword'
64
+ if (key === 'role') return 'role'
64
65
  if (key === 'semantic') return 'semantic'
65
66
  if (key === 'color') return 'color'
66
67
  if (key === 'auto') return 'auto'
@@ -135,7 +136,7 @@ const buildShikiInternalLangAliasMap = (highlighter) => {
135
136
  } catch (e) {}
136
137
  rawMap[rawName] = canonical
137
138
  }
138
- return normalizeShikiKeywordLangAliasMap(rawMap)
139
+ return normalizeShikiRoleLangAliasMap(rawMap)
139
140
  }
140
141
 
141
142
  const normalizeCustomHighlightOpt = (opt = {}) => {
@@ -162,11 +163,11 @@ const normalizeCustomHighlightOpt = (opt = {}) => {
162
163
  } else {
163
164
  next.theme = normalizedTheme.singleTheme || null
164
165
  }
165
- if (typeof next.shikiKeywordClassifier !== 'function') next.shikiKeywordClassifier = null
166
- if (typeof next.shikiKeywordLangResolver !== 'function') next.shikiKeywordLangResolver = null
167
- next.shikiKeywordLangAliases = normalizeShikiKeywordLangAliasMap(next.shikiKeywordLangAliases)
166
+ if (typeof next.shikiRoleClassifier !== 'function') next.shikiRoleClassifier = null
167
+ if (typeof next.shikiRoleLangResolver !== 'function') next.shikiRoleLangResolver = null
168
+ next.shikiRoleLangAliases = normalizeShikiRoleLangAliasMap(next.shikiRoleLangAliases)
168
169
  next._shikiInternalLangAliasMap =
169
- (next.provider === 'shiki' && next.shikiScopeMode === 'keyword')
170
+ (next.provider === 'shiki' && next.shikiScopeMode === 'role')
170
171
  ? buildShikiInternalLangAliasMap(next.highlighter)
171
172
  : null
172
173
  const fallbackOn = Array.isArray(next.fallbackOn) ? next.fallbackOn : fallbackOnDefault
@@ -272,27 +273,26 @@ const sameScopeStyles = (a, b) => {
272
273
  return true
273
274
  }
274
275
 
275
- const getLogicalLinesAndOffsets = (text) => {
276
- const rawLines = text.split('\n')
277
- const logicalLineCount = rawLines.length > 0 && rawLines[rawLines.length - 1] === '' ? rawLines.length - 1 : rawLines.length
278
- const lines = new Array(logicalLineCount)
279
- const offsets = new Array(logicalLineCount)
280
- let cursor = 0
281
- for (let i = 0; i < logicalLineCount; i++) {
282
- const line = rawLines[i]
283
- lines[i] = line
284
- offsets[i] = [cursor, cursor + line.length]
285
- cursor += line.length + 1
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
286
287
  }
287
- return { lines, offsets }
288
+ if (lineStart < str.length) offsets.push([lineStart, str.length])
289
+ return offsets
288
290
  }
289
291
 
290
292
  const getShikiTokenStyle = (token) => {
291
293
  const style = {}
292
294
  if (typeof token.color === 'string' && token.color) style.color = token.color
293
295
  const fontStyle = Number.isFinite(token.fontStyle) ? token.fontStyle : 0
294
- if (fontStyle & 1) style.fontStyle = 'italic'
295
- if (fontStyle & 2) style.fontWeight = '700'
296
296
  if (fontStyle & 4) style.textDecoration = 'underline'
297
297
  return normalizeScopeStyle(style)
298
298
  }
@@ -459,21 +459,21 @@ const hasShikiOffsets = (tokenLines) => {
459
459
  return false
460
460
  }
461
461
 
462
- const getShikiScopeName = (tok, lang, style, opt, preResolvedKeywordLang = '') => {
462
+ const getShikiScopeName = (tok, lang, style, opt, preResolvedRoleLang = '') => {
463
463
  const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
464
464
  const rawScope = getShikiRawScopeName(tok)
465
- let customKeywordName = null
466
- const isKeywordScopeMode = scopeMode === 'keyword'
467
- if (isKeywordScopeMode && opt && typeof opt.shikiKeywordClassifier === 'function') {
465
+ let customRoleName = null
466
+ const isRoleScopeMode = scopeMode === 'role'
467
+ if (isRoleScopeMode && opt && typeof opt.shikiRoleClassifier === 'function') {
468
468
  try {
469
- const customName = opt.shikiKeywordClassifier(rawScope, tok, { lang, style, scopeMode })
470
- if (customName != null && customName !== '') customKeywordName = String(customName)
469
+ const customName = opt.shikiRoleClassifier(rawScope, tok, { lang, style, scopeMode })
470
+ if (customName != null && customName !== '') customRoleName = String(customName)
471
471
  } catch (e) {}
472
472
  }
473
- if (isKeywordScopeMode) {
474
- if (customKeywordName) return 'shiki-' + customKeywordName
475
- const bucket = classifyShikiScopeKeyword(rawScope, tok, lang, opt, preResolvedKeywordLang)
476
- return 'shiki-' + bucket
473
+ if (isRoleScopeMode) {
474
+ if (customRoleName) return 'shiki-role-' + customRoleName
475
+ const bucket = classifyShikiScopeRole(rawScope, tok, lang, opt, preResolvedRoleLang)
476
+ return 'shiki-role-' + bucket
477
477
  }
478
478
  if (scopeMode === 'semantic') {
479
479
  if (rawScope) return 'shiki-' + rawScope
@@ -515,11 +515,11 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
515
515
  scopeMode === 'auto' ||
516
516
  scopeMode === 'color' ||
517
517
  scopeMode === 'semantic' ||
518
- (scopeMode === 'keyword' && !!(opt && typeof opt.shikiKeywordClassifier === 'function'))
518
+ (scopeMode === 'role' && !!(opt && typeof opt.shikiRoleClassifier === 'function'))
519
519
  const needTokenStyle = includeScopeStyles || needStyleForScopeName
520
- let preResolvedKeywordLang = ''
521
- if (scopeMode === 'keyword' && (!opt || typeof opt.shikiKeywordLangResolver !== 'function')) {
522
- preResolvedKeywordLang = resolveShikiKeywordLangForFence(lang, opt)
520
+ let preResolvedRoleLang = ''
521
+ if (scopeMode === 'role' && (!opt || typeof opt.shikiRoleLangResolver !== 'function')) {
522
+ preResolvedRoleLang = resolveShikiRoleLangForFence(lang, opt)
523
523
  }
524
524
  let cursor = 0
525
525
  for (let i = 0; i < tokenLines.length; i++) {
@@ -534,7 +534,7 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
534
534
  const start = hasOffset ? tok.offset : cursor
535
535
  const end = start + content.length
536
536
  const style = needTokenStyle ? getShikiTokenStyle(tok) : null
537
- const scope = getShikiScopeName(tok, lang, style, opt, preResolvedKeywordLang)
537
+ const scope = getShikiScopeName(tok, lang, style, opt, preResolvedRoleLang)
538
538
  entries.push({ scope, start, end, style })
539
539
  cursor = end
540
540
  }
@@ -551,6 +551,7 @@ const normalizeCustomProviderRanges = (result) => {
551
551
  const entries = []
552
552
  for (const range of source.ranges) {
553
553
  let scope
554
+ let scopeIndex = null
554
555
  let start
555
556
  let end
556
557
  let style
@@ -567,10 +568,11 @@ const normalizeCustomProviderRanges = (result) => {
567
568
  } else {
568
569
  continue
569
570
  }
570
- if (typeof scope === 'number' && scopes && scopes[scope] != null) scope = scopes[scope]
571
+ if (Number.isSafeInteger(scope)) scopeIndex = scope
572
+ if (scopeIndex != null && scopes && scopes[scopeIndex] != null) scope = scopes[scopeIndex]
571
573
  if (scope == null) continue
572
574
  if (!style && scopeStyles) {
573
- if (typeof scope === 'number' && Array.isArray(scopeStyles)) style = scopeStyles[scope]
575
+ if (scopeIndex != null && Array.isArray(scopeStyles)) style = scopeStyles[scopeIndex]
574
576
  else style = scopeStyles[String(scope)]
575
577
  }
576
578
  entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
@@ -609,7 +611,12 @@ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeS
609
611
  }
610
612
  scopeKeyToIndex.set(key, scopeIndex)
611
613
  }
612
- ranges.push([scopeIndex, start, end])
614
+ const previousRange = ranges.length ? ranges[ranges.length - 1] : null
615
+ if (previousRange && previousRange[0] === scopeIndex && previousRange[2] === start) {
616
+ previousRange[2] = end
617
+ } else {
618
+ ranges.push([scopeIndex, start, end])
619
+ }
613
620
  }
614
621
  const payload = {
615
622
  v: customHighlightPayloadSchemaVersion,
@@ -629,8 +636,8 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
629
636
  const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
630
637
  const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
631
638
  if (!needsEmphasis && !needsCommentScan) return
632
- const { lines, offsets } = getLogicalLinesAndOffsets(content)
633
- const maxLine = lines.length
639
+ const offsets = needsEmphasis ? getLogicalLineOffsets(content) : null
640
+ const maxLine = offsets ? offsets.length : 0
634
641
  if (needsEmphasis) {
635
642
  const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
636
643
  for (const [s, e] of normalized) {
@@ -640,11 +647,11 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
640
647
  }
641
648
  }
642
649
  if (needsCommentScan) {
643
- for (let i = 0; i < lines.length; i++) {
644
- if (lines[i].trimStart().startsWith(commentMarkValue)) {
645
- const [start, end] = offsets[i]
646
- if (end > start) entries.push({ scope: commentLineClass, start, end })
647
- }
650
+ const ranges = getCommentLineRanges(content, commentMarkValue)
651
+ if (!ranges) return
652
+ for (let i = 0; i < ranges.length; i++) {
653
+ const [start, end] = ranges[i]
654
+ entries.push({ scope: commentLineClass, start, end })
648
655
  }
649
656
  }
650
657
  }
@@ -657,8 +664,8 @@ const buildShikiTokenOption = (chOpt, targetLang, themeOverride = '') => {
657
664
  }
658
665
 
659
666
  const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
660
- const context = { token, md, env, option: chOpt }
661
667
  if (chOpt.provider === 'custom') {
668
+ const context = { token, md, env, option: chOpt }
662
669
  const getRanges = chOpt._customGetRanges
663
670
  if (typeof getRanges !== 'function') throw new Error('customHighlight.getRanges must be a function')
664
671
  const result = getRanges(token.content, lang, context)
@@ -666,6 +673,7 @@ const getApiProviderEntries = (token, lang, chOpt, md, env, override) => {
666
673
  return normalizeCustomProviderRanges(result)
667
674
  }
668
675
  if (chOpt.provider === 'hljs') {
676
+ const context = { token, md, env, option: chOpt }
669
677
  const highlightFn = chOpt._hljsHighlightFn
670
678
  if (typeof highlightFn !== 'function') {
671
679
  throw new Error('customHighlight.hljsHighlight (or customHighlight.highlight / md.options.highlight) must be a function when provider=hljs')
@@ -1,4 +1,5 @@
1
1
  import {
2
+ escapeHtmlAttr,
2
3
  escapeJsonForScript,
3
4
  } from '../custom-highlight/payload-utils.js'
4
5
  import {
@@ -28,7 +29,7 @@ import {
28
29
  renderFenceMarkup,
29
30
  } from './render-markup.js'
30
31
 
31
- const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold', 'line-notes'])
32
+ const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes'])
32
33
 
33
34
  let fallbackCustomHighlightSeq = 0
34
35
 
@@ -49,8 +50,7 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
49
50
  return preAttrs
50
51
  }
51
52
 
52
- const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
53
- const isSamp = opt._sampReg.test(lang)
53
+ const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
54
54
  const tag = isSamp ? 'samp' : 'code'
55
55
  let content = md.utils.escapeHtml(token.content)
56
56
  const lineStrategy = opt.customHighlight.lineFeatureStrategy
@@ -92,8 +92,9 @@ const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emph
92
92
  env[customHighlightDataEnvKey][id] = payload
93
93
  } else {
94
94
  const payloadJson = escapeJsonForScript(payload)
95
- const scriptId = `pre-highlight-data-${id}`
96
- inlinePayloadScript = `<script type="application/json" id="${scriptId}" ${customHighlightPreAttr}="${id}">${payloadJson}</script>\n`
95
+ const scriptId = escapeHtmlAttr(`pre-highlight-data-${id}`)
96
+ const escapedId = escapeHtmlAttr(id)
97
+ inlinePayloadScript = `<script type="application/json" id="${scriptId}" ${customHighlightPreAttr}="${escapedId}">${payloadJson}</script>\n`
97
98
  }
98
99
  }
99
100
 
@@ -119,6 +120,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
119
120
  const commentMarkValue = context.commentMarkValue
120
121
  const lineNotes = context.lineNotes
121
122
  const lineNoteIdPrefix = context.lineNoteIdPrefix
123
+ const isSamp = context.isSamp
122
124
 
123
125
  const apiDecisionBase = {
124
126
  renderer: 'api',
@@ -130,12 +132,13 @@ const getFenceHtml = (context, md, opt, slf, env) => {
130
132
  : [],
131
133
  }
132
134
  try {
133
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
135
+ const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
134
136
  if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
135
137
  emitFenceDecision(opt, apiDecisionBase)
136
138
  return html
137
139
  } catch (err) {
138
- if (opt.customHighlight.fallback === 'plain' && shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) {
140
+ if (!shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) throw err
141
+ if (opt.customHighlight.fallback === 'plain') {
139
142
  const fallbackDecision = {
140
143
  renderer: 'api',
141
144
  includePayload: false,
@@ -144,14 +147,27 @@ const getFenceHtml = (context, md, opt, slf, env) => {
144
147
  reason: 'provider-error',
145
148
  lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
146
149
  }
147
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
150
+ const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
148
151
  if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
149
152
  emitFenceDecision(opt, fallbackDecision)
150
153
  return html
151
154
  }
155
+ const fallbackDecision = {
156
+ renderer: 'api',
157
+ includePayload: false,
158
+ fallbackUsed: true,
159
+ fallback: 'markup',
160
+ reason: 'provider-error',
161
+ lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
162
+ }
163
+ const markupOpt = typeof opt.onFenceDecision === 'function'
164
+ ? { ...opt, onFenceDecision: null }
165
+ : opt
166
+ const html = renderFenceMarkup(context, md, markupOpt, slf)
167
+ if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
168
+ emitFenceDecision(opt, fallbackDecision)
169
+ return html
152
170
  }
153
-
154
- return renderFenceMarkup(context, md, opt, slf)
155
171
  }
156
172
 
157
173
  export {
@@ -297,6 +297,20 @@ const applyCustomHighlights = (root, options = {}) => {
297
297
  break
298
298
  }
299
299
  }
300
+ if (sameRefs && prevState.blockCache instanceof Map) {
301
+ for (const [blockId, codeEl] of blockRefs) {
302
+ const cached = prevState.blockCache.get(blockId)
303
+ const textSnapshot = typeof codeEl.textContent === 'string' ? codeEl.textContent : null
304
+ const boundaryRefs = getCodeBoundaryRefs(codeEl)
305
+ if (!cached ||
306
+ cached.textSnapshot !== textSnapshot ||
307
+ cached.firstRef !== boundaryRefs.first ||
308
+ cached.lastRef !== boundaryRefs.last) {
309
+ sameRefs = false
310
+ break
311
+ }
312
+ }
313
+ }
300
314
  if (sameRefs) {
301
315
  if (emitDiag) emitDiag({ type: 'runtime-skip', reason: 'unchanged' })
302
316
  return { appliedBlocks: 0, appliedRanges: 0, skipped: true, reason: 'unchanged' }
@@ -308,6 +322,7 @@ const applyCustomHighlights = (root, options = {}) => {
308
322
  let insertedScopeStyleMap = null
309
323
  if (!incremental) clearAppliedHighlightNames(queryRoot)
310
324
  const allScopeRanges = new Map()
325
+ const runtimeScopeNamesByVariant = new Map()
311
326
  let appliedBlocks = 0
312
327
  let appliedRanges = 0
313
328
  let pendingCss = ''
@@ -342,17 +357,12 @@ const applyCustomHighlights = (root, options = {}) => {
342
357
  if (payloadDigestByBlock) {
343
358
  blockPayloadDigest = payloadDigestByBlock.get(blockId)
344
359
  if (blockPayloadDigest === undefined) {
345
- const prevCached = prevBlockCache ? prevBlockCache.get(blockId) : null
346
- if (prevCached && prevCached.payloadRef === payload && prevCached.variantKey === activePayload.variantKey) {
347
- blockPayloadDigest = prevCached.payloadDigest || ''
348
- } else {
349
- blockPayloadDigest = getCachedPayloadString(payload, payloadStringifyCache) + `|variant=${activePayload.variantKey || ''}`
350
- }
360
+ blockPayloadDigest = getCachedPayloadString(payload, payloadStringifyCache) + `|variant=${activePayload.variantKey || ''}`
351
361
  payloadDigestByBlock.set(blockId, blockPayloadDigest)
352
362
  }
353
363
  }
354
- const textSnapshot = typeof codeEl.textContent === 'string' ? codeEl.textContent : null
355
- const boundaryRefs = getCodeBoundaryRefs(codeEl)
364
+ const textSnapshot = incremental && typeof codeEl.textContent === 'string' ? codeEl.textContent : null
365
+ const boundaryRefs = incremental ? getCodeBoundaryRefs(codeEl) : null
356
366
  const cached = prevBlockCache ? prevBlockCache.get(blockId) : null
357
367
  if (cached &&
358
368
  cached.codeEl === codeEl &&
@@ -404,6 +414,11 @@ const applyCustomHighlights = (root, options = {}) => {
404
414
  const appliedNames = new Set()
405
415
  const appliedNameList = []
406
416
  const scopeRuntimeNames = new Array(activePayload.scopes.length)
417
+ let runtimeScopeNameCache = runtimeScopeNamesByVariant.get(activePayload.variantKey)
418
+ if (!runtimeScopeNameCache) {
419
+ runtimeScopeNameCache = new Map()
420
+ runtimeScopeNamesByVariant.set(activePayload.variantKey, runtimeScopeNameCache)
421
+ }
407
422
  const scopeStyles = Array.isArray(activePayload.scopeStyles) ? activePayload.scopeStyles : null
408
423
  const blockScopeRanges = nextBlockCache ? new Map() : null
409
424
  const blockScopeMetaParts = nextBlockCache ? new Map() : null
@@ -448,7 +463,12 @@ const applyCustomHighlights = (root, options = {}) => {
448
463
  }
449
464
  let runtimeName = scopeRuntimeNames[scopeIdx]
450
465
  if (!runtimeName) {
451
- runtimeName = getRuntimeScopeName(scopeName, activePayload.variantKey)
466
+ const scopeCacheKey = String(scopeName)
467
+ runtimeName = runtimeScopeNameCache.get(scopeCacheKey)
468
+ if (!runtimeName) {
469
+ runtimeName = getRuntimeScopeName(scopeCacheKey, activePayload.variantKey)
470
+ runtimeScopeNameCache.set(scopeCacheKey, runtimeName)
471
+ }
452
472
  scopeRuntimeNames[scopeIdx] = runtimeName
453
473
  if (scopeStyles) {
454
474
  const css = styleToHighlightCss(scopeStyles[scopeIdx])
@@ -514,7 +534,6 @@ const applyCustomHighlights = (root, options = {}) => {
514
534
  if (nextBlockCache) {
515
535
  nextBlockCache.set(blockId, {
516
536
  codeEl,
517
- payloadRef: payload,
518
537
  variantKey: activePayload.variantKey || '',
519
538
  payloadDigest: blockPayloadDigest,
520
539
  textSnapshot,
@@ -40,6 +40,9 @@ const shouldRuntimeFallback = (reason, opt = {}) => {
40
40
  return shouldApplyApiFallbackForReason(chOpt, reason)
41
41
  }
42
42
 
43
+ const customHighlightEnvInitInstalledKey = '__rendererFenceCustomHighlightEnvInitInstalled'
44
+ const customHighlightEnvInitOptionKey = '__rendererFenceCustomHighlightEnvInitOption'
45
+
43
46
  const mditRendererFenceCustomHighlight = (md, option) => {
44
47
  const opt = {
45
48
  ...createCommonFenceOptionDefaults(md),
@@ -69,7 +72,7 @@ const mditRendererFenceCustomHighlight = (md, option) => {
69
72
  opt.customHighlight._hasShikiHighlighter = !!(opt.customHighlight.highlighter && typeof opt.customHighlight.highlighter.codeToTokens === 'function')
70
73
  const tokenOptionBase = {}
71
74
  if (opt.customHighlight._singleTheme) tokenOptionBase.theme = opt.customHighlight._singleTheme
72
- if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === 'keyword') {
75
+ if (opt.customHighlight.shikiScopeMode === 'semantic' || opt.customHighlight.shikiScopeMode === 'role') {
73
76
  tokenOptionBase.includeExplanation = 'scopeName'
74
77
  }
75
78
  opt.customHighlight._shikiTokenOptionBase = Object.keys(tokenOptionBase).length ? tokenOptionBase : null
@@ -78,17 +81,22 @@ const mditRendererFenceCustomHighlight = (md, option) => {
78
81
  finalizeCommonFenceOption(opt)
79
82
  installLineNotesCoreRule(md)
80
83
 
81
- md.core.ruler.before('block', customHighlightEnvInitRuleName, (state) => {
82
- const env = state && state.env
83
- if (!env || typeof env !== 'object') return
84
- if (opt.customHighlight.transport === 'env') {
85
- env[customHighlightDataEnvKey] = {}
86
- env[customHighlightSeqEnvKey] = 0
87
- return
88
- }
89
- delete env[customHighlightDataEnvKey]
90
- delete env[customHighlightSeqEnvKey]
91
- })
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
+ }
92
100
 
93
101
  md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
94
102
  const context = prepareFenceRenderContext(tokens, idx, opt)
@@ -8,6 +8,7 @@ import {
8
8
  commentLineClass,
9
9
  emitFenceDecision,
10
10
  finalizeFenceTimings,
11
+ getCommentLineFlags,
11
12
  getLogicalLineCount,
12
13
  getNowMs,
13
14
  normalizeEmphasisRanges,
@@ -22,7 +23,8 @@ import {
22
23
  parsePreCodeWrapper,
23
24
  } from '../utils/pre-code-wrapper-parser.js'
24
25
 
25
- const markupPassthroughDisabledFeatures = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes', 'samp'])
26
+ const markupHighlightPreDisabledFeatures = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes', 'samp'])
27
+ const markupPassthroughDisabledFeatures = Object.freeze([...markupHighlightPreDisabledFeatures, 'wrap', 'setPreWrapStyle'])
26
28
 
27
29
  const renderFenceMarkup = (context, md, opt, slf) => {
28
30
  const token = context.token
@@ -40,7 +42,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
40
42
  const lineNotes = context.lineNotes
41
43
  const lineNoteIdPrefix = context.lineNoteIdPrefix
42
44
 
43
- const isSamp = opt._sampReg.test(lang)
45
+ const isSamp = context.isSamp
44
46
  const sourceContent = token.content
45
47
  let content = sourceContent
46
48
  let commentLines
@@ -113,41 +115,48 @@ const renderFenceMarkup = (context, md, opt, slf) => {
113
115
 
114
116
  const useHighlightPre = opt.useHighlightPre && hasHighlightPre
115
117
  const needLineNumber = !useHighlightPre && opt.setLineNumber && startNumber >= 0
118
+ const wantsEmphasis = !useHighlightPre && opt.setEmphasizeLines && emphasizeLines.length > 0
119
+ const wantsAdvancedLineNumbers = needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)
120
+ const wantsLineNotes = !useHighlightPre && !!(lineNotes && lineNotes.length)
121
+ const hasCommentCandidate = !!(
122
+ !useHighlightPre &&
123
+ commentMarkValue &&
124
+ sourceContent.indexOf(commentMarkValue) !== -1
125
+ )
126
+ const needsMappedLineCounts = wantsAdvancedLineNumbers || wantsLineNotes || hasCommentCandidate
116
127
  let sourceLogicalLineCount = -1
117
128
  let highlightedLogicalLineCount = -1
118
- const ensureLogicalLineCounts = () => {
119
- if (sourceLogicalLineCount === -1) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
120
- if (highlightedLogicalLineCount === -1) highlightedLogicalLineCount = getLogicalLineCount(content)
121
- }
129
+ if (wantsEmphasis || needsMappedLineCounts) highlightedLogicalLineCount = getLogicalLineCount(content)
130
+ if (needsMappedLineCounts) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
131
+ const highlightedHasTrailingBreak = content.endsWith('\n') || content.endsWith('\r')
132
+ const needsLastLineWrapCheck = !highlightedHasTrailingBreak && (needLineNumber || wantsEmphasis || wantsLineNotes)
133
+ if (needsLastLineWrapCheck && highlightedLogicalLineCount < 0) highlightedLogicalLineCount = getLogicalLineCount(content)
134
+ if (needsLastLineWrapCheck && sourceLogicalLineCount < 0) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
135
+ const wrapLastLine = !!(
136
+ needsLastLineWrapCheck &&
137
+ highlightedLogicalLineCount > 0 &&
138
+ highlightedLogicalLineCount === sourceLogicalLineCount
139
+ )
140
+
122
141
  let normalizedEmphasis = []
123
- if (!useHighlightPre && opt.setEmphasizeLines && emphasizeLines.length > 0) {
124
- ensureLogicalLineCounts()
142
+ if (wantsEmphasis) {
125
143
  normalizedEmphasis = normalizeEmphasisRanges(emphasizeLines, highlightedLogicalLineCount)
126
144
  }
127
145
  let lineNumberPlan = null
128
- if (needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)) {
129
- ensureLogicalLineCounts()
146
+ if (wantsAdvancedLineNumbers) {
130
147
  lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, sourceLogicalLineCount, highlightedLogicalLineCount)
131
148
  }
132
149
  let lineNotePlan = null
133
- if (!useHighlightPre && lineNotes && lineNotes.length) {
134
- ensureLogicalLineCounts()
150
+ if (wantsLineNotes) {
135
151
  lineNotePlan = resolveLineNotesPlan(lineNotes, sourceLogicalLineCount, highlightedLogicalLineCount, lineNoteIdPrefix)
136
152
  }
137
153
  const needEmphasis = normalizedEmphasis.length > 0
138
154
  const needEndSpan = opt.lineEndSpanThreshold > 0
139
155
 
140
- if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
141
- ensureLogicalLineCounts()
156
+ if (hasCommentCandidate) {
142
157
  if (highlightedLogicalLineCount === sourceLogicalLineCount) {
143
- const rawLines = sourceContent.split('\n')
144
- for (let i = 0; i < rawLines.length; i++) {
145
- if (rawLines[i].trimStart().startsWith(commentMarkValue)) {
146
- if (!commentLines) commentLines = []
147
- commentLines[i] = true
148
- needComment = true
149
- }
150
- }
158
+ commentLines = getCommentLineFlags(sourceContent, commentMarkValue)
159
+ needComment = !!commentLines
151
160
  }
152
161
  }
153
162
 
@@ -168,16 +177,17 @@ const renderFenceMarkup = (context, md, opt, slf) => {
168
177
  commentLineClass,
169
178
  lineNumberPlan,
170
179
  lineNotePlan,
180
+ wrapLastLine,
171
181
  )
172
182
  if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
173
183
  }
174
184
 
175
- const tag = isSamp ? 'samp' : 'code'
185
+ const tag = useHighlightPre ? 'code' : (isSamp ? 'samp' : 'code')
176
186
  const decision = {
177
187
  renderer: 'markup',
178
188
  useHighlightPre,
179
189
  hasHighlightPre,
180
- disabledFeatures: useHighlightPre ? markupPassthroughDisabledFeatures : [],
190
+ disabledFeatures: useHighlightPre ? markupHighlightPreDisabledFeatures : [],
181
191
  }
182
192
  if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
183
193
  emitFenceDecision(opt, decision)