@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 { renderBlock } = require('./render-block')
2
+ const { renderBlocks } = require('./render-block')
3
3
 
4
4
  const markdownToHtml = (markdown, options = {}) => {
5
- if (!markdown) return ''
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
- return ast.blocks.map((block) => renderBlock(block, ctx)).join('')
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
- return `<h${node.level} id="${slug}">${text}</h${node.level}>\n`
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
- return `<p>${renderInline(node.children, options)}</p>\n`
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 && lang && options.codeRenderers[lang]) {
23
- return options.codeRenderers[lang](node, options)
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 innerHtml = node.children ? node.children.map((child) => renderBlock(child, options)).join('') : ''
31
- return `<blockquote>\n${innerHtml}</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 innerHtml = node.children ? node.children.map((child) => renderBlock(child, options)).join('') : ''
37
- return `<div class="callout callout-${style}">\n<div class="callout-title">${title}</div>\n${innerHtml}</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 cellTag = isHeader ? 'th' : 'td'
53
- const cells = row
54
- .map((cell, colIdx) => {
55
- const align = alignments[colIdx] || 'default'
56
- const alignAttr = align !== 'default' ? ` align="${align}"` : ''
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
- let tableHtml = '<table>\n<thead>\n' + renderRow(headerRow, true) + '</thead>\n'
64
- if (bodyRows.length > 0) {
65
- tableHtml += '<tbody>\n' + bodyRows.map((r) => renderRow(r, false)).join('') + '</tbody>\n'
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
- module.exports = { renderBlock }
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
- return `<li${taskClass}>${checkHtml}${renderInline(item, options)}</li>\n`
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
- const children = item.children || []
14
- const childrenHtml = children
15
- .map((child, idx) => {
16
- if (child.type === 'paragraph' && tight && idx === 0) {
17
- return renderInline(child.children, options)
18
- }
19
- return renderBlock(child, options)
20
- })
21
- .join('')
22
-
23
- return `<li${taskClass}>${checkHtml}${childrenHtml}</li>\n`
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
- const itemsHtml = listItems.map((item) => renderListItem(item, options, tight, renderBlock)).join('')
33
+ let head = []
31
34
 
32
- if (isOrdered) {
33
- const startAttr = node.start && node.start !== 1 ? ` start="${node.start}"` : ''
34
- return `<ol${startAttr}>\n${itemsHtml}</ol>\n`
35
- }
36
- return `<ul>\n${itemsHtml}</ul>\n`
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.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>",