docs(readme): add doc
This commit is contained in:
205
scripts/render-markdown-pdf.mjs
Normal file
205
scripts/render-markdown-pdf.mjs
Normal file
@@ -0,0 +1,205 @@
|
||||
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const [mode, inputPath, workDir, outputPath] = process.argv.slice(2)
|
||||
|
||||
if (!['prepare', 'render'].includes(mode) || !inputPath || !workDir) {
|
||||
console.error('Usage: render-markdown-pdf.mjs <prepare|render> <input.md> <work-dir> [output.html]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const markdown = await readFile(inputPath, 'utf8')
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function inline(text) {
|
||||
const tokens = []
|
||||
let value = escapeHtml(text)
|
||||
|
||||
value = value.replace(/`([^`]+)`/g, (_, code) => {
|
||||
tokens.push(`<code>${code}</code>`)
|
||||
return `\u0000${tokens.length - 1}\u0000`
|
||||
})
|
||||
value = value.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
value = value.replace(/\[(.+?)\]\((https?:[^)]+)\)/g, '<a href="$2">$1</a>')
|
||||
|
||||
return value.replace(/\u0000(\d+)\u0000/g, (_, index) => tokens[Number(index)])
|
||||
}
|
||||
|
||||
function isTableDivider(line) {
|
||||
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line)
|
||||
}
|
||||
|
||||
function cells(line) {
|
||||
return line
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => inline(cell.trim()))
|
||||
}
|
||||
|
||||
function renderMarkdown(source, diagrams) {
|
||||
const diagramPattern = /```mermaid\n[\s\S]*?\n```/g
|
||||
let index = 0
|
||||
const prepared = source.replace(diagramPattern, () => `@@MERMAID_${index++}@@`)
|
||||
const lines = prepared.replace(/\r\n/g, '\n').split('\n')
|
||||
const output = []
|
||||
|
||||
for (let i = 0; i < lines.length;) {
|
||||
const line = lines[i]
|
||||
|
||||
if (!line.trim()) {
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const diagram = line.match(/^@@MERMAID_(\d+)@@$/)
|
||||
if (diagram) {
|
||||
const svg = diagrams[Number(diagram[1])] || ''
|
||||
output.push(`<div class="mermaid-diagram">${svg}</div>`)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const fence = line.match(/^```([^`]*)$/)
|
||||
if (fence) {
|
||||
const language = fence[1].trim()
|
||||
const code = []
|
||||
i += 1
|
||||
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
||||
code.push(lines[i])
|
||||
i += 1
|
||||
}
|
||||
if (i < lines.length) i += 1
|
||||
output.push(`<pre><code class="language-${escapeHtml(language)}">${escapeHtml(code.join('\n'))}</code></pre>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/)
|
||||
if (heading) {
|
||||
const level = heading[1].length
|
||||
output.push(`<h${level}>${inline(heading[2])}</h${level}>`)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith('> ')) {
|
||||
const quote = []
|
||||
while (i < lines.length && lines[i].startsWith('> ')) {
|
||||
quote.push(lines[i].slice(2))
|
||||
i += 1
|
||||
}
|
||||
output.push(`<blockquote>${inline(quote.join('<br>'))}</blockquote>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.includes('|') && i + 1 < lines.length && isTableDivider(lines[i + 1])) {
|
||||
const header = cells(line)
|
||||
const rows = []
|
||||
i += 2
|
||||
while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
|
||||
rows.push(cells(lines[i]))
|
||||
i += 1
|
||||
}
|
||||
output.push(`<table><thead><tr>${header.map((cell) => `<th>${cell}</th>`).join('')}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`).join('')}</tbody></table>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const unordered = line.match(/^[-*]\s+(.*)$/)
|
||||
const ordered = line.match(/^\d+\.\s+(.*)$/)
|
||||
if (unordered || ordered) {
|
||||
const tag = ordered ? 'ol' : 'ul'
|
||||
const items = []
|
||||
while (i < lines.length) {
|
||||
const match = tag === 'ol'
|
||||
? lines[i].match(/^\d+\.\s+(.*)$/)
|
||||
: lines[i].match(/^[-*]\s+(.*)$/)
|
||||
if (!match) break
|
||||
const task = match[1].match(/^\[([ xX])\]\s+(.*)$/)
|
||||
const content = task
|
||||
? `<span class="checkbox">${task[1].trim() ? '☑' : '☐'}</span> ${inline(task[2])}`
|
||||
: inline(match[1])
|
||||
items.push(`<li>${content}</li>`)
|
||||
i += 1
|
||||
}
|
||||
output.push(`<${tag}>${items.join('')}</${tag}>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraph = [line.trim()]
|
||||
i += 1
|
||||
while (
|
||||
i < lines.length &&
|
||||
lines[i].trim() &&
|
||||
!/^(#{1,6})\s+/.test(lines[i]) &&
|
||||
!/^```/.test(lines[i]) &&
|
||||
!/^@@MERMAID_\d+@@$/.test(lines[i]) &&
|
||||
!/^[-*]\s+/.test(lines[i]) &&
|
||||
!/^\d+\.\s+/.test(lines[i]) &&
|
||||
!lines[i].startsWith('> ') &&
|
||||
!(lines[i].includes('|') && i + 1 < lines.length && isTableDivider(lines[i + 1]))
|
||||
) {
|
||||
paragraph.push(lines[i].trim())
|
||||
i += 1
|
||||
}
|
||||
output.push(`<p>${inline(paragraph.join(' '))}</p>`)
|
||||
}
|
||||
|
||||
return output.join('\n')
|
||||
}
|
||||
|
||||
if (mode === 'prepare') {
|
||||
const diagrams = [...markdown.matchAll(/```mermaid\n([\s\S]*?)\n```/g)].map((match) => match[1])
|
||||
await Promise.all(diagrams.map((diagram, index) => writeFile(path.join(workDir, `diagram-${index}.mmd`), diagram)))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!outputPath) {
|
||||
console.error('An HTML output path is required for render mode.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const svgFiles = (await readdir(workDir))
|
||||
.filter((file) => /^diagram-\d+\.svg$/.test(file))
|
||||
.sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0]))
|
||||
const diagrams = await Promise.all(svgFiles.map((file) => readFile(path.join(workDir, file), 'utf8')))
|
||||
const body = renderMarkdown(markdown, diagrams)
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Markdown PDF</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 15mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { color: #1f2937; font: 10.5pt/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
||||
h1 { margin: 0 0 8px; color: #0f4c81; font-size: 24pt; }
|
||||
h2 { margin-top: 26px; border-bottom: 1px solid #dbe4ef; padding-bottom: 5px; color: #123b62; font-size: 16pt; }
|
||||
h3 { margin-top: 18px; color: #174f84; font-size: 12pt; }
|
||||
p, li, table, .mermaid-diagram { break-inside: avoid; }
|
||||
table { width: 100%; margin: 12px 0; border-collapse: collapse; font-size: 9.5pt; }
|
||||
th { background: #eaf3fb; color: #123b62; font-weight: 600; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 7px 8px; text-align: left; vertical-align: top; }
|
||||
code { border-radius: 3px; background: #f1f5f9; padding: 1px 4px; color: #a33d10; font-family: "SFMono-Regular", Consolas, monospace; }
|
||||
pre { overflow-wrap: anywhere; border: 1px solid #dbe4ef; border-radius: 4px; background: #f8fafc; padding: 10px; white-space: pre-wrap; font-size: 8.8pt; }
|
||||
pre code { background: transparent; padding: 0; color: #1f2937; }
|
||||
blockquote { margin: 10px 0; border-left: 3px solid #67a6d8; padding-left: 12px; color: #475569; }
|
||||
.mermaid-diagram { margin: 14px 0; text-align: center; }
|
||||
.mermaid-diagram svg { max-width: 100%; height: auto; }
|
||||
.checkbox { color: #0f4c81; }
|
||||
a { color: #0f4c81; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`
|
||||
|
||||
await writeFile(outputPath, html)
|
||||
Reference in New Issue
Block a user