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
|
@@ -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
|
+
}
|