@sullux/markdown-html 2.0.0 → 2.0.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.
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const KATEX_HEAD = [
|
|
2
|
+
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" />',
|
|
3
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>',
|
|
4
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body, { delimiters: [{left: \'$$\', right: \'$$\', display: true}, {left: \'$\', right: \'$\', display: false}], throwOnError: false });"></script>',
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
const MERMAID_HEAD = [
|
|
8
|
+
`<script type="module">
|
|
9
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
10
|
+
const getTheme = () => document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default';
|
|
11
|
+
mermaid.initialize({ startOnLoad: true, theme: getTheme() });
|
|
12
|
+
window.addEventListener('@sullux/markdown:theme', (e) => {
|
|
13
|
+
mermaid.initialize({ theme: e.detail?.mode === 'dark' ? 'dark' : 'default' });
|
|
14
|
+
});
|
|
15
|
+
</script>`,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
module.exports = { KATEX_HEAD, MERMAID_HEAD }
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
const { parse } = require('@sullux/markdown-compiler')
|
|
2
|
-
const {
|
|
2
|
+
const { renderBlocks } = require('./render-block')
|
|
3
3
|
|
|
4
4
|
const markdownToHtml = (markdown, options = {}) => {
|
|
5
|
-
if (!markdown)
|
|
5
|
+
if (!markdown) {
|
|
6
|
+
return { html: '', head: [], toString() { return '' } }
|
|
7
|
+
}
|
|
6
8
|
const ast = typeof markdown === 'string' ? parse(markdown) : markdown
|
|
7
9
|
const ctx = { ...options, usedSlugs: new Set() }
|
|
8
|
-
|
|
10
|
+
const result = renderBlocks(ast.blocks, ctx)
|
|
11
|
+
const uniqueHead = [...new Set(result.head)]
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
html: result.html,
|
|
15
|
+
head: uniqueHead,
|
|
16
|
+
toString() {
|
|
17
|
+
return this.html
|
|
18
|
+
},
|
|
19
|
+
}
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
module.exports = { markdownToHtml }
|
|
@@ -3,6 +3,9 @@ const { slugify } = require('./slugify')
|
|
|
3
3
|
const { renderInline } = require('./render-inline')
|
|
4
4
|
const { highlightCode } = require('./highlight')
|
|
5
5
|
const { renderList } = require('./render-list')
|
|
6
|
+
const { KATEX_HEAD, MERMAID_HEAD } = require('./assets')
|
|
7
|
+
|
|
8
|
+
const hasInlineMath = (children) => Boolean(children?.some((c) => c.type === 'inlineMath'))
|
|
6
9
|
|
|
7
10
|
const renderBlock = (node, options = {}) => {
|
|
8
11
|
if (!node) return ''
|
|
@@ -12,76 +15,77 @@ const renderBlock = (node, options = {}) => {
|
|
|
12
15
|
const text = renderInline(node.children, options)
|
|
13
16
|
const rawText = node.children ? node.children.map((c) => (c.type === 'text' ? c.value : '')).join('') : ''
|
|
14
17
|
const slug = (options.slugify || slugify)(rawText, options.usedSlugs)
|
|
15
|
-
|
|
18
|
+
const html = `<h${node.level} id="${slug}">${text}</h${node.level}>\n`
|
|
19
|
+
return hasInlineMath(node.children) ? { html, head: KATEX_HEAD } : html
|
|
16
20
|
}
|
|
17
21
|
case 'paragraph': {
|
|
18
|
-
|
|
22
|
+
const html = `<p>${renderInline(node.children, options)}</p>\n`
|
|
23
|
+
return hasInlineMath(node.children) ? { html, head: KATEX_HEAD } : html
|
|
19
24
|
}
|
|
20
25
|
case 'codeBlock': {
|
|
21
26
|
const lang = node.language || ''
|
|
22
|
-
if (options.codeRenderers
|
|
23
|
-
|
|
27
|
+
if (options.codeRenderers?.[lang]) return options.codeRenderers[lang](node, options)
|
|
28
|
+
if (lang === 'math') {
|
|
29
|
+
if (options.mathBlockRenderer) return options.mathBlockRenderer(node, options)
|
|
30
|
+
return { html: `<div class="math-display" data-latex="${escapeHtml(node.value)}">$$\n${escapeHtml(node.value)}\n$$</div>\n`, head: KATEX_HEAD }
|
|
31
|
+
}
|
|
32
|
+
if (lang === 'mermaid') {
|
|
33
|
+
return { html: `<pre class="mermaid">${escapeHtml(node.value)}</pre>\n`, head: MERMAID_HEAD }
|
|
24
34
|
}
|
|
25
35
|
const highlighted = highlightCode(node.value, lang, options.tokenizers)
|
|
26
36
|
const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ''
|
|
27
37
|
return `<pre><code${langClass}>${highlighted}</code></pre>\n`
|
|
28
38
|
}
|
|
29
39
|
case 'blockquote': {
|
|
30
|
-
const
|
|
31
|
-
return `<blockquote>\n${
|
|
40
|
+
const { html, head } = renderBlocks(node.children, options)
|
|
41
|
+
return { html: `<blockquote>\n${html}</blockquote>\n`, head }
|
|
32
42
|
}
|
|
33
43
|
case 'callout': {
|
|
34
44
|
const style = node.style || 'note'
|
|
35
45
|
const title = style.charAt(0).toUpperCase() + style.slice(1)
|
|
36
|
-
const
|
|
37
|
-
return `<div class="callout callout-${style}">\n<div class="callout-title">${title}</div>\n${
|
|
46
|
+
const { html, head } = renderBlocks(node.children, options)
|
|
47
|
+
return { html: `<div class="callout callout-${style}">\n<div class="callout-title">${title}</div>\n${html}</div>\n`, head }
|
|
38
48
|
}
|
|
39
49
|
case 'bulletList':
|
|
40
50
|
case 'orderedList': {
|
|
41
|
-
return renderList(node, options, renderBlock)
|
|
51
|
+
return renderList(node, options, renderBlock, hasInlineMath, KATEX_HEAD)
|
|
42
52
|
}
|
|
43
53
|
case 'table': {
|
|
44
54
|
const alignments = node.alignments || []
|
|
45
55
|
const rows = node.rows || []
|
|
46
56
|
if (rows.length === 0) return ''
|
|
47
|
-
|
|
48
|
-
const headerRow = rows[0]
|
|
49
|
-
const bodyRows = rows.slice(1)
|
|
50
|
-
|
|
51
57
|
const renderRow = (row, isHeader) => {
|
|
52
|
-
const
|
|
53
|
-
const cells = row
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return `<${cellTag}${alignAttr}>${renderInline(cell, options)}</${cellTag}>`
|
|
58
|
-
})
|
|
59
|
-
.join('')
|
|
58
|
+
const tag = isHeader ? 'th' : 'td'
|
|
59
|
+
const cells = row.map((cell, i) => {
|
|
60
|
+
const align = alignments[i] || 'default'
|
|
61
|
+
return `<${tag}${align !== 'default' ? ` align="${align}"` : ''}>${renderInline(cell, options)}</${tag}>`
|
|
62
|
+
}).join('')
|
|
60
63
|
return `<tr>${cells}</tr>\n`
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
tableHtml += '</table>\n'
|
|
68
|
-
return tableHtml
|
|
69
|
-
}
|
|
70
|
-
case 'hr': {
|
|
71
|
-
return '<hr />\n'
|
|
72
|
-
}
|
|
73
|
-
case 'html': {
|
|
74
|
-
return `${node.value}\n`
|
|
65
|
+
const tableBody = rows.slice(1).length ? `<tbody>\n${rows.slice(1).map((r) => renderRow(r, false)).join('')}</tbody>\n` : ''
|
|
66
|
+
const tableHtml = `<table>\n<thead>\n${renderRow(rows[0], true)}</thead>\n${tableBody}</table>\n`
|
|
67
|
+
const hasMath = rows.some((row) => row.some((cell) => hasInlineMath(cell)))
|
|
68
|
+
return hasMath ? { html: tableHtml, head: KATEX_HEAD } : tableHtml
|
|
75
69
|
}
|
|
70
|
+
case 'hr': return '<hr />\n'
|
|
71
|
+
case 'html': return `${node.value}\n`
|
|
76
72
|
case 'mathBlock': {
|
|
77
73
|
const mathRenderer = options.mathBlockRenderer || options.codeRenderers?.math
|
|
78
74
|
if (mathRenderer) return mathRenderer(node, options)
|
|
79
|
-
return `<div class="math-display" data-latex="${escapeHtml(node.value)}">$$\n${escapeHtml(node.value)}\n$$</div>\n
|
|
80
|
-
}
|
|
81
|
-
default: {
|
|
82
|
-
return ''
|
|
75
|
+
return { html: `<div class="math-display" data-latex="${escapeHtml(node.value)}">$$\n${escapeHtml(node.value)}\n$$</div>\n`, head: KATEX_HEAD }
|
|
83
76
|
}
|
|
77
|
+
default: return ''
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
const renderBlocks = (blocks, options) => (blocks || []).reduce(
|
|
82
|
+
(acc, block) => {
|
|
83
|
+
const res = renderBlock(block, options)
|
|
84
|
+
const html = typeof res === 'string' ? res : res?.html || ''
|
|
85
|
+
const head = typeof res === 'object' && Array.isArray(res?.head) ? res.head : []
|
|
86
|
+
return { html: acc.html + html, head: acc.head.concat(head) }
|
|
87
|
+
},
|
|
88
|
+
{ html: '', head: [] }
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
module.exports = { renderBlock, renderBlocks }
|
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
const { renderInline } = require('./render-inline')
|
|
2
2
|
|
|
3
|
-
const renderListItem = (item, options, tight, renderBlock) => {
|
|
4
|
-
const checkHtml = item.checked !== undefined
|
|
5
|
-
? `<input type="checkbox"${item.checked ? ' checked' : ''} disabled /> `
|
|
6
|
-
: ''
|
|
3
|
+
const renderListItem = (item, options, tight, renderBlock, hasInlineMath, KATEX_HEAD) => {
|
|
4
|
+
const checkHtml = item.checked !== undefined ? `<input type="checkbox"${item.checked ? ' checked' : ''} disabled /> ` : ''
|
|
7
5
|
const taskClass = item.checked !== undefined ? ' class="task-list-item"' : ''
|
|
8
6
|
|
|
9
7
|
if (Array.isArray(item)) {
|
|
10
|
-
|
|
8
|
+
const itemMath = hasInlineMath(item)
|
|
9
|
+
return {
|
|
10
|
+
html: `<li${taskClass}>${checkHtml}${renderInline(item, options)}</li>\n`,
|
|
11
|
+
head: itemMath ? KATEX_HEAD : [],
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
const childrenHtml = children
|
|
15
|
-
|
|
16
|
-
if (child.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
let head = []
|
|
16
|
+
const childrenHtml = (item.children || []).map((child, idx) => {
|
|
17
|
+
if (child.type === 'paragraph' && tight && idx === 0) {
|
|
18
|
+
if (hasInlineMath(child.children)) head = head.concat(KATEX_HEAD)
|
|
19
|
+
return renderInline(child.children, options)
|
|
20
|
+
}
|
|
21
|
+
const res = renderBlock(child, options)
|
|
22
|
+
if (typeof res === 'object' && Array.isArray(res.head)) head = head.concat(res.head)
|
|
23
|
+
return typeof res === 'string' ? res : res?.html || ''
|
|
24
|
+
}).join('')
|
|
25
|
+
|
|
26
|
+
return { html: `<li${taskClass}>${checkHtml}${childrenHtml}</li>\n`, head }
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const renderList = (node, options, renderBlock) => {
|
|
29
|
+
const renderList = (node, options, renderBlock, hasInlineMath, KATEX_HEAD) => {
|
|
27
30
|
const tight = node.tight !== false
|
|
28
31
|
const isOrdered = node.type === 'orderedList'
|
|
29
32
|
const listItems = node.children || node.items || []
|
|
30
|
-
|
|
33
|
+
let head = []
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const itemsHtml = listItems.map((item) => {
|
|
36
|
+
const res = renderListItem(item, options, tight, renderBlock, hasInlineMath, KATEX_HEAD)
|
|
37
|
+
if (res.head) head = head.concat(res.head)
|
|
38
|
+
return res.html
|
|
39
|
+
}).join('')
|
|
40
|
+
|
|
41
|
+
const startAttr = isOrdered && node.start && node.start !== 1 ? ` start="${node.start}"` : ''
|
|
42
|
+
const tag = isOrdered ? 'ol' : 'ul'
|
|
43
|
+
return { html: `<${tag}${startAttr}>\n${itemsHtml}</${tag}>\n`, head }
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
module.exports = { renderList }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sullux/markdown-html",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A high-performance, bidirectional Markdown <-> HTML compiler supporting GFM, GitBook hints, syntax highlighting, and custom image dimensions.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"author": "Charles Sullivan <charles@sullux.com>",
|