@peaceroad/markdown-it-renderer-fence 0.3.0 → 0.3.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 +1 -0
  2. package/index.js +134 -22
  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"}
package/index.js CHANGED
@@ -8,6 +8,9 @@ const emphOpenTag = '<span class="pre-lines-emphasis">'
8
8
  const closeTag = '</span>'
9
9
  const closeTagLen = closeTag.length
10
10
  const preWrapStyle = 'white-space: pre-wrap; overflow-wrap: anywhere;'
11
+ const preCodeWrapperReg = /^\s*<pre\b([^>]*)>\s*<code\b([^>]*)>([\s\S]*?)<\/code>\s*<\/pre>\s*$/i
12
+ const htmlAttrReg = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
13
+ const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
11
14
 
12
15
  const appendStyleValue = (style, addition) => {
13
16
  if (!addition) return style
@@ -20,18 +23,75 @@ const appendStyleValue = (style, addition) => {
20
23
  return base + separator + next
21
24
  }
22
25
 
26
+ const parseHtmlAttrs = (attrText) => {
27
+ const attrs = []
28
+ if (!attrText) return attrs
29
+ htmlAttrReg.lastIndex = 0
30
+ let match
31
+ while ((match = htmlAttrReg.exec(attrText)) !== null) {
32
+ const name = match[1]
33
+ const val = match[2] ?? match[3] ?? match[4] ?? ''
34
+ attrs.push([name, val])
35
+ }
36
+ return attrs
37
+ }
38
+
39
+ const mergeAttrSets = (target, source) => {
40
+ if (!source || source.length === 0) return
41
+ const index = new Map()
42
+ for (let i = 0; i < target.length; i++) index.set(target[i][0], i)
43
+ for (const [name, val] of source) {
44
+ const idx = index.get(name)
45
+ if (name === 'class') {
46
+ if (idx === undefined) {
47
+ target.push([name, val])
48
+ index.set(name, target.length - 1)
49
+ } else if (val) {
50
+ const existing = target[idx][1]
51
+ target[idx][1] = existing ? existing + ' ' + val : val
52
+ }
53
+ continue
54
+ }
55
+ if (name === 'style') {
56
+ if (idx === undefined) {
57
+ target.push([name, val])
58
+ index.set(name, target.length - 1)
59
+ } else {
60
+ target[idx][1] = appendStyleValue(target[idx][1], val)
61
+ }
62
+ continue
63
+ }
64
+ if (idx === undefined) {
65
+ target.push([name, val])
66
+ index.set(name, target.length - 1)
67
+ }
68
+ }
69
+ }
70
+
23
71
  const getEmphasizeLines = (attrVal) => {
24
72
  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])
73
+ for (const range of attrVal.split(',')) {
74
+ const part = range.trim()
75
+ if (!part) continue
76
+ const dash = part.indexOf('-')
77
+ if (dash > -1) {
78
+ const left = part.slice(0, dash).trim()
79
+ const right = part.slice(dash + 1).trim()
80
+ const s = left ? parseInt(left, 10) : null
81
+ const e = right ? parseInt(right, 10) : null
82
+ if (s !== null && (!Number.isFinite(s) || s <= 0)) continue
83
+ if (e !== null && (!Number.isFinite(e) || e <= 0)) continue
84
+ if (s === null && e === null) {
85
+ lines.push([null, null])
86
+ } else {
87
+ lines.push([s, e])
88
+ }
30
89
  } else {
31
- s = parseInt(range.trim(), 10)
90
+ const s = parseInt(part, 10)
91
+ if (!Number.isFinite(s) || s <= 0) continue
32
92
  lines.push([s, s])
33
93
  }
34
- })
94
+ }
35
95
  return lines
36
96
  }
37
97
 
@@ -39,8 +99,33 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
39
99
  const lines = content.split(br)
40
100
  const len = lines.length
41
101
  const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
102
+ const maxLine = len - (lines[len - 1] === '' ? 1 : 0)
42
103
  let emIdx = 0
43
- let [emStart, emEnd] = emphasizeLines[0] || []
104
+ let emStart
105
+ let emEnd
106
+ let doEmphasis = false
107
+ if (needEmphasis && maxLine > 0) {
108
+ const normalized = []
109
+ for (const range of emphasizeLines) {
110
+ let [s, e] = range
111
+ if (s == null) s = 1
112
+ if (e == null) e = maxLine
113
+ if (!Number.isFinite(s) || !Number.isFinite(e) || s <= 0 || e <= 0) continue
114
+ if (s > e) {
115
+ const tmp = s
116
+ s = e
117
+ e = tmp
118
+ }
119
+ if (s > maxLine || e < 1) continue
120
+ if (e > maxLine) e = maxLine
121
+ normalized.push([s, e])
122
+ }
123
+ if (normalized.length > 0) {
124
+ doEmphasis = true
125
+ emphasizeLines = normalized
126
+ ;[emStart, emEnd] = emphasizeLines[0]
127
+ }
128
+ }
44
129
  for (let n = 0; n < len; n++) {
45
130
  let line = lines[n]
46
131
 
@@ -69,10 +154,11 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
69
154
  let match
70
155
  while ((match = tagReg.exec(line)) !== null) {
71
156
  const [fullMatch, tagName] = match
157
+ const tagNameLower = tagName.toLowerCase()
72
158
  if (fullMatch.startsWith('</')) {
73
- if (tagStack[tagStack.length-1] === tagName) tagStack.pop()
74
- } else if (!fullMatch.endsWith('/>')) {
75
- tagStack.push(tagName)
159
+ if (tagStack[tagStack.length-1] === tagNameLower) tagStack.pop()
160
+ } else if (!fullMatch.endsWith('/>') && !voidTags.has(tagNameLower)) {
161
+ tagStack.push(tagNameLower)
76
162
  }
77
163
  }
78
164
  for (let i = tagStack.length-1; i >= 0; i--) {
@@ -84,7 +170,7 @@ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmp
84
170
  line = preLineTag + line + closeTag
85
171
  }
86
172
 
87
- if (needEmphasis) {
173
+ if (doEmphasis) {
88
174
  if (emStart === n + 1) line = emphOpenTag + line
89
175
  if (emEnd === n) {
90
176
  line = closeTag + line
@@ -258,15 +344,6 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
258
344
  if (wrapEnabled) preWrapValue = 'true'
259
345
  token.attrs = newAttrs
260
346
  }
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) + '"'
268
- }
269
- }
270
347
 
271
348
  if (opt.setHighlight && md.options.highlight) {
272
349
  if (lang && lang !== 'samp' ) {
@@ -278,6 +355,41 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
278
355
  content = md.utils.escapeHtml(token.content)
279
356
  }
280
357
 
358
+ let preAttrsFromHighlight
359
+ if (content.indexOf('<pre') !== -1) {
360
+ const preMatch = content.match(preCodeWrapperReg)
361
+ if (preMatch) {
362
+ preAttrsFromHighlight = parseHtmlAttrs(preMatch[1])
363
+ const codeAttrsFromHighlight = parseHtmlAttrs(preMatch[2])
364
+ content = preMatch[3]
365
+ if (codeAttrsFromHighlight.length) {
366
+ if (!token.attrs) token.attrs = []
367
+ mergeAttrSets(token.attrs, codeAttrsFromHighlight)
368
+ }
369
+ }
370
+ }
371
+ orderTokenAttrs(token, opt)
372
+
373
+ let preAttrs = preAttrsFromHighlight ? preAttrsFromHighlight.slice() : []
374
+ if (preWrapValue !== undefined) {
375
+ const idx = preAttrs.findIndex(attr => attr[0] === 'data-pre-wrap')
376
+ if (idx === -1) {
377
+ preAttrs.push(['data-pre-wrap', preWrapValue])
378
+ } else {
379
+ preAttrs[idx][1] = preWrapValue
380
+ }
381
+ }
382
+ if (wrapEnabled && opt.setPreWrapStyle !== false) {
383
+ const idx = preAttrs.findIndex(attr => attr[0] === 'style')
384
+ if (idx === -1) {
385
+ preAttrs.push(['style', preWrapStyle])
386
+ } else {
387
+ preAttrs[idx][1] = appendStyleValue(preAttrs[idx][1], preWrapStyle)
388
+ }
389
+ }
390
+ if (preAttrs.length) orderTokenAttrs({ attrs: preAttrs }, opt)
391
+ const preAttrsText = preAttrs.length ? slf.renderAttrs({ attrs: preAttrs }) : ''
392
+
281
393
  const needLineNumber = opt.setLineNumber && startNumber >= 0
282
394
  const needEmphasis = opt.setEmphasizeLines && emphasizeLines.length > 0
283
395
  const needEndSpan = opt.setLineEndSpan > 0
@@ -288,7 +400,7 @@ const getFenceHtml = (tokens, idx, md, opt, slf) => {
288
400
  }
289
401
 
290
402
  const tag = opt._sampReg.test(lang) ? 'samp' : 'code'
291
- return `<pre${preAttrs}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
403
+ return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
292
404
  }
293
405
 
294
406
  const mditRendererFence = (md, option) => {
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.3.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": [
@@ -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.1",
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
  }