@peaceroad/markdown-it-renderer-fence 0.5.0 → 0.6.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 +56 -2
- package/package.json +1 -1
- package/src/entry/markup-highlight.js +2 -15
- package/src/fence/render-api-renderer.js +14 -5
- package/src/fence/render-api.js +2 -12
- package/src/fence/render-markup.js +22 -6
- package/src/fence/render-shared.js +171 -9
package/README.md
CHANGED
|
@@ -114,7 +114,8 @@ Note:
|
|
|
114
114
|
### Main Features
|
|
115
115
|
|
|
116
116
|
- `samp` rendering for `samp`, `shell`, `console` languages.
|
|
117
|
-
- line number wrapping via `start` / `data-pre-start`.
|
|
117
|
+
- line number wrapping via `start` (`line-number-start` long form) / `data-pre-start`.
|
|
118
|
+
- line number skip/reset controls via `line-number-skip` / `line-number-reset`.
|
|
118
119
|
- emphasized lines via `em-lines` / `emphasize-lines`.
|
|
119
120
|
- optional line-end spacer via `lineEndSpanThreshold`.
|
|
120
121
|
- optional pre-wrap support via `wrap` / `pre-wrap`.
|
|
@@ -153,6 +154,30 @@ console.log(a)
|
|
|
153
154
|
</code></pre>
|
|
154
155
|
```
|
|
155
156
|
|
|
157
|
+
Advanced line number control:
|
|
158
|
+
|
|
159
|
+
~~~md
|
|
160
|
+
```txt {start="25" line-number-skip="5" line-number-reset="6:136"}
|
|
161
|
+
line1
|
|
162
|
+
line2
|
|
163
|
+
line3
|
|
164
|
+
line4
|
|
165
|
+
...
|
|
166
|
+
line6
|
|
167
|
+
```
|
|
168
|
+
~~~
|
|
169
|
+
|
|
170
|
+
```html
|
|
171
|
+
<pre><code class="language-txt" data-pre-start="25" data-pre-line-number-skip="5" data-pre-line-number-reset="6:136" style="counter-set:pre-line-number 25;">
|
|
172
|
+
<span class="pre-line">line1</span>
|
|
173
|
+
<span class="pre-line">line2</span>
|
|
174
|
+
<span class="pre-line">line3</span>
|
|
175
|
+
<span class="pre-line">line4</span>
|
|
176
|
+
<span class="pre-line pre-line-no-number">...</span>
|
|
177
|
+
<span class="pre-line" style="counter-set:pre-line-number 136;">line6</span>
|
|
178
|
+
</code></pre>
|
|
179
|
+
```
|
|
180
|
+
|
|
156
181
|
Emphasis:
|
|
157
182
|
|
|
158
183
|
~~~md
|
|
@@ -209,16 +234,38 @@ echo 1
|
|
|
209
234
|
- `useHighlightPre: true` keeps highlighter-provided `<pre><code>` when present.
|
|
210
235
|
- In that passthrough path, line-splitting features are intentionally disabled:
|
|
211
236
|
- line numbers
|
|
237
|
+
- `line-number-skip`
|
|
238
|
+
- `line-number-reset`
|
|
212
239
|
- `em-lines`
|
|
213
240
|
- line-end spacer
|
|
214
241
|
- `comment-mark`
|
|
215
242
|
- `samp` conversion
|
|
216
243
|
|
|
244
|
+
Official line-number CSS contract:
|
|
245
|
+
|
|
246
|
+
```css
|
|
247
|
+
.pre-line {
|
|
248
|
+
counter-increment: pre-line-number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.pre-line::before {
|
|
252
|
+
content: counter(pre-line-number);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.pre-line.pre-line-no-number {
|
|
256
|
+
counter-increment: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.pre-line.pre-line-no-number::before {
|
|
260
|
+
content: "";
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
217
264
|
### Markup Options
|
|
218
265
|
|
|
219
266
|
- `attrsOrder` (default: `['class', 'id', 'data-*', 'style']`): output attribute order (`data-*` wildcard supported).
|
|
220
267
|
- `setHighlight` (default: `true`): call `md.options.highlight` when available.
|
|
221
|
-
- `setLineNumber` (default: `true`): enable line wrapper spans when `start` is valid.
|
|
268
|
+
- `setLineNumber` (default: `true`): enable line wrapper spans when `start` / `line-number-start` is valid.
|
|
222
269
|
- `setEmphasizeLines` (default: `true`): enable `em-lines` / `emphasize-lines`.
|
|
223
270
|
- `lineEndSpanThreshold` (default: `0`): append line-end spacer span when visual width threshold is met.
|
|
224
271
|
- `setLineEndSpan`: alias of `lineEndSpanThreshold`.
|
|
@@ -236,6 +283,13 @@ echo 1
|
|
|
236
283
|
- supports open-ended forms (`3-`, `-2`)
|
|
237
284
|
- reversed ranges are normalized (`5-3` behaves as `3-5`)
|
|
238
285
|
|
|
286
|
+
Line-number attr syntax note:
|
|
287
|
+
|
|
288
|
+
- `line-number-start` is the long form of `start`; rendered output still uses `data-pre-start`.
|
|
289
|
+
- `line-number-skip` supports single values (`2`), ranges (`4-6`), and open-ended forms (`3-`).
|
|
290
|
+
- `line-number-reset` uses `line:number` pairs (for example `6:136,14:220`).
|
|
291
|
+
- `line-number-skip` / `line-number-reset` are applied only when source and highlighted logical line counts match; otherwise renderer falls back to plain sequential numbering.
|
|
292
|
+
|
|
239
293
|
Migration note (`0.5.0`):
|
|
240
294
|
|
|
241
295
|
- `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-renderer-fence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyLineEndAlias,
|
|
3
|
+
createCommonFenceOptionDefaults,
|
|
3
4
|
finalizeCommonFenceOption,
|
|
4
5
|
prepareFenceRenderContext,
|
|
5
6
|
} from '../fence/render-shared.js'
|
|
@@ -8,27 +9,13 @@ import {
|
|
|
8
9
|
} from '../fence/render-markup.js'
|
|
9
10
|
|
|
10
11
|
const mditRendererFenceMarkup = (md, option) => {
|
|
11
|
-
const opt =
|
|
12
|
-
attrsOrder: ['class', 'id', 'data-*', 'style'],
|
|
13
|
-
setHighlight: true,
|
|
14
|
-
setLineNumber: true,
|
|
15
|
-
setEmphasizeLines: true,
|
|
16
|
-
lineEndSpanThreshold: 0,
|
|
17
|
-
lineEndSpanClass: 'pre-lineend-spacer',
|
|
18
|
-
setPreWrapStyle: true,
|
|
19
|
-
useHighlightPre: false,
|
|
20
|
-
onFenceDecision: null,
|
|
21
|
-
onFenceDecisionTiming: false,
|
|
22
|
-
sampLang: 'shell,console',
|
|
23
|
-
langPrefix: md.options.langPrefix || 'language-',
|
|
24
|
-
}
|
|
12
|
+
const opt = createCommonFenceOptionDefaults(md)
|
|
25
13
|
|
|
26
14
|
if (option) {
|
|
27
15
|
Object.assign(opt, option)
|
|
28
16
|
applyLineEndAlias(opt, option)
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
delete opt.customHighlight
|
|
32
19
|
finalizeCommonFenceOption(opt)
|
|
33
20
|
|
|
34
21
|
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
commentLineClass,
|
|
7
7
|
emitFenceDecision,
|
|
8
8
|
finalizeFenceTimings,
|
|
9
|
+
getLogicalLineCount,
|
|
9
10
|
getNowMs,
|
|
10
11
|
orderTokenAttrs,
|
|
11
12
|
preWrapStyle,
|
|
13
|
+
resolveAdvancedLineNumberPlan,
|
|
12
14
|
splitFenceBlockToLines,
|
|
13
15
|
} from './render-shared.js'
|
|
14
16
|
import {
|
|
@@ -43,18 +45,23 @@ const buildApiPreAttrs = (preWrapValue, wrapEnabled, opt) => {
|
|
|
43
45
|
return preAttrs
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, wrapEnabled, preWrapValue, commentMarkValue, includePayload, timings) => {
|
|
48
|
+
const renderFenceApiOrPlain = (token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberResetValue, wrapEnabled, preWrapValue, commentMarkValue, includePayload, timings) => {
|
|
47
49
|
const isSamp = opt._sampReg.test(lang)
|
|
48
50
|
const tag = isSamp ? 'samp' : 'code'
|
|
49
51
|
let content = md.utils.escapeHtml(token.content)
|
|
50
52
|
const lineStrategy = opt.customHighlight.lineFeatureStrategy
|
|
51
53
|
const needLineNumber = lineStrategy === 'hybrid' && opt.setLineNumber && startNumber >= 0
|
|
52
54
|
const needEndSpan = lineStrategy === 'hybrid' && opt.lineEndSpanThreshold > 0
|
|
55
|
+
let lineNumberPlan = null
|
|
56
|
+
if (needLineNumber && (lineNumberSkipValue !== undefined || lineNumberResetValue !== undefined)) {
|
|
57
|
+
const logicalLineCount = getLogicalLineCount(token.content)
|
|
58
|
+
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberResetValue, logicalLineCount, logicalLineCount)
|
|
59
|
+
}
|
|
53
60
|
if (needLineNumber || needEndSpan) {
|
|
54
61
|
const splitStartedAt = timings ? getNowMs() : 0
|
|
55
62
|
const nlIndex = content.indexOf('\n')
|
|
56
63
|
const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
|
|
57
|
-
content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass)
|
|
64
|
+
content = splitFenceBlockToLines(content, [], needLineNumber, false, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, undefined, commentLineClass, lineNumberPlan)
|
|
58
65
|
if (timings) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -93,6 +100,8 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
93
100
|
const fenceStartedAt = context.fenceStartedAt
|
|
94
101
|
const startNumber = context.startNumber
|
|
95
102
|
const emphasizeLines = context.emphasizeLines
|
|
103
|
+
const lineNumberSkipValue = context.lineNumberSkipValue
|
|
104
|
+
const lineNumberResetValue = context.lineNumberResetValue
|
|
96
105
|
const wrapEnabled = context.wrapEnabled
|
|
97
106
|
const preWrapValue = context.preWrapValue
|
|
98
107
|
const commentMarkValue = context.commentMarkValue
|
|
@@ -103,11 +112,11 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
103
112
|
fallbackUsed: false,
|
|
104
113
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
105
114
|
disabledFeatures: opt.customHighlight.lineFeatureStrategy === 'disable'
|
|
106
|
-
? ['setLineNumber', 'lineEndSpanThreshold']
|
|
115
|
+
? ['setLineNumber', 'line-number-skip', 'line-number-reset', 'lineEndSpanThreshold']
|
|
107
116
|
: [],
|
|
108
117
|
}
|
|
109
118
|
try {
|
|
110
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, wrapEnabled, preWrapValue, commentMarkValue, true, timings)
|
|
119
|
+
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberResetValue, wrapEnabled, preWrapValue, commentMarkValue, true, timings)
|
|
111
120
|
if (timingEnabled) apiDecisionBase.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
112
121
|
emitFenceDecision(opt, apiDecisionBase)
|
|
113
122
|
return html
|
|
@@ -121,7 +130,7 @@ const getFenceHtml = (context, md, opt, slf, env) => {
|
|
|
121
130
|
reason: 'provider-error',
|
|
122
131
|
lineFeatureStrategy: opt.customHighlight.lineFeatureStrategy,
|
|
123
132
|
}
|
|
124
|
-
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, wrapEnabled, preWrapValue, commentMarkValue, false, timings)
|
|
133
|
+
const html = renderFenceApiOrPlain(token, lang, md, opt, slf, env, startNumber, emphasizeLines, lineNumberSkipValue, lineNumberResetValue, wrapEnabled, preWrapValue, commentMarkValue, false, timings)
|
|
125
134
|
if (timingEnabled) fallbackDecision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
126
135
|
emitFenceDecision(opt, fallbackDecision)
|
|
127
136
|
return html
|
package/src/fence/render-api.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from '../custom-highlight/payload-utils.js'
|
|
4
4
|
import {
|
|
5
5
|
applyLineEndAlias,
|
|
6
|
+
createCommonFenceOptionDefaults,
|
|
6
7
|
finalizeCommonFenceOption,
|
|
7
8
|
prepareFenceRenderContext,
|
|
8
9
|
} from './render-shared.js'
|
|
@@ -38,18 +39,7 @@ const shouldRuntimeFallback = (reason, opt = {}) => {
|
|
|
38
39
|
|
|
39
40
|
const mditRendererFenceCustomHighlight = (md, option) => {
|
|
40
41
|
const opt = {
|
|
41
|
-
|
|
42
|
-
setHighlight: true,
|
|
43
|
-
setLineNumber: true,
|
|
44
|
-
setEmphasizeLines: true,
|
|
45
|
-
lineEndSpanThreshold: 0,
|
|
46
|
-
lineEndSpanClass: 'pre-lineend-spacer',
|
|
47
|
-
setPreWrapStyle: true,
|
|
48
|
-
useHighlightPre: false,
|
|
49
|
-
onFenceDecision: null,
|
|
50
|
-
onFenceDecisionTiming: false,
|
|
51
|
-
sampLang: 'shell,console',
|
|
52
|
-
langPrefix: md.options.langPrefix || 'language-',
|
|
42
|
+
...createCommonFenceOptionDefaults(md),
|
|
53
43
|
customHighlight: null,
|
|
54
44
|
}
|
|
55
45
|
if (option) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
normalizeEmphasisRanges,
|
|
14
14
|
orderTokenAttrs,
|
|
15
15
|
preWrapStyle,
|
|
16
|
+
resolveAdvancedLineNumberPlan,
|
|
16
17
|
splitFenceBlockToLines,
|
|
17
18
|
} from './render-shared.js'
|
|
18
19
|
import {
|
|
@@ -27,6 +28,8 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
27
28
|
const fenceStartedAt = context.fenceStartedAt
|
|
28
29
|
const startNumber = context.startNumber
|
|
29
30
|
const emphasizeLines = context.emphasizeLines
|
|
31
|
+
const lineNumberSkipValue = context.lineNumberSkipValue
|
|
32
|
+
const lineNumberResetValue = context.lineNumberResetValue
|
|
30
33
|
const wrapEnabled = context.wrapEnabled
|
|
31
34
|
const preWrapValue = context.preWrapValue
|
|
32
35
|
const commentMarkValue = context.commentMarkValue
|
|
@@ -103,16 +106,28 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
103
106
|
const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
|
|
104
107
|
|
|
105
108
|
const needLineNumber = opt.setLineNumber && startNumber >= 0
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
let sourceLogicalLineCount = -1
|
|
110
|
+
let highlightedLogicalLineCount = -1
|
|
111
|
+
const ensureLogicalLineCounts = () => {
|
|
112
|
+
if (sourceLogicalLineCount === -1) sourceLogicalLineCount = getLogicalLineCount(sourceContent)
|
|
113
|
+
if (highlightedLogicalLineCount === -1) highlightedLogicalLineCount = getLogicalLineCount(content)
|
|
114
|
+
}
|
|
115
|
+
let normalizedEmphasis = []
|
|
116
|
+
if (opt.setEmphasizeLines && emphasizeLines.length > 0) {
|
|
117
|
+
ensureLogicalLineCounts()
|
|
118
|
+
normalizedEmphasis = normalizeEmphasisRanges(emphasizeLines, highlightedLogicalLineCount)
|
|
119
|
+
}
|
|
120
|
+
let lineNumberPlan = null
|
|
121
|
+
if (needLineNumber && (lineNumberSkipValue !== undefined || lineNumberResetValue !== undefined)) {
|
|
122
|
+
ensureLogicalLineCounts()
|
|
123
|
+
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberResetValue, sourceLogicalLineCount, highlightedLogicalLineCount)
|
|
124
|
+
}
|
|
109
125
|
const needEmphasis = normalizedEmphasis.length > 0
|
|
110
126
|
const needEndSpan = opt.lineEndSpanThreshold > 0
|
|
111
127
|
const useHighlightPre = opt.useHighlightPre && hasHighlightPre
|
|
112
128
|
|
|
113
129
|
if (!useHighlightPre && commentMarkValue && sourceContent.indexOf(commentMarkValue) !== -1) {
|
|
114
|
-
|
|
115
|
-
const highlightedLogicalLineCount = getLogicalLineCount(content)
|
|
130
|
+
ensureLogicalLineCounts()
|
|
116
131
|
if (highlightedLogicalLineCount === sourceLogicalLineCount) {
|
|
117
132
|
const rawLines = sourceContent.split('\n')
|
|
118
133
|
for (let i = 0; i < rawLines.length; i++) {
|
|
@@ -140,6 +155,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
140
155
|
br,
|
|
141
156
|
commentLines,
|
|
142
157
|
commentLineClass,
|
|
158
|
+
lineNumberPlan,
|
|
143
159
|
)
|
|
144
160
|
if (timingEnabled) addTimingMs(timings, 'lineSplitMs', getNowMs() - splitStartedAt)
|
|
145
161
|
}
|
|
@@ -149,7 +165,7 @@ const renderFenceMarkup = (context, md, opt, slf) => {
|
|
|
149
165
|
renderer: 'markup',
|
|
150
166
|
useHighlightPre,
|
|
151
167
|
hasHighlightPre,
|
|
152
|
-
disabledFeatures: useHighlightPre ? ['setLineNumber', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'] : [],
|
|
168
|
+
disabledFeatures: useHighlightPre ? ['setLineNumber', 'line-number-skip', 'line-number-reset', 'setEmphasizeLines', 'lineEndSpanThreshold', 'comment-mark', 'samp'] : [],
|
|
153
169
|
}
|
|
154
170
|
if (timingEnabled) decision.timings = finalizeFenceTimings(timings, fenceStartedAt)
|
|
155
171
|
emitFenceDecision(opt, decision)
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
|
|
9
9
|
const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
|
|
10
10
|
const preLineTag = '<span class="pre-line">'
|
|
11
|
+
const preLineNoNumberClass = 'pre-line-no-number'
|
|
11
12
|
const emphOpenTag = '<span class="pre-lines-emphasis">'
|
|
12
13
|
const commentLineClass = 'pre-line-comment'
|
|
13
14
|
const closeTag = '</span>'
|
|
@@ -15,6 +16,7 @@ const closeTagLen = closeTag.length
|
|
|
15
16
|
const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
|
|
16
17
|
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
17
18
|
const nonNegativeIntReg = /^\d+$/
|
|
19
|
+
const positiveIntReg = /^[1-9]\d*$/
|
|
18
20
|
|
|
19
21
|
const getLineVisualLengthIgnoringTags = (line, threshold) => {
|
|
20
22
|
let len = 0
|
|
@@ -67,6 +69,23 @@ const applyLineEndAlias = (opt, sourceOption) => {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
const createCommonFenceOptionDefaults = (md) => {
|
|
73
|
+
return {
|
|
74
|
+
attrsOrder: ['class', 'id', 'data-*', 'style'],
|
|
75
|
+
setHighlight: true,
|
|
76
|
+
setLineNumber: true,
|
|
77
|
+
setEmphasizeLines: true,
|
|
78
|
+
lineEndSpanThreshold: 0,
|
|
79
|
+
lineEndSpanClass: 'pre-lineend-spacer',
|
|
80
|
+
setPreWrapStyle: true,
|
|
81
|
+
useHighlightPre: false,
|
|
82
|
+
onFenceDecision: null,
|
|
83
|
+
onFenceDecisionTiming: false,
|
|
84
|
+
sampLang: 'shell,console',
|
|
85
|
+
langPrefix: md.options.langPrefix || 'language-',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
const finalizeCommonFenceOption = (opt) => {
|
|
71
90
|
opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
|
|
72
91
|
opt._attrOrderIndex = createAttrOrderIndexGetter(opt.attrsOrder || [])
|
|
@@ -88,6 +107,14 @@ const parseStartNumber = (val) => {
|
|
|
88
107
|
return num
|
|
89
108
|
}
|
|
90
109
|
|
|
110
|
+
const parsePositiveLineIndex = (val) => {
|
|
111
|
+
const str = String(val ?? '').trim()
|
|
112
|
+
if (!positiveIntReg.test(str)) return null
|
|
113
|
+
const num = Number(str)
|
|
114
|
+
if (!Number.isSafeInteger(num) || num <= 0) return null
|
|
115
|
+
return num
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
const getLogicalLineCount = (text) => {
|
|
92
119
|
const str = String(text ?? '')
|
|
93
120
|
if (!str) return 0
|
|
@@ -103,7 +130,7 @@ const getLogicalLineCount = (text) => {
|
|
|
103
130
|
return count
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
const
|
|
133
|
+
const parseLineRangeList = (attrVal, parseValue) => {
|
|
107
134
|
const str = String(attrVal ?? '')
|
|
108
135
|
if (!str) return []
|
|
109
136
|
const result = []
|
|
@@ -112,22 +139,36 @@ const getEmphasizeLines = (attrVal) => {
|
|
|
112
139
|
if (!part) continue
|
|
113
140
|
const hyphen = part.indexOf('-')
|
|
114
141
|
if (hyphen === -1) {
|
|
115
|
-
const n =
|
|
116
|
-
if (
|
|
142
|
+
const n = parseValue(part)
|
|
143
|
+
if (n != null) result.push([n, n])
|
|
117
144
|
continue
|
|
118
145
|
}
|
|
119
146
|
const start = part.slice(0, hyphen).trim()
|
|
120
147
|
const end = part.slice(hyphen + 1).trim()
|
|
121
|
-
const s = start ?
|
|
122
|
-
const e = end ?
|
|
148
|
+
const s = start ? parseValue(start) : null
|
|
149
|
+
const e = end ? parseValue(end) : null
|
|
123
150
|
if (s == null && e == null) continue
|
|
124
|
-
if (
|
|
125
|
-
if (
|
|
151
|
+
if (start && s == null) continue
|
|
152
|
+
if (end && e == null) continue
|
|
126
153
|
result.push([s, e])
|
|
127
154
|
}
|
|
128
155
|
return result
|
|
129
156
|
}
|
|
130
157
|
|
|
158
|
+
const parsePositiveLineNumber = (val) => {
|
|
159
|
+
const num = Number(val)
|
|
160
|
+
if (!Number.isFinite(num) || num <= 0) return null
|
|
161
|
+
return num
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const getEmphasizeLines = (attrVal) => {
|
|
165
|
+
return parseLineRangeList(attrVal, parsePositiveLineNumber)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const getLineNumberSkipRanges = (attrVal) => {
|
|
169
|
+
return parseLineRangeList(attrVal, parsePositiveLineIndex)
|
|
170
|
+
}
|
|
171
|
+
|
|
131
172
|
const normalizeEmphasisRanges = (emphasizeLines, maxLine) => {
|
|
132
173
|
const normalized = []
|
|
133
174
|
if (!emphasizeLines || !emphasizeLines.length || maxLine <= 0) return normalized
|
|
@@ -149,9 +190,87 @@ const normalizeEmphasisRanges = (emphasizeLines, maxLine) => {
|
|
|
149
190
|
return normalized
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
const
|
|
193
|
+
const getLineNumberResetEntries = (attrVal) => {
|
|
194
|
+
const str = String(attrVal ?? '')
|
|
195
|
+
if (!str) return []
|
|
196
|
+
const result = []
|
|
197
|
+
for (const partRaw of str.split(',')) {
|
|
198
|
+
const part = partRaw.trim()
|
|
199
|
+
if (!part) continue
|
|
200
|
+
const colon = part.indexOf(':')
|
|
201
|
+
if (colon === -1) continue
|
|
202
|
+
const lineNumber = parsePositiveLineIndex(part.slice(0, colon).trim())
|
|
203
|
+
const resetNumber = parseStartNumber(part.slice(colon + 1).trim())
|
|
204
|
+
if (lineNumber == null || resetNumber == null) continue
|
|
205
|
+
result.push([lineNumber, resetNumber])
|
|
206
|
+
}
|
|
207
|
+
return result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const createSparseLineFlagMap = (ranges, maxLine) => {
|
|
211
|
+
const normalized = normalizeEmphasisRanges(ranges, maxLine)
|
|
212
|
+
if (!normalized.length) return null
|
|
213
|
+
const flags = []
|
|
214
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
215
|
+
const range = normalized[i]
|
|
216
|
+
for (let line = range[0]; line <= range[1]; line++) flags[line - 1] = true
|
|
217
|
+
}
|
|
218
|
+
return flags
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const createSparseLineResetMap = (entries, maxLine) => {
|
|
222
|
+
if (!entries || !entries.length || maxLine <= 0) return null
|
|
223
|
+
const resets = []
|
|
224
|
+
let hasValue = false
|
|
225
|
+
for (let i = 0; i < entries.length; i++) {
|
|
226
|
+
const line = entries[i][0]
|
|
227
|
+
const value = entries[i][1]
|
|
228
|
+
if (!Number.isSafeInteger(line) || line <= 0 || line > maxLine) continue
|
|
229
|
+
resets[line - 1] = value
|
|
230
|
+
hasValue = true
|
|
231
|
+
}
|
|
232
|
+
return hasValue ? resets : null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const buildAdvancedLineNumberPlan = (skipRanges, resetEntries, sourceLineCount, renderedLineCount) => {
|
|
236
|
+
const hasSkip = !!(skipRanges && skipRanges.length)
|
|
237
|
+
const hasReset = !!(resetEntries && resetEntries.length)
|
|
238
|
+
if (!hasSkip && !hasReset) return null
|
|
239
|
+
if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
|
|
240
|
+
if (sourceLineCount !== renderedLineCount) return null
|
|
241
|
+
const hidden = hasSkip ? createSparseLineFlagMap(skipRanges, sourceLineCount) : null
|
|
242
|
+
const resets = hasReset ? createSparseLineResetMap(resetEntries, sourceLineCount) : null
|
|
243
|
+
if (!hidden && !resets) return null
|
|
244
|
+
return { hidden, resets }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const resolveAdvancedLineNumberPlan = (lineNumberSkipValue, lineNumberResetValue, sourceLineCount, renderedLineCount) => {
|
|
248
|
+
const hasSkipValue = lineNumberSkipValue !== undefined
|
|
249
|
+
const hasResetValue = lineNumberResetValue !== undefined
|
|
250
|
+
if (!hasSkipValue && !hasResetValue) return null
|
|
251
|
+
if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
|
|
252
|
+
if (sourceLineCount !== renderedLineCount) return null
|
|
253
|
+
const skipRanges = hasSkipValue ? getLineNumberSkipRanges(lineNumberSkipValue) : null
|
|
254
|
+
const resetEntries = hasResetValue ? getLineNumberResetEntries(lineNumberResetValue) : null
|
|
255
|
+
return buildAdvancedLineNumberPlan(skipRanges, resetEntries, sourceLineCount, renderedLineCount)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getPreLineOpenTag = (lineNumberPlan, lineIndex) => {
|
|
259
|
+
if (!lineNumberPlan) return preLineTag
|
|
260
|
+
const hidden = !!(lineNumberPlan.hidden && lineNumberPlan.hidden[lineIndex])
|
|
261
|
+
const resetNumber = lineNumberPlan.resets ? lineNumberPlan.resets[lineIndex] : undefined
|
|
262
|
+
if (!hidden && resetNumber == null) return preLineTag
|
|
263
|
+
let tag = '<span class="pre-line'
|
|
264
|
+
if (hidden) tag += ' ' + preLineNoNumberClass
|
|
265
|
+
tag += '"'
|
|
266
|
+
if (resetNumber != null) tag += ` style="counter-set:pre-line-number ${resetNumber};"`
|
|
267
|
+
return tag + '>'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan) => {
|
|
153
271
|
const lines = content.split(br)
|
|
154
272
|
const max = lines.length
|
|
273
|
+
const hasDynamicLineNumberPlan = !!lineNumberPlan
|
|
155
274
|
let emIdx = 0
|
|
156
275
|
let emStart = -1
|
|
157
276
|
let emEnd = -1
|
|
@@ -222,7 +341,7 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
|
|
|
222
341
|
}
|
|
223
342
|
|
|
224
343
|
if (needLineNumber && notLastLine) {
|
|
225
|
-
line = preLineTag + line + closeTag
|
|
344
|
+
line = (hasDynamicLineNumberPlan ? getPreLineOpenTag(lineNumberPlan, n) : preLineTag) + line + closeTag
|
|
226
345
|
}
|
|
227
346
|
|
|
228
347
|
if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
|
|
@@ -279,6 +398,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
279
398
|
|
|
280
399
|
let startNumber = -1
|
|
281
400
|
let emphasizeLines = []
|
|
401
|
+
let lineNumberSkipValue
|
|
402
|
+
let lineNumberResetValue
|
|
282
403
|
let wrapEnabled = false
|
|
283
404
|
let preWrapValue
|
|
284
405
|
let commentMarkValue
|
|
@@ -290,12 +411,16 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
290
411
|
let dataPreEmphasisIndex = -1
|
|
291
412
|
let styleIndex = -1
|
|
292
413
|
let dataPreCommentIndex = -1
|
|
414
|
+
let dataPreLineNumberSkipIndex = -1
|
|
415
|
+
let dataPreLineNumberResetIndex = -1
|
|
293
416
|
let startValue
|
|
294
417
|
let emphasisValue
|
|
295
418
|
let styleValue
|
|
296
419
|
let sawCommentMark = false
|
|
297
420
|
let sawStartAttr = false
|
|
298
421
|
let sawEmphasisAttr = false
|
|
422
|
+
let sawLineNumberSkipAttr = false
|
|
423
|
+
let sawLineNumberResetAttr = false
|
|
299
424
|
const appendOrder = []
|
|
300
425
|
|
|
301
426
|
for (const attr of token.attrs) {
|
|
@@ -320,6 +445,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
320
445
|
dataPreStartIndex = newAttrs.length
|
|
321
446
|
newAttrs.push(attr)
|
|
322
447
|
break
|
|
448
|
+
case 'line-number-start':
|
|
323
449
|
case 'start':
|
|
324
450
|
case 'pre-start':
|
|
325
451
|
startNumber = parseStartNumber(val) ?? -1
|
|
@@ -338,6 +464,16 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
338
464
|
commentMarkValue = val
|
|
339
465
|
newAttrs.push(attr)
|
|
340
466
|
break
|
|
467
|
+
case 'data-pre-line-number-skip':
|
|
468
|
+
dataPreLineNumberSkipIndex = newAttrs.length
|
|
469
|
+
lineNumberSkipValue = val
|
|
470
|
+
newAttrs.push(attr)
|
|
471
|
+
break
|
|
472
|
+
case 'data-pre-line-number-reset':
|
|
473
|
+
dataPreLineNumberResetIndex = newAttrs.length
|
|
474
|
+
lineNumberResetValue = val
|
|
475
|
+
newAttrs.push(attr)
|
|
476
|
+
break
|
|
341
477
|
case 'em-lines':
|
|
342
478
|
case 'emphasize-lines':
|
|
343
479
|
if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
|
|
@@ -354,6 +490,22 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
354
490
|
sawCommentMark = true
|
|
355
491
|
}
|
|
356
492
|
break
|
|
493
|
+
case 'line-number-skip':
|
|
494
|
+
case 'pre-line-number-skip':
|
|
495
|
+
lineNumberSkipValue = val
|
|
496
|
+
if (!sawLineNumberSkipAttr) {
|
|
497
|
+
appendOrder.push('line-number-skip')
|
|
498
|
+
sawLineNumberSkipAttr = true
|
|
499
|
+
}
|
|
500
|
+
break
|
|
501
|
+
case 'line-number-reset':
|
|
502
|
+
case 'pre-line-number-reset':
|
|
503
|
+
lineNumberResetValue = val
|
|
504
|
+
if (!sawLineNumberResetAttr) {
|
|
505
|
+
appendOrder.push('line-number-reset')
|
|
506
|
+
sawLineNumberResetAttr = true
|
|
507
|
+
}
|
|
508
|
+
break
|
|
357
509
|
case 'data-pre-wrap':
|
|
358
510
|
preWrapValue = val
|
|
359
511
|
if (val === '' || val === 'true') wrapEnabled = true
|
|
@@ -370,6 +522,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
370
522
|
if (startValue !== undefined && dataPreStartIndex >= 0) newAttrs[dataPreStartIndex][1] = startValue
|
|
371
523
|
if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) newAttrs[dataPreEmphasisIndex][1] = emphasisValue
|
|
372
524
|
if (commentMarkValue !== undefined && dataPreCommentIndex >= 0) newAttrs[dataPreCommentIndex][1] = commentMarkValue
|
|
525
|
+
if (lineNumberSkipValue !== undefined && dataPreLineNumberSkipIndex >= 0) newAttrs[dataPreLineNumberSkipIndex][1] = lineNumberSkipValue
|
|
526
|
+
if (lineNumberResetValue !== undefined && dataPreLineNumberResetIndex >= 0) newAttrs[dataPreLineNumberResetIndex][1] = lineNumberResetValue
|
|
373
527
|
|
|
374
528
|
for (const kind of appendOrder) {
|
|
375
529
|
if (kind === 'start' && dataPreStartIndex === -1 && startValue !== undefined) {
|
|
@@ -378,6 +532,10 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
378
532
|
newAttrs.push(['data-pre-emphasis', emphasisValue])
|
|
379
533
|
} else if (kind === 'comment' && dataPreCommentIndex === -1 && commentMarkValue !== undefined) {
|
|
380
534
|
newAttrs.push(['data-pre-comment-mark', commentMarkValue])
|
|
535
|
+
} else if (kind === 'line-number-skip' && dataPreLineNumberSkipIndex === -1 && lineNumberSkipValue !== undefined) {
|
|
536
|
+
newAttrs.push(['data-pre-line-number-skip', lineNumberSkipValue])
|
|
537
|
+
} else if (kind === 'line-number-reset' && dataPreLineNumberResetIndex === -1 && lineNumberResetValue !== undefined) {
|
|
538
|
+
newAttrs.push(['data-pre-line-number-reset', lineNumberResetValue])
|
|
381
539
|
}
|
|
382
540
|
}
|
|
383
541
|
|
|
@@ -401,6 +559,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
401
559
|
lang,
|
|
402
560
|
startNumber,
|
|
403
561
|
emphasizeLines,
|
|
562
|
+
lineNumberSkipValue,
|
|
563
|
+
lineNumberResetValue,
|
|
404
564
|
wrapEnabled,
|
|
405
565
|
preWrapValue,
|
|
406
566
|
commentMarkValue,
|
|
@@ -414,6 +574,7 @@ export {
|
|
|
414
574
|
addTimingMs,
|
|
415
575
|
applyLineEndAlias,
|
|
416
576
|
commentLineClass,
|
|
577
|
+
createCommonFenceOptionDefaults,
|
|
417
578
|
emitFenceDecision,
|
|
418
579
|
finalizeCommonFenceOption,
|
|
419
580
|
finalizeFenceTimings,
|
|
@@ -424,5 +585,6 @@ export {
|
|
|
424
585
|
orderTokenAttrs,
|
|
425
586
|
preWrapStyle,
|
|
426
587
|
prepareFenceRenderContext,
|
|
588
|
+
resolveAdvancedLineNumberPlan,
|
|
427
589
|
splitFenceBlockToLines,
|
|
428
590
|
}
|