@peaceroad/markdown-it-renderer-fence 0.5.0 → 0.6.1
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 +74 -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 +175 -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/set controls via `line-number-skip` / `line-number-set`.
|
|
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-set="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-set="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,54 @@ 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-set`
|
|
212
239
|
- `em-lines`
|
|
213
240
|
- line-end spacer
|
|
214
241
|
- `comment-mark`
|
|
215
242
|
- `samp` conversion
|
|
216
243
|
|
|
244
|
+
Reference line-number CSS contract:
|
|
245
|
+
|
|
246
|
+
Minimum counter contract:
|
|
247
|
+
|
|
248
|
+
```css
|
|
249
|
+
pre :is(code, samp)[data-pre-start] .pre-line::before {
|
|
250
|
+
content: counter(pre-line-number);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
pre :is(code, samp)[data-pre-start] .pre-line::after {
|
|
254
|
+
content: "";
|
|
255
|
+
counter-increment: pre-line-number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pre :is(code, samp)[data-pre-start] .pre-line.pre-line-no-number::before {
|
|
259
|
+
content: "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pre :is(code, samp)[data-pre-start] .pre-line.pre-line-no-number::after {
|
|
263
|
+
counter-increment: none;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Recommended display CSS:
|
|
268
|
+
|
|
269
|
+
- See [`example/line-number.css`](./example/line-number.css) for the full reference stylesheet.
|
|
270
|
+
- See [`example/line-number-sample.html`](./example/line-number-sample.html) for Markdown / Preview / HTML side-by-side examples.
|
|
271
|
+
|
|
272
|
+
Note:
|
|
273
|
+
|
|
274
|
+
- renderer-fence emits `counter-set` as the displayed line number value itself
|
|
275
|
+
- the reference CSS increments the counter in `span.pre-line::after`, so `start="1"` maps directly to `counter-set:pre-line-number 1;`
|
|
276
|
+
- `line-number-set="6:136"` similarly emits `counter-set:pre-line-number 136;` on that line wrapper
|
|
277
|
+
- the minimum contract above is the required part; `example/line-number.css` adds gutter width, divider line, and whitespace handling
|
|
278
|
+
- if a consumer uses a different counter strategy, its CSS must be aligned with this HTML contract
|
|
279
|
+
|
|
217
280
|
### Markup Options
|
|
218
281
|
|
|
219
282
|
- `attrsOrder` (default: `['class', 'id', 'data-*', 'style']`): output attribute order (`data-*` wildcard supported).
|
|
220
283
|
- `setHighlight` (default: `true`): call `md.options.highlight` when available.
|
|
221
|
-
- `setLineNumber` (default: `true`): enable line wrapper spans when `start` is valid.
|
|
284
|
+
- `setLineNumber` (default: `true`): enable line wrapper spans when `start` / `line-number-start` is valid.
|
|
222
285
|
- `setEmphasizeLines` (default: `true`): enable `em-lines` / `emphasize-lines`.
|
|
223
286
|
- `lineEndSpanThreshold` (default: `0`): append line-end spacer span when visual width threshold is met.
|
|
224
287
|
- `setLineEndSpan`: alias of `lineEndSpanThreshold`.
|
|
@@ -236,10 +299,19 @@ echo 1
|
|
|
236
299
|
- supports open-ended forms (`3-`, `-2`)
|
|
237
300
|
- reversed ranges are normalized (`5-3` behaves as `3-5`)
|
|
238
301
|
|
|
302
|
+
Line-number attr syntax note:
|
|
303
|
+
|
|
304
|
+
- `line-number-start` is the long form of `start`; rendered output still uses `data-pre-start`.
|
|
305
|
+
- `line-number-skip` supports single values (`2`), ranges (`4-6`), and open-ended forms (`3-`).
|
|
306
|
+
- `line-number-set` uses `line:number` pairs (for example `6:136,14:220`).
|
|
307
|
+
- `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
|
+
- `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
|
+
|
|
239
310
|
Migration note (`0.5.0`):
|
|
240
311
|
|
|
241
312
|
- `comment-line` / `data-pre-comment-line` / `pre-comment-line` were removed
|
|
242
313
|
- use `comment-mark` / `data-pre-comment-mark` / `pre-line-comment`
|
|
314
|
+
- `line-number-reset` was removed; use `line-number-set`
|
|
243
315
|
|
|
244
316
|
## Custom Highlight API Mode (Experimental / Advanced)
|
|
245
317
|
|
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.1",
|
|
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, lineNumberSetValue, 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 || lineNumberSetValue !== undefined)) {
|
|
57
|
+
const logicalLineCount = getLogicalLineCount(token.content)
|
|
58
|
+
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, 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 lineNumberSetValue = context.lineNumberSetValue
|
|
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-set', '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, lineNumberSetValue, 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, lineNumberSetValue, 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 lineNumberSetValue = context.lineNumberSetValue
|
|
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 || lineNumberSetValue !== undefined)) {
|
|
122
|
+
ensureLogicalLineCounts()
|
|
123
|
+
lineNumberPlan = resolveAdvancedLineNumberPlan(lineNumberSkipValue, lineNumberSetValue, 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-set', '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,86 @@ const normalizeEmphasisRanges = (emphasizeLines, maxLine) => {
|
|
|
149
190
|
return normalized
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
const
|
|
193
|
+
const getLineNumberSetEntries = (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 setNumber = parseStartNumber(part.slice(colon + 1).trim())
|
|
204
|
+
if (lineNumber == null || setNumber == null) continue
|
|
205
|
+
result.push([lineNumber, setNumber])
|
|
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 createSparseLineSetMap = (entries, maxLine, hidden) => {
|
|
222
|
+
if (!entries || !entries.length || maxLine <= 0) return null
|
|
223
|
+
const sets = []
|
|
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
|
+
if (hidden && hidden[line - 1]) continue
|
|
230
|
+
sets[line - 1] = value
|
|
231
|
+
hasValue = true
|
|
232
|
+
}
|
|
233
|
+
return hasValue ? sets : null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const buildAdvancedLineNumberPlan = (skipRanges, setEntries, lineCount) => {
|
|
237
|
+
const hasSkip = !!(skipRanges && skipRanges.length)
|
|
238
|
+
const hasSet = !!(setEntries && setEntries.length)
|
|
239
|
+
if (!hasSkip && !hasSet) return null
|
|
240
|
+
const hidden = hasSkip ? createSparseLineFlagMap(skipRanges, lineCount) : null
|
|
241
|
+
const sets = hasSet ? createSparseLineSetMap(setEntries, lineCount, hidden) : null
|
|
242
|
+
if (!hidden && !sets) return null
|
|
243
|
+
return { hidden, sets }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const resolveAdvancedLineNumberPlan = (lineNumberSkipValue, lineNumberSetValue, sourceLineCount, renderedLineCount) => {
|
|
247
|
+
const hasSkipValue = lineNumberSkipValue !== undefined
|
|
248
|
+
const hasSetValue = lineNumberSetValue !== undefined
|
|
249
|
+
if (!hasSkipValue && !hasSetValue) return null
|
|
250
|
+
if (!Number.isSafeInteger(sourceLineCount) || sourceLineCount <= 0) return null
|
|
251
|
+
if (sourceLineCount !== renderedLineCount) return null
|
|
252
|
+
const skipRanges = hasSkipValue ? getLineNumberSkipRanges(lineNumberSkipValue) : null
|
|
253
|
+
const setEntries = hasSetValue ? getLineNumberSetEntries(lineNumberSetValue) : null
|
|
254
|
+
return buildAdvancedLineNumberPlan(skipRanges, setEntries, sourceLineCount)
|
|
255
|
+
}
|
|
256
|
+
|
|
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
|
|
262
|
+
let tag = '<span class="pre-line'
|
|
263
|
+
if (hidden) tag += ' ' + preLineNoNumberClass
|
|
264
|
+
tag += '"'
|
|
265
|
+
if (setNumber != null) tag += ` style="counter-set:pre-line-number ${setNumber};"`
|
|
266
|
+
return tag + '>'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass, lineNumberPlan) => {
|
|
153
270
|
const lines = content.split(br)
|
|
154
271
|
const max = lines.length
|
|
272
|
+
const hasDynamicLineNumberPlan = !!lineNumberPlan
|
|
155
273
|
let emIdx = 0
|
|
156
274
|
let emStart = -1
|
|
157
275
|
let emEnd = -1
|
|
@@ -222,7 +340,7 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
|
|
|
222
340
|
}
|
|
223
341
|
|
|
224
342
|
if (needLineNumber && notLastLine) {
|
|
225
|
-
line = preLineTag + line + closeTag
|
|
343
|
+
line = (hasDynamicLineNumberPlan ? getPreLineOpenTag(lineNumberPlan, n) : preLineTag) + line + closeTag
|
|
226
344
|
}
|
|
227
345
|
|
|
228
346
|
if (needEmphasis && emIdx < emphasizeLines.length && emStart === n + 1) {
|
|
@@ -279,6 +397,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
279
397
|
|
|
280
398
|
let startNumber = -1
|
|
281
399
|
let emphasizeLines = []
|
|
400
|
+
let lineNumberSkipValue
|
|
401
|
+
let lineNumberSetValue
|
|
282
402
|
let wrapEnabled = false
|
|
283
403
|
let preWrapValue
|
|
284
404
|
let commentMarkValue
|
|
@@ -290,12 +410,16 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
290
410
|
let dataPreEmphasisIndex = -1
|
|
291
411
|
let styleIndex = -1
|
|
292
412
|
let dataPreCommentIndex = -1
|
|
413
|
+
let dataPreLineNumberSkipIndex = -1
|
|
414
|
+
let dataPreLineNumberSetIndex = -1
|
|
293
415
|
let startValue
|
|
294
416
|
let emphasisValue
|
|
295
417
|
let styleValue
|
|
296
418
|
let sawCommentMark = false
|
|
297
419
|
let sawStartAttr = false
|
|
298
420
|
let sawEmphasisAttr = false
|
|
421
|
+
let sawLineNumberSkipAttr = false
|
|
422
|
+
let sawLineNumberSetAttr = false
|
|
299
423
|
const appendOrder = []
|
|
300
424
|
|
|
301
425
|
for (const attr of token.attrs) {
|
|
@@ -320,6 +444,7 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
320
444
|
dataPreStartIndex = newAttrs.length
|
|
321
445
|
newAttrs.push(attr)
|
|
322
446
|
break
|
|
447
|
+
case 'line-number-start':
|
|
323
448
|
case 'start':
|
|
324
449
|
case 'pre-start':
|
|
325
450
|
startNumber = parseStartNumber(val) ?? -1
|
|
@@ -338,6 +463,18 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
338
463
|
commentMarkValue = val
|
|
339
464
|
newAttrs.push(attr)
|
|
340
465
|
break
|
|
466
|
+
case 'data-pre-line-number-skip':
|
|
467
|
+
dataPreLineNumberSkipIndex = newAttrs.length
|
|
468
|
+
lineNumberSkipValue = val
|
|
469
|
+
newAttrs.push(attr)
|
|
470
|
+
break
|
|
471
|
+
case 'data-pre-line-number-set':
|
|
472
|
+
dataPreLineNumberSetIndex = newAttrs.length
|
|
473
|
+
lineNumberSetValue = val
|
|
474
|
+
newAttrs.push(attr)
|
|
475
|
+
break
|
|
476
|
+
case 'data-pre-line-number-reset':
|
|
477
|
+
break
|
|
341
478
|
case 'em-lines':
|
|
342
479
|
case 'emphasize-lines':
|
|
343
480
|
if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
|
|
@@ -354,6 +491,25 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
354
491
|
sawCommentMark = true
|
|
355
492
|
}
|
|
356
493
|
break
|
|
494
|
+
case 'line-number-skip':
|
|
495
|
+
case 'pre-line-number-skip':
|
|
496
|
+
lineNumberSkipValue = val
|
|
497
|
+
if (!sawLineNumberSkipAttr) {
|
|
498
|
+
appendOrder.push('line-number-skip')
|
|
499
|
+
sawLineNumberSkipAttr = true
|
|
500
|
+
}
|
|
501
|
+
break
|
|
502
|
+
case 'line-number-set':
|
|
503
|
+
case 'pre-line-number-set':
|
|
504
|
+
lineNumberSetValue = val
|
|
505
|
+
if (!sawLineNumberSetAttr) {
|
|
506
|
+
appendOrder.push('line-number-set')
|
|
507
|
+
sawLineNumberSetAttr = true
|
|
508
|
+
}
|
|
509
|
+
break
|
|
510
|
+
case 'line-number-reset':
|
|
511
|
+
case 'pre-line-number-reset':
|
|
512
|
+
break
|
|
357
513
|
case 'data-pre-wrap':
|
|
358
514
|
preWrapValue = val
|
|
359
515
|
if (val === '' || val === 'true') wrapEnabled = true
|
|
@@ -370,6 +526,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
370
526
|
if (startValue !== undefined && dataPreStartIndex >= 0) newAttrs[dataPreStartIndex][1] = startValue
|
|
371
527
|
if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) newAttrs[dataPreEmphasisIndex][1] = emphasisValue
|
|
372
528
|
if (commentMarkValue !== undefined && dataPreCommentIndex >= 0) newAttrs[dataPreCommentIndex][1] = commentMarkValue
|
|
529
|
+
if (lineNumberSkipValue !== undefined && dataPreLineNumberSkipIndex >= 0) newAttrs[dataPreLineNumberSkipIndex][1] = lineNumberSkipValue
|
|
530
|
+
if (lineNumberSetValue !== undefined && dataPreLineNumberSetIndex >= 0) newAttrs[dataPreLineNumberSetIndex][1] = lineNumberSetValue
|
|
373
531
|
|
|
374
532
|
for (const kind of appendOrder) {
|
|
375
533
|
if (kind === 'start' && dataPreStartIndex === -1 && startValue !== undefined) {
|
|
@@ -378,6 +536,10 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
378
536
|
newAttrs.push(['data-pre-emphasis', emphasisValue])
|
|
379
537
|
} else if (kind === 'comment' && dataPreCommentIndex === -1 && commentMarkValue !== undefined) {
|
|
380
538
|
newAttrs.push(['data-pre-comment-mark', commentMarkValue])
|
|
539
|
+
} else if (kind === 'line-number-skip' && dataPreLineNumberSkipIndex === -1 && lineNumberSkipValue !== undefined) {
|
|
540
|
+
newAttrs.push(['data-pre-line-number-skip', lineNumberSkipValue])
|
|
541
|
+
} else if (kind === 'line-number-set' && dataPreLineNumberSetIndex === -1 && lineNumberSetValue !== undefined) {
|
|
542
|
+
newAttrs.push(['data-pre-line-number-set', lineNumberSetValue])
|
|
381
543
|
}
|
|
382
544
|
}
|
|
383
545
|
|
|
@@ -401,6 +563,8 @@ const prepareFenceRenderContext = (tokens, idx, opt) => {
|
|
|
401
563
|
lang,
|
|
402
564
|
startNumber,
|
|
403
565
|
emphasizeLines,
|
|
566
|
+
lineNumberSkipValue,
|
|
567
|
+
lineNumberSetValue,
|
|
404
568
|
wrapEnabled,
|
|
405
569
|
preWrapValue,
|
|
406
570
|
commentMarkValue,
|
|
@@ -414,6 +578,7 @@ export {
|
|
|
414
578
|
addTimingMs,
|
|
415
579
|
applyLineEndAlias,
|
|
416
580
|
commentLineClass,
|
|
581
|
+
createCommonFenceOptionDefaults,
|
|
417
582
|
emitFenceDecision,
|
|
418
583
|
finalizeCommonFenceOption,
|
|
419
584
|
finalizeFenceTimings,
|
|
@@ -424,5 +589,6 @@ export {
|
|
|
424
589
|
orderTokenAttrs,
|
|
425
590
|
preWrapStyle,
|
|
426
591
|
prepareFenceRenderContext,
|
|
592
|
+
resolveAdvancedLineNumberPlan,
|
|
427
593
|
splitFenceBlockToLines,
|
|
428
594
|
}
|