@peaceroad/markdown-it-renderer-fence 0.3.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +30 -2
  2. package/index.js +97 -16
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -71,6 +71,7 @@ $ pwd
71
71
  ## Add span elements to display line number.
72
72
 
73
73
  Add `start` or `data-pre-start` attribute by adding attributes used markdown-it-attrs.
74
+ Line numbering is enabled only when `start` is a non-negative integer (`0`, `1`, `2`, ...).
74
75
 
75
76
  ~~~md
76
77
  ```js {start="1"}
@@ -89,9 +90,11 @@ md.render('Nyaan')
89
90
 
90
91
  CSS example: <https://codepen.io/peaceroad/pen/qBGpYGK>
91
92
 
93
+ If `start` is empty or not a non-negative integer (for example `start=""`, `start="abc"`, `start="1.5"`), the attribute is kept as `data-pre-start` but line-number wrapping/counter style is not enabled.
94
+
92
95
  ## Add span elements to add background color to the row ranges.
93
96
 
94
- Add `em-lines` or `emphasize-lines` attribute by by adding attributes used markdown-it-attrs.
97
+ Add `em-lines` or `emphasize-lines` attribute by adding attributes used markdown-it-attrs.
95
98
  Open-ended ranges are supported: `-5` (1..5), `3-` (3..last), `-` (all).
96
99
 
97
100
  ~~~md
@@ -133,6 +136,27 @@ const longLine = 'This line is long but wraps.'
133
136
 
134
137
  When `setPreWrapStyle` is set to false, only `data-pre-wrap="true"` is added and the inline style is omitted.
135
138
 
139
+ ## Mark comment lines in code blocks
140
+
141
+ Add `comment-line` attribute to mark comment lines. Lines that start with the given marker (after leading whitespace) are wrapped with `<span class="pre-comment-line">`.
142
+
143
+ ~~~md
144
+ ```samp {comment-line="#"}
145
+ # comment
146
+ echo 1
147
+ ```
148
+ ~~~
149
+
150
+ ~~~html
151
+ <pre><samp data-pre-comment-line="#"><span class="pre-comment-line"># comment</span>
152
+ echo 1
153
+ </samp></pre>
154
+ ~~~
155
+
156
+ Note: this feature relies on line splitting and is disabled when `useHighlightPre` is true and `<pre><code>` is provided by the highlighter.
157
+ If a highlighter changes logical line count (for example by injecting extra lines), comment-line marking is skipped to avoid wrong line mapping.
158
+ CRLF/LF mixed newlines are supported for logical line-count checks.
159
+
136
160
 
137
161
  ## Options
138
162
 
@@ -142,8 +166,12 @@ The following options can be specified when initializing the plugin:
142
166
  - setHighlight: default true — enable calling highlight function (e.g., highlight.js).
143
167
  - setLineNumber: default true — wrap lines in spans for line numbers.
144
168
  - setEmphasizeLines: default true — enable emphasis based on emphasize-lines attribute.
145
- - setLineEndSpan: default 0 — character count threshold to append end-of-line span (0 to disable).
169
+ - lineEndSpanThreshold: default 0 — character count threshold to append end-of-line span (0 to disable).
170
+ - setLineEndSpan: alias for lineEndSpanThreshold.
146
171
  - lineEndSpanClass: default 'pre-lineend-spacer' — CSS class for end-of-line span.
147
172
  - setPreWrapStyle: default true — include inline pre-wrap styles on `<pre>` when wrap is enabled.
148
173
  - sampLang: default 'shell,console' — comma-separated list of languages (in addition to 'samp') that will be rendered using `<samp>`.
149
174
  - langPrefix: default md.options.langPrefix || 'language-' — prefix for language class on code blocks.
175
+ - useHighlightPre: default false — if highlight returns `<pre><code>`, keep that wrapper and skip line-splitting features; attributes are still merged.
176
+
177
+ When `useHighlightPre` is true and the highlight output contains `<pre><code>`, line-splitting features are disabled (setLineNumber, setEmphasizeLines, lineEndSpanThreshold, comment-line). In this mode `<samp>` conversion is not possible, so avoid `useHighlightPre` when you need samp output.
package/index.js CHANGED
@@ -5,12 +5,15 @@ const interAttrsSpaceReg = / +/
5
5
  const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
6
6
  const preLineTag = '<span class="pre-line">'
7
7
  const emphOpenTag = '<span class="pre-lines-emphasis">'
8
+ const commentLineClass = 'pre-comment-line'
8
9
  const closeTag = '</span>'
9
10
  const closeTagLen = closeTag.length
10
11
  const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
11
12
  const preCodeWrapperReg = /^\s*<pre\b([^>]*)>\s*<code\b([^>]*)>([\s\S]*?)<\/code>\s*<\/pre>\s*$/i
12
13
  const htmlAttrReg = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
13
14
  const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
15
+ const nonNegativeIntReg = /^\d+$/
16
+ const lineBreakReg = /\r\n|\n|\r/
14
17
 
15
18
  const appendStyleValue = (style, addition) => {
16
19
  if (!addition) return style
@@ -68,6 +71,21 @@ const mergeAttrSets = (target, source) => {
68
71
  }
69
72
  }
70
73
 
74
+ const parseStartNumber = (val) => {
75
+ if (val == null) return null
76
+ const text = String(val).trim()
77
+ if (!nonNegativeIntReg.test(text)) return null
78
+ const n = Number(text)
79
+ if (!Number.isSafeInteger(n)) return null
80
+ return n
81
+ }
82
+
83
+ const getLogicalLineCount = (text) => {
84
+ const lines = text.split(lineBreakReg)
85
+ if (lines.length === 0) return 0
86
+ return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length
87
+ }
88
+
71
89
  const getEmphasizeLines = (attrVal) => {
72
90
  const lines = []
73
91
  for (const range of attrVal.split(',')) {
@@ -95,10 +113,11 @@ const getEmphasizeLines = (attrVal) => {
95
113
  return lines
96
114
  }
97
115
 
98
- const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br) => {
99
- const lines = content.split(br)
116
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass) => {
117
+ const lines = content.split(lineBreakReg)
100
118
  const len = lines.length
101
119
  const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
120
+ const needComment = !!(commentLines && commentClass)
102
121
  const maxLine = len - (lines[len - 1] === '' ? 1 : 0)
103
122
  let emIdx = 0
104
123
  let emStart
@@ -167,6 +186,13 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
167
186
  lines[n+1] = `<${tagName}>` + (lines[n+1]||'')
168
187
  }
169
188
  }
189
+ }
190
+
191
+ if (needComment && commentLines[n]) {
192
+ line = `<span class="${commentClass}">` + line + closeTag
193
+ }
194
+
195
+ if (needLineNumber && n !== len - 1) {
170
196
  line = preLineTag + line + closeTag
171
197
  }
172
198
 
@@ -224,16 +250,14 @@ const orderTokenAttrs = (token, opt) => {
224
250
  const getFenceHtml = (tokens, idx, md, opt, slf) => {
225
251
  const token = tokens[idx]
226
252
  let content = token.content
227
- const info = token.info.trim()
228
- const match = info.match(infoReg)
253
+ const match = token.info.trim().match(infoReg)
229
254
  let lang = match ? match[1] : ''
230
255
 
231
256
  if (match && match[2]) {
232
257
  getInfoAttr(match[2]).forEach(([name, val]) => token.attrJoin(name, val))
233
258
  }
234
- let langClass = ''
235
259
  if (lang && lang !== 'samp') {
236
- langClass = opt.langPrefix + lang
260
+ const langClass = opt.langPrefix + lang
237
261
  const existingClass = token.attrGet('class')
238
262
  token.attrSet('class', existingClass ? langClass + ' ' + existingClass : langClass)
239
263
  }
@@ -242,15 +266,18 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
242
266
  let emphasizeLines = []
243
267
  let wrapEnabled = false
244
268
  let preWrapValue
269
+ let commentLineValue
245
270
 
246
271
  if (token.attrs) {
247
272
  const newAttrs = []
248
273
  let dataPreStartIndex = -1
249
274
  let dataPreEmphasisIndex = -1
250
275
  let styleIndex = -1
276
+ let dataPreCommentIndex = -1
251
277
  let startValue
252
278
  let emphasisValue
253
279
  let styleValue
280
+ let sawCommentLine = false
254
281
  let sawStartAttr = false
255
282
  let sawEmphasisAttr = false
256
283
  const appendOrder = []
@@ -274,14 +301,14 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
274
301
  newAttrs.push(attr)
275
302
  break
276
303
  case 'data-pre-start':
277
- startNumber = +val
304
+ startNumber = parseStartNumber(val) ?? -1
278
305
  startValue = val
279
306
  dataPreStartIndex = newAttrs.length
280
307
  newAttrs.push(attr)
281
308
  break
282
309
  case 'start':
283
310
  case 'pre-start':
284
- startNumber = +val
311
+ startNumber = parseStartNumber(val) ?? -1
285
312
  startValue = val
286
313
  if (!sawStartAttr) {
287
314
  appendOrder.push('start')
@@ -292,15 +319,27 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
292
319
  dataPreEmphasisIndex = newAttrs.length
293
320
  newAttrs.push(attr)
294
321
  break
322
+ case 'data-pre-comment-line':
323
+ dataPreCommentIndex = newAttrs.length
324
+ commentLineValue = val
325
+ newAttrs.push(attr)
326
+ break
295
327
  case 'em-lines':
296
328
  case 'emphasize-lines':
297
- emphasizeLines = getEmphasizeLines(val)
329
+ if (opt.setEmphasizeLines) emphasizeLines = getEmphasizeLines(val)
298
330
  emphasisValue = val
299
331
  if (!sawEmphasisAttr) {
300
332
  appendOrder.push('emphasis')
301
333
  sawEmphasisAttr = true
302
334
  }
303
335
  break
336
+ case 'comment-line':
337
+ commentLineValue = val
338
+ if (!sawCommentLine) {
339
+ appendOrder.push('comment')
340
+ sawCommentLine = true
341
+ }
342
+ break
304
343
  case 'data-pre-wrap':
305
344
  preWrapValue = val
306
345
  if (val === '' || val === 'true') wrapEnabled = true
@@ -322,6 +361,9 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
322
361
  if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) {
323
362
  newAttrs[dataPreEmphasisIndex][1] = emphasisValue
324
363
  }
364
+ if (commentLineValue !== undefined && dataPreCommentIndex >= 0) {
365
+ newAttrs[dataPreCommentIndex][1] = commentLineValue
366
+ }
325
367
  for (const kind of appendOrder) {
326
368
  if (kind === 'start') {
327
369
  if (dataPreStartIndex === -1 && startValue !== undefined) {
@@ -331,6 +373,10 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
331
373
  if (dataPreEmphasisIndex === -1 && emphasisValue !== undefined) {
332
374
  newAttrs.push(['data-pre-emphasis', emphasisValue])
333
375
  }
376
+ } else if (kind === 'comment') {
377
+ if (dataPreCommentIndex === -1 && commentLineValue !== undefined) {
378
+ newAttrs.push(['data-pre-comment-line', commentLineValue])
379
+ }
334
380
  }
335
381
  }
336
382
  if (startNumber !== -1) {
@@ -345,6 +391,11 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
345
391
  token.attrs = newAttrs
346
392
  }
347
393
 
394
+ const isSamp = opt._sampReg.test(lang)
395
+ let commentLines
396
+ let needComment = false
397
+ let sourceLogicalLineCount = -1
398
+
348
399
  if (opt.setHighlight && md.options.highlight) {
349
400
  if (lang && lang !== 'samp' ) {
350
401
  content = md.options.highlight(content, lang)
@@ -356,9 +407,12 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
356
407
  }
357
408
 
358
409
  let preAttrsFromHighlight
359
- if (content.indexOf('<pre') !== -1) {
410
+ let hasHighlightPre = false
411
+ const hasPreTag = (content.indexOf('<pre') !== -1 || content.indexOf('<PRE') !== -1)
412
+ if (hasPreTag) {
360
413
  const preMatch = content.match(preCodeWrapperReg)
361
414
  if (preMatch) {
415
+ hasHighlightPre = true
362
416
  preAttrsFromHighlight = parseHtmlAttrs(preMatch[1])
363
417
  const codeAttrsFromHighlight = parseHtmlAttrs(preMatch[2])
364
418
  content = preMatch[3]
@@ -368,6 +422,9 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
368
422
  }
369
423
  }
370
424
  }
425
+ if (opt.useHighlightPre && hasPreTag && !hasHighlightPre) {
426
+ return content.endsWith('\n') ? content : content + '\n'
427
+ }
371
428
  orderTokenAttrs(token, opt)
372
429
 
373
430
  let preAttrs = preAttrsFromHighlight ? preAttrsFromHighlight.slice() : []
@@ -392,14 +449,32 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
392
449
 
393
450
  const needLineNumber = opt.setLineNumber && startNumber >= 0
394
451
  const needEmphasis = opt.setEmphasizeLines && emphasizeLines.length > 0
395
- const needEndSpan = opt.setLineEndSpan > 0
396
- if (needLineNumber || needEmphasis || needEndSpan) {
452
+ const needEndSpan = opt.lineEndSpanThreshold > 0
453
+ const useHighlightPre = opt.useHighlightPre && hasHighlightPre
454
+ if (!useHighlightPre && commentLineValue) {
455
+ const rawLines = token.content.split(lineBreakReg)
456
+ sourceLogicalLineCount = getLogicalLineCount(token.content)
457
+ commentLines = new Array(rawLines.length)
458
+ for (let i = 0; i < rawLines.length; i++) {
459
+ const isComment = rawLines[i].trimStart().startsWith(commentLineValue)
460
+ commentLines[i] = isComment
461
+ if (isComment) needComment = true
462
+ }
463
+ }
464
+ if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
397
465
  const nlIndex = content.indexOf('\n')
398
466
  const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
399
- content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.setLineEndSpan, opt.lineEndSpanClass, br)
467
+ if (needComment && sourceLogicalLineCount >= 0) {
468
+ const highlightedLogicalLineCount = getLogicalLineCount(content)
469
+ if (highlightedLogicalLineCount !== sourceLogicalLineCount) {
470
+ needComment = false
471
+ commentLines = undefined
472
+ }
473
+ }
474
+ content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, commentLines, commentLineClass)
400
475
  }
401
476
 
402
- const tag = opt._sampReg.test(lang) ? 'samp' : 'code'
477
+ const tag = isSamp ? 'samp' : 'code'
403
478
  return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
404
479
  }
405
480
 
@@ -409,13 +484,19 @@ const mditRendererFence = (md, option) => {
409
484
  setHighlight: true,
410
485
  setLineNumber: true,
411
486
  setEmphasizeLines: true,
412
- setLineEndSpan: 0,
487
+ lineEndSpanThreshold: 0,
413
488
  lineEndSpanClass: 'pre-lineend-spacer',
414
489
  setPreWrapStyle: true,
490
+ useHighlightPre: false,
415
491
  sampLang: 'shell,console',
416
492
  langPrefix: md.options.langPrefix || 'language-',
417
493
  }
418
- if (option) Object.assign(opt, option)
494
+ if (option) {
495
+ Object.assign(opt, option)
496
+ if (option.lineEndSpanThreshold == null && option.setLineEndSpan != null) {
497
+ opt.lineEndSpanThreshold = option.setLineEndSpan
498
+ }
499
+ }
419
500
 
420
501
  opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
421
502
  opt._langReg = new RegExp(opt.langPrefix + '([A-Za-z0-9-]+)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-renderer-fence",
3
- "version": "0.3.1",
3
+ "version": "0.4.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
  "files": [
@@ -10,13 +10,15 @@
10
10
  ],
11
11
  "type": "module",
12
12
  "scripts": {
13
- "test": "node test/test.js"
13
+ "test": "node test/test.js",
14
+ "test:performance": "node test/performance/benchmark.js",
15
+ "test:performance:highlighter": "node test/performance/highlighter-benchmark.js"
14
16
  },
15
17
  "author": "peaceroad <peaceroad@gmail.com>",
16
18
  "repository": "https://github.com/peaceroad/p7d-markdown-it-renderer-fence",
17
19
  "license": "MIT",
18
20
  "devDependencies": {
19
- "@peaceroad/markdown-it-figure-with-p-caption": "^0.15.1",
21
+ "@peaceroad/markdown-it-figure-with-p-caption": "^0.15.2",
20
22
  "highlight.js": "^11.11.1",
21
23
  "markdown-it": "^14.1.0",
22
24
  "markdown-it-attrs": "^4.3.1",