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.
Files changed (186) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,270 @@
1
+ // Lists. The reader declares each list the design draws in its top-level
2
+ // `lists` — items with a mark painted in front of every one, the mark's kind
3
+ // (bullet | check | cross | number) and its paint — and the mapping writes
4
+ // `marker` (+ `markerToken`) onto exactly those items (concept-list-decl.ts).
5
+ // No text is an item because a field defaulted to "bullet" (x110 GTM 3: all
6
+ // 25 texts marked, every KPI label bulleted). Older reads typed the
7
+ // glyph into the text ("• Revenue grew") or listed the mark beside the line
8
+ // as a disc, a tick stroke or an icon; a live read still does both now and
9
+ // then (x102: declared `check` items AND their tick strokes). Each of those is
10
+ // the same declaration in another spelling, and all end here as one thing: a
11
+ // text element with `marker` (+ `markerToken`), its words without the glyph,
12
+ // no mark drawn as an element, the items of one list sharing one `block`.
13
+ // foldTextBlocks then makes the block one element with `paragraphs`, the
14
+ // transcriber writes the items as one native list (list-jsx.mjs) and the
15
+ // export ships native bullets — never a typed glyph, never a mark shape.
16
+ import { bulletItems, markerOfLine, stripMarker, textLines } from "./concept-text.mjs";
17
+ import { glyphMarkOf } from "./concept-marks.mjs";
18
+
19
+ const MARKER_KINDS = new Set(["bullet", "check", "cross", "number"]);
20
+ // A dot drawn as its own shape is at most this big (slot px) and no taller
21
+ // than the line it marks; anything larger is a disc of the design.
22
+ const DOT_MAX_PX = 12;
23
+ // A mark of a few strokes (a tick, a dash) or a glyph icon is at most this big.
24
+ const MARK_MAX_PX = 28;
25
+ // How far a mark may stand left of its item's words.
26
+ const MARK_GAP_PX = 40;
27
+ const DOT_SHAPES = new Set([null, undefined, "circle", "ellipse", "rounded", "rect"]);
28
+ const ITEM_KEYS = ["role", "weight", "color", "colorHex", "colorToken", "ground", "groundHex", "align", "markerToken", "markerHex"];
29
+
30
+ const isText = (e) => e.kind === "text" && typeof e.text === "string" && e.text.trim();
31
+ const wordless = (e) => e.text == null || e.textSource !== "visible";
32
+ const small = (e, max) => e.w > 0 && e.h > 0 && e.w <= max && e.h <= max;
33
+
34
+ // An element whose every line opens with the same glyph holds a whole list:
35
+ // it becomes its own block, one paragraph per item.
36
+ function splitItems(e, list, marker) {
37
+ e.paragraphs = list.items.map((text, i) => ({
38
+ id: `${e.id}-${i + 1}`, kind: "text", text, marker,
39
+ ...Object.fromEntries(ITEM_KEYS.filter((k) => e[k] != null).map((k) => [k, e[k]])),
40
+ }));
41
+ e.text = list.items.join("\n");
42
+ delete e.marker;
43
+ }
44
+
45
+ // A heading is never an item: the reader declares the structure of a block
46
+ // with `role` (a title over its items), and a marker on the title contradicts
47
+ // it (x105: five card titles read as bullet items, the first draft bulleted
48
+ // them, the agent flattened the whole list). The heading keeps its words
49
+ // without any typed glyph and without a marker.
50
+ const HEADING_ROLES = new Set(["title", "section", "subtitle"]);
51
+
52
+ // What the reader wrote on and into the texts: a declared marker, or the
53
+ // glyph it typed, becomes `marker` and leaves the words.
54
+ function declareFromText(elements) {
55
+ for (const e of elements) {
56
+ if (e.marker != null && (e.kind !== "text" || !MARKER_KINDS.has(e.marker) || HEADING_ROLES.has(e.role))) {
57
+ delete e.marker;
58
+ delete e.markerToken;
59
+ delete e.markerHex;
60
+ }
61
+ if (e.markerToken != null && !e.marker) delete e.markerToken;
62
+ if (!isText(e) || e.paragraphs) continue;
63
+ if (HEADING_ROLES.has(e.role)) {
64
+ const lines = textLines(e.text).map((l) => (markerOfLine(l) ? stripMarker(l, markerOfLine(l)) : l));
65
+ if (lines.every((l) => l.trim())) e.text = lines.join("\n");
66
+ continue;
67
+ }
68
+ const list = bulletItems(e.text);
69
+ if (list) { splitItems(e, list, e.marker ?? markerOfLine(`${list.marker} x`)); continue; }
70
+ const lines = textLines(e.text);
71
+ const typed = markerOfLine(lines[0]);
72
+ const marker = e.marker ?? typed;
73
+ if (!marker) continue;
74
+ if (typed || marker === "number") lines[0] = stripMarker(lines[0], marker);
75
+ if (!lines[0].trim()) continue;
76
+ e.text = lines.join("\n");
77
+ e.marker = marker;
78
+ }
79
+ }
80
+
81
+ // The glyph a mark element draws — a tick, a cross, a dot, a few strokes —
82
+ // as a marker kind, else null (a pictogram, a carrier, a shape of the design).
83
+ function glyphOf(e) {
84
+ if (e.kind === "icon") {
85
+ const mark = glyphMarkOf(e.icon?.query, e.icon?.keywords ?? []);
86
+ if (mark === "check" || mark === "cross") return mark;
87
+ if (mark === "dots") return "bullet";
88
+ const query = String(e.icon?.query ?? "").toLowerCase().replace(/\b(?:icon|outline|outlined|small|blue|black|white|green|glyph|symbol)\b/g, "").replace(/\s+/g, " ").trim();
89
+ return /^(?:check(?: ?mark)?|tick(?: mark)?)$/.test(query) ? "check" : /^(?:bullet|bullet point|dot)$/.test(query) ? "bullet" : null;
90
+ }
91
+ if (e.kind === "path" || e.kind === "rule") return wordless(e) && small(e, MARK_MAX_PX) ? "bullet" : null;
92
+ if (["disc", "shape", "badge"].includes(e.kind)) return wordless(e) && small(e, DOT_MAX_PX) && DOT_SHAPES.has(e.shape) ? "bullet" : null;
93
+ return null;
94
+ }
95
+
96
+ // The mark's paint, for the item's native bullet.
97
+ const paintOf = (e) => ({
98
+ token: (e.kind === "disc" || e.kind === "shape" || e.kind === "badge" ? e.fillToken : e.colorToken ?? e.stroke?.colorToken) ?? null,
99
+ hex: (e.kind === "disc" || e.kind === "shape" || e.kind === "badge" ? e.fillHex : e.colorHex ?? e.stroke?.colorHex) ?? null,
100
+ });
101
+
102
+ // Does `e` stand directly in front of the first line of `text`?
103
+ const inFront = (e, text, gap = MARK_GAP_PX) => text.x >= e.x + e.w - 2 && text.x - e.x - e.w <= gap
104
+ && e.y + e.h / 2 >= text.y - 4 && e.y + e.h / 2 <= text.y + Math.max(Math.min(text.h, 24), 12) + 4;
105
+
106
+ // The element the reader drew in front of each declared item. A glyph — a
107
+ // tick stroke, a dot, a "check" icon — IS the item's marker: it goes, its
108
+ // paint stays on the item. A painted carrier (a numbered badge, a disc with a
109
+ // pictogram) or a pictogram is an icon row: it stays, and the item's marker
110
+ // goes instead, so nothing is marked twice.
111
+ function foldDeclaredMarks(elements, linked, used) {
112
+ const byParent = new Map();
113
+ for (const e of elements) {
114
+ const key = e.parent ?? "";
115
+ if (!byParent.has(key)) byParent.set(key, []);
116
+ byParent.get(key).push(e);
117
+ }
118
+ const hasKids = new Set(elements.map((e) => e.parent).filter(Boolean));
119
+ for (const item of elements) {
120
+ if (!item.marker || !isText(item) || item.paragraphs) continue;
121
+ const marks = (byParent.get(item.parent ?? "") ?? []).filter((e) => e !== item && !isText(e) && e.kind !== "text" && !linked.has(e.id) && !used.has(e.id)
122
+ && !e.image?.assetUseId && (e.kind === "image" || small(e, 48)) && inFront(e, item));
123
+ if (!marks.length) continue;
124
+ const mark = marks.sort((a, b) => (item.x - b.x - b.w) - (item.x - a.x - a.w))[0];
125
+ const glyph = glyphOf(mark);
126
+ if (glyph && !hasKids.has(mark.id)) {
127
+ const paint = paintOf(mark);
128
+ if (item.markerToken == null && paint.token) item.markerToken = paint.token;
129
+ if (item.markerHex == null && paint.hex) item.markerHex = paint.hex;
130
+ used.add(mark.id);
131
+ } else {
132
+ delete item.marker;
133
+ delete item.markerToken;
134
+ delete item.markerHex;
135
+ }
136
+ }
137
+ }
138
+
139
+ // The pairs the reader declared as rows of one set: a dot and its text.
140
+ function declaredPairs(repeats) {
141
+ const pairs = new Set();
142
+ for (const g of repeats ?? []) for (const m of g.members ?? []) {
143
+ if (!Array.isArray(m)) continue;
144
+ for (const a of m) for (const b of m) if (a !== b) pairs.add(`${a}>${b}`);
145
+ }
146
+ return pairs;
147
+ }
148
+
149
+ // A tick or cross glyph beside a text the reader did NOT declare an item: a
150
+ // check or cross marker when the two are one row of a declared set, or the
151
+ // parent repeats the same mark — same glyph, same paint — before another line.
152
+ // A tick or a cross before a line is a list's mark by what it is. A dot is
153
+ // not: equal dots before lines are a timeline's stations, a legend's swatches
154
+ // or status lights as often as bullets (x110 GTM 3: the handoff timeline's two
155
+ // dots made its two lines a bulleted list), so a dot is a bullet only where
156
+ // the reader declared the list (`lists`) and otherwise stays a shape.
157
+ function foldUndeclaredDots(elements, linked, used, repeats) {
158
+ const byId = new Map(elements.map((e) => [e.id, e]));
159
+ const declared = declaredPairs(repeats);
160
+ const pairs = [];
161
+ for (const dot of elements) {
162
+ if (used.has(dot.id) || linked.has(dot.id) || dot.image?.assetUseId) continue;
163
+ const glyph = dot.kind === "path" || dot.kind === "rule" ? null : glyphOf(dot);
164
+ if (glyph !== "check" && glyph !== "cross") continue;
165
+ if (dot.kind === "icon" && !small(dot, MARK_MAX_PX)) continue;
166
+ // A marker may sit in its own small painted carrier. Keep that shape,
167
+ // but compare the adjacent copy against the carrier's enclosing list.
168
+ const carrier = byId.get(dot.parent);
169
+ const inCarrier = carrier && !linked.has(carrier.id) && !carrier.text?.trim() &&
170
+ ["badge", "disc"].includes(carrier.kind) && carrier.w <= 32 && carrier.h <= 32 &&
171
+ dot.x >= carrier.x && dot.y >= carrier.y &&
172
+ dot.x + dot.w <= carrier.x + carrier.w && dot.y + dot.h <= carrier.y + carrier.h;
173
+ const parent = inCarrier ? carrier.parent : dot.parent;
174
+ const candidates = elements.filter((e) => isText(e) && !e.paragraphs && !e.marker && e.parent === parent && !linked.has(e.id)
175
+ && inFront(dot, e, 24) && dot.h <= e.h + 2);
176
+ if (candidates.length === 1) pairs.push({ dot, text: candidates[0], glyph, parent, carrier: inCarrier ? carrier : null });
177
+ }
178
+ for (const pair of pairs) {
179
+ const { dot, text, glyph } = pair;
180
+ if (pairs.filter((p) => p.text.id === text.id).length !== 1) continue;
181
+ const paint = (d) => d.fillToken ?? d.fillHex ?? d.fill ?? null;
182
+ const alike = pairs.filter((p) => p.parent === pair.parent && p.glyph === glyph && paint(p.dot) === paint(dot)).length >= 2;
183
+ if (!declared.has(`${dot.id}>${text.id}`) && !alike) continue;
184
+ if (pair.carrier) {
185
+ // A painted badge owns its glyph. Native shape text preserves both
186
+ // its center and its colour; an outside list marker would drift away.
187
+ Object.assign(pair.carrier, { text: glyph === "check" ? "✓" : "✗", textSource: "visible",
188
+ role: "body", colorToken: dot.colorToken, colorHex: dot.colorHex,
189
+ color: dot.icon?.color === "white" ? "light" : dot.icon?.color ?? dot.color,
190
+ align: "center", valign: "middle", placement: "center" });
191
+ } else {
192
+ text.marker = glyph;
193
+ const own = paintOf(dot);
194
+ if (own.token) text.markerToken = own.token;
195
+ if (own.hex) text.markerHex = own.hex;
196
+ }
197
+ used.add(dot.id);
198
+ }
199
+ }
200
+
201
+ // The horizontal dividers the reader drew: rules still read as elements, and
202
+ // the free lines applyLines made of them (`fromPoint`/`toPoint`, `points`).
203
+ function dividers(elements, connectors) {
204
+ const out = [];
205
+ for (const e of elements) if (e.kind === "rule" && e.w > e.h) out.push({ x: e.x, w: e.w, y: e.y + e.h / 2 });
206
+ for (const c of connectors) {
207
+ const a = c.fromPoint ?? c.points?.[0];
208
+ const b = c.toPoint ?? c.points?.at(-1);
209
+ if (!a || !b || c.from || c.to || Math.abs(a.y - b.y) > 2) continue;
210
+ out.push({ x: Math.min(a.x, b.x), w: Math.abs(b.x - a.x), y: (a.y + b.y) / 2 });
211
+ }
212
+ return out;
213
+ }
214
+
215
+ // Consecutive items of one parent are one list: they share a block, so
216
+ // foldTextBlocks writes them as one element. The reader's own block ids win
217
+ // (a title with its items); an item it left alone joins the items around it.
218
+ // A divider the reader drew between two items ends the list (x105 GTM 03:
219
+ // three cells of a card, a rule between each, became one list in the first
220
+ // cell): items on either side of a rule are separate lists or cells.
221
+ function groupItems(elements, connectors) {
222
+ const rules = dividers(elements, connectors);
223
+ const divided = (a, b) => rules.some((r) => r.y > a.y + a.h - 1 && r.y < b.y + 1
224
+ && r.x < Math.max(a.x + a.w, b.x + b.w) && r.x + r.w > Math.min(a.x, b.x));
225
+ const ids = new Set(elements.map((e) => e.id));
226
+ const byParent = new Map();
227
+ for (const e of elements) {
228
+ if (!isText(e) || e.paragraphs) continue;
229
+ const key = e.parent ?? "";
230
+ if (!byParent.has(key)) byParent.set(key, []);
231
+ byParent.get(key).push(e);
232
+ }
233
+ for (const texts of byParent.values()) {
234
+ texts.sort((a, b) => a.y - b.y || a.x - b.x);
235
+ let run = [];
236
+ const close = () => {
237
+ if (run.length >= 2) {
238
+ let id = `${run[0].id}-list`;
239
+ while (ids.has(id)) id = `${id}-text`;
240
+ ids.add(id);
241
+ for (const e of run) e.block = id;
242
+ }
243
+ run = [];
244
+ };
245
+ for (const e of texts) {
246
+ const prev = run.at(-1);
247
+ const joins = prev && e.marker && !e.block && e.marker === prev.marker
248
+ && e.y >= prev.y + prev.h / 2 && e.x < prev.x + prev.w && prev.x < e.x + e.w && !divided(prev, e);
249
+ if (!joins) close();
250
+ if (e.marker && !e.block) run.push(e);
251
+ }
252
+ close();
253
+ }
254
+ return elements;
255
+ }
256
+
257
+ /**
258
+ * Every list the reader read, in one form: items carry `marker` (and the
259
+ * mark's paint), their words carry no glyph, no mark is an element, the
260
+ * items of one list share a block. Idempotent: a spec folded once is left as
261
+ * it is.
262
+ */
263
+ export function foldListMarkers(elements, connectors = [], repeats = []) {
264
+ const linked = new Set(connectors.flatMap((c) => [c.from, c.to]));
265
+ const used = new Set();
266
+ declareFromText(elements);
267
+ foldDeclaredMarks(elements, linked, used);
268
+ foldUndeclaredDots(elements, linked, used, repeats);
269
+ return groupItems(elements.filter((e) => !used.has(e.id)), connectors);
270
+ }
@@ -0,0 +1,84 @@
1
+ // Marks the transcriber DRAWS instead of fetching: plain stroke glyphs (tick,
2
+ // cross, chevrons, dots) and the arrow mark. Every stroke of one is a native
3
+ // line piece (the transcriber writes a mark as a chain group with round caps,
4
+ // the arrow as one line with its head); dots are small filled discs.
5
+ export const nameWords = (s) => (String(s).toLowerCase().match(/[a-z]+/g) ?? []).map((w) => (w.length > 3 && w.endsWith("s") && !/(ss|us|is)$/.test(w) ? w.slice(0, -1) : w));
6
+ export const ICON_STYLE_WORDS = new Set(['icon', 'outline', 'outlined', 'filled', 'thin', 'solid', 'symbol', 'sign', 'mark', 'glyph']);
7
+
8
+ // ---- Plain marks drawn as native geometry.
9
+ // Stroke marks in a unit box, as polylines; dots are circles.
10
+ const MARKS = {
11
+ check: { square: true, d: [[[0.14, 0.54], [0.4, 0.8], [0.86, 0.22]]] },
12
+ cross: { square: true, d: [[[0.22, 0.22], [0.78, 0.78]], [[0.78, 0.22], [0.22, 0.78]]] },
13
+ plus: { square: true, d: [[[0.5, 0.16], [0.5, 0.84]], [[0.16, 0.5], [0.84, 0.5]]] },
14
+ minus: { square: true, d: [[[0.16, 0.5], [0.84, 0.5]]] },
15
+ chevron: { square: true, d: [[[0.34, 0.14], [0.68, 0.5], [0.34, 0.86]]] },
16
+ "double chevron": { d: [[[0.18, 0.14], [0.48, 0.5], [0.18, 0.86]], [[0.52, 0.14], [0.82, 0.5], [0.52, 0.86]]] },
17
+ arrow: { d: [[[0.08, 0.5], [0.92, 0.5]], [[0.66, 0.22], [0.92, 0.5], [0.66, 0.78]]] },
18
+ dots: { dots: true },
19
+ };
20
+ const MARK_WORDS = {
21
+ check: "check checkmark tick", cross: "cross x close times multiply", plus: "plus add",
22
+ minus: "minus subtract", chevron: "chevron angle caret", arrow: "arrow", dots: "ellipsis dot more",
23
+ };
24
+ const MARK_MODIFIERS = new Set("double single right left up down forward back backward next previous horizontal vertical three small simple plain thin bold pointing straight".split(" "));
25
+ const TURN = { right: (x, y) => [x, y], left: (x, y) => [1 - x, y], up: (x, y) => [y, 1 - x], down: (x, y) => [y, x] };
26
+
27
+ /** Which plain mark an icon query names, or null for a picture of a thing. */
28
+ export function glyphMarkOf(query, keywords = []) {
29
+ const words = nameWords(String(query ?? "")).filter((w) => !ICON_STYLE_WORDS.has(w));
30
+ if (!words.length) return null;
31
+ const kinds = new Set();
32
+ for (const w of words) {
33
+ const kind = Object.keys(MARK_WORDS).find((k) => MARK_WORDS[k].split(" ").includes(w));
34
+ if (kind) kinds.add(kind);
35
+ else if (!MARK_MODIFIERS.has(w)) return null; // another object: a composite icon
36
+ }
37
+ // The reader's synonyms may name the mark when the query was descriptive.
38
+ if (!kinds.size) for (const k of keywords.flatMap((x) => nameWords(x))) {
39
+ const kind = Object.keys(MARK_WORDS).find((m) => MARK_WORDS[m].split(" ").includes(k));
40
+ if (kind) kinds.add(kind);
41
+ }
42
+ if (kinds.size !== 1) return null;
43
+ const [kind] = kinds;
44
+ if (kind === "dots" && !words.some((w) => w === "ellipsis" || w === "dot")) return null;
45
+ const dir = ["left", "up", "down"].find((d) => words.includes(d)) ?? (words.includes("back") || words.includes("backward") || words.includes("previous") ? "left" : "right");
46
+ return { kind: kind === "chevron" && words.includes("double") ? "double chevron" : kind, dir };
47
+ }
48
+
49
+ /**
50
+ * The strokes of a plain mark, in slot px: `{ kind, strokes: [[pt, …], …] }`
51
+ * (each stroke a polyline — the transcriber writes it as a chain of native
52
+ * line pieces), `{ kind: "arrow", strokes: [[tail, tip]] }` (one line, head at
53
+ * the tip) or `{ kind: "dots", dots: [{ x, y, r }] }`. Null for a picture.
54
+ */
55
+ export function glyphMarkStrokes(e) {
56
+ const mark = glyphMarkOf(e.icon?.query, e.icon?.keywords ?? []);
57
+ if (!mark || !(e.w > 0) || !(e.h > 0)) return null;
58
+ const spec = MARKS[mark.kind];
59
+ const r2 = (v) => Math.round(v * 100) / 100;
60
+ if (mark.kind === "arrow") {
61
+ const cx = e.x + e.w / 2, cy = e.y + e.h / 2;
62
+ const horizontal = mark.dir === "left" || mark.dir === "right";
63
+ const half = (horizontal ? e.w : e.h) * 0.42;
64
+ const sign = mark.dir === "left" || mark.dir === "up" ? -1 : 1;
65
+ const ends = horizontal
66
+ ? [{ x: cx - sign * half, y: cy }, { x: cx + sign * half, y: cy }]
67
+ : [{ x: cx, y: cy - sign * half }, { x: cx, y: cy + sign * half }];
68
+ return { kind: "arrow", strokes: [ends.map((p) => ({ x: r2(p.x), y: r2(p.y) }))] };
69
+ }
70
+ if (spec.dots) {
71
+ const r = r2(Math.min(e.h * 0.3, e.w / 10));
72
+ return { kind: "dots", dots: [1, 3, 5].map((k) => ({ x: r2(e.x + (e.w * k) / 6), y: r2(e.y + e.h / 2), r })) };
73
+ }
74
+ const side = Math.min(e.w, e.h);
75
+ const box = spec.square ? { x: (e.w - side) / 2, y: (e.h - side) / 2, w: side, h: side } : { x: 0, y: 0, w: e.w, h: e.h };
76
+ const turn = TURN[mark.dir] ?? TURN.right;
77
+ return {
78
+ kind: mark.kind,
79
+ strokes: spec.d.map((stroke) => stroke.map(([x, y]) => {
80
+ const [u, v] = turn(x, y);
81
+ return { x: r2(e.x + box.x + u * box.w), y: r2(e.y + box.y + v * box.h) };
82
+ })),
83
+ };
84
+ }
@@ -0,0 +1,12 @@
1
+ // Which strokes belong to a picture rather than to the slide's relationships.
2
+ // Line topology itself lives in concept-lines.mjs.
3
+ import { nativeChartData } from './concept-geometry.mjs';
4
+
5
+ /**
6
+ * The illustration an element is a plot mark of: a chart without readable
7
+ * values is kept as drawn marks, and every stroke inside it is part of that
8
+ * picture (tagged `data-sm-plot-owner`), never a line between things.
9
+ */
10
+ export function illustrationOwner(elements, e) {
11
+ return elements.find(c => c.kind === 'chart' && !nativeChartData(c.chart) && e.x >= c.x - 2 && e.y >= c.y - 2 && e.x + e.w <= c.x + c.w + 2 && e.y + e.h <= c.y + c.h + 2) ?? null;
12
+ }