slidemind 0.3.20 → 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/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,230 @@
|
|
|
1
|
+
// How a line relates to the painted shapes it meets — one relation, read by
|
|
2
|
+
// the transcriber that orders a free line among the shapes (placement.mjs) and
|
|
3
|
+
// by the draft check that judges the render (the add-in's
|
|
4
|
+
// connector-occlusion.ts). Two readings of it disagreed: the transcriber took
|
|
5
|
+
// as ground only a panel holding ALL of the line and put it under everything
|
|
6
|
+
// else it crossed, the check takes the panel under most of it — a divider
|
|
7
|
+
// 13px past its card's edge was written under that card and the check named
|
|
8
|
+
// it hidden (x114 QBR 6).
|
|
9
|
+
//
|
|
10
|
+
// - Ground: the painted shape under most of a free line's length. The line
|
|
11
|
+
// lies on it and paints after it (a divider on its card, an axis on its
|
|
12
|
+
// panel). A line glued at both ends relates its nodes; only a shape holding
|
|
13
|
+
// all of it is its ground.
|
|
14
|
+
// - Station: an opaque node the line threads near its centre (a timeline's
|
|
15
|
+
// stop, the disc on a divider). It sits on the line and paints after it.
|
|
16
|
+
// A bead (≤16px) reads as a dot on its line whichever paints first.
|
|
17
|
+
//
|
|
18
|
+
// A shape is `{ rect, outline, content, alpha, lines }`: `outline` is
|
|
19
|
+
// `{ ellipse, poly }` when anything paints it (null otherwise), `content` a
|
|
20
|
+
// picture, icon or chart (its pixels are unknown), `alpha` how opaque its
|
|
21
|
+
// paint is, `lines` its text lines. Points and rects are px in one frame.
|
|
22
|
+
//
|
|
23
|
+
// Shared by both clients; mirrored by scripts/sync-transcription-runtime.mjs —
|
|
24
|
+
// no imports here.
|
|
25
|
+
|
|
26
|
+
export const STATION_MAX_PX = 16;
|
|
27
|
+
export const STATION_CENTRE_FRAC = 0.75;
|
|
28
|
+
export const NODE_CENTRE_FRAC = 0.4;
|
|
29
|
+
export const OPAQUE = 0.99;
|
|
30
|
+
|
|
31
|
+
// ---- Points and segments against rects, ellipses and clip polygons ----------
|
|
32
|
+
|
|
33
|
+
export function pointInsideRect(point, rect) {
|
|
34
|
+
return point.x >= rect.x && point.x <= rect.x + rect.w && point.y >= rect.y && point.y <= rect.y + rect.h;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pointInsideShape(point, rect, ellipse, grow) {
|
|
38
|
+
const r = { x: rect.x - grow, y: rect.y - grow, w: rect.w + grow * 2, h: rect.h + grow * 2 };
|
|
39
|
+
if (r.w <= 0 || r.h <= 0) return false;
|
|
40
|
+
if (!ellipse) return pointInsideRect(point, r);
|
|
41
|
+
const nx = (point.x - (r.x + r.w / 2)) / (r.w / 2);
|
|
42
|
+
const ny = (point.y - (r.y + r.h / 2)) / (r.h / 2);
|
|
43
|
+
return nx * nx + ny * ny <= 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The point sits ON the shape's outline (within `band` px either side), not deep inside it. */
|
|
47
|
+
export function pointOnOutline(point, rect, ellipse, band) {
|
|
48
|
+
return pointInsideShape(point, rect, ellipse, band) && !pointInsideShape(point, rect, ellipse, -band);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function rectContainsRect(outer, inner, eps = 0.5) {
|
|
52
|
+
return inner.x >= outer.x - eps && inner.y >= outer.y - eps && inner.x + inner.w <= outer.x + outer.w + eps && inner.y + inner.h <= outer.y + outer.h + eps;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function insidePolygon(p, poly) {
|
|
56
|
+
let inside = false;
|
|
57
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
58
|
+
const a = poly[i];
|
|
59
|
+
const b = poly[j];
|
|
60
|
+
if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;
|
|
61
|
+
}
|
|
62
|
+
return inside;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function orientation(a, b, c) {
|
|
66
|
+
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
|
|
67
|
+
if (Math.abs(value) < 0.0001) return 0;
|
|
68
|
+
return value > 0 ? 1 : 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onSegment(a, b, c) {
|
|
72
|
+
return b.x <= Math.max(a.x, c.x) + 0.0001 && b.x + 0.0001 >= Math.min(a.x, c.x) && b.y <= Math.max(a.y, c.y) + 0.0001 && b.y + 0.0001 >= Math.min(a.y, c.y);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function segmentsIntersect(a, b, c, d) {
|
|
76
|
+
const o1 = orientation(a, b, c);
|
|
77
|
+
const o2 = orientation(a, b, d);
|
|
78
|
+
const o3 = orientation(c, d, a);
|
|
79
|
+
const o4 = orientation(c, d, b);
|
|
80
|
+
if (o1 !== o2 && o3 !== o4) return true;
|
|
81
|
+
if (o1 === 0 && onSegment(a, c, b)) return true;
|
|
82
|
+
if (o2 === 0 && onSegment(a, d, b)) return true;
|
|
83
|
+
if (o3 === 0 && onSegment(c, a, d)) return true;
|
|
84
|
+
if (o4 === 0 && onSegment(c, b, d)) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function segmentHitsRect(a, b, rect) {
|
|
89
|
+
if (pointInsideRect(a, rect) || pointInsideRect(b, rect)) return true;
|
|
90
|
+
const tl = { x: rect.x, y: rect.y };
|
|
91
|
+
const tr = { x: rect.x + rect.w, y: rect.y };
|
|
92
|
+
const br = { x: rect.x + rect.w, y: rect.y + rect.h };
|
|
93
|
+
const bl = { x: rect.x, y: rect.y + rect.h };
|
|
94
|
+
return segmentsIntersect(a, b, tl, tr) || segmentsIntersect(a, b, tr, br) || segmentsIntersect(a, b, br, bl) || segmentsIntersect(a, b, bl, tl);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function segmentHitsEllipse(a, b, rect) {
|
|
98
|
+
const rx = rect.w / 2;
|
|
99
|
+
const ry = rect.h / 2;
|
|
100
|
+
const cx = rect.x + rx;
|
|
101
|
+
const cy = rect.y + ry;
|
|
102
|
+
const ax = (a.x - cx) / rx;
|
|
103
|
+
const ay = (a.y - cy) / ry;
|
|
104
|
+
const dx = (b.x - cx) / rx - ax;
|
|
105
|
+
const dy = (b.y - cy) / ry - ay;
|
|
106
|
+
const len2 = dx * dx + dy * dy;
|
|
107
|
+
const t = len2 ? Math.max(0, Math.min(1, -(ax * dx + ay * dy) / len2)) : 0;
|
|
108
|
+
const px = ax + t * dx;
|
|
109
|
+
const py = ay + t * dy;
|
|
110
|
+
return px * px + py * py < 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function segmentHitsPolygon(a, b, poly) {
|
|
114
|
+
if (insidePolygon(a, poly) || insidePolygon(b, poly)) return true;
|
|
115
|
+
return poly.some((p, i) => segmentsIntersect(a, b, p, poly[(i + 1) % poly.length]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- A route against one painted shape --------------------------------------
|
|
119
|
+
|
|
120
|
+
/** Points along the route, every few px, each weighted by the length it stands for. */
|
|
121
|
+
export function routeSamples(points) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (let i = 1; i < points.length; i++) {
|
|
124
|
+
const a = points[i - 1];
|
|
125
|
+
const b = points[i];
|
|
126
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
127
|
+
const n = Math.max(1, Math.ceil(len / 4));
|
|
128
|
+
for (let k = 0; k < n; k++) {
|
|
129
|
+
const t = (k + 0.5) / n;
|
|
130
|
+
out.push({ p: { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }, w: len / n });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function insideOutline(p, shape) {
|
|
137
|
+
if (!shape.outline) return false;
|
|
138
|
+
return shape.outline.poly ? insidePolygon(p, shape.outline.poly) : pointInsideShape(p, shape.rect, shape.outline.ellipse, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The share of a route's length that lies inside a shape's painted outline. */
|
|
142
|
+
export function routeShare(points, shape, samples = routeSamples(points)) {
|
|
143
|
+
const total = samples.reduce((sum, s) => sum + s.w, 0);
|
|
144
|
+
if (!total) return 0;
|
|
145
|
+
return samples.reduce((sum, s) => sum + (insideOutline(s.p, shape) ? s.w : 0), 0) / total;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The line's ground: the painted shape under most of a free line, or — a line
|
|
150
|
+
* glued at both ends (`related`) — one holding all of it.
|
|
151
|
+
*/
|
|
152
|
+
export function isGround(shape, points, related = false, samples = undefined) {
|
|
153
|
+
if (!shape.outline) return false;
|
|
154
|
+
if (related) return points.every((p) => pointInsideRect(p, { x: shape.rect.x - 0.5, y: shape.rect.y - 0.5, w: shape.rect.w + 1, h: shape.rect.h + 1 }));
|
|
155
|
+
return routeShare(points, shape, samples) > 0.5;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The route's nearest approach to `point`: the distance, and the route's direction there. */
|
|
159
|
+
export function nearestOnRoute(point, points) {
|
|
160
|
+
let best = { d: Number.POSITIVE_INFINITY, ux: 1, uy: 0 };
|
|
161
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
162
|
+
const a = points[i];
|
|
163
|
+
const b = points[i + 1];
|
|
164
|
+
const dx = b.x - a.x;
|
|
165
|
+
const dy = b.y - a.y;
|
|
166
|
+
const len2 = dx * dx + dy * dy;
|
|
167
|
+
const t = len2 ? Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / len2)) : 0;
|
|
168
|
+
const d = Math.hypot(a.x + t * dx - point.x, a.y + t * dy - point.y);
|
|
169
|
+
if (d < best.d) best = len2 ? { d, ux: dx / Math.sqrt(len2), uy: dy / Math.sqrt(len2) } : { ...best, d };
|
|
170
|
+
}
|
|
171
|
+
return best;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const isBead = (shape) => Math.max(shape.rect.w, shape.rect.h) <= STATION_MAX_PX;
|
|
175
|
+
|
|
176
|
+
/** The route enters the shape's painted outline. */
|
|
177
|
+
function meets(points, shape) {
|
|
178
|
+
const { rect, outline } = shape;
|
|
179
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
180
|
+
if (outline.poly) {
|
|
181
|
+
if (segmentHitsPolygon(points[i], points[i + 1], outline.poly)) return true;
|
|
182
|
+
} else if (rect.w > 0 && rect.h > 0) {
|
|
183
|
+
if (outline.ellipse ? segmentHitsEllipse(points[i], points[i + 1], rect) : segmentHitsRect(points[i], points[i + 1], rect)) return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A station: a bead, or an opaque node — only an opaque node covers the line
|
|
191
|
+
* and whatever it holds; through a wash the line still strikes its text. It is
|
|
192
|
+
* threaded near its centre: within NODE_CENTRE_FRAC of its half-extent across
|
|
193
|
+
* the route (a bead: STATION_CENTRE_FRAC).
|
|
194
|
+
*/
|
|
195
|
+
export function isStation(shape, points) {
|
|
196
|
+
const { rect } = shape;
|
|
197
|
+
if (!shape.outline || shape.content || (isBead(shape) && shape.lines.length)) return false;
|
|
198
|
+
if (!isBead(shape) && shape.alpha < OPAQUE) return false;
|
|
199
|
+
const { d, ux, uy } = nearestOnRoute({ x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }, points);
|
|
200
|
+
// The node's half-extent across the route: a disc's radius, half a bar's
|
|
201
|
+
// length where a line crosses it.
|
|
202
|
+
const across = shape.outline.ellipse ? Math.hypot((uy * rect.w) / 2, (ux * rect.h) / 2) : (Math.abs(uy) * rect.w) / 2 + (Math.abs(ux) * rect.h) / 2;
|
|
203
|
+
if (d > across * (isBead(shape) ? STATION_CENTRE_FRAC : NODE_CENTRE_FRAC)) return false;
|
|
204
|
+
return meets(points, shape);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The smallest painted shape whose outline a free end touches (a ring arc
|
|
209
|
+
* meeting its disc, a rule ending at a card's edge): the line is attached to
|
|
210
|
+
* it in all but name. An end deep inside a panel touches nothing.
|
|
211
|
+
*/
|
|
212
|
+
export function touchedShape(point, shapes, band) {
|
|
213
|
+
let node = null;
|
|
214
|
+
for (const o of shapes) {
|
|
215
|
+
if (!o.outline || !pointOnOutline(point, o.rect, o.outline.ellipse, band)) continue;
|
|
216
|
+
if (!node || o.rect.w * o.rect.h < node.rect.w * node.rect.h) node = o;
|
|
217
|
+
}
|
|
218
|
+
return node;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The stations a line passes through: stations that hold none of its ends
|
|
223
|
+
* (`ends`: rects — a zero rect at a free point, a node's rect) and are not its
|
|
224
|
+
* ground.
|
|
225
|
+
*/
|
|
226
|
+
export function stationsOf(points, shapes, ends, related = false) {
|
|
227
|
+
if (related) return [];
|
|
228
|
+
const samples = routeSamples(points);
|
|
229
|
+
return shapes.filter((o) => isStation(o, points) && !ends.some((e) => rectContainsRect(o.rect, e)) && !isGround(o, points, false, samples));
|
|
230
|
+
}
|
|
@@ -16,9 +16,22 @@
|
|
|
16
16
|
// Everything else the lint engine flags (raw colors, token choices, spacing
|
|
17
17
|
// scales) is a judgment call and deliberately NOT auto-fixed.
|
|
18
18
|
|
|
19
|
+
import crypto from "node:crypto";
|
|
19
20
|
import fs from "node:fs";
|
|
20
21
|
import path from "node:path";
|
|
21
22
|
|
|
23
|
+
import { CONVERSIONS_DIR, conversionProvenance } from "../lib/conversions.mjs";
|
|
24
|
+
import { statePath } from "../lib/deck-paths.mjs";
|
|
25
|
+
import { selectSlideFiles } from "../lib/slide-selection.mjs";
|
|
26
|
+
|
|
27
|
+
// "This source says it is converted", as two markers only the converter writes:
|
|
28
|
+
// the dialect pragma `slidemind adopt` stamps at the top of the file, and the
|
|
29
|
+
// dialect's own attributes/host in the body. Identical to the pair
|
|
30
|
+
// `validateSettleTarget` refuses on (src/builders/settle.mjs DIALECT_RE /
|
|
31
|
+
// NATIVE_RE) on purpose — one definition of the dialect across the CLI.
|
|
32
|
+
const DIALECT_RE = /^\s*\/\/\s*SLIDEMIND_DIALECT:\s*converted\b/m;
|
|
33
|
+
const NATIVE_RE = /\bdata-sm-(?:native-object|source-id)\b|<ConvertedHost/;
|
|
34
|
+
|
|
22
35
|
// Declaration blocks inside a <style> string or a style attribute: convert
|
|
23
36
|
// `letter-spacing: Nem` when the SAME block carries `font-size: Mpx`.
|
|
24
37
|
const DECL_BLOCK = /\{[^{}]*\}/g;
|
|
@@ -61,28 +74,123 @@ export function fixSlideSource(src) {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
77
|
+
* Slides this whole-file rewrite may NOT touch, by file name → why. Preview is
|
|
78
|
+
* a look-at-it step: a slide whose source someone has taken over keeps its
|
|
79
|
+
* exact bytes, or an edit safeguard anywhere else is undone by the next
|
|
80
|
+
* `view-draft`. Three owners are knowable from what the deck already records:
|
|
81
|
+
*
|
|
82
|
+
* - CONVERTED (`slidemind adopt`, conversions/<stem>.json): its raw values
|
|
83
|
+
* ARE the original .pptx's. Rewriting one is a silent edit of an imported
|
|
84
|
+
* slide the user never asked for.
|
|
85
|
+
* - UNREADABLE record: the file is there and will not parse, so whether the
|
|
86
|
+
* slide is converted is UNANSWERED. That is the strongest reason to keep
|
|
87
|
+
* hands off it, not the weakest — which is why this asks
|
|
88
|
+
* `conversionProvenance` per slide instead of `loadConversions`: a map
|
|
89
|
+
* cannot say "something was here and I could not read it", so a corrupt
|
|
90
|
+
* record reaches that map's callers as plain absence, i.e. as permission.
|
|
91
|
+
* - MARKED converted by its own source, with no record found. The record and
|
|
92
|
+
* the marker are two INDEPENDENT signals and either one refuses, matching
|
|
93
|
+
* the add-in's `slideNormalizationPolicy`. The asymmetry is deliberate:
|
|
94
|
+
* there the threat is a worker DELETING the marker to launder an import,
|
|
95
|
+
* so the persisted record is the backstop; here it is the reverse, a record
|
|
96
|
+
* deleted, lost or never written on a slide whose source still says what it
|
|
97
|
+
* is. Two signals, either sufficient, covers both directions — and without
|
|
98
|
+
* this one the same slide is protected in one client and rewritten in the
|
|
99
|
+
* other.
|
|
100
|
+
* - EDITED since `transcribe` stamped it (state/transcribed.json): the bytes
|
|
101
|
+
* no longer match the hash the machine wrote, so a person or an agent owns
|
|
102
|
+
* the file — the same rule `transcribe --force` already refuses on.
|
|
103
|
+
*
|
|
104
|
+
* A slide with NO stamp is not "edited": nothing ever recorded a machine
|
|
105
|
+
* baseline for it, so there is nothing it can differ from. Ordinary authored
|
|
106
|
+
* decks keep the autofix exactly as before.
|
|
107
|
+
*/
|
|
108
|
+
function unrewritableSlides(deckDir, slideFiles) {
|
|
109
|
+
const why = new Map();
|
|
110
|
+
for (const name of slideFiles) {
|
|
111
|
+
const record = conversionProvenance(deckDir, name);
|
|
112
|
+
if (record === undefined) {
|
|
113
|
+
why.set(
|
|
114
|
+
name,
|
|
115
|
+
`its ${CONVERSIONS_DIR}/${name.replace(/\.jsx$/, "")}.json could not be read, so whether ` +
|
|
116
|
+
"this slide is converted is unknown — an unreadable record is not permission to rewrite it",
|
|
117
|
+
);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (record) {
|
|
121
|
+
why.set(name, "converted from .pptx — its raw values are the original slide's");
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// No record. The source itself is the second, independent signal.
|
|
125
|
+
let src;
|
|
126
|
+
try {
|
|
127
|
+
src = fs.readFileSync(path.join(deckDir, "slides", name), "utf8");
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (DIALECT_RE.test(src) || NATIVE_RE.test(src)) {
|
|
132
|
+
why.set(
|
|
133
|
+
name,
|
|
134
|
+
"its source is marked converted, though no conversion record was found — " +
|
|
135
|
+
`the record ${CONVERSIONS_DIR}/${name.replace(/\.jsx$/, "")}.json is missing, which is worth ` +
|
|
136
|
+
"repairing on its own (re-run 'slidemind adopt' for this deck)",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let stamps = {};
|
|
141
|
+
try {
|
|
142
|
+
stamps = JSON.parse(fs.readFileSync(statePath(deckDir, "transcribed.json"), "utf8"));
|
|
143
|
+
} catch {
|
|
144
|
+
stamps = {};
|
|
145
|
+
}
|
|
146
|
+
for (const [name, hash] of Object.entries(stamps)) {
|
|
147
|
+
if (why.has(name)) continue;
|
|
148
|
+
let src;
|
|
149
|
+
try {
|
|
150
|
+
src = fs.readFileSync(path.join(deckDir, "slides", name), "utf8");
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (crypto.createHash("sha256").update(src).digest("hex") !== hash) {
|
|
155
|
+
why.set(name, "edited since it was transcribed — the edit stands");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return why;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Fix every slides/*.jsx in a deck, writing only changed files — except the
|
|
163
|
+
* ones `unrewritableSlides` withholds, which keep their bytes.
|
|
164
|
+
* Returns { files, fixes, withheld: [{ file, reason, fixes }] }.
|
|
66
165
|
*/
|
|
67
|
-
export function fixDeckSlides(deckDir) {
|
|
166
|
+
export function fixDeckSlides(deckDir, { only = null } = {}) {
|
|
68
167
|
const slidesDir = path.join(deckDir, "slides");
|
|
69
168
|
let files = 0;
|
|
70
169
|
let fixes = 0;
|
|
170
|
+
const withheld = [];
|
|
71
171
|
let entries = [];
|
|
72
172
|
try {
|
|
73
173
|
entries = fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx"));
|
|
74
174
|
} catch {
|
|
75
|
-
return { files, fixes };
|
|
175
|
+
return { files, fixes, withheld };
|
|
76
176
|
}
|
|
177
|
+
entries = selectSlideFiles(entries, only);
|
|
178
|
+
const unrewritable = unrewritableSlides(deckDir, entries);
|
|
77
179
|
for (const f of entries) {
|
|
78
180
|
const p = path.join(slidesDir, f);
|
|
79
181
|
const src = fs.readFileSync(p, "utf8");
|
|
80
182
|
const res = fixSlideSource(src);
|
|
81
|
-
if (res.fixes.length)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
183
|
+
if (!res.fixes.length) continue;
|
|
184
|
+
const reason = unrewritable.get(f);
|
|
185
|
+
if (reason) {
|
|
186
|
+
// Reported, never applied: the lint still prints every finding on this
|
|
187
|
+
// file, so the fix is offered rather than performed.
|
|
188
|
+
withheld.push({ file: f, reason, fixes: res.fixes });
|
|
189
|
+
continue;
|
|
85
190
|
}
|
|
191
|
+
fs.writeFileSync(p, res.src);
|
|
192
|
+
files += 1;
|
|
193
|
+
fixes += res.fixes.length;
|
|
86
194
|
}
|
|
87
|
-
return { files, fixes };
|
|
195
|
+
return { files, fixes, withheld };
|
|
88
196
|
}
|
package/src/builders/lint.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
|
|
18
18
|
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
19
|
+
import { deckSlug } from "../lib/deck-paths.mjs";
|
|
19
20
|
|
|
20
21
|
// Resolve the cached engine for a deck and explain every miss actionably.
|
|
21
22
|
// Returns { enginePath, version, primitives }.
|
|
@@ -23,8 +24,8 @@ function resolveEngine(deckDir) {
|
|
|
23
24
|
const bundleDir = resolveBundleDirForDeck(deckDir);
|
|
24
25
|
if (!bundleDir) {
|
|
25
26
|
throw new Error(
|
|
26
|
-
`No materialized design bundle for deck ${
|
|
27
|
-
` Run 'slidemind download-bundle ${
|
|
27
|
+
`No materialized design bundle for deck ${deckSlug(deckDir)}.\n` +
|
|
28
|
+
` Run 'slidemind download-bundle ${deckSlug(deckDir)}' first.`,
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
const enginePath = path.join(bundleDir, "lint-engine.mjs");
|
|
@@ -42,14 +43,14 @@ function resolveEngine(deckDir) {
|
|
|
42
43
|
throw new Error(
|
|
43
44
|
`The paired slidemind add-in does not serve a lint engine yet.\n` +
|
|
44
45
|
` Update/redeploy the add-in backend, then re-run ` +
|
|
45
|
-
`'slidemind download-bundle ${
|
|
46
|
+
`'slidemind download-bundle ${deckSlug(deckDir)}'.`,
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
// Cache written before the engine existed (meta lacks the key), or the
|
|
49
50
|
// file went missing — refetch repairs both.
|
|
50
51
|
throw new Error(
|
|
51
52
|
`This bundle cache predates the lint engine.\n` +
|
|
52
|
-
` Run 'slidemind download-bundle ${
|
|
53
|
+
` Run 'slidemind download-bundle ${deckSlug(deckDir)}'.`,
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -84,7 +85,7 @@ export async function lintSlidesOrThrow({ deckDir, slideFiles, layoutSources })
|
|
|
84
85
|
if (typeof engine.lintSlidesInMemory !== "function") {
|
|
85
86
|
throw new Error(
|
|
86
87
|
`Cached lint engine is corrupt (no lintSlidesInMemory export).\n` +
|
|
87
|
-
` Run 'slidemind download-bundle ${
|
|
88
|
+
` Run 'slidemind download-bundle ${deckSlug(deckDir)}'.`,
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -124,25 +125,29 @@ const DYNAMIC_ID_RE =
|
|
|
124
125
|
export function collapseLintLines(lines) {
|
|
125
126
|
const out = [];
|
|
126
127
|
const seen = new Set();
|
|
127
|
-
const
|
|
128
|
+
const arrows = new Map();
|
|
128
129
|
for (const line of lines) {
|
|
129
130
|
if (seen.has(line)) continue;
|
|
130
131
|
seen.add(line);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
// Template-id refs resolve at render time; the engine from 2026-09-19 on
|
|
133
|
+
// no longer emits this line, and an older engine's copy is dropped here —
|
|
134
|
+
// the render-time endpoint check is what catches a real miss.
|
|
135
|
+
if (DYNAMIC_ID_RE.test(line)) continue;
|
|
136
|
+
// One "SVG path arrow" advice per file, naming its lines: repeated per
|
|
137
|
+
// arrow it fired 162 times in one x66 run, mostly on arcs no route draws,
|
|
138
|
+
// and buried the lines that mattered.
|
|
139
|
+
const arrow = PATH_ARROW_RE.exec(line);
|
|
140
|
+
if (arrow) {
|
|
141
|
+
const [, file, at] = arrow;
|
|
142
|
+
const prior = arrows.get(file);
|
|
143
|
+
if (prior) { prior.lines.push(at); continue; }
|
|
144
|
+
arrows.set(file, { index: out.length, lines: [at], text: line.slice(arrow[0].length) });
|
|
140
145
|
}
|
|
141
146
|
out.push(line);
|
|
142
147
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
148
|
+
for (const [file, { index, lines: at, text }] of arrows) {
|
|
149
|
+
if (at.length > 1) out[index] = ` [lint:warn] ${file}:${at.join(",")} — (${at.length}×) ${text.trim()}`;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
148
152
|
}
|
|
153
|
+
const PATH_ARROW_RE = /^\s*\[lint:warn\] ([^:\s]+\.jsx):(\d+) — (?=SVG path arrow)/;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The native list the transcriber writes. An item (`marker` on a paragraph,
|
|
2
|
+
// concept-lists.mjs) is never a glyph typed into a run or a mark parked beside
|
|
3
|
+
// a line: it is an `<li>` of an `<L>`, so the export writes a PowerPoint bullet
|
|
4
|
+
// (`<a:buChar>` / `<a:buAutoNum>`) with a hanging indent. The glyph is the
|
|
5
|
+
// design's: `.slide ul/ol` and `.slide li::marker` in the bundle's CSS carry
|
|
6
|
+
// the brand bullet (layouts-css-codegen.ts); L sets none of its own. A check
|
|
7
|
+
// list keeps its tick (`bullet="✓"`), a don't list its cross, a numbered list
|
|
8
|
+
// counts (`ordered`). The marker's paint is the reader's when it named one
|
|
9
|
+
// (`markerVar`: the concept's accent dots stay accent), else the brand's.
|
|
10
|
+
// The items' words sit where the reader measured them and the marker hangs
|
|
11
|
+
// outside, left of them (`padding-left: 0`, `list-style-position: outside`),
|
|
12
|
+
// as the painting had it. Placed by `x`/`y` it stands alone; without them it
|
|
13
|
+
// flows inside a block's `<T>` after the paragraphs above it, at its own
|
|
14
|
+
// `indent` off the block's left edge (its words' offset, the marker hanging in
|
|
15
|
+
// it), aligned by its own `align`, and the export merges title and items into
|
|
16
|
+
// ONE text frame (oox/adapter/text-merge.ts).
|
|
17
|
+
// Pure; mirrored into the add-in by scripts/sync-transcription-runtime.mjs.
|
|
18
|
+
|
|
19
|
+
export const LIST_HELPER = ` const L = ({ id, x, y, w, px, indent, align, role = "body", tone = "dark", colorVar, bullet, ordered, markerVar, stack, block, children }) => {
|
|
20
|
+
const style = { ...(x != null ? { position: "absolute", left: x, top: y, width: w } : {}), ...(indent ? { marginLeft: indent } : {}), ...(align ? { textAlign: align } : {}), margin: 0, paddingLeft: 0, boxSizing: "border-box",
|
|
21
|
+
listStylePosition: "outside", ...(ordered ? { listStyleType: "decimal" } : bullet ? { listStyleType: \`"\${bullet} "\` } : {}),
|
|
22
|
+
...(markerVar ? { "--sm-marker": markerVar } : {}), ...ROLE[role], color: colorVar ?? TONE[tone], ...(px ? { fontSize: HALF_PT(px) } : {}), lineHeight: "var(--lh-tight)" };
|
|
23
|
+
return (<>{markerVar ? <style>{".sm-list[data-marker] > li::marker { color: var(--sm-marker); }"}</style> : null}
|
|
24
|
+
{ordered ? <ol id={id} className="sm-list" data-marker={markerVar} data-sm-stack={stack} data-sm-block={block} style={style}>{children}</ol> : <ul id={id} className="sm-list" data-marker={markerVar} data-sm-stack={stack} data-sm-block={block} style={style}>{children}</ul>}</>);
|
|
25
|
+
};
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/** The `L` attributes a marker kind adds: a check or don't list's glyph, a numbered list's count. */
|
|
29
|
+
export const markerAttrs = (marker) => (marker === "check" ? ['bullet="✓"'] : marker === "cross" ? ['bullet="✗"'] : marker === "number" ? ["ordered"] : []);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The JSX children of a text block: one `<P>` per plain paragraph, a run of
|
|
33
|
+
* items with one marker as one `<L>` of `<li>`s. `para(p)` writes a plain
|
|
34
|
+
* paragraph, `attrs(p)` the role, paint, indent and marker attributes of the
|
|
35
|
+
* paragraph that opens a list, `item(p, head)` an item's text (painted like
|
|
36
|
+
* `head` unless its own paint differs). `single` is set when the block is
|
|
37
|
+
* exactly one list.
|
|
38
|
+
*/
|
|
39
|
+
export function blockChildren(paragraphs, { para, item, attrs }) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let lists = 0;
|
|
42
|
+
for (let i = 0; i < paragraphs.length; ) {
|
|
43
|
+
const head = paragraphs[i];
|
|
44
|
+
if (!head.marker) {
|
|
45
|
+
out.push(para(head));
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let j = i;
|
|
50
|
+
while (j < paragraphs.length && paragraphs[j].marker === head.marker) j += 1;
|
|
51
|
+
const items = paragraphs.slice(i, j).map((p) => `<li>${item(p, head)}</li>`).join("");
|
|
52
|
+
out.push(`<L ${[...attrs(head), ...markerAttrs(head.marker)].join(" ")}>${items}</L>`);
|
|
53
|
+
lists += 1;
|
|
54
|
+
i = j;
|
|
55
|
+
}
|
|
56
|
+
return { children: out.join(""), single: lists === 1 && out.length === 1 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A styled run inside a paragraph: the accent figure in "under 21 days", the
|
|
60
|
+
// bold term in a sentence (text-blocks.mjs `runs`). It takes its role's rung
|
|
61
|
+
// and paint from the kit, never a size of its own, so one sentence stays ONE
|
|
62
|
+
// text frame whose runs the export writes with their own size, weight and
|
|
63
|
+
// colour (dom-serialiser text runs → `<a:r>`).
|
|
64
|
+
export const RUN_HELPER = ` const R = ({ role, bold, tone, colorVar, children }) => (
|
|
65
|
+
<span style={{ ...(role ? ROLE[role] : {}), ...(colorVar || tone ? { color: colorVar ?? TONE[tone] } : {}), ...(bold ? { fontWeight: "var(--fw-bold)" } : {}) }}>{children}</span>
|
|
66
|
+
);
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `body` (escaped JSX text) with each run wrapped in `<R …>`, in order: a run
|
|
71
|
+
* is found after the one before it, never inside a tag. `attrs(run)` names its
|
|
72
|
+
* role and paint, `escape` escapes its words like the body's.
|
|
73
|
+
*/
|
|
74
|
+
export function wrapRuns(body, runs, attrs, escape) {
|
|
75
|
+
let out = body;
|
|
76
|
+
let at = 0;
|
|
77
|
+
for (const run of runs ?? []) {
|
|
78
|
+
const needle = escape(String(run.text ?? "").trim());
|
|
79
|
+
if (!needle) continue;
|
|
80
|
+
let i = out.indexOf(needle, at);
|
|
81
|
+
while (i >= 0 && out.lastIndexOf("<", i) > out.lastIndexOf(">", i)) i = out.indexOf(needle, i + 1);
|
|
82
|
+
if (i < 0) continue;
|
|
83
|
+
const tag = `<R${attrs(run).map((a) => ` ${a}`).join("")}>${needle}</R>`;
|
|
84
|
+
out = out.slice(0, i) + tag + out.slice(i + needle.length);
|
|
85
|
+
at = i + tag.length;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|