@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,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,19 +273,20 @@ 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) => {
@@ -459,21 +461,21 @@ const hasShikiOffsets = (tokenLines) => {
459
461
  return false
460
462
  }
461
463
 
462
- const getShikiScopeName = (tok, lang, style, opt, preResolvedKeywordLang = '') => {
464
+ const getShikiScopeName = (tok, lang, style, opt, preResolvedRoleLang = '') => {
463
465
  const scopeMode = (opt && opt.shikiScopeMode) || 'auto'
464
466
  const rawScope = getShikiRawScopeName(tok)
465
- let customKeywordName = null
466
- const isKeywordScopeMode = scopeMode === 'keyword'
467
- if (isKeywordScopeMode && opt && typeof opt.shikiKeywordClassifier === 'function') {
467
+ let customRoleName = null
468
+ const isRoleScopeMode = scopeMode === 'role'
469
+ if (isRoleScopeMode && opt && typeof opt.shikiRoleClassifier === 'function') {
468
470
  try {
469
- const customName = opt.shikiKeywordClassifier(rawScope, tok, { lang, style, scopeMode })
470
- if (customName != null && customName !== '') customKeywordName = String(customName)
471
+ const customName = opt.shikiRoleClassifier(rawScope, tok, { lang, style, scopeMode })
472
+ if (customName != null && customName !== '') customRoleName = String(customName)
471
473
  } catch (e) {}
472
474
  }
473
- if (isKeywordScopeMode) {
474
- if (customKeywordName) return 'shiki-' + customKeywordName
475
- const bucket = classifyShikiScopeKeyword(rawScope, tok, lang, opt, preResolvedKeywordLang)
476
- return 'shiki-' + bucket
475
+ if (isRoleScopeMode) {
476
+ if (customRoleName) return 'shiki-role-' + customRoleName
477
+ const bucket = classifyShikiScopeRole(rawScope, tok, lang, opt, preResolvedRoleLang)
478
+ return 'shiki-role-' + bucket
477
479
  }
478
480
  if (scopeMode === 'semantic') {
479
481
  if (rawScope) return 'shiki-' + rawScope
@@ -515,11 +517,11 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
515
517
  scopeMode === 'auto' ||
516
518
  scopeMode === 'color' ||
517
519
  scopeMode === 'semantic' ||
518
- (scopeMode === 'keyword' && !!(opt && typeof opt.shikiKeywordClassifier === 'function'))
520
+ (scopeMode === 'role' && !!(opt && typeof opt.shikiRoleClassifier === 'function'))
519
521
  const needTokenStyle = includeScopeStyles || needStyleForScopeName
520
- let preResolvedKeywordLang = ''
521
- if (scopeMode === 'keyword' && (!opt || typeof opt.shikiKeywordLangResolver !== 'function')) {
522
- preResolvedKeywordLang = resolveShikiKeywordLangForFence(lang, opt)
522
+ let preResolvedRoleLang = ''
523
+ if (scopeMode === 'role' && (!opt || typeof opt.shikiRoleLangResolver !== 'function')) {
524
+ preResolvedRoleLang = resolveShikiRoleLangForFence(lang, opt)
523
525
  }
524
526
  let cursor = 0
525
527
  for (let i = 0; i < tokenLines.length; i++) {
@@ -534,7 +536,7 @@ const createRangesFromShikiTokens = (lang, tokenLines, opt) => {
534
536
  const start = hasOffset ? tok.offset : cursor
535
537
  const end = start + content.length
536
538
  const style = needTokenStyle ? getShikiTokenStyle(tok) : null
537
- const scope = getShikiScopeName(tok, lang, style, opt, preResolvedKeywordLang)
539
+ const scope = getShikiScopeName(tok, lang, style, opt, preResolvedRoleLang)
538
540
  entries.push({ scope, start, end, style })
539
541
  cursor = end
540
542
  }
@@ -551,6 +553,7 @@ const normalizeCustomProviderRanges = (result) => {
551
553
  const entries = []
552
554
  for (const range of source.ranges) {
553
555
  let scope
556
+ let scopeIndex = null
554
557
  let start
555
558
  let end
556
559
  let style
@@ -567,10 +570,11 @@ const normalizeCustomProviderRanges = (result) => {
567
570
  } else {
568
571
  continue
569
572
  }
570
- if (typeof scope === 'number' && scopes && scopes[scope] != null) scope = scopes[scope]
573
+ if (Number.isSafeInteger(scope)) scopeIndex = scope
574
+ if (scopeIndex != null && scopes && scopes[scopeIndex] != null) scope = scopes[scopeIndex]
571
575
  if (scope == null) continue
572
576
  if (!style && scopeStyles) {
573
- if (typeof scope === 'number' && Array.isArray(scopeStyles)) style = scopeStyles[scope]
577
+ if (scopeIndex != null && Array.isArray(scopeStyles)) style = scopeStyles[scopeIndex]
574
578
  else style = scopeStyles[String(scope)]
575
579
  }
576
580
  entries.push({ scope: String(scope), start, end, style: normalizeScopeStyle(style) })
@@ -609,7 +613,12 @@ const buildApiPayload = (entries, text, lang, engine, scopePrefix, includeScopeS
609
613
  }
610
614
  scopeKeyToIndex.set(key, scopeIndex)
611
615
  }
612
- ranges.push([scopeIndex, start, end])
616
+ const previousRange = ranges.length ? ranges[ranges.length - 1] : null
617
+ if (previousRange && previousRange[0] === scopeIndex && previousRange[2] === start) {
618
+ previousRange[2] = end
619
+ } else {
620
+ ranges.push([scopeIndex, start, end])
621
+ }
613
622
  }
614
623
  const payload = {
615
624
  v: customHighlightPayloadSchemaVersion,
@@ -629,8 +638,8 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
629
638
  const needsEmphasis = !!(setEmphasizeLines && emphasizeLines && emphasizeLines.length > 0)
630
639
  const needsCommentScan = !!(commentMarkValue && content.indexOf(commentMarkValue) !== -1)
631
640
  if (!needsEmphasis && !needsCommentScan) return
632
- const { lines, offsets } = getLogicalLinesAndOffsets(content)
633
- const maxLine = lines.length
641
+ const offsets = needsEmphasis ? getLogicalLineOffsets(content) : null
642
+ const maxLine = offsets ? offsets.length : 0
634
643
  if (needsEmphasis) {
635
644
  const normalized = normalizeEmphasisRanges(emphasizeLines, maxLine)
636
645
  for (const [s, e] of normalized) {
@@ -640,11 +649,11 @@ const pushLineFeatureRanges = (entries, content, emphasizeLines, setEmphasizeLin
640
649
  }
641
650
  }
642
651
  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
- }
652
+ const ranges = getCommentLineRanges(content, commentMarkValue)
653
+ if (!ranges) return
654
+ for (let i = 0; i < ranges.length; i++) {
655
+ const [start, end] = ranges[i]
656
+ entries.push({ scope: commentLineClass, start, end })
648
657
  }
649
658
  }
650
659
  }
@@ -28,7 +28,7 @@ import {
28
28
  renderFenceMarkup,
29
29
  } from './render-markup.js'
30
30
 
31
- const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold', 'line-notes'])
31
+ const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes'])
32
32
 
33
33
  let fallbackCustomHighlightSeq = 0
34
34
 
@@ -49,8 +49,7 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
49
49
  return preAttrs
50
50
  }
51
51
 
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)
52
+ const renderFenceApiOrPlain = (token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
54
53
  const tag = isSamp ? 'samp' : 'code'
55
54
  let content = md.utils.escapeHtml(token.content)
56
55
  const lineStrategy = opt.customHighlight.lineFeatureStrategy
@@ -119,6 +118,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
119
118
  const commentMarkValue = context.commentMarkValue
120
119
  const lineNotes = context.lineNotes
121
120
  const lineNoteIdPrefix = context.lineNoteIdPrefix
121
+ const isSamp = context.isSamp
122
122
 
123
123
  const apiDecisionBase = {
124
124
  renderer: 'api',
@@ -130,12 +130,13 @@ const getFenceHtml = (context, md, opt, slf, env) => {
130
130
  : [],
131
131
  }
132
132
  try {
133
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
133
+ const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
134
134
  if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
135
135
  emitFenceDecision(opt, apiDecisionBase)
136
136
  return html
137
137
  } catch (err) {
138
- if (opt.customHighlight.fallback === 'plain' && shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) {
138
+ if (!shouldApplyApiFallbackForReason(opt.customHighlight, 'provider-error')) throw err
139
+ if (opt.customHighlight.fallback === 'plain') {
139
140
  const fallbackDecision = {
140
141
  renderer: 'api',
141
142
  includePayload: false,
@@ -144,7 +145,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
144
145
  reason: 'provider-error',
145
146
  lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
146
147
  }
147
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
148
+ const html = renderFenceApiOrPlain(token, lang, isSamp, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
148
149
  if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
149
150
  emitFenceDecision(opt, fallbackDecision)
150
151
  return html
@@ -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,
@@ -40,7 +41,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
40
41
  const lineNotes = context.lineNotes
41
42
  const lineNoteIdPrefix = context.lineNoteIdPrefix
42
43
 
43
- const isSamp = opt._sampReg.test(lang)
44
+ const isSamp = context.isSamp
44
45
  const sourceContent = token.content
45
46
  let content = sourceContent
46
47
  let commentLines
@@ -140,14 +141,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
140
141
  if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
141
142
  ensureLogicalLineCounts()
142
143
  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
- }
144
+ commentLines = getCommentLineFlags(sourceContent, commentMarkValue)
145
+ needComment = !!commentLines
151
146
  }
152
147
  }
153
148
 
@@ -103,7 +103,13 @@ const createCommonFenceOptionDefaults = (md) => {
103
103
  }
104
104
 
105
105
  const finalizeCommonFenceOption = (opt) => {
106
- opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
106
+ const sampLangs = ['samp']
107
+ const extraSampLangs = String(opt.sampLang ?? '').split(',')
108
+ for (let i = 0; i < extraSampLangs.length; i++) {
109
+ const lang = extraSampLangs[i].trim()
110
+ if (lang) sampLangs.push(lang)
111
+ }
112
+ opt._sampLangSet = new Set(sampLangs)
107
113
  opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
108
114
  opt._attrOrderRankCache = new Map()
109
115
  return opt
@@ -132,6 +138,8 @@ const parsePositiveLineIndex = (val) => {
132
138
  return num
133
139
  }
134
140
 
141
+ const isTruthyAttrValue = (val) => val !== 'false' && val !== '0'
142
+
135
143
  const escapeHtmlText = (value) => {
136
144
  return String(value ?? '')
137
145
  .replace(/&/g, '&amp;')
@@ -154,6 +162,62 @@ const getLogicalLineCount = (text) => {
154
162
  return count
155
163
  }
156
164
 
165
+ const isLineStartWhitespaceCode = (code) => {
166
+ return code <= 32 || code === 160 || code === 0xFEFF
167
+ }
168
+
169
+ const lineStartsWithAfterIndent = (str, start, end, marker) => {
170
+ let pos = start
171
+ while (pos < end && isLineStartWhitespaceCode(str.charCodeAt(pos))) pos++
172
+ return str.startsWith(marker, pos)
173
+ }
174
+
175
+ const scanLogicalLineBounds = (str, onLine) => {
176
+ let lineStart = 0
177
+ let lineIndex = 0
178
+
179
+ for (let i = 0; i <= str.length; i++) {
180
+ const code = i < str.length ? str.charCodeAt(i) : 10
181
+ if (code !== 10 && code !== 13) continue
182
+ onLine(lineStart, i, lineIndex)
183
+ if (code === 13 && i + 1 < str.length && str.charCodeAt(i + 1) === 10) i++
184
+ lineStart = i + 1
185
+ lineIndex++
186
+ }
187
+ }
188
+
189
+ const getCommentLineFlags = (text, commentMarkValue) => {
190
+ const str = String(text ?? '')
191
+ const marker = String(commentMarkValue ?? '')
192
+ if (!str || !marker) return null
193
+ const flags = []
194
+ let hasComment = false
195
+
196
+ scanLogicalLineBounds(str, (lineStart, lineEnd, lineIndex) => {
197
+ if (lineStartsWithAfterIndent(str, lineStart, lineEnd, marker)) {
198
+ flags[lineIndex] = true
199
+ hasComment = true
200
+ }
201
+ })
202
+
203
+ return hasComment ? flags : null
204
+ }
205
+
206
+ const getCommentLineRanges = (text, commentMarkValue) => {
207
+ const str = String(text ?? '')
208
+ const marker = String(commentMarkValue ?? '')
209
+ if (!str || !marker) return null
210
+ const ranges = []
211
+
212
+ scanLogicalLineBounds(str, (lineStart, lineEnd) => {
213
+ if (lineEnd > lineStart && lineStartsWithAfterIndent(str, lineStart, lineEnd, marker)) {
214
+ ranges.push([lineStart, lineEnd])
215
+ }
216
+ })
217
+
218
+ return ranges.length ? ranges : null
219
+ }
220
+
157
221
  const parseLineRangeList = (attrVal, parseValue) => {
158
222
  const str = String(attrVal ?? '')
159
223
  if (!str) return []
@@ -179,14 +243,8 @@ const parseLineRangeList = (attrVal, parseValue) => {
179
243
  return result
180
244
  }
181
245
 
182
- const parsePositiveLineNumber = (val) => {
183
- const num = Number(val)
184
- if (!Number.isFinite(num) || num <= 0) return null
185
- return num
186
- }
187
-
188
246
  const getEmphasizeLines = (attrVal) => {
189
- return parseLineRangeList(attrVal, parsePositiveLineNumber)
247
+ return parseLineRangeList(attrVal, parsePositiveLineIndex)
190
248
  }
191
249
 
192
250
  const getLineNumberSkipRanges = (attrVal) => {
@@ -537,6 +595,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
537
595
  let wrapEnabled = false
538
596
  let preWrapValue
539
597
  let commentMarkValue
598
+ let tagOverride = ''
540
599
  const lineNotes = getTokenLineNotes(token)
541
600
  const lineNoteIdPrefix = lineNotes && lineNotes.length ? getLineNoteIdPrefix(token, idx) : ''
542
601
  const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
@@ -592,6 +651,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
592
651
  }
593
652
  break
594
653
  case 'data-pre-emphasis':
654
+ if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
595
655
  dataPreEmphasisIndex = newAttrs.length
596
656
  newAttrs.push(attr)
597
657
  break
@@ -628,6 +688,12 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
628
688
  sawCommentMark = true
629
689
  }
630
690
  break
691
+ case 'samp':
692
+ tagOverride = isTruthyAttrValue(val) ? 'samp' : ''
693
+ break
694
+ case 'code':
695
+ tagOverride = isTruthyAttrValue(val) ? 'code' : ''
696
+ break
631
697
  case 'line-number-skip':
632
698
  case 'pre-line-number-skip':
633
699
  lineNumberSkipValue = val
@@ -695,10 +761,12 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
695
761
 
696
762
  if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
697
763
  if (lineNotes && lineNotes.length) token.attrSet('data-pre-line-notes', 'true')
764
+ const isSamp = tagOverride ? tagOverride === 'samp' : opt._sampLangSet.has(lang)
698
765
 
699
766
  return {
700
767
  token,
701
768
  lang,
769
+ isSamp,
702
770
  startNumber,
703
771
  emphasizeLines,
704
772
  lineNumberSkipValue,
@@ -722,6 +790,8 @@ export {
722
790
  emitFenceDecision,
723
791
  finalizeCommonFenceOption,
724
792
  finalizeFenceTimings,
793
+ getCommentLineFlags,
794
+ getCommentLineRanges,
725
795
  getLogicalLineCount,
726
796
  getNowMs,
727
797
  getEmphasizeLines,
@@ -0,0 +1,164 @@
1
+ /*! Renderer Fence shared base | MIT License */
2
+
3
+ :root {
4
+ color-scheme: light dark;
5
+
6
+ --rf-code-bg: oklch(99.4% 0 0);
7
+ --rf-code-fg: oklch(4% 0 0);
8
+ --rf-code-muted: oklch(42% 0 0);
9
+ --rf-code-border: oklch(86% 0 0);
10
+
11
+ --rf-samp-bg: oklch(18% 0 0);
12
+ --rf-samp-fg: oklch(95% 0 0);
13
+ --rf-samp-muted: oklch(76% 0 0);
14
+ --rf-samp-border: oklch(41% 0 0);
15
+ --rf-samp-prompt: oklch(78% 0.13 285);
16
+
17
+ --rf-accent: oklch(50% 0.18 285);
18
+ --rf-accent-soft: oklch(94% 0.035 285);
19
+ --rf-accent-border: oklch(82% 0.08 285);
20
+
21
+ --rf-syntax-keyword: oklch(38% 0.255 25);
22
+ --rf-syntax-string: oklch(34% 0.235 250);
23
+ --rf-syntax-number: oklch(36% 0.245 260);
24
+ --rf-syntax-comment: oklch(39% 0.026 255);
25
+ --rf-syntax-type: oklch(38% 0.235 295);
26
+ --rf-syntax-title: oklch(39% 0.27 305);
27
+ --rf-syntax-variable: oklch(37% 0.205 80);
28
+ --rf-syntax-literal: oklch(36% 0.245 260);
29
+ --rf-syntax-tag: oklch(34% 0.18 150);
30
+ --rf-syntax-attribute: oklch(36% 0.23 260);
31
+ --rf-syntax-punctuation: oklch(12% 0.018 255);
32
+ --rf-syntax-meta: oklch(12% 0.018 255);
33
+
34
+ --rf-samp-syntax-keyword: oklch(78% 0.08 25);
35
+ --rf-samp-syntax-string: oklch(82% 0.055 250);
36
+ --rf-samp-syntax-number: oklch(80% 0.07 255);
37
+ --rf-samp-syntax-comment: oklch(75% 0.01 255);
38
+ --rf-samp-syntax-type: oklch(81% 0.06 295);
39
+ --rf-samp-syntax-title: oklch(82% 0.07 305);
40
+ --rf-samp-syntax-variable: oklch(82% 0.075 90);
41
+ --rf-samp-syntax-literal: oklch(80% 0.07 255);
42
+ --rf-samp-syntax-tag: oklch(81% 0.065 150);
43
+ --rf-samp-syntax-attribute: oklch(80% 0.07 255);
44
+ --rf-samp-syntax-punctuation: oklch(86% 0.012 255);
45
+
46
+ --rf-line-emphasis-bg: color-mix(in oklch, var(--rf-accent-soft) 70%, transparent);
47
+ --rf-line-number-fg: var(--rf-code-muted);
48
+ --rf-line-number-rule: color-mix(in oklch, var(--rf-code-border) 85%, transparent);
49
+ --rf-line-comment-bg: color-mix(in oklch, var(--rf-syntax-comment) 12%, transparent);
50
+ --rf-line-note-bg: oklch(100% 0 0);
51
+ --rf-line-note-border: var(--rf-accent-border);
52
+ --rf-line-note-fg: var(--rf-code-muted);
53
+ }
54
+
55
+ @media (prefers-color-scheme: dark) {
56
+ :root {
57
+ --rf-code-bg: oklch(18% 0 0);
58
+ --rf-code-fg: oklch(95% 0 0);
59
+ --rf-code-muted: oklch(76% 0 0);
60
+ --rf-code-border: oklch(38% 0 0);
61
+
62
+ --rf-samp-bg: oklch(99.4% 0 0);
63
+ --rf-samp-fg: oklch(4% 0 0);
64
+ --rf-samp-muted: oklch(42% 0 0);
65
+ --rf-samp-border: oklch(86% 0 0);
66
+ --rf-samp-prompt: oklch(50% 0.18 285);
67
+
68
+ --rf-accent: oklch(78% 0.13 285);
69
+ --rf-accent-soft: oklch(27% 0.05 285);
70
+ --rf-accent-border: oklch(48% 0.11 285);
71
+
72
+ --rf-syntax-keyword: oklch(78% 0.08 25);
73
+ --rf-syntax-string: oklch(82% 0.055 250);
74
+ --rf-syntax-number: oklch(80% 0.07 255);
75
+ --rf-syntax-comment: oklch(75% 0.01 255);
76
+ --rf-syntax-type: oklch(81% 0.06 295);
77
+ --rf-syntax-title: oklch(82% 0.07 305);
78
+ --rf-syntax-variable: oklch(82% 0.075 90);
79
+ --rf-syntax-literal: oklch(80% 0.07 255);
80
+ --rf-syntax-tag: oklch(81% 0.065 150);
81
+ --rf-syntax-attribute: oklch(80% 0.07 255);
82
+ --rf-syntax-punctuation: oklch(86% 0.012 255);
83
+ --rf-syntax-meta: oklch(86% 0.012 255);
84
+
85
+ --rf-samp-syntax-keyword: oklch(38% 0.255 25);
86
+ --rf-samp-syntax-string: oklch(34% 0.235 250);
87
+ --rf-samp-syntax-number: oklch(36% 0.245 260);
88
+ --rf-samp-syntax-comment: oklch(39% 0.026 255);
89
+ --rf-samp-syntax-type: oklch(38% 0.235 295);
90
+ --rf-samp-syntax-title: oklch(39% 0.27 305);
91
+ --rf-samp-syntax-variable: oklch(37% 0.205 80);
92
+ --rf-samp-syntax-literal: oklch(36% 0.245 260);
93
+ --rf-samp-syntax-tag: oklch(34% 0.18 150);
94
+ --rf-samp-syntax-attribute: oklch(36% 0.23 260);
95
+ --rf-samp-syntax-punctuation: oklch(12% 0.018 255);
96
+
97
+ --rf-line-emphasis-bg: color-mix(in oklch, var(--rf-accent-soft) 70%, transparent);
98
+ --rf-line-number-fg: var(--rf-code-muted);
99
+ --rf-line-number-rule: color-mix(in oklch, var(--rf-code-border) 85%, transparent);
100
+ --rf-line-comment-bg: color-mix(in oklch, var(--rf-syntax-comment) 16%, transparent);
101
+ --rf-line-note-bg: oklch(24% 0.01 255);
102
+ --rf-line-note-border: var(--rf-accent-border);
103
+ --rf-line-note-fg: var(--rf-code-muted);
104
+ }
105
+ }
106
+
107
+ pre {
108
+ margin: 0;
109
+ padding: 1rem;
110
+ overflow: auto;
111
+ color: var(--rf-code-fg);
112
+ background: var(--rf-code-bg);
113
+ border: 1px solid var(--rf-code-border);
114
+ border-radius: 0.5rem;
115
+ }
116
+
117
+ pre code,
118
+ pre samp {
119
+ color: inherit;
120
+ background: transparent;
121
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
122
+ }
123
+
124
+ .pre-lines-emphasis {
125
+ background: var(--rf-line-emphasis-bg);
126
+ }
127
+
128
+ .pre-line-comment {
129
+ color: var(--rf-syntax-comment);
130
+ background: var(--rf-line-comment-bg);
131
+ padding-block: 0.04em;
132
+ line-height: 1.45;
133
+ -webkit-box-decoration-break: clone;
134
+ box-decoration-break: clone;
135
+ }
136
+
137
+ pre:has(> samp) {
138
+ --rf-syntax-keyword: var(--rf-samp-syntax-keyword);
139
+ --rf-syntax-string: var(--rf-samp-syntax-string);
140
+ --rf-syntax-number: var(--rf-samp-syntax-number);
141
+ --rf-syntax-comment: var(--rf-samp-syntax-comment);
142
+ --rf-syntax-type: var(--rf-samp-syntax-type);
143
+ --rf-syntax-title: var(--rf-samp-syntax-title);
144
+ --rf-syntax-variable: var(--rf-samp-syntax-variable);
145
+ --rf-syntax-literal: var(--rf-samp-syntax-literal);
146
+ --rf-syntax-tag: var(--rf-samp-syntax-tag);
147
+ --rf-syntax-attribute: var(--rf-samp-syntax-attribute);
148
+ --rf-syntax-punctuation: var(--rf-samp-syntax-punctuation);
149
+ --rf-syntax-meta: var(--rf-samp-syntax-punctuation);
150
+ --line-number-color: var(--rf-samp-muted);
151
+ --line-number-rule-color: var(--rf-samp-border);
152
+ --line-note-color: var(--rf-samp-muted);
153
+ --line-note-label-color: var(--rf-samp-muted);
154
+ --line-note-border-color: var(--rf-samp-border);
155
+ --line-note-background: color-mix(in oklch, var(--rf-samp-bg) 92%, var(--rf-samp-fg));
156
+ color: var(--rf-samp-fg);
157
+ background: var(--rf-samp-bg);
158
+ border-color: var(--rf-samp-border);
159
+ }
160
+
161
+ pre:has(> samp) .pre-line-comment {
162
+ color: var(--rf-samp-muted);
163
+ background: color-mix(in oklch, var(--rf-samp-muted) 18%, transparent);
164
+ }