@peaceroad/markdown-it-renderer-fence 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +25 -1
  2. package/index.js +196 -27
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -92,6 +92,7 @@ CSS example: <https://codepen.io/peaceroad/pen/qBGpYGK>
92
92
  ## Add span elements to add background color to the row ranges.
93
93
 
94
94
  Add `em-lines` or `emphasize-lines` attribute by by adding attributes used markdown-it-attrs.
95
+ Open-ended ranges are supported: `-5` (1..5), `3-` (3..last), `-` (all).
95
96
 
96
97
  ~~~md
97
98
  ``` {em-lines="2,4-5"}
@@ -132,6 +133,25 @@ const longLine = 'This line is long but wraps.'
132
133
 
133
134
  When `setPreWrapStyle` is set to false, only `data-pre-wrap="true"` is added and the inline style is omitted.
134
135
 
136
+ ## Mark comment lines in code blocks
137
+
138
+ 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">`.
139
+
140
+ ~~~md
141
+ ```samp {comment-line="#"}
142
+ # comment
143
+ echo 1
144
+ ```
145
+ ~~~
146
+
147
+ ~~~html
148
+ <pre><samp data-pre-comment-line="#"><span class="pre-comment-line"># comment</span>
149
+ echo 1
150
+ </samp></pre>
151
+ ~~~
152
+
153
+ Note: this feature relies on line splitting and is disabled when `useHighlightPre` is true and `<pre><code>` is provided by the highlighter.
154
+
135
155
 
136
156
  ## Options
137
157
 
@@ -141,8 +161,12 @@ The following options can be specified when initializing the plugin:
141
161
  - setHighlight: default true — enable calling highlight function (e.g., highlight.js).
142
162
  - setLineNumber: default true — wrap lines in spans for line numbers.
143
163
  - setEmphasizeLines: default true — enable emphasis based on emphasize-lines attribute.
144
- - setLineEndSpan: default 0 — character count threshold to append end-of-line span (0 to disable).
164
+ - lineEndSpanThreshold: default 0 — character count threshold to append end-of-line span (0 to disable).
165
+ - setLineEndSpan: alias for lineEndSpanThreshold.
145
166
  - lineEndSpanClass: default 'pre-lineend-spacer' — CSS class for end-of-line span.
146
167
  - setPreWrapStyle: default true — include inline pre-wrap styles on `<pre>` when wrap is enabled.
168
+ - useHighlightPre: default false — if highlight returns `<pre><code>`, keep that wrapper and skip line-splitting features; attributes are still merged.
169
+
170
+ When `useHighlightPre` is true and the highlight output contains `<pre><code>`, line-splitting features are disabled (setLineNumber, setEmphasizeLines, lineEndSpanThreshold, comment-line).
147
171
  - sampLang: default 'shell,console' — comma-separated list of languages (in addition to 'samp') that will be rendered using `<samp>`.
148
172
  - langPrefix: default md.options.langPrefix || 'language-' — prefix for language class on code blocks.
package/index.js CHANGED
@@ -5,9 +5,13 @@ 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;'
12
+ const preCodeWrapperReg = /^\s*<pre\b([^>]*)>\s*<code\b([^>]*)>([\s\S]*?)<\/code>\s*<\/pre>\s*$/i
13
+ const htmlAttrReg = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
14
+ const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
11
15
 
12
16
  const appendStyleValue = (style, addition) => {
13
17
  if (!addition) return style
@@ -20,27 +24,110 @@ const appendStyleValue = (style, addition) => {
20
24
  return base + separator + next
21
25
  }
22
26
 
27
+ const parseHtmlAttrs = (attrText) => {
28
+ const attrs = []
29
+ if (!attrText) return attrs
30
+ htmlAttrReg.lastIndex = 0
31
+ let match
32
+ while ((match = htmlAttrReg.exec(attrText)) !== null) {
33
+ const name = match[1]
34
+ const val = match[2] ?? match[3] ?? match[4] ?? ''
35
+ attrs.push([name, val])
36
+ }
37
+ return attrs
38
+ }
39
+
40
+ const mergeAttrSets = (target, source) => {
41
+ if (!source || source.length === 0) return
42
+ const index = new Map()
43
+ for (let i = 0; i < target.length; i++) index.set(target[i][0], i)
44
+ for (const [name, val] of source) {
45
+ const idx = index.get(name)
46
+ if (name === 'class') {
47
+ if (idx === undefined) {
48
+ target.push([name, val])
49
+ index.set(name, target.length - 1)
50
+ } else if (val) {
51
+ const existing = target[idx][1]
52
+ target[idx][1] = existing ? existing + ' ' + val : val
53
+ }
54
+ continue
55
+ }
56
+ if (name === 'style') {
57
+ if (idx === undefined) {
58
+ target.push([name, val])
59
+ index.set(name, target.length - 1)
60
+ } else {
61
+ target[idx][1] = appendStyleValue(target[idx][1], val)
62
+ }
63
+ continue
64
+ }
65
+ if (idx === undefined) {
66
+ target.push([name, val])
67
+ index.set(name, target.length - 1)
68
+ }
69
+ }
70
+ }
71
+
23
72
  const getEmphasizeLines = (attrVal) => {
24
73
  const lines = []
25
- let s, e
26
- attrVal.split(',').forEach(range => {
27
- if (range.indexOf('-') > -1) {
28
- [s, e] = range.split('-').map(n => parseInt(n.trim(), 10))
29
- lines.push([s, e])
74
+ for (const range of attrVal.split(',')) {
75
+ const part = range.trim()
76
+ if (!part) continue
77
+ const dash = part.indexOf('-')
78
+ if (dash > -1) {
79
+ const left = part.slice(0, dash).trim()
80
+ const right = part.slice(dash + 1).trim()
81
+ const s = left ? parseInt(left, 10) : null
82
+ const e = right ? parseInt(right, 10) : null
83
+ if (s !== null && (!Number.isFinite(s) || s <= 0)) continue
84
+ if (e !== null && (!Number.isFinite(e) || e <= 0)) continue
85
+ if (s === null && e === null) {
86
+ lines.push([null, null])
87
+ } else {
88
+ lines.push([s, e])
89
+ }
30
90
  } else {
31
- s = parseInt(range.trim(), 10)
91
+ const s = parseInt(part, 10)
92
+ if (!Number.isFinite(s) || s <= 0) continue
32
93
  lines.push([s, s])
33
94
  }
34
- })
95
+ }
35
96
  return lines
36
97
  }
37
98
 
38
- const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br) => {
99
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass) => {
39
100
  const lines = content.split(br)
40
101
  const len = lines.length
41
102
  const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
103
+ const needComment = !!(commentLines && commentClass)
104
+ const maxLine = len - (lines[len - 1] === '' ? 1 : 0)
42
105
  let emIdx = 0
43
- let [emStart, emEnd] = emphasizeLines[0] || []
106
+ let emStart
107
+ let emEnd
108
+ let doEmphasis = false
109
+ if (needEmphasis && maxLine > 0) {
110
+ const normalized = []
111
+ for (const range of emphasizeLines) {
112
+ let [s, e] = range
113
+ if (s == null) s = 1
114
+ if (e == null) e = maxLine
115
+ if (!Number.isFinite(s) || !Number.isFinite(e) || s <= 0 || e <= 0) continue
116
+ if (s > e) {
117
+ const tmp = s
118
+ s = e
119
+ e = tmp
120
+ }
121
+ if (s > maxLine || e < 1) continue
122
+ if (e > maxLine) e = maxLine
123
+ normalized.push([s, e])
124
+ }
125
+ if (normalized.length > 0) {
126
+ doEmphasis = true
127
+ emphasizeLines = normalized
128
+ ;[emStart, emEnd] = emphasizeLines[0]
129
+ }
130
+ }
44
131
  for (let n = 0; n < len; n++) {
45
132
  let line = lines[n]
46
133
 
@@ -69,10 +156,11 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
69
156
  let match
70
157
  while ((match = tagReg.exec(line)) !== null) {
71
158
  const [fullMatch, tagName] = match
159
+ const tagNameLower = tagName.toLowerCase()
72
160
  if (fullMatch.startsWith('</')) {
73
- if (tagStack[tagStack.length-1] === tagName) tagStack.pop()
74
- } else if (!fullMatch.endsWith('/>')) {
75
- tagStack.push(tagName)
161
+ if (tagStack[tagStack.length-1] === tagNameLower) tagStack.pop()
162
+ } else if (!fullMatch.endsWith('/>') && !voidTags.has(tagNameLower)) {
163
+ tagStack.push(tagNameLower)
76
164
  }
77
165
  }
78
166
  for (let i = tagStack.length-1; i >= 0; i--) {
@@ -81,10 +169,17 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
81
169
  lines[n+1] = `<${tagName}>` + (lines[n+1]||'')
82
170
  }
83
171
  }
172
+ }
173
+
174
+ if (needComment && commentLines[n]) {
175
+ line = `<span class="${commentClass}">` + line + closeTag
176
+ }
177
+
178
+ if (needLineNumber && n !== len - 1) {
84
179
  line = preLineTag + line + closeTag
85
180
  }
86
181
 
87
- if (needEmphasis) {
182
+ if (doEmphasis) {
88
183
  if (emStart === n + 1) line = emphOpenTag + line
89
184
  if (emEnd === n) {
90
185
  line = closeTag + line
@@ -156,15 +251,18 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
156
251
  let emphasizeLines = []
157
252
  let wrapEnabled = false
158
253
  let preWrapValue
254
+ let commentLineValue
159
255
 
160
256
  if (token.attrs) {
161
257
  const newAttrs = []
162
258
  let dataPreStartIndex = -1
163
259
  let dataPreEmphasisIndex = -1
164
260
  let styleIndex = -1
261
+ let dataPreCommentIndex = -1
165
262
  let startValue
166
263
  let emphasisValue
167
264
  let styleValue
265
+ let sawCommentLine = false
168
266
  let sawStartAttr = false
169
267
  let sawEmphasisAttr = false
170
268
  const appendOrder = []
@@ -206,6 +304,11 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
206
304
  dataPreEmphasisIndex = newAttrs.length
207
305
  newAttrs.push(attr)
208
306
  break
307
+ case 'data-pre-comment-line':
308
+ dataPreCommentIndex = newAttrs.length
309
+ commentLineValue = val
310
+ newAttrs.push(attr)
311
+ break
209
312
  case 'em-lines':
210
313
  case 'emphasize-lines':
211
314
  emphasizeLines = getEmphasizeLines(val)
@@ -215,6 +318,13 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
215
318
  sawEmphasisAttr = true
216
319
  }
217
320
  break
321
+ case 'comment-line':
322
+ commentLineValue = val
323
+ if (!sawCommentLine) {
324
+ appendOrder.push('comment')
325
+ sawCommentLine = true
326
+ }
327
+ break
218
328
  case 'data-pre-wrap':
219
329
  preWrapValue = val
220
330
  if (val === '' || val === 'true') wrapEnabled = true
@@ -236,6 +346,9 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
236
346
  if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) {
237
347
  newAttrs[dataPreEmphasisIndex][1] = emphasisValue
238
348
  }
349
+ if (commentLineValue !== undefined && dataPreCommentIndex >= 0) {
350
+ newAttrs[dataPreCommentIndex][1] = commentLineValue
351
+ }
239
352
  for (const kind of appendOrder) {
240
353
  if (kind === 'start') {
241
354
  if (dataPreStartIndex === -1 && startValue !== undefined) {
@@ -245,6 +358,10 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
245
358
  if (dataPreEmphasisIndex === -1 && emphasisValue !== undefined) {
246
359
  newAttrs.push(['data-pre-emphasis', emphasisValue])
247
360
  }
361
+ } else if (kind === 'comment') {
362
+ if (dataPreCommentIndex === -1 && commentLineValue !== undefined) {
363
+ newAttrs.push(['data-pre-comment-line', commentLineValue])
364
+ }
248
365
  }
249
366
  }
250
367
  if (startNumber !== -1) {
@@ -258,13 +375,17 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
258
375
  if (wrapEnabled) preWrapValue = 'true'
259
376
  token.attrs = newAttrs
260
377
  }
261
- orderTokenAttrs(token, opt)
262
- let preAttrs = ''
263
- if (preWrapValue !== undefined) {
264
- const escapeHtml = md.utils.escapeHtml
265
- preAttrs = ' data-pre-wrap="' + escapeHtml(preWrapValue) + '"'
266
- if (wrapEnabled && opt.setPreWrapStyle !== false) {
267
- preAttrs += ' style="' + escapeHtml(preWrapStyle) + '"'
378
+
379
+ const isSamp = opt._sampReg.test(lang)
380
+ let commentLines
381
+ let needComment = false
382
+ if (commentLineValue) {
383
+ const rawLines = token.content.split(/\r?\n/)
384
+ commentLines = new Array(rawLines.length)
385
+ for (let i = 0; i < rawLines.length; i++) {
386
+ const isComment = rawLines[i].trimStart().startsWith(commentLineValue)
387
+ commentLines[i] = isComment
388
+ if (isComment) needComment = true
268
389
  }
269
390
  }
270
391
 
@@ -278,17 +399,59 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
278
399
  content = md.utils.escapeHtml(token.content)
279
400
  }
280
401
 
402
+ let preAttrsFromHighlight
403
+ let hasHighlightPre = false
404
+ const hasPreTag = (content.indexOf('<pre') !== -1 || content.indexOf('<PRE') !== -1)
405
+ if (hasPreTag) {
406
+ const preMatch = content.match(preCodeWrapperReg)
407
+ if (preMatch) {
408
+ hasHighlightPre = true
409
+ preAttrsFromHighlight = parseHtmlAttrs(preMatch[1])
410
+ const codeAttrsFromHighlight = parseHtmlAttrs(preMatch[2])
411
+ content = preMatch[3]
412
+ if (codeAttrsFromHighlight.length) {
413
+ if (!token.attrs) token.attrs = []
414
+ mergeAttrSets(token.attrs, codeAttrsFromHighlight)
415
+ }
416
+ }
417
+ }
418
+ if (opt.useHighlightPre && hasPreTag && !hasHighlightPre) {
419
+ return content.endsWith('\n') ? content : content + '\n'
420
+ }
421
+ orderTokenAttrs(token, opt)
422
+
423
+ let preAttrs = preAttrsFromHighlight ? preAttrsFromHighlight.slice() : []
424
+ if (preWrapValue !== undefined) {
425
+ const idx = preAttrs.findIndex(attr => attr[0] === 'data-pre-wrap')
426
+ if (idx === -1) {
427
+ preAttrs.push(['data-pre-wrap', preWrapValue])
428
+ } else {
429
+ preAttrs[idx][1] = preWrapValue
430
+ }
431
+ }
432
+ if (wrapEnabled && opt.setPreWrapStyle !== false) {
433
+ const idx = preAttrs.findIndex(attr => attr[0] === 'style')
434
+ if (idx === -1) {
435
+ preAttrs.push(['style', preWrapStyle])
436
+ } else {
437
+ preAttrs[idx][1] = appendStyleValue(preAttrs[idx][1], preWrapStyle)
438
+ }
439
+ }
440
+ if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
441
+ const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
442
+
281
443
  const needLineNumber = opt.setLineNumber && startNumber >= 0
282
444
  const needEmphasis = opt.setEmphasizeLines && emphasizeLines.length > 0
283
- const needEndSpan = opt.setLineEndSpan > 0
284
- if (needLineNumber || needEmphasis || needEndSpan) {
445
+ const needEndSpan = opt.lineEndSpanThreshold > 0
446
+ const useHighlightPre = opt.useHighlightPre && hasHighlightPre
447
+ if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
285
448
  const nlIndex = content.indexOf('\n')
286
449
  const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
287
- content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.setLineEndSpan, opt.lineEndSpanClass, br)
450
+ content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, commentLines, commentLineClass)
288
451
  }
289
452
 
290
- const tag = opt._sampReg.test(lang) ? 'samp' : 'code'
291
- return `<pre${preAttrs}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
453
+ const tag = isSamp ? 'samp' : 'code'
454
+ return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
292
455
  }
293
456
 
294
457
  const mditRendererFence = (md, option) => {
@@ -297,13 +460,19 @@ const mditRendererFence = (md, option) => {
297
460
  setHighlight: true,
298
461
  setLineNumber: true,
299
462
  setEmphasizeLines: true,
300
- setLineEndSpan: 0,
463
+ lineEndSpanThreshold: 0,
301
464
  lineEndSpanClass: 'pre-lineend-spacer',
302
465
  setPreWrapStyle: true,
466
+ useHighlightPre: false,
303
467
  sampLang: 'shell,console',
304
468
  langPrefix: md.options.langPrefix || 'language-',
305
469
  }
306
- if (option) Object.assign(opt, option)
470
+ if (option) {
471
+ Object.assign(opt, option)
472
+ if (option.lineEndSpanThreshold == null && option.setLineEndSpan != null) {
473
+ opt.lineEndSpanThreshold = option.setLineEndSpan
474
+ }
475
+ }
307
476
 
308
477
  opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
309
478
  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.0",
3
+ "version": "0.4.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
  "files": [
@@ -16,9 +16,10 @@
16
16
  "repository": "https://github.com/peaceroad/p7d-markdown-it-renderer-fence",
17
17
  "license": "MIT",
18
18
  "devDependencies": {
19
- "@peaceroad/markdown-it-figure-with-p-caption": "^0.14.2",
19
+ "@peaceroad/markdown-it-figure-with-p-caption": "^0.15.2",
20
+ "highlight.js": "^11.11.1",
20
21
  "markdown-it": "^14.1.0",
21
22
  "markdown-it-attrs": "^4.3.1",
22
- "highlight.js": "^11.11.1"
23
+ "shiki": "^3.21.0"
23
24
  }
24
25
  }