micra.js 2.3.1 → 2.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/CHANGELOG.md +96 -0
- package/README.md +22 -2
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +306 -67
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +306 -67
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +306 -67
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +3 -2
- package/dist/types.d.ts +1 -0
- package/dist/utils/expr.d.ts +29 -22
- package/llms-full.txt +19 -19
- package/llms.txt +3 -3
- package/package.json +3 -3
- package/src/core/mount.ts +4 -0
- package/src/dom/each.ts +19 -2
- package/src/dom/events.ts +37 -10
- package/src/index.ts +1 -1
- package/src/types.ts +1 -0
- package/src/utils/expr.ts +278 -122
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,102 @@ All notable changes to Micra.js will be documented in this file. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.4.0] — 2026-06-14
|
|
8
|
+
|
|
9
|
+
### Added — CSP-safe expression evaluator (works under strict CSP)
|
|
10
|
+
|
|
11
|
+
- **Directive expressions are now parsed and interpreted by a built-in
|
|
12
|
+
evaluator — no `new Function`, no `eval`.** Micra runs under a strict
|
|
13
|
+
Content-Security-Policy (`default-src 'self'`, no `unsafe-eval`): the
|
|
14
|
+
exact policy used by security-sensitive server-rendered apps (banking,
|
|
15
|
+
gov, healthcare). Previously any expression beyond a bare property path
|
|
16
|
+
(`count > 0`, ternaries, comparisons, method calls) compiled via
|
|
17
|
+
`new Function` and was blocked under such a CSP.
|
|
18
|
+
- The build now fails if `eval` / `new Function` ever reappears in the
|
|
19
|
+
bundle (`🔒 CSP guard`).
|
|
20
|
+
- **Stronger security model, by construction.** Globals like `window`,
|
|
21
|
+
`fetch`, `constructor` are unreachable because no scope contains them —
|
|
22
|
+
not because they're shadowed. Member access additionally blocks
|
|
23
|
+
`__proto__` / `constructor` / `prototype`, closing the
|
|
24
|
+
`item.constructor.constructor("…")()` escape the old `with()`-based
|
|
25
|
+
evaluator left open.
|
|
26
|
+
|
|
27
|
+
### Added — call expressions in `@event`
|
|
28
|
+
|
|
29
|
+
- `@event` handlers accept call expressions with arguments, evaluated
|
|
30
|
+
against an event scope (the row `item` inside `data-each`, `$event` /
|
|
31
|
+
`event`, and component methods):
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<button @click="select(item.id)">pick</button>
|
|
35
|
+
<input @input="set($event.target.value)">
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Bare method names (`@click="save"`) work as before. `data-on` keeps
|
|
39
|
+
bare method names only (its handler separator is `,`).
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **Bundle: ~5.5 KB → ~6.6 KB gzip** (size guard raised to 7 KB). The
|
|
44
|
+
eval-based path was *removed*, not kept as a fallback, so this is the
|
|
45
|
+
whole cost of the parser/interpreter. Micra is no longer the very
|
|
46
|
+
smallest in its class (petite-vue ~6 KB) — the trade is CSP-safety,
|
|
47
|
+
a stronger security model, and call-args in events.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- `data-each` row root detection counted whitespace text nodes around a
|
|
52
|
+
single element, wrapping pretty-printed `<tr>` rows in
|
|
53
|
+
`<micra-each-item>` (invalid inside `<tbody>`). Carried over from 2.3.2;
|
|
54
|
+
now also covered by the new evaluator's tests.
|
|
55
|
+
|
|
56
|
+
### Migration
|
|
57
|
+
|
|
58
|
+
- No API changes. Existing expressions evaluate identically (full parity
|
|
59
|
+
suite). If you relied on an expression feature outside the documented
|
|
60
|
+
grammar (assignments, `new`, computed `[]` indexing, arrow functions —
|
|
61
|
+
none of which were ever recommended in directives), it no longer works;
|
|
62
|
+
move that logic into a component method.
|
|
63
|
+
|
|
64
|
+
## [2.3.2] — 2026-06-10
|
|
65
|
+
|
|
66
|
+
### Fixed — `data-each` row root detection
|
|
67
|
+
|
|
68
|
+
- **A pretty-printed template with one root element is no longer wrapped
|
|
69
|
+
in `<micra-each-item>`.** Single-root detection used
|
|
70
|
+
`frag.childNodes.length === 1`, which counts whitespace text nodes — so
|
|
71
|
+
`<template data-each>\n <tr>…</tr>\n</template>` (three child nodes:
|
|
72
|
+
text, element, text) took the multi-root path and wrapped every row.
|
|
73
|
+
For table rows this put invalid content inside `<tbody>` and broke
|
|
74
|
+
`tbody > tr` child selectors.
|
|
75
|
+
- Exact semantics of the new check (top-level child nodes only, O(1)-ish
|
|
76
|
+
per row): plain whitespace (space, `\t`, `\n`, `\f`, `\r`) beside the
|
|
77
|
+
single element is ignored; **NBSP and any other visible character keep
|
|
78
|
+
the wrapper** (they render, so they must survive); **comment nodes
|
|
79
|
+
beside the root are dropped** — they don't render and aren't worth
|
|
80
|
+
invalid wrapper content inside `<tbody>`.
|
|
81
|
+
- Found by the official `isKeyed` compliance check while preparing the
|
|
82
|
+
[js-framework-benchmark](https://github.com/krausest/js-framework-benchmark)
|
|
83
|
+
submission — Micra now passes it for run / remove / swap.
|
|
84
|
+
- Affects both keyed and non-keyed paths (shared `createRowNode`).
|
|
85
|
+
- Internal: `ALLOWED_GLOBALS` in the expression evaluator is now built
|
|
86
|
+
from a split string (identical semantics, smaller minified output).
|
|
87
|
+
Bundle: **5.5 KB gzip** (5632 bytes — exactly at the size guard; the
|
|
88
|
+
next feature pays for itself or raises the limit consciously).
|
|
89
|
+
|
|
90
|
+
### Internal — LLM-benchmark harness hardening (no library impact)
|
|
91
|
+
|
|
92
|
+
Post-review fixes to `bench-llm/` so published numbers are trustworthy:
|
|
93
|
+
windows now close even when an assertion fails (stray timers no longer
|
|
94
|
+
misattribute errors to the next generation); errors aggregate across all
|
|
95
|
+
pages of multi-scenario tasks; quoted `>` inside template attributes no
|
|
96
|
+
longer mangles pages; ESM micra imports are rewritten to UMD bindings
|
|
97
|
+
instead of being dropped; the injected bundle is marked with
|
|
98
|
+
`data-harness-bundle` (single source of truth for loader and lint);
|
|
99
|
+
`Object.groupBy` replaced for Node 20 compatibility; `--only` no longer
|
|
100
|
+
overwrites aggregate results; the `@next` publish guard distinguishes
|
|
101
|
+
"version not published" from registry/network failures.
|
|
102
|
+
|
|
7
103
|
## [2.3.1] — 2026-05-30
|
|
8
104
|
|
|
9
105
|
### Performance
|
package/README.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# Micra.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/denisfl/micra.js/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/micra.js)
|
|
5
|
+
[](https://bundlephobia.com/package/micra.js)
|
|
6
|
+
[](./dist/index.d.ts)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Micra.js is a lightweight reactive TypeScript framework for small sites and SaaS apps. It gives you reactive state, DOM directives, keyed list rendering, an event bus, SSR-friendly props, and auto-mounting in about 7 KB gzip.
|
|
10
|
+
|
|
11
|
+
## Project status
|
|
12
|
+
|
|
13
|
+
- **Stable, SemVer-disciplined.** Breaking changes only in majors; every
|
|
14
|
+
release documented in [CHANGELOG.md](./CHANGELOG.md) with migration notes.
|
|
15
|
+
- **Tested.** 267 tests across 15 suites run on every push and before every
|
|
16
|
+
npm publish; the build fails if the bundle exceeds **7 KB gzip** or if
|
|
17
|
+
`eval` / `new Function` ever reappears (CSP guard).
|
|
18
|
+
- **CSP-safe.** Runs under a strict `default-src 'self'` Content-Security-Policy —
|
|
19
|
+
directive expressions are parsed and interpreted, never `eval`'d.
|
|
20
|
+
- **Typed.** Ships its own `.d.ts` — state, methods, and event-bus payloads
|
|
21
|
+
are checked end-to-end (see [TypeScript](#typescript)).
|
|
22
|
+
- **Security policy.** See [SECURITY.md](./SECURITY.md) — private reporting,
|
|
23
|
+
72-hour acknowledgement, supported-versions table.
|
|
4
24
|
|
|
5
25
|
## When to use Micra.js
|
|
6
26
|
|
|
@@ -8,7 +28,7 @@ Built for **server-rendered apps** (Rails, Laravel, Django, Phoenix, ASP.NET) an
|
|
|
8
28
|
|
|
9
29
|
Reach for Micra.js instead of React/Vue when:
|
|
10
30
|
|
|
11
|
-
- ~
|
|
31
|
+
- ~7 KB gzip matters (full bundle, not "core")
|
|
12
32
|
- you want to drop a `<script>` tag on an existing page and go — no toolchain
|
|
13
33
|
- you have HTML rendered by your server template engine that needs reactive directives
|
|
14
34
|
- you don't need client-side routing or a full SPA
|
package/dist/index.d.ts
CHANGED
package/dist/micra.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Micra.js v2.
|
|
1
|
+
/* Micra.js v2.4.0 — https://github.com/micra-js/micra — MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -116,56 +116,196 @@ function debug() {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// src/utils/expr.ts
|
|
119
|
-
var
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
var
|
|
123
|
-
"Math",
|
|
124
|
-
"JSON",
|
|
125
|
-
"Date",
|
|
126
|
-
"String",
|
|
127
|
-
"Number",
|
|
128
|
-
"Boolean",
|
|
129
|
-
"Array",
|
|
130
|
-
"Object",
|
|
131
|
-
"parseInt",
|
|
132
|
-
"parseFloat",
|
|
133
|
-
"isNaN",
|
|
134
|
-
"isFinite",
|
|
135
|
-
"NaN",
|
|
136
|
-
"Infinity",
|
|
137
|
-
"undefined"
|
|
138
|
-
]);
|
|
139
|
-
var PARAM_S = "$s";
|
|
140
|
-
var PARAM_SAFE = "$safe";
|
|
141
|
-
var SAFE_OUTER = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
142
|
-
has(_target, key) {
|
|
143
|
-
if (typeof key !== "string") return false;
|
|
144
|
-
if (key === PARAM_S || key === PARAM_SAFE) return false;
|
|
145
|
-
return !ALLOWED_GLOBALS.has(key);
|
|
146
|
-
},
|
|
147
|
-
get() {
|
|
148
|
-
return void 0;
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
var safeWrapCache = /* @__PURE__ */ new WeakMap();
|
|
119
|
+
var ALLOWED_GLOBALS = new Set(
|
|
120
|
+
"Math,JSON,Date,String,Number,Boolean,Array,Object,parseInt,parseFloat,isNaN,isFinite,NaN,Infinity,undefined".split(",")
|
|
121
|
+
);
|
|
122
|
+
var BLOCKED_PROPS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
152
123
|
var OBJ_PROTO_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
124
|
+
var PUNCT = [
|
|
125
|
+
"===",
|
|
126
|
+
"!==",
|
|
127
|
+
"==",
|
|
128
|
+
"!=",
|
|
129
|
+
"<=",
|
|
130
|
+
">=",
|
|
131
|
+
"&&",
|
|
132
|
+
"||",
|
|
133
|
+
"(",
|
|
134
|
+
")",
|
|
135
|
+
".",
|
|
136
|
+
",",
|
|
137
|
+
"?",
|
|
138
|
+
":",
|
|
139
|
+
"!",
|
|
140
|
+
"<",
|
|
141
|
+
">",
|
|
142
|
+
"+",
|
|
143
|
+
"-",
|
|
144
|
+
"*",
|
|
145
|
+
"/",
|
|
146
|
+
"%"
|
|
147
|
+
];
|
|
148
|
+
function tokenize(src) {
|
|
149
|
+
var _a;
|
|
150
|
+
const toks = [];
|
|
151
|
+
let i = 0;
|
|
152
|
+
const n = src.length;
|
|
153
|
+
while (i < n) {
|
|
154
|
+
const c = src[i];
|
|
155
|
+
if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\f") {
|
|
156
|
+
i++;
|
|
157
|
+
continue;
|
|
162
158
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
if (c === '"' || c === "'") {
|
|
160
|
+
let s = "";
|
|
161
|
+
i++;
|
|
162
|
+
while (i < n && src[i] !== c) {
|
|
163
|
+
if (src[i] === "\\") {
|
|
164
|
+
s += (_a = src[i + 1]) != null ? _a : "";
|
|
165
|
+
i += 2;
|
|
166
|
+
} else {
|
|
167
|
+
s += src[i];
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (src[i] !== c) throw 0;
|
|
172
|
+
i++;
|
|
173
|
+
toks.push({ t: "str", v: s });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (c >= "0" && c <= "9") {
|
|
177
|
+
let s = "";
|
|
178
|
+
while (i < n && (src[i] >= "0" && src[i] <= "9" || src[i] === ".")) {
|
|
179
|
+
s += src[i];
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
toks.push({ t: "num", v: s });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
186
|
+
let s = "";
|
|
187
|
+
while (i < n && /[A-Za-z0-9_$]/.test(src[i])) {
|
|
188
|
+
s += src[i];
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
toks.push({ t: "id", v: s });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const m = PUNCT.find((p) => src.startsWith(p, i));
|
|
195
|
+
if (!m) throw 0;
|
|
196
|
+
toks.push({ t: "p", v: m });
|
|
197
|
+
i += m.length;
|
|
198
|
+
}
|
|
199
|
+
return toks;
|
|
200
|
+
}
|
|
201
|
+
var BIN_PREC = {
|
|
202
|
+
"||": 1,
|
|
203
|
+
"&&": 2,
|
|
204
|
+
"==": 3,
|
|
205
|
+
"!=": 3,
|
|
206
|
+
"===": 3,
|
|
207
|
+
"!==": 3,
|
|
208
|
+
"<": 4,
|
|
209
|
+
"<=": 4,
|
|
210
|
+
">": 4,
|
|
211
|
+
">=": 4,
|
|
212
|
+
"+": 5,
|
|
213
|
+
"-": 5,
|
|
214
|
+
"*": 6,
|
|
215
|
+
"/": 6,
|
|
216
|
+
"%": 6
|
|
217
|
+
};
|
|
218
|
+
function parse(toks) {
|
|
219
|
+
let pos = 0;
|
|
220
|
+
const peek = () => toks[pos];
|
|
221
|
+
const next = () => toks[pos++];
|
|
222
|
+
const eat = (v) => {
|
|
223
|
+
var _a;
|
|
224
|
+
if (((_a = peek()) == null ? void 0 : _a.v) !== v) throw 0;
|
|
225
|
+
pos++;
|
|
226
|
+
};
|
|
227
|
+
function parseExpr() {
|
|
228
|
+
var _a;
|
|
229
|
+
const c = parseBin(1);
|
|
230
|
+
if (((_a = peek()) == null ? void 0 : _a.v) === "?") {
|
|
231
|
+
next();
|
|
232
|
+
const a = parseExpr();
|
|
233
|
+
eat(":");
|
|
234
|
+
const b = parseExpr();
|
|
235
|
+
return { k: "tern", c, a, b };
|
|
236
|
+
}
|
|
237
|
+
return c;
|
|
238
|
+
}
|
|
239
|
+
function parseBin(minPrec) {
|
|
240
|
+
let left = parseUnary();
|
|
241
|
+
for (; ; ) {
|
|
242
|
+
const t = peek();
|
|
243
|
+
const prec = t && t.t === "p" ? BIN_PREC[t.v] : void 0;
|
|
244
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
245
|
+
next();
|
|
246
|
+
const right = parseBin(prec + 1);
|
|
247
|
+
left = { k: "bin", op: t.v, l: left, r: right };
|
|
248
|
+
}
|
|
249
|
+
return left;
|
|
250
|
+
}
|
|
251
|
+
function parseUnary() {
|
|
252
|
+
const t = peek();
|
|
253
|
+
if (t && t.t === "p" && (t.v === "!" || t.v === "-")) {
|
|
254
|
+
next();
|
|
255
|
+
return { k: "un", op: t.v, x: parseUnary() };
|
|
256
|
+
}
|
|
257
|
+
return parsePostfix();
|
|
258
|
+
}
|
|
259
|
+
function parsePostfix() {
|
|
260
|
+
var _a, _b;
|
|
261
|
+
let node = parsePrimary();
|
|
262
|
+
for (; ; ) {
|
|
263
|
+
const t = peek();
|
|
264
|
+
if ((t == null ? void 0 : t.v) === ".") {
|
|
265
|
+
next();
|
|
266
|
+
const id = next();
|
|
267
|
+
if (!id || id.t !== "id") throw 0;
|
|
268
|
+
node = { k: "mem", o: node, p: id.v };
|
|
269
|
+
} else if ((t == null ? void 0 : t.v) === "(") {
|
|
270
|
+
next();
|
|
271
|
+
const args = [];
|
|
272
|
+
if (((_a = peek()) == null ? void 0 : _a.v) !== ")") {
|
|
273
|
+
args.push(parseExpr());
|
|
274
|
+
while (((_b = peek()) == null ? void 0 : _b.v) === ",") {
|
|
275
|
+
next();
|
|
276
|
+
args.push(parseExpr());
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
eat(")");
|
|
280
|
+
node = { k: "call", c: node, a: args };
|
|
281
|
+
} else break;
|
|
282
|
+
}
|
|
283
|
+
return node;
|
|
284
|
+
}
|
|
285
|
+
function parsePrimary() {
|
|
286
|
+
const t = next();
|
|
287
|
+
if (!t) throw 0;
|
|
288
|
+
if (t.t === "num") return { k: "lit", v: Number(t.v) };
|
|
289
|
+
if (t.t === "str") return { k: "lit", v: t.v };
|
|
290
|
+
if (t.v === "(") {
|
|
291
|
+
const e = parseExpr();
|
|
292
|
+
eat(")");
|
|
293
|
+
return e;
|
|
294
|
+
}
|
|
295
|
+
if (t.t === "id") {
|
|
296
|
+
if (t.v === "true") return { k: "lit", v: true };
|
|
297
|
+
if (t.v === "false") return { k: "lit", v: false };
|
|
298
|
+
if (t.v === "null") return { k: "lit", v: null };
|
|
299
|
+
if (t.v === "undefined") return { k: "lit", v: void 0 };
|
|
300
|
+
return { k: "id", n: t.v };
|
|
301
|
+
}
|
|
302
|
+
throw 0;
|
|
303
|
+
}
|
|
304
|
+
const ast = parseExpr();
|
|
305
|
+
if (pos !== toks.length) throw 0;
|
|
306
|
+
return ast;
|
|
166
307
|
}
|
|
167
308
|
function safeStateHas(state, key) {
|
|
168
|
-
if (typeof key !== "string") return false;
|
|
169
309
|
if (!Reflect.has(state, key)) return false;
|
|
170
310
|
if (!OBJ_PROTO_KEYS.has(key)) return true;
|
|
171
311
|
let obj = state;
|
|
@@ -175,6 +315,87 @@ function safeStateHas(state, key) {
|
|
|
175
315
|
}
|
|
176
316
|
return false;
|
|
177
317
|
}
|
|
318
|
+
function resolveIdent(name, scope) {
|
|
319
|
+
if (safeStateHas(scope, name)) return scope[name];
|
|
320
|
+
if (ALLOWED_GLOBALS.has(name)) return globalThis[name];
|
|
321
|
+
return void 0;
|
|
322
|
+
}
|
|
323
|
+
function evalNode(node, scope) {
|
|
324
|
+
switch (node.k) {
|
|
325
|
+
case "lit":
|
|
326
|
+
return node.v;
|
|
327
|
+
case "id":
|
|
328
|
+
return resolveIdent(node.n, scope);
|
|
329
|
+
case "mem": {
|
|
330
|
+
const o = evalNode(node.o, scope);
|
|
331
|
+
if (o == null || BLOCKED_PROPS.has(node.p)) return void 0;
|
|
332
|
+
return o[node.p];
|
|
333
|
+
}
|
|
334
|
+
case "un": {
|
|
335
|
+
const x = evalNode(node.x, scope);
|
|
336
|
+
return node.op === "!" ? !x : -x;
|
|
337
|
+
}
|
|
338
|
+
case "tern":
|
|
339
|
+
return evalNode(node.c, scope) ? evalNode(node.a, scope) : evalNode(node.b, scope);
|
|
340
|
+
case "bin": {
|
|
341
|
+
const op = node.op;
|
|
342
|
+
if (op === "&&") {
|
|
343
|
+
const l2 = evalNode(node.l, scope);
|
|
344
|
+
return l2 ? evalNode(node.r, scope) : l2;
|
|
345
|
+
}
|
|
346
|
+
if (op === "||") {
|
|
347
|
+
const l2 = evalNode(node.l, scope);
|
|
348
|
+
return l2 ? l2 : evalNode(node.r, scope);
|
|
349
|
+
}
|
|
350
|
+
const l = evalNode(node.l, scope);
|
|
351
|
+
const r = evalNode(node.r, scope);
|
|
352
|
+
switch (op) {
|
|
353
|
+
case "+":
|
|
354
|
+
return l + r;
|
|
355
|
+
case "-":
|
|
356
|
+
return l - r;
|
|
357
|
+
case "*":
|
|
358
|
+
return l * r;
|
|
359
|
+
case "/":
|
|
360
|
+
return l / r;
|
|
361
|
+
case "%":
|
|
362
|
+
return l % r;
|
|
363
|
+
case "<":
|
|
364
|
+
return l < r;
|
|
365
|
+
case "<=":
|
|
366
|
+
return l <= r;
|
|
367
|
+
case ">":
|
|
368
|
+
return l > r;
|
|
369
|
+
case ">=":
|
|
370
|
+
return l >= r;
|
|
371
|
+
case "==":
|
|
372
|
+
return l == r;
|
|
373
|
+
case "!=":
|
|
374
|
+
return l != r;
|
|
375
|
+
case "===":
|
|
376
|
+
return l === r;
|
|
377
|
+
case "!==":
|
|
378
|
+
return l !== r;
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
case "call": {
|
|
383
|
+
let fn;
|
|
384
|
+
let self;
|
|
385
|
+
if (node.c.k === "mem") {
|
|
386
|
+
self = evalNode(node.c.o, scope);
|
|
387
|
+
fn = self == null || BLOCKED_PROPS.has(node.c.p) ? void 0 : self[node.c.p];
|
|
388
|
+
} else {
|
|
389
|
+
fn = evalNode(node.c, scope);
|
|
390
|
+
}
|
|
391
|
+
if (typeof fn !== "function") throw new TypeError("not a function");
|
|
392
|
+
return fn.apply(self, node.a.map((x) => evalNode(x, scope)));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
var exprCache = /* @__PURE__ */ new Map();
|
|
397
|
+
var warnedRuntime = /* @__PURE__ */ new Set();
|
|
398
|
+
var SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;
|
|
178
399
|
function evalExpr(expr, state) {
|
|
179
400
|
let cached = exprCache.get(expr);
|
|
180
401
|
if (!cached) {
|
|
@@ -182,26 +403,24 @@ function evalExpr(expr, state) {
|
|
|
182
403
|
cached = { kind: "path", parts: expr.split(".") };
|
|
183
404
|
} else {
|
|
184
405
|
try {
|
|
185
|
-
cached = {
|
|
186
|
-
kind: "fn",
|
|
187
|
-
fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
|
|
188
|
-
};
|
|
406
|
+
cached = { kind: "ast", ast: parse(tokenize(expr)) };
|
|
189
407
|
} catch {
|
|
190
408
|
warn(`invalid expression "${expr}"`);
|
|
191
|
-
cached = { kind: "
|
|
409
|
+
cached = { kind: "err" };
|
|
192
410
|
}
|
|
193
411
|
}
|
|
194
412
|
exprCache.set(expr, cached);
|
|
195
413
|
}
|
|
196
414
|
if (cached.kind === "path") {
|
|
197
|
-
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
415
|
+
const parts = cached.parts;
|
|
416
|
+
if (!safeStateHas(state, parts[0])) return void 0;
|
|
417
|
+
let obj = state;
|
|
418
|
+
for (const key of parts) obj = obj != null ? obj[key] : void 0;
|
|
419
|
+
return obj;
|
|
202
420
|
}
|
|
421
|
+
if (cached.kind === "err") return void 0;
|
|
203
422
|
try {
|
|
204
|
-
return cached.
|
|
423
|
+
return evalNode(cached.ast, state);
|
|
205
424
|
} catch (e) {
|
|
206
425
|
if (!warnedRuntime.has(expr)) {
|
|
207
426
|
warnedRuntime.add(expr);
|
|
@@ -361,6 +580,23 @@ function track(instance, el, type, fn) {
|
|
|
361
580
|
el.addEventListener(type, fn);
|
|
362
581
|
((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
|
|
363
582
|
}
|
|
583
|
+
function runHandler(instance, el, value, e) {
|
|
584
|
+
var _a;
|
|
585
|
+
if (value.includes("(")) {
|
|
586
|
+
let base;
|
|
587
|
+
for (let n = el; n && !base; n = n.parentElement) {
|
|
588
|
+
base = n._itemState;
|
|
589
|
+
}
|
|
590
|
+
const scope = Object.create((_a = base != null ? base : instance.__micraExpr) != null ? _a : null);
|
|
591
|
+
scope["$event"] = e;
|
|
592
|
+
scope["event"] = e;
|
|
593
|
+
evalExpr(value, scope);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const fn = instance[value];
|
|
597
|
+
if (typeof fn === "function") fn.call(instance, e);
|
|
598
|
+
else warn(`method "${value}" not found`);
|
|
599
|
+
}
|
|
364
600
|
function bindDataOn(els, instance) {
|
|
365
601
|
var _a;
|
|
366
602
|
for (const el of els) {
|
|
@@ -372,13 +608,12 @@ function bindDataOn(els, instance) {
|
|
|
372
608
|
const [evSpec, method] = part.trim().split(":");
|
|
373
609
|
if (!evSpec || !method) continue;
|
|
374
610
|
const [evName, ...mods] = evSpec.split(".");
|
|
611
|
+
const handler = method.trim();
|
|
375
612
|
track(instance, el, evName, (e) => {
|
|
376
613
|
if (mods.includes("prevent")) e.preventDefault();
|
|
377
614
|
if (mods.includes("stop")) e.stopPropagation();
|
|
378
615
|
if (mods.includes("self") && e.target !== el) return;
|
|
379
|
-
|
|
380
|
-
if (typeof fn === "function") fn.call(instance, e);
|
|
381
|
-
else warn(`method "${method.trim()}" not found`);
|
|
616
|
+
runHandler(instance, el, handler, e);
|
|
382
617
|
});
|
|
383
618
|
}
|
|
384
619
|
}
|
|
@@ -391,14 +626,12 @@ function bindAtEvents(els, instance) {
|
|
|
391
626
|
for (const attr of Array.from(el.attributes)) {
|
|
392
627
|
if (!attr.name.startsWith("@")) continue;
|
|
393
628
|
const [evSpec, ...rest] = attr.name.slice(1).split(".");
|
|
394
|
-
const
|
|
629
|
+
const handler = attr.value.trim();
|
|
395
630
|
track(instance, el, evSpec, (e) => {
|
|
396
631
|
if (rest.includes("prevent")) e.preventDefault();
|
|
397
632
|
if (rest.includes("stop")) e.stopPropagation();
|
|
398
633
|
if (rest.includes("self") && e.target !== el) return;
|
|
399
|
-
|
|
400
|
-
if (typeof fn === "function") fn.call(instance, e);
|
|
401
|
-
else warn(`method "${method}" not found`);
|
|
634
|
+
runHandler(instance, el, handler, e);
|
|
402
635
|
});
|
|
403
636
|
bound = true;
|
|
404
637
|
}
|
|
@@ -574,8 +807,13 @@ function renderList(templates, state, rawState, instance, triggerKey) {
|
|
|
574
807
|
function createRowNode(tmpl, state, instance) {
|
|
575
808
|
const frag = tmpl.content.cloneNode(true);
|
|
576
809
|
let node;
|
|
577
|
-
|
|
578
|
-
|
|
810
|
+
const first = frag.firstElementChild;
|
|
811
|
+
const single = !!first && !first.nextElementSibling && !Array.prototype.some.call(
|
|
812
|
+
frag.childNodes,
|
|
813
|
+
(c) => c.nodeType === 3 && /[^\x00- ]/.test(c.textContent)
|
|
814
|
+
);
|
|
815
|
+
if (single) {
|
|
816
|
+
node = first;
|
|
579
817
|
} else {
|
|
580
818
|
node = document.createElement("micra-each-item");
|
|
581
819
|
node.style.display = "contents";
|
|
@@ -803,6 +1041,7 @@ function mount(selector, definition) {
|
|
|
803
1041
|
return Object.prototype.hasOwnProperty.call(instance, key) && typeof instance[key] === "function";
|
|
804
1042
|
}
|
|
805
1043
|
});
|
|
1044
|
+
instance.__micraExpr = exprState;
|
|
806
1045
|
let warnedReentry = false;
|
|
807
1046
|
instance.render = function() {
|
|
808
1047
|
var _a2;
|