@peaceroad/markdown-it-renderer-fence 0.6.1 → 0.7.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.
package/README.md CHANGED
@@ -120,6 +120,7 @@ Note:
120
120
  - optional line-end spacer via `lineEndSpanThreshold`.
121
121
  - optional pre-wrap support via `wrap` / `pre-wrap`.
122
122
  - comment line markers via `comment-mark`.
123
+ - sidecar line notes via immediate `line-notes` fence (`notes` alias).
123
124
 
124
125
  ### Fence Attribute Examples
125
126
 
@@ -229,6 +230,25 @@ echo 1
229
230
  </samp></pre>
230
231
  ```
231
232
 
233
+ Line notes:
234
+
235
+ ~~~md
236
+ ```js {start="5"}
237
+ const a = 1
238
+ console.log(a)
239
+ ```
240
+ ```line-notes
241
+ 1: setup {width="7em"}
242
+ 2:result {width="10em"}
243
+ ```
244
+ ~~~
245
+
246
+ ```html
247
+ <div class="pre-wrapper-line-notes" data-pre-line-notes-layout="anchor"><pre><code class="language-js" data-pre-start="5" data-pre-line-notes="true" style="counter-set:pre-line-number 5;"><span class="pre-line pre-line-has-end-note" data-pre-line-note-from="1" data-pre-line-note-to="1"><span class="pre-line-content" style="anchor-name:--pre-line-note-1;" aria-describedby="pre-line-note-1-1-1">const a = 1</span></span>
248
+ <span class="pre-line pre-line-has-end-note" data-pre-line-note-from="2" data-pre-line-note-to="2"><span class="pre-line-content" style="anchor-name:--pre-line-note-2;" aria-describedby="pre-line-note-1-1-2">console.log(a)</span></span>
249
+ </code></pre><div class="pre-line-note-layer"><div id="pre-line-note-1-1-1" class="pre-line-note" role="note" data-pre-line-note-from="1" data-pre-line-note-to="1" data-pre-line-note-label="1" style="position-anchor:--pre-line-note-1; --line-note-width:7em;">setup</div><div id="pre-line-note-1-1-2" class="pre-line-note" role="note" data-pre-line-note-from="2" data-pre-line-note-to="2" data-pre-line-note-label="2" style="position-anchor:--pre-line-note-2; --line-note-width:10em;">result</div></div></div>
250
+ ```
251
+
232
252
  ### Markup Notes
233
253
 
234
254
  - `useHighlightPre: true` keeps highlighter-provided `<pre><code>` when present.
@@ -239,6 +259,7 @@ echo 1
239
259
  - `em-lines`
240
260
  - line-end spacer
241
261
  - `comment-mark`
262
+ - `line-notes`
242
263
  - `samp` conversion
243
264
 
244
265
  Reference line-number CSS contract:
@@ -268,6 +289,8 @@ Recommended display CSS:
268
289
 
269
290
  - See [`example/line-number.css`](./example/line-number.css) for the full reference stylesheet.
270
291
  - See [`example/line-number-sample.html`](./example/line-number-sample.html) for Markdown / Preview / HTML side-by-side examples.
292
+ - See [`example/line-notes.css`](./example/line-notes.css) for the companion line-note stylesheet.
293
+ - See [`example/line-notes-sample.html`](./example/line-notes-sample.html) for sidecar line-note examples.
271
294
 
272
295
  Note:
273
296
 
@@ -307,6 +330,25 @@ Line-number attr syntax note:
307
330
  - `line-number-skip` and `line-number-set` must not target the same source line. If they overlap, the skipped line stays blank and the conflicting `line-number-set` entry is ignored.
308
331
  - `line-number-skip` / `line-number-set` are applied only when source and highlighted logical line counts match; otherwise renderer falls back to plain sequential numbering.
309
332
 
333
+ Line-note syntax note:
334
+
335
+ - `line-notes` is the canonical sidecar fence name; `notes` is a short alias.
336
+ - Sidecar notes are folded only when they immediately follow a code/samp fence.
337
+ - Immediate sidecar folding is fail-closed: if any non-empty line is malformed, a note entry is incomplete, or the same note start line appears twice, the `line-notes` fence stays as a normal literal fence block.
338
+ - Syntax supports `N: text`, `N-M: text`, `N:text`, and `N-M:text`.
339
+ - Per-note attrs can be appended in markdown-it-attrs style on the same line for single-line notes, for example `5: cache lookup key {width="8rem"}`.
340
+ - For multiline notes, prefer a trailing attrs-only continuation line, for example ` {width="11rem"}`.
341
+ - Continuation lines start with indentation (` ` or tab) and append multiline note text to the previous note.
342
+ - Output keeps note text outside `<code>/<samp>` in a sibling note layer for safer copy semantics.
343
+ - Line-note blocks use the external wrapper contract `pre-wrapper-line-notes`.
344
+ - `data-pre-line-notes-layout="anchor"` is added only for simple non-overlapping note layouts. The current check is intentionally lightweight: it uses the note start line plus the note text's explicit logical line count, so a two-line note starting at line `3` prevents another anchored note from starting at line `4`.
345
+ - This anchor-safety check does not measure CSS wrapping. If a note wraps because of narrow CSS width, consumer CSS should still be prepared to fall back to a below-block list.
346
+ - When anchor positioning is unavailable or the layout is not marked safe, consumer CSS should render notes as a below-block list. The reference stylesheet uses `data-pre-line-note-label` so the fallback still shows which source line each note belongs to.
347
+ - In anchor layouts, the rendered line HTML carries note metadata on `.pre-line`, and the anchor itself is attached to `.pre-line-content` so notes can start from the rendered code end.
348
+ - Accessibility contract: the rendered `.pre-line-content` carries `aria-describedby`, and each note item renders with a stable `id` plus `role="note"`, so assistive tech can associate the note text with its anchor line.
349
+ - When a sidecar fence is folded, the previous fence token `map` is extended to cover the absorbed note-fence lines so editor scroll/jump integrations can still treat the merged output as one source span.
350
+ - `line-notes` are applied only when source and highlighted logical line counts match; otherwise note markup is skipped.
351
+
310
352
  Migration note (`0.5.0`):
311
353
 
312
354
  - `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
@@ -537,6 +579,7 @@ Then include that bridge in generated HTML:
537
579
  - `customHighlight.idPrefix` (default: `'hl-'`): generated per-block payload id prefix.
538
580
  - `customHighlight.scopePrefix` (default: `'hl'`): scope name prefix used for highlight registration.
539
581
  - `customHighlight.lineFeatureStrategy` (`'hybrid' | 'disable'`, default: `'hybrid'`): keep or disable line-span features in API rendering.
582
+ - In `NODE_ENV=development`, invalid known enum-like values and unknown `customHighlight` keys emit warn-once diagnostics while keeping normalized fallback behavior.
540
583
 
541
584
  ### API Helper Exports
542
585
 
@@ -550,11 +593,19 @@ Then include that bridge in generated HTML:
550
593
  - `customHighlightPayloadSchemaVersion`
551
594
  - `customHighlightPayloadSupportedVersions`
552
595
 
596
+ ### Runtime Version Policy
597
+
598
+ - `applyCustomHighlights(..., { strictVersion: true })` accepts only `customHighlightPayloadSupportedVersions` (currently `v:1`).
599
+ - `supportedVersion` / `supportedVersions` allow explicit accepted payload versions.
600
+ - If `strictVersion` is `true`, it takes precedence and custom accepted versions are ignored.
601
+
553
602
  ## Docs and Examples
554
603
 
555
604
  - API styling guide: `docs/custom-highlight-styling-guide.md`
556
605
  - default preset CSS sample: `docs/default-highlight-theme.css`
557
606
  - provider matrix page: `example/custom-highlight-provider-matrix.html`
607
+ - line-number sample: `example/line-number-sample.html`
608
+ - line-notes sample: `example/line-notes-sample.html`
558
609
 
559
610
  ## Tests and Benchmarks
560
611
 
@@ -565,8 +616,6 @@ Then include that bridge in generated HTML:
565
616
  - performance baseline: `npm run test:performance`
566
617
  - runtime apply benchmark: `npm run test:performance:runtime`
567
618
 
568
- ## License and Notices
619
+ ## License
569
620
 
570
621
  - Project license: MIT (`LICENSE`)
571
- - Third-party notices: `THIRD_PARTY_NOTICES.md`
572
- - `docs/default-highlight-theme.css` contains mappings derived from highlight.js theme families (BSD-3-Clause; see notices).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-renderer-fence",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "In code blocks, the `<samp>` tag is used with the language keywords `samp`, `shell`, and `console`, and provides an option to display line numbers.",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -13,7 +13,6 @@
13
13
  "index.js",
14
14
  "src",
15
15
  "LICENSE",
16
- "THIRD_PARTY_NOTICES.md",
17
16
  "README.md"
18
17
  ],
19
18
  "type": "module",
@@ -42,10 +41,10 @@
42
41
  "repository": "https://github.com/peaceroad/p7d-markdown-it-renderer-fence",
43
42
  "license": "MIT",
44
43
  "devDependencies": {
45
- "@peaceroad/markdown-it-figure-with-p-caption": "^0.16.0",
44
+ "@peaceroad/markdown-it-figure-with-p-caption": "^0.17.0",
46
45
  "highlight.js": "^11.11.1",
47
- "markdown-it": "^14.1.0",
46
+ "markdown-it": "^14.1.1",
48
47
  "markdown-it-attrs": "^4.3.1",
49
- "shiki": "^3.21.0"
48
+ "shiki": "^4.0.2"
50
49
  }
51
50
  }
@@ -42,6 +42,15 @@ const knownCustomHighlightKeys = new Set([
42
42
  'theme',
43
43
  ])
44
44
 
45
+ const isExactValidOptionValue = (value, validSet) => {
46
+ return typeof value === 'string' && validSet.has(value)
47
+ }
48
+
49
+ const isNormalizedValidOptionValue = (value, validSet) => {
50
+ if (typeof value !== 'string') return false
51
+ return validSet.has(value.trim().toLowerCase())
52
+ }
53
+
45
54
  const validateCustomHighlightOptions = (rawOpt, normalizedOpt) => {
46
55
  if (!rawOpt || typeof rawOpt !== 'object') return
47
56
 
@@ -54,31 +63,31 @@ const validateCustomHighlightOptions = (rawOpt, normalizedOpt) => {
54
63
  }
55
64
  }
56
65
 
57
- if (rawOpt.provider != null && !validProviderSet.has(normalizedOpt.provider)) {
66
+ if (rawOpt.provider != null && !isExactValidOptionValue(rawOpt.provider, validProviderSet)) {
58
67
  warnOptionIssueOnce(
59
68
  'provider',
60
69
  `Invalid customHighlight.provider "${String(rawOpt.provider)}". Using "${normalizedOpt.provider}".`,
61
70
  )
62
71
  }
63
- if (rawOpt.fallback != null && !validFallbackSet.has(normalizedOpt.fallback)) {
72
+ if (rawOpt.fallback != null && !isExactValidOptionValue(rawOpt.fallback, validFallbackSet)) {
64
73
  warnOptionIssueOnce(
65
74
  'fallback',
66
75
  `Invalid customHighlight.fallback "${String(rawOpt.fallback)}". Using "${normalizedOpt.fallback}".`,
67
76
  )
68
77
  }
69
- if (rawOpt.transport != null && !validTransportSet.has(normalizedOpt.transport)) {
78
+ if (rawOpt.transport != null && !isExactValidOptionValue(rawOpt.transport, validTransportSet)) {
70
79
  warnOptionIssueOnce(
71
80
  'transport',
72
81
  `Invalid customHighlight.transport "${String(rawOpt.transport)}". Using "${normalizedOpt.transport}".`,
73
82
  )
74
83
  }
75
- if (rawOpt.lineFeatureStrategy != null && !validLineFeatureStrategySet.has(normalizedOpt.lineFeatureStrategy)) {
84
+ if (rawOpt.lineFeatureStrategy != null && !isExactValidOptionValue(rawOpt.lineFeatureStrategy, validLineFeatureStrategySet)) {
76
85
  warnOptionIssueOnce(
77
86
  'line-feature-strategy',
78
87
  `Invalid customHighlight.lineFeatureStrategy "${String(rawOpt.lineFeatureStrategy)}". Using "${normalizedOpt.lineFeatureStrategy}".`,
79
88
  )
80
89
  }
81
- if (rawOpt.shikiScopeMode != null && !validShikiScopeModeSet.has(normalizedOpt.shikiScopeMode)) {
90
+ if (rawOpt.shikiScopeMode != null && !isNormalizedValidOptionValue(rawOpt.shikiScopeMode, validShikiScopeModeSet)) {
82
91
  warnOptionIssueOnce(
83
92
  'shiki-scope-mode',
84
93
  `Invalid customHighlight.shikiScopeMode "${String(rawOpt.shikiScopeMode)}". Using "${normalizedOpt.shikiScopeMode}".`,
@@ -0,0 +1,23 @@
1
+ const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
2
+ const hyphenMultiReg = /-+/g
3
+
4
+ const sanitizeHighlightNamePart = (value, fallback = '') => {
5
+ let safe = String(value == null ? '' : value)
6
+ .replace(highlightNameUnsafeReg, '-')
7
+ .replace(hyphenMultiReg, '-')
8
+ .replace(/^-+|-+$/g, '')
9
+ if (!safe) safe = fallback
10
+ if (/^[0-9]/.test(safe)) safe = 'x-' + safe
11
+ if (safe.startsWith('--')) safe = safe.slice(2) || fallback
12
+ return safe
13
+ }
14
+
15
+ const sanitizeHighlightName = (name, prefix = '') => {
16
+ const safe = sanitizeHighlightNamePart(name, 'scope')
17
+ const prefixBase = sanitizeHighlightNamePart(prefix, '')
18
+ return prefixBase ? `${prefixBase}-${safe}` : safe
19
+ }
20
+
21
+ export {
22
+ sanitizeHighlightName,
23
+ }
@@ -4,6 +4,9 @@ import {
4
4
  finalizeCommonFenceOption,
5
5
  prepareFenceRenderContext,
6
6
  } from '../fence/render-shared.js'
7
+ import {
8
+ installLineNotesCoreRule,
9
+ } from '../fence/line-notes.js'
7
10
  import {
8
11
  renderFenceMarkup,
9
12
  } from '../fence/render-markup.js'
@@ -17,6 +20,7 @@ const mditRendererFenceMarkup = (md, option) => {
17
20
  }
18
21
 
19
22
  finalizeCommonFenceOption(opt)
23
+ installLineNotesCoreRule(md)
20
24
 
21
25
  md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
22
26
  const context = prepareFenceRenderContext(tokens, idx, opt)
@@ -0,0 +1,181 @@
1
+ import {
2
+ getInfoAttr,
3
+ } from '../utils/attr-utils.js'
4
+
5
+ const lineNotesRuleName = 'renderer_fence_line_notes'
6
+ const lineNotesMetaKey = '__rendererFenceLineNotes'
7
+ const lineNotesInstalledKey = '__rendererFenceLineNotesInstalled'
8
+ const fenceInfoNameReg = /^([^{\s]*)/
9
+ const lineNoteHeaderReg = /^\s*([1-9]\d*)(?:\s*-\s*([1-9]\d*))?\s*[::]\s*(.*)$/
10
+ const lineNoteContinuationReg = /^(?: {2,}|\t)/
11
+ const lineNoteFenceNameSet = new Set(['line-notes', 'notes'])
12
+ const lineNoteAttrsReg = /^(.*?)(?:\s+\{([^{}]+)\})\s*$/
13
+ const lineNoteAttrsOnlyReg = /^\{([^{}]+)\}\s*$/
14
+ const lineNoteWidthReg = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|ch|vw|svw|lvw|dvw|%)$/
15
+
16
+ const getFenceInfoName = (info) => {
17
+ const match = String(info ?? '').trim().match(fenceInfoNameReg)
18
+ return match ? match[1] : ''
19
+ }
20
+
21
+ const stripContinuationIndent = (line) => {
22
+ if (!line) return ''
23
+ if (line[0] === '\t') return line.slice(1)
24
+ if (line.startsWith(' ')) return line.slice(2)
25
+ return line
26
+ }
27
+
28
+ const parseValidatedLineNoteWidth = (attrText) => {
29
+ if (!attrText || attrText.indexOf('width') === -1) return ''
30
+ const attrs = getInfoAttr(attrText)
31
+ if (!attrs.length) return ''
32
+ for (let i = 0; i < attrs.length; i++) {
33
+ const name = attrs[i][0]
34
+ const value = String(attrs[i][1] || '').trim()
35
+ if (name !== 'width') continue
36
+ if (lineNoteWidthReg.test(value)) return value
37
+ }
38
+ return ''
39
+ }
40
+
41
+ const parseLineNoteHeaderText = (text) => {
42
+ const str = String(text ?? '')
43
+ if (str.indexOf('{') === -1 || str.indexOf('}') === -1) {
44
+ return { text: str, width: '' }
45
+ }
46
+ const match = str.match(lineNoteAttrsReg)
47
+ if (!match) return { text: str, width: '' }
48
+ return {
49
+ text: match[1].trimEnd(),
50
+ width: parseValidatedLineNoteWidth(match[2]),
51
+ }
52
+ }
53
+
54
+ const parseLineNoteAttrsOnlyText = (text) => {
55
+ const str = String(text ?? '').trim()
56
+ if (str[0] !== '{' || str[str.length - 1] !== '}') return null
57
+ const match = str.match(lineNoteAttrsOnlyReg)
58
+ if (!match) return null
59
+ const width = parseValidatedLineNoteWidth(match[1])
60
+ return width ? { width } : null
61
+ }
62
+
63
+ const parseLineNotesContent = (content) => {
64
+ const raw = String(content ?? '')
65
+ if (!raw || (raw.indexOf(':') === -1 && raw.indexOf(':') === -1)) return []
66
+ const lines = raw.split('\n')
67
+ const notes = []
68
+ const seenStarts = new Set()
69
+ let current = null
70
+
71
+ const pushCurrent = () => {
72
+ if (!current) return true
73
+ if (!current.lines.length) return false
74
+ notes.push({
75
+ from: current.from,
76
+ to: current.to,
77
+ text: current.lines.join('\n'),
78
+ lineCount: current.lines.length,
79
+ width: current.width || '',
80
+ })
81
+ seenStarts.add(current.from)
82
+ current = null
83
+ return true
84
+ }
85
+
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const line = lines[i]
88
+ const header = line.match(lineNoteHeaderReg)
89
+ if (header) {
90
+ if (!pushCurrent()) return null
91
+ let from = Number(header[1])
92
+ let to = header[2] ? Number(header[2]) : from
93
+ if (from > to) {
94
+ const swap = from
95
+ from = to
96
+ to = swap
97
+ }
98
+ if (seenStarts.has(from)) return null
99
+ current = {
100
+ from,
101
+ to,
102
+ lines: [],
103
+ width: '',
104
+ }
105
+ const parsed = parseLineNoteHeaderText(header[3])
106
+ current.width = parsed.width
107
+ if (parsed.text) current.lines.push(parsed.text)
108
+ continue
109
+ }
110
+
111
+ if (current && lineNoteContinuationReg.test(line)) {
112
+ const stripped = stripContinuationIndent(line)
113
+ const attrsOnly = parseLineNoteAttrsOnlyText(stripped)
114
+ if (attrsOnly) {
115
+ if (attrsOnly.width) current.width = attrsOnly.width
116
+ continue
117
+ }
118
+ current.lines.push(stripped)
119
+ continue
120
+ }
121
+
122
+ if (!line.trim()) {
123
+ continue
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ if (!pushCurrent()) return null
130
+ return notes
131
+ }
132
+
133
+ const getTokenLineNotes = (token) => {
134
+ if (!token || !token.meta || typeof token.meta !== 'object') return null
135
+ const notes = token.meta[lineNotesMetaKey]
136
+ return Array.isArray(notes) && notes.length ? notes : null
137
+ }
138
+
139
+ const setTokenLineNotes = (token, notes) => {
140
+ if (!token || !notes || !notes.length) return
141
+ if (!token.meta || typeof token.meta !== 'object') token.meta = {}
142
+ token.meta[lineNotesMetaKey] = notes
143
+ }
144
+
145
+ const extendTokenMapWithImmediateFollower = (token, next) => {
146
+ if (!token || !Array.isArray(token.map) || token.map.length !== 2) return
147
+ if (!next || !Array.isArray(next.map) || next.map.length !== 2) return
148
+ const start = token.map[0]
149
+ const end = next.map[1]
150
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)) return
151
+ if (end > token.map[1]) token.map = [start, end]
152
+ }
153
+
154
+ const installLineNotesCoreRule = (md) => {
155
+ if (!md || md[lineNotesInstalledKey]) return
156
+ md[lineNotesInstalledKey] = true
157
+ md.core.ruler.after('block', lineNotesRuleName, (state) => {
158
+ const tokens = state && state.tokens
159
+ if (!Array.isArray(tokens) || tokens.length < 2) return
160
+
161
+ for (let i = 0; i < tokens.length - 1; i++) {
162
+ const token = tokens[i]
163
+ const next = tokens[i + 1]
164
+ if (!token || token.type !== 'fence' || !next || next.type !== 'fence') continue
165
+ const nextName = getFenceInfoName(next.info)
166
+ if (!lineNoteFenceNameSet.has(nextName)) continue
167
+ const currentName = getFenceInfoName(token.info)
168
+ if (lineNoteFenceNameSet.has(currentName)) continue
169
+ const notes = parseLineNotesContent(next.content)
170
+ if (!notes || !notes.length) continue
171
+ setTokenLineNotes(token, notes)
172
+ extendTokenMapWithImmediateFollower(token, next)
173
+ tokens.splice(i + 1, 1)
174
+ }
175
+ })
176
+ }
177
+
178
+ export {
179
+ getTokenLineNotes,
180
+ installLineNotesCoreRule,
181
+ }
@@ -15,6 +15,9 @@ import {
15
15
  import {
16
16
  validateCustomHighlightOptions,
17
17
  } from '../custom-highlight/option-validator.js'
18
+ import {
19
+ sanitizeHighlightName,
20
+ } from '../custom-highlight/scope-name.js'
18
21
  import {
19
22
  commentLineClass,
20
23
  normalizeEmphasisRanges,
@@ -31,7 +34,6 @@ import {
31
34
  } from './render-api-constants.js'
32
35
 
33
36
  const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
34
- const hyphenMultiReg = /-+/g
35
37
 
36
38
  const defaultCustomHighlightOpt = {
37
39
  provider: 'shiki',
@@ -183,16 +185,6 @@ const shouldApplyApiFallbackForReason = (chOpt, reason) => {
183
185
  return chOpt._fallbackOnSet.has(reason)
184
186
  }
185
187
 
186
- const sanitizeHighlightName = (name, prefix = '') => {
187
- const prefixBase = String(prefix == null ? '' : prefix).replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
188
- const raw = String(name || '')
189
- let safe = raw.replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
190
- if (!safe) safe = 'scope'
191
- if (/^[0-9]/.test(safe)) safe = 'x-' + safe
192
- if (safe.startsWith('--')) safe = safe.slice(2) || 'scope'
193
- return prefixBase ? `${prefixBase}-${safe}` : safe
194
- }
195
-
196
188
  const uniqueHighlightName = (base, usedMap) => {
197
189
  if (!usedMap.has(base)) {
198
190
  usedMap.set(base, 1)
@@ -805,6 +797,5 @@ export {
805
797
  normalizeCustomHighlightOpt,
806
798
  renderCustomHighlightPayloadScript,
807
799
  renderCustomHighlightScopeStyleTag,
808
- sanitizeHighlightName,
809
800
  shouldApplyApiFallbackForReason,
810
801
  }
@@ -11,7 +11,9 @@ import {
11
11
  orderTokenAttrs,
12
12
  preWrapStyle,
13
13
  resolveAdvancedLineNumberPlan,
14
+ resolveLineNotesPlan,
14
15
  splitFenceBlockToLines,
16
+ wrapFencePreWithLineNotes,
15
17
  } from './render-shared.js'
16
18
  import {
17
19
  createApiPayloadForFence,
@@ -26,6 +28,8 @@ import {
26
28
  renderFenceMarkup,
27
29
  } from './render-markup.js'
28
30
 
31
+ const apiDisableLineFeatureList = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold', 'line-notes'])
32
+
29
33
  let fallbackCustomHighlightSeq = 0
30
34
 
31
35
  const nextCustomHighlightId = (env, prefix) => {
@@ -45,23 +49,30 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
45
49
  return preAttrs
46
50
  }
47
51
 
48
- const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, includePayload, timings) => {
52
+ const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, includePayload, timings) => {
49
53
  const isSamp = opt._sampReg.test(lang)
50
54
  const tag = isSamp ? 'samp' : 'code'
51
55
  let content = md.utils.escapeHtml(token.content)
52
56
  const lineStrategy = opt.customHighlight.lineFeatureStrategy
53
57
  const needLineNumber = lineStrategy === 'hybrid' && opt.setLineNumber && startNumber >= 0
54
58
  const needEndSpan = lineStrategy === 'hybrid' && opt.lineEndSpanThreshold > 0
59
+ const needAdvancedLineNumberPlan = needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)
60
+ const needLineNotes = lineStrategy !== 'disable' && !!(lineNotes && lineNotes.length)
61
+ const logicalLineCount = (needAdvancedLineNumberPlan || needLineNotes)
62
+ ? getLogicalLineCount(token.content)
63
+ : -1
55
64
  let lineNumberPlan = null
56
- if (needLineNumber && (lineNumberSkipValue !== undefined || lineNumberSetValue !== undefined)) {
57
- const logicalLineCount = getLogicalLineCount(token.content)
65
+ if (needAdvancedLineNumberPlan) {
58
66
  lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, logicalLineCount, logicalLineCount)
59
67
  }
60
- if (needLineNumber || needEndSpan) {
68
+ const lineNotePlan = needLineNotes
69
+ ? resolveLineNotesPlan(lineNotes, logicalLineCount, logicalLineCount, lineNoteIdPrefix)
70
+ : null
71
+ if (needLineNumber || needEndSpan || lineNotePlan) {
61
72
  const splitStartedAt = timings ? getNowMs() : 0
62
73
  const nlIndex = content.indexOf('\n')
63
74
  const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
64
- content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass, lineNumberPlan)
75
+ content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass, lineNumberPlan, lineNotePlan)
65
76
  if (timings) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
66
77
  }
67
78
 
@@ -88,7 +99,8 @@ const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emph
88
99
 
89
100
  if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
90
101
  const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
91
- const html = `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
102
+ const preHtml = `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>`
103
+ const html = wrapFencePreWithLineNotes(preHtml, lineNotePlan)
92
104
  return inlinePayloadScript ? html + inlinePayloadScript : html
93
105
  }
94
106
 
@@ -105,6 +117,8 @@ const getFenceHtml = (context, md, opt, slf, env) => {
105
117
  const wrapEnabled = context.wrapEnabled
106
118
  const preWrapValue = context.preWrapValue
107
119
  const commentMarkValue = context.commentMarkValue
120
+ const lineNotes = context.lineNotes
121
+ const lineNoteIdPrefix = context.lineNoteIdPrefix
108
122
 
109
123
  const apiDecisionBase = {
110
124
  renderer: 'api',
@@ -112,11 +126,11 @@ const getFenceHtml = (context, md, opt, slf, env) => {
112
126
  fallbackUsed: false,
113
127
  lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
114
128
  disabledFeatures: opt.customHighlight.lineFeatureStrategy === 'disable'
115
- ? ['setLineNumber', 'line-number-skip', 'line-number-set', 'lineEndSpanThreshold']
129
+ ? apiDisableLineFeatureList
116
130
  : [],
117
131
  }
118
132
  try {
119
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, true, timings)
133
+ const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, true, timings)
120
134
  if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
121
135
  emitFenceDecision(opt, apiDecisionBase)
122
136
  return html
@@ -130,7 +144,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
130
144
  reason: 'provider-error',
131
145
  lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
132
146
  }
133
- const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, false, timings)
147
+ const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberSetValue, wrapEnabled, preWrapValue, commentMarkValue, lineNotes, lineNoteIdPrefix, false, timings)
134
148
  if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
135
149
  emitFenceDecision(opt, fallbackDecision)
136
150
  return html
@@ -4,6 +4,9 @@ import {
4
4
  getPayloadMapFromScript,
5
5
  styleToHighlightCss,
6
6
  } from '../custom-highlight/runtime-utils.js'
7
+ import {
8
+ sanitizeHighlightName,
9
+ } from '../custom-highlight/scope-name.js'
7
10
  import {
8
11
  customHighlightAppliedAttr,
9
12
  customHighlightAppliedSelector,
@@ -19,17 +22,6 @@ const globalRuntimeInsertedScopeStyles = new Map()
19
22
  const runtimeInsertedScopeStylesByDoc = new WeakMap()
20
23
  const runtimeApplyStateByRoot = new WeakMap()
21
24
  const validColorSchemeSet = new Set(['light', 'dark', 'auto'])
22
- const highlightNameUnsafeReg = /[^A-Za-z0-9_-]+/g
23
- const hyphenMultiReg = /-+/g
24
-
25
- const sanitizeHighlightName = (name) => {
26
- const raw = String(name || '')
27
- let safe = raw.replace(highlightNameUnsafeReg, '-').replace(hyphenMultiReg, '-').replace(/^-+|-+$/g, '')
28
- if (!safe) safe = 'scope'
29
- if (/^[0-9]/.test(safe)) safe = 'x-' + safe
30
- if (safe.startsWith('--')) safe = safe.slice(2) || 'scope'
31
- return safe
32
- }
33
25
 
34
26
  const getCustomHighlightBlockId = (node) => {
35
27
  if (!node || typeof node.getAttribute !== 'function') return null
@@ -133,10 +125,10 @@ const addMediaQueryChangeListener = (queryList, listener) => {
133
125
  }
134
126
 
135
127
  const getRuntimeSupportedPayloadVersions = (options = {}) => {
136
- const set = new Set()
137
128
  if (options.strictVersion === true) {
138
- for (const v of customHighlightPayloadSupportedVersions) set.add(v)
129
+ return new Set(customHighlightPayloadSupportedVersions)
139
130
  }
131
+ const set = new Set()
140
132
  if (Number.isSafeInteger(options.supportedVersion)) set.add(options.supportedVersion)
141
133
  if (Array.isArray(options.supportedVersions)) {
142
134
  for (const v of options.supportedVersions) {
@@ -7,6 +7,9 @@ import {
7
7
  finalizeCommonFenceOption,
8
8
  prepareFenceRenderContext,
9
9
  } from './render-shared.js'
10
+ import {
11
+ installLineNotesCoreRule,
12
+ } from './line-notes.js'
10
13
  import {
11
14
  customHighlightDataEnvKey,
12
15
  customHighlightEnvInitRuleName,
@@ -73,6 +76,7 @@ const mditRendererFenceCustomHighlight = (md, option) => {
73
76
  }
74
77
 
75
78
  finalizeCommonFenceOption(opt)
79
+ installLineNotesCoreRule(md)
76
80
 
77
81
  md.core.ruler.before('block', customHighlightEnvInitRuleName, (state) => {
78
82
  const env = state && state.env
@@ -14,12 +14,16 @@ import {
14
14
  orderTokenAttrs,
15
15
  preWrapStyle,
16
16
  resolveAdvancedLineNumberPlan,
17
+ resolveLineNotesPlan,
17
18
  splitFenceBlockToLines,
19
+ wrapFencePreWithLineNotes,
18
20
  } from './render-shared.js'
19
21
  import {
20
22
  parsePreCodeWrapper,
21
23
  } from '../utils/pre-code-wrapper-parser.js'
22
24
 
25
+ const markupPassthroughDisabledFeatures = Object.freeze(['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'line-notes', 'samp'])
26
+
23
27
  const renderFenceMarkup = (context, md, opt, slf) => {
24
28
  const token = context.token
25
29
  const lang = context.lang
@@ -33,6 +37,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
33
37
  const wrapEnabled = context.wrapEnabled
34
38
  const preWrapValue = context.preWrapValue
35
39
  const commentMarkValue = context.commentMarkValue
40
+ const lineNotes = context.lineNotes
41
+ const lineNoteIdPrefix = context.lineNoteIdPrefix
36
42
 
37
43
  const isSamp = opt._sampReg.test(lang)
38
44
  const sourceContent = token.content
@@ -76,7 +82,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
76
82
  passthrough: true,
77
83
  passthroughReason: 'pre-code-parse-failed',
78
84
  hasHighlightPre: false,
79
- disabledFeatures: ['setLineNumber', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'],
85
+ disabledFeatures: markupPassthroughDisabledFeatures,
80
86
  }
81
87
  if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
82
88
  emitFenceDecision(opt, decision)
@@ -105,7 +111,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
105
111
  if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
106
112
  const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
107
113
 
108
- const needLineNumber = opt.setLineNumber && startNumber >= 0
114
+ const useHighlightPre = opt.useHighlightPre && hasHighlightPre
115
+ const needLineNumber = !useHighlightPre && opt.setLineNumber && startNumber >= 0
109
116
  let sourceLogicalLineCount = -1
110
117
  let highlightedLogicalLineCount = -1
111
118
  const ensureLogicalLineCounts = () => {
@@ -113,7 +120,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
113
120
  if (highlightedLogicalLineCount === -1) highlightedLogicalLineCount = getLogicalLineCount(content)
114
121
  }
115
122
  let normalizedEmphasis = []
116
- if (opt.setEmphasizeLines && emphasizeLines.length > 0) {
123
+ if (!useHighlightPre && opt.setEmphasizeLines && emphasizeLines.length > 0) {
117
124
  ensureLogicalLineCounts()
118
125
  normalizedEmphasis = normalizeEmphasisRanges(emphasizeLines, highlightedLogicalLineCount)
119
126
  }
@@ -122,9 +129,13 @@ const renderFenceMarkup = (context, md, opt, slf) => {
122
129
  ensureLogicalLineCounts()
123
130
  lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, sourceLogicalLineCount, highlightedLogicalLineCount)
124
131
  }
132
+ let lineNotePlan = null
133
+ if (!useHighlightPre && lineNotes && lineNotes.length) {
134
+ ensureLogicalLineCounts()
135
+ lineNotePlan = resolveLineNotesPlan(lineNotes, sourceLogicalLineCount, highlightedLogicalLineCount, lineNoteIdPrefix)
136
+ }
125
137
  const needEmphasis = normalizedEmphasis.length > 0
126
138
  const needEndSpan = opt.lineEndSpanThreshold > 0
127
- const useHighlightPre = opt.useHighlightPre && hasHighlightPre
128
139
 
129
140
  if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
130
141
  ensureLogicalLineCounts()
@@ -140,7 +151,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
140
151
  }
141
152
  }
142
153
 
143
- if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
154
+ if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment || lineNotePlan)) {
144
155
  const splitStartedAt = timingEnabled ? getNowMs() : 0
145
156
  const nlIndex = content.indexOf('\n')
146
157
  const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
@@ -156,6 +167,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
156
167
  commentLines,
157
168
  commentLineClass,
158
169
  lineNumberPlan,
170
+ lineNotePlan,
159
171
  )
160
172
  if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
161
173
  }
@@ -165,11 +177,12 @@ const renderFenceMarkup = (context, md, opt, slf) => {
165
177
  renderer: 'markup',
166
178
  useHighlightPre,
167
179
  hasHighlightPre,
168
- disabledFeatures: useHighlightPre ? ['setLineNumber', 'line-number-skip', 'line-number-set', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'] : [],
180
+ disabledFeatures: useHighlightPre ? markupPassthroughDisabledFeatures : [],
169
181
  }
170
182
  if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
171
183
  emitFenceDecision(opt, decision)
172
- return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
184
+ const preHtml = `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>`
185
+ return wrapFencePreWithLineNotes(preHtml, lineNotePlan)
173
186
  }
174
187
 
175
188
  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
@@ -89,6 +105,7 @@ const createCommonFenceOptionDefaults = (md) => {
89
105
  const finalizeCommonFenceOption = (opt) => {
90
106
  opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
91
107
  opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
108
+ opt._attrOrderRankCache = new Map()
92
109
  return opt
93
110
  }
94
111
 
@@ -115,6 +132,13 @@ const parsePositiveLineIndex = (val) => {
115
132
  return num
116
133
  }
117
134
 
135
+ const escapeHtmlText = (value) => {
136
+ return String(value ?? '')
137
+ .replace(/&/g, '&amp;')
138
+ .replace(/</g, '&lt;')
139
+ .replace(/>/g, '&gt;')
140
+ }
141
+
118
142
  const getLogicalLineCount = (text) => {
119
143
  const str = String(text ?? '')
120
144
  if (!str) return 0
@@ -254,22 +278,106 @@ const resolveAdvancedLineNumberPlan = (lineNumberSkipValue, lineNumberSetValue,
254
278
  return buildAdvancedLineNumberPlan(skipRanges, setEntries, sourceLineCount)
255
279
  }
256
280
 
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
281
+ const createLineNotePlan = (lineNotes, maxLine, idPrefix) => {
282
+ if (!Array.isArray(lineNotes) || !lineNotes.length || maxLine <= 0) return null
283
+ const anchors = []
284
+ const items = []
285
+ const noteIdPrefix = String(idPrefix || 'pre-line-note')
286
+ let prevFrom = 0
287
+ let prevTo = 0
288
+ let needsSort = false
289
+ for (let i = 0; i < lineNotes.length; i++) {
290
+ const note = lineNotes[i]
291
+ if (!note || !Number.isSafeInteger(note.from) || !Number.isSafeInteger(note.to)) continue
292
+ let from = note.from
293
+ let to = note.to
294
+ if (from > to) {
295
+ const swap = from
296
+ from = to
297
+ to = swap
298
+ }
299
+ if (to < 1 || from > maxLine) continue
300
+ if (from < 1) from = 1
301
+ if (to > maxLine) to = maxLine
302
+ if (anchors[from - 1] !== undefined) return null
303
+ const lineCount = Number.isSafeInteger(note.lineCount) && note.lineCount > 0
304
+ ? note.lineCount
305
+ : Math.max(getLogicalLineCount(note.text), 1)
306
+ const item = {
307
+ from,
308
+ to,
309
+ label: from === to ? String(from) : `${from}-${to}`,
310
+ html: escapeHtmlText(note.text),
311
+ anchorName: `--pre-line-note-${from}`,
312
+ id: `${noteIdPrefix}-${from}`,
313
+ lineCount,
314
+ width: note.width || '',
315
+ }
316
+ anchors[from - 1] = item
317
+ items.push(item)
318
+ if (!needsSort && items.length > 1 && (from < prevFrom || (from === prevFrom && to < prevTo))) {
319
+ needsSort = true
320
+ }
321
+ prevFrom = from
322
+ prevTo = to
323
+ }
324
+ if (!items.length) return null
325
+
326
+ if (needsSort) items.sort((a, b) => a.from - b.from || a.to - b.to)
327
+ let canAnchor = true
328
+ let previousVisualEnd = 0
329
+ let overflowLines = 0
330
+ for (let i = 0; i < items.length; i++) {
331
+ const item = items[i]
332
+ const visualEnd = item.from + item.lineCount - 1
333
+ if (item.from <= previousVisualEnd) canAnchor = false
334
+ if (visualEnd > maxLine) overflowLines = Math.max(overflowLines, visualEnd - maxLine)
335
+ previousVisualEnd = visualEnd
336
+ }
337
+
338
+ return {
339
+ anchors,
340
+ items,
341
+ canAnchor,
342
+ overflowLines,
343
+ }
344
+ }
345
+
346
+ const resolveLineNotesPlan = (lineNotes, sourceLineCount, renderedLineCount, idPrefix) => {
347
+ if (!Array.isArray(lineNotes) || !lineNotes.length) return null
348
+ if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
349
+ if (sourceLineCount !== renderedLineCount) return null
350
+ return createLineNotePlan(lineNotes, sourceLineCount, idPrefix)
351
+ }
352
+
353
+ const buildPreLineTags = (hidden, setNumber, lineNote) => {
354
+ if (!hidden && setNumber == null && !lineNote) return defaultPreLineTags
262
355
  let tag = '<span class="pre-line'
263
356
  if (hidden) tag += ' ' + preLineNoNumberClass
357
+ if (lineNote) tag += ' ' + preLineHasEndNoteClass
264
358
  tag += '"'
265
359
  if (setNumber != null) tag += ` style="counter-set:pre-line-number ${setNumber};"`
266
- return tag + '>'
360
+ if (lineNote) {
361
+ tag += ` data-pre-line-note-from="${lineNote.from}"`
362
+ tag += ` data-pre-line-note-to="${lineNote.to}"`
363
+ }
364
+ tag += '>'
365
+ if (!lineNote) return { open: tag, close: closeTag }
366
+ const describedByAttr = lineNote.id ? ` aria-describedby="${lineNote.id}"` : ''
367
+ return {
368
+ open: `${tag}<span class="${preLineContentClass}" style="anchor-name:${lineNote.anchorName};"${describedByAttr}>`,
369
+ close: `${closeTag}${closeTag}`,
370
+ }
267
371
  }
268
372
 
269
- const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan) => {
373
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan, lineNotePlan) => {
270
374
  const lines = content.split(br)
271
375
  const max = lines.length
272
- const hasDynamicLineNumberPlan = !!lineNumberPlan
376
+ const lineNumberHidden = lineNumberPlan ? lineNumberPlan.hidden : null
377
+ const lineNumberSets = lineNumberPlan ? lineNumberPlan.sets : null
378
+ const lineNoteAnchors = lineNotePlan ? lineNotePlan.anchors : null
379
+ const hasLineNotes = !!lineNoteAnchors
380
+ const needLineWrap = needLineNumber || hasLineNotes
273
381
  let emIdx = 0
274
382
  let emStart = -1
275
383
  let emEnd = -1
@@ -286,6 +394,9 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
286
394
  let line = lines[n]
287
395
  const notLastLine = n < max - 1
288
396
  const doComment = commentLines && commentLines[n]
397
+ const hidden = !!(lineNumberHidden && lineNumberHidden[n])
398
+ const setNumber = lineNumberSets ? lineNumberSets[n] : undefined
399
+ const lineNote = hasLineNotes ? lineNoteAnchors[n] : null
289
400
  let hasLt = false
290
401
  let hasLtChecked = false
291
402
 
@@ -309,7 +420,7 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
309
420
  }
310
421
  }
311
422
 
312
- if (needLineNumber && notLastLine) {
423
+ if (needLineWrap && notLastLine) {
313
424
  if (!hasLtChecked) {
314
425
  hasLt = line.indexOf('<') !== -1
315
426
  hasLtChecked = true
@@ -339,8 +450,9 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
339
450
  line = `<span class="${commentClass}">` + line + closeTag
340
451
  }
341
452
 
342
- if (needLineNumber && notLastLine) {
343
- line = (hasDynamicLineNumberPlan ? getPreLineOpenTag(lineNumberPlan, n) : preLineTag) + line + closeTag
453
+ if (needLineWrap && notLastLine) {
454
+ const lineTags = buildPreLineTags(hidden, setNumber, lineNote)
455
+ line = lineTags.open + line + lineTags.close
344
456
  }
345
457
 
346
458
  if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
@@ -359,10 +471,33 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
359
471
  return lines.join(br)
360
472
  }
361
473
 
474
+ const renderLineNotesLayer = (lineNotePlan) => {
475
+ const items = lineNotePlan && lineNotePlan.items
476
+ if (!items || !items.length) return ''
477
+ let html = `<div class="${lineNoteLayerClass}">`
478
+ for (let i = 0; i < items.length; i++) {
479
+ const item = items[i]
480
+ let style = `position-anchor:${item.anchorName};`
481
+ if (item.width) style += ` --line-note-width:${item.width};`
482
+ 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>`
483
+ }
484
+ html += '</div>'
485
+ return html
486
+ }
487
+
488
+ const wrapFencePreWithLineNotes = (preHtml, lineNotePlan) => {
489
+ const items = lineNotePlan && lineNotePlan.items
490
+ if (!items || !items.length) return preHtml + '\n'
491
+ let attrs = ` class="${preWithLineNotesClass}"`
492
+ if (lineNotePlan.canAnchor) attrs += ' data-pre-line-notes-layout="anchor"'
493
+ if (lineNotePlan.overflowLines > 0) attrs += ` style="--pre-line-note-overflow-lines:${lineNotePlan.overflowLines};"`
494
+ return `<div${attrs}>${preHtml}${renderLineNotesLayer(lineNotePlan)}</div>\n`
495
+ }
496
+
362
497
  const orderTokenAttrs = (token, opt) => {
363
498
  const attrs = token.attrs
364
499
  if (!attrs || attrs.length < 2) return
365
- const rankCache = new Map()
500
+ const rankCache = opt._attrOrderRankCache || new Map()
366
501
  const getRank = (name) => {
367
502
  let rank = rankCache.get(name)
368
503
  if (rank === undefined) {
@@ -402,6 +537,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
402
537
  let wrapEnabled = false
403
538
  let preWrapValue
404
539
  let commentMarkValue
540
+ const lineNotes = getTokenLineNotes(token)
541
+ const lineNoteIdPrefix = lineNotes && lineNotes.length ? getLineNoteIdPrefix(token, idx) : ''
405
542
  const attrNormalizeStartedAt = timingEnabled ? getNowMs() : 0
406
543
 
407
544
  if (token.attrs) {
@@ -557,6 +694,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
557
694
  }
558
695
 
559
696
  if (timingEnabled) addTimingMs(timings, 'attrNormalizeMs', getNowMs() - attrNormalizeStartedAt)
697
+ if (lineNotes && lineNotes.length) token.attrSet('data-pre-line-notes', 'true')
560
698
 
561
699
  return {
562
700
  token,
@@ -568,6 +706,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
568
706
  wrapEnabled,
569
707
  preWrapValue,
570
708
  commentMarkValue,
709
+ lineNotes,
710
+ lineNoteIdPrefix,
571
711
  timingEnabled,
572
712
  timings,
573
713
  fenceStartedAt,
@@ -590,5 +730,7 @@ export {
590
730
  preWrapStyle,
591
731
  prepareFenceRenderContext,
592
732
  resolveAdvancedLineNumberPlan,
733
+ resolveLineNotesPlan,
593
734
  splitFenceBlockToLines,
735
+ wrapFencePreWithLineNotes,
594
736
  }
@@ -1,56 +0,0 @@
1
- # Third-Party Notices
2
-
3
- This repository is licensed under MIT (see `LICENSE`), and also contains documentation assets derived from third-party themes.
4
-
5
- ## highlight.js theme references used in docs
6
-
7
- Files:
8
-
9
- - `docs/default-highlight-theme.css`
10
-
11
- Source references:
12
-
13
- - `highlight.js` style family (GitHub / GitHub Dark)
14
- - Upstream paths (for reference):
15
- - `node_modules/highlight.js/styles/github.css`
16
- - `node_modules/highlight.js/styles/github-dark.css`
17
-
18
- Upstream license:
19
-
20
- - BSD 3-Clause License
21
- - Copyright (c) 2006, Ivan Sagalaev.
22
-
23
- Full upstream license text:
24
-
25
- ```text
26
- BSD 3-Clause License
27
-
28
- Copyright (c) 2006, Ivan Sagalaev.
29
- All rights reserved.
30
-
31
- Redistribution and use in source and binary forms, with or without
32
- modification, are permitted provided that the following conditions are met:
33
-
34
- * Redistributions of source code must retain the above copyright notice, this
35
- list of conditions and the following disclaimer.
36
-
37
- * Redistributions in binary form must reproduce the above copyright notice,
38
- this list of conditions and the following disclaimer in the documentation
39
- and/or other materials provided with the distribution.
40
-
41
- * Neither the name of the copyright holder nor the names of its
42
- contributors may be used to endorse or promote products derived from
43
- this software without specific prior written permission.
44
-
45
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
46
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
47
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
48
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
49
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
50
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
51
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
52
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
53
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
54
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
55
- ```
56
-