优化后端

This commit is contained in:
Miayui
2026-09-11 17:56:56 +08:00
parent 7eed11b774
commit 812e67422d
7299 changed files with 1744959 additions and 112 deletions
+11
View File
@@ -0,0 +1,11 @@
# Viz.js
This project builds [Graphviz](http://www.graphviz.org) with [Emscripten](https://emscripten.org) and provides a simple wrapper for using it on the web.
## Install
Viz.js is published on NPM as [`@viz-js/viz`](https://www.npmjs.com/package/@viz-js/viz).
## API
[API Reference](https://viz-js.com/api/#viz)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
export const graphvizVersion = "14.1.3";
export const formats = ["canon","cmap","cmapx","cmapx_np","dot","dot_json","eps","fig","gv","imap","imap_np","ismap","json","json0","pic","plain","plain-ext","pov","ps","ps2","svg","svg_inline","tk","xdot","xdot1.2","xdot1.4","xdot_json"];
export const engines = ["circo","dot","fdp","neato","nop","nop1","nop2","osage","patchwork","sfdp","twopi"];
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@viz-js/viz",
"description": "WebAssembly build of Graphviz with a simple wrapper for using it on the web",
"keywords": [
"graphviz",
"emscripten"
],
"version": "3.25.0",
"repository": {
"type": "git",
"url": "git+https://github.com/mdaines/viz-js.git",
"directory": "packages/viz"
},
"license": "MIT",
"type": "module",
"main": "./dist/viz.js",
"types": "./types/index.d.ts",
"exports": {
"types": "./types/index.d.ts",
"require": "./dist/viz.cjs",
"default": "./dist/viz.js"
},
"files": [
"dist",
"lib",
"src",
"types"
],
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-terser": "^0.4.4",
"jsdom": "^21.1.2",
"mocha": "^11.7.5",
"rollup": "^3.30.0",
"typedoc": "^0.28.17",
"typedoc-plugin-mdn-links": "^5.1.1"
},
"scripts": {
"test": "mocha",
"docs": "typedoc types/index.d.ts"
}
}
+39
View File
@@ -0,0 +1,39 @@
const errorPatterns = [
[/^Error: (.*)/, "error"],
[/^Warning: (.*)/, "warning"]
];
export function parseStderrMessages(messages) {
return messages.map(message => {
for (let i = 0; i < errorPatterns.length; i++) {
const [pattern, level] = errorPatterns[i];
let match;
if ((match = pattern.exec(message)) !== null) {
return { message: match[1].trimEnd(), level };
}
}
return { message: message.trimEnd() };
});
}
export function parseAgerrMessages(messages) {
const result = [];
let level = undefined;
for (let i = 0; i < messages.length; i++) {
if (messages[i] == "Error" && messages[i+1] == ": ") {
level = "error";
i += 1;
} else if (messages[i] == "Warning" && messages[i+1] == ": ") {
level = "warning";
i += 1;
} else {
result.push({ message: messages[i].trimEnd(), level });
}
}
return result;
}
+8
View File
@@ -0,0 +1,8 @@
import Module from "../lib/backend.js";
import Viz from "./viz.js";
export { graphvizVersion, formats, engines } from "../lib/metadata.js";
export function instance() {
return Module().then(m => new Viz(m));
}
+73
View File
@@ -0,0 +1,73 @@
import { getGraphvizVersion, getPluginList, renderInput } from "./wrapper.js";
class Viz {
constructor(module) {
this.module = module;
}
get graphvizVersion() {
return getGraphvizVersion(this.module);
}
get formats() {
return getPluginList(this.module, "device");
}
get engines() {
return getPluginList(this.module, "layout");
}
renderFormats(input, formats, options = {}) {
return renderInput(this.module, input, formats, { engine: "dot", ...options });
}
render(input, options = {}) {
let format;
if (options.format === void 0) {
format = "dot";
} else {
format = options.format;
}
let result = renderInput(this.module, input, [format], { engine: "dot", ...options });
if (result.status === "success") {
result.output = result.output[format];
}
return result;
}
renderString(src, options = {}) {
const result = this.render(src, options);
if (result.status !== "success") {
throw new Error(result.errors.find(e => e.level == "error")?.message || "render failed");
}
return result.output;
}
renderSVGElement(src, options = {}) {
const str = this.renderString(src, { ...options, format: "svg" });
let input;
if (typeof options.trustedTypePolicy !== "undefined") {
input = options.trustedTypePolicy.createHTML(str);
} else {
input = str;
}
const parser = new DOMParser();
return parser.parseFromString(input, "image/svg+xml").documentElement;
}
renderJSON(src, options = {}) {
const str = this.renderString(src, { ...options, format: "json" });
return JSON.parse(str);
}
}
export default Viz;
+291
View File
@@ -0,0 +1,291 @@
import { parseStderrMessages, parseAgerrMessages } from "./errors.js";
export function getGraphvizVersion(module) {
const resultPointer = module.ccall("viz_get_graphviz_version", "number", [], []);
return module.UTF8ToString(resultPointer);
}
export function getPluginList(module, kind) {
const resultPointer = module.ccall("viz_get_plugin_list", "number", ["string"], [kind]);
if (resultPointer == 0) {
throw new Error(`couldn't get plugin list: ${kind}`);
}
const list = [];
let itemPointer = resultPointer;
let stringPointer;
while (stringPointer = module.getValue(itemPointer, "*")) {
list.push(module.UTF8ToString(stringPointer));
module.ccall("free", "number", ["number"], [stringPointer]);
itemPointer += 4;
}
module.ccall("free", "number", ["number"], [resultPointer]);
return list;
}
export function renderInput(module, input, formats, options) {
let graphPointer, contextPointer, resultPointer, imageFilePaths;
try {
module["agerrMessages"] = [];
module["stderrMessages"] = [];
imageFilePaths = createImageFiles(module, options.images);
if (typeof input === "string") {
graphPointer = readStringInput(module, input, options);
} else if (typeof input === "object") {
graphPointer = readObjectInput(module, input, options);
} else {
throw new Error("input must be a string or object");
}
if (graphPointer === 0) {
return {
status: "failure",
output: undefined,
errors: parseErrorMessages(module)
};
}
setDefaultAttributes(module, graphPointer, options);
module.ccall("viz_set_y_invert", "number", ["number"], [options.yInvert ? 1 : 0]);
module.ccall("viz_set_reduce", "number", ["number"], [options.reduce ? 1 : 0]);
contextPointer = module.ccall("viz_create_context");
module.ccall("viz_reset_errors");
let layoutError = module.ccall("viz_layout", "number", ["number", "number", "string"], [contextPointer, graphPointer, options.engine]);
if (layoutError !== 0) {
return {
status: "failure",
output: undefined,
errors: parseErrorMessages(module)
};
}
let output = {};
for (let format of formats) {
resultPointer = module.ccall("viz_render", "number", ["number", "number", "string"], [contextPointer, graphPointer, format]);
if (resultPointer === 0) {
return {
status: "failure",
output: undefined,
errors: parseErrorMessages(module)
};
} else {
output[format] = module.UTF8ToString(resultPointer);
module.ccall("free", "number", ["number"], [resultPointer]);
resultPointer = 0;
}
}
return {
status: "success",
output: output,
errors: parseErrorMessages(module)
};
} catch (error) {
if (/^exit\(\d+\)/.test(error)) {
return {
status: "failure",
output: undefined,
errors: parseErrorMessages(module)
};
} else {
throw error;
}
} finally {
if (contextPointer && graphPointer) {
module.ccall("viz_free_layout", "number", ["number"], [contextPointer, graphPointer]);
}
if (graphPointer) {
module.ccall("viz_free_graph", "number", ["number"], [graphPointer]);
}
if (contextPointer) {
module.ccall("viz_free_context", "number", ["number"], [contextPointer]);
}
if (resultPointer) {
module.ccall("free", "number", ["number"], [resultPointer]);
}
if (imageFilePaths) {
removeImageFiles(module, imageFilePaths);
}
}
}
function parseErrorMessages(module) {
return parseAgerrMessages(module["agerrMessages"]).concat(parseStderrMessages(module["stderrMessages"]));
}
function createImageFiles(module, images) {
if (!images) {
return [];
}
return images.map(image => {
if (typeof image.name !== "string") {
throw new Error("image name must be a string");
} else if (typeof image.width !== "number" && typeof image.width !== "string") {
throw new Error("image width must be a number or string");
} else if (typeof image.height !== "number" && typeof image.height !== "string") {
throw new Error("image height must be a number or string");
}
const path = module.PATH.join("/", image.name);
const data = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${image.width}" height="${image.height}"></svg>
`;
module.FS.createPath("/", module.PATH.dirname(path));
module.FS.writeFile(path, data);
return path;
});
}
function removeImageFiles(module, imageFilePaths) {
for (const path of imageFilePaths) {
if (module.FS.analyzePath(path).exists) {
module.FS.unlink(path);
}
}
}
function readStringInput(module, src, options) {
let srcPointer;
try {
const srcLength = module.lengthBytesUTF8(src);
srcPointer = module.ccall("malloc", "number", ["number"], [srcLength + 1]);
module.stringToUTF8(src, srcPointer, srcLength + 1);
return module.ccall("viz_read_one_graph", "number", ["number"], [srcPointer]);
} finally {
if (srcPointer) {
module.ccall("free", "number", ["number"], [srcPointer]);
}
}
}
function readObjectInput(module, object, options) {
const graphPointer = module.ccall("viz_create_graph", "number", ["string", "number", "number"], [object.name, typeof object.directed !== "undefined" ? object.directed : true, typeof object.strict !== "undefined" ? object.strict : false]);
readGraph(module, graphPointer, object);
return graphPointer;
}
function readGraph(module, graphPointer, graphData) {
setDefaultAttributes(module, graphPointer, graphData);
if (graphData.nodes) {
graphData.nodes.forEach(nodeData => {
if (typeof nodeData.name === "undefined") {
throw new Error("nodes must have a name");
}
const nodePointer = module.ccall("viz_add_node", "number", ["number", "string"], [graphPointer, String(nodeData.name)]);
if (nodeData.attributes) {
setAttributes(module, graphPointer, nodePointer, nodeData.attributes);
}
});
}
if (graphData.edges) {
graphData.edges.forEach(edgeData => {
if (typeof edgeData.tail === "undefined") {
throw new Error("edges must have a tail");
}
if (typeof edgeData.head === "undefined") {
throw new Error("edges must have a head");
}
const edgePointer = module.ccall("viz_add_edge", "number", ["number", "string", "string"], [graphPointer, String(edgeData.tail), String(edgeData.head)]);
if (edgeData.attributes) {
setAttributes(module, graphPointer, edgePointer, edgeData.attributes);
}
});
}
if (graphData.subgraphs) {
graphData.subgraphs.forEach(subgraphData => {
const subgraphPointer = module.ccall("viz_add_subgraph", "number", ["number", "string"], [graphPointer, typeof subgraphData.name !== "undefined" ? String(subgraphData.name) : 0]);
readGraph(module, subgraphPointer, subgraphData);
});
}
}
function setDefaultAttributes(module, graphPointer, data) {
if (data.graphAttributes) {
for (const [name, value] of Object.entries(data.graphAttributes)) {
withStringPointer(module, graphPointer, value, stringPointer => {
module.ccall("viz_set_default_graph_attribute", "number", ["number", "string", "number"], [graphPointer, name, stringPointer]);
});
}
}
if (data.nodeAttributes) {
for (const [name, value] of Object.entries(data.nodeAttributes)) {
withStringPointer(module, graphPointer, value, stringPointer => {
module.ccall("viz_set_default_node_attribute", "number", ["number", "string", "number"], [graphPointer, name, stringPointer]);
});
}
}
if (data.edgeAttributes) {
for (const [name, value] of Object.entries(data.edgeAttributes)) {
withStringPointer(module, graphPointer, value, stringPointer => {
module.ccall("viz_set_default_edge_attribute", "number", ["number", "string", "number"], [graphPointer, name, stringPointer]);
});
}
}
}
function setAttributes(module, graphPointer, objectPointer, attributes) {
for (const [key, value] of Object.entries(attributes)) {
withStringPointer(module, graphPointer, value, stringPointer => {
module.ccall("viz_set_attribute", "number", ["number", "string", "number"], [objectPointer, key, stringPointer]);
});
}
}
function withStringPointer(module, graphPointer, value, callbackFn) {
let stringPointer;
if (typeof value === "object" && "html" in value) {
stringPointer = module.ccall("viz_string_dup_html", "number", ["number", "string"], [graphPointer, String(value.html)]);
} else {
stringPointer = module.ccall("viz_string_dup", "number", ["number", "string"], [graphPointer, String(value)]);
}
if (stringPointer == 0) {
throw new Error("couldn't dup string");
}
callbackFn(stringPointer);
if (typeof value === "object" && "html" in value) {
module.ccall("viz_string_free_html", "number", ["number", "number"], [graphPointer, stringPointer]);
} else {
module.ccall("viz_string_free", "number", ["number", "number"], [graphPointer, stringPointer]);
}
}
+366
View File
@@ -0,0 +1,366 @@
export {}
/**
* The version of Graphviz used for this build.
*/
export const graphvizVersion: string
/**
* The names of the {@link https://www.graphviz.org/docs/outputs/ | Graphviz output formats} supported in this build.
*/
export const formats: string[]
/**
* The names of the {@link https://www.graphviz.org/docs/layouts/ | Graphviz layout engines} supported in this build.
*/
export const engines: string[]
/**
* Returns a promise that resolves to an instance of the {@link Viz} class.
*/
export function instance(): Promise<Viz>
/**
* The {@link Viz} class isn't exported, but it can be instantiated using the {@link instance} function.
*/
declare class Viz {
/**
* The version of Graphviz at runtime.
*/
get graphvizVersion(): string
/**
* The names of the {@link https://www.graphviz.org/docs/outputs/ | Graphviz output formats} supported at runtime.
*/
get formats(): string[]
/**
* The names of the {@link https://www.graphviz.org/docs/layouts/ | Graphviz layout engines} supported at runtime.
*/
get engines(): string[]
/**
* @internal
*/
constructor()
/**
* Renders the graph described by the input and returns the result as an object.
*
* `input` may be a string in {@link https://www.graphviz.org/doc/info/lang.html | DOT syntax} or a {@link Graph | graph object}.
*
* This method does not throw an error if rendering failed, including for invalid DOT syntax, but it will throw for invalid types in input or unexpected runtime errors.
*/
render(input: string | Graph, options?: RenderOptions): RenderResult
/**
* Renders the graph described by the input for each format in `formats` and returns the result as an object. For a successful result, `output` is an object keyed by format.
*/
renderFormats(input: string | Graph, formats: string[], options?: RenderOptions): MultipleRenderResult
/**
* Renders the input and returns the result as a string. Throws an error if rendering failed.
*/
renderString(input: string | Graph, options?: RenderOptions): string
/**
* Convenience method that renders the input, parses the output, and returns an SVG element. The `format` option is ignored. Throws an error if rendering failed.
*/
renderSVGElement(input: string | Graph, options?: SVGRenderOptions): SVGSVGElement
/**
* Convenience method that renders the input, parses the output, and returns a JSON object. The `format` option is ignored. Throws an error if rendering failed.
*/
renderJSON(input: string | Graph, options?: RenderOptions): object
}
export { type Viz }
/**
* @property format
* The {@link https://www.graphviz.org/docs/outputs/ | Graphviz output format} to render. For example, `"dot"` or `"svg"`.
*
* @property engine
* The {@link https://www.graphviz.org/docs/layouts/ | Graphviz layout engine} to use for graph layout. For example, `"dot"` or `"neato"`.
*
* @property yInvert
* Invert y coordinates in output. This corresponds to the {@link https://www.graphviz.org/doc/info/command.html#-y | `-y`} Graphviz command-line option</a>.
*
* @property reduce
* Reduce the graph. This corresponds to the {@link https://www.graphviz.org/doc/info/command.html#-x | `-x`} Graphviz command-line option</a>.
*
* @property graphAttributes
* Sets the default graph attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-G | `-G`} Graphviz command-line option</a>.
*
* @property nodeAttributes
* Sets the default node attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-N `-N`} Graphviz command-line option</a>.
*
* @property edgeAttributes
* Sets the default edge attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-E | `-E`} Graphviz command-line option</a>.
*
* @property images
* Image sizes to use when rendering nodes with <code>image</code> attributes.
*
* For example, to indicate to Graphviz that the image <code>test.png</code> has size 300x200:
*
* ```js
* viz.render("graph { a[image=\"test.png\"] }", {
* images: [
* { name: "test.png", width: 300, height: 200 }
* ]
* });
* ```
*/
export interface RenderOptions {
format?: string
engine?: string
yInvert?: boolean
reduce?: boolean
graphAttributes?: Attributes
nodeAttributes?: Attributes
edgeAttributes?: Attributes
images?: ImageSize[]
}
/**
* @property engine
* The {@link https://www.graphviz.org/docs/layouts/ | Graphviz layout engine} to use for graph layout. For example, `"dot"` or `"neato"`.
*
* @property yInvert
* Invert y coordinates in output. This corresponds to the {@link https://www.graphviz.org/doc/info/command.html#-y | `-y`} Graphviz command-line option</a>.
*
* @property reduce
* Reduce the graph. This corresponds to the {@link https://www.graphviz.org/doc/info/command.html#-x | `-x`} Graphviz command-line option</a>.
*
* @property graphAttributes
* Sets the default graph attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-G | `-G`} Graphviz command-line option</a>.
*
* @property nodeAttributes
* Sets the default node attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-N `-N`} Graphviz command-line option</a>.
*
* @property edgeAttributes
* Sets the default edge attributes. This corresponds the {@link https://www.graphviz.org/doc/info/command.html#-E | `-E`} Graphviz command-line option</a>.
*
* @property images
* Image sizes to use when rendering nodes with <code>image</code> attributes.
*
* For example, to indicate to Graphviz that the image <code>test.png</code> has size 300x200:
*
* ```js
* viz.render("graph { a[image=\"test.png\"] }", {
* images: [
* { name: "test.png", width: 300, height: 200 }
* ]
* });
* ```
*
* @property trustedTypePolicy
* The {@link https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy | TrustedTypePolicy} object used to sanitize the rendered SVG string before parsing.
*/
export interface SVGRenderOptions {
engine?: string
yInvert?: boolean
reduce?: boolean
graphAttributes?: Attributes
nodeAttributes?: Attributes
edgeAttributes?: Attributes
images?: ImageSize[]
trustedTypePolicy?: object
}
/**
* The result object returned by {@link Viz.render}.
*/
export type RenderResult = SuccessResult | FailureResult
/**
* Returned by {@link Viz.render} if rendering was successful. `errors` may contain warning messages even if the graph rendered successfully.
*/
export interface SuccessResult {
status: "success"
output: string
errors: RenderError[]
}
/**
* Returned by {@link Viz.render} or {@link Viz.renderFormats} if rendering failed.
*/
export interface FailureResult {
status: "failure"
output: undefined
errors: RenderError[]
}
/**
* The result object returned by {@link Viz.renderFormats}.
*/
export type MultipleRenderResult = MultipleSuccessResult | FailureResult
/**
* Returned by {@link Viz.renderFormats} if rendering was successful. `errors` may contain warning messages even if the graph rendered successfully.
*/
export interface MultipleSuccessResult {
status: "success"
output: { [format: string]: string }
errors: RenderError[]
}
export interface RenderError {
level?: "error" | "warning"
message: string
}
/**
* In addition to strings in {@link https://www.graphviz.org/doc/info/lang.html | DOT syntax}, {@link Viz.render | rendering methods} accept <i>graph objects</i>.
*
* Graph objects are plain JavaScript objects, similar to {@link https://jsongraphformat.info | JSON Graph} or {@link https://github.com/dagrejs/graphlib/wiki/API-Reference#json-write | the Dagre JSON serialization}, but are specifically designed for working with Graphviz. Because of that, they use terminology from the Graphviz API (edges have a "head" and "tail", and nodes are identified with "name") and support features such as subgraphs, HTML labels, and default attributes.
*
* Some example graph objects and the corresponding graph in DOT:
*
* ## Empty directed graph
*
* ```json
* {}
* ```
*
* ```
* digraph { }
* ```
*
* ## Simple Undirected Graph
*
* ```json
* {
* directed: false,
* edges: [
* { tail: "a", head: "b" },
* { tail: "b", head: "c" },
* { tail: "c", head: "a" }
* ]
* }
* ```
*
* ```
* graph {
* a -- b
* b -- c
* c -- a
* }
* ```
*
* ## Attributes, Subgraphs, HTML Labels
*
* ```json
* {
* graphAttributes: {
* rankdir: "LR"
* },
* nodeAttributes: {
* shape: "circle"
* },
* nodes: [
* { name: "a", attributes: { label: { html: "&lt;i&gt;A&lt;/i&gt;" }, color: "red" } },
* { name: "b", attributes: { label: { html: "&lt;b&gt;A&lt;/b&gt;" }, color: "green" } }
* ],
* edges: [
* { tail: "a", head: "b", attributes: { label: "1" } },
* { tail: "b", head: "c", attributes: { label: "2", headport: "name" } }
* ],
* subgraphs: [
* {
* name: "cluster_1",
* nodes: [
* {
* name: "c",
* attributes: {
* label: {
* html: "&lt;table&gt;&lt;tr&gt;&lt;td&gt;test&lt;/td&gt;&lt;td port=\"name\"&gt;C&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;"
* }
* }
* }
* ]
* }
* ]
* }
* ```
*
* ```
* digraph {
* graph [rankdir="LR"]
* node [shape="circle"]
* a [label=&lt;&lt;i&gt;A&lt;/i&gt;&gt;, color="red"]
* b [label=&lt;&lt;b&gt;B&lt;/b&gt;&gt;, color="green"]
* a -> b [label="1"]
* b -> c:name [label="2"]
* subgraph cluster_1 {
* c [label=&lt;&lt;table&gt;&lt;tr&gt;&lt;td port="name"&gt;C&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&gt;]
* }
* }
* ```
*/
export interface Graph {
name?: string
strict?: boolean
directed?: boolean
graphAttributes?: Attributes
nodeAttributes?: Attributes
edgeAttributes?: Attributes
nodes?: Node[]
edges?: Edge[]
subgraphs?: Subgraph[]
}
export interface Attributes {
[name: string]: string | number | boolean | HTMLString
}
export interface HTMLString {
html: string
}
export interface Node {
name: string
attributes?: Attributes
}
export interface Edge {
tail: string
head: string
attributes?: Attributes
}
export interface Subgraph {
name?: string
graphAttributes?: Attributes
nodeAttributes?: Attributes
edgeAttributes?: Attributes
nodes?: Node[]
edges?: Edge[]
subgraphs?: Subgraph[]
}
/**
* Specifies the size of an image used as a node's `image` attribute. See {@link RenderOptions.images}.
*
* `width` and `height` may be specified as numbers or strings with units: in, px, pc, pt, cm, or mm. If no units are given or measurements are given as numbers, points (pt) are used.
*
* @property name
* The name of the image. In addition to filenames, names that look like absolute filesystem paths or URLs can be used. For example:
*
* - `"example.png"`
* - `"/images/example.png"`
* - `"http://example.com/image.png"`
*
* Names that look like relative filesystem paths, such as `"../example.png"`, are not supported.
*
* @property width
* The width of the image.
*
* @property height
* The height of the image.
*/
export interface ImageSize {
name: string,
width: string | number,
height: string | number
}