Files

93 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Documentation-only tool. Install pinned packages in a separate temporary directory;
// UML_RENDER_TOOLS points there. No app dependencies or remote rendering service.
import { createRequire } from 'node:module';
import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const tools = process.env.UML_RENDER_TOOLS;
// 工具依赖放在独立目录,避免给应用运行依赖增加文档渲染包。
if (!tools) throw new Error('Set UML_RENDER_TOOLS to the documentation tool directory; see docs/architecture/uml/README.md');
const require = createRequire(path.join(path.resolve(tools), 'package.json'));
const { createCanvas } = require('@napi-rs/canvas');
const { DOMImplementation, XMLSerializer, DOMParser } = require('@xmldom/xmldom');
const sharp = require('sharp');
// PlantUML's official JS engine uses DOM construction and Canvas text metrics.
// Supply a real XML DOM and native Canvas metrics, not approximate text widths.
globalThis.PLANTUML_VIZ = require('@viz-js/viz');
globalThis.window = globalThis;
globalThis.XMLSerializer = XMLSerializer;
globalThis.DOMParser = DOMParser;
const document = new DOMImplementation().createDocument(null, 'root', null);
const element = document.createElement.bind(document);
const elementNS = document.createElementNS.bind(document);
function measurementCanvas() {
// PlantUML 的文本布局需要真实字体宽高,统一用本机 Canvas 测量而非固定字符宽度。
const canvas = createCanvas(1, 1);
const context = canvas.getContext('2d');
const measure = context.measureText.bind(context);
context.measureText = text => {
const metrics = measure(text);
// PlantUML uses this height for line spacing. Font-wide ascent/descent
// keeps adjacent class fields from colliding when glyphs lack descenders.
return {
...metrics,
width: metrics.width,
actualBoundingBoxAscent: metrics.fontBoundingBoxAscent || metrics.actualBoundingBoxAscent,
actualBoundingBoxDescent: metrics.fontBoundingBoxDescent || metrics.actualBoundingBoxDescent,
};
};
return canvas;
}
document.createElement = tag => tag === 'canvas' ? measurementCanvas() : element(tag);
document.createElementNS = (namespace, tag) => {
// 为无浏览器 DOM 补齐样式与 getBBox,满足官方渲染引擎的 SVG 文本布局需求。
const node = elementNS(namespace, tag);
node.style = {};
node.getBBox = () => {
const context = measurementCanvas().getContext('2d');
context.font = `${node.getAttribute('font-size') || 14}px ${node.getAttribute('font-family') || 'Arial'}`;
const metrics = context.measureText(node.textContent || '');
return {
width: metrics.width,
height: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
x: 0,
y: -metrics.actualBoundingBoxAscent,
};
};
return node;
};
document.body = element('body');
document.documentElement.appendChild(document.body);
globalThis.document = document;
const { renderToString } = await import(pathToFileURL(require.resolve('@plantuml/core')));
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const source = path.join(root, 'docs/architecture/uml');
const output = path.join(source, 'rendered');
await mkdir(output, { recursive: true });
for (const filename of (await readdir(source)).filter(name => name.endsWith('.puml')).sort()) {
// 每张图独立渲染并检查错误文本;错误图不能被当作正常产物覆盖已有预览。
const text = await readFile(path.join(source, filename), 'utf8');
const rendered = await new Promise((resolve, reject) => renderToString(text.split('\n'), resolve, reject));
const xml = new DOMParser().parseFromString(rendered, 'image/svg+xml');
const labels = Array.from(xml.getElementsByTagName('text'), node => node.textContent).join('\n');
if (xml.documentElement.tagName !== 'svg' || /syntax error|error line|assumed diagram type|an error has occurr/i.test(labels)) {
throw new Error(`${filename}: renderer returned an error diagram\n${labels}`);
}
// Explicit opaque paper keeps arrows/labels legible in dark Markdown viewers.
const paper = xml.createElementNS('http://www.w3.org/2000/svg', 'rect');
for (const [key, value] of Object.entries({ x: '0', y: '0', width: '100%', height: '100%', fill: '#FFFFFF' })) paper.setAttribute(key, value);
xml.documentElement.insertBefore(paper, xml.documentElement.firstChild);
const svg = new XMLSerializer().serializeToString(xml);
// 同时保存可缩放 SVG 和通用 PNG;写完读取 PNG 元数据确认生成尺寸。
const stem = filename.slice(0, -5);
await writeFile(path.join(output, `${stem}.svg`), svg);
// High-resolution PNG is the universally visible Markdown preview; SVG keeps zoomable text.
const png = sharp(Buffer.from(svg), { density: 192 });
await png.png().toFile(path.join(output, `${stem}.png`));
const metadata = await sharp(path.join(output, `${stem}.png`)).metadata();
console.log(`${filename} -> SVG + PNG (${metadata.width}×${metadata.height})`);
}