synthesisui 0.16.112 → 0.16.113
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/dist/anatomy-read.js +19 -0
- package/dist/doctor/resting.js +225 -0
- package/dist/doctor/sketch.js +99 -0
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -265,6 +265,25 @@ sketch) {
|
|
|
265
265
|
continue;
|
|
266
266
|
}
|
|
267
267
|
const node_ = { as };
|
|
268
|
+
/**
|
|
269
|
+
* THE GUARD TRAVELS WITH THE NODE, off the sketch it points at - so neither a
|
|
270
|
+
* reading nor the deriver has to carry it, and both get it.
|
|
271
|
+
*/
|
|
272
|
+
if (typeof node.at === "number") {
|
|
273
|
+
const measured = sketch?.[node.at];
|
|
274
|
+
const when = node.when ?? measured?.when;
|
|
275
|
+
if (when)
|
|
276
|
+
node_.when = when;
|
|
277
|
+
const atRest = node.atRest ?? measured?.atRest;
|
|
278
|
+
if (atRest === false)
|
|
279
|
+
node_.atRest = false;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
if (node.when)
|
|
283
|
+
node_.when = node.when;
|
|
284
|
+
if (node.atRest === false)
|
|
285
|
+
node_.atRest = false;
|
|
286
|
+
}
|
|
268
287
|
/**
|
|
269
288
|
* A PART THAT IS A LIBRARY ELEMENT CARRIES BOTH FACTS - see `AnatomyRead.from`.
|
|
270
289
|
* Inert for the renderer, which dispatches on `as`; read by the usage snippet,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IS THIS ELEMENT THERE WHEN NOBODY HAS TOUCHED THE COMPONENT?
|
|
3
|
+
*
|
|
4
|
+
* Their markup guards elements with conditions - `{isLoading && (…)}`,
|
|
5
|
+
* `{status === "success" && (…)}` - and the preview drew every one of them. So a card
|
|
6
|
+
* that is not loading wore a loading veil across its whole surface, and a status that is
|
|
7
|
+
* `waiting` showed the ping that belongs to `success` (dono, 03/08).
|
|
8
|
+
*
|
|
9
|
+
* ANSWERED FROM THEIR OWN DECLARATIONS, or not answered at all. Three facts, all
|
|
10
|
+
* measured in their source and none of them ours:
|
|
11
|
+
*
|
|
12
|
+
* a prop's default `{ status = "waiting" }`
|
|
13
|
+
* a local alias `const isLoading = status === "loading"`
|
|
14
|
+
* a state's first value `useState(defaultOpen)` → the default of `defaultOpen`
|
|
15
|
+
*
|
|
16
|
+
* `null` means the condition cannot be settled - `{tag && <Tag/>}` on a component whose
|
|
17
|
+
* `tag` has no default is optional CONTENT, and drawing it is right: 56 of their 79
|
|
18
|
+
* guards are that shape, and hiding them would empty the previews out. Only a condition
|
|
19
|
+
* that resolves to FALSE takes an element off the resting view, and the blueprint says
|
|
20
|
+
* every one of them out loud either way.
|
|
21
|
+
*/
|
|
22
|
+
const STATE = /const\s*\[\s*([A-Za-z_$][\w$]*)\s*,[^\]]*\]\s*=\s*(?:React\.)?useState[^(]*\(\s*([^)]*?)\s*\)/g;
|
|
23
|
+
const ALIAS = /const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*([^;\n]+);/g;
|
|
24
|
+
/**
|
|
25
|
+
* The aliases and first-render state values a component declares about itself.
|
|
26
|
+
*
|
|
27
|
+
* A state read is honest here for one reason: its INITIAL value is what "at rest" means.
|
|
28
|
+
* `useState(defaultOpen)` with `defaultOpen = false` says a collapsible card starts
|
|
29
|
+
* closed, which is a fact about their component rather than a guess about a user.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* EVERY DEFAULT THE DESTRUCTURING DECLARES, not only the string ones.
|
|
33
|
+
*
|
|
34
|
+
* `readDefinitionProps` reads string literals on purpose - it answers "which option is
|
|
35
|
+
* the resting one", and a boolean is not an option. Here a boolean is the whole answer:
|
|
36
|
+
* `{ defaultOpen = false }` is what says a collapsible card starts closed.
|
|
37
|
+
*/
|
|
38
|
+
function paramDefaults(body) {
|
|
39
|
+
const open = body.indexOf("({");
|
|
40
|
+
if (open === -1)
|
|
41
|
+
return {};
|
|
42
|
+
let depth = 0;
|
|
43
|
+
let end = -1;
|
|
44
|
+
for (let i = open + 1; i < body.length; i++) {
|
|
45
|
+
const c = body[i];
|
|
46
|
+
if (c === "{")
|
|
47
|
+
depth++;
|
|
48
|
+
else if (c === "}") {
|
|
49
|
+
depth--;
|
|
50
|
+
if (depth === 0) {
|
|
51
|
+
end = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (end === -1)
|
|
57
|
+
return {};
|
|
58
|
+
const out = {};
|
|
59
|
+
let depth2 = 0;
|
|
60
|
+
let part = "";
|
|
61
|
+
const take = () => {
|
|
62
|
+
const eq = part.indexOf("=");
|
|
63
|
+
if (eq > 0) {
|
|
64
|
+
const key = part.slice(0, eq).split(":")[0].trim();
|
|
65
|
+
const value = part.slice(eq + 1).trim();
|
|
66
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key) && value)
|
|
67
|
+
out[key] = value;
|
|
68
|
+
}
|
|
69
|
+
part = "";
|
|
70
|
+
};
|
|
71
|
+
for (const c of body.slice(open + 2, end)) {
|
|
72
|
+
if ("([{".includes(c))
|
|
73
|
+
depth2++;
|
|
74
|
+
else if (")]}".includes(c))
|
|
75
|
+
depth2--;
|
|
76
|
+
if (c === "," && depth2 === 0)
|
|
77
|
+
take();
|
|
78
|
+
else
|
|
79
|
+
part += c;
|
|
80
|
+
}
|
|
81
|
+
take();
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
export function restingFacts(body, defaults) {
|
|
85
|
+
const aliases = {};
|
|
86
|
+
ALIAS.lastIndex = 0;
|
|
87
|
+
for (const m of body.matchAll(ALIAS)) {
|
|
88
|
+
const value = m[2].trim();
|
|
89
|
+
// A function or a component is not a condition; only value-shaped reads help.
|
|
90
|
+
if (/=>|\bfunction\b/.test(value))
|
|
91
|
+
continue;
|
|
92
|
+
if (!aliases[m[1]])
|
|
93
|
+
aliases[m[1]] = value;
|
|
94
|
+
}
|
|
95
|
+
STATE.lastIndex = 0;
|
|
96
|
+
for (const m of body.matchAll(STATE)) {
|
|
97
|
+
if (!aliases[m[1]])
|
|
98
|
+
aliases[m[1]] = m[2].trim() || "undefined";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* THE DESTRUCTURING WINS, because it kept the quotes.
|
|
102
|
+
*
|
|
103
|
+
* `readDefinitionProps` hands back `waiting` already unquoted, and an unquoted word is
|
|
104
|
+
* indistinguishable from a reference to something else - so `status === "success"` went
|
|
105
|
+
* unsettled while the answer sat in the source. Read verbatim, `"waiting"` is a value
|
|
106
|
+
* and `defaultOpen` is a name, and the shape says which.
|
|
107
|
+
*/
|
|
108
|
+
return { defaults: { ...defaults, ...paramDefaults(body) }, aliases };
|
|
109
|
+
}
|
|
110
|
+
const LITERAL = /^(?:"([^"]*)"|'([^']*)'|`([^`]*)`)$/;
|
|
111
|
+
/** A value their code wrote, as the string a comparison would see. */
|
|
112
|
+
function literalOf(text) {
|
|
113
|
+
const t = text.trim();
|
|
114
|
+
const m = LITERAL.exec(t);
|
|
115
|
+
if (m)
|
|
116
|
+
return m[1] ?? m[2] ?? m[3] ?? "";
|
|
117
|
+
if (t === "true" || t === "false" || t === "undefined" || t === "null")
|
|
118
|
+
return t;
|
|
119
|
+
if (/^-?\d+(\.\d+)?$/.test(t))
|
|
120
|
+
return t;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Split on a top-level operator, so `a && (b || c)` reads as two halves. */
|
|
124
|
+
function splitTop(text, op) {
|
|
125
|
+
let depth = 0;
|
|
126
|
+
for (let i = 0; i < text.length - 1; i++) {
|
|
127
|
+
const c = text[i];
|
|
128
|
+
if (c === "(" || c === "[" || c === "{")
|
|
129
|
+
depth++;
|
|
130
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
131
|
+
depth--;
|
|
132
|
+
else if (depth === 0 && text.startsWith(op, i))
|
|
133
|
+
return [text.slice(0, i), text.slice(i + 2)];
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* True, false, or `null` when their own declarations do not settle it.
|
|
139
|
+
*
|
|
140
|
+
* `seen` guards a chain that refers back to itself - `const a = b; const b = a` is not
|
|
141
|
+
* something to crash on.
|
|
142
|
+
*/
|
|
143
|
+
export function holdsAtRest(condition, facts, seen = new Set()) {
|
|
144
|
+
const text = condition.trim();
|
|
145
|
+
if (!text)
|
|
146
|
+
return null;
|
|
147
|
+
if (text.startsWith("(") && text.endsWith(")")) {
|
|
148
|
+
const inner = text.slice(1, -1);
|
|
149
|
+
if (splitTop(inner, "&&") || splitTop(inner, "||") || !/[()]/.test(inner))
|
|
150
|
+
return holdsAtRest(inner, facts, seen);
|
|
151
|
+
}
|
|
152
|
+
const and = splitTop(text, "&&");
|
|
153
|
+
if (and) {
|
|
154
|
+
const [a, b] = [
|
|
155
|
+
holdsAtRest(and[0], facts, seen),
|
|
156
|
+
holdsAtRest(and[1], facts, seen),
|
|
157
|
+
];
|
|
158
|
+
if (a === false || b === false)
|
|
159
|
+
return false;
|
|
160
|
+
return a === true && b === true ? true : null;
|
|
161
|
+
}
|
|
162
|
+
const or = splitTop(text, "||");
|
|
163
|
+
if (or) {
|
|
164
|
+
const [a, b] = [
|
|
165
|
+
holdsAtRest(or[0], facts, seen),
|
|
166
|
+
holdsAtRest(or[1], facts, seen),
|
|
167
|
+
];
|
|
168
|
+
if (a === true || b === true)
|
|
169
|
+
return true;
|
|
170
|
+
return a === false && b === false ? false : null;
|
|
171
|
+
}
|
|
172
|
+
if (text.startsWith("!")) {
|
|
173
|
+
const inner = holdsAtRest(text.slice(1), facts, seen);
|
|
174
|
+
return inner === null ? null : !inner;
|
|
175
|
+
}
|
|
176
|
+
const cmp = /^(.+?)\s*(===|!==|==|!=)\s*(.+)$/.exec(text);
|
|
177
|
+
if (cmp) {
|
|
178
|
+
const left = restingValue(cmp[1], facts, seen);
|
|
179
|
+
const right = literalOf(cmp[3]) ?? restingValue(cmp[3], facts, seen);
|
|
180
|
+
if (left === null || right === null)
|
|
181
|
+
return null;
|
|
182
|
+
const same = left === right;
|
|
183
|
+
return cmp[2].startsWith("!") ? !same : same;
|
|
184
|
+
}
|
|
185
|
+
const value = restingValue(text, facts, seen);
|
|
186
|
+
if (value === null)
|
|
187
|
+
return null;
|
|
188
|
+
return (value !== "false" &&
|
|
189
|
+
value !== "undefined" &&
|
|
190
|
+
value !== "null" &&
|
|
191
|
+
value !== "");
|
|
192
|
+
}
|
|
193
|
+
/** What a name holds at rest, as a literal string, or null when nothing says. */
|
|
194
|
+
function restingValue(name, facts, seen) {
|
|
195
|
+
const key = name.trim();
|
|
196
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key) || seen.has(key))
|
|
197
|
+
return null;
|
|
198
|
+
const direct = facts.defaults[key];
|
|
199
|
+
if (direct != null) {
|
|
200
|
+
const asLiteral = literalOf(direct);
|
|
201
|
+
if (asLiteral != null)
|
|
202
|
+
return asLiteral;
|
|
203
|
+
/**
|
|
204
|
+
* `readDefinitionProps` hands back `waiting`, already unquoted, while the
|
|
205
|
+
* destructuring reads `defaultOpen` - a NAME to follow. One is a value and the other
|
|
206
|
+
* is a reference, and the shape says which.
|
|
207
|
+
*/
|
|
208
|
+
if (/^[A-Za-z_$][\w$]*$/.test(direct.trim()))
|
|
209
|
+
return restingValue(direct, facts, new Set([...seen, key]));
|
|
210
|
+
return direct.trim();
|
|
211
|
+
}
|
|
212
|
+
const alias = facts.aliases[key];
|
|
213
|
+
if (alias == null)
|
|
214
|
+
return null;
|
|
215
|
+
const next = new Set([...seen, key]);
|
|
216
|
+
const asLiteral = literalOf(alias);
|
|
217
|
+
if (asLiteral != null)
|
|
218
|
+
return asLiteral;
|
|
219
|
+
/**
|
|
220
|
+
* The alias is an expression - `const isLoading = status === "loading"`. Settling it is
|
|
221
|
+
* the same question one level down, and the answer comes back as a boolean.
|
|
222
|
+
*/
|
|
223
|
+
const held = holdsAtRest(alias, facts, next);
|
|
224
|
+
return held === null ? null : String(held);
|
|
225
|
+
}
|
package/dist/doctor/sketch.js
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* GENERALIZED BY CONSTRUCTION: it reads JSX shape, not folder layout, so it works the
|
|
19
19
|
* same in an atomic library, a feature-foldered app, or a single flat directory.
|
|
20
20
|
*/
|
|
21
|
+
import { readDefinitionProps } from "./call-sites.js";
|
|
21
22
|
import { scanTags } from "./css-modules.js";
|
|
22
23
|
import { importMap, packageOfTag } from "./imports.js";
|
|
24
|
+
import { holdsAtRest, restingFacts } from "./resting.js";
|
|
23
25
|
import { definitionSpan, returnAt } from "./transcribe.js";
|
|
24
26
|
/** `{children}` / `{props.children}` - the caller's content, spelled either way. */
|
|
25
27
|
const CHILDREN = /\{\s*(?:props\.)?children\s*\}/;
|
|
@@ -160,6 +162,87 @@ function staticCallClasses(body) {
|
|
|
160
162
|
* sitting directly between an open and its close - dynamic children are the caller's,
|
|
161
163
|
* and the slot form already says so.
|
|
162
164
|
*/
|
|
165
|
+
/**
|
|
166
|
+
* THE CONDITION THAT HAS TO HOLD FOR THIS ELEMENT TO EXIST.
|
|
167
|
+
*
|
|
168
|
+
* Their code guards markup with `{isLoading && (…)}` and `{imageUrl ? <img/> : <box/>}`,
|
|
169
|
+
* and the sketch recorded the element without the guard - so the preview drew a loading
|
|
170
|
+
* veil over a card that is not loading and a status ping for a status it does not have
|
|
171
|
+
* (dono, 03/08). Measured on their library: 76 guards across 22 of 36 components.
|
|
172
|
+
*
|
|
173
|
+
* VERBATIM, in their own vocabulary. This is not a condition we evaluate; it is a fact
|
|
174
|
+
* about their markup that the screen can say out loud ("only when `isLoading`"). The
|
|
175
|
+
* falsy branch of a ternary reads as the negation of the same expression, which is their
|
|
176
|
+
* logic restated rather than anything of ours.
|
|
177
|
+
*/
|
|
178
|
+
export function guardAt(source, at) {
|
|
179
|
+
let i = at - 1;
|
|
180
|
+
const skip = () => {
|
|
181
|
+
while (i >= 0 && /\s/.test(source[i]))
|
|
182
|
+
i--;
|
|
183
|
+
};
|
|
184
|
+
skip();
|
|
185
|
+
// `{cond && (` and `{cond ? (` both put a paren between the operator and the tag.
|
|
186
|
+
if (source[i] === "(") {
|
|
187
|
+
i--;
|
|
188
|
+
skip();
|
|
189
|
+
}
|
|
190
|
+
let negated = false;
|
|
191
|
+
if (source[i] === "&" && source[i - 1] === "&")
|
|
192
|
+
i -= 2;
|
|
193
|
+
else if (source[i] === "?")
|
|
194
|
+
i -= 1;
|
|
195
|
+
else if (source[i] === ":") {
|
|
196
|
+
// The ELSE half of `cond ? <a/> : <b/>` - it exists when the condition does not.
|
|
197
|
+
negated = true;
|
|
198
|
+
i -= 1;
|
|
199
|
+
// Walk back over the truthy branch to reach the `?`.
|
|
200
|
+
let depth = 0;
|
|
201
|
+
while (i >= 0) {
|
|
202
|
+
const c = source[i];
|
|
203
|
+
if (c === ">" || c === ")" || c === "}")
|
|
204
|
+
depth++;
|
|
205
|
+
else if (c === "<" || c === "(" || c === "{") {
|
|
206
|
+
if (depth === 0)
|
|
207
|
+
return null;
|
|
208
|
+
depth--;
|
|
209
|
+
}
|
|
210
|
+
else if (c === "?" && depth === 0)
|
|
211
|
+
break;
|
|
212
|
+
i--;
|
|
213
|
+
}
|
|
214
|
+
if (i < 0 || source[i] !== "?")
|
|
215
|
+
return null;
|
|
216
|
+
i -= 1;
|
|
217
|
+
}
|
|
218
|
+
else
|
|
219
|
+
return null;
|
|
220
|
+
// Back to the `{` that opened the expression, minding nesting on the way.
|
|
221
|
+
let depth = 0;
|
|
222
|
+
const end = i;
|
|
223
|
+
while (i >= 0) {
|
|
224
|
+
const c = source[i];
|
|
225
|
+
if (c === ")" || c === "]" || c === "}")
|
|
226
|
+
depth++;
|
|
227
|
+
else if (c === "(" || c === "[") {
|
|
228
|
+
if (depth === 0)
|
|
229
|
+
return null;
|
|
230
|
+
depth--;
|
|
231
|
+
}
|
|
232
|
+
else if (c === "{") {
|
|
233
|
+
if (depth === 0)
|
|
234
|
+
break;
|
|
235
|
+
depth--;
|
|
236
|
+
}
|
|
237
|
+
i--;
|
|
238
|
+
}
|
|
239
|
+
if (i < 0 || source[i] !== "{")
|
|
240
|
+
return null;
|
|
241
|
+
const cond = source.slice(i + 1, end + 1).trim();
|
|
242
|
+
if (!cond || cond.length > 90 || cond.includes("\n"))
|
|
243
|
+
return null;
|
|
244
|
+
return negated ? `!(${cond})` : cond;
|
|
245
|
+
}
|
|
163
246
|
export function sketchOf(source, name) {
|
|
164
247
|
/**
|
|
165
248
|
* READ THE IMPORTS BEFORE SLICING - they live at the top of the file, and the
|
|
@@ -189,6 +272,14 @@ export function sketchOf(source, name) {
|
|
|
189
272
|
*/
|
|
190
273
|
const ownReturn = returnAt(source, name);
|
|
191
274
|
const rootAt = ownReturn === -1 ? -1 : source.indexOf("<", ownReturn);
|
|
275
|
+
/**
|
|
276
|
+
* WHAT THE COMPONENT DECLARES ABOUT ITSELF, read once for the whole sketch.
|
|
277
|
+
*
|
|
278
|
+
* Against the SLICED source, so a file that declares two components does not lend one
|
|
279
|
+
* of them the other's defaults - `RadioCard.tsx` declares `RadioCard` and
|
|
280
|
+
* `RadioCardGroup`, and that is the same slice the node indices are numbered against.
|
|
281
|
+
*/
|
|
282
|
+
const facts = restingFacts(source, readDefinitionProps(name ?? "", source).defaults);
|
|
192
283
|
const events = scanTags(source);
|
|
193
284
|
const out = [];
|
|
194
285
|
let depth = 0;
|
|
@@ -210,6 +301,14 @@ export function sketchOf(source, name) {
|
|
|
210
301
|
if (pkg)
|
|
211
302
|
node.from = pkg;
|
|
212
303
|
}
|
|
304
|
+
// What has to be true for this element to exist at all - see `guardAt`.
|
|
305
|
+
const guard = guardAt(source, event.at);
|
|
306
|
+
if (guard) {
|
|
307
|
+
node.when = guard;
|
|
308
|
+
// And whether their own defaults settle it - see `holdsAtRest`.
|
|
309
|
+
if (holdsAtRest(guard, facts) === false)
|
|
310
|
+
node.atRest = false;
|
|
311
|
+
}
|
|
213
312
|
const cls = CLASS_ATTR.exec(event.body);
|
|
214
313
|
if (cls?.[1].trim())
|
|
215
314
|
node.classes = cls[1].trim();
|
package/package.json
CHANGED