@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.
- package/LICENSE +21 -0
- package/README.md +180 -2
- package/dist/EditableNode.svelte +111 -0
- package/dist/EditableNode.svelte.d.ts +35 -0
- package/dist/Flowchart.svelte +170 -0
- package/dist/Flowchart.svelte.d.ts +27 -0
- package/dist/FlowchartEditDialog.svelte +188 -0
- package/dist/FlowchartEditDialog.svelte.d.ts +42 -0
- package/dist/FlowchartNodeView.svelte +71 -0
- package/dist/FlowchartNodeView.svelte.d.ts +16 -0
- package/dist/flowchart-node-view.d.ts +25 -0
- package/dist/flowchart-node-view.js +35 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/node/data.d.ts +28 -0
- package/dist/node/data.js +57 -0
- package/dist/node/flowchart-node.d.ts +63 -0
- package/dist/node/flowchart-node.js +100 -0
- package/dist/node/html.d.ts +31 -0
- package/dist/node/html.js +46 -0
- package/dist/node/index.d.ts +16 -0
- package/dist/node/index.js +16 -0
- package/dist/node/markdown.d.ts +31 -0
- package/dist/node/markdown.js +34 -0
- package/dist/node/overrides.d.ts +43 -0
- package/dist/node/overrides.js +42 -0
- package/dist/node/schema.d.ts +62 -0
- package/dist/node/schema.js +50 -0
- package/dist/node/svg.d.ts +41 -0
- package/dist/node/svg.js +124 -0
- package/dist/toolbar.d.ts +35 -0
- package/dist/toolbar.js +74 -0
- package/package.json +68 -3
|
@@ -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;
|
package/dist/node/svg.js
ADDED
|
@@ -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, '&')
|
|
120
|
+
.replace(/</g, '<')
|
|
121
|
+
.replace(/>/g, '>')
|
|
122
|
+
.replace(/"/g, '"')
|
|
123
|
+
.replace(/'/g, ''');
|
|
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;
|
package/dist/toolbar.js
ADDED
|
@@ -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
|
+
}
|