@peaceroad/markdown-it-renderer-fence 0.1.2 → 0.2.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
@@ -106,7 +106,7 @@ Add `em-lines` or `emphasize-lines` attribute by by adding attributes used markd
106
106
 
107
107
  ~~~html
108
108
  [HTML]
109
- <pre><code>1
109
+ <pre><code data-pre-emphasis="2,4-5">1
110
110
  <span class="pre-lines-emphasis">2
111
111
  </span>3
112
112
  <span class="pre-lines-emphasis">4
@@ -114,3 +114,17 @@ Add `em-lines` or `emphasize-lines` attribute by by adding attributes used markd
114
114
  </span>6
115
115
  </code></pre>
116
116
  ~~~
117
+
118
+
119
+ ## Options
120
+
121
+ The following options can be specified when initializing the plugin:
122
+
123
+ - attrsOrder: default ['class','id','data-*','style'] — order of attributes in output; wildcards supported.
124
+ - setHighlight: default true — enable calling highlight function (e.g., highlight.js).
125
+ - setLineNumber: default true — wrap lines in spans for line numbers.
126
+ - setEmphasizeLines: default true — enable emphasis based on emphasize-lines attribute.
127
+ - setLineEndSpan: default 0 — character count threshold to append end-of-line span (0 to disable).
128
+ - lineEndSpanClass: default 'pre-lineend-spacer' — CSS class for end-of-line span.
129
+ - sampLang: default 'shell,console' — comma-separated list of languages (in addition to 'samp') that will be rendered using `<samp>`.
130
+ - langPrefix: default md.options.langPrefix || 'language-' — prefix for language class on code blocks.
package/index.js CHANGED
@@ -1,68 +1,93 @@
1
- //import highlightjs from 'highlight.js'
2
-
3
- const fenceStartTag = (tagName, sAttr) => {
4
- let orderedAttrs = [...sAttr.id, ...sAttr.clas, ...sAttr.data, ...sAttr.style, ...sAttr.other]
5
- //console.log(orderedAttrs)
6
- let tag = '<' + tagName
7
- for (let attr of orderedAttrs) {
8
- if (attr[0]) tag += ' ' + attr[0] + '="' + attr[1] + '"'
9
- }
10
- return tag + '>'
11
- }
1
+ const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
2
+ const startReg = /^(?:(?:data-)?pre-)?start$/
3
+ const startFilterReg = /^(?:pre-)?start$/
4
+ const emphasisReg = /^em(?:phasize)?-lines$/
5
+
6
+ const attrReg = /^(?:([.#])(.+)|(.+?)(?:=(["'])?(.*?)\1)?)$/
7
+ const interAttrsSpaceReg = / +/
8
+ const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
9
+ const preLineTag = '<span class="pre-line">'
10
+ const emphOpenTag = '<span class="pre-lines-emphasis">'
11
+ const closeTag = '</span>'
12
+ const closeTagLen = closeTag.length
12
13
 
13
- const parseEmphasizeLines = (attrValue) => {
14
+ const getEmphasizeLines = (attrVal) => {
14
15
  const lines = []
15
16
  let s, e
16
- attrValue.split(',').forEach(range => {
17
- if (range.includes('-')) {
18
- [s, e] = range.split('-').map(n => parseInt(n.trim(), 10));
19
- lines.push([s, e]);
17
+ attrVal.split(',').forEach(range => {
18
+ if (range.indexOf('-') > -1) {
19
+ [s, e] = range.split('-').map(n => parseInt(n.trim(), 10))
20
+ lines.push([s, e])
20
21
  } else {
21
22
  s = parseInt(range.trim(), 10)
22
23
  lines.push([s, s])
23
24
  }
24
- });
25
- return lines;
25
+ })
26
+ return lines
26
27
  }
27
28
 
28
- const splitFenceBlockToLines = (content, emphasizeLines, opt, hasPreLineStart) => {
29
- const br = content.match(/\r?\n/)
30
- const lines = content.split(/\r?\n/)
31
- lines.map((line, n) => {
32
- if (opt.setLineNumber && hasPreLineStart) {
33
- const lastElementTag = line.match(/<(\w+)( +[^>]*?)>[^>]*?(<\/\1>)?[^>]*?$/)
34
- if (lastElementTag && !lastElementTag[3]) {
35
- line += '</span>'
36
- if (n < lines.length - 2) {
37
- lines[n + 1] = `<${lastElementTag[1]}${lastElementTag[2]}>` + lines[n + 1]
38
- }
29
+ const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br) => {
30
+ const lines = content.split(br)
31
+ const len = lines.length
32
+ const endSpanTag = `<span class="${lineEndSpanClass}"></span>`
33
+ let emIdx = 0
34
+ let [emStart, emEnd] = emphasizeLines[0] || []
35
+ for (let n = 0; n < len; n++) {
36
+ let line = lines[n]
37
+
38
+ if (needEndSpan) {
39
+ let lineLen = 0
40
+ for (let i = 0, L = line.length; i < L; i++) {
41
+ lineLen += line.charCodeAt(i) > 255 ? 2 : 1
39
42
  }
40
- if (n < lines.length - 1) {
41
- lines[n] = '<span class="pre-line">' + line + '</span>'
43
+ if (lineLen >= threshold) {
44
+ if (line.slice(-closeTagLen) === closeTag) {
45
+ line = line.slice(0, -closeTagLen) + endSpanTag + closeTag
46
+ } else {
47
+ line = line + endSpanTag
48
+ }
42
49
  }
43
50
  }
44
51
 
45
- if (opt.setEmphasizeLines && emphasizeLines.length > 0) {
46
- if (emphasizeLines[0][0] === n + 1) {
47
- lines[n] = `<span class="pre-lines-emphasis">${lines[n]}`
52
+ if (needLineNumber && n !== len - 1) {
53
+ const tagStack = []
54
+ tagReg.lastIndex = 0
55
+ let match
56
+ while ((match = tagReg.exec(line)) !== null) {
57
+ const [fullMatch, tagName] = match
58
+ if (fullMatch.startsWith('</')) {
59
+ if (tagStack[tagStack.length-1] === tagName) tagStack.pop()
60
+ } else if (!fullMatch.endsWith('/>')) {
61
+ tagStack.push(tagName)
62
+ }
48
63
  }
49
- if (emphasizeLines[0][1] === n) {
50
- lines[n] = '</span>' + lines[n]
51
- emphasizeLines.shift()
64
+ for (let i = tagStack.length-1; i >= 0; i--) {
65
+ const tagName = tagStack[i]
66
+ line += `</${tagName}>`
67
+ lines[n+1] = `<${tagName}>` + (lines[n+1]||'')
52
68
  }
69
+ line = preLineTag + line + closeTag
53
70
  }
54
71
 
55
- })
72
+ if (needEmphasis) {
73
+ if (emStart === n + 1) line = emphOpenTag + line
74
+ if (emEnd === n) {
75
+ line = closeTag + line
76
+ emIdx++
77
+ [emStart, emEnd] = emphasizeLines[emIdx] || []
78
+ }
79
+ }
80
+ lines[n] = line
81
+ }
56
82
  return lines.join(br)
57
83
  }
58
84
 
59
- const setInfoAttr = (infoAttr) => {
85
+ const getInfoAttr = (infoAttr) => {
86
+ let attrSets = infoAttr.trim().split(interAttrsSpaceReg)
60
87
  let arr = []
61
- let attrSets = infoAttr.trim().split(/ +/)
62
88
  for (let attrSet of attrSets) {
63
- const str = attrSet.match(/^(?:([.#])(.+)|(.+?)(?:=("')?(.*?)\1?)?)$/)
89
+ const str = attrReg.exec(attrSet)
64
90
  if (str) {
65
- //console.log(str)
66
91
  if (str[1] === '.') str[1] = 'class'
67
92
  if (str[1] === '#') str[1] = 'id'
68
93
  if (str[3]) {
@@ -75,102 +100,110 @@ const setInfoAttr = (infoAttr) => {
75
100
  return arr
76
101
  }
77
102
 
78
- const getFenceHtml = (tokens, idx, env, slf, md, options) => {
79
- const opt = {
80
- setHighlight: true,
81
- setLineNumber: true,
82
- setEmphasizeLines: true,
83
- langPrefix: 'language-',
84
- highlight: null,
85
- }
86
- if (options) Object.assign(opt, options)
103
+ const orderTokenAttrs = (token, opt) => {
104
+ if (!token.attrs) return
105
+ const order = opt.attrsOrder || []
106
+ token.attrs.sort((a, b) => {
107
+ const idx = (name) => {
108
+ for (let i = 0; i < order.length; i++) {
109
+ const key = order[i]
110
+ if (key.endsWith('*')) {
111
+ const prefix = key.slice(0, -1)
112
+ if (name.startsWith(prefix)) return i
113
+ } else if (name === key) {
114
+ return i
115
+ }
116
+ }
117
+ return order.length
118
+ }
119
+ return idx(a[0]) - idx(b[0])
120
+ })
121
+ }
87
122
 
123
+ const getFenceHtml = (tokens, idx, md, opt, slf) => {
88
124
  const token = tokens[idx]
89
125
  let content = token.content
90
- const infoAttr = token.info.trim().match(/{(.*)}$/)
91
- if(infoAttr) {
92
- token.attrs = token.attrs ? [...token.attrs, ...setInfoAttr(infoAttr[1])] : setInfoAttr(infoAttr[1])
126
+ const match = token.info.trim().match(infoReg)
127
+ let lang = match ? match[1] : ''
128
+
129
+ if (match && match[2]) {
130
+ getInfoAttr(match[2]).forEach(([name, val]) => token.attrJoin(name, val))
131
+ }
132
+ let langClass = ''
133
+ if (lang && lang !== 'samp') {
134
+ langClass = opt.langPrefix + lang
135
+ token.attrSet('class', langClass + (token.attrGet('class') ? ' ' + token.attrGet('class') : ''))
93
136
  }
94
137
 
95
- let lang = token.info.trim().replace(/ *({.*)?$/, '')
96
- const langClass = lang && token.info !== 'samp' ? opt.langPrefix + lang : ''
97
- let sAttr = {id: [], clas: [], data: [], style: [], other: []}
98
- let hasPreLineStart = false
99
- let preLineStart = -1
138
+ let startNumber = -1
100
139
  let emphasizeLines = []
140
+
101
141
  if (token.attrs) {
102
- //console.log('start: ' +token.attrs)
103
142
  for (let attr of token.attrs) {
104
- if (attr[0] === 'id') {
105
- sAttr.id.push(attr)
106
- } else if (attr[0] === 'class') {
107
- const sAttrClass = langClass ? langClass + ' ' + attr[1] : attr[1]
108
- const hasLang = attr[1].match(new RegExp('(?:^| )' + opt.langPrefix + '(.*)(?: |$)'))
109
- if (hasLang) lang = hasLang[1]
110
- sAttr.clas.push([attr[0], sAttrClass])
111
- } else if (attr[0].startsWith('data-') || /^(?:pre-)?start$/.test(attr[0])) {
112
- if (/^(?:(?:data-)?pre-)?start$/.test(attr[0])) {
113
- hasPreLineStart = true
114
- preLineStart = attr[1]
115
- attr[0] = 'data-pre-start'
143
+ if (attr[0] === 'class') {
144
+ const hasClassLang = attr[1].match(opt._langReg)
145
+ if (hasClassLang) lang = hasClassLang[1]
146
+ }
147
+ if (startReg.test(attr[0])) {
148
+ startNumber = +attr[1]
149
+ if (attr[0] !== 'data-pre-start') {
150
+ token.attrSet('data-pre-start', attr[1])
151
+ token.attrs = token.attrs.filter(attr => !startFilterReg.test(attr[0]))
116
152
  }
117
- sAttr.data.push(attr)
118
- } else if (/^em(?:phasize)?-lines$/.test(attr[0])) {
119
- emphasizeLines = parseEmphasizeLines(attr[1])
120
- } else if (attr[0] === 'style') {
121
- sAttr.style.push(attr)
122
- } else {
123
- sAttr.other.push(attr)
153
+ }
154
+ if (emphasisReg.test(attr[0])) {
155
+ emphasizeLines = getEmphasizeLines(attr[1])
156
+ token.attrSet('data-pre-emphasis', attr[1])
157
+ token.attrs = token.attrs.filter(attr => !emphasisReg.test(attr[0]))
124
158
  }
125
159
  }
126
160
  }
127
- if (sAttr.clas.length === 0 && langClass !== '') sAttr.clas.push(['class', langClass])
128
- if (hasPreLineStart && preLineStart !== -1) {
129
- if (sAttr.style.length === 0) {
130
- sAttr.style.push(['style', 'counter-set:pre-line-number ' + preLineStart + ';'])
131
- } else {
132
- sAttr.style[0][1] = sAttr.style[0][1].replace(/;?$/, '; counter-set:pre-line-number ' + preLineStart + ';')
133
- }
161
+ if (startNumber !== -1) {
162
+ token.attrJoin('style', 'counter-set:pre-line-number ' + startNumber + ';')
134
163
  }
135
- //console.log(JSON.stringify(sAttr))
164
+ orderTokenAttrs(token, opt)
165
+
136
166
  if (opt.setHighlight && md.options.highlight) {
137
167
  if (lang && lang !== 'samp' ) {
138
168
  content = md.options.highlight(content, lang)
139
- /*
140
- try {
141
- content = highlightjs.highlight(token.content, {language: lang}).value
142
- } catch (__) {}
143
- */
144
169
  } else {
145
170
  content = md.utils.escapeHtml(token.content)
146
171
  }
147
172
  } else {
148
173
  content = md.utils.escapeHtml(token.content)
149
174
  }
150
- if (opt.setLineNumber || opt.setEmphasizeLines) {
151
- content = splitFenceBlockToLines(content, emphasizeLines, opt, hasPreLineStart)
152
- }
153
175
 
154
- let fenceHtml = '<pre>'
155
- let isSamp = /^(?:samp|shell|console)$/.test(lang)
156
- if (isSamp) {
157
- fenceHtml += fenceStartTag('samp', sAttr)
158
- } else {
159
- fenceHtml += fenceStartTag('code', sAttr)
176
+ const needLineNumber = opt.setLineNumber && startNumber >= 0
177
+ const needEmphasis = opt.setEmphasizeLines && emphasizeLines.length > 0
178
+ const needEndSpan = opt.setLineEndSpan > 0
179
+ if (needLineNumber || needEmphasis || needEndSpan) {
180
+ const brMatch = content.match(/\r?\n/)
181
+ const br = brMatch ? brMatch[0] : '\n'
182
+ content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.setLineEndSpan, opt.lineEndSpanClass, br)
160
183
  }
161
184
 
162
- fenceHtml += content
163
- if (isSamp) {
164
- fenceHtml += '</samp>'
165
- } else {
166
- fenceHtml += '</code>'
167
- }
168
- return fenceHtml + '</pre>\n'
185
+ const tag = opt._sampReg.test(lang) ? 'samp' : 'code'
186
+ return `<pre><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
169
187
  }
170
188
 
171
- const mditRendererFence = (md, options) => {
172
- md.renderer.rules['fence'] = (tokens, idx, env, slf) => {
173
- return getFenceHtml(tokens, idx, env, slf, md, options)
189
+ const mditRendererFence = (md, option) => {
190
+ const opt = {
191
+ attrsOrder: ['class', 'id', 'data-*', 'style'],
192
+ setHighlight: true,
193
+ setLineNumber: true,
194
+ setEmphasizeLines: true,
195
+ setLineEndSpan: 0,
196
+ lineEndSpanClass: 'pre-lineend-spacer',
197
+ sampLang: 'shell,console',
198
+ langPrefix: md.options.langPrefix || 'language-',
199
+ }
200
+ if (option) Object.assign(opt, option)
201
+
202
+ opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
203
+ opt._langReg = new RegExp(opt.langPrefix + '([A-Za-z0-9-]+)')
204
+
205
+ md.renderer.rules['fence'] = (tokens, idx, options, env, slf) => {
206
+ return getFenceHtml(tokens, idx, md, opt, slf)
174
207
  }
175
208
  }
176
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-renderer-fence",
3
- "version": "0.1.2",
3
+ "version": "0.2.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
  "type": "module",
@@ -0,0 +1,55 @@
1
+ [Markdown]
2
+ ```
3
+ 1234567890
4
+ 2
5
+ 1234567890
6
+ 4
7
+ 1234567890
8
+ 6
9
+ ```
10
+ [HTML]
11
+ <pre><code>1234567890<span class="pre-lineend-spacer"></span>
12
+ 2
13
+ 1234567890<span class="pre-lineend-spacer"></span>
14
+ 4
15
+ 1234567890<span class="pre-lineend-spacer"></span>
16
+ 6
17
+ </code></pre>
18
+
19
+
20
+ [Markdown]
21
+ ``` {em-lines="3"}
22
+ 1234567890
23
+ 2
24
+ 1234567890
25
+ 4
26
+ 1234567890
27
+ 6
28
+ ```
29
+ [HTML]
30
+ <pre><code data-pre-emphasis="3">1234567890<span class="pre-lineend-spacer"></span>
31
+ 2
32
+ <span class="pre-lines-emphasis">1234567890<span class="pre-lineend-spacer"></span>
33
+ </span>4
34
+ 1234567890<span class="pre-lineend-spacer"></span>
35
+ 6
36
+ </code></pre>
37
+
38
+
39
+ [Markdown]
40
+ ``` {em-lines="3" start="4"}
41
+ 1234567890
42
+ 2
43
+ 1234567890
44
+ 4
45
+ 1234567890
46
+ 6
47
+ ```
48
+ [HTML]
49
+ <pre><code data-pre-emphasis="3" data-pre-start="4" style="counter-set:pre-line-number 4;"><span class="pre-line">1234567890<span class="pre-lineend-spacer"></span></span>
50
+ <span class="pre-line">2</span>
51
+ <span class="pre-lines-emphasis"><span class="pre-line">1234567890<span class="pre-lineend-spacer"></span></span>
52
+ </span><span class="pre-line">4</span>
53
+ <span class="pre-line">1234567890<span class="pre-lineend-spacer"></span></span>
54
+ <span class="pre-line">6</span>
55
+ </code></pre>
@@ -8,7 +8,7 @@
8
8
  6
9
9
  ```
10
10
  [HTML]
11
- <pre><code>1
11
+ <pre><code data-pre-emphasis="3-5">1
12
12
  2
13
13
  <span class="pre-lines-emphasis">3
14
14
  4
@@ -27,7 +27,7 @@
27
27
  6
28
28
  ```
29
29
  [HTML]
30
- <pre><code><span class="pre-lines-emphasis">1
30
+ <pre><code data-pre-emphasis="1"><span class="pre-lines-emphasis">1
31
31
  </span>2
32
32
  3
33
33
  4
@@ -46,7 +46,7 @@
46
46
  6
47
47
  ```
48
48
  [HTML]
49
- <pre><code><span class="pre-lines-emphasis">1
49
+ <pre><code data-pre-emphasis="1-3,5-6"><span class="pre-lines-emphasis">1
50
50
  2
51
51
  3
52
52
  </span>4
@@ -65,7 +65,7 @@
65
65
  6
66
66
  ```
67
67
  [HTML]
68
- <pre><code data-pre-start="1" style="counter-set:pre-line-number 1;"><span class="pre-line">1</span>
68
+ <pre><code data-pre-emphasis="3-5" data-pre-start="1" style="counter-set:pre-line-number 1;"><span class="pre-line">1</span>
69
69
  <span class="pre-line">2</span>
70
70
  <span class="pre-lines-emphasis"><span class="pre-line">3</span>
71
71
  <span class="pre-line">4</span>
@@ -84,15 +84,10 @@
84
84
  6
85
85
  ```
86
86
  [HTML]
87
- <pre><code>1
87
+ <pre><code data-pre-emphasis="2,4-5">1
88
88
  <span class="pre-lines-emphasis">2
89
89
  </span>3
90
90
  <span class="pre-lines-emphasis">4
91
91
  5
92
92
  </span>6
93
93
  </code></pre>
94
-
95
-
96
-
97
-
98
-
package/test/examples.txt CHANGED
@@ -81,7 +81,7 @@ console.log('A test.')
81
81
  console.log('A test.')
82
82
  ```
83
83
  [HTML]
84
- <pre><code id="id" class="language-js style">console.log('A test.')
84
+ <pre><code class="language-js style" id="id">console.log('A test.')
85
85
  </code></pre>
86
86
 
87
87
  [Markdown]
package/test/test.js CHANGED
@@ -26,6 +26,7 @@ const mdHighlightJs = mdit({
26
26
  }
27
27
  }).use(mditRendererFence, opt).use(mditAttrs)
28
28
  const mdLinesEmphasis = mdit({ html: true }).use(mditRendererFence).use(mditAttrs)
29
+ const mdLIneEndSpan = mdit({ html: true }).use(mditRendererFence, {setLineEndSpan: 8}).use(mditAttrs)
29
30
 
30
31
  let __dirname = path.dirname(new URL(import.meta.url).pathname)
31
32
  const isWindows = (process.platform === 'win32')
@@ -37,6 +38,7 @@ const testData = {
37
38
  noOption: __dirname + path.sep + 'examples.txt',
38
39
  highlightjs: __dirname + path.sep + 'examples-highlightjs.txt',
39
40
  linesEmphasis: __dirname + path.sep + 'example-lines-emphasis.txt',
41
+ lineEndSpan: __dirname + path.sep + 'example-line-end-span.txt',
40
42
  }
41
43
 
42
44
  const getTestData = (pat) => {
@@ -88,7 +90,7 @@ const runTest = (process, pat, pass, testId) => {
88
90
 
89
91
  while(n <= end) {
90
92
  if (!ms[n]
91
- //|| n != 2
93
+ //|| n != 14
92
94
  ) {
93
95
  n++
94
96
  continue
@@ -116,5 +118,6 @@ let pass = true
116
118
  pass = runTest(md, testData.noOption, pass)
117
119
  pass = runTest(mdHighlightJs, testData.highlightjs, pass)
118
120
  pass = runTest(mdLinesEmphasis, testData.linesEmphasis, pass)
121
+ pass = runTest(mdLIneEndSpan, testData.lineEndSpan, pass)
119
122
 
120
123
  if (pass) console.log('Passed all test.')