@jxsuite/runtime 5.2.0 → 6.0.1
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/runtime.js +48 -4
- package/dist/runtime.js.map +5 -5
- package/package.json +2 -2
- package/src/css.ts +74 -4
- package/src/pointer.ts +26 -0
- package/src/runtime.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"upgrade": "bun update --latest && bun install"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jxsuite/schema": "^
|
|
36
|
+
"@jxsuite/schema": "^5.0.0",
|
|
37
37
|
"@vue/reactivity": "3.5.44"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
package/src/css.ts
CHANGED
|
@@ -122,7 +122,8 @@ export function isKeyframesAtRule(atKey: string): boolean {
|
|
|
122
122
|
/**
|
|
123
123
|
* Resolve one nested style key against its parent selector.
|
|
124
124
|
*
|
|
125
|
-
* `&` splices
|
|
125
|
+
* A key with an `&` anywhere splices the scope in for each one (`& > li`, `.wrap &`), else
|
|
126
|
+
* `:`/`.`/`[` concatenate, and anything else is a descendant. Extracted because the same
|
|
126
127
|
* four-branch decision was written THREE times in `applyStyle` — the top-level nested loop,
|
|
127
128
|
* `emitNested`'s recursion and `emitMediaNested` — and three copies of one decision is three
|
|
128
129
|
* chances for a selector to mean different things depending on how deeply it was nested.
|
|
@@ -143,8 +144,9 @@ export function resolveNestedSelector(scope: string, key: string): string {
|
|
|
143
144
|
|
|
144
145
|
/** One member of a scope against one member of a key — the four-branch decision itself. */
|
|
145
146
|
function resolveOneNestedSelector(scope: string, key: string): string {
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
const spliced = spliceNestingSelector(key, scope);
|
|
148
|
+
if (spliced !== null) {
|
|
149
|
+
return spliced;
|
|
148
150
|
}
|
|
149
151
|
if (key.startsWith("[") || key.startsWith(":") || key.startsWith(".")) {
|
|
150
152
|
return `${scope}${key}`;
|
|
@@ -152,6 +154,70 @@ function resolveOneNestedSelector(scope: string, key: string): string {
|
|
|
152
154
|
return `${scope} ${key}`;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Where a selector member holds the nesting selector `&`: outside quoted strings and attribute
|
|
159
|
+
* brackets, where an `&` is text (`[title="R&D"]`) and not the scope.
|
|
160
|
+
*
|
|
161
|
+
* The scope is wherever `&` stands, not only at the start. `.wrap &` is the element when it sits
|
|
162
|
+
* inside a `.wrap`, and `:root &` is the element under the root. Splicing only a LEADING `&` read
|
|
163
|
+
* such a key as a compound and emitted `#box.wrap &`: a selector with a raw `&`, which the browser
|
|
164
|
+
* discards, so the rule never applied anywhere (spec.md §9.2).
|
|
165
|
+
*
|
|
166
|
+
* @param {string} member - One member of a selector list
|
|
167
|
+
* @returns {number[]} The indexes of its `&` characters, in order
|
|
168
|
+
*/
|
|
169
|
+
function nestingIndexes(member: string): number[] {
|
|
170
|
+
const found: number[] = [];
|
|
171
|
+
let quote: string | null = null;
|
|
172
|
+
let brackets = 0;
|
|
173
|
+
for (let i = 0; i < member.length; i += 1) {
|
|
174
|
+
const ch = member[i]!;
|
|
175
|
+
if (quote) {
|
|
176
|
+
if (ch === "\\") {
|
|
177
|
+
i += 1;
|
|
178
|
+
} else if (ch === quote) {
|
|
179
|
+
quote = null;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === "\\") {
|
|
184
|
+
i += 1;
|
|
185
|
+
} else if (ch === '"' || ch === "'") {
|
|
186
|
+
quote = ch;
|
|
187
|
+
} else if (ch === "[") {
|
|
188
|
+
brackets += 1;
|
|
189
|
+
} else if (ch === "]") {
|
|
190
|
+
brackets = Math.max(0, brackets - 1);
|
|
191
|
+
} else if (ch === "&" && brackets === 0) {
|
|
192
|
+
found.push(i);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return found;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Replace every nesting `&` of a selector member with `scope`, or null when it holds none: the one
|
|
200
|
+
* rule every resolver of a nested key shares, so `.wrap &` means the same in every emitter.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} member - One member of a selector list
|
|
203
|
+
* @param {string} scope - What `&` stands for
|
|
204
|
+
* @returns {string | null}
|
|
205
|
+
*/
|
|
206
|
+
export function spliceNestingSelector(member: string, scope: string): string | null {
|
|
207
|
+
return nestingIndexes(member).length > 0 ? spliceScope(member, scope) : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Replace every nesting `&` of `member` ({@link nestingIndexes}) with `scope`. */
|
|
211
|
+
function spliceScope(member: string, scope: string): string {
|
|
212
|
+
let out = "";
|
|
213
|
+
let from = 0;
|
|
214
|
+
for (const at of nestingIndexes(member)) {
|
|
215
|
+
out += member.slice(from, at) + scope;
|
|
216
|
+
from = at + 1;
|
|
217
|
+
}
|
|
218
|
+
return out + member.slice(from);
|
|
219
|
+
}
|
|
220
|
+
|
|
155
221
|
/**
|
|
156
222
|
* Split a selector list on its top-level commas.
|
|
157
223
|
*
|
|
@@ -609,7 +675,11 @@ function withoutKeyframes(block: JxStyle): JxStyle {
|
|
|
609
675
|
* {@link CssRuleTarget}, so it is decided once, here.
|
|
610
676
|
*/
|
|
611
677
|
function compoundsOntoScope(key: string): boolean {
|
|
612
|
-
|
|
678
|
+
/* With an `&` anywhere, what follows the last one decides: `.wrap &:hover` is the element in a
|
|
679
|
+
context, `.wrap & span` something inside it. */
|
|
680
|
+
const nesting = nestingIndexes(key);
|
|
681
|
+
const last = nesting.at(-1);
|
|
682
|
+
const rest = last === undefined ? key : key.slice(last + 1);
|
|
613
683
|
return rest === "" || rest.startsWith(":") || rest.startsWith(".") || rest.startsWith("[");
|
|
614
684
|
}
|
|
615
685
|
|
package/src/pointer.ts
CHANGED
|
@@ -95,6 +95,32 @@ export function refAccessor(base: string, path: string): string {
|
|
|
95
95
|
return expr;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A JavaScript expression reading `path` from `base` that answers `undefined` through a missing
|
|
100
|
+
* step instead of throwing: `s.user?.name`, where {@link refAccessor} emits `s.user.name`.
|
|
101
|
+
*
|
|
102
|
+
* Same member forms as `refAccessor` (dotted where a segment is an identifier, bracketed and
|
|
103
|
+
* JSON-quoted where it is not), with every step after the first optional. That is what the
|
|
104
|
+
* interpreter's {@link readPath} already answers for a missing intermediate, so a compiled read of
|
|
105
|
+
* state a server call or a fetch fills in later agrees with it while the value is still `null`. The
|
|
106
|
+
* accessor stays as it is for the reads that predate this, because making every compiled read
|
|
107
|
+
* optional is a change to what every compiled site does at runtime (see the equivalence tests).
|
|
108
|
+
*
|
|
109
|
+
* @param {string} base - Receiver expression, e.g. `state`
|
|
110
|
+
* @param {string} path
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function refReader(base: string, path: string): string {
|
|
114
|
+
let expr = base;
|
|
115
|
+
for (const [index, key] of refSegments(path).entries()) {
|
|
116
|
+
const optional = index === 0 ? "" : "?.";
|
|
117
|
+
expr += IDENTIFIER.test(key)
|
|
118
|
+
? `${index === 0 ? "." : optional}${key}`
|
|
119
|
+
: `${optional}[${JSON.stringify(key)}]`;
|
|
120
|
+
}
|
|
121
|
+
return expr;
|
|
122
|
+
}
|
|
123
|
+
|
|
98
124
|
/**
|
|
99
125
|
* An object-literal key: bare where it is an identifier, JSON-quoted where it is not.
|
|
100
126
|
*
|