@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 +52 -3
- package/package.json +4 -5
- package/src/custom-highlight/option-validator.js +14 -5
- package/src/custom-highlight/scope-name.js +23 -0
- package/src/entry/markup-highlight.js +4 -0
- package/src/fence/line-notes.js +181 -0
- package/src/fence/render-api-provider.js +3 -12
- package/src/fence/render-api-renderer.js +23 -9
- package/src/fence/render-api-runtime.js +5 -13
- package/src/fence/render-api.js +4 -0
- package/src/fence/render-markup.js +20 -7
- package/src/fence/render-shared.js +154 -12
- package/THIRD_PARTY_NOTICES.md +0 -56
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
|
|
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.
|
|
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.
|
|
44
|
+
"@peaceroad/markdown-it-figure-with-p-caption": "^0.17.0",
|
|
46
45
|
"highlight.js": "^11.11.1",
|
|
47
|
-
"markdown-it": "^14.1.
|
|
46
|
+
"markdown-it": "^14.1.1",
|
|
48
47
|
"markdown-it-attrs": "^4.3.1",
|
|
49
|
-
"shiki": "^
|
|
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 && !
|
|
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 && !
|
|
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 && !
|
|
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 && !
|
|
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 && !
|
|
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 (
|
|
57
|
-
const logicalLineCount = getLogicalLineCount(token.content)
|
|
65
|
+
if (needAdvancedLineNumberPlan) {
|
|
58
66
|
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, logicalLineCount, logicalLineCount)
|
|
59
67
|
}
|
|
60
|
-
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
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) {
|
package/src/fence/render-api.js
CHANGED
|
@@ -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:
|
|
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
|
|
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 ?
|
|
180
|
+
disabledFeatures: useHighlightPre ? markupPassthroughDisabledFeatures : [],
|
|
169
181
|
}
|
|
170
182
|
if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
171
183
|
emitFenceDecision(opt, decision)
|
|
172
|
-
|
|
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, '&')
|
|
138
|
+
.replace(/</g, '<')
|
|
139
|
+
.replace(/>/g, '>')
|
|
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
|
|
258
|
-
if (!
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
343
|
-
|
|
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
|
}
|
package/THIRD_PARTY_NOTICES.md
DELETED
|
@@ -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
|
-
|