@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.
- package/README.md +30 -2
- package/index.js +97 -16
- 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
|
|
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
|
-
-
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
396
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
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
|
+
"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.
|
|
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",
|