@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 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.5.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
@@ -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 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
- 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 || 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
- 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-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 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,87 @@ 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 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
  }