slidemind 0.3.19 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/engine-slides.mjs +1 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
// plan.md carries inside each ```storyboard-canvas``` fence.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// The per-slide plan record: the defensive parser/serializer for the JSON block a
|
|
2
|
+
// plan.md carries inside each ```storyboard-canvas``` fence. It holds the three
|
|
3
|
+
// things a human signs off — the story beat, the content points, the layout —
|
|
4
|
+
// plus the agent's own bookkeeping (role, sequence link, layout provenance,
|
|
5
|
+
// image direction) and — since the image-intent contract — the slide's
|
|
6
|
+
// `assetUses`, the approved obligations for the deck's imported images
|
|
7
|
+
// (src/lib/image-intent.mjs). It deliberately holds NO design input: no blocks, no
|
|
8
|
+
// geometry, no design-element names. Composition is decided by the concept
|
|
9
|
+
// generator from the story, the content and the layout's own content region,
|
|
10
|
+
// not guessed at plan time in a coordinate space nobody reviewed. The fence
|
|
11
|
+
// keeps its name so every producer and consumer reads unchanged.
|
|
12
|
+
// Canonical here;
|
|
13
|
+
// the add-in imports a byte-identical mirror (scripts/sync-transcription-runtime.mjs),
|
|
14
|
+
// typed by its src/ui/src/pages/plan-review/storyboard-schema.ts. This module also
|
|
15
|
+
// ships into the plan gate's browser bundle, concatenated with the other plan
|
|
16
|
+
// modules into a single IIFE — so module-scope helpers use an `sb`/`SB_` prefix
|
|
17
|
+
// to stay unique across that shared scope.
|
|
18
|
+
|
|
19
|
+
import { validateSlideUses } from "../lib/image-intent.mjs";
|
|
8
20
|
|
|
9
21
|
export const STORYBOARD_CANVAS_LANG = "storyboard-canvas";
|
|
10
|
-
export const STORYBOARD_WIDTH = 1600;
|
|
11
|
-
export const STORYBOARD_HEIGHT = 900;
|
|
12
|
-
|
|
13
|
-
export const BODY_SAFE_AREA = {
|
|
14
|
-
x: 80,
|
|
15
|
-
y: 110,
|
|
16
|
-
w: 1440,
|
|
17
|
-
h: 670,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const MIN_BLOCK_WIDTH = 180;
|
|
21
|
-
export const MIN_BLOCK_HEIGHT = 96;
|
|
22
|
-
export const MAX_BLOCKS = 8;
|
|
23
|
-
const SB_MAX_ID_LENGTH = 64;
|
|
24
|
-
export const MAX_DESIGN_ELEMENT_LENGTH = 56;
|
|
25
|
-
export const MAX_CONTENT_LENGTH = 28;
|
|
26
22
|
const SB_MAX_KEY_MESSAGE_LENGTH = 180;
|
|
27
|
-
const SB_MAX_VISUAL_FAMILY_LENGTH = 64;
|
|
28
23
|
const SB_MAX_LAYOUT_NAME_LENGTH = 64;
|
|
29
|
-
const SB_MAX_DEPTH_NOTES = 6;
|
|
30
|
-
const SB_MAX_DEPTH_NOTE_LENGTH = 220;
|
|
31
24
|
export const MAX_IMAGE_PROMPT_LENGTH = 240;
|
|
32
25
|
const SB_MAX_IMAGE_PROMPTS = 4;
|
|
33
26
|
const SB_IMAGE_PROMPT_KEY = /^image\d{0,2}$/;
|
|
@@ -45,64 +38,6 @@ function sbAsFiniteNumber(value, fallback) {
|
|
|
45
38
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
function sbClamp(value, min, max) {
|
|
49
|
-
return Math.min(Math.max(value, min), max);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sbNormalizeBlock(value, index, warnings) {
|
|
53
|
-
if (!sbIsRecord(value)) {
|
|
54
|
-
warnings.push(`Block ${index + 1} is not an object and was ignored.`);
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const id = sbAsTrimmedString(value.id, SB_MAX_ID_LENGTH) || `block-${index + 1}`;
|
|
59
|
-
const designElement = sbAsTrimmedString(
|
|
60
|
-
value.designElement,
|
|
61
|
-
MAX_DESIGN_ELEMENT_LENGTH,
|
|
62
|
-
);
|
|
63
|
-
const content = sbAsTrimmedString(value.content, MAX_CONTENT_LENGTH);
|
|
64
|
-
if (!designElement || !content) {
|
|
65
|
-
warnings.push(
|
|
66
|
-
`Block "${id}" needs both designElement and content and was ignored.`,
|
|
67
|
-
);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const minW = MIN_BLOCK_WIDTH;
|
|
72
|
-
const minH = MIN_BLOCK_HEIGHT;
|
|
73
|
-
const maxW = BODY_SAFE_AREA.w;
|
|
74
|
-
const maxH = BODY_SAFE_AREA.h;
|
|
75
|
-
const w = sbClamp(sbAsFiniteNumber(value.w, 360), minW, maxW);
|
|
76
|
-
const h = sbClamp(sbAsFiniteNumber(value.h, 150), minH, maxH);
|
|
77
|
-
const x = sbClamp(
|
|
78
|
-
sbAsFiniteNumber(value.x, BODY_SAFE_AREA.x),
|
|
79
|
-
BODY_SAFE_AREA.x,
|
|
80
|
-
BODY_SAFE_AREA.x + BODY_SAFE_AREA.w - w,
|
|
81
|
-
);
|
|
82
|
-
const y = sbClamp(
|
|
83
|
-
sbAsFiniteNumber(value.y, BODY_SAFE_AREA.y),
|
|
84
|
-
BODY_SAFE_AREA.y,
|
|
85
|
-
BODY_SAFE_AREA.y + BODY_SAFE_AREA.h - h,
|
|
86
|
-
);
|
|
87
|
-
const emphasis =
|
|
88
|
-
value.emphasis === "primary" ||
|
|
89
|
-
value.emphasis === "secondary" ||
|
|
90
|
-
value.emphasis === "tertiary"
|
|
91
|
-
? value.emphasis
|
|
92
|
-
: undefined;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
id,
|
|
96
|
-
x,
|
|
97
|
-
y,
|
|
98
|
-
w,
|
|
99
|
-
h,
|
|
100
|
-
...(emphasis ? { emphasis } : {}),
|
|
101
|
-
designElement,
|
|
102
|
-
content,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
41
|
export function parseStoryboardCanvas(raw) {
|
|
107
42
|
let parsed;
|
|
108
43
|
try {
|
|
@@ -129,33 +64,8 @@ export function parseStoryboardCanvas(raw) {
|
|
|
129
64
|
}
|
|
130
65
|
|
|
131
66
|
const slide = Math.max(1, Math.round(sbAsFiniteNumber(parsed.slide, 1)));
|
|
132
|
-
const rawBlocks = Array.isArray(parsed.blocks) ? parsed.blocks : [];
|
|
133
|
-
if (!Array.isArray(parsed.blocks)) {
|
|
134
|
-
warnings.push("Storyboard blocks must be an array.");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const blocks = rawBlocks
|
|
138
|
-
.slice(0, MAX_BLOCKS)
|
|
139
|
-
.map((block, index) => sbNormalizeBlock(block, index, warnings))
|
|
140
|
-
.filter((block) => block !== null);
|
|
141
|
-
|
|
142
|
-
if (rawBlocks.length > MAX_BLOCKS) {
|
|
143
|
-
warnings.push(`Only the first ${MAX_BLOCKS} storyboard blocks are shown.`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// An empty block list is a valid storyboard: a cover or closing slide whose
|
|
147
|
-
// layout carries the text itself has no content blocks. Rejecting it dropped
|
|
148
|
-
// the slide from concept generation — and shifted the server's slideCount so
|
|
149
|
-
// the LAST slide fell off the request list too.
|
|
150
|
-
if (rawBlocks.length > 0 && blocks.length === 0) {
|
|
151
|
-
warnings.push("No storyboard block was valid; the canvas is empty.");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
67
|
const keyMessage = sbAsTrimmedString(parsed.keyMessage, SB_MAX_KEY_MESSAGE_LENGTH);
|
|
155
|
-
const
|
|
156
|
-
parsed.visualFamily,
|
|
157
|
-
SB_MAX_VISUAL_FAMILY_LENGTH,
|
|
158
|
-
);
|
|
68
|
+
const continues = Math.round(sbAsFiniteNumber(parsed.continues, 0));
|
|
159
69
|
const layout = sbAsTrimmedString(parsed.layout, SB_MAX_LAYOUT_NAME_LENGTH);
|
|
160
70
|
const selectionFields = {};
|
|
161
71
|
if (parsed.role === "body" || parsed.role === "frame") selectionFields.role = parsed.role;
|
|
@@ -179,24 +89,54 @@ export function parseStoryboardCanvas(raw) {
|
|
|
179
89
|
const rawContentNotes = Array.isArray(parsed.contentNotes)
|
|
180
90
|
? parsed.contentNotes
|
|
181
91
|
: parsed.depthNotes;
|
|
92
|
+
// The slide's content is never shortened here: every concept prompt and
|
|
93
|
+
// worker brief reads it from this parse, and a cut note reached the image
|
|
94
|
+
// model mid-word (x80–x87: 195 of 606 notes were over the old 220 cap).
|
|
182
95
|
const contentNotes = Array.isArray(rawContentNotes)
|
|
183
96
|
? rawContentNotes
|
|
184
|
-
.
|
|
185
|
-
.map((note) => sbAsTrimmedString(note, SB_MAX_DEPTH_NOTE_LENGTH))
|
|
97
|
+
.map((note) => (typeof note === "string" ? note.trim() : ""))
|
|
186
98
|
.filter(Boolean)
|
|
187
99
|
: undefined;
|
|
100
|
+
// The slide's full content draft, written from the approved key points after
|
|
101
|
+
// sign-off: concept generation and authoring build from it.
|
|
102
|
+
const content = Array.isArray(parsed.content)
|
|
103
|
+
? parsed.content
|
|
104
|
+
.map((line) => (typeof line === "string" ? line.trim() : ""))
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
// The one field normalization may NOT touch. Everything else in this parser
|
|
109
|
+
// is defensive — plan.md is human-edited, so an unreadable field is dropped
|
|
110
|
+
// and the record still opens. An image use is an APPROVED OBLIGATION: a
|
|
111
|
+
// malformed one has to surface as an error, because dropping it is exactly
|
|
112
|
+
// how a picture the user asked for disappears between the plan and the .pptx
|
|
113
|
+
// (implementation-guide §1). The contract's own strict parser decides;
|
|
114
|
+
// this module adds no image rule of its own.
|
|
115
|
+
const uses = validateSlideUses(parsed.assetUses, { slide });
|
|
116
|
+
if (!uses.value) {
|
|
117
|
+
return {
|
|
118
|
+
data: null,
|
|
119
|
+
warnings,
|
|
120
|
+
error: uses.issues[0]?.message ?? "assetUses could not be read.",
|
|
121
|
+
issues: uses.issues,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
188
124
|
|
|
189
125
|
return {
|
|
190
126
|
data: {
|
|
191
127
|
version: 1,
|
|
192
128
|
slide,
|
|
193
|
-
|
|
194
|
-
...(visualFamily ? { visualFamily } : {}),
|
|
129
|
+
...(continues >= 1 && continues < slide ? { continues } : {}),
|
|
195
130
|
...(layout ? { layout } : {}),
|
|
196
131
|
...selectionFields,
|
|
197
132
|
...(keyMessage ? { keyMessage } : {}),
|
|
198
133
|
...(contentNotes?.length ? { contentNotes } : {}),
|
|
134
|
+
...(content?.length ? { content } : {}),
|
|
199
135
|
...(Object.keys(imagePrompts).length > 0 ? { imagePrompts } : {}),
|
|
136
|
+
// Empty means "no uses", which is the absence of the key — a slide that
|
|
137
|
+
// must CLEAR an obligation does it through the targeted-edit override
|
|
138
|
+
// file, not through an empty array in the plan.
|
|
139
|
+
...(uses.value.length ? { assetUses: uses.value } : {}),
|
|
200
140
|
},
|
|
201
141
|
warnings,
|
|
202
142
|
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The reader marks texts that read as ONE unit with a shared `block` id — a
|
|
2
|
+
// title and the description under it, or the pieces of one sentence painted
|
|
3
|
+
// in two styles ("under **21** days"). Such a block becomes one text element
|
|
4
|
+
// whose members are its `paragraphs`: the transcriber writes one box whose
|
|
5
|
+
// paragraphs stack in flow on the design's own line-height (the image only
|
|
6
|
+
// places the block), settle fits and moves it as one unit, and the export
|
|
7
|
+
// ships one text frame. Every paragraph keeps its role, weight and paint.
|
|
8
|
+
// Pieces set side by side on one line are ONE paragraph: the piece with the
|
|
9
|
+
// most words gives it its style and every piece painted otherwise is a styled
|
|
10
|
+
// `run` of it (an accent figure, a bold term) — x109 GTM 4 shipped a scorecard
|
|
11
|
+
// sentence as five boxes with gaps between them, and x107–x109 left 8–11 such
|
|
12
|
+
// blocks per run as separate texts. A sentence the reader broke at a run and
|
|
13
|
+
// at its wraps is joined again across those lines: a line of pieces and the
|
|
14
|
+
// line before or after it in the same style are one paragraph.
|
|
15
|
+
// A list item is a paragraph with `marker` (concept-lists.mjs): a title over
|
|
16
|
+
// its items is one block, written as one text box whose items are one native
|
|
17
|
+
// list. A block stays as its separate texts only when it cannot be written as
|
|
18
|
+
// one — a member a line glues to, rotated or self-painted text, members in
|
|
19
|
+
// different containers, pieces set apart by a gutter (a name and its value in
|
|
20
|
+
// two columns) — and the fold says so in a note: one sentence is never
|
|
21
|
+
// silently left in several boxes.
|
|
22
|
+
import { recordBlockDividers } from "./block-dividers.mjs";
|
|
23
|
+
|
|
24
|
+
export const PARAGRAPH_KEYS = ["kind", "x", "text", "role", "weight", "color", "colorHex", "colorToken", "ground", "groundHex", "align", "leadIn", "emphasis", "runs", "marker", "markerToken", "markerHex"];
|
|
25
|
+
|
|
26
|
+
// Pieces of one line further apart than this many line heights are two
|
|
27
|
+
// columns (a depot name and its share, 118 px apart), not one sentence (the
|
|
28
|
+
// gaps the reader measures between the pieces of a sentence: 2–16 px).
|
|
29
|
+
const RUN_GAP_LINES = 1.5;
|
|
30
|
+
|
|
31
|
+
const lineHeight = (m) => m.h / Math.max(1, String(m.text).split("\n").length);
|
|
32
|
+
const style = (m) => JSON.stringify([m.role ?? (m.kind === "number" ? "number" : null), m.weight ?? null, m.colorToken ?? null, m.colorHex ?? null, m.color ?? null]);
|
|
33
|
+
const paragraphOf = (m) => Object.fromEntries([["id", m.id], ...PARAGRAPH_KEYS.filter((k) => m[k] != null).map((k) => [k, m[k]])]);
|
|
34
|
+
const flatText = (t) => String(t).replace(/\s*\n\s*/g, " ").trim();
|
|
35
|
+
// No space after a lone opening mark, none before closing punctuation.
|
|
36
|
+
const joinText = (a, b) => (!a ? b : !b ? a : /^[(\[“‘"'«]$/.test(a.trim()) || /^[.,;:!?%)\]”’»]/.test(b) ? `${a}${b}` : `${a} ${b}`);
|
|
37
|
+
// The next painted line of the same sentence: an indented line continues the
|
|
38
|
+
// paragraph (concept-text.mjs textLines), and the break keeps it from being
|
|
39
|
+
// written as one unwrappable line.
|
|
40
|
+
const joinLines = (a, b) => (/^[.,;:!?%)\]”’»]/.test(b) ? `${a}${b}` : `${a}\n ${b}`);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The block's members in reading order: rows top to bottom (a member whose
|
|
44
|
+
* middle lies within a row's band sits on that row), each row left to right.
|
|
45
|
+
* The reader declared the block; this only orders what it named.
|
|
46
|
+
*/
|
|
47
|
+
export function readingRows(members) {
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const m of [...members].sort((a, b) => a.y - b.y || a.x - b.x)) {
|
|
50
|
+
const row = rows.at(-1);
|
|
51
|
+
const mid = m.y + m.h / 2;
|
|
52
|
+
if (row && mid >= Math.min(...row.map((r) => r.y)) && mid <= Math.max(...row.map((r) => r.y + r.h))) row.push(m);
|
|
53
|
+
else rows.push([m]);
|
|
54
|
+
}
|
|
55
|
+
return rows.map((row) => row.sort((a, b) => a.x - b.x));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Why these rows cannot be one text element, or null. */
|
|
59
|
+
function rowsProblem(rows) {
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
for (let i = 1; i < row.length; i++) {
|
|
62
|
+
const a = row[i - 1];
|
|
63
|
+
const b = row[i];
|
|
64
|
+
const gap = b.x - (a.x + a.w);
|
|
65
|
+
if (gap > RUN_GAP_LINES * Math.max(lineHeight(a), lineHeight(b))) return "its pieces on one line are set apart by a gutter, two columns rather than one sentence";
|
|
66
|
+
if (gap < -0.5 * Math.min(lineHeight(a), lineHeight(b))) return "two of its pieces are painted over each other";
|
|
67
|
+
}
|
|
68
|
+
if (row.length > 1 && row.some((m) => m.marker || m.paragraphs)) return "a list item shares its line with other pieces";
|
|
69
|
+
}
|
|
70
|
+
const extent = (row) => [Math.min(...row.map((m) => m.x)), Math.max(...row.map((m) => m.x + m.w))];
|
|
71
|
+
for (let i = 1; i < rows.length; i++) {
|
|
72
|
+
const [a0, a1] = extent(rows[i - 1]);
|
|
73
|
+
const [b0, b1] = extent(rows[i]);
|
|
74
|
+
if (!(b0 < a1 && a0 < b1)) return "its lines do not stack in one column";
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const bounds = (ms) => {
|
|
80
|
+
const x = Math.min(...ms.map((m) => m.x));
|
|
81
|
+
const y = Math.min(...ms.map((m) => m.y));
|
|
82
|
+
return { x, y, w: Math.max(...ms.map((m) => m.x + m.w)) - x, h: Math.max(...ms.map((m) => m.y + m.h)) - y };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** One unit per row: its paragraphs, its box, and the style a neighbouring line must share to continue it. */
|
|
86
|
+
function rowUnit(row) {
|
|
87
|
+
if (row.length === 1) {
|
|
88
|
+
const [m] = row;
|
|
89
|
+
return { ...bounds(row), paras: m.paragraphs ?? [paragraphOf(m)], style: m.paragraphs ? null : style(m), pieces: false };
|
|
90
|
+
}
|
|
91
|
+
// The piece with the most words styles the paragraph; every other style is a run.
|
|
92
|
+
const base = row.reduce((best, m) => (flatText(m.text).length > flatText(best.text).length ? m : best));
|
|
93
|
+
const runs = row.filter((m) => style(m) !== style(base)).map((m) => Object.fromEntries(
|
|
94
|
+
Object.entries({ text: flatText(m.text), kind: m.kind, role: m.role, weight: m.weight, color: m.color, colorHex: m.colorHex, colorToken: m.colorToken }).filter(([, v]) => v != null),
|
|
95
|
+
));
|
|
96
|
+
const para = { ...paragraphOf(base), x: row[0].x, text: row.reduce((t, m) => joinText(t, String(m.text).trim()), "") };
|
|
97
|
+
if (base !== row[0]) delete para.leadIn;
|
|
98
|
+
if (runs.length) para.runs = runs;
|
|
99
|
+
return { ...bounds(row), paras: [para], style: style(base), pieces: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const WORDS = new Set(["text", "number"]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The indent a paragraph of `block` is written with, off the block's left
|
|
106
|
+
* edge: only structure, never alignment. A centred or right-aligned line has
|
|
107
|
+
* no left edge of its own — its painted x is where its width put it — so an
|
|
108
|
+
* indent would only shift the axis it is centred on (x110 GTM 3: "412" set
|
|
109
|
+
* `algn=ctr` with a 33 pt `marL`, off its tile's centre, which users had to
|
|
110
|
+
* undo by hand; 33–57 such paragraphs per run from x107 on). A list keeps its
|
|
111
|
+
* words' offset: the marker hangs in it. A plain left line keeps its offset
|
|
112
|
+
* only where a drawing of the same container ends in the strip it clears — the
|
|
113
|
+
* disc or icon a title stands beside; any other offset is the painting's noise
|
|
114
|
+
* and the line starts at the block's edge.
|
|
115
|
+
*/
|
|
116
|
+
export function paragraphIndent(para, block, elements) {
|
|
117
|
+
const offset = Math.round((para.x ?? block.x) - block.x);
|
|
118
|
+
if (offset <= 1 || (para.align ?? "left") !== "left") return 0;
|
|
119
|
+
if (para.marker) return offset;
|
|
120
|
+
const clears = elements.some((d) => d.parent === block.parent && d.id !== block.id && !WORDS.has(d.kind)
|
|
121
|
+
&& d.x + d.w > block.x - 1 && d.x + d.w <= block.x + offset + 2
|
|
122
|
+
&& d.y < block.y + block.h && d.y + d.h > block.y);
|
|
123
|
+
return clears ? offset : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A line of pieces and the line before or after it in the same style are one paragraph. */
|
|
127
|
+
function joinSentences(units) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const u of units) {
|
|
130
|
+
const prev = out.at(-1);
|
|
131
|
+
const a = prev?.paras.length === 1 ? prev.paras[0] : null;
|
|
132
|
+
const b = u.paras.length === 1 ? u.paras[0] : null;
|
|
133
|
+
if (a && b && prev.style && prev.style === u.style && (prev.pieces || u.pieces) && !a.marker && !b.marker) {
|
|
134
|
+
a.text = joinLines(String(a.text).trim(), String(b.text).trim());
|
|
135
|
+
const runs = [...(a.runs ?? []), ...(b.runs ?? [])];
|
|
136
|
+
if (runs.length) a.runs = runs;
|
|
137
|
+
const emphasis = [...(a.emphasis ?? []), ...(b.emphasis ?? [])];
|
|
138
|
+
if (emphasis.length) a.emphasis = emphasis;
|
|
139
|
+
Object.assign(prev, bounds([prev, u]), { pieces: true });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
out.push(u);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function foldTextBlocks(elements, { connectors = [], notes = null } = {}) {
|
|
148
|
+
const referenced = new Set(connectors.flatMap((c) => [c.from, c.to]));
|
|
149
|
+
for (const e of elements) {
|
|
150
|
+
for (const end of [e.strokeEnds?.start, e.strokeEnds?.end]) if (end?.element) referenced.add(end.element);
|
|
151
|
+
}
|
|
152
|
+
const parents = new Set(elements.map((e) => e.parent).filter(Boolean));
|
|
153
|
+
const writable = (e) => (e.kind === "text" || e.kind === "number") && e.text != null
|
|
154
|
+
&& !e.textRotation && !(e.fill && e.fill !== "none") && !referenced.has(e.id) && !parents.has(e.id);
|
|
155
|
+
const blocks = new Map();
|
|
156
|
+
for (const e of elements) {
|
|
157
|
+
if (!e.block) continue;
|
|
158
|
+
if (!blocks.has(e.block)) blocks.set(e.block, []);
|
|
159
|
+
blocks.get(e.block).push(e);
|
|
160
|
+
}
|
|
161
|
+
const ids = new Set(elements.map((e) => e.id));
|
|
162
|
+
const replace = new Map();
|
|
163
|
+
for (const [block, members] of blocks) {
|
|
164
|
+
if (members.length < 2) continue;
|
|
165
|
+
const rows = readingRows(members);
|
|
166
|
+
const problem = !members.every(writable) ? "a member is a line's end, a container, rotated or painted"
|
|
167
|
+
: !members.every((c) => c.parent === members[0].parent) ? "its members sit in different containers"
|
|
168
|
+
: rowsProblem(rows);
|
|
169
|
+
if (problem) {
|
|
170
|
+
notes?.push(`text block ${block} kept as ${members.length} texts — ${problem}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
let id = block;
|
|
174
|
+
while (ids.has(id)) id = `${id}-text`;
|
|
175
|
+
ids.add(id);
|
|
176
|
+
const units = joinSentences(rows.map(rowUnit));
|
|
177
|
+
const paragraphs = units.flatMap((u) => u.paras);
|
|
178
|
+
const box = bounds(members);
|
|
179
|
+
const aligns = new Set(members.map((c) => c.align ?? "left"));
|
|
180
|
+
const first = rows[0][0];
|
|
181
|
+
// The box's own style: its first member's, or the paragraph its first line of pieces makes.
|
|
182
|
+
const lead = rows[0].length === 1 ? first : paragraphs[0];
|
|
183
|
+
const folded = {
|
|
184
|
+
id,
|
|
185
|
+
kind: "text",
|
|
186
|
+
parent: first.parent ?? null,
|
|
187
|
+
...box,
|
|
188
|
+
text: paragraphs.map((p) => String(p.text)).join("\n"),
|
|
189
|
+
textSource: "visible",
|
|
190
|
+
role: lead.role ?? null,
|
|
191
|
+
weight: lead.weight ?? null,
|
|
192
|
+
color: lead.color ?? null,
|
|
193
|
+
colorHex: lead.colorHex ?? null,
|
|
194
|
+
colorToken: lead.colorToken ?? null,
|
|
195
|
+
align: aligns.size === 1 ? [...aligns][0] : "left",
|
|
196
|
+
// One seat for the block where its members agree (seat.mjs).
|
|
197
|
+
...(first.seat && members.every((c) => JSON.stringify(c.seat ?? null) === JSON.stringify(first.seat)) ? { seat: first.seat } : {}),
|
|
198
|
+
// A member that is already a block of its own (a list read as one
|
|
199
|
+
// element) brings its paragraphs along.
|
|
200
|
+
paragraphs,
|
|
201
|
+
};
|
|
202
|
+
if ("ground" in first) folded.ground = first.ground;
|
|
203
|
+
if ("groundHex" in first) folded.groundHex = first.groundHex;
|
|
204
|
+
// A rule drawn between two of the lines rides with that paragraph boundary (block-dividers.mjs).
|
|
205
|
+
const dividers = recordBlockDividers(units, connectors, (u) => u.paras.length);
|
|
206
|
+
if (dividers.length) folded.dividers = dividers;
|
|
207
|
+
replace.set(first.id, folded);
|
|
208
|
+
for (const c of members) if (c !== first) replace.set(c.id, null);
|
|
209
|
+
}
|
|
210
|
+
if (!replace.size) return elements;
|
|
211
|
+
return elements.flatMap((e) => (replace.has(e.id) ? (replace.get(e.id) ? [replace.get(e.id)] : []) : [e]));
|
|
212
|
+
}
|