libpetri 0.4.0 → 0.4.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.
- package/dist/{chunk-ZE6RR3R4.js → chunk-MJPQNC5D.js} +1 -1
- package/dist/debug/index.js +1 -1
- package/dist/doclet/index.d.ts +108 -0
- package/dist/doclet/index.js +215 -0
- package/dist/doclet/index.js.map +1 -0
- package/dist/doclet/resources/petrinet-diagrams.css +126 -0
- package/dist/doclet/resources/petrinet-diagrams.js +111 -0
- package/dist/dot-exporter-U6BRCQNK.js +8 -0
- package/dist/dot-exporter-U6BRCQNK.js.map +1 -0
- package/dist/export/index.js +1 -1
- package/package.json +25 -3
- /package/dist/{chunk-ZE6RR3R4.js.map → chunk-MJPQNC5D.js.map} +0 -0
package/dist/debug/index.js
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Application } from 'typedoc';
|
|
2
|
+
import { a as PetriNet } from '../petri-net-C3Jy5HCt.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TypeDoc plugin for `@petrinet` tag — auto-generates interactive SVG diagrams
|
|
6
|
+
* from PetriNet definitions and embeds them in TypeDoc output.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors: `org.libpetri.doclet.PetriNetTaglet`
|
|
9
|
+
*
|
|
10
|
+
* Plugin lifecycle (TypeDoc hooks):
|
|
11
|
+
* 1. `bootstrapEnd` → register `@petrinet` as known block tag
|
|
12
|
+
* 2. `preRenderAsyncJobs` → walk reflections, resolve nets, generate SVGs, cache HTML
|
|
13
|
+
* 3. `comment.beforeTags` → inject cached HTML via JSX.Raw, skip default rendering
|
|
14
|
+
*
|
|
15
|
+
* @module doclet/petri-net-plugin
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads the petri-net plugin into the TypeDoc application.
|
|
20
|
+
*
|
|
21
|
+
* @param app - the TypeDoc Application instance
|
|
22
|
+
*/
|
|
23
|
+
declare function load(app: Application): void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared HTML renderer for Petri Net diagrams in TypeDoc.
|
|
27
|
+
*
|
|
28
|
+
* Generates consistent HTML markup with interactive controls for zoom, pan,
|
|
29
|
+
* and fullscreen functionality. Accepts pre-rendered SVG from `@viz-js/viz`.
|
|
30
|
+
*
|
|
31
|
+
* CSS and JS are loaded from bundled resources and inlined into the generated
|
|
32
|
+
* HTML, making the plugin fully self-contained with no external file dependencies.
|
|
33
|
+
*
|
|
34
|
+
* Mirrors: `org.libpetri.doclet.DiagramRenderer`
|
|
35
|
+
*
|
|
36
|
+
* @module doclet/diagram-renderer
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Escapes HTML special characters for safe embedding.
|
|
40
|
+
*/
|
|
41
|
+
declare function escapeHtml(text: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Renders a pre-built SVG diagram with an optional title.
|
|
44
|
+
*
|
|
45
|
+
* Inlines CSS and JS from bundled resources. The JS is guarded by an
|
|
46
|
+
* idempotency check so it only executes once per page, even when multiple
|
|
47
|
+
* `@petrinet` tags appear.
|
|
48
|
+
*
|
|
49
|
+
* @param title - optional title (null/undefined for no title)
|
|
50
|
+
* @param svgContent - the SVG markup
|
|
51
|
+
* @param dotSource - the DOT source code for display in a collapsible block
|
|
52
|
+
* @returns HTML markup with diagram controls
|
|
53
|
+
*/
|
|
54
|
+
declare function renderSvg(title: string | null | undefined, svgContent: string, dotSource: string): string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* SVG renderer using `@viz-js/viz` (Graphviz WASM).
|
|
58
|
+
*
|
|
59
|
+
* Pure WASM — zero external dependencies. No system `dot` binary required.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors: Java's `PetriNetTaglet.dotToSvg()` (which uses `dot -Tsvg` subprocess)
|
|
62
|
+
*
|
|
63
|
+
* @module doclet/svg-renderer
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Renders a DOT string to SVG using the `@viz-js/viz` WASM engine.
|
|
67
|
+
*
|
|
68
|
+
* Strips the XML prolog/DOCTYPE and explicit width/height attributes so the SVG
|
|
69
|
+
* scales via viewBox + CSS instead of overriding with fixed pt sizes.
|
|
70
|
+
*
|
|
71
|
+
* @param dot - the DOT source string
|
|
72
|
+
* @returns SVG markup string
|
|
73
|
+
* @throws if `@viz-js/viz` is not installed or DOT parsing fails
|
|
74
|
+
*/
|
|
75
|
+
declare function dotToSvg(dot: string): Promise<string>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dynamic import-based PetriNet resolver for TypeDoc.
|
|
79
|
+
*
|
|
80
|
+
* TypeScript cannot use reflection like Java — modules must be importable
|
|
81
|
+
* at doc time (from `dist/` after `npm run build`).
|
|
82
|
+
*
|
|
83
|
+
* Tag format:
|
|
84
|
+
* ```
|
|
85
|
+
* @petrinet ./path/to/module#exportName — access a PetriNet constant
|
|
86
|
+
* @petrinet ./path/to/module#functionName() — call a function returning PetriNet
|
|
87
|
+
* @petrinet #localExport — resolve from same file
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* Mirrors: `org.libpetri.doclet.PetriNetTaglet.resolvePetriNet()`
|
|
91
|
+
*
|
|
92
|
+
* @module doclet/net-resolver
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
interface ResolvedNet {
|
|
96
|
+
readonly net: PetriNet;
|
|
97
|
+
readonly title: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parses a `@petrinet` reference and resolves the PetriNet via dynamic import.
|
|
101
|
+
*
|
|
102
|
+
* @param reference - the tag content, e.g. `./definition#buildDebugNet()`
|
|
103
|
+
* @param sourceFilePath - absolute path to the source file containing the tag
|
|
104
|
+
* @returns the resolved PetriNet with title, or null on failure
|
|
105
|
+
*/
|
|
106
|
+
declare function resolveNet(reference: string, sourceFilePath: string): Promise<ResolvedNet | null>;
|
|
107
|
+
|
|
108
|
+
export { type ResolvedNet, dotToSvg, escapeHtml, load, renderSvg, resolveNet };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/doclet/petri-net-plugin.ts
|
|
2
|
+
import {
|
|
3
|
+
JSX
|
|
4
|
+
} from "typedoc";
|
|
5
|
+
|
|
6
|
+
// src/doclet/net-resolver.ts
|
|
7
|
+
import { resolve, dirname } from "path";
|
|
8
|
+
import { pathToFileURL } from "url";
|
|
9
|
+
async function resolveNet(reference, sourceFilePath) {
|
|
10
|
+
const trimmed = reference.trim().split(/\s+/)[0] ?? "";
|
|
11
|
+
if (!trimmed) return null;
|
|
12
|
+
const hashIndex = trimmed.indexOf("#");
|
|
13
|
+
if (hashIndex === -1) return null;
|
|
14
|
+
const modulePath = trimmed.substring(0, hashIndex);
|
|
15
|
+
const exportRef = trimmed.substring(hashIndex + 1);
|
|
16
|
+
if (!exportRef) return null;
|
|
17
|
+
const isCall = exportRef.endsWith("()");
|
|
18
|
+
const exportName = isCall ? exportRef.slice(0, -2) : exportRef;
|
|
19
|
+
let absolutePath;
|
|
20
|
+
if (modulePath) {
|
|
21
|
+
absolutePath = resolve(dirname(sourceFilePath), modulePath);
|
|
22
|
+
} else {
|
|
23
|
+
absolutePath = sourceFilePath;
|
|
24
|
+
}
|
|
25
|
+
const candidates = [
|
|
26
|
+
absolutePath,
|
|
27
|
+
absolutePath + ".js",
|
|
28
|
+
absolutePath + ".ts",
|
|
29
|
+
absolutePath.replace(/\.ts$/, ".js")
|
|
30
|
+
];
|
|
31
|
+
let mod;
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
mod = await import(pathToFileURL(candidate).href);
|
|
35
|
+
break;
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!mod) return null;
|
|
40
|
+
const exported = mod[exportName];
|
|
41
|
+
if (exported == null) return null;
|
|
42
|
+
let net;
|
|
43
|
+
if (isCall) {
|
|
44
|
+
if (typeof exported !== "function") return null;
|
|
45
|
+
const result = exported();
|
|
46
|
+
net = "net" in result ? result.net : result;
|
|
47
|
+
} else {
|
|
48
|
+
net = exported;
|
|
49
|
+
}
|
|
50
|
+
if (typeof net?.name !== "string" || !(net?.transitions instanceof Set)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return { net, title: net.name };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/doclet/svg-renderer.ts
|
|
57
|
+
async function dotToSvg(dot) {
|
|
58
|
+
const { instance } = await import("@viz-js/viz");
|
|
59
|
+
const viz = await instance();
|
|
60
|
+
let svg = viz.renderString(dot, { format: "svg", engine: "dot" });
|
|
61
|
+
const svgStart = svg.indexOf("<svg");
|
|
62
|
+
if (svgStart > 0) svg = svg.substring(svgStart);
|
|
63
|
+
svg = svg.replace(/\s+(?:width|height)="[^"]*"/g, "");
|
|
64
|
+
return svg;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/doclet/diagram-renderer.ts
|
|
68
|
+
import { readFileSync } from "fs";
|
|
69
|
+
import { fileURLToPath } from "url";
|
|
70
|
+
import { dirname as dirname2, join } from "path";
|
|
71
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
72
|
+
function loadResource(filename) {
|
|
73
|
+
return readFileSync(join(__dirname, "resources", filename), "utf-8");
|
|
74
|
+
}
|
|
75
|
+
var inlineCss;
|
|
76
|
+
var inlineJs;
|
|
77
|
+
function css() {
|
|
78
|
+
return inlineCss ??= loadResource("petrinet-diagrams.css");
|
|
79
|
+
}
|
|
80
|
+
function js() {
|
|
81
|
+
return inlineJs ??= loadResource("petrinet-diagrams.js");
|
|
82
|
+
}
|
|
83
|
+
function escapeHtml(text) {
|
|
84
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
85
|
+
}
|
|
86
|
+
function renderSvg(title, svgContent, dotSource) {
|
|
87
|
+
const titleHtml = title ? `<h4>${escapeHtml(title)}</h4>
|
|
88
|
+
` : "";
|
|
89
|
+
const summaryText = title ? "View DOT Source" : "View Source";
|
|
90
|
+
return `<style>${css()}</style>
|
|
91
|
+
<script>if(!window._petriNetDiagramsInit){window._petriNetDiagramsInit=true;
|
|
92
|
+
${js()}
|
|
93
|
+
}</script>
|
|
94
|
+
<div class="petrinet-diagram">
|
|
95
|
+
${titleHtml}<div class="diagram-container">
|
|
96
|
+
<div class="diagram-controls">
|
|
97
|
+
<button class="diagram-btn btn-reset" title="Reset zoom">Reset</button>
|
|
98
|
+
<button class="diagram-btn btn-fullscreen" onclick="PetriNetDiagrams.toggleFullscreen(this)">Fullscreen</button>
|
|
99
|
+
</div>
|
|
100
|
+
${svgContent}
|
|
101
|
+
</div>
|
|
102
|
+
<details>
|
|
103
|
+
<summary>${summaryText}</summary>
|
|
104
|
+
<pre><code>${escapeHtml(dotSource)}</code></pre>
|
|
105
|
+
</details>
|
|
106
|
+
</div>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/doclet/petri-net-plugin.ts
|
|
110
|
+
async function getDotExport() {
|
|
111
|
+
const mod = await import("../dot-exporter-U6BRCQNK.js");
|
|
112
|
+
return mod.dotExport;
|
|
113
|
+
}
|
|
114
|
+
var TAG_NAME = "@petrinet";
|
|
115
|
+
var htmlCache = /* @__PURE__ */ new Map();
|
|
116
|
+
function load(app) {
|
|
117
|
+
app.on("bootstrapEnd", () => {
|
|
118
|
+
const blockTags = app.options.getValue("blockTags");
|
|
119
|
+
if (!blockTags.includes(TAG_NAME)) {
|
|
120
|
+
app.options.setValue("blockTags", [...blockTags, TAG_NAME]);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
app.renderer.preRenderAsyncJobs.push(async (output) => {
|
|
124
|
+
htmlCache.clear();
|
|
125
|
+
await processProject(app, output.project);
|
|
126
|
+
});
|
|
127
|
+
app.renderer.hooks.on("comment.beforeTags", (_context, comment, reflection) => {
|
|
128
|
+
const cached = htmlCache.get(reflection.id);
|
|
129
|
+
if (!cached) return JSX.createElement(JSX.Raw, { html: "" });
|
|
130
|
+
for (const tag of comment.blockTags) {
|
|
131
|
+
if (tag.tag === TAG_NAME) {
|
|
132
|
+
tag.skipRendering = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return JSX.createElement(JSX.Raw, { html: cached });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async function processProject(app, project) {
|
|
139
|
+
const reflections = Object.values(project.reflections);
|
|
140
|
+
for (const reflection of reflections) {
|
|
141
|
+
const comment = reflection.comment;
|
|
142
|
+
if (!comment) continue;
|
|
143
|
+
const petrinetTags = comment.blockTags.filter(
|
|
144
|
+
(tag) => tag.tag === TAG_NAME
|
|
145
|
+
);
|
|
146
|
+
if (petrinetTags.length === 0) continue;
|
|
147
|
+
for (const tag of petrinetTags) {
|
|
148
|
+
const reference = tagContent(tag);
|
|
149
|
+
try {
|
|
150
|
+
const html = await generateDiagram(reference, reflection, app);
|
|
151
|
+
const existing = htmlCache.get(reflection.id) ?? "";
|
|
152
|
+
htmlCache.set(reflection.id, existing + html);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
155
|
+
app.logger.warn(`@petrinet error for ${reflection.getFriendlyFullName()}: ${msg}`);
|
|
156
|
+
const html = errorHtml(`Error generating diagram for '${reference}': ${msg}`);
|
|
157
|
+
const existing = htmlCache.get(reflection.id) ?? "";
|
|
158
|
+
htmlCache.set(reflection.id, existing + html);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function generateDiagram(reference, reflection, app) {
|
|
164
|
+
const trimmed = reference.trim();
|
|
165
|
+
if (!trimmed) {
|
|
166
|
+
return errorHtml(`Empty @petrinet reference on ${reflection.getFriendlyFullName()}`);
|
|
167
|
+
}
|
|
168
|
+
const sourceFile = getSourceFilePath(reflection);
|
|
169
|
+
if (!sourceFile) {
|
|
170
|
+
return errorHtml(`Cannot determine source file for ${reflection.getFriendlyFullName()}`);
|
|
171
|
+
}
|
|
172
|
+
const resolved = await resolveNet(trimmed, sourceFile);
|
|
173
|
+
if (!resolved) {
|
|
174
|
+
return errorHtml(`Cannot resolve PetriNet: ${trimmed}`);
|
|
175
|
+
}
|
|
176
|
+
const dotExport = await getDotExport();
|
|
177
|
+
const dot = dotExport(resolved.net);
|
|
178
|
+
try {
|
|
179
|
+
const svg = await dotToSvg(dot);
|
|
180
|
+
return renderSvg(resolved.title, svg, dot);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
app.logger.warn(`SVG rendering failed, falling back to DOT source: ${e}`);
|
|
183
|
+
return renderSvg(
|
|
184
|
+
resolved.title,
|
|
185
|
+
`<pre><code>${escapeHtml(dot)}</code></pre>`,
|
|
186
|
+
dot
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function tagContent(tag) {
|
|
191
|
+
return tag.content.map((part) => part.text).join("").trim();
|
|
192
|
+
}
|
|
193
|
+
function getSourceFilePath(reflection) {
|
|
194
|
+
const decl = reflection;
|
|
195
|
+
if (decl.sources && decl.sources.length > 0) {
|
|
196
|
+
return decl.sources[0].fullFileName;
|
|
197
|
+
}
|
|
198
|
+
if (reflection.parent) {
|
|
199
|
+
return getSourceFilePath(reflection.parent);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function errorHtml(message) {
|
|
204
|
+
return `<div class="petrinet-error" style="color: #dc3545; border: 1px solid #dc3545; padding: 10px; border-radius: 4px;">
|
|
205
|
+
<strong>@petrinet Error:</strong> ${escapeHtml(message)}
|
|
206
|
+
</div>`;
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
dotToSvg,
|
|
210
|
+
escapeHtml,
|
|
211
|
+
load,
|
|
212
|
+
renderSvg,
|
|
213
|
+
resolveNet
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/doclet/petri-net-plugin.ts","../../src/doclet/net-resolver.ts","../../src/doclet/svg-renderer.ts","../../src/doclet/diagram-renderer.ts"],"sourcesContent":["/**\n * TypeDoc plugin for `@petrinet` tag — auto-generates interactive SVG diagrams\n * from PetriNet definitions and embeds them in TypeDoc output.\n *\n * Mirrors: `org.libpetri.doclet.PetriNetTaglet`\n *\n * Plugin lifecycle (TypeDoc hooks):\n * 1. `bootstrapEnd` → register `@petrinet` as known block tag\n * 2. `preRenderAsyncJobs` → walk reflections, resolve nets, generate SVGs, cache HTML\n * 3. `comment.beforeTags` → inject cached HTML via JSX.Raw, skip default rendering\n *\n * @module doclet/petri-net-plugin\n */\n\nimport {\n type Application,\n type CommentTag,\n type DeclarationReflection,\n type ProjectReflection,\n type Reflection,\n JSX,\n} from 'typedoc';\nimport { resolveNet } from './net-resolver.js';\nimport { dotToSvg } from './svg-renderer.js';\nimport { renderSvg, escapeHtml } from './diagram-renderer.js';\n\n// Lazy import to avoid requiring libpetri/export at module level\nasync function getDotExport(): Promise<typeof import('../export/dot-exporter.js').dotExport> {\n const mod = await import('../export/dot-exporter.js');\n return mod.dotExport;\n}\n\n/** Tag name including @ prefix. */\nconst TAG_NAME = '@petrinet' as `@${string}`;\n\n/** Cache: reflection ID → rendered HTML string */\nconst htmlCache = new Map<number, string>();\n\n/**\n * Loads the petri-net plugin into the TypeDoc application.\n *\n * @param app - the TypeDoc Application instance\n */\nexport function load(app: Application): void {\n // 1. Register @petrinet as a known block tag at bootstrap time\n app.on('bootstrapEnd', () => {\n const blockTags = app.options.getValue('blockTags') as string[];\n if (!blockTags.includes(TAG_NAME)) {\n app.options.setValue('blockTags', [...blockTags, TAG_NAME]);\n }\n });\n\n // 2. Resolve nets and generate SVG during pre-render async phase\n app.renderer.preRenderAsyncJobs.push(async (output) => {\n htmlCache.clear();\n await processProject(app, output.project);\n });\n\n // 3. Inject cached HTML into comment output, suppress default tag rendering\n app.renderer.hooks.on('comment.beforeTags', (_context, comment, reflection) => {\n const cached = htmlCache.get(reflection.id);\n if (!cached) return JSX.createElement(JSX.Raw, { html: '' });\n\n // Mark @petrinet tags as skip so TypeDoc doesn't render them as plain text\n for (const tag of comment.blockTags) {\n if (tag.tag === TAG_NAME) {\n tag.skipRendering = true;\n }\n }\n\n // Inject raw HTML using TypeDoc's JSX\n return JSX.createElement(JSX.Raw, { html: cached });\n });\n}\n\n/**\n * Walks all reflections in the project and processes `@petrinet` tags.\n */\nasync function processProject(app: Application, project: ProjectReflection): Promise<void> {\n const reflections = Object.values(project.reflections);\n\n for (const reflection of reflections) {\n const comment = reflection.comment;\n if (!comment) continue;\n\n const petrinetTags = comment.blockTags.filter(\n (tag: CommentTag) => tag.tag === TAG_NAME,\n );\n if (petrinetTags.length === 0) continue;\n\n for (const tag of petrinetTags) {\n const reference = tagContent(tag);\n try {\n const html = await generateDiagram(reference, reflection, app);\n const existing = htmlCache.get(reflection.id) ?? '';\n htmlCache.set(reflection.id, existing + html);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n app.logger.warn(`@petrinet error for ${reflection.getFriendlyFullName()}: ${msg}`);\n const html = errorHtml(`Error generating diagram for '${reference}': ${msg}`);\n const existing = htmlCache.get(reflection.id) ?? '';\n htmlCache.set(reflection.id, existing + html);\n }\n }\n }\n}\n\n/**\n * Generates a diagram HTML string for a single `@petrinet` reference.\n */\nasync function generateDiagram(\n reference: string,\n reflection: Reflection,\n app: Application,\n): Promise<string> {\n const trimmed = reference.trim();\n if (!trimmed) {\n return errorHtml(`Empty @petrinet reference on ${reflection.getFriendlyFullName()}`);\n }\n\n const sourceFile = getSourceFilePath(reflection);\n if (!sourceFile) {\n return errorHtml(`Cannot determine source file for ${reflection.getFriendlyFullName()}`);\n }\n\n const resolved = await resolveNet(trimmed, sourceFile);\n if (!resolved) {\n return errorHtml(`Cannot resolve PetriNet: ${trimmed}`);\n }\n\n const dotExport = await getDotExport();\n const dot = dotExport(resolved.net);\n\n try {\n const svg = await dotToSvg(dot);\n return renderSvg(resolved.title, svg, dot);\n } catch (e) {\n app.logger.warn(`SVG rendering failed, falling back to DOT source: ${e}`);\n return renderSvg(\n resolved.title,\n `<pre><code>${escapeHtml(dot)}</code></pre>`,\n dot,\n );\n }\n}\n\n/**\n * Extracts text content from a CommentTag.\n */\nfunction tagContent(tag: CommentTag): string {\n return tag.content\n .map((part) => part.text)\n .join('')\n .trim();\n}\n\n/**\n * Gets the source file path from a reflection.\n */\nfunction getSourceFilePath(reflection: Reflection): string | null {\n const decl = reflection as DeclarationReflection;\n if (decl.sources && decl.sources.length > 0) {\n return decl.sources[0]!.fullFileName;\n }\n if (reflection.parent) {\n return getSourceFilePath(reflection.parent);\n }\n return null;\n}\n\n/**\n * Renders an error message as styled HTML.\n */\nfunction errorHtml(message: string): string {\n return `<div class=\"petrinet-error\" style=\"color: #dc3545; border: 1px solid #dc3545; padding: 10px; border-radius: 4px;\">\n<strong>@petrinet Error:</strong> ${escapeHtml(message)}\n</div>`;\n}\n","/**\n * Dynamic import-based PetriNet resolver for TypeDoc.\n *\n * TypeScript cannot use reflection like Java — modules must be importable\n * at doc time (from `dist/` after `npm run build`).\n *\n * Tag format:\n * ```\n * @petrinet ./path/to/module#exportName — access a PetriNet constant\n * @petrinet ./path/to/module#functionName() — call a function returning PetriNet\n * @petrinet #localExport — resolve from same file\n * ```\n *\n * Mirrors: `org.libpetri.doclet.PetriNetTaglet.resolvePetriNet()`\n *\n * @module doclet/net-resolver\n */\n\nimport { resolve, dirname } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { PetriNet } from '../core/petri-net.js';\n\nexport interface ResolvedNet {\n readonly net: PetriNet;\n readonly title: string;\n}\n\n/**\n * Parses a `@petrinet` reference and resolves the PetriNet via dynamic import.\n *\n * @param reference - the tag content, e.g. `./definition#buildDebugNet()`\n * @param sourceFilePath - absolute path to the source file containing the tag\n * @returns the resolved PetriNet with title, or null on failure\n */\nexport async function resolveNet(\n reference: string,\n sourceFilePath: string,\n): Promise<ResolvedNet | null> {\n const trimmed = reference.trim().split(/\\s+/)[0] ?? '';\n if (!trimmed) return null;\n\n const hashIndex = trimmed.indexOf('#');\n if (hashIndex === -1) return null;\n\n const modulePath = trimmed.substring(0, hashIndex);\n const exportRef = trimmed.substring(hashIndex + 1);\n if (!exportRef) return null;\n\n const isCall = exportRef.endsWith('()');\n const exportName = isCall ? exportRef.slice(0, -2) : exportRef;\n\n // Resolve module path relative to the source file\n let absolutePath: string;\n if (modulePath) {\n absolutePath = resolve(dirname(sourceFilePath), modulePath);\n } else {\n // #localExport — resolve from same file's compiled output\n absolutePath = sourceFilePath;\n }\n\n // Try .js extension for compiled output, then .ts for ts-node scenarios\n const candidates = [\n absolutePath,\n absolutePath + '.js',\n absolutePath + '.ts',\n absolutePath.replace(/\\.ts$/, '.js'),\n ];\n\n let mod: Record<string, unknown> | undefined;\n for (const candidate of candidates) {\n try {\n mod = await import(pathToFileURL(candidate).href) as Record<string, unknown>;\n break;\n } catch {\n // Try next candidate\n }\n }\n\n if (!mod) return null;\n\n const exported = mod[exportName];\n if (exported == null) return null;\n\n let net: PetriNet;\n if (isCall) {\n if (typeof exported !== 'function') return null;\n const result = exported() as PetriNet | { net: PetriNet };\n // Support functions that return { net: PetriNet, ... }\n net = 'net' in result ? result.net : result;\n } else {\n net = exported as PetriNet;\n }\n\n // Validate it looks like a PetriNet (has name and transitions)\n if (typeof net?.name !== 'string' || !(net?.transitions instanceof Set)) {\n return null;\n }\n\n return { net, title: net.name };\n}\n","/**\n * SVG renderer using `@viz-js/viz` (Graphviz WASM).\n *\n * Pure WASM — zero external dependencies. No system `dot` binary required.\n *\n * Mirrors: Java's `PetriNetTaglet.dotToSvg()` (which uses `dot -Tsvg` subprocess)\n *\n * @module doclet/svg-renderer\n */\n\n/**\n * Renders a DOT string to SVG using the `@viz-js/viz` WASM engine.\n *\n * Strips the XML prolog/DOCTYPE and explicit width/height attributes so the SVG\n * scales via viewBox + CSS instead of overriding with fixed pt sizes.\n *\n * @param dot - the DOT source string\n * @returns SVG markup string\n * @throws if `@viz-js/viz` is not installed or DOT parsing fails\n */\nexport async function dotToSvg(dot: string): Promise<string> {\n // Dynamic import — @viz-js/viz is an optional peer dependency\n const { instance } = await import('@viz-js/viz');\n const viz = await instance();\n let svg = viz.renderString(dot, { format: 'svg', engine: 'dot' });\n\n // Strip XML prolog and DOCTYPE — invalid inside HTML5\n const svgStart = svg.indexOf('<svg');\n if (svgStart > 0) svg = svg.substring(svgStart);\n\n // Strip explicit width/height attributes (e.g. \"1942pt\") so the SVG\n // scales via viewBox + CSS instead of overriding with fixed pt sizes\n svg = svg.replace(/\\s+(?:width|height)=\"[^\"]*\"/g, '');\n\n return svg;\n}\n","/**\n * Shared HTML renderer for Petri Net diagrams in TypeDoc.\n *\n * Generates consistent HTML markup with interactive controls for zoom, pan,\n * and fullscreen functionality. Accepts pre-rendered SVG from `@viz-js/viz`.\n *\n * CSS and JS are loaded from bundled resources and inlined into the generated\n * HTML, making the plugin fully self-contained with no external file dependencies.\n *\n * Mirrors: `org.libpetri.doclet.DiagramRenderer`\n *\n * @module doclet/diagram-renderer\n */\n\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nfunction loadResource(filename: string): string {\n return readFileSync(join(__dirname, 'resources', filename), 'utf-8');\n}\n\nlet inlineCss: string | undefined;\nlet inlineJs: string | undefined;\n\nfunction css(): string {\n return (inlineCss ??= loadResource('petrinet-diagrams.css'));\n}\n\nfunction js(): string {\n return (inlineJs ??= loadResource('petrinet-diagrams.js'));\n}\n\n/**\n * Escapes HTML special characters for safe embedding.\n */\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\n/**\n * Renders a pre-built SVG diagram with an optional title.\n *\n * Inlines CSS and JS from bundled resources. The JS is guarded by an\n * idempotency check so it only executes once per page, even when multiple\n * `@petrinet` tags appear.\n *\n * @param title - optional title (null/undefined for no title)\n * @param svgContent - the SVG markup\n * @param dotSource - the DOT source code for display in a collapsible block\n * @returns HTML markup with diagram controls\n */\nexport function renderSvg(\n title: string | null | undefined,\n svgContent: string,\n dotSource: string,\n): string {\n const titleHtml = title ? `<h4>${escapeHtml(title)}</h4>\\n` : '';\n const summaryText = title ? 'View DOT Source' : 'View Source';\n\n return `<style>${css()}</style>\n<script>if(!window._petriNetDiagramsInit){window._petriNetDiagramsInit=true;\n${js()}\n}</script>\n<div class=\"petrinet-diagram\">\n${titleHtml}<div class=\"diagram-container\">\n<div class=\"diagram-controls\">\n<button class=\"diagram-btn btn-reset\" title=\"Reset zoom\">Reset</button>\n<button class=\"diagram-btn btn-fullscreen\" onclick=\"PetriNetDiagrams.toggleFullscreen(this)\">Fullscreen</button>\n</div>\n${svgContent}\n</div>\n<details>\n<summary>${summaryText}</summary>\n<pre><code>${escapeHtml(dotSource)}</code></pre>\n</details>\n</div>`;\n}\n"],"mappings":"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","names":["dirname"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
.petrinet-diagram {
|
|
2
|
+
margin: 1.5em 0;
|
|
3
|
+
border: 1px solid #ddd;
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
background: #fafafa;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.petrinet-diagram h4 {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 12px 16px;
|
|
11
|
+
background: #f0f0f0;
|
|
12
|
+
border-bottom: 1px solid #ddd;
|
|
13
|
+
border-radius: 8px 8px 0 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.petrinet-diagram .diagram-container {
|
|
17
|
+
position: relative;
|
|
18
|
+
padding: 16px;
|
|
19
|
+
height: 70vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.petrinet-diagram .diagram-viewport {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
cursor: grab;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.petrinet-diagram .diagram-viewport:active {
|
|
30
|
+
cursor: grabbing;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.petrinet-diagram .diagram-container svg {
|
|
34
|
+
display: block;
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.petrinet-diagram .diagram-controls {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 8px;
|
|
42
|
+
right: 8px;
|
|
43
|
+
display: flex;
|
|
44
|
+
gap: 4px;
|
|
45
|
+
z-index: 10;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.petrinet-diagram .diagram-btn {
|
|
49
|
+
padding: 6px 10px;
|
|
50
|
+
border: 1px solid #ccc;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
background: white;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
min-width: 32px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.petrinet-diagram .diagram-btn:hover {
|
|
59
|
+
background: #e8e8e8;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.petrinet-diagram details {
|
|
63
|
+
padding: 0 16px 16px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.petrinet-diagram summary {
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
padding: 8px 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Fullscreen mode */
|
|
73
|
+
.diagram-fullscreen {
|
|
74
|
+
position: fixed !important;
|
|
75
|
+
top: 0 !important;
|
|
76
|
+
left: 0 !important;
|
|
77
|
+
width: 100vw !important;
|
|
78
|
+
height: 100vh !important;
|
|
79
|
+
max-height: 100vh !important;
|
|
80
|
+
background: white !important;
|
|
81
|
+
z-index: 9999 !important;
|
|
82
|
+
margin: 0 !important;
|
|
83
|
+
border-radius: 0 !important;
|
|
84
|
+
overflow: hidden !important;
|
|
85
|
+
display: flex !important;
|
|
86
|
+
flex-direction: column !important;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.diagram-fullscreen .diagram-container {
|
|
90
|
+
flex: 1 !important;
|
|
91
|
+
min-height: 0 !important;
|
|
92
|
+
height: auto !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.diagram-fullscreen .diagram-controls {
|
|
96
|
+
position: fixed;
|
|
97
|
+
top: 16px;
|
|
98
|
+
right: 16px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.diagram-fullscreen details {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.diagram-fullscreen svg {
|
|
106
|
+
max-width: none;
|
|
107
|
+
max-height: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Zoom indicator */
|
|
111
|
+
.zoom-indicator {
|
|
112
|
+
position: absolute;
|
|
113
|
+
bottom: 8px;
|
|
114
|
+
left: 8px;
|
|
115
|
+
background: rgba(0,0,0,0.6);
|
|
116
|
+
color: white;
|
|
117
|
+
padding: 4px 8px;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
opacity: 0;
|
|
121
|
+
transition: opacity 0.3s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.zoom-indicator.visible {
|
|
125
|
+
opacity: 1;
|
|
126
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Petri Net Diagram Viewer
|
|
2
|
+
// Provides: zoom/pan and fullscreen for pre-rendered SVG diagrams
|
|
3
|
+
|
|
4
|
+
(function() {
|
|
5
|
+
if (document.readyState === 'loading') {
|
|
6
|
+
document.addEventListener('DOMContentLoaded', enhanceDiagrams);
|
|
7
|
+
} else {
|
|
8
|
+
enhanceDiagrams();
|
|
9
|
+
}
|
|
10
|
+
})();
|
|
11
|
+
|
|
12
|
+
function enhanceDiagrams() {
|
|
13
|
+
document.querySelectorAll('.petrinet-diagram').forEach(function(diagram) {
|
|
14
|
+
var container = diagram.querySelector('.diagram-container');
|
|
15
|
+
var svg = container ? container.querySelector('svg') : null;
|
|
16
|
+
if (!svg) return;
|
|
17
|
+
|
|
18
|
+
var scale = 1, panX = 0, panY = 0;
|
|
19
|
+
var isPanning = false, startX, startY;
|
|
20
|
+
|
|
21
|
+
// Zoom indicator
|
|
22
|
+
var indicator = document.createElement('div');
|
|
23
|
+
indicator.className = 'zoom-indicator';
|
|
24
|
+
indicator.textContent = '100%';
|
|
25
|
+
container.appendChild(indicator);
|
|
26
|
+
var hideTimeout;
|
|
27
|
+
|
|
28
|
+
function applyTransform() {
|
|
29
|
+
svg.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + ')';
|
|
30
|
+
svg.style.transformOrigin = 'center center';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function showZoom() {
|
|
34
|
+
indicator.textContent = Math.round(scale * 100) + '%';
|
|
35
|
+
indicator.classList.add('visible');
|
|
36
|
+
clearTimeout(hideTimeout);
|
|
37
|
+
hideTimeout = setTimeout(function() { indicator.classList.remove('visible'); }, 1000);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ctrl+wheel to zoom
|
|
41
|
+
container.addEventListener('wheel', function(e) {
|
|
42
|
+
if (!e.ctrlKey) return;
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
var delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
45
|
+
var newScale = Math.max(0.1, Math.min(5, scale * delta));
|
|
46
|
+
var rect = container.getBoundingClientRect();
|
|
47
|
+
var mouseX = e.clientX - rect.left - rect.width / 2;
|
|
48
|
+
var mouseY = e.clientY - rect.top - rect.height / 2;
|
|
49
|
+
panX = mouseX - (mouseX - panX) * (newScale / scale);
|
|
50
|
+
panY = mouseY - (mouseY - panY) * (newScale / scale);
|
|
51
|
+
scale = newScale;
|
|
52
|
+
applyTransform();
|
|
53
|
+
showZoom();
|
|
54
|
+
}, { passive: false });
|
|
55
|
+
|
|
56
|
+
// Drag to pan
|
|
57
|
+
container.addEventListener('mousedown', function(e) {
|
|
58
|
+
if (e.button !== 0) return;
|
|
59
|
+
isPanning = true;
|
|
60
|
+
startX = e.clientX - panX;
|
|
61
|
+
startY = e.clientY - panY;
|
|
62
|
+
container.style.cursor = 'grabbing';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
document.addEventListener('mousemove', function(e) {
|
|
66
|
+
if (!isPanning) return;
|
|
67
|
+
panX = e.clientX - startX;
|
|
68
|
+
panY = e.clientY - startY;
|
|
69
|
+
applyTransform();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
document.addEventListener('mouseup', function() {
|
|
73
|
+
if (isPanning) {
|
|
74
|
+
isPanning = false;
|
|
75
|
+
container.style.cursor = 'grab';
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Reset button
|
|
80
|
+
var resetBtn = diagram.querySelector('.btn-reset');
|
|
81
|
+
if (resetBtn) {
|
|
82
|
+
resetBtn.addEventListener('click', function() {
|
|
83
|
+
scale = 1; panX = 0; panY = 0;
|
|
84
|
+
applyTransform();
|
|
85
|
+
showZoom();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
container.style.cursor = 'grab';
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fullscreen toggle (namespaced to avoid global pollution)
|
|
94
|
+
window.PetriNetDiagrams = window.PetriNetDiagrams || {};
|
|
95
|
+
PetriNetDiagrams.toggleFullscreen = function(btn) {
|
|
96
|
+
var diagram = btn.closest('.petrinet-diagram');
|
|
97
|
+
diagram.classList.toggle('diagram-fullscreen');
|
|
98
|
+
btn.textContent = diagram.classList.contains('diagram-fullscreen') ? 'Exit' : 'Fullscreen';
|
|
99
|
+
document.body.style.overflow = diagram.classList.contains('diagram-fullscreen') ? 'hidden' : '';
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ESC to exit fullscreen
|
|
103
|
+
document.addEventListener('keydown', function(e) {
|
|
104
|
+
if (e.key === 'Escape') {
|
|
105
|
+
var fs = document.querySelector('.diagram-fullscreen');
|
|
106
|
+
if (fs) {
|
|
107
|
+
var btn = fs.querySelector('.btn-fullscreen');
|
|
108
|
+
if (btn) btn.click();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/export/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libpetri",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Coloured Time Petri Net engine — TypeScript port",
|
|
5
5
|
"homepage": "https://libpetri.org",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"import": "./dist/debug/index.js",
|
|
37
37
|
"types": "./dist/debug/index.d.ts"
|
|
38
38
|
}
|
|
39
|
+
},
|
|
40
|
+
"./doclet": {
|
|
41
|
+
"node": {
|
|
42
|
+
"import": "./dist/doclet/index.js",
|
|
43
|
+
"types": "./dist/doclet/index.d.ts"
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
},
|
|
41
47
|
"scripts": {
|
|
@@ -43,13 +49,29 @@
|
|
|
43
49
|
"check": "tsc --noEmit",
|
|
44
50
|
"test": "vitest run",
|
|
45
51
|
"test:watch": "vitest",
|
|
46
|
-
"bench": "vitest bench"
|
|
52
|
+
"bench": "vitest bench",
|
|
53
|
+
"docs": "typedoc"
|
|
47
54
|
},
|
|
48
55
|
"devDependencies": {
|
|
49
|
-
"
|
|
56
|
+
"@types/node": "^25.3.3",
|
|
57
|
+
"@viz-js/viz": "^3.24.0",
|
|
50
58
|
"tsup": "^8.4.0",
|
|
59
|
+
"typedoc": "^0.28.17",
|
|
60
|
+
"typescript": "^5.7.0",
|
|
51
61
|
"vitest": "^3.0.0"
|
|
52
62
|
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@viz-js/viz": ">=3.0.0",
|
|
65
|
+
"typedoc": ">=0.27.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"typedoc": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@viz-js/viz": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
53
75
|
"dependencies": {
|
|
54
76
|
"z3-solver": "^4.13.4"
|
|
55
77
|
}
|
|
File without changes
|