diff --git a/docs/external/build/make-pdf.sh b/docs/external/build/make-pdf.sh new file mode 100755 index 0000000..6fe7466 --- /dev/null +++ b/docs/external/build/make-pdf.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Builds the external briefs as styled HTML and prints them to docs/external/pdf/ with headless Chrome. +set -euo pipefail +cd "$(dirname "$0")" +CHROME="${CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" +tmp=$(mktemp -d) +mkdir -p ../pdf + +build() { # <eyebrow> + local md=$1 out=$2 lang=$3 title=$4 eyebrow=$5 + PAGE_LANG="$lang" PAGE_TITLE="$title" PAGE_EYEBROW="$eyebrow" node page.mjs "$md" "$tmp/body.html" >/dev/null + { echo "<!doctype html><html lang=\"$lang\"><head><meta charset=\"utf-8\"></head><body style=\"margin:0\">" + cat "$tmp/body.html"; echo "</body></html>"; } > "$tmp/page.html" + "$CHROME" --headless=new --disable-gpu --no-pdf-header-footer --virtual-time-budget=8000 \ + --print-to-pdf="../pdf/$out" "file://$tmp/page.html" >/dev/null 2>&1 + echo "wrote pdf/$out" +} + +build ../architecture-brief.en.md architecture-brief.en.pdf en "VPP AI Platform Brief" "Architecture brief · condensed external edition" +build ../architecture-brief.zh.md architecture-brief.zh.pdf zh "虚拟电厂 AI 平台架构" "系统架构 · 对外精简版" +rm -rf "$tmp" diff --git a/docs/external/build/page.mjs b/docs/external/build/page.mjs new file mode 100644 index 0000000..bb6cdda --- /dev/null +++ b/docs/external/build/page.mjs @@ -0,0 +1,217 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' + +const src = readFileSync(process.argv[2], 'utf8') +const out = process.argv[3] + +const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') +const inline = (s) => + esc(s) + .replace(/`([^`]+)`/g, '<code>$1</code>') + .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') + .replace(/\*([^*]+)\*/g, '<em>$1</em>') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>') + .replace(/→/g, '<span class="arr">→</span>') + +const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + +const lines = src.split('\n') +let html = [] +const toc = [] +let i = 0 +let para = [] +const flushPara = () => { + if (para.length) { + html.push(`<p>${inline(para.join(' '))}</p>`) + para = [] + } +} +let titleLine = null +let subtitle = null +let meta = null + +while (i < lines.length) { + const line = lines[i] + if (line.startsWith('```')) { + flushPara() + const lang = line.slice(3).trim() + const buf = [] + i++ + while (i < lines.length && !lines[i].startsWith('```')) buf.push(lines[i++]) + i++ + if (lang === 'mermaid') html.push(`<div class="diagram"><pre class="mermaid">${esc(buf.join('\n'))}</pre></div>`) + else html.push(`<div class="codewrap"><pre class="code">${esc(buf.join('\n'))}</pre></div>`) + continue + } + if (/^#{1,3} /.test(line)) { + flushPara() + const level = line.match(/^#+/)[0].length + const text = line.replace(/^#+ /, '') + if (level === 1 && titleLine === null) { titleLine = text; i++; continue } + const id = slug(text) + if (level === 2) toc.push({ id, text }) + html.push(`<h${level} id="${id}">${inline(text)}</h${level}>`) + i++ + continue + } + if (line.startsWith('|')) { + flushPara() + const rows = [] + while (i < lines.length && lines[i].startsWith('|')) rows.push(lines[i++]) + const cells = (r) => r.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim()) + const head = cells(rows[0]) + const body = rows.slice(2).map(cells) + let t = '<div class="tablewrap"><table><thead><tr>' + head.map((h) => `<th>${inline(h)}</th>`).join('') + '</tr></thead><tbody>' + for (const r of body) t += '<tr>' + r.map((c) => `<td>${inline(c)}</td>`).join('') + '</tr>' + t += '</tbody></table></div>' + html.push(t) + continue + } + if (/^[-*] /.test(line) || /^\d+\. /.test(line)) { + flushPara() + const ordered = /^\d+\. /.test(line) + const items = [] + while (i < lines.length && (/^[-*] /.test(lines[i]) || /^\d+\. /.test(lines[i]))) { + let item = lines[i].replace(/^([-*]|\d+\.) /, '') + i++ + while (i < lines.length && /^\s{2,}\S/.test(lines[i])) item += ' ' + lines[i++].trim() + items.push(item) + } + const tag = ordered ? 'ol' : 'ul' + html.push(`<${tag}>` + items.map((it) => `<li>${inline(it)}</li>`).join('') + `</${tag}>`) + continue + } + if (/^!\[[^\]]*\]\([^)]+\)$/.test(line.trim())) { + flushPara() + const m = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/) + const data = readFileSync(join(dirname(process.argv[2]), m[2])).toString('base64') + html.push(`<figure><img src="data:image/png;base64,${data}" alt="${esc(m[1])}"></figure>`) + i++ + continue + } + if (line.trim() === '---') { flushPara(); html.push('<hr>'); i++; continue } + if (line.trim() === '') { flushPara(); i++; continue } + if (titleLine !== null && subtitle === null && line.startsWith('**')) { subtitle = line.replace(/\*\*/g, ''); i++; continue } + if (subtitle !== null && meta === null && line.startsWith('*')) { meta = line.replace(/^\*|\*$/g, ''); i++; continue } + para.push(line.trim()) + i++ +} +flushPara() +// drop the first hr (after meta) +const firstHr = html.indexOf('<hr>') +if (firstHr === 0) html.splice(0, 1) + +const metaParts = (meta || '').split('·').map((s) => s.trim()) + +const page = `${process.env.PAGE_LANG ? `<meta name="lang" content="${process.env.PAGE_LANG}">` : ''}<title>${process.env.PAGE_TITLE || 'VPP AI Platform Architecture'} + + + +
+ +
+
+

${process.env.PAGE_EYEBROW || 'System architecture · external edition'}

+

${inline(titleLine.trim())}

+

${inline(subtitle || '')}

+ ${meta ? `
${metaParts.map((m) => `${inline(m)}`).join('')}
` : ''} +
+${html.join('\n')} +
+
+` +writeFileSync(out, page) +console.log('wrote', out, page.length, 'bytes; toc', toc.length) diff --git a/docs/external/pdf/architecture-brief.en.pdf b/docs/external/pdf/architecture-brief.en.pdf new file mode 100644 index 0000000..f620ee4 Binary files /dev/null and b/docs/external/pdf/architecture-brief.en.pdf differ diff --git a/docs/external/pdf/architecture-brief.zh.pdf b/docs/external/pdf/architecture-brief.zh.pdf new file mode 100644 index 0000000..ac45ce3 Binary files /dev/null and b/docs/external/pdf/architecture-brief.zh.pdf differ