@symbiote-native/engine 0.2.0 → 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/README.md +24 -11
- package/build/accessibility-props.d.ts +19 -0
- package/build/accessibility-props.js +209 -0
- package/build/action-sheet-ios/index.js +4 -1
- package/build/animated/animations/composition.js +3 -1
- package/build/animated/animations/spring-config.js +4 -1
- package/build/animated/animations/spring.js +23 -9
- package/build/animated/animations/timing.js +3 -2
- package/build/animated/bezier.js +2 -1
- package/build/animated/color.js +4 -2
- package/build/animated/event.js +7 -3
- package/build/animated/graph.js +8 -3
- package/build/animated/index.d.ts +2 -2
- package/build/animated/index.js +2 -2
- package/build/animated/interpolation.js +7 -2
- package/build/animated/mock.js +1 -1
- package/build/animated/native/native-animated.js +1 -1
- package/build/animated/operators.js +27 -6
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +18 -3
- package/build/animated/style.js +13 -3
- package/build/animated/value-xy.d.ts +8 -5
- package/build/animated/value-xy.js +4 -1
- package/build/app-registry/index.d.ts +1 -0
- package/build/app-registry/index.js +15 -5
- package/build/appearance/index.js +4 -1
- package/build/commit.d.ts +35 -0
- package/build/commit.js +613 -74
- package/build/debug.js +10 -4
- package/build/events/index.js +142 -88
- package/build/fabric-props.js +171 -11
- package/build/fabric.d.ts +10 -3
- package/build/fabric.js +11 -2
- package/build/host-behavior.d.ts +47 -0
- package/build/host-behavior.js +262 -0
- package/build/host-instance/index.d.ts +2 -10
- package/build/host-instance/index.js +13 -46
- package/build/image-loader.js +3 -2
- package/build/index.d.ts +24 -17
- package/build/index.js +27 -13
- package/build/interaction-manager/index.js +2 -1
- package/build/layout-animation/index.js +12 -4
- package/build/native-events.js +2 -1
- package/build/native-modules/index.js +3 -1
- package/build/node.d.ts +121 -2
- package/build/node.js +623 -41
- package/build/pan-responder/index.js +20 -9
- package/build/permissions-android/index.js +3 -1
- package/build/platform/index.android.d.ts +1 -1
- package/build/platform/index.android.js +1 -1
- package/build/platform/index.ios.d.ts +1 -1
- package/build/platform-color/index.js +3 -1
- package/build/post-commit.d.ts +1 -0
- package/build/post-commit.js +6 -0
- package/build/process-background-image/index.js +30 -9
- package/build/process-filter.js +11 -4
- package/build/registry.js +10 -2
- package/build/report-error.js +4 -1
- package/build/share/index.android.js +1 -1
- package/build/share/index.ios.js +1 -1
- package/build/status-bar/index.android.js +3 -4
- package/build/status-bar/index.ios.js +1 -1
- package/build/style-registry/index.d.ts +11 -2
- package/build/style-registry/index.js +223 -169
- package/build/style-registry/scope.d.ts +15 -1
- package/build/style-registry/scope.js +29 -27
- package/build/styles.d.ts +11 -1
- package/build/surface.d.ts +1 -1
- package/build/surface.js +8 -0
- package/build/tags.d.ts +1 -0
- package/build/tags.js +31 -1
- package/build/touch-history.js +9 -2
- package/build/vibration/index.ios.js +1 -1
- package/build/view-config.js +20 -2
- package/package.json +12 -2
|
@@ -1,55 +1,146 @@
|
|
|
1
1
|
// Runtime style registry. Side-effect CSS imports (compiled by the sibling CSS-to-style
|
|
2
|
-
// build package, not this module) call
|
|
3
|
-
// look them up via resolveClassName(). No CSS parsing here - just
|
|
2
|
+
// build package, not this module) call registerRules() with the token SET each selector
|
|
3
|
+
// names; components look them up via resolveClassName(). No CSS parsing here - just an
|
|
4
|
+
// inverted Map<token, rules> and a subset test.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// A rule matches when its tokens are a SUBSET of the element's class list, in any order and
|
|
7
|
+
// at any length, and the matches cascade by (derived-scope, specificity, epoch, order).
|
|
8
|
+
// That replaced an earlier flat Map<key, style>, where the compiler COLLAPSED a selector's
|
|
9
|
+
// tokens into one camelCase name (`.a.b` -> `aB`) and the runtime guessed the split back
|
|
10
|
+
// apart by permuting the class string: 60 built keys on a 4-token miss, a camelCase
|
|
11
|
+
// round-trip that could not survive a scope suffix, no real cascade, and a silent cliff at
|
|
12
|
+
// 5 tokens where the compound branch stopped running at all. Tokens now arrive AS AUTHORED,
|
|
13
|
+
// so there is nothing left to guess.
|
|
14
|
+
// The scope tail as OUR OWN compilers emit it — all three shapes, the hash being css-parser's
|
|
15
|
+
// base36 hashFilePath:
|
|
16
|
+
// `card__data-v-<h>` Vue `<style scoped>`
|
|
17
|
+
// `card__svelte-<h>` Svelte `<style>`
|
|
18
|
+
// `card__module__<h>` CSS Modules — BOTH a standalone `.module.css` and Vue's `<style module>`
|
|
19
|
+
// (A preprocessor source — SCSS/Less/Stylus — reduces to plain CSS before any of this, so it
|
|
20
|
+
// carries whichever of the three its host block uses; there is no fourth shape.)
|
|
8
21
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
// `
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const SCOPE_SEPARATOR = '__';
|
|
25
|
-
const SCOPE_SUFFIX_PATTERN = /^(?:data-v|svelte)-[0-9a-z]+$/;
|
|
22
|
+
// Matched by SHAPE, never by "there is a `__` somewhere in the name" — a BEM class
|
|
23
|
+
// (`card__title`) must never be read as a scoped `card`, which would silently merge the block's
|
|
24
|
+
// styles into the element's.
|
|
25
|
+
//
|
|
26
|
+
// Matched as ONE TAIL UNIT anchored at the end, rather than as "split on the last `__`, then
|
|
27
|
+
// shape-test what follows": the module form carries its own `__`, so a `lastIndexOf` split cuts
|
|
28
|
+
// `badge__module__<h>` into base `badge__module` + scope `<h>` and the base it hands back names
|
|
29
|
+
// no class. Widening the shape test alone would NOT fix that — the split POINT is wrong, not the
|
|
30
|
+
// alphabet.
|
|
31
|
+
//
|
|
32
|
+
// Note this suffix scheme is ours, not the frameworks'. Svelte appends a SEPARATE token
|
|
33
|
+
// (`class="card svelte-<h>"`) and Vue `<style scoped>` renames nothing at all (it adds a
|
|
34
|
+
// `data-v-<h>` attribute). We have no DOM and no attribute matching, so a scope is expressed by
|
|
35
|
+
// renaming the token; the names only borrow their vocabulary.
|
|
36
|
+
const SCOPE_TAIL_PATTERN = /__((?:data-v|svelte)-[0-9a-z]+|module__[0-9a-z]+)$/;
|
|
26
37
|
function splitScopedToken(token) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const scope = token.slice(separator + SCOPE_SEPARATOR.length);
|
|
31
|
-
if (!SCOPE_SUFFIX_PATTERN.test(scope))
|
|
38
|
+
const match = SCOPE_TAIL_PATTERN.exec(token);
|
|
39
|
+
// index 0 would mean the token is nothing BUT a scope tail, which names no class.
|
|
40
|
+
if (match === null || match.index <= 0)
|
|
32
41
|
return null;
|
|
33
|
-
return { base: token.slice(0,
|
|
42
|
+
return { base: token.slice(0, match.index), scope: match[1] };
|
|
43
|
+
}
|
|
44
|
+
// Inverted index, ONE bucket per rule rather than one per token: a rule can only match when the
|
|
45
|
+
// element carries every one of its tokens, so any single token of it is a sufficient hook. The
|
|
46
|
+
// alternative (indexing under all of them) surfaces the same rule once per shared token and buys
|
|
47
|
+
// nothing but a dedupe pass.
|
|
48
|
+
const ruleIndex = new Map();
|
|
49
|
+
let ruleEpoch = 0;
|
|
50
|
+
// resolveClassName runs from routeProp on every class-prop WRITE, per node, and a screen only
|
|
51
|
+
// ever uses a few dozen distinct class strings — so after warm-up the whole resolution collapses
|
|
52
|
+
// to one Map.get. Only the STRING branch is memoized: an object/array argument has no stable key
|
|
53
|
+
// (a fresh literal every render) and would just fill the map.
|
|
54
|
+
//
|
|
55
|
+
// Safe to hand the SAME object out repeatedly because no caller mutates it — every consumer
|
|
56
|
+
// (routeProp's commitClassStyle, and the ScrollView / VirtualizedList / FlatList / ImageBackground
|
|
57
|
+
// class-to-style props in all five adapters) spreads it or drops it into a style array, and
|
|
58
|
+
// flattenStyle shallow-copies.
|
|
59
|
+
const resolvedCache = new Map();
|
|
60
|
+
// Bounded so a screen generating unique class strings at runtime cannot grow it without limit.
|
|
61
|
+
// Overflow drops everything rather than evicting one entry: the cache is a warm-up optimization,
|
|
62
|
+
// not a working set, and an LRU's bookkeeping costs more than the rebuild it saves.
|
|
63
|
+
const RESOLVED_CACHE_LIMIT = 512;
|
|
64
|
+
// The pressed variant, keyed by the SAME authored string. Separate from `resolvedCache` so the two
|
|
65
|
+
// can never collide on one key, and string-keyed for the same reason that one is: identity is the
|
|
66
|
+
// whole point. `isAlreadyPublished` (node.ts) turns a re-push away by comparing slot 0 with
|
|
67
|
+
// Object.is, so a thousand unpressed rows only cost zero writes if they all publish the SAME
|
|
68
|
+
// object. Resolve an array or an object instead and every call returns a fresh one, the guard can
|
|
69
|
+
// never fire, and the re-push storm it was added for comes back with a new cause.
|
|
70
|
+
//
|
|
71
|
+
// This doubles the distinct strings a screen can hold. A SECOND state would multiply them, and the
|
|
72
|
+
// cache clears WHOLE on overflow rather than evicting — so a drop breaks identity for every node at
|
|
73
|
+
// once. That is the wall a `:focus` or `:hover` would hit, and it would read as an unexplained
|
|
74
|
+
// regression rather than as a cache limit.
|
|
75
|
+
const activeCache = new Map();
|
|
76
|
+
// The token the css-parser emits for `:active` (its `STATE_TOKEN`). Duplicated as a literal rather
|
|
77
|
+
// than imported: the engine does not depend on the parser, and a CSS identifier cannot carry an
|
|
78
|
+
// unescaped `:`, so this can never collide with a real class name.
|
|
79
|
+
const STATE_TOKEN = ':active';
|
|
80
|
+
// Whether ANY registered rule carries the state token. Not derivable from `ruleIndex`, which hooks
|
|
81
|
+
// a rule on `tokens[0]` — for `.btn:active` that is `btn`, so `ruleIndex.has(':active')` is always
|
|
82
|
+
// false and a gate built on it silently disables the whole feature. Found by a test, not by
|
|
83
|
+
// reading: the wrong gate makes pressing a no-op with everything green.
|
|
84
|
+
let hasActiveRules = false;
|
|
85
|
+
// Every registration is a cascade change, so nothing resolved before it can be trusted. Clearing
|
|
86
|
+
// wholesale beats versioning each entry — registration happens at import time, resolution happens
|
|
87
|
+
// per commit, and only the second one is hot.
|
|
88
|
+
function invalidateResolved() {
|
|
89
|
+
resolvedCache.clear();
|
|
90
|
+
activeCache.clear();
|
|
34
91
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
92
|
+
export function registerRules(rules) {
|
|
93
|
+
invalidateResolved();
|
|
94
|
+
const epoch = ruleEpoch++;
|
|
95
|
+
for (const rule of rules) {
|
|
96
|
+
if (rule.tokens.includes(STATE_TOKEN))
|
|
97
|
+
hasActiveRules = true;
|
|
98
|
+
const hook = rule.tokens[0];
|
|
99
|
+
// The empty set is a subset of every element's tokens, so a token-less rule would paint
|
|
100
|
+
// everything. It names no class and can only come from a broken compile.
|
|
101
|
+
if (hook === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
const bucket = ruleIndex.get(hook);
|
|
104
|
+
if (bucket === undefined) {
|
|
105
|
+
ruleIndex.set(hook, [{ rule, epoch }]);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
bucket.push({ rule, epoch });
|
|
41
109
|
}
|
|
42
110
|
}
|
|
43
111
|
// Used between tests to reset registry state.
|
|
44
112
|
export function clearGlobalStyles() {
|
|
45
|
-
|
|
113
|
+
ruleIndex.clear();
|
|
114
|
+
hasActiveRules = false;
|
|
115
|
+
ruleEpoch = 0;
|
|
116
|
+
invalidateResolved();
|
|
46
117
|
}
|
|
47
118
|
// A `class`/`className` prop arrives as `unknown` at the routeProp boundary (any adapter can
|
|
48
119
|
// hand over anything); this narrows before resolveClassName without an `as` cast. Shared with
|
|
49
120
|
// adapters/vue/src/components/scroll-view/shared.ts's identical need, rather than each keeping
|
|
50
121
|
// its own copy - that file imports this one instead of redeclaring it.
|
|
51
122
|
export function isClassNameValue(value) {
|
|
52
|
-
return typeof value === 'string' || (typeof value === 'object' && value !== null);
|
|
123
|
+
return (typeof value === 'string' || (typeof value === 'object' && value !== null));
|
|
124
|
+
}
|
|
125
|
+
// An ARRAY of plain strings joined into one, so it takes the memoised string path like any other
|
|
126
|
+
// class. Everything downstream keys off identity — `resolveClassName`'s cache, the `:active`
|
|
127
|
+
// variant, and `isAlreadyPublished`'s Object.is on slot 0 — and the array branch below reduces
|
|
128
|
+
// into a FRESH object on every call, so an array class silently opted out of all three: no pressed
|
|
129
|
+
// styling, and a re-push that re-dirties the node on every update.
|
|
130
|
+
//
|
|
131
|
+
// Only an ALL-STRING array converts. A mixed array carries IResolvedStyle entries — the channel
|
|
132
|
+
// ScrollView / VirtualizedList / FlatList / ImageBackground use to pass a style through the class
|
|
133
|
+
// prop — and those are not tokens; folding them into a string would be a category error rather
|
|
134
|
+
// than a fix. No adapter produces an all-string array today (Vue's createVNode normalises class to
|
|
135
|
+
// a string, Angular joins its accumulated tokens, React's className is a string by convention), so
|
|
136
|
+
// this closes a gap an APPLICATION can reach rather than one we can.
|
|
137
|
+
export function canonicalClassName(className) {
|
|
138
|
+
if (typeof className === 'string' || !Array.isArray(className)) {
|
|
139
|
+
return className;
|
|
140
|
+
}
|
|
141
|
+
return className.every(part => typeof part === 'string')
|
|
142
|
+
? className.join(' ')
|
|
143
|
+
: className;
|
|
53
144
|
}
|
|
54
145
|
export function resolveClassName(className) {
|
|
55
146
|
if (!className)
|
|
@@ -58,150 +149,113 @@ export function resolveClassName(className) {
|
|
|
58
149
|
return className;
|
|
59
150
|
}
|
|
60
151
|
if (Array.isArray(className)) {
|
|
152
|
+
// Direct callers (an adapter resolving a class prop itself) get the same canonicalisation as
|
|
153
|
+
// routeProp's path, so an all-string array is memoised for them too.
|
|
154
|
+
const canonical = canonicalClassName(className);
|
|
155
|
+
if (typeof canonical === 'string')
|
|
156
|
+
return resolveClassName(canonical);
|
|
61
157
|
return className.reduce((acc, item) => {
|
|
62
158
|
return { ...acc, ...resolveClassName(item) };
|
|
63
159
|
}, {});
|
|
64
160
|
}
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return resolveOne(trimmed);
|
|
74
|
-
const exactMatch = lookupKey(trimmed);
|
|
75
|
-
if (exactMatch)
|
|
76
|
-
return exactMatch;
|
|
77
|
-
const merged = parts.reduce((acc, cls) => {
|
|
78
|
-
return { ...acc, ...resolveOne(cls) };
|
|
79
|
-
}, {});
|
|
80
|
-
// A compound rule LAYERS OVER the single-class rules rather than replacing them, matching the
|
|
81
|
-
// cascade: `.card { padding: 8; background: white }` + `.card.big { padding: 16 }` on
|
|
82
|
-
// `class="card big"` is padding 16 AND background white. Returning the compound alone (what
|
|
83
|
-
// this did before) silently dropped every property the compound did not itself restate.
|
|
84
|
-
if (parts.length >= COMPOUND_MIN_PARTS && parts.length <= COMPOUND_MAX_PARTS) {
|
|
85
|
-
const compound = tryCompoundLookup(parts);
|
|
86
|
-
if (compound)
|
|
87
|
-
return { ...merged, ...compound };
|
|
88
|
-
}
|
|
89
|
-
return merged;
|
|
161
|
+
const cached = resolvedCache.get(className);
|
|
162
|
+
if (cached !== undefined)
|
|
163
|
+
return cached;
|
|
164
|
+
const resolved = resolveClassString(className);
|
|
165
|
+
if (resolvedCache.size >= RESOLVED_CACHE_LIMIT)
|
|
166
|
+
resolvedCache.clear();
|
|
167
|
+
resolvedCache.set(className, resolved);
|
|
168
|
+
return resolved;
|
|
90
169
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (let size = COMPOUND_MIN_PARTS; size <= parts.length; size++) {
|
|
96
|
-
compounds.push(...generateKPermutations(parts, size));
|
|
97
|
-
}
|
|
98
|
-
return compounds;
|
|
99
|
-
}
|
|
100
|
-
function generateKPermutations(parts, size) {
|
|
101
|
-
if (size === 0)
|
|
102
|
-
return [[]];
|
|
103
|
-
if (parts.length === 0)
|
|
104
|
-
return [];
|
|
105
|
-
const result = [];
|
|
106
|
-
function helper(current, remaining, depth) {
|
|
107
|
-
if (depth === size) {
|
|
108
|
-
result.push(current);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
for (let i = 0; i < remaining.length; i++) {
|
|
112
|
-
helper([...current, remaining[i]], remaining.slice(0, i).concat(remaining.slice(i + 1)), depth + 1);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
helper([], parts, 0);
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
// Compound permutations join as camelCase ("btn primary" -> "btnPrimary") because this
|
|
119
|
-
// repo's CSS-to-style compiler emits plain camelCase keys for every class, single or
|
|
120
|
-
// compound, so "btn primary" must resolve against a registered "btnPrimary".
|
|
121
|
-
function toCompoundKey(parts) {
|
|
122
|
-
return parts.reduce((key, part, index) => (index === 0 ? part : key + capitalize(part)), '');
|
|
123
|
-
}
|
|
124
|
-
function capitalize(value) {
|
|
125
|
-
return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
|
|
126
|
-
}
|
|
127
|
-
// Duplicated from @symbiote-native/css-parser's identical helper rather than imported: css-parser
|
|
128
|
-
// pulls in postcss and is build-time only (never shipped in the app bundle), and this registry
|
|
129
|
-
// is the opposite - pure runtime, in every app bundle - so importing it here would leak a
|
|
130
|
-
// build-time dependency into the shipped app. The conversion itself is two lines; keeping both
|
|
131
|
-
// copies in sync is a smaller cost than the alternative.
|
|
170
|
+
// The same element's style with `:active` added to its token list — so a `.btn:active` rule joins
|
|
171
|
+
// the cascade exactly as its specificity says, merged by the SAME matcher. That makes the result a
|
|
172
|
+
// complete REPLACEMENT for the unpressed style rather than an overlay, which is why no extra style
|
|
173
|
+
// slot is needed on the node: pressing swaps what sits in slot 0.
|
|
132
174
|
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
175
|
+
// Non-string input resolves to the plain style, unchanged. An object or array has no stable key to
|
|
176
|
+
// cache under, and the guard above is worthless without one.
|
|
177
|
+
export function resolveActiveClassName(className) {
|
|
178
|
+
if (typeof className !== 'string')
|
|
179
|
+
return resolveClassName(className);
|
|
180
|
+
// No `:active` rule anywhere in the app — hand back the very SAME object the unpressed path
|
|
181
|
+
// returns, not an equal one. Two cache keys give two objects even when their contents are
|
|
182
|
+
// byte-identical, and `isAlreadyPublished` compares with Object.is: pressing a node that has no
|
|
183
|
+
// pressed styling would otherwise publish a fresh base, dirty the node and pay a walk for a
|
|
184
|
+
// payload Fabric then rejects as unchanged. One node rather than a thousand, so not a storm —
|
|
185
|
+
// but it is a cost for literally nothing, and this makes the whole feature free for every app
|
|
186
|
+
// that does not use it.
|
|
187
|
+
if (!hasActiveRules)
|
|
188
|
+
return resolveClassName(className);
|
|
189
|
+
const cached = activeCache.get(className);
|
|
190
|
+
if (cached !== undefined)
|
|
191
|
+
return cached;
|
|
192
|
+
const parts = className.trim().split(/\s+/).filter(Boolean);
|
|
193
|
+
const resolved = parts.length === 0 ? {} : (matchRules([...parts, STATE_TOKEN]) ?? {});
|
|
194
|
+
if (activeCache.size >= RESOLVED_CACHE_LIMIT)
|
|
195
|
+
activeCache.clear();
|
|
196
|
+
activeCache.set(className, resolved);
|
|
197
|
+
return resolved;
|
|
150
198
|
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
|
|
199
|
+
function resolveClassString(className) {
|
|
200
|
+
const parts = className.trim().split(/\s+/).filter(Boolean);
|
|
201
|
+
if (parts.length === 0)
|
|
202
|
+
return {};
|
|
203
|
+
return matchRules(parts) ?? {};
|
|
154
204
|
}
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// written for: `.card :global(.reset)` registers the key `cardReset`, which the scoper suffixes
|
|
165
|
-
// as a whole (the escape hatch exempts the `reset` MARKUP TOKEN, not the collapsed key), while
|
|
166
|
-
// the element carries `card__svelte-h reset`. The same shape covers a class handed down from a
|
|
167
|
-
// parent component, which likewise arrives unsuffixed.
|
|
168
|
-
//
|
|
169
|
-
// Widening, stated so it stays deliberate: a fully-scoped `.card.reset` collapses to that same
|
|
170
|
-
// key, so an element carrying a FOREIGN `reset` now matches a rule the author scoped to their
|
|
171
|
-
// own. The key format cannot tell the two apart — telling them apart needs a registry indexed by
|
|
172
|
-
// token set, with per-token scope. The scopes still have to agree: two tokens from two different
|
|
173
|
-
// components have no single suffix to factor out and stay unmatched.
|
|
174
|
-
function scopedCompoundKey(subset) {
|
|
175
|
-
const bases = [];
|
|
176
|
-
let scope;
|
|
177
|
-
for (const token of subset) {
|
|
178
|
-
const split = splitScopedToken(token);
|
|
179
|
-
if (split === null) {
|
|
180
|
-
bases.push(token);
|
|
205
|
+
// `null` rather than `{}` for "nothing matched", so the caller can skip the empty-object churn on
|
|
206
|
+
// every class-prop set that has no rules.
|
|
207
|
+
function matchRules(parts) {
|
|
208
|
+
const literal = new Set(parts);
|
|
209
|
+
const tokens = elementTokens(parts);
|
|
210
|
+
let matched = null;
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
const bucket = ruleIndex.get(token);
|
|
213
|
+
if (bucket === undefined)
|
|
181
214
|
continue;
|
|
215
|
+
for (const indexed of bucket) {
|
|
216
|
+
if (!indexed.rule.tokens.every(needed => tokens.has(needed)))
|
|
217
|
+
continue;
|
|
218
|
+
matched ??= [];
|
|
219
|
+
matched.push({
|
|
220
|
+
indexed,
|
|
221
|
+
derivedTokens: indexed.rule.tokens.filter(token => !literal.has(token))
|
|
222
|
+
.length,
|
|
223
|
+
});
|
|
182
224
|
}
|
|
183
|
-
if (scope !== undefined && split.scope !== scope)
|
|
184
|
-
return null;
|
|
185
|
-
scope = split.scope;
|
|
186
|
-
bases.push(split.base);
|
|
187
225
|
}
|
|
188
|
-
|
|
226
|
+
if (matched === null)
|
|
227
|
+
return null;
|
|
228
|
+
matched.sort(byCascadeOrder);
|
|
229
|
+
return matched.reduce((acc, match) => ({ ...acc, ...match.indexed.rule.style }), {});
|
|
189
230
|
}
|
|
190
|
-
// A scoped token
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return { ...lookupKey(split.base), ...scoped };
|
|
231
|
+
// A scoped token contributes its BASE as well: the scope is expressed by RENAMING the token
|
|
232
|
+
// rather than adding a second one, so a global `.card` in App.css only survives a component
|
|
233
|
+
// defining its own `.card` if the base is put back into the set here. On the web the element
|
|
234
|
+
// would carry both names and no such step would exist.
|
|
235
|
+
function elementTokens(parts) {
|
|
236
|
+
const tokens = new Set();
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
tokens.add(part);
|
|
239
|
+
const split = splitScopedToken(part);
|
|
240
|
+
if (split !== null)
|
|
241
|
+
tokens.add(split.base);
|
|
242
|
+
}
|
|
243
|
+
return tokens;
|
|
204
244
|
}
|
|
205
|
-
|
|
206
|
-
|
|
245
|
+
// Ascending, because the caller merges left to right: the winner has to be spread LAST. Ties
|
|
246
|
+
// break the way the cascade does — later import first, then later line in the file.
|
|
247
|
+
function byCascadeOrder(left, right) {
|
|
248
|
+
if (left.derivedTokens !== right.derivedTokens) {
|
|
249
|
+
return right.derivedTokens - left.derivedTokens;
|
|
250
|
+
}
|
|
251
|
+
const leftRule = left.indexed.rule;
|
|
252
|
+
const rightRule = right.indexed.rule;
|
|
253
|
+
for (let index = 0; index < leftRule.specificity.length; index++) {
|
|
254
|
+
const diff = leftRule.specificity[index] - rightRule.specificity[index];
|
|
255
|
+
if (diff !== 0)
|
|
256
|
+
return diff;
|
|
257
|
+
}
|
|
258
|
+
return left.indexed.epoch === right.indexed.epoch
|
|
259
|
+
? leftRule.order - rightRule.order
|
|
260
|
+
: left.indexed.epoch - right.indexed.epoch;
|
|
207
261
|
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
export type IClassToggleMap = Record<string, boolean | undefined>;
|
|
2
2
|
export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
|
|
3
|
-
export
|
|
3
|
+
export type IClassNameRenames = Readonly<Record<string, string>>;
|
|
4
|
+
/**
|
|
5
|
+
* Rewrites every class token the style compiler renamed, leaving the rest alone.
|
|
6
|
+
*
|
|
7
|
+
* The names come from the compiler's own rename table, emitted verbatim, rather than being
|
|
8
|
+
* RECOMPUTED here as `token + '__' + scopeId`. Recomputing made the runtime a second,
|
|
9
|
+
* independent implementation of "what is this class called now" beside the style compiler —
|
|
10
|
+
* they agreed by construction until a rename mechanism changed under one of them. lightningcss
|
|
11
|
+
* renames the class; this reads the result.
|
|
12
|
+
*
|
|
13
|
+
* A token absent from the table is left alone: it belongs to another file (a `:global()` escape
|
|
14
|
+
* hatch, App.css, a class handed down by a parent), and the registry resolves it under its own
|
|
15
|
+
* authored name.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renameClassTokens(value: IScopableClassValue, renames: IClassNameRenames): IScopableClassValue;
|
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// no
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// Class-name rewriter for scoped styles. Distinct responsibility from the sibling ./index.ts
|
|
2
|
+
// (the CSS class -> style registry): this module does pure NAME rewriting, no registry lookup,
|
|
3
|
+
// no CSS parsing. It runs at the compiled call site of a scoped-style template -
|
|
4
|
+
// `adapters/vue/metro-vue-transformer.cjs` emits calls to renameClassTokens (imported there as
|
|
5
|
+
// `__scopeClass`) - BEFORE Vue's own normalizeClass() collapses string/object/array `class`
|
|
6
|
+
// values to a final string, so it must pre-process all three shapes normalizeClass understands.
|
|
7
|
+
// resolveClassName in ./index.ts still does the actual style lookup, unchanged, against the
|
|
8
|
+
// rewritten name.
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites every class token the style compiler renamed, leaving the rest alone.
|
|
11
|
+
*
|
|
12
|
+
* The names come from the compiler's own rename table, emitted verbatim, rather than being
|
|
13
|
+
* RECOMPUTED here as `token + '__' + scopeId`. Recomputing made the runtime a second,
|
|
14
|
+
* independent implementation of "what is this class called now" beside the style compiler —
|
|
15
|
+
* they agreed by construction until a rename mechanism changed under one of them. lightningcss
|
|
16
|
+
* renames the class; this reads the result.
|
|
17
|
+
*
|
|
18
|
+
* A token absent from the table is left alone: it belongs to another file (a `:global()` escape
|
|
19
|
+
* hatch, App.css, a class handed down by a parent), and the registry resolves it under its own
|
|
20
|
+
* authored name.
|
|
21
|
+
*/
|
|
22
|
+
export function renameClassTokens(value, renames) {
|
|
13
23
|
if (value === undefined || value === null)
|
|
14
24
|
return value;
|
|
15
25
|
if (Array.isArray(value)) {
|
|
16
|
-
return value.map(item =>
|
|
26
|
+
return value.map(item => renameClassEntry(item, renames));
|
|
17
27
|
}
|
|
18
|
-
return
|
|
28
|
+
return renameClassEntry(value, renames);
|
|
19
29
|
}
|
|
20
|
-
|
|
21
|
-
// authoring form (`section-label`) - normalize to camelCase FIRST, then decide scoping, so
|
|
22
|
-
// `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
|
|
23
|
-
// kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
|
|
24
|
-
function scopeToken(token, localNames, scopeId) {
|
|
25
|
-
const camelToken = kebabToCamel(token);
|
|
26
|
-
return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
|
|
27
|
-
}
|
|
28
|
-
function scopeClassEntry(value, localNames, scopeId) {
|
|
30
|
+
function renameClassEntry(value, renames) {
|
|
29
31
|
if (typeof value === 'object') {
|
|
30
|
-
const
|
|
32
|
+
const renamed = {};
|
|
31
33
|
for (const [name, enabled] of Object.entries(value)) {
|
|
32
|
-
|
|
34
|
+
renamed[renames[name] ?? name] = enabled;
|
|
33
35
|
}
|
|
34
|
-
return
|
|
36
|
+
return renamed;
|
|
35
37
|
}
|
|
36
38
|
return value
|
|
37
39
|
.split(/\s+/)
|
|
38
40
|
.filter(Boolean)
|
|
39
|
-
.map(token =>
|
|
41
|
+
.map(token => renames[token] ?? token)
|
|
40
42
|
.join(' ');
|
|
41
43
|
}
|
package/build/styles.d.ts
CHANGED
|
@@ -187,7 +187,11 @@ export interface IViewStyle {
|
|
|
187
187
|
shadowRadius?: number;
|
|
188
188
|
elevation?: number;
|
|
189
189
|
transform?: ITransformProp[];
|
|
190
|
-
transformOrigin?: [
|
|
190
|
+
transformOrigin?: [
|
|
191
|
+
string | number,
|
|
192
|
+
string | number,
|
|
193
|
+
string | number
|
|
194
|
+
] | string;
|
|
191
195
|
boxShadow?: IBoxShadowValue[] | string;
|
|
192
196
|
filter?: IFilterFunction[] | string;
|
|
193
197
|
experimental_backgroundImage?: IBackgroundImageValue[] | string;
|
|
@@ -207,6 +211,12 @@ export interface ITextStyle extends IViewStyle {
|
|
|
207
211
|
textDecorationLine?: 'none' | 'underline' | 'line-through' | 'underline line-through';
|
|
208
212
|
textDecorationColor?: IColorValue;
|
|
209
213
|
textDecorationStyle?: 'solid' | 'double' | 'dotted' | 'dashed';
|
|
214
|
+
textShadowColor?: IColorValue;
|
|
215
|
+
textShadowOffset?: {
|
|
216
|
+
width: number;
|
|
217
|
+
height: number;
|
|
218
|
+
};
|
|
219
|
+
textShadowRadius?: number;
|
|
210
220
|
fontVariant?: string[];
|
|
211
221
|
writingDirection?: 'auto' | 'ltr' | 'rtl';
|
|
212
222
|
includeFontPadding?: boolean;
|
package/build/surface.d.ts
CHANGED
package/build/surface.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { commitChildren } from './commit.js';
|
|
5
5
|
import { dlog } from './debug.js';
|
|
6
6
|
import { installEventHandler } from './events/index.js';
|
|
7
|
+
import { markStructureDirty } from './node.js';
|
|
7
8
|
export class SymbioteSurface {
|
|
8
9
|
rootTag;
|
|
9
10
|
children = [];
|
|
@@ -46,9 +47,16 @@ export class SymbioteSurface {
|
|
|
46
47
|
this.commit();
|
|
47
48
|
});
|
|
48
49
|
}
|
|
50
|
+
// Splices `parent.children` directly instead of going through node.ts's removeChild, so it
|
|
51
|
+
// owes the same marks - otherwise a node pulled out of a subtree here leaves that subtree
|
|
52
|
+
// looking clean and the commit walk skips right over the hole, and commitTargeted would rebuild
|
|
53
|
+
// that parent's child set from a snapshot that still contains the removed node.
|
|
49
54
|
detach(child) {
|
|
50
55
|
const parent = child.parent;
|
|
51
56
|
if (parent) {
|
|
57
|
+
// Marks before the splice, like node.ts's own structural ops: the committed record may be
|
|
58
|
+
// aliasing `parent.children`, and this call is what copies it out of the way.
|
|
59
|
+
markStructureDirty(parent);
|
|
52
60
|
const index = parent.children.indexOf(child);
|
|
53
61
|
if (index >= 0)
|
|
54
62
|
parent.children.splice(index, 1);
|
package/build/tags.d.ts
CHANGED