@thomasfosterau/prosekit-flowchart 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Flowchart post-processing pass for a ProseKit HTML renderer.
3
+ *
4
+ * `@thomasfosterau/prosekit-utilities/prosekit`'s `createHtmlRenderer` (and
5
+ * ProseKit's own `toDOM`) render a flowchart node as an inert
6
+ * `<div data-flowchart data-payload="…">` placeholder — the node spec has no
7
+ * way to synthesise the SVG itself, since `toDOM` runs with no access to
8
+ * `generateFlowchartSvg`. A consuming app wires this in as that renderer's
9
+ * `postProcess` hook so the placeholder becomes a real diagram in the served
10
+ * HTML:
11
+ *
12
+ * ```ts
13
+ * createHtmlRenderer({ postProcess: postProcessFlowcharts });
14
+ * ```
15
+ *
16
+ * Ported verbatim from Website's `postProcessFlowcharts` in
17
+ * `src/editor/html.ts`.
18
+ *
19
+ * @module @thomasfosterau/prosekit-flowchart/node/html
20
+ */
21
+ /**
22
+ * Replaces the inner content of every `[data-flowchart]` placeholder with a
23
+ * server-rendered SVG snapshot derived from the placeholder's `data-payload`
24
+ * JSON attribute. The `data-payload` attribute stays on the element so the
25
+ * editor can still round-trip the diagram via {@link parseFlowchartData}.
26
+ *
27
+ * `document` is the caller's DOM implementation (the browser's global, or a
28
+ * server-side one such as linkedom) — this module never constructs one
29
+ * itself, keeping it usable from both a browser bundle and a Worker.
30
+ */
31
+ export declare function postProcessFlowcharts(html: string, document: Document): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Flowchart post-processing pass for a ProseKit HTML renderer.
3
+ *
4
+ * `@thomasfosterau/prosekit-utilities/prosekit`'s `createHtmlRenderer` (and
5
+ * ProseKit's own `toDOM`) render a flowchart node as an inert
6
+ * `<div data-flowchart data-payload="…">` placeholder — the node spec has no
7
+ * way to synthesise the SVG itself, since `toDOM` runs with no access to
8
+ * `generateFlowchartSvg`. A consuming app wires this in as that renderer's
9
+ * `postProcess` hook so the placeholder becomes a real diagram in the served
10
+ * HTML:
11
+ *
12
+ * ```ts
13
+ * createHtmlRenderer({ postProcess: postProcessFlowcharts });
14
+ * ```
15
+ *
16
+ * Ported verbatim from Website's `postProcessFlowcharts` in
17
+ * `src/editor/html.ts`.
18
+ *
19
+ * @module @thomasfosterau/prosekit-flowchart/node/html
20
+ */
21
+ import * as Array from 'effect/Array';
22
+ import { parseFlowchartData } from "./data.js";
23
+ import { generateFlowchartSvg } from "./svg.js";
24
+ /**
25
+ * Replaces the inner content of every `[data-flowchart]` placeholder with a
26
+ * server-rendered SVG snapshot derived from the placeholder's `data-payload`
27
+ * JSON attribute. The `data-payload` attribute stays on the element so the
28
+ * editor can still round-trip the diagram via {@link parseFlowchartData}.
29
+ *
30
+ * `document` is the caller's DOM implementation (the browser's global, or a
31
+ * server-side one such as linkedom) — this module never constructs one
32
+ * itself, keeping it usable from both a browser bundle and a Worker.
33
+ */
34
+ export function postProcessFlowcharts(html, document) {
35
+ if (!html.includes('data-flowchart'))
36
+ return html;
37
+ const container = document.createElement('div');
38
+ container.innerHTML = html;
39
+ const placeholders = Array.fromIterable(container.querySelectorAll('[data-flowchart]'));
40
+ placeholders.forEach((placeholder) => {
41
+ const payload = placeholder.getAttribute('data-payload') ?? '';
42
+ const data = parseFlowchartData(payload);
43
+ placeholder.innerHTML = generateFlowchartSvg(data);
44
+ });
45
+ return container.innerHTML;
46
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@thomasfosterau/prosekit-flowchart/node` — the flowchart block without a
3
+ * canvas: the ProseKit node spec and command, the Effect Schema payload, the
4
+ * server-renderable SVG, the attribute-overlay entry and the HTML / Markdown
5
+ * serialiser hooks. Safe on a server and in a bundle that never renders the
6
+ * canvas.
7
+ *
8
+ * @module
9
+ */
10
+ export { EMPTY_FLOWCHART, parseFlowchartData, type FlowchartPosition } from './data.ts';
11
+ export { defineFlowchart } from './flowchart-node.ts';
12
+ export { postProcessFlowcharts } from './html.ts';
13
+ export { flowchartMarkdown } from './markdown.ts';
14
+ export { FLOWCHART_ATTRIBUTE_OVERRIDES } from './overrides.ts';
15
+ export { FlowchartData, FlowchartEdge, FlowchartNode } from './schema.ts';
16
+ export { generateFlowchartSvg, type FlowchartSvgLabels } from './svg.ts';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@thomasfosterau/prosekit-flowchart/node` — the flowchart block without a
3
+ * canvas: the ProseKit node spec and command, the Effect Schema payload, the
4
+ * server-renderable SVG, the attribute-overlay entry and the HTML / Markdown
5
+ * serialiser hooks. Safe on a server and in a bundle that never renders the
6
+ * canvas.
7
+ *
8
+ * @module
9
+ */
10
+ export { EMPTY_FLOWCHART, parseFlowchartData } from "./data.js";
11
+ export { defineFlowchart } from "./flowchart-node.js";
12
+ export { postProcessFlowcharts } from "./html.js";
13
+ export { flowchartMarkdown } from "./markdown.js";
14
+ export { FLOWCHART_ATTRIBUTE_OVERRIDES } from "./overrides.js";
15
+ export { FlowchartData, FlowchartEdge, FlowchartNode } from "./schema.js";
16
+ export { generateFlowchartSvg } from "./svg.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Markdown placeholder for the flowchart block.
3
+ *
4
+ * Markdown has no diagram syntax, so a flowchart node renders as a fixed,
5
+ * overridable label rather than being silently dropped — the Markdown stays
6
+ * a truthful reflection of what the HTML renders.
7
+ *
8
+ * A factory rather than a constant (unlike `@thomasfosterau/prosekit-utilities`'s
9
+ * other opt-in serialisers, `mentionMarkdown` / `figureMarkdown` /
10
+ * `sidenoteMarkdown`) so a consuming app can change the label without
11
+ * re-implementing the `BlockSerialiser`:
12
+ *
13
+ * ```ts
14
+ * const toMarkdown = createMarkdownSerialiser({
15
+ * blocks: { flowchart: flowchartMarkdown('_[diagram omitted]_') },
16
+ * });
17
+ * ```
18
+ *
19
+ * Ported verbatim from the `case "flowchart": return "_[flowchart
20
+ * diagram]_";` branch of Website's `src/editor/markdown.ts`.
21
+ *
22
+ * @module @thomasfosterau/prosekit-flowchart/node/markdown
23
+ */
24
+ import type { BlockSerialiser } from '@thomasfosterau/prosekit-utilities';
25
+ /**
26
+ * Builds a {@link BlockSerialiser} that renders every flowchart node as
27
+ * `label`, ignoring the node's own content — there is nothing in a
28
+ * {@link import("./schema.ts").FlowchartData} payload with a faithful
29
+ * Markdown form.
30
+ */
31
+ export declare function flowchartMarkdown(label?: string): BlockSerialiser;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Markdown placeholder for the flowchart block.
3
+ *
4
+ * Markdown has no diagram syntax, so a flowchart node renders as a fixed,
5
+ * overridable label rather than being silently dropped — the Markdown stays
6
+ * a truthful reflection of what the HTML renders.
7
+ *
8
+ * A factory rather than a constant (unlike `@thomasfosterau/prosekit-utilities`'s
9
+ * other opt-in serialisers, `mentionMarkdown` / `figureMarkdown` /
10
+ * `sidenoteMarkdown`) so a consuming app can change the label without
11
+ * re-implementing the `BlockSerialiser`:
12
+ *
13
+ * ```ts
14
+ * const toMarkdown = createMarkdownSerialiser({
15
+ * blocks: { flowchart: flowchartMarkdown('_[diagram omitted]_') },
16
+ * });
17
+ * ```
18
+ *
19
+ * Ported verbatim from the `case "flowchart": return "_[flowchart
20
+ * diagram]_";` branch of Website's `src/editor/markdown.ts`.
21
+ *
22
+ * @module @thomasfosterau/prosekit-flowchart/node/markdown
23
+ */
24
+ /** Website's own copy, and this factory's default `label`. */
25
+ const DEFAULT_LABEL = '_[flowchart diagram]_';
26
+ /**
27
+ * Builds a {@link BlockSerialiser} that renders every flowchart node as
28
+ * `label`, ignoring the node's own content — there is nothing in a
29
+ * {@link import("./schema.ts").FlowchartData} payload with a faithful
30
+ * Markdown form.
31
+ */
32
+ export function flowchartMarkdown(label = DEFAULT_LABEL) {
33
+ return () => label;
34
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The flowchart node's attribute-overlay entry.
3
+ *
4
+ * `@thomasfosterau/prosekit-utilities/derive` recovers a ProseMirror node's
5
+ * attribute *structure* (its default, whether the key is omissible) but not
6
+ * its value *type* — a caller supplies that through an `AttributeOverlay` /
7
+ * `attributeOverlay()` record. `flowchart.data` is the one attribute this
8
+ * package's node spec owns, so this is the entry a consuming app merges into
9
+ * its own overlay:
10
+ *
11
+ * ```ts
12
+ * import { attributeOverlay } from '@thomasfosterau/prosekit-utilities/derive';
13
+ * import { FLOWCHART_ATTRIBUTE_OVERRIDES } from '@thomasfosterau/prosekit-flowchart/node';
14
+ *
15
+ * const overlay = attributeOverlay({
16
+ * ...STANDARD_ATTRIBUTE_OVERRIDES,
17
+ * ...FLOWCHART_ATTRIBUTE_OVERRIDES,
18
+ * });
19
+ * ```
20
+ *
21
+ * Wrapped in `required(...)`: ProseMirror's own rule is that any attribute
22
+ * with a `default` — even one an overlay supplies a value type for — becomes
23
+ * an omissible key, but a live editor's `flowchart` node always serialises
24
+ * `data` in practice (`Node.toJSON()` includes every attr, defaulted or not).
25
+ * Leaving it omissible would let a machine-authored document skip the value
26
+ * the model was supposed to specify (a flowchart with no diagram data)
27
+ * without failing validation.
28
+ *
29
+ * Ported verbatim from the `flowchart: { data: required(FlowchartData) }`
30
+ * entry in Website's `src/editor/document-schema.ts`.
31
+ *
32
+ * @module @thomasfosterau/prosekit-flowchart/node/overrides
33
+ */
34
+ import { type RequiredAttribute } from '@thomasfosterau/prosekit-utilities/derive';
35
+ /**
36
+ * The flowchart node's attribute overlay: `data` typed as {@link FlowchartData}
37
+ * and required.
38
+ */
39
+ export declare const FLOWCHART_ATTRIBUTE_OVERRIDES: {
40
+ readonly flowchart: {
41
+ readonly data: RequiredAttribute;
42
+ };
43
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The flowchart node's attribute-overlay entry.
3
+ *
4
+ * `@thomasfosterau/prosekit-utilities/derive` recovers a ProseMirror node's
5
+ * attribute *structure* (its default, whether the key is omissible) but not
6
+ * its value *type* — a caller supplies that through an `AttributeOverlay` /
7
+ * `attributeOverlay()` record. `flowchart.data` is the one attribute this
8
+ * package's node spec owns, so this is the entry a consuming app merges into
9
+ * its own overlay:
10
+ *
11
+ * ```ts
12
+ * import { attributeOverlay } from '@thomasfosterau/prosekit-utilities/derive';
13
+ * import { FLOWCHART_ATTRIBUTE_OVERRIDES } from '@thomasfosterau/prosekit-flowchart/node';
14
+ *
15
+ * const overlay = attributeOverlay({
16
+ * ...STANDARD_ATTRIBUTE_OVERRIDES,
17
+ * ...FLOWCHART_ATTRIBUTE_OVERRIDES,
18
+ * });
19
+ * ```
20
+ *
21
+ * Wrapped in `required(...)`: ProseMirror's own rule is that any attribute
22
+ * with a `default` — even one an overlay supplies a value type for — becomes
23
+ * an omissible key, but a live editor's `flowchart` node always serialises
24
+ * `data` in practice (`Node.toJSON()` includes every attr, defaulted or not).
25
+ * Leaving it omissible would let a machine-authored document skip the value
26
+ * the model was supposed to specify (a flowchart with no diagram data)
27
+ * without failing validation.
28
+ *
29
+ * Ported verbatim from the `flowchart: { data: required(FlowchartData) }`
30
+ * entry in Website's `src/editor/document-schema.ts`.
31
+ *
32
+ * @module @thomasfosterau/prosekit-flowchart/node/overrides
33
+ */
34
+ import { required } from '@thomasfosterau/prosekit-utilities/derive';
35
+ import { FlowchartData } from "./schema.js";
36
+ /**
37
+ * The flowchart node's attribute overlay: `data` typed as {@link FlowchartData}
38
+ * and required.
39
+ */
40
+ export const FLOWCHART_ATTRIBUTE_OVERRIDES = {
41
+ flowchart: { data: /* @__PURE__ */ required(FlowchartData) },
42
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Effect Schema for the flowchart node's persisted `data` attribute.
3
+ *
4
+ * This is the value-type seam a ProseMirror `AttributeSpec` cannot express on
5
+ * its own: `FLOWCHART_ATTRIBUTE_OVERRIDES` (`./overrides.ts`) hands these
6
+ * schemas to a consuming app's `@thomasfosterau/prosekit-utilities/derive`
7
+ * attribute overlay so the derived document schema can decode/encode the
8
+ * flowchart payload precisely, rather than treating it as an opaque blob.
9
+ *
10
+ * The persisted shape is a strict subset of what this package's canvas
11
+ * dependency (Svelte Flow) accepts: only the fields that must survive
12
+ * persistence, so its internal fields (selection, measured dimensions) never
13
+ * round-trip.
14
+ *
15
+ * Ported verbatim from Website's `src/editor/flowchart/schema.ts`.
16
+ *
17
+ * @module @thomasfosterau/prosekit-flowchart/node/schema
18
+ */
19
+ import * as Schema from 'effect/Schema';
20
+ /** Persisted shape of a single flowchart node. */
21
+ export declare const FlowchartNode: Schema.Struct<{
22
+ readonly id: Schema.String;
23
+ readonly position: Schema.Struct<{
24
+ readonly x: Schema.Finite;
25
+ readonly y: Schema.Finite;
26
+ }>;
27
+ readonly data: Schema.Struct<{
28
+ readonly label: Schema.String;
29
+ }>;
30
+ }>;
31
+ /** The decoded type of {@link FlowchartNode}. */
32
+ export type FlowchartNode = typeof FlowchartNode.Type;
33
+ /** Persisted shape of a single flowchart edge. */
34
+ export declare const FlowchartEdge: Schema.Struct<{
35
+ readonly id: Schema.String;
36
+ readonly source: Schema.String;
37
+ readonly target: Schema.String;
38
+ }>;
39
+ /** The decoded type of {@link FlowchartEdge}. */
40
+ export type FlowchartEdge = typeof FlowchartEdge.Type;
41
+ /**
42
+ * Persisted payload stored on the ProseMirror `flowchart` node's `data` attr.
43
+ */
44
+ export declare const FlowchartData: Schema.Struct<{
45
+ readonly nodes: Schema.$Array<Schema.Struct<{
46
+ readonly id: Schema.String;
47
+ readonly position: Schema.Struct<{
48
+ readonly x: Schema.Finite;
49
+ readonly y: Schema.Finite;
50
+ }>;
51
+ readonly data: Schema.Struct<{
52
+ readonly label: Schema.String;
53
+ }>;
54
+ }>>;
55
+ readonly edges: Schema.$Array<Schema.Struct<{
56
+ readonly id: Schema.String;
57
+ readonly source: Schema.String;
58
+ readonly target: Schema.String;
59
+ }>>;
60
+ }>;
61
+ /** The decoded type of {@link FlowchartData}. */
62
+ export type FlowchartData = typeof FlowchartData.Type;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Effect Schema for the flowchart node's persisted `data` attribute.
3
+ *
4
+ * This is the value-type seam a ProseMirror `AttributeSpec` cannot express on
5
+ * its own: `FLOWCHART_ATTRIBUTE_OVERRIDES` (`./overrides.ts`) hands these
6
+ * schemas to a consuming app's `@thomasfosterau/prosekit-utilities/derive`
7
+ * attribute overlay so the derived document schema can decode/encode the
8
+ * flowchart payload precisely, rather than treating it as an opaque blob.
9
+ *
10
+ * The persisted shape is a strict subset of what this package's canvas
11
+ * dependency (Svelte Flow) accepts: only the fields that must survive
12
+ * persistence, so its internal fields (selection, measured dimensions) never
13
+ * round-trip.
14
+ *
15
+ * Ported verbatim from Website's `src/editor/flowchart/schema.ts`.
16
+ *
17
+ * @module @thomasfosterau/prosekit-flowchart/node/schema
18
+ */
19
+ import * as Schema from 'effect/Schema';
20
+ /**
21
+ * Position of a node on the flowchart canvas.
22
+ *
23
+ * Not exported: nothing outside this module needs the schema value, and
24
+ * `./data.ts` derives the public `FlowchartPosition` type from
25
+ * {@link FlowchartNode}'s `position` field instead, so there is no dead
26
+ * export to carry.
27
+ */
28
+ const FlowchartPosition = /* @__PURE__ */ Schema.Struct({
29
+ x: Schema.Finite,
30
+ y: Schema.Finite,
31
+ });
32
+ /** Persisted shape of a single flowchart node. */
33
+ export const FlowchartNode = /* @__PURE__ */ Schema.Struct({
34
+ id: Schema.String,
35
+ position: FlowchartPosition,
36
+ data: /* @__PURE__ */ Schema.Struct({ label: Schema.String }),
37
+ });
38
+ /** Persisted shape of a single flowchart edge. */
39
+ export const FlowchartEdge = /* @__PURE__ */ Schema.Struct({
40
+ id: Schema.String,
41
+ source: Schema.String,
42
+ target: Schema.String,
43
+ });
44
+ /**
45
+ * Persisted payload stored on the ProseMirror `flowchart` node's `data` attr.
46
+ */
47
+ export const FlowchartData = /* @__PURE__ */ Schema.Struct({
48
+ nodes: /* @__PURE__ */ Schema.Array(FlowchartNode),
49
+ edges: /* @__PURE__ */ Schema.Array(FlowchartEdge),
50
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Server-renderable SVG synthesis for flowchart blocks.
3
+ *
4
+ * Walks a {@link FlowchartData} payload and emits a self-contained SVG markup
5
+ * string — no DOM measurement required, since node positions are already
6
+ * persisted. This is the read-only fallback a consuming app's HTML renderer
7
+ * injects (via `postProcessFlowcharts`, `./html.ts`) so readers see the
8
+ * diagram without loading this package's canvas runtime.
9
+ *
10
+ * Output is not pixel-perfect with the interactive canvas (font metrics and
11
+ * edge curves are approximated) but is structurally faithful and cheap to
12
+ * generate in a Worker.
13
+ *
14
+ * Ported verbatim from Website's `src/editor/flowchart/svg.ts`, with the
15
+ * three user-facing strings (the empty-state label, the diagram's
16
+ * node-count summary, the untitled-node fallback) promoted to an optional
17
+ * `labels` parameter — each still defaults to Website's own copy.
18
+ *
19
+ * @module @thomasfosterau/prosekit-flowchart/node/svg
20
+ */
21
+ import type { FlowchartData } from './schema.ts';
22
+ /**
23
+ * Overridable copy for {@link generateFlowchartSvg}'s accessible strings;
24
+ * each field documents its English default.
25
+ */
26
+ export interface FlowchartSvgLabels {
27
+ /** The empty-state `<svg>`'s `aria-label` and visible text. Default: `'Empty flowchart'`. */
28
+ readonly emptyLabel?: string | undefined;
29
+ /** The populated diagram's `aria-label`, given the node count. Default: `Flowchart diagram with ${n} node(s)`. */
30
+ readonly diagramLabel?: ((nodeCount: number) => string) | undefined;
31
+ /** Fallback text for a node whose label is blank. Default: `'Untitled'`. */
32
+ readonly untitledLabel?: string | undefined;
33
+ }
34
+ /**
35
+ * Returns an SVG markup string for the given flowchart payload.
36
+ *
37
+ * The SVG uses `currentColor` for strokes and text so it adapts to the
38
+ * surrounding theme. Width is constrained to the container via `width="100%"`;
39
+ * height is derived from the viewBox aspect ratio.
40
+ */
41
+ export declare function generateFlowchartSvg(data: FlowchartData, labels?: FlowchartSvgLabels): string;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Server-renderable SVG synthesis for flowchart blocks.
3
+ *
4
+ * Walks a {@link FlowchartData} payload and emits a self-contained SVG markup
5
+ * string — no DOM measurement required, since node positions are already
6
+ * persisted. This is the read-only fallback a consuming app's HTML renderer
7
+ * injects (via `postProcessFlowcharts`, `./html.ts`) so readers see the
8
+ * diagram without loading this package's canvas runtime.
9
+ *
10
+ * Output is not pixel-perfect with the interactive canvas (font metrics and
11
+ * edge curves are approximated) but is structurally faithful and cheap to
12
+ * generate in a Worker.
13
+ *
14
+ * Ported verbatim from Website's `src/editor/flowchart/svg.ts`, with the
15
+ * three user-facing strings (the empty-state label, the diagram's
16
+ * node-count summary, the untitled-node fallback) promoted to an optional
17
+ * `labels` parameter — each still defaults to Website's own copy.
18
+ *
19
+ * @module @thomasfosterau/prosekit-flowchart/node/svg
20
+ */
21
+ import * as Array from 'effect/Array';
22
+ import * as HashMap from 'effect/HashMap';
23
+ import * as Option from 'effect/Option';
24
+ /** Minimum width of a rendered node box, in canvas units. */
25
+ const MIN_NODE_WIDTH = 100;
26
+ /** Fixed height of a rendered node box (single-line label). */
27
+ const NODE_HEIGHT = 40;
28
+ /** Horizontal padding inside the node box. */
29
+ const NODE_PADDING_X = 16;
30
+ /** Approximate advance width per character at the rendered font size. */
31
+ const CHAR_WIDTH = 7.2;
32
+ /** Padding around the rendered diagram inside the SVG viewBox. */
33
+ const CANVAS_PADDING = 20;
34
+ /** Default for `labels.emptyLabel`. */
35
+ const DEFAULT_EMPTY_LABEL = 'Empty flowchart';
36
+ /** Default for `labels.diagramLabel`. */
37
+ const defaultDiagramLabel = (nodeCount) => `Flowchart diagram with ${nodeCount} node(s)`;
38
+ /** Default for `labels.untitledLabel`. */
39
+ const DEFAULT_UNTITLED_LABEL = 'Untitled';
40
+ function sizeNode(node) {
41
+ const label = node.data.label;
42
+ const width = Math.max(MIN_NODE_WIDTH, label.length * CHAR_WIDTH + NODE_PADDING_X * 2);
43
+ return { ...node, width, height: NODE_HEIGHT };
44
+ }
45
+ /**
46
+ * Returns an SVG markup string for the given flowchart payload.
47
+ *
48
+ * The SVG uses `currentColor` for strokes and text so it adapts to the
49
+ * surrounding theme. Width is constrained to the container via `width="100%"`;
50
+ * height is derived from the viewBox aspect ratio.
51
+ */
52
+ export function generateFlowchartSvg(data, labels) {
53
+ const emptyLabel = labels?.emptyLabel ?? DEFAULT_EMPTY_LABEL;
54
+ const diagramLabel = labels?.diagramLabel ?? defaultDiagramLabel;
55
+ const untitledLabel = labels?.untitledLabel ?? DEFAULT_UNTITLED_LABEL;
56
+ const sized = data.nodes.map(sizeNode);
57
+ if (Array.isArrayEmpty(sized)) {
58
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 60" width="100%" ` +
59
+ `role="img" aria-label="${escapeXml(emptyLabel)}">` +
60
+ `<text x="120" y="30" text-anchor="middle" dominant-baseline="central" ` +
61
+ `font-family="ui-sans-serif, system-ui, sans-serif" font-size="12" fill="currentColor" ` +
62
+ `opacity="0.6">${escapeXml(emptyLabel)}</text>` +
63
+ `</svg>`);
64
+ }
65
+ const minX = Math.min(...sized.map((n) => n.position.x)) - CANVAS_PADDING;
66
+ const minY = Math.min(...sized.map((n) => n.position.y)) - CANVAS_PADDING;
67
+ const maxX = Math.max(...sized.map((n) => n.position.x + n.width)) + CANVAS_PADDING;
68
+ const maxY = Math.max(...sized.map((n) => n.position.y + n.height)) + CANVAS_PADDING;
69
+ const width = maxX - minX;
70
+ const height = maxY - minY;
71
+ const byId = HashMap.fromIterable(sized.map((n) => [n.id, n]));
72
+ const defs = `<defs>` +
73
+ `<marker id="flowchart-arrow" viewBox="0 0 10 10" refX="9" refY="5" ` +
74
+ `markerWidth="6" markerHeight="6" orient="auto-start-reverse">` +
75
+ `<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />` +
76
+ `</marker>` +
77
+ `</defs>`;
78
+ const edgePaths = Array.getSomes(data.edges.map((edge) => renderEdge(edge, byId))).join('');
79
+ const nodeRects = sized.map((node) => renderNode(node, untitledLabel)).join('');
80
+ return (`<svg xmlns="http://www.w3.org/2000/svg" ` +
81
+ `viewBox="${minX} ${minY} ${width} ${height}" ` +
82
+ `width="100%" preserveAspectRatio="xMidYMid meet" ` +
83
+ `role="img" aria-label="${escapeXml(diagramLabel(sized.length))}">` +
84
+ defs +
85
+ edgePaths +
86
+ nodeRects +
87
+ `</svg>`);
88
+ }
89
+ function renderNode(node, untitledLabel) {
90
+ const { position, width, height, data } = node;
91
+ return (`<g transform="translate(${position.x},${position.y})">` +
92
+ `<rect width="${width}" height="${height}" rx="4" ry="4" ` +
93
+ `fill="transparent" stroke="currentColor" stroke-width="1" />` +
94
+ `<text x="${width / 2}" y="${height / 2}" text-anchor="middle" ` +
95
+ `dominant-baseline="central" font-family="ui-sans-serif, system-ui, sans-serif" ` +
96
+ `font-size="12" fill="currentColor">${escapeXml(data.label || untitledLabel)}</text>` +
97
+ `</g>`);
98
+ }
99
+ function renderEdge(edge, nodes) {
100
+ const sourceOption = HashMap.get(nodes, edge.source);
101
+ const targetOption = HashMap.get(nodes, edge.target);
102
+ if (Option.isNone(sourceOption) || Option.isNone(targetOption))
103
+ return Option.none();
104
+ const source = sourceOption.value;
105
+ const target = targetOption.value;
106
+ // Source handle at bottom-center; target handle at top-center. Approximates
107
+ // Svelte Flow's default bezier edge with vertical handles.
108
+ const x1 = source.position.x + source.width / 2;
109
+ const y1 = source.position.y + source.height;
110
+ const x2 = target.position.x + target.width / 2;
111
+ const y2 = target.position.y;
112
+ const cyMid = (y1 + y2) / 2;
113
+ const path = `M ${x1},${y1} C ${x1},${cyMid} ${x2},${cyMid} ${x2},${y2}`;
114
+ return Option.some(`<path d="${path}" fill="none" stroke="currentColor" stroke-width="1.5" ` +
115
+ `marker-end="url(#flowchart-arrow)" />`);
116
+ }
117
+ function escapeXml(value) {
118
+ return value
119
+ .replace(/&/g, '&amp;')
120
+ .replace(/</g, '&lt;')
121
+ .replace(/>/g, '&gt;')
122
+ .replace(/"/g, '&quot;')
123
+ .replace(/'/g, '&apos;');
124
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The flowchart's toolbar and insert-menu entries.
3
+ *
4
+ * `@thomasfosterau/prosekit-svelte`'s `BlockToolbar` renders a list of values
5
+ * rather than a hard-coded row, so a block that lives in its own package can
6
+ * add itself to a toolbar without either package importing the other's UI.
7
+ * These two factories are that entry: an application appends
8
+ * `flowchartToolbarItem()` to its `items` and `flowchartSlashCommand()` to its
9
+ * `slashCommands`, and gets the same button and menu row the source
10
+ * application had.
11
+ *
12
+ * Both insert the starter diagram the source application used — one node
13
+ * labelled "Start" at (120, 80) — rather than an empty payload, because an
14
+ * empty canvas gives an author nothing to double-click and no anchor to drag a
15
+ * connection from.
16
+ *
17
+ * @module @thomasfosterau/prosekit-flowchart/toolbar
18
+ */
19
+ import type { SlashCommand, ToolbarItem } from '@thomasfosterau/prosekit-svelte';
20
+ /**
21
+ * The toolbar button that inserts a flowchart block.
22
+ *
23
+ * An action, not a toggle: inserting a block has no pressed state, and a
24
+ * button claiming one would lie to every assistive technology reading it.
25
+ *
26
+ * @param label - Visible text and accessible name. Default: `'Flowchart'`.
27
+ */
28
+ export declare function flowchartToolbarItem(label?: string): ToolbarItem;
29
+ /**
30
+ * The insert-menu entry that inserts a flowchart block — the same insertion as
31
+ * {@link flowchartToolbarItem}, reached from the Insert menu instead.
32
+ *
33
+ * @param label - Visible text and accessible name. Default: `'Flowchart'`.
34
+ */
35
+ export declare function flowchartSlashCommand(label?: string): SlashCommand;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The flowchart's toolbar and insert-menu entries.
3
+ *
4
+ * `@thomasfosterau/prosekit-svelte`'s `BlockToolbar` renders a list of values
5
+ * rather than a hard-coded row, so a block that lives in its own package can
6
+ * add itself to a toolbar without either package importing the other's UI.
7
+ * These two factories are that entry: an application appends
8
+ * `flowchartToolbarItem()` to its `items` and `flowchartSlashCommand()` to its
9
+ * `slashCommands`, and gets the same button and menu row the source
10
+ * application had.
11
+ *
12
+ * Both insert the starter diagram the source application used — one node
13
+ * labelled "Start" at (120, 80) — rather than an empty payload, because an
14
+ * empty canvas gives an author nothing to double-click and no anchor to drag a
15
+ * connection from.
16
+ *
17
+ * @module @thomasfosterau/prosekit-flowchart/toolbar
18
+ */
19
+ /**
20
+ * The diagram a freshly inserted flowchart block carries.
21
+ *
22
+ * Built per call, never shared: the payload becomes a ProseMirror node
23
+ * attribute, and handing two nodes the same object would let an edit to one
24
+ * diagram appear in the other.
25
+ */
26
+ function starterDiagram() {
27
+ return {
28
+ nodes: [{ id: 'n-start', position: { x: 120, y: 80 }, data: { label: 'Start' } }],
29
+ edges: [],
30
+ };
31
+ }
32
+ /**
33
+ * Inserts a `flowchart` node at the selection, then returns focus to the
34
+ * document.
35
+ *
36
+ * The command is reached by name through the editor's own `commands` record —
37
+ * the same loose lookup the default item set uses — so the entry is inert
38
+ * rather than throwing against an extension that never registered
39
+ * `defineFlowchart()`.
40
+ */
41
+ function insertFlowchart({ editor }) {
42
+ const commands = editor.commands;
43
+ commands['insertNode']?.({ type: 'flowchart', attrs: { data: starterDiagram() } });
44
+ editor.focus();
45
+ }
46
+ /**
47
+ * The toolbar button that inserts a flowchart block.
48
+ *
49
+ * An action, not a toggle: inserting a block has no pressed state, and a
50
+ * button claiming one would lie to every assistive technology reading it.
51
+ *
52
+ * @param label - Visible text and accessible name. Default: `'Flowchart'`.
53
+ */
54
+ export function flowchartToolbarItem(label = 'Flowchart') {
55
+ return {
56
+ id: 'flowchart',
57
+ label,
58
+ kind: 'action',
59
+ run: insertFlowchart,
60
+ };
61
+ }
62
+ /**
63
+ * The insert-menu entry that inserts a flowchart block — the same insertion as
64
+ * {@link flowchartToolbarItem}, reached from the Insert menu instead.
65
+ *
66
+ * @param label - Visible text and accessible name. Default: `'Flowchart'`.
67
+ */
68
+ export function flowchartSlashCommand(label = 'Flowchart') {
69
+ return {
70
+ id: 'flowchart',
71
+ label,
72
+ run: insertFlowchart,
73
+ };
74
+ }