@zoijs/core 1.0.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 +36 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/package.json +63 -0
- package/src/core/each.js +24 -0
- package/src/core/html.js +243 -0
- package/src/core/mount.js +31 -0
- package/src/core/renderer.js +364 -0
- package/src/index.d.ts +113 -0
- package/src/index.js +14 -0
- package/src/reactivity/computed.js +10 -0
- package/src/reactivity/core.js +205 -0
- package/src/reactivity/effect.js +7 -0
- package/src/reactivity/env.js +24 -0
- package/src/reactivity/owner.js +61 -0
- package/src/reactivity/scheduler.js +7 -0
- package/src/reactivity/state.js +10 -0
- package/src/utils/dom.js +51 -0
- package/src/utils/security.js +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Zoijs are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and Zoijs follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/) (see `VERSIONING.md`).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — Unreleased
|
|
8
|
+
|
|
9
|
+
First stable release. The public API is frozen at seven functions.
|
|
10
|
+
|
|
11
|
+
### Public API
|
|
12
|
+
- `html` — tagged-template renderer (no JSX, no build step).
|
|
13
|
+
- `mount(component, target)` → `unmount()`.
|
|
14
|
+
- `createState(value)` → `{ get, set, peek }`.
|
|
15
|
+
- `computed(fn)` → `{ get, peek }` — lazy, cached, value-gated.
|
|
16
|
+
- `each(items, keyFn, renderFn)` — keyed list reconciliation.
|
|
17
|
+
- `configure({ dev })` — development/production mode.
|
|
18
|
+
- `onCleanup(fn)` — teardown for components and list items.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
- Fine-grained, direct DOM updates (no Virtual DOM); setup runs once.
|
|
22
|
+
- Push-pull reactive core with automatic dependency tracking and microtask batching.
|
|
23
|
+
- Owner-scoped cleanup; deterministic teardown on unmount and list-item removal.
|
|
24
|
+
- Context-aware template parser (quoted/unquoted/partial/multi-hole attributes,
|
|
25
|
+
boolean/URL/aria/data attributes, SVG, nested templates and lists).
|
|
26
|
+
- Secure by default: inert text, URL-scheme allowlist (control-char resistant),
|
|
27
|
+
`data:` raster-image rules, `on*`/`srcdoc` blocked, function-only handlers,
|
|
28
|
+
no `eval`, CSP- and Trusted-Types-friendly.
|
|
29
|
+
- TypeScript definitions with generics for state/computed/lists.
|
|
30
|
+
|
|
31
|
+
### Tooling
|
|
32
|
+
- 100+ unit/DOM tests (jsdom), real-browser tests on Chromium/Firefox/WebKit
|
|
33
|
+
(Playwright), and TypeScript type tests.
|
|
34
|
+
- No build step required at any point.
|
|
35
|
+
|
|
36
|
+
[1.0.0]: https://github.com/Zoijs/zoijs/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Easy Framework contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Zoijs
|
|
2
|
+
|
|
3
|
+
A lightweight frontend framework you don't have to learn before you use it — **plain HTML, CSS, and JavaScript**, no JSX, no build step, no Virtual DOM.
|
|
4
|
+
|
|
5
|
+
**[Website](https://zoijs.com)** · **[Documentation](https://zoijs.dev)** · **[GitHub](https://github.com/Zoijs)** · **[npm](https://www.npmjs.com/package/@zoijs/core)**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zoijs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { html, mount, createState } from "@zoijs/core";
|
|
13
|
+
|
|
14
|
+
function Counter() {
|
|
15
|
+
const count = createState(0);
|
|
16
|
+
return html`<button onclick=${() => count.set(count.get() + 1)}>${() => count.get()}</button>`;
|
|
17
|
+
}
|
|
18
|
+
mount(Counter, "#app");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 📚 Documentation
|
|
22
|
+
|
|
23
|
+
**New here? Start at [zoijs.dev](https://zoijs.dev) (or the [docs folder](docs/README.md)) — designed to get you productive in under 30 minutes.**
|
|
24
|
+
|
|
25
|
+
- [Installation](docs/installation.md) · [Your First App](docs/first-app.md) · [Core Concepts](docs/concepts/core-concepts.md)
|
|
26
|
+
- [Tutorials](docs/README.md#tutorials-build-something) · [API Reference](docs/api-reference.md) · [Examples](docs/examples.md)
|
|
27
|
+
- [Troubleshooting](docs/troubleshooting.md) · [FAQ](docs/faq.md) · [Migrating from React/Vue/Solid/Lit/vanilla](docs/README.md#coming-from-another-framework)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Mission
|
|
32
|
+
|
|
33
|
+
Make building modern web applications feel as approachable as writing plain HTML, CSS, and JavaScript — so that any developer, on day one, can ship real software without first learning a framework.
|
|
34
|
+
|
|
35
|
+
The framework should disappear into the skills developers already have. The whole mental model is three verbs: **write a function that returns `html`, put `createState` values in it, `mount` it.**
|
|
36
|
+
|
|
37
|
+
## Goals
|
|
38
|
+
|
|
39
|
+
- **Beginner-friendly** — concepts you already know from vanilla JS/HTML/CSS.
|
|
40
|
+
- **No build step** — runs from a single `<script type="module">`.
|
|
41
|
+
- **No Virtual DOM** — fine-grained, direct DOM updates; only what changed is touched.
|
|
42
|
+
- **Minimal runtime** — the browser does the heavy lifting (native `<template>`, `cloneNode`, events).
|
|
43
|
+
- **Secure by default** — inert text rendering, URL-scheme guards, handler references (never strings), no `eval`.
|
|
44
|
+
- **Small & readable** — a junior developer can read the source.
|
|
45
|
+
|
|
46
|
+
See [`docs/Phase-1-MVP-Spec.md`](docs/Phase-1-MVP-Spec.md) for the full specification.
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
No build step is required — the framework is plain ES modules.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# from the framework/ directory
|
|
54
|
+
|
|
55
|
+
# run the counter example (serves the project root over http so ES modules resolve)
|
|
56
|
+
npm run dev
|
|
57
|
+
# then open: http://localhost:3000/examples/counter/ (keep the trailing slash)
|
|
58
|
+
|
|
59
|
+
# run the tests (DOM tests run automatically via jsdom)
|
|
60
|
+
npm test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> Tips:
|
|
64
|
+
> - ES module imports need to be served over `http://`, not opened as a `file://` path. `npm run dev` handles that.
|
|
65
|
+
> - Use the **trailing slash** on the example URL (`/examples/counter/`). Without it, some static servers resolve `./app.js` against the wrong directory and the app won't load.
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
|
|
69
|
+
| Command | What it runs |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `npm test` | Unit + DOM tests via jsdom (fast, no browser) |
|
|
72
|
+
| `npm run test:unit` | Pure-logic tests only (no DOM) |
|
|
73
|
+
| `npm run test:types` | TypeScript type-checks (`tsc --noEmit`) |
|
|
74
|
+
| `npm run test:browser` | Real-browser tests in Chromium, Firefox, WebKit (Playwright) |
|
|
75
|
+
|
|
76
|
+
Browser tests live in `browser-tests/` and run the example apps plus framework regressions against real engines. First-time setup downloads the browsers:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
npx playwright install chromium firefox webkit
|
|
81
|
+
npm run test:browser
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Playwright starts a static server automatically (`npx serve` on port 3000) — still no build step.
|
|
85
|
+
|
|
86
|
+
## Browser support
|
|
87
|
+
|
|
88
|
+
Modern evergreen browsers. Verified automatically (Playwright) in:
|
|
89
|
+
|
|
90
|
+
| Browser | Engine | Status |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| Chrome / Edge | Chromium | ✅ tested |
|
|
93
|
+
| Firefox | Gecko | ✅ tested |
|
|
94
|
+
| Safari | WebKit | ✅ tested |
|
|
95
|
+
|
|
96
|
+
Relies on baseline-modern platform APIs: ES modules, `<template>`, `TreeWalker`, `Proxy`, `queueMicrotask`, `replaceChildren`, `addEventListener`, `setAttributeNS`. No IE support, no transpilation, no polyfills.
|
|
97
|
+
|
|
98
|
+
## Public API
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { html, mount, createState, computed, each, configure, onCleanup } from "@zoijs/core";
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `html` — tagged template; parsed once, cached.
|
|
105
|
+
- `mount(component, target)` — render a component; returns `unmount()`.
|
|
106
|
+
- `createState(value)` — a reactive value (`get` / `set` / `peek`).
|
|
107
|
+
- `computed(fn)` — a lazy, cached, **value-gated** derived value (`get` / `peek`).
|
|
108
|
+
- `each(itemsFn, keyFn, renderFn)` — keyed list rendering (reuse / move / remove nodes).
|
|
109
|
+
- `configure({ dev })` — toggle development warnings (default `dev: true`).
|
|
110
|
+
- `onCleanup(fn)` — register teardown for a component or list item (timers, subscriptions).
|
|
111
|
+
|
|
112
|
+
See the [Documentation site](docs/README.md) for the full guide, tutorials, and API reference.
|
|
113
|
+
|
|
114
|
+
**TypeScript:** ships type definitions ([`src/index.d.ts`](src/index.d.ts)) for autocomplete and optional type-checking — JS-first, no build step required. `createState<T>`, `computed<T>`, and `each<T>` are generic. Type-check with `npm run test:types`.
|
|
115
|
+
|
|
116
|
+
## What's built
|
|
117
|
+
|
|
118
|
+
- Fine-grained text/attribute bindings — `${() => state.get()}` updates one node in place; setup runs once (no re-render).
|
|
119
|
+
- Native events, secure-by-default rendering (inert text, URL-scheme guards, no `eval`).
|
|
120
|
+
- `computed()` derived values — lazy, cached, nestable, and **value-gated** (unchanged results don't wake downstream).
|
|
121
|
+
- `each()` keyed list reconciliation — preserves focus / input / scroll on reorder.
|
|
122
|
+
- Microtask batching, push-pull dependency tracking, **owner-scoped cleanup** (unmount and removed items dispose their subscriptions).
|
|
123
|
+
- Production mode via `configure({ dev: false })` — no build step.
|
|
124
|
+
- Safety: self-triggering effects are warned + stopped; a throwing binding doesn't break others.
|
|
125
|
+
|
|
126
|
+
**Out of scope (by design):** router, CLI, plugins, SSR, global store, TypeScript-first setup, Virtual DOM.
|
|
127
|
+
|
|
128
|
+
## Project Structure
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
framework/
|
|
132
|
+
package.json
|
|
133
|
+
README.md
|
|
134
|
+
src/
|
|
135
|
+
core/
|
|
136
|
+
html.js # html() — parse template into a cached blueprint
|
|
137
|
+
mount.js # mount() — entry point; instantiate + attach + cleanup
|
|
138
|
+
renderer.js # internal: bind dynamic slots, apply fine-grained updates
|
|
139
|
+
reactivity/
|
|
140
|
+
state.js # createState() + internal dependency tracking
|
|
141
|
+
utils/
|
|
142
|
+
dom.js # small native-DOM helpers
|
|
143
|
+
security.js # escaping, URL-scheme allowlist, attribute-name guards
|
|
144
|
+
index.js # public entry — re-exports html, mount, createState
|
|
145
|
+
examples/
|
|
146
|
+
counter/ # the first working app
|
|
147
|
+
tests/ # basic unit tests (node --test)
|
|
148
|
+
docs/
|
|
149
|
+
Phase-1-MVP-Spec.md
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoijs/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zoijs — a beginner-friendly, no-build, no-Virtual-DOM frontend framework. Plain HTML, CSS, and JavaScript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"author": "Zoijs contributors (https://zoijs.com)",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://zoijs.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Zoijs/zoijs.git",
|
|
30
|
+
"directory": "framework"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/Zoijs/zoijs/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"zoijs",
|
|
37
|
+
"framework",
|
|
38
|
+
"frontend",
|
|
39
|
+
"ui",
|
|
40
|
+
"reactive",
|
|
41
|
+
"reactivity",
|
|
42
|
+
"signals",
|
|
43
|
+
"no-build",
|
|
44
|
+
"no-jsx",
|
|
45
|
+
"no-virtual-dom",
|
|
46
|
+
"fine-grained",
|
|
47
|
+
"beginner-friendly",
|
|
48
|
+
"html",
|
|
49
|
+
"spa"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "node --test --import ./tests/setup-dom.js \"tests/**/*.test.js\"",
|
|
53
|
+
"test:unit": "node --test \"tests/**/*.test.js\"",
|
|
54
|
+
"test:types": "tsc --noEmit -p tsconfig.json",
|
|
55
|
+
"test:browser": "playwright test",
|
|
56
|
+
"dev": "npx serve -l 3000 ."
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@playwright/test": "^1.61.1",
|
|
60
|
+
"jsdom": "^29.1.1",
|
|
61
|
+
"typescript": "^5.9.3"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/core/each.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// each() — keyed list rendering (Milestone 3).
|
|
2
|
+
//
|
|
3
|
+
// Returns a small marker object that the renderer recognizes in a text slot and
|
|
4
|
+
// turns into a keyed list binding. Keeping this factory tiny (and separate from
|
|
5
|
+
// the renderer) avoids a circular import — the renderer just checks the marker.
|
|
6
|
+
//
|
|
7
|
+
// ${each(
|
|
8
|
+
// () => todos.get(), // items: a reactive function (or a plain array)
|
|
9
|
+
// todo => todo.id, // keyFn: a stable unique key per item
|
|
10
|
+
// todo => html`<li>...</li>` // renderFn: builds DOM for one item
|
|
11
|
+
// )}
|
|
12
|
+
//
|
|
13
|
+
// When the array changes, items with matching keys reuse their DOM nodes (moved
|
|
14
|
+
// if reordered); new keys are inserted; removed keys are disposed and removed.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Function|Array} items a function returning the array, or an array
|
|
18
|
+
* @param {Function} keyFn item -> unique key
|
|
19
|
+
* @param {Function} renderFn item -> html() result
|
|
20
|
+
* @returns {{ __easyEach: true, items: any, keyFn: Function, renderFn: Function }}
|
|
21
|
+
*/
|
|
22
|
+
export function each(items, keyFn, renderFn) {
|
|
23
|
+
return { __easyEach: true, items, keyFn, renderFn };
|
|
24
|
+
}
|
package/src/core/html.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// html() — the template function.
|
|
2
|
+
//
|
|
3
|
+
// html`<button onclick=${handler}>${() => count.get()}</button>`
|
|
4
|
+
//
|
|
5
|
+
// Parses the STATIC structure once (cached), tracking real HTML lexical context
|
|
6
|
+
// with a small state machine — so it knows whether each ${} falls in text, an
|
|
7
|
+
// attribute value, an event, etc. Dynamic values are kept in a separate channel
|
|
8
|
+
// from the static HTML; a value can never change the template's structure.
|
|
9
|
+
//
|
|
10
|
+
// Output of the parse (cached per `strings`):
|
|
11
|
+
// template : a <template> element with unique markers
|
|
12
|
+
// parts : ordered list of binding descriptors
|
|
13
|
+
// { type: "child" } ← a comment-marker anchor
|
|
14
|
+
// { type: "element", attrs: [AttrPart] } ← element carrying data-zoijs-bind
|
|
15
|
+
// AttrPart : { name, strings, holes, event, whole }
|
|
16
|
+
//
|
|
17
|
+
// Matching is by DOCUMENT ORDER (renderer walks the clone), so markers can never
|
|
18
|
+
// collide across nested templates or list items.
|
|
19
|
+
|
|
20
|
+
import { createTemplate } from "../utils/dom.js";
|
|
21
|
+
|
|
22
|
+
const cache = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {TemplateStringsArray} strings static HTML chunks (author-controlled)
|
|
26
|
+
* @param {...any} values dynamic values, one per slot
|
|
27
|
+
* @returns {{ template: HTMLTemplateElement, parts: object[], values: any[] }}
|
|
28
|
+
*/
|
|
29
|
+
export function html(strings, ...values) {
|
|
30
|
+
let compiled = cache.get(strings);
|
|
31
|
+
if (!compiled) {
|
|
32
|
+
compiled = compile(strings);
|
|
33
|
+
cache.set(strings, compiled);
|
|
34
|
+
}
|
|
35
|
+
return { template: compiled.template, parts: compiled.parts, hasElements: compiled.hasElements, values };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- scanner ----------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const TEXT = 0;
|
|
41
|
+
const TAGNAME = 1;
|
|
42
|
+
const BEFORE_ATTR = 2;
|
|
43
|
+
const ATTR_NAME = 3;
|
|
44
|
+
const AFTER_NAME = 4;
|
|
45
|
+
const BEFORE_VAL = 5;
|
|
46
|
+
const VAL_DQ = 6;
|
|
47
|
+
const VAL_SQ = 7;
|
|
48
|
+
const VAL_UQ = 8;
|
|
49
|
+
const CLOSE_TAG = 9;
|
|
50
|
+
const COMMENT = 10;
|
|
51
|
+
const RAWTEXT = 11;
|
|
52
|
+
|
|
53
|
+
const RAWTEXT_TAGS = new Set(["script", "style", "textarea", "title"]);
|
|
54
|
+
const isWS = (c) => c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f";
|
|
55
|
+
const isLetter = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z";
|
|
56
|
+
|
|
57
|
+
function unsupported(message) {
|
|
58
|
+
throw new Error(`Zoijs template: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compile(strings) {
|
|
62
|
+
let out = "";
|
|
63
|
+
const parts = [];
|
|
64
|
+
let state = TEXT;
|
|
65
|
+
|
|
66
|
+
// current tag / attribute being built
|
|
67
|
+
let tagName = "";
|
|
68
|
+
let dynAttrs = [];
|
|
69
|
+
let selfClose = false;
|
|
70
|
+
let rawtextTag = "";
|
|
71
|
+
let attrStart = 0; // index in `out` where the current attribute's name starts
|
|
72
|
+
let attrName = "";
|
|
73
|
+
let dynamic = false; // current attribute value contains a hole
|
|
74
|
+
let strs = null; // static fragments of the current dynamic attribute value
|
|
75
|
+
let holes = null; // hole indices of the current dynamic attribute value
|
|
76
|
+
let frag = ""; // running static fragment of the current attribute value
|
|
77
|
+
|
|
78
|
+
const finalizeAttr = () => {
|
|
79
|
+
if (dynamic) {
|
|
80
|
+
strs.push(frag);
|
|
81
|
+
const whole = strs.length === 2 && strs[0] === "" && strs[1] === "";
|
|
82
|
+
let event = false;
|
|
83
|
+
if (/^on/i.test(attrName)) {
|
|
84
|
+
if (!whole) unsupported(`event handler "${attrName}" must be a single \${} value`);
|
|
85
|
+
event = true;
|
|
86
|
+
}
|
|
87
|
+
dynAttrs.push({ name: attrName, strings: strs, holes, event, whole });
|
|
88
|
+
}
|
|
89
|
+
dynamic = false;
|
|
90
|
+
strs = null;
|
|
91
|
+
holes = null;
|
|
92
|
+
frag = "";
|
|
93
|
+
attrName = "";
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const finalizeTag = () => {
|
|
97
|
+
if (dynAttrs.length) {
|
|
98
|
+
out += " data-zoijs-bind";
|
|
99
|
+
parts.push({ type: "element", attrs: dynAttrs });
|
|
100
|
+
}
|
|
101
|
+
out += selfClose ? "/>" : ">";
|
|
102
|
+
const lower = tagName.toLowerCase();
|
|
103
|
+
if (RAWTEXT_TAGS.has(lower)) {
|
|
104
|
+
state = RAWTEXT;
|
|
105
|
+
rawtextTag = lower;
|
|
106
|
+
} else {
|
|
107
|
+
state = TEXT;
|
|
108
|
+
}
|
|
109
|
+
tagName = "";
|
|
110
|
+
dynAttrs = [];
|
|
111
|
+
selfClose = false;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < strings.length; i++) {
|
|
115
|
+
const s = strings[i];
|
|
116
|
+
|
|
117
|
+
for (let k = 0; k < s.length; k++) {
|
|
118
|
+
const c = s[k];
|
|
119
|
+
|
|
120
|
+
switch (state) {
|
|
121
|
+
case TEXT:
|
|
122
|
+
if (c === "<") {
|
|
123
|
+
const nx = s[k + 1];
|
|
124
|
+
if (nx === "/") { out += "<"; state = CLOSE_TAG; }
|
|
125
|
+
else if (nx === "!") { out += "<"; state = s.substr(k + 1, 3) === "!--" ? COMMENT : CLOSE_TAG; }
|
|
126
|
+
else if (nx && isLetter(nx)) { out += "<"; state = TAGNAME; tagName = ""; dynAttrs = []; }
|
|
127
|
+
else if (nx === undefined && i < strings.length - 1) { out += "<"; state = TAGNAME; tagName = ""; dynAttrs = []; } // "<${tag}>" → clear error at the hole
|
|
128
|
+
else out += "<"; // a literal "<" in text (e.g. "a < b")
|
|
129
|
+
} else out += c;
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case TAGNAME:
|
|
133
|
+
if (isWS(c)) { out += c; state = BEFORE_ATTR; }
|
|
134
|
+
else if (c === ">") finalizeTag();
|
|
135
|
+
else if (c === "/") selfClose = true;
|
|
136
|
+
else { tagName += c; out += c; }
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case BEFORE_ATTR:
|
|
140
|
+
if (isWS(c)) out += c;
|
|
141
|
+
else if (c === ">") finalizeTag();
|
|
142
|
+
else if (c === "/") selfClose = true;
|
|
143
|
+
else { state = ATTR_NAME; attrName = c; attrStart = out.length; out += c; dynamic = false; }
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case ATTR_NAME:
|
|
147
|
+
if (c === "=") { out += c; state = BEFORE_VAL; }
|
|
148
|
+
else if (isWS(c)) { out += c; state = AFTER_NAME; }
|
|
149
|
+
else if (c === ">") finalizeTag();
|
|
150
|
+
else if (c === "/") { selfClose = true; state = AFTER_NAME; }
|
|
151
|
+
else { attrName += c; out += c; }
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case AFTER_NAME:
|
|
155
|
+
if (isWS(c)) out += c;
|
|
156
|
+
else if (c === "=") { out += c; state = BEFORE_VAL; }
|
|
157
|
+
else if (c === ">") finalizeTag();
|
|
158
|
+
else if (c === "/") selfClose = true;
|
|
159
|
+
else { state = ATTR_NAME; attrName = c; attrStart = out.length; out += c; dynamic = false; }
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case BEFORE_VAL:
|
|
163
|
+
if (isWS(c)) out += c;
|
|
164
|
+
else if (c === '"' || c === "'") { out += c; frag = ""; dynamic = false; state = c === '"' ? VAL_DQ : VAL_SQ; }
|
|
165
|
+
else { frag = c; out += c; dynamic = false; state = VAL_UQ; }
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case VAL_DQ:
|
|
169
|
+
case VAL_SQ: {
|
|
170
|
+
const q = state === VAL_DQ ? '"' : "'";
|
|
171
|
+
if (c === q) { if (dynamic) finalizeAttr(); else out += c; state = BEFORE_ATTR; }
|
|
172
|
+
else if (dynamic) frag += c;
|
|
173
|
+
else { frag += c; out += c; }
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case VAL_UQ:
|
|
178
|
+
if (isWS(c)) { if (dynamic) finalizeAttr(); out += c; state = BEFORE_ATTR; }
|
|
179
|
+
else if (c === ">") { if (dynamic) finalizeAttr(); finalizeTag(); }
|
|
180
|
+
else if (dynamic) frag += c;
|
|
181
|
+
else { frag += c; out += c; }
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case CLOSE_TAG:
|
|
185
|
+
out += c;
|
|
186
|
+
if (c === ">") state = TEXT;
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case COMMENT:
|
|
190
|
+
out += c;
|
|
191
|
+
if (c === ">" && out.endsWith("-->")) state = TEXT;
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case RAWTEXT:
|
|
195
|
+
out += c;
|
|
196
|
+
if (c === ">" && out.toLowerCase().endsWith("</" + rawtextTag + ">")) state = TEXT;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// end of chunk → a hole (unless this was the last chunk)
|
|
202
|
+
if (i < strings.length - 1) {
|
|
203
|
+
const hole = i;
|
|
204
|
+
switch (state) {
|
|
205
|
+
case TEXT:
|
|
206
|
+
out += "<!--zoijs-->";
|
|
207
|
+
parts.push({ type: "child", hole });
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case BEFORE_VAL: // attr=${x} → unquoted whole-value hole
|
|
211
|
+
out = out.slice(0, attrStart);
|
|
212
|
+
dynamic = true; strs = [""]; holes = [hole]; frag = "";
|
|
213
|
+
state = VAL_UQ;
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case VAL_DQ:
|
|
217
|
+
case VAL_SQ:
|
|
218
|
+
case VAL_UQ:
|
|
219
|
+
if (!dynamic) { out = out.slice(0, attrStart); dynamic = true; strs = [frag]; holes = [hole]; frag = ""; }
|
|
220
|
+
else { strs.push(frag); holes.push(hole); frag = ""; }
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case TAGNAME:
|
|
224
|
+
unsupported("dynamic tag names are not supported (e.g. `<${tag}>`)");
|
|
225
|
+
case BEFORE_ATTR:
|
|
226
|
+
case ATTR_NAME:
|
|
227
|
+
case AFTER_NAME:
|
|
228
|
+
unsupported("dynamic attribute names / spreads are not supported (e.g. `<el ${x}>`)");
|
|
229
|
+
case COMMENT:
|
|
230
|
+
unsupported("interpolation inside an HTML comment is not supported");
|
|
231
|
+
case RAWTEXT:
|
|
232
|
+
unsupported(`interpolation inside <${rawtextTag}> is not supported — set its content via a property instead`);
|
|
233
|
+
case CLOSE_TAG:
|
|
234
|
+
unsupported("interpolation inside a closing tag is not supported");
|
|
235
|
+
default:
|
|
236
|
+
unsupported("interpolation in an unsupported position");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hasElements = parts.some((p) => p.type === "element");
|
|
242
|
+
return { template: createTemplate(out), parts, hasElements };
|
|
243
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// mount() — the single entry point that starts an app.
|
|
2
|
+
//
|
|
3
|
+
// Runs the component once inside a root owner scope, inserts its DOM, and returns
|
|
4
|
+
// an unmount() that disposes that scope (every effect, computed, list item, and
|
|
5
|
+
// event listener created under it) and detaches the DOM.
|
|
6
|
+
|
|
7
|
+
import { render } from "./renderer.js";
|
|
8
|
+
import { resolveTarget } from "../utils/dom.js";
|
|
9
|
+
import { createOwner, runWithOwner, disposeOwner } from "../reactivity/owner.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Function|object} component a component function, or an html() result
|
|
13
|
+
* @param {Element|string} target a DOM element or a CSS selector
|
|
14
|
+
* @returns {Function} unmount
|
|
15
|
+
*/
|
|
16
|
+
export function mount(component, target) {
|
|
17
|
+
const el = resolveTarget(target);
|
|
18
|
+
const owner = createOwner();
|
|
19
|
+
|
|
20
|
+
let node;
|
|
21
|
+
runWithOwner(owner, () => {
|
|
22
|
+
const result = typeof component === "function" ? component() : component;
|
|
23
|
+
node = render(result).node;
|
|
24
|
+
});
|
|
25
|
+
el.replaceChildren(node);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
disposeOwner(owner);
|
|
29
|
+
el.replaceChildren();
|
|
30
|
+
};
|
|
31
|
+
}
|