@peaceroad/markdown-it-renderer-fence 0.4.0 → 0.5.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/index.js CHANGED
@@ -1,485 +1,39 @@
1
- const infoReg = /^([^{\s]*)(?:\s*\{(.*)\})?$/
2
-
3
- const attrReg = /^(?:([.#])(.+)|(.+?)(?:=(["'])?(.*?)\1)?)$/
4
- const interAttrsSpaceReg = / +/
5
- const tagReg = /<\/?([A-Za-z][A-Za-z0-9-]*)(?:\s+[^>]*?)?\/?\s*>/g
6
- const preLineTag = '<span class="pre-line">'
7
- const emphOpenTag = '<span class="pre-lines-emphasis">'
8
- const commentLineClass = 'pre-comment-line'
9
- const closeTag = '</span>'
10
- const closeTagLen = closeTag.length
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'])
15
-
16
- const appendStyleValue = (style, addition) => {
17
- if (!addition) return style
18
- let next = addition.trim()
19
- if (!next) return style
20
- if (!next.endsWith(';')) next += ';'
21
- if (!style) return next
22
- const base = style.trimEnd()
23
- const separator = base.endsWith(';') ? ' ' : '; '
24
- return base + separator + next
25
- }
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
-
72
- const getEmphasizeLines = (attrVal) => {
73
- const lines = []
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
- }
90
- } else {
91
- const s = parseInt(part, 10)
92
- if (!Number.isFinite(s) || s <= 0) continue
93
- lines.push([s, s])
94
- }
95
- }
96
- return lines
97
- }
98
-
99
- const splitFenceBlockToLines = (content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, threshold, lineEndSpanClass, br, commentLines, commentClass) => {
100
- const lines = content.split(br)
101
- const len = lines.length
102
- const endSpanTag = needEndSpan ? `<span class="${lineEndSpanClass}"></span>` : ''
103
- const needComment = !!(commentLines && commentClass)
104
- const maxLine = len - (lines[len - 1] === '' ? 1 : 0)
105
- let emIdx = 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
- }
131
- for (let n = 0; n < len; n++) {
132
- let line = lines[n]
133
-
134
- if (needEndSpan) {
135
- let lineLen = line.length
136
- if (lineLen < threshold) {
137
- lineLen = 0
138
- for (let i = 0, L = line.length; i < L; i++) {
139
- lineLen += line.charCodeAt(i) > 255 ? 2 : 1
140
- if (lineLen >= threshold) break
141
- }
142
- }
143
- if (lineLen >= threshold) {
144
- if (line.slice(-closeTagLen) === closeTag) {
145
- line = line.slice(0, -closeTagLen) + endSpanTag + closeTag
146
- } else {
147
- line = line + endSpanTag
148
- }
149
- }
150
- }
151
-
152
- if (needLineNumber && n !== len - 1) {
153
- if (line.indexOf('<') !== -1) {
154
- const tagStack = []
155
- tagReg.lastIndex = 0
156
- let match
157
- while ((match = tagReg.exec(line)) !== null) {
158
- const [fullMatch, tagName] = match
159
- const tagNameLower = tagName.toLowerCase()
160
- if (fullMatch.startsWith('</')) {
161
- if (tagStack[tagStack.length-1] === tagNameLower) tagStack.pop()
162
- } else if (!fullMatch.endsWith('/>') && !voidTags.has(tagNameLower)) {
163
- tagStack.push(tagNameLower)
164
- }
165
- }
166
- for (let i = tagStack.length-1; i >= 0; i--) {
167
- const tagName = tagStack[i]
168
- line += `</${tagName}>`
169
- lines[n+1] = `<${tagName}>` + (lines[n+1]||'')
170
- }
171
- }
172
- }
173
-
174
- if (needComment && commentLines[n]) {
175
- line = `<span class="${commentClass}">` + line + closeTag
176
- }
177
-
178
- if (needLineNumber && n !== len - 1) {
179
- line = preLineTag + line + closeTag
180
- }
181
-
182
- if (doEmphasis) {
183
- if (emStart === n + 1) line = emphOpenTag + line
184
- if (emEnd === n) {
185
- line = closeTag + line
186
- emIdx++
187
- [emStart, emEnd] = emphasizeLines[emIdx] || []
188
- }
189
- }
190
- lines[n] = line
191
- }
192
- return lines.join(br)
193
- }
194
-
195
- const getInfoAttr = (infoAttr) => {
196
- let attrSets = infoAttr.trim().split(interAttrsSpaceReg)
197
- let arr = []
198
- for (let attrSet of attrSets) {
199
- const str = attrReg.exec(attrSet)
200
- if (str) {
201
- if (str[1] === '.') str[1] = 'class'
202
- if (str[1] === '#') str[1] = 'id'
203
- if (str[3]) {
204
- str[1] = str[3]
205
- str[2] = str[5] ? str[5].replace(/^['"]/, '').replace(/['"]$/, '') : ''
206
- }
207
- arr.push([str[1], str[2]])
208
- }
209
- }
210
- return arr
211
- }
212
-
213
- const orderTokenAttrs = (token, opt) => {
214
- if (!token.attrs) return
215
- const order = opt.attrsOrder || []
216
- token.attrs.sort((a, b) => {
217
- const idx = (name) => {
218
- for (let i = 0; i < order.length; i++) {
219
- const key = order[i]
220
- if (key.endsWith('*')) {
221
- const prefix = key.slice(0, -1)
222
- if (name.startsWith(prefix)) return i
223
- } else if (name === key) {
224
- return i
225
- }
226
- }
227
- return order.length
228
- }
229
- return idx(a[0]) - idx(b[0])
230
- })
231
- }
232
-
233
- const getFenceHtml = (tokens, idx, md, opt, slf) => {
234
- const token = tokens[idx]
235
- let content = token.content
236
- const info = token.info.trim()
237
- const match = info.match(infoReg)
238
- let lang = match ? match[1] : ''
239
-
240
- if (match && match[2]) {
241
- getInfoAttr(match[2]).forEach(([name, val]) => token.attrJoin(name, val))
242
- }
243
- let langClass = ''
244
- if (lang && lang !== 'samp') {
245
- langClass = opt.langPrefix + lang
246
- const existingClass = token.attrGet('class')
247
- token.attrSet('class', existingClass ? langClass + ' ' + existingClass : langClass)
248
- }
249
-
250
- let startNumber = -1
251
- let emphasizeLines = []
252
- let wrapEnabled = false
253
- let preWrapValue
254
- let commentLineValue
255
-
256
- if (token.attrs) {
257
- const newAttrs = []
258
- let dataPreStartIndex = -1
259
- let dataPreEmphasisIndex = -1
260
- let styleIndex = -1
261
- let dataPreCommentIndex = -1
262
- let startValue
263
- let emphasisValue
264
- let styleValue
265
- let sawCommentLine = false
266
- let sawStartAttr = false
267
- let sawEmphasisAttr = false
268
- const appendOrder = []
269
-
270
- for (const attr of token.attrs) {
271
- const name = attr[0]
272
- const val = attr[1]
273
-
274
- switch (name) {
275
- case 'class': {
276
- if (val.indexOf(opt.langPrefix) !== -1) {
277
- const hasClassLang = opt._langReg.exec(val)
278
- if (hasClassLang) lang = hasClassLang[1]
279
- }
280
- newAttrs.push(attr)
281
- break
282
- }
283
- case 'style':
284
- styleIndex = newAttrs.length
285
- styleValue = val
286
- newAttrs.push(attr)
287
- break
288
- case 'data-pre-start':
289
- startNumber = +val
290
- startValue = val
291
- dataPreStartIndex = newAttrs.length
292
- newAttrs.push(attr)
293
- break
294
- case 'start':
295
- case 'pre-start':
296
- startNumber = +val
297
- startValue = val
298
- if (!sawStartAttr) {
299
- appendOrder.push('start')
300
- sawStartAttr = true
301
- }
302
- break
303
- case 'data-pre-emphasis':
304
- dataPreEmphasisIndex = newAttrs.length
305
- newAttrs.push(attr)
306
- break
307
- case 'data-pre-comment-line':
308
- dataPreCommentIndex = newAttrs.length
309
- commentLineValue = val
310
- newAttrs.push(attr)
311
- break
312
- case 'em-lines':
313
- case 'emphasize-lines':
314
- emphasizeLines = getEmphasizeLines(val)
315
- emphasisValue = val
316
- if (!sawEmphasisAttr) {
317
- appendOrder.push('emphasis')
318
- sawEmphasisAttr = true
319
- }
320
- break
321
- case 'comment-line':
322
- commentLineValue = val
323
- if (!sawCommentLine) {
324
- appendOrder.push('comment')
325
- sawCommentLine = true
326
- }
327
- break
328
- case 'data-pre-wrap':
329
- preWrapValue = val
330
- if (val === '' || val === 'true') wrapEnabled = true
331
- break
332
- case 'wrap':
333
- case 'pre-wrap':
334
- if (val === '' || val === 'true') {
335
- wrapEnabled = true
336
- }
337
- break
338
- default:
339
- newAttrs.push(attr)
340
- }
341
- }
342
-
343
- if (startValue !== undefined && dataPreStartIndex >= 0) {
344
- newAttrs[dataPreStartIndex][1] = startValue
345
- }
346
- if (emphasisValue !== undefined && dataPreEmphasisIndex >= 0) {
347
- newAttrs[dataPreEmphasisIndex][1] = emphasisValue
348
- }
349
- if (commentLineValue !== undefined && dataPreCommentIndex >= 0) {
350
- newAttrs[dataPreCommentIndex][1] = commentLineValue
351
- }
352
- for (const kind of appendOrder) {
353
- if (kind === 'start') {
354
- if (dataPreStartIndex === -1 && startValue !== undefined) {
355
- newAttrs.push(['data-pre-start', startValue])
356
- }
357
- } else if (kind === 'emphasis') {
358
- if (dataPreEmphasisIndex === -1 && emphasisValue !== undefined) {
359
- newAttrs.push(['data-pre-emphasis', emphasisValue])
360
- }
361
- } else if (kind === 'comment') {
362
- if (dataPreCommentIndex === -1 && commentLineValue !== undefined) {
363
- newAttrs.push(['data-pre-comment-line', commentLineValue])
364
- }
365
- }
366
- }
367
- if (startNumber !== -1) {
368
- styleValue = appendStyleValue(styleValue, 'counter-set:pre-line-number ' + startNumber + ';')
369
- }
370
- if (styleIndex >= 0) {
371
- if (styleValue !== undefined) newAttrs[styleIndex][1] = styleValue
372
- } else if (styleValue) {
373
- newAttrs.push(['style', styleValue])
374
- }
375
- if (wrapEnabled) preWrapValue = 'true'
376
- token.attrs = newAttrs
377
- }
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
389
- }
390
- }
391
-
392
- if (opt.setHighlight && md.options.highlight) {
393
- if (lang && lang !== 'samp' ) {
394
- content = md.options.highlight(content, lang)
395
- } else {
396
- content = md.utils.escapeHtml(token.content)
397
- }
398
- } else {
399
- content = md.utils.escapeHtml(token.content)
400
- }
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
-
443
- const needLineNumber = opt.setLineNumber && startNumber >= 0
444
- const needEmphasis = opt.setEmphasizeLines && emphasizeLines.length > 0
445
- const needEndSpan = opt.lineEndSpanThreshold > 0
446
- const useHighlightPre = opt.useHighlightPre && hasHighlightPre
447
- if (!useHighlightPre && (needLineNumber || needEmphasis || needEndSpan || needComment)) {
448
- const nlIndex = content.indexOf('\n')
449
- const br = nlIndex > 0 && content[nlIndex - 1] === '\r' ? '\r\n' : '\n'
450
- content = splitFenceBlockToLines(content, emphasizeLines, needLineNumber, needEmphasis, needEndSpan, opt.lineEndSpanThreshold, opt.lineEndSpanClass, br, commentLines, commentLineClass)
451
- }
452
-
453
- const tag = isSamp ? 'samp' : 'code'
454
- return `<pre${preAttrsText}><${tag}${slf.renderAttrs(token)}>${content}</${tag}></pre>\n`
1
+ import mditRendererFenceMarkup from './src/entry/markup-highlight.js'
2
+ import mditRendererFenceCustomHighlight, {
3
+ applyCustomHighlights,
4
+ clearCustomHighlights,
5
+ customHighlightPayloadSchemaVersion,
6
+ customHighlightPayloadSupportedVersions,
7
+ getCustomHighlightPayloadMap,
8
+ observeCustomHighlights,
9
+ renderCustomHighlightPayloadScript,
10
+ renderCustomHighlightScopeStyleTag,
11
+ shouldRuntimeFallback,
12
+ } from './src/entry/custom-highlight.js'
13
+
14
+ const normalizeHighlightRenderer = (value) => {
15
+ const key = String(value || '').trim().toLowerCase()
16
+ if (key === 'api' || key === 'custom-highlight-api') return 'api'
17
+ return 'markup'
455
18
  }
456
19
 
457
20
  const mditRendererFence = (md, option) => {
458
- const opt = {
459
- attrsOrder: ['class', 'id', 'data-*', 'style'],
460
- setHighlight: true,
461
- setLineNumber: true,
462
- setEmphasizeLines: true,
463
- lineEndSpanThreshold: 0,
464
- lineEndSpanClass: 'pre-lineend-spacer',
465
- setPreWrapStyle: true,
466
- useHighlightPre: false,
467
- sampLang: 'shell,console',
468
- langPrefix: md.options.langPrefix || 'language-',
469
- }
470
- if (option) {
471
- Object.assign(opt, option)
472
- if (option.lineEndSpanThreshold == null && option.setLineEndSpan != null) {
473
- opt.lineEndSpanThreshold = option.setLineEndSpan
474
- }
475
- }
476
-
477
- opt._sampReg = new RegExp('^(?:samp|' + opt.sampLang.split(',').join('|') + ')$')
478
- opt._langReg = new RegExp(opt.langPrefix + '([A-Za-z0-9-]+)')
479
-
480
- md.renderer.rules['fence'] = (tokens, idx, options, env, slf) => {
481
- return getFenceHtml(tokens, idx, md, opt, slf)
482
- }
21
+ if (normalizeHighlightRenderer(option && option.highlightRenderer) === 'api') {
22
+ return mditRendererFenceCustomHighlight(md, option)
23
+ }
24
+ return mditRendererFenceMarkup(md, option)
25
+ }
26
+
27
+ export {
28
+ applyCustomHighlights,
29
+ clearCustomHighlights,
30
+ customHighlightPayloadSchemaVersion,
31
+ customHighlightPayloadSupportedVersions,
32
+ getCustomHighlightPayloadMap,
33
+ observeCustomHighlights,
34
+ renderCustomHighlightPayloadScript,
35
+ renderCustomHighlightScopeStyleTag,
36
+ shouldRuntimeFallback,
483
37
  }
484
38
 
485
39
  export default mditRendererFence
package/package.json CHANGED
@@ -1,22 +1,48 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-renderer-fence",
3
- "version": "0.4.0",
3
+ "version": "0.5.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
+ "exports": {
7
+ ".": "./index.js",
8
+ "./custom-highlight": "./src/entry/custom-highlight.js",
9
+ "./custom-highlight-runtime": "./src/entry/custom-highlight-runtime.js",
10
+ "./markup-highlight": "./src/entry/markup-highlight.js"
11
+ },
6
12
  "files": [
7
13
  "index.js",
14
+ "src",
8
15
  "LICENSE",
16
+ "THIRD_PARTY_NOTICES.md",
9
17
  "README.md"
10
18
  ],
11
19
  "type": "module",
20
+ "customHighlightPayload": {
21
+ "schemaVersion": 1,
22
+ "supportedVersions": [
23
+ 1
24
+ ]
25
+ },
12
26
  "scripts": {
13
- "test": "node test/test.js"
27
+ "test": "node test/test.js && node test/custom-highlight/provider-contract.test.js && node test/custom-highlight/provider-keyword-coverage.test.js && node test/custom-highlight/provider-keyword-holdout-parity.test.js && node -e \"console.log('Passed all test.')\"",
28
+ "test:provider:contract": "node test/custom-highlight/provider-contract.test.js",
29
+ "test:provider:keyword": "node test/custom-highlight/provider-keyword-coverage.test.js",
30
+ "test:provider:keyword:holdout": "node test/custom-highlight/provider-keyword-holdout-parity.test.js",
31
+ "test:provider:keyword:mismatch": "node test/custom-highlight/provider-keyword-mismatch-report.js",
32
+ "test:provider:matrix": "node test/custom-highlight/provider-matrix-audit.js",
33
+ "build:demo:provider:keyword": "node test/custom-highlight/build-provider-keyword-demos.js",
34
+ "build:example:custom-highlight": "node example/build-custom-highlight-compare.js",
35
+ "test:performance": "node test/performance/benchmark.js",
36
+ "test:performance:highlighter": "node test/performance/highlighter-benchmark.js",
37
+ "test:performance:provider-modes": "node test/performance/provider-mode-benchmark.js",
38
+ "test:performance:runtime": "node test/performance/runtime-apply-benchmark.js",
39
+ "test:performance:keyword": "node test/performance/provider-mode-benchmark.js"
14
40
  },
15
41
  "author": "peaceroad <peaceroad@gmail.com>",
16
42
  "repository": "https://github.com/peaceroad/p7d-markdown-it-renderer-fence",
17
43
  "license": "MIT",
18
44
  "devDependencies": {
19
- "@peaceroad/markdown-it-figure-with-p-caption": "^0.15.2",
45
+ "@peaceroad/markdown-it-figure-with-p-caption": "^0.16.0",
20
46
  "highlight.js": "^11.11.1",
21
47
  "markdown-it": "^14.1.0",
22
48
  "markdown-it-attrs": "^4.3.1",