@peaceroad/markdown-it-renderer-fence 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +183 -430
  2. package/docs/README.md +37 -0
  3. package/docs/code-highlighting-design.md +317 -0
  4. package/docs/custom-highlight-styling-guide.md +204 -0
  5. package/package.json +22 -13
  6. package/src/custom-highlight/option-validator.js +19 -10
  7. package/src/custom-highlight/scope-name.js +23 -0
  8. package/src/custom-highlight/{shiki-keyword-rules.js → shiki-role-rules.js} +229 -44
  9. package/src/custom-highlight/{shiki-keyword.js → shiki-role.js} +64 -56
  10. package/src/custom-highlight/shiki-theme-role.js +176 -0
  11. package/src/entry/markup-highlight.js +4 -0
  12. package/src/fence/line-notes.js +199 -0
  13. package/src/fence/render-api-provider.js +62 -62
  14. package/src/fence/render-api-renderer.js +26 -11
  15. package/src/fence/render-api-runtime.js +5 -13
  16. package/src/fence/render-api.js +24 -12
  17. package/src/fence/render-markup.js +24 -16
  18. package/src/fence/render-shared.js +232 -20
  19. package/theme/_shared/rf-core.css +164 -0
  20. package/theme/rf-basic/README.md +91 -0
  21. package/theme/rf-basic/api/highlightjs.css +71 -0
  22. package/theme/rf-basic/api/shiki.css +67 -0
  23. package/theme/rf-basic/base.css +3 -0
  24. package/theme/rf-basic/index.css +7 -0
  25. package/theme/rf-basic/index.js +155 -0
  26. package/theme/rf-basic/markup/highlightjs.css +77 -0
  27. package/theme/rf-basic/markup/shiki.css +11 -0
  28. package/theme/rf-monochrome/README.md +71 -0
  29. package/theme/rf-monochrome/base.css +3 -0
  30. package/theme/rf-monochrome/index.css +5 -0
  31. package/theme/rf-monochrome/index.js +72 -0
  32. package/theme/rf-monochrome/markup/highlightjs.css +134 -0
  33. package/theme/rf-monochrome/markup/shiki.css +27 -0
  34. package/THIRD_PARTY_NOTICES.md +0 -56
@@ -8,18 +8,23 @@ import {
8
8
  commentLineClass,
9
9
  emitFenceDecision,
10
10
  finalizeFenceTimings,
11
+ getCommentLineFlags,
11
12
  getLogicalLineCount,
12
13
  getNowMs,
13
14
  normalizeEmphasisRanges,
14
15
  orderTokenAttrs,
15
16
  preWrapStyle,
16
17
  resolveAdvancedLineNumberPlan,
18
+ resolveLineNotesPlan,
17
19
  splitFenceBlockToLines,
20
+ wrapFencePreWithLineNotes,
18
21
  } from './render-shared.js'
19
22
  import {
20
23
  parsePreCodeWrapper,
21
24
  } from '../utils/pre-code-wrapper-parser.js'
22
25
 
26
+ const markupPassthroughDisabledFeatures = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes', 'samp'])
27
+
23
28
  const renderFenceMarkup = (context, md, opt, slf) => {
24
29
  const token = context.token
25
30
  const lang = context.lang
@@ -33,8 +38,10 @@ const renderFenceMarkup = (context, md, opt, slf) => {
33
38
  const wrapEnabled = context.wrapEnabled
34
39
  const preWrapValue = context.preWrapValue
35
40
  const commentMarkValue = context.commentMarkValue
41
+ const lineNotes = context.lineNotes
42
+ const lineNoteIdPrefix = context.lineNoteIdPrefix
36
43
 
37
- const isSamp = opt._sampReg.test(lang)
44
+ const isSamp = context.isSamp
38
45
  const sourceContent = token.content
39
46
  let content = sourceContent
40
47
  let commentLines
@@ -76,7 +83,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
76
83
  passthrough: true,
77
84
  passthroughReason: 'pre-code-parse-failed',
78
85
  hasHighlightPre: false,
79
- disabledFeatures: ['setLineNumber', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'],
86
+ disabledFeatures: markupPassthroughDisabledFeatures,
80
87
  }
81
88
  if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
82
89
  emitFenceDecision(opt, decision)
@@ -105,7 +112,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
105
112
  if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
106
113
  const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
107
114
 
108
- const needLineNumber = opt.setLineNumber && startNumber >= 0
115
+ const useHighlightPre = opt.useHighlightPre && hasHighlightPre
116
+ const needLineNumber = !useHighlightPre && opt.setLineNumber && startNumber >= 0
109
117
  let sourceLogicalLineCount = -1
110
118
  let highlightedLogicalLineCount = -1
111
119
  const ensureLogicalLineCounts = () => {
@@ -113,7 +121,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
113
121
  if (highlightedLogicalLineCount === -1) highlightedLogicalLineCount = getLogicalLineCount(content)
114
122
  }
115
123
  let normalizedEmphasis = []
116
- if (opt.setEmphasizeLines && emphasizeLines.length > 0) {
124
+ if (!useHighlightPre && opt.setEmphasizeLines && emphasizeLines.length > 0) {
117
125
  ensureLogicalLineCounts()
118
126
  normalizedEmphasis = normalizeEmphasisRanges(emphasizeLines, highlightedLogicalLineCount)
119
127
  }
@@ -122,25 +130,23 @@ const renderFenceMarkup = (context, md, opt, slf) => {
122
130
  ensureLogicalLineCounts()
123
131
  lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, sourceLogicalLineCount, highlightedLogicalLineCount)
124
132
  }
133
+ let lineNotePlan = null
134
+ if (!useHighlightPre && lineNotes && lineNotes.length) {
135
+ ensureLogicalLineCounts()
136
+ lineNotePlan = resolveLineNotesPlan(lineNotes, sourceLogicalLineCount, highlightedLogicalLineCount, lineNoteIdPrefix)
137
+ }
125
138
  const needEmphasis = normalizedEmphasis.length > 0
126
139
  const needEndSpan = opt.lineEndSpanThreshold > 0
127
- const useHighlightPre = opt.useHighlightPre && hasHighlightPre
128
140
 
129
141
  if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
130
142
  ensureLogicalLineCounts()
131
143
  if (highlightedLogicalLineCount === sourceLogicalLineCount) {
132
- const rawLines = sourceContent.split('\n')
133
- for (let i = 0; i < rawLines.length; i++) {
134
- if (rawLines[i].trimStart().startsWith(commentMarkValue)) {
135
- if (!commentLines) commentLines = []
136
- commentLines[i] = true
137
- needComment = true
138
- }
139
- }
144
+ commentLines = getCommentLineFlags(sourceContent, commentMarkValue)
145
+ needComment = !!commentLines
140
146
  }
141
147
  }
142
148
 
143
- if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
149
+ if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment || lineNotePlan)) {
144
150
  const splitStartedAt = timingEnabled ? getNowMs() : 0
145
151
  const nlIndex = content.indexOf('\n')
146
152
  const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
@@ -156,6 +162,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
156
162
  commentLines,
157
163
  commentLineClass,
158
164
  lineNumberPlan,
165
+ lineNotePlan,
159
166
  )
160
167
  if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
161
168
  }
@@ -165,11 +172,12 @@ const renderFenceMarkup = (context, md, opt, slf) => {
165
172
  renderer: 'markup',
166
173
  useHighlightPre,
167
174
  hasHighlightPre,
168
- disabledFeatures: useHighlightPre ? ['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'] : [],
175
+ disabledFeatures: useHighlightPre ? markupPassthroughDisabledFeatures : [],
169
176
  }
170
177
  if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
171
178
  emitFenceDecision(opt, decision)
172
- return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
179
+ const preHtml = `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>`
180
+ return wrapFencePreWithLineNotes(preHtml, lineNotePlan)
173
181
  }
174
182
 
175
183
  export {
@@ -4,20 +4,36 @@ import {
4
4
  getInfoAttr,
5
5
  getLangFromClassAttr,
6
6
  } from '../utils/attr-utils.js'
7
+ import {
8
+ getTokenLineNotes,
9
+ } from './line-notes.js'
7
10
 
8
11
  const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
9
12
  const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
10
13
  const preLineTag = '<span class="pre-line">'
11
14
  const preLineNoNumberClass = 'pre-line-no-number'
15
+ const preLineHasEndNoteClass = 'pre-line-has-end-note'
16
+ const preLineContentClass = 'pre-line-content'
12
17
  const emphOpenTag = '<span class="pre-lines-emphasis">'
13
18
  const commentLineClass = 'pre-line-comment'
19
+ const preWithLineNotesClass = 'pre-wrapper-line-notes'
20
+ const lineNoteLayerClass = 'pre-line-note-layer'
21
+ const lineNoteClass = 'pre-line-note'
14
22
  const closeTag = '</span>'
15
23
  const closeTagLen = closeTag.length
24
+ const defaultPreLineTags = { open: preLineTag, close: closeTag }
16
25
  const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
17
26
  const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
18
27
  const nonNegativeIntReg = /^\d+$/
19
28
  const positiveIntReg = /^[1-9]\d*$/
20
29
 
30
+ const getLineNoteIdPrefix = (token, idx) => {
31
+ const tokenMap = token && Array.isArray(token.map) ? token.map : null
32
+ const mapStart = tokenMap && Number.isSafeInteger(tokenMap[0]) ? tokenMap[0] + 1 : 0
33
+ const tokenIndex = Number.isSafeInteger(idx) ? idx + 1 : 0
34
+ return `pre-line-note-${mapStart}-${tokenIndex}`
35
+ }
36
+
21
37
  const getLineVisualLengthIgnoringTags = (line, threshold) => {
22
38
  let len = 0
23
39
  let inTag = false
@@ -87,8 +103,15 @@ const createCommonFenceOptionDefaults = (md) => {
87
103
  }
88
104
 
89
105
  const finalizeCommonFenceOption = (opt) => {
90
- 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)
91
113
  opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
114
+ opt._attrOrderRankCache = new Map()
92
115
  return opt
93
116
  }
94
117
 
@@ -115,6 +138,15 @@ const parsePositiveLineIndex = (val) => {
115
138
  return num
116
139
  }
117
140
 
141
+ const isTruthyAttrValue = (val) => val !== 'false' && val !== '0'
142
+
143
+ const escapeHtmlText = (value) => {
144
+ return String(value ?? '')
145
+ .replace(/&/g, '&amp;')
146
+ .replace(/</g, '&lt;')
147
+ .replace(/>/g, '&gt;')
148
+ }
149
+
118
150
  const getLogicalLineCount = (text) => {
119
151
  const str = String(text ?? '')
120
152
  if (!str) return 0
@@ -130,6 +162,62 @@ const getLogicalLineCount = (text) => {
130
162
  return count
131
163
  }
132
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
+
133
221
  const parseLineRangeList = (attrVal, parseValue) => {
134
222
  const str = String(attrVal ?? '')
135
223
  if (!str) return []
@@ -155,14 +243,8 @@ const parseLineRangeList = (attrVal, parseValue) => {
155
243
  return result
156
244
  }
157
245
 
158
- const parsePositiveLineNumber = (val) => {
159
- const num = Number(val)
160
- if (!Number.isFinite(num) || num <= 0) return null
161
- return num
162
- }
163
-
164
246
  const getEmphasizeLines = (attrVal) => {
165
- return parseLineRangeList(attrVal, parsePositiveLineNumber)
247
+ return parseLineRangeList(attrVal, parsePositiveLineIndex)
166
248
  }
167
249
 
168
250
  const getLineNumberSkipRanges = (attrVal) => {
@@ -254,22 +336,106 @@ const resolveAdvancedLineNumberPlan = (lineNumberSkipValue, lineNumberSetValue,
254
336
  return buildAdvancedLineNumberPlan(skipRanges, setEntries, sourceLineCount)
255
337
  }
256
338
 
257
- const getPreLineOpenTag = (lineNumberPlan, lineIndex) => {
258
- if (!lineNumberPlan) return preLineTag
259
- const hidden = !!(lineNumberPlan.hidden && lineNumberPlan.hidden[lineIndex])
260
- const setNumber = lineNumberPlan.sets ? lineNumberPlan.sets[lineIndex] : undefined
261
- if (!hidden && setNumber == null) return preLineTag
339
+ const createLineNotePlan = (lineNotes, maxLine, idPrefix) => {
340
+ if (!Array.isArray(lineNotes) || !lineNotes.length || maxLine <= 0) return null
341
+ const anchors = []
342
+ const items = []
343
+ const noteIdPrefix = String(idPrefix || 'pre-line-note')
344
+ let prevFrom = 0
345
+ let prevTo = 0
346
+ let needsSort = false
347
+ for (let i = 0; i < lineNotes.length; i++) {
348
+ const note = lineNotes[i]
349
+ if (!note || !Number.isSafeInteger(note.from) || !Number.isSafeInteger(note.to)) continue
350
+ let from = note.from
351
+ let to = note.to
352
+ if (from > to) {
353
+ const swap = from
354
+ from = to
355
+ to = swap
356
+ }
357
+ if (to < 1 || from > maxLine) continue
358
+ if (from < 1) from = 1
359
+ if (to > maxLine) to = maxLine
360
+ if (anchors[from - 1] !== undefined) return null
361
+ const lineCount = Number.isSafeInteger(note.lineCount) && note.lineCount > 0
362
+ ? note.lineCount
363
+ : Math.max(getLogicalLineCount(note.text), 1)
364
+ const item = {
365
+ from,
366
+ to,
367
+ label: from === to ? String(from) : `${from}-${to}`,
368
+ html: escapeHtmlText(note.text),
369
+ anchorName: `--pre-line-note-${from}`,
370
+ id: `${noteIdPrefix}-${from}`,
371
+ lineCount,
372
+ width: note.width || '',
373
+ }
374
+ anchors[from - 1] = item
375
+ items.push(item)
376
+ if (!needsSort && items.length > 1 && (from < prevFrom || (from === prevFrom && to < prevTo))) {
377
+ needsSort = true
378
+ }
379
+ prevFrom = from
380
+ prevTo = to
381
+ }
382
+ if (!items.length) return null
383
+
384
+ if (needsSort) items.sort((a, b) => a.from - b.from || a.to - b.to)
385
+ let canAnchor = true
386
+ let previousVisualEnd = 0
387
+ let overflowLines = 0
388
+ for (let i = 0; i < items.length; i++) {
389
+ const item = items[i]
390
+ const visualEnd = item.from + item.lineCount - 1
391
+ if (item.from <= previousVisualEnd) canAnchor = false
392
+ if (visualEnd > maxLine) overflowLines = Math.max(overflowLines, visualEnd - maxLine)
393
+ previousVisualEnd = visualEnd
394
+ }
395
+
396
+ return {
397
+ anchors,
398
+ items,
399
+ canAnchor,
400
+ overflowLines,
401
+ }
402
+ }
403
+
404
+ const resolveLineNotesPlan = (lineNotes, sourceLineCount, renderedLineCount, idPrefix) => {
405
+ if (!Array.isArray(lineNotes) || !lineNotes.length) return null
406
+ if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
407
+ if (sourceLineCount !== renderedLineCount) return null
408
+ return createLineNotePlan(lineNotes, sourceLineCount, idPrefix)
409
+ }
410
+
411
+ const buildPreLineTags = (hidden, setNumber, lineNote) => {
412
+ if (!hidden && setNumber == null && !lineNote) return defaultPreLineTags
262
413
  let tag = '<span class="pre-line'
263
414
  if (hidden) tag += ' ' + preLineNoNumberClass
415
+ if (lineNote) tag += ' ' + preLineHasEndNoteClass
264
416
  tag += '"'
265
417
  if (setNumber != null) tag += ` style="counter-set:pre-line-number ${setNumber};"`
266
- return tag + '>'
418
+ if (lineNote) {
419
+ tag += ` data-pre-line-note-from="${lineNote.from}"`
420
+ tag += ` data-pre-line-note-to="${lineNote.to}"`
421
+ }
422
+ tag += '>'
423
+ if (!lineNote) return { open: tag, close: closeTag }
424
+ const describedByAttr = lineNote.id ? ` aria-describedby="${lineNote.id}"` : ''
425
+ return {
426
+ open: `${tag}<span class="${preLineContentClass}" style="anchor-name:${lineNote.anchorName};"${describedByAttr}>`,
427
+ close: `${closeTag}${closeTag}`,
428
+ }
267
429
  }
268
430
 
269
- const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan) => {
431
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan, lineNotePlan) => {
270
432
  const lines = content.split(br)
271
433
  const max = lines.length
272
- const hasDynamicLineNumberPlan = !!lineNumberPlan
434
+ const lineNumberHidden = lineNumberPlan ? lineNumberPlan.hidden : null
435
+ const lineNumberSets = lineNumberPlan ? lineNumberPlan.sets : null
436
+ const lineNoteAnchors = lineNotePlan ? lineNotePlan.anchors : null
437
+ const hasLineNotes = !!lineNoteAnchors
438
+ const needLineWrap = needLineNumber || hasLineNotes
273
439
  let emIdx = 0
274
440
  let emStart = -1
275
441
  let emEnd = -1
@@ -286,6 +452,9 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
286
452
  let line = lines[n]
287
453
  const notLastLine = n < max - 1
288
454
  const doComment = commentLines && commentLines[n]
455
+ const hidden = !!(lineNumberHidden && lineNumberHidden[n])
456
+ const setNumber = lineNumberSets ? lineNumberSets[n] : undefined
457
+ const lineNote = hasLineNotes ? lineNoteAnchors[n] : null
289
458
  let hasLt = false
290
459
  let hasLtChecked = false
291
460
 
@@ -309,7 +478,7 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
309
478
  }
310
479
  }
311
480
 
312
- if (needLineNumber && notLastLine) {
481
+ if (needLineWrap && notLastLine) {
313
482
  if (!hasLtChecked) {
314
483
  hasLt = line.indexOf('<') !== -1
315
484
  hasLtChecked = true
@@ -339,8 +508,9 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
339
508
  line = `<span class="${commentClass}">` + line + closeTag
340
509
  }
341
510
 
342
- if (needLineNumber && notLastLine) {
343
- line = (hasDynamicLineNumberPlan ? getPreLineOpenTag(lineNumberPlan, n) : preLineTag) + line + closeTag
511
+ if (needLineWrap && notLastLine) {
512
+ const lineTags = buildPreLineTags(hidden, setNumber, lineNote)
513
+ line = lineTags.open + line + lineTags.close
344
514
  }
345
515
 
346
516
  if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
@@ -359,10 +529,33 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
359
529
  return lines.join(br)
360
530
  }
361
531
 
532
+ const renderLineNotesLayer = (lineNotePlan) => {
533
+ const items = lineNotePlan && lineNotePlan.items
534
+ if (!items || !items.length) return ''
535
+ let html = `<div class="${lineNoteLayerClass}">`
536
+ for (let i = 0; i < items.length; i++) {
537
+ const item = items[i]
538
+ let style = `position-anchor:${item.anchorName};`
539
+ if (item.width) style += ` --line-note-width:${item.width};`
540
+ html += `<div id="${item.id}" class="${lineNoteClass}" role="note" data-pre-line-note-from="${item.from}" data-pre-line-note-to="${item.to}" data-pre-line-note-label="${item.label}" style="${style}">${item.html}</div>`
541
+ }
542
+ html += '</div>'
543
+ return html
544
+ }
545
+
546
+ const wrapFencePreWithLineNotes = (preHtml, lineNotePlan) => {
547
+ const items = lineNotePlan && lineNotePlan.items
548
+ if (!items || !items.length) return preHtml + '\n'
549
+ let attrs = ` class="${preWithLineNotesClass}"`
550
+ if (lineNotePlan.canAnchor) attrs += ' data-pre-line-notes-layout="anchor"'
551
+ if (lineNotePlan.overflowLines > 0) attrs += ` style="--pre-line-note-overflow-lines:${lineNotePlan.overflowLines};"`
552
+ return `<div${attrs}>${preHtml}${renderLineNotesLayer(lineNotePlan)}</div>\n`
553
+ }
554
+
362
555
  const orderTokenAttrs = (token, opt) => {
363
556
  const attrs = token.attrs
364
557
  if (!attrs || attrs.length < 2) return
365
- const rankCache = new Map()
558
+ const rankCache = opt._attrOrderRankCache || new Map()
366
559
  const getRank = (name) => {
367
560
  let rank = rankCache.get(name)
368
561
  if (rank === undefined) {
@@ -402,6 +595,9 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
402
595
  let wrapEnabled = false
403
596
  let preWrapValue
404
597
  let commentMarkValue
598
+ let tagOverride = ''
599
+ const lineNotes = getTokenLineNotes(token)
600
+ const lineNoteIdPrefix = lineNotes && lineNotes.length ? getLineNoteIdPrefix(token, idx) : ''
405
601
  const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
406
602
 
407
603
  if (token.attrs) {
@@ -455,6 +651,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
455
651
  }
456
652
  break
457
653
  case 'data-pre-emphasis':
654
+ if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
458
655
  dataPreEmphasisIndex = newAttrs.length
459
656
  newAttrs.push(attr)
460
657
  break
@@ -491,6 +688,12 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
491
688
  sawCommentMark = true
492
689
  }
493
690
  break
691
+ case 'samp':
692
+ tagOverride = isTruthyAttrValue(val) ? 'samp' : ''
693
+ break
694
+ case 'code':
695
+ tagOverride = isTruthyAttrValue(val) ? 'code' : ''
696
+ break
494
697
  case 'line-number-skip':
495
698
  case 'pre-line-number-skip':
496
699
  lineNumberSkipValue = val
@@ -557,10 +760,13 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
557
760
  }
558
761
 
559
762
  if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
763
+ if (lineNotes && lineNotes.length) token.attrSet('data-pre-line-notes', 'true')
764
+ const isSamp = tagOverride ? tagOverride === 'samp' : opt._sampLangSet.has(lang)
560
765
 
561
766
  return {
562
767
  token,
563
768
  lang,
769
+ isSamp,
564
770
  startNumber,
565
771
  emphasizeLines,
566
772
  lineNumberSkipValue,
@@ -568,6 +774,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
568
774
  wrapEnabled,
569
775
  preWrapValue,
570
776
  commentMarkValue,
777
+ lineNotes,
778
+ lineNoteIdPrefix,
571
779
  timingEnabled,
572
780
  timings,
573
781
  fenceStartedAt,
@@ -582,6 +790,8 @@ export {
582
790
  emitFenceDecision,
583
791
  finalizeCommonFenceOption,
584
792
  finalizeFenceTimings,
793
+ getCommentLineFlags,
794
+ getCommentLineRanges,
585
795
  getLogicalLineCount,
586
796
  getNowMs,
587
797
  getEmphasizeLines,
@@ -590,5 +800,7 @@ export {
590
800
  preWrapStyle,
591
801
  prepareFenceRenderContext,
592
802
  resolveAdvancedLineNumberPlan,
803
+ resolveLineNotesPlan,
593
804
  splitFenceBlockToLines,
805
+ wrapFencePreWithLineNotes,
594
806
  }
@@ -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
+ }