@sullux/markdown-html 1.0.2 → 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.
@@ -60,17 +60,19 @@ const processBlockNode = (node, style, blocks, currentInline, traverse, toAst) =
60
60
 
61
61
  if (tagName === 'ul' || tagName === 'ol') {
62
62
  flushInline()
63
- const items = []
63
+ const children = []
64
64
  for (const child of node.children) {
65
65
  if (child.tagName === 'li') {
66
66
  const itemInline = []
67
67
  for (const liChild of child.children) traverse(liChild, style, itemInline)
68
- if (itemInline.length > 0) items.push(itemInline)
68
+ if (itemInline.length > 0) {
69
+ children.push({ type: 'listItem', children: [{ type: 'paragraph', children: itemInline }] })
70
+ }
69
71
  } else {
70
72
  traverse(child, style, currentInline)
71
73
  }
72
74
  }
73
- if (items.length > 0) blocks.push({ type: tagName === 'ul' ? 'bulletList' : 'orderedList', items })
75
+ if (children.length > 0) blocks.push({ type: tagName === 'ul' ? 'bulletList' : 'orderedList', tight: true, children })
74
76
  return true
75
77
  }
76
78
 
@@ -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 }
@@ -2,6 +2,10 @@ const { escapeHtml } = require('./escape')
2
2
  const { slugify } = require('./slugify')
3
3
  const { renderInline } = require('./render-inline')
4
4
  const { highlightCode } = require('./highlight')
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'))
5
9
 
6
10
  const renderBlock = (node, options = {}) => {
7
11
  if (!node) return ''
@@ -11,80 +15,77 @@ const renderBlock = (node, options = {}) => {
11
15
  const text = renderInline(node.children, options)
12
16
  const rawText = node.children ? node.children.map((c) => (c.type === 'text' ? c.value : '')).join('') : ''
13
17
  const slug = (options.slugify || slugify)(rawText, options.usedSlugs)
14
- 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
15
20
  }
16
21
  case 'paragraph': {
17
- 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
18
24
  }
19
25
  case 'codeBlock': {
20
26
  const lang = node.language || ''
21
- if (options.codeRenderers && lang && options.codeRenderers[lang]) {
22
- 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 }
23
34
  }
24
35
  const highlighted = highlightCode(node.value, lang, options.tokenizers)
25
36
  const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ''
26
37
  return `<pre><code${langClass}>${highlighted}</code></pre>\n`
27
38
  }
28
39
  case 'blockquote': {
29
- const innerHtml = node.children ? node.children.map((child) => renderBlock(child, options)).join('') : ''
30
- return `<blockquote>\n${innerHtml}</blockquote>\n`
40
+ const { html, head } = renderBlocks(node.children, options)
41
+ return { html: `<blockquote>\n${html}</blockquote>\n`, head }
31
42
  }
32
43
  case 'callout': {
33
44
  const style = node.style || 'note'
34
45
  const title = style.charAt(0).toUpperCase() + style.slice(1)
35
- const innerHtml = node.children ? node.children.map((child) => renderBlock(child, options)).join('') : ''
36
- return `<div class="callout callout-${style}">\n<div class="callout-title">${title}</div>\n${innerHtml}</div>\n`
37
- }
38
- case 'bulletList': {
39
- const itemsHtml = node.items ? node.items.map((item) => `<li>${renderInline(item, options)}</li>\n`).join('') : ''
40
- return `<ul>\n${itemsHtml}</ul>\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 }
41
48
  }
49
+ case 'bulletList':
42
50
  case 'orderedList': {
43
- const itemsHtml = node.items ? node.items.map((item) => `<li>${renderInline(item, options)}</li>\n`).join('') : ''
44
- return `<ol>\n${itemsHtml}</ol>\n`
51
+ return renderList(node, options, renderBlock, hasInlineMath, KATEX_HEAD)
45
52
  }
46
53
  case 'table': {
47
54
  const alignments = node.alignments || []
48
55
  const rows = node.rows || []
49
56
  if (rows.length === 0) return ''
50
-
51
- const headerRow = rows[0]
52
- const bodyRows = rows.slice(1)
53
-
54
57
  const renderRow = (row, isHeader) => {
55
- const cellTag = isHeader ? 'th' : 'td'
56
- const cells = row
57
- .map((cell, colIdx) => {
58
- const align = alignments[colIdx] || 'default'
59
- const alignAttr = align !== 'default' ? ` align="${align}"` : ''
60
- return `<${cellTag}${alignAttr}>${renderInline(cell, options)}</${cellTag}>`
61
- })
62
- .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('')
63
63
  return `<tr>${cells}</tr>\n`
64
64
  }
65
-
66
- let tableHtml = '<table>\n<thead>\n' + renderRow(headerRow, true) + '</thead>\n'
67
- if (bodyRows.length > 0) {
68
- tableHtml += '<tbody>\n' + bodyRows.map((r) => renderRow(r, false)).join('') + '</tbody>\n'
69
- }
70
- tableHtml += '</table>\n'
71
- return tableHtml
72
- }
73
- case 'hr': {
74
- return '<hr />\n'
75
- }
76
- case 'html': {
77
- 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
78
69
  }
70
+ case 'hr': return '<hr />\n'
71
+ case 'html': return `${node.value}\n`
79
72
  case 'mathBlock': {
80
73
  const mathRenderer = options.mathBlockRenderer || options.codeRenderers?.math
81
74
  if (mathRenderer) return mathRenderer(node, options)
82
- return `<div class="math-display" data-latex="${escapeHtml(node.value)}">$$\n${escapeHtml(node.value)}\n$$</div>\n`
83
- }
84
- default: {
85
- return ''
75
+ return { html: `<div class="math-display" data-latex="${escapeHtml(node.value)}">$$\n${escapeHtml(node.value)}\n$$</div>\n`, head: KATEX_HEAD }
86
76
  }
77
+ default: return ''
87
78
  }
88
79
  }
89
80
 
90
- 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 }
@@ -0,0 +1,46 @@
1
+ const { renderInline } = require('./render-inline')
2
+
3
+ const renderListItem = (item, options, tight, renderBlock, hasInlineMath, KATEX_HEAD) => {
4
+ const checkHtml = item.checked !== undefined ? `<input type="checkbox"${item.checked ? ' checked' : ''} disabled /> ` : ''
5
+ const taskClass = item.checked !== undefined ? ' class="task-list-item"' : ''
6
+
7
+ if (Array.isArray(item)) {
8
+ const itemMath = hasInlineMath(item)
9
+ return {
10
+ html: `<li${taskClass}>${checkHtml}${renderInline(item, options)}</li>\n`,
11
+ head: itemMath ? KATEX_HEAD : [],
12
+ }
13
+ }
14
+
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 }
27
+ }
28
+
29
+ const renderList = (node, options, renderBlock, hasInlineMath, KATEX_HEAD) => {
30
+ const tight = node.tight !== false
31
+ const isOrdered = node.type === 'orderedList'
32
+ const listItems = node.children || node.items || []
33
+ let head = []
34
+
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 }
44
+ }
45
+
46
+ module.exports = { renderList }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sullux/markdown-html",
3
- "version": "1.0.2",
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>",
@@ -31,6 +31,6 @@
31
31
  "registry": "https://registry.npmjs.org/"
32
32
  },
33
33
  "dependencies": {
34
- "@sullux/markdown-compiler": "^1.0.2"
34
+ "@sullux/markdown-compiler": "^2.0.0"
35
35
  }
36
36
  }