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,326 @@
1
+ // A chart's own labelling drawn as separate elements — a legend of swatch and
2
+ // name rows, value labels set beside the bars, category names under the axis —
3
+ // belongs to the chart, and the chart draws it natively. x111 QBR 3 drew its
4
+ // doughnut's legend as four texts ("DE–PL lane 21%"); x108 cloud 5 as swatch,
5
+ // figure and name in three boxes per row; x110 QBR 2 set every category and
6
+ // value of its bridge beside it as text. Built so, they are boxes that drift
7
+ // from the data and that nobody can edit with it.
8
+ //
9
+ // Folded here, before any layout, on declaration and evidence, never on
10
+ // geometry: the elements the reader declared the chart's own (`chart.owns`),
11
+ // and texts whose words ARE the chart's data — a category name (alone, or with
12
+ // that category's value), a series name of a chart with more than one series,
13
+ // or a figure the reader listed among the chart's labels that equals one of
14
+ // its values. A legend row's key (the wordless parts of a declared stack row
15
+ // whose text folded) goes with it. The chart is told what it now draws itself
16
+ // (`chart.folded`) and chart-jsx.mjs switches its legend, category labels and
17
+ // value labels on. What a doughnut paints inside its hole is the chart's own
18
+ // content, not its labelling: claimed first (chart-hole.mjs), never folded.
19
+ import { claimHoles, holeMembers } from "./chart-hole.mjs";
20
+ import { nativeChartData } from "./concept-geometry.mjs";
21
+
22
+ const TEXT = new Set(["text", "number"]);
23
+ // Marks a legend draws beside its names: a swatch, a dot, a tick.
24
+ const KEY = new Set(["shape", "disc", "badge", "rule", "bar", "icon"]);
25
+ // What a short label may carry beside its figure: a sign, a unit.
26
+ const UNIT = /^[\s%‰$€£¥+\-−–(),.:]*(?:pp|pts?|bn|mn|[kmb]|x|×)?[\s%‰$€£¥+\-−–(),.:]*$/i;
27
+
28
+ /** A label's words as compared: no list glyph, one space, no trailing colon, any case. */
29
+ export function labelKey(text) {
30
+ return String(text ?? "")
31
+ .replace(/^[\s•·▪◦‣■□●○◆◇▶►✓✔✗-]+/u, "")
32
+ .replace(/\s+/g, " ")
33
+ .trim()
34
+ .replace(/:$/, "")
35
+ .toLowerCase();
36
+ }
37
+
38
+ // The one figure a short label carries, as a number: "21%" 21, "$610k" 610,
39
+ // "−1.1 pp" -1.1, "1,200" 1200, "95,4" 95.4. Null for none or several.
40
+ export function figureOf(text) {
41
+ const found = String(text ?? "").match(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/g);
42
+ if (!found || found.length !== 1) return null;
43
+ let digits = found[0].replace(/\s/g, "").replace("−", "-").replace("+", "");
44
+ if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(digits)) digits = digits.replace(/,/g, "");
45
+ else if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(digits)) digits = digits.replace(/\./g, "").replace(",", ".");
46
+ else digits = digits.replace(",", ".");
47
+ const value = Number(digits);
48
+ return Number.isFinite(value) ? value : null;
49
+ }
50
+
51
+ const sameValue = (a, v) => a != null && Number.isFinite(v) && Math.abs(Math.abs(a) - Math.abs(v)) <= 0.051 + Math.abs(v) * 0.005;
52
+
53
+ // A label that says nothing but its figure (and a sign or unit).
54
+ export const onlyFigure = (text) => figureOf(text) != null && UNIT.test(String(text).replace(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/, ""));
55
+
56
+ /**
57
+ * What one text is to one chart, from its words alone, or null: `as` "legend"
58
+ * (a slice's or a series' name), "category" (a category name of a plotted
59
+ * chart) or "value" (a figure among the chart's labels that equals one of its
60
+ * values); `value` true when a name carries its own value beside it.
61
+ */
62
+ export function chartLabelOf(text, chart, data, { declared = false } = {}) {
63
+ const key = labelKey(text);
64
+ if (!key) return null;
65
+ const sliced = ["pie", "doughnut"].includes(chart.type);
66
+ const named = sliced ? "legend" : "category";
67
+ const valuesAt = (i) => data.series.map((s) => s.values[i]);
68
+ for (const [i, category] of data.categories.entries()) {
69
+ const name = labelKey(category);
70
+ if (!name) continue;
71
+ if (key === name) return { as: named, value: false };
72
+ for (const rest of [key.startsWith(`${name} `) ? key.slice(name.length) : null, key.endsWith(` ${name}`) ? key.slice(0, -name.length) : null]) {
73
+ if (rest != null && onlyFigure(rest) && valuesAt(i).some((v) => sameValue(figureOf(rest), v))) return { as: named, value: true };
74
+ }
75
+ }
76
+ if (data.series.length > 1 && data.series.some((s) => labelKey(s.name) === key)) return { as: "legend", value: false };
77
+ // A series named at one of its own points, with that point's value: a
78
+ // target line's "Target 96.5%" (x113 QBR 3). The chart labels the series'
79
+ // last point with its name and value.
80
+ for (const [s, series] of data.series.entries()) {
81
+ const name = labelKey(series.name);
82
+ const rest = name && key.startsWith(`${name} `) ? key.slice(name.length) : null;
83
+ if (rest != null && onlyFigure(rest) && series.values.some((v) => sameValue(figureOf(rest), v))) return { as: "series", value: true, series: s };
84
+ }
85
+ const listed = declared || (chart.labels ?? []).some((l) => labelKey(l) === key);
86
+ if (listed && onlyFigure(text) && data.series.some((s) => s.values.some((v) => sameValue(figureOf(text), v)))) return { as: "value", value: true };
87
+ return null;
88
+ }
89
+
90
+ // A figure's format: its words and signs around the digits ("21%" and "52%"
91
+ // are one format, "9" another).
92
+ const formatOf = (text) => String(text).replace(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/, "#").replace(/\s+/g, "").toLowerCase();
93
+
94
+ /**
95
+ * Which of `texts` are one chart's own labelling, each as `chartLabelOf` says
96
+ * or null — judged as sets, because a legend, an axis and a row of value
97
+ * labels are sets: names count only when they cover at least two and at least
98
+ * half of the chart's categories (or series) — one name alone is a heading or a
99
+ * callout (x107 QBR 3: "On-time delivery" over a two-series chart) — and
100
+ * figures only as one format labelling every point of a series (x111 GTM 1:
101
+ * "9%", "27%", "12%" — not the unrelated "9" beside them). `declared`
102
+ * (indices) are the reader's: they count alone.
103
+ */
104
+ export function chartLabelling(texts, chart, data, { declared = new Set() } = {}) {
105
+ const out = texts.map((t, i) => chartLabelOf(t, chart, data, { declared: declared.has(i) }));
106
+ const nameOf = (t) => {
107
+ const key = labelKey(t);
108
+ const i = data.categories.findIndex((c) => { const n = labelKey(c); return n && (key === n || key.startsWith(`${n} `) || key.endsWith(` ${n}`)); });
109
+ return i >= 0 ? `c${i}` : `s${data.series.findIndex((s) => labelKey(s.name) === key)}`;
110
+ };
111
+ // A typed legend or axis covers most of what it names: at least two of the
112
+ // chart's categories (series) and at least half of them. Two of a bridge's
113
+ // six category names under two KPI figures are captions (x112 QBR 2:
114
+ // "actual", "plan" beside "18.6%" and "19.5%").
115
+ out.forEach((l, i) => { if (l?.as === "series" && !declared.has(i)) out[i] = null; });
116
+ const named = new Set(out.map((l, i) => (l && l.as !== "value" && !declared.has(i) ? nameOf(texts[i]) : null)).filter(Boolean));
117
+ const covers = (prefix, total) => { const n = [...named].filter((k) => k.startsWith(prefix)).length; return n >= 2 && n >= Math.ceil(total / 2); };
118
+ const categoriesSet = covers("c", data.categories.filter((c) => labelKey(c)).length);
119
+ const seriesSet = covers("s", data.series.length);
120
+ out.forEach((l, i) => {
121
+ if (!l || l.as === "value" || declared.has(i)) return;
122
+ if (!(nameOf(texts[i]).startsWith("c") ? categoriesSet : seriesSet)) out[i] = null;
123
+ });
124
+ const groups = new Map();
125
+ out.forEach((l, i) => { if (l?.as === "value" && !declared.has(i)) groups.set(formatOf(texts[i]), [...(groups.get(formatOf(texts[i])) ?? []), i]); });
126
+ for (const members of groups.values()) {
127
+ const set = new Set((seriesLabelSet(members.map((i) => texts[i]), data) ?? []).map((k) => members[k]));
128
+ for (const i of members) if (!set.has(i)) out[i] = null;
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /**
134
+ * Fold every native chart's own labelling into the chart (see the header).
135
+ * Mutates `spec`: drops the folded elements, prunes the structures that named
136
+ * them (stacks, grids, repeats, connectors), records `chart.folded` and says
137
+ * what it did in `notes` (default `spec.notes`). Idempotent.
138
+ */
139
+ export function foldChartLabels(spec, notes = null) {
140
+ const elements = spec.elements ?? [];
141
+ const charts = elements.filter((e) => e.kind === "chart" && e.w > 0 && e.h > 0 && nativeChartData(e.chart));
142
+ if (!charts.length) return spec;
143
+ const said = notes ?? (spec.notes ??= []);
144
+ const say = (note) => { if (!said.includes(note)) said.push(note); };
145
+ for (const note of claimHoles(spec)) say(note);
146
+ const inHole = holeMembers(elements);
147
+ const byId = new Map(elements.map((e) => [e.id, e]));
148
+ const parents = new Set(elements.map((e) => e.parent).filter(Boolean));
149
+ const ancestors = (e) => { const out = new Set(); for (let p = byId.get(e.parent); p && !out.has(p.id); p = byId.get(p.parent)) out.add(p.id); return out; };
150
+ const rows = (spec.stacks ?? []).flatMap((s) => s.rows ?? []);
151
+ const gone = new Set();
152
+ // Which chart the reader declared each element the own labelling of: a text
153
+ // another chart owns is never this chart's, however its words read (x113 QBR
154
+ // 1: eight mini charts with the same "Q2"/"Q3" names — the first took every
155
+ // card's category labels and the other seven drew none).
156
+ const ownerOf = new Map();
157
+ for (const c of charts) for (const id of Array.isArray(c.chart.owns) ? c.chart.owns : []) if (!ownerOf.has(id)) ownerOf.set(id, c.id);
158
+ for (const c of charts) {
159
+ const data = nativeChartData(c.chart);
160
+ const own = ancestors(c);
161
+ const foldable = (e) => e && !gone.has(e.id) && e !== c && e.kind !== "chart" && !own.has(e.id) && !parents.has(e.id) && !inHole.has(e.id)
162
+ && (ownerOf.get(e.id) ?? c.id) === c.id
163
+ // Undeclared, a text is this chart's only where it shares the chart's
164
+ // container — beside several charts, words alone do not say whose.
165
+ && (ownerOf.has(e.id) || charts.length === 1 || !c.parent || ancestors(e).has(c.parent));
166
+ const before = c.chart.folded ?? {};
167
+ const folded = { legend: !!before.legend, categories: !!before.categories, values: !!before.values, side: before.side ?? null, texts: [...(before.texts ?? [])], ids: [...(before.ids ?? [])], ...(before.axis ? { axis: before.axis } : {}), ...(before.seriesLabels ? { seriesLabels: [...before.seriesLabels] } : {}), ...(before.frame ? { frame: before.frame } : {}) };
168
+ const take = (e, label) => {
169
+ gone.add(e.id);
170
+ folded.ids.push(e.id);
171
+ if (e.text != null) folded.texts.push(String(e.text));
172
+ if (label?.as === "legend") folded.legend = true;
173
+ if (label?.as === "category") folded.categories = true;
174
+ if (label?.as === "series") folded.seriesLabels = [...new Set([...(folded.seriesLabels ?? []), label.series])];
175
+ if (label?.value) folded.values = folded.values || label.as !== "series";
176
+ };
177
+ const declared = new Set(Array.isArray(c.chart.owns) ? c.chart.owns : []);
178
+ // The texts: those whose words are this chart's own data — the reader's
179
+ // declaration names them first, the evidence finds the rest. A declared
180
+ // text whose words are not the chart's data stays, and is said.
181
+ const candidates = elements.filter((e) => foldable(e) && TEXT.has(e.kind) && e.text != null);
182
+ const labels = chartLabelling(candidates.map((e) => String(e.text)), c.chart, data, {
183
+ declared: new Set(candidates.map((e, i) => (declared.has(e.id) ? i : -1)).filter((i) => i >= 0)),
184
+ });
185
+ // A declared run of figures that are not the chart's data but step evenly
186
+ // through one format is its value axis, painted with its ticks (x113 QBR 3:
187
+ // 94.5% … 97.0% beside the line): the chart draws that axis natively.
188
+ const dataValues = data.series.flatMap((s) => s.values).filter(Number.isFinite);
189
+ const ticks = axisTicks(candidates.filter((e) => declared.has(e.id)), dataValues);
190
+ if (ticks) {
191
+ for (const e of ticks.elements) take(e, null);
192
+ folded.axis = ticks.axis;
193
+ }
194
+ candidates.forEach((e, i) => {
195
+ if (ticks?.elements.includes(e)) return;
196
+ if (labels[i]) take(e, labels[i]);
197
+ else if (declared.has(e.id)) say(`chart ${c.id}: ${e.id} was declared the chart's own, but "${String(e.text).slice(0, 40)}" is not its data (a category, a series or one of its values) — kept as text`);
198
+ });
199
+ // A legend's keys go with its names: the wordless marks the reader declared
200
+ // the chart's, and those in a declared stack row whose text folded.
201
+ if (folded.legend) {
202
+ const inRow = new Set(rows.filter((row) => row.some((id) => folded.ids.includes(id))).flat());
203
+ for (const e of elements) {
204
+ if (foldable(e) && KEY.has(e.kind) && e.text == null && (declared.has(e.id) || inRow.has(e.id))) take(e, null);
205
+ }
206
+ }
207
+ // Where the image drew the legend rows: the side of the chart they stand on.
208
+ const legendTexts = folded.ids.map((id) => byId.get(id)).filter((e) => e && e.text != null && [e.x, e.y, e.w, e.h].every(Number.isFinite));
209
+ if (folded.legend && legendTexts.length && !folded.side) {
210
+ const cx = legendTexts.reduce((a, e) => a + e.x + e.w / 2, 0) / legendTexts.length;
211
+ const cy = legendTexts.reduce((a, e) => a + e.y + e.h / 2, 0) / legendTexts.length;
212
+ folded.side = cy >= c.y + c.h ? "bottom" : cy <= c.y ? "top" : cx <= c.x ? "left" : "right";
213
+ }
214
+ // The chart's frame holds its own labelling: the boxes the folded legend
215
+ // rows, axis and value labels were drawn in join the chart's (x113 QBR 3:
216
+ // a 190px doughnut whose legend stood beside it shrank to a dot once the
217
+ // legend was drawn inside its frame).
218
+ const newly = folded.ids.slice((before.ids ?? []).length).map((id) => byId.get(id)).filter((e) => e && [e.x, e.y, e.w, e.h].every(Number.isFinite));
219
+ if (newly.length) {
220
+ const x0 = Math.min(c.x, ...newly.map((e) => e.x));
221
+ const y0 = Math.min(c.y, ...newly.map((e) => e.y));
222
+ const x1 = Math.max(c.x + c.w, ...newly.map((e) => e.x + e.w));
223
+ const y1 = Math.max(c.y + c.h, ...newly.map((e) => e.y + e.h));
224
+ if (x0 < c.x || y0 < c.y || x1 > c.x + c.w || y1 > c.y + c.h) {
225
+ folded.frame = { from: { x: c.x, y: c.y, w: c.w, h: c.h } };
226
+ Object.assign(c, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
227
+ }
228
+ }
229
+ if (folded.ids.length > (before.ids ?? []).length) {
230
+ const drawn = [folded.legend && "legend", folded.categories && "category labels", folded.values && "value labels"].filter(Boolean);
231
+ say(`chart ${c.id}: ${folded.ids.join(", ")} drew the chart's own ${drawn.join(" and ") || "labelling"} beside it — folded into the chart, which shows ${drawn.length ? "them" : "it"} natively`);
232
+ }
233
+ c.chart.folded = folded;
234
+ }
235
+ if (!gone.size) return spec;
236
+ spec.elements = elements.filter((e) => !gone.has(e.id));
237
+ pruneStructures(spec, gone);
238
+ return spec;
239
+ }
240
+
241
+ /**
242
+ * A value axis painted with its tick labels, among a chart's declared figures:
243
+ * the longest run of at least three in one format that step evenly (94.5% 95.0%
244
+ * … 97.0% beside the value labels 95.4% 95.9% 96.2%), of which at least two are
245
+ * not the chart's data — evenly spaced value labels (10 20 30) are labels, not
246
+ * an axis. Returns the run's elements and the axis (`min`, `max`, `step`, the
247
+ * ticks' `format` as an Excel number format), or null.
248
+ */
249
+ export function axisTicks(elements, dataValues = []) {
250
+ const figures = elements.filter((e) => onlyFigure(e.text)).map((e) => ({ e, v: figureOf(e.text), f: formatOf(e.text) }));
251
+ let best = null;
252
+ for (const f of new Set(figures.map((x) => x.f))) {
253
+ const same = figures.filter((x) => x.f === f);
254
+ const values = [...new Set(same.map((x) => x.v))].sort((a, b) => a - b);
255
+ for (let i = 0; i < values.length; i++) {
256
+ for (let j = i + 1; j < values.length; j++) {
257
+ const step = values[j] - values[i];
258
+ const at = (v) => same.find((x) => Math.abs(x.v - v) <= step * 1e-3);
259
+ const run = [];
260
+ for (let k = 0; at(values[i] + k * step); k++) run.push(at(values[i] + k * step));
261
+ // A tick IS a data value only at the tick's own precision (95.0% is not
262
+ // the value 95.4).
263
+ const isData = (x) => { const dec = (/[.,](\d+)\D*$/.exec(String(x.e.text))?.[1] ?? "").length; return dataValues.some((d) => Math.abs(x.v - d) <= 0.5 * 10 ** -dec + 1e-9); };
264
+ if (run.length < 3 || run.filter((x) => !isData(x)).length < 2) continue;
265
+ if (!best || run.length > best.run.length) best = { run, step };
266
+ }
267
+ }
268
+ }
269
+ if (!best) return null;
270
+ const { run, step } = best;
271
+ const sample = String(run[0].e.text).trim();
272
+ const decimals = (/[.,](\d+)/.exec(sample.replace(/^[^\d]*/, ""))?.[1] ?? "").length;
273
+ const [pre, post] = sample.split(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/).map((s) => s.trim().replace(/"/g, ""));
274
+ const body = `0${decimals ? `.${"0".repeat(decimals)}` : ""}`;
275
+ const format = pre || post || decimals ? `${pre ? `"${pre}"` : ""}${body}${post ? `"${post}"` : ""}` : null;
276
+ const round = (v) => Number(v.toFixed(decimals + 2));
277
+ return { elements: run.map((x) => x.e), axis: { min: round(run[0].v), max: round(run[run.length - 1].v), step: round(step), ...(format ? { format } : {}) } };
278
+ }
279
+
280
+ /**
281
+ * The indices of `texts` that label every value of one series, one text per
282
+ * value (the first series they cover), or null. Spare copies of a value (a
283
+ * doughnut's centre "58%" beside its legend's "58%") are not part of the set.
284
+ */
285
+ export function seriesLabelSet(texts, data) {
286
+ for (const series of data.series) {
287
+ if (!series.values.length) continue;
288
+ const free = texts.map((t, i) => i);
289
+ const picked = [];
290
+ const complete = series.values.every((v) => {
291
+ const k = free.findIndex((i) => sameValue(figureOf(texts[i]), v));
292
+ if (k < 0) return false;
293
+ picked.push(free[k]);
294
+ free.splice(k, 1);
295
+ return true;
296
+ });
297
+ if (complete) return picked;
298
+ }
299
+ return null;
300
+ }
301
+
302
+ /** Whether `texts` carry every value of at least one series, one text each. */
303
+ export const labelsEverySeriesPoint = (texts, data) => seriesLabelSet(texts, data) != null;
304
+
305
+ // The structures that named a folded element follow it: a stack row loses it
306
+ // (an empty row goes, a stack of fewer than two rows goes, and a grid whose
307
+ // column lost its stack), a repeat member loses it, a connector to it goes.
308
+ function pruneStructures(spec, gone) {
309
+ if (Array.isArray(spec.stacks)) {
310
+ spec.stacks = spec.stacks
311
+ .map((s) => ({ ...s, rows: (s.rows ?? []).map((r) => r.filter((id) => !gone.has(id))).filter((r) => r.length) }))
312
+ .filter((s) => s.rows.length >= 2);
313
+ }
314
+ if (Array.isArray(spec.grids)) {
315
+ const stackOf = (col) => (spec.stacks ?? []).find((s) => s.parent === col);
316
+ spec.grids = spec.grids.filter((g) => (g.columns ?? []).every((col) => stackOf(col)?.rows.length === g.rows));
317
+ }
318
+ if (Array.isArray(spec.repeats)) {
319
+ spec.repeats = spec.repeats
320
+ .map((g) => ({ ...g, members: (g.members ?? []).map((m) => (Array.isArray(m) ? m.filter((id) => !gone.has(id)) : gone.has(m) ? null : m)).filter((m) => m != null && (!Array.isArray(m) || m.length)) }))
321
+ .filter((g) => g.members.length >= 2);
322
+ }
323
+ if (Array.isArray(spec.connectors)) {
324
+ spec.connectors = spec.connectors.filter((x) => !gone.has(x.from) && !gone.has(x.to));
325
+ }
326
+ }