@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 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.5.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
@@ -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
- attrsOrder: ['class', 'id', 'data-*', 'style'],
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
- const normalizedEmphasis = (opt.setEmphasizeLines && emphasizeLines.length > 0)
107
- ? normalizeEmphasisRanges(emphasizeLines, getLogicalLineCount(content))
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
- const sourceLogicalLineCount = getLogicalLineCount(sourceContent)
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 getEmphasizeLines = (attrVal) => {
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 = Number(part)
116
- if (Number.isFinite(n) && n > 0) result.push([n, n])
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 ? Number(start) : null
122
- const e = end ? Number(end) : null
148
+ const s = start ? parseValue(start) : null
149
+ const e = end ? parseValue(end) : null
123
150
  if (s == null && e == null) continue
124
- if (s != null && (!Number.isFinite(s) || s <= 0)) continue
125
- if (e != null && (!Number.isFinite(e) || e <= 0)) continue
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 splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass) => {
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
  }