@panphora/clayjs 0.7.4 → 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/README.md CHANGED
@@ -4,39 +4,63 @@
4
4
 
5
5
  Self-saving, malleable HTML in one classic `<script>`. Load `clay.js` on a page and it
6
6
  becomes editable in place, snapshots its own DOM, and saves that DOM back to the file it
7
- came from. No build step, no framework.
7
+ came from. No build step, no framework, no account.
8
+
9
+ The document is the app *and* the database. State lives in the DOM, and a save is one POST
10
+ of the entire serialized document. There is no data layer, no JSON on a server, no schema.
11
+ Open the saved file in a text editor and your edit is right there in the HTML.
12
+
13
+ ## See it save itself
14
+
15
+ [`examples/notes.html`](https://github.com/panphora/clayjs/blob/main/examples/notes.html) is a complete self-saving document. Two
16
+ attributes do the work: `autosave` on `<html>`, and `editable` on the parts you type into.
17
+
18
+ ```html
19
+ <!DOCTYPE html>
20
+ <html lang="en" autosave>
21
+ <head><meta charset="utf-8"><title>Notes</title></head>
22
+ <body>
23
+ <h1 editable>My notes</h1>
24
+ <div editable><p>Type anything here.</p></div>
25
+ <script src="https://clayjs.com/v1/clay.js"></script>
26
+ </body>
27
+ </html>
28
+ ```
29
+
30
+ Type into it, then open the file in a text editor: your words are in the HTML.
31
+
32
+ **Something has to write the bytes.** clayjs runs in the browser; it makes the document
33
+ and posts it, and a host puts it on disk. The shortest path is
34
+ [HTML Clay](https://htmlclay.com), a desktop app: rename the file to `.htmlclay` and
35
+ double-click it, and it serves the file at `http://127.0.0.1` and writes every save back.
36
+ [hyperclay.com](https://hyperclay.com) hosts the same file online. Your own server needs
37
+ one route, about twenty lines: see [the endpoint spec](https://clayjs.com/docs#endpoint).
38
+ Full walkthrough: [the tutorial](https://clayjs.com/get-started) and
39
+ [`examples/`](https://github.com/panphora/clayjs/tree/main/examples).
8
40
 
9
41
  ## Use
10
42
 
11
43
  ```html
12
- <script src="https://clayjs.com/clay.js"></script>
44
+ <script src="https://clayjs.com/v1/clay.js"></script>
13
45
  ```
14
46
 
15
47
  The loader detects edit vs view mode and pulls only the modules it needs. Tune it with
16
48
  query params on the script URL:
17
49
 
18
50
  ```html
19
- <script src="/clay.js?plugins=sync,cms&exclude=indicator"></script>
51
+ <script src="https://clayjs.com/v1/clay.js?plugins=sync,cms&exclude=indicator"></script>
20
52
  ```
21
53
 
22
54
  - `?plugins=` — add optional plugins: `sync`, `cms`, `undo`, `sortable`, `indicator`, `quickcrop`, `upload`, `wire`, `demo`.
23
55
  Only `richclay` loads by default, and only in edit mode. `cms` brings `quickcrop` with it, because
24
56
  the CMS uses it for `data-hcms-crop` image fields.
25
57
  - `?exclude=` — drop a plugin that would otherwise load (a default, or one another plugin pulled in).
26
- - `?editmode=false` — force view mode (URL param wins over everything below).
27
-
28
- ## Host attributes
58
+ - `?editmode=false` — force view mode (URL param wins over everything else).
29
59
 
30
- A host can put these on `<html>` in the response it serves. They are ephemeral:
31
- the host injects them and strips them back out of whatever gets saved, so they
32
- never reach the file on disk.
33
-
34
- - `savetoken="…"` — the per-document save credential. clayjs posts to
35
- `/_/save/{token}` and sends no cookies, because the token is the credential.
36
- A token also implies edit mode, which is the only such signal a sandboxed
37
- document can see. `htmlclaytoken` is the older spelling of the same thing.
38
- Edit mode is decided in this order: the `?editmode` param, then
39
- `window.clayEditMode`, then a save token, then the platform's owner cookie.
60
+ Edit mode is decided in this order: the `?editmode` param, then `window.clayEditMode`,
61
+ then a save token stamped on `<html>` by the host, then the platform's owner cookie. Hosts
62
+ and save tokens are covered in [the reference](https://github.com/panphora/clayjs/blob/main/docs/reference.md) and on
63
+ [clayjs.com/docs](https://clayjs.com/docs#editmode).
40
64
 
41
65
  ## Readiness
42
66
 
@@ -48,12 +72,6 @@ await clay.ready; // or: document.addEventListener("clay:ready", ...)
48
72
  clay.save();
49
73
  ```
50
74
 
51
- Edit mode exposes `clay.save()` (+ `clay.save.force()`), `clay.getHTML()`, `clay.addDocumentTransform(fn)`,
52
- `clay.onSnapshot(fn)`, `clay.toggleEditMode()`, `clay.isEditMode`, `clay.isOwner`, `clay.Mutation`,
53
- `clay.region`, `clay.cacheBust(el)`, plus `clay.undo` / `clay.cms` / `clay.morph` / `clay.RichClay` /
54
- `clay.quickcrop` when those plugins load. View mode keeps only the always-available members (`toggleEditMode`, `isEditMode`, `isOwner`,
55
- `Mutation`, `region`, `ready`); edit-only members are simply absent.
56
-
57
75
  ## API
58
76
 
59
77
  Three tiers. The first two are a promise; the third is not.
@@ -64,55 +82,23 @@ Three tiers. The first two are a promise; the third is not.
64
82
  | `clay.*` from a satellite (`clay-ui`, `clay-utils`, `clay-dom`, `clay-events`, `clay-options`, `clay-internals`) | opt-in, one script tag each | stable |
65
83
  | anything else under `src/` | reachable by direct import, because `src/` ships | **may change in any release** |
66
84
 
67
- The contract starts at **0.3.0**: no name below changes without a major version.
85
+ The contract starts at **1.0.0**: no name below changes without a major version.
68
86
 
69
87
  **`clay.js`** — `ready`, `save()`, `save.force()`, `getHTML()`, `addDocumentTransform(fn)`, `onSnapshot(fn)`,
70
88
  `toggleEditMode()`, `isEditMode`, `isOwner`, `Mutation`, `region`, `cacheBust(el)`, plus `undo` / `cms` / `morph` /
71
- `RichClay` / `quickcrop` when those plugins load. View mode keeps only the always-available members, as above.
72
-
73
- `addDocumentTransform(fn)` runs your callback over a detached clone whenever the page
74
- prepares to save AND whenever it checks whether anything changed. Keep it pure and
75
- repeatable: it is a transform, not a "a save is happening" event.
76
-
77
- `quickcrop(file, options)` opens a crop modal over a File or Blob and resolves
78
- `{ blob, dataURL, width, height }`, or `null` if the person cancels. `aspect` (a number,
79
- or null for freeform), `type`, `quality`, `maxWidth`/`maxHeight` and `labels.confirm` are
80
- the options worth knowing. Its modal and injected stylesheet carry `save-remove`, so they
81
- never reach the saved file.
82
-
83
- `region` is the region-policy model: `resolveRegionPolicy`, `isInert`, `skipForPolicy`,
84
- `strictestPolicy`, the `PERSIST` and `REGION_ATTRS` token constants, and the
85
- `STRIP_FROM_SAVE`, `FREEZE_SELECTOR` and `STRIP_FROM_COMPARISON` selectors.
86
- `clay.internals.region` is a different shape, narrower in aim, built for snapshot work.
89
+ `RichClay` / `quickcrop` when those plugins load. View mode keeps only the always-available members
90
+ (`toggleEditMode`, `isEditMode`, `isOwner`, `Mutation`, `region`, `ready`); edit-only members are simply absent.
87
91
 
88
92
  **Satellites** — one script tag each, and each resolves its own `clay.loaded.*` promise. `clay-ui` adds
89
93
  `toast`, `toastPersistent`, `ask`, `confirm`, `tell`, `snippet`, `modal`; `clay-utils` adds `clay.utils`
90
94
  (`throttle`, `debounce`, `cookie`, `slugify`, `copyToClipboard`); `clay-internals` adds `clay.internals`,
91
- below. `clay-events`, `clay-dom`, `clay-options` and `all.js` add HTML attributes and DOM helpers rather
95
+ the low-level surface for code that needs to sit *inside* the save lifecycle rather than call it.
96
+ `clay-events`, `clay-dom`, `clay-options` and `all.js` add HTML attributes and DOM helpers rather
92
97
  than members on `clay`.
93
98
 
94
- **`clay.internals`** — the pieces the library builds itself out of, for code that needs to sit inside the
95
- save lifecycle rather than call it. Lower level than `clay.*` deliberately: it assumes you know the
96
- lifecycle.
97
-
98
- ```html
99
- <script src="https://clayjs.com/clay-internals.js"></script>
100
- ```
101
-
102
- - `captureSnapshot()`, `captureForSave()` — the snapshot pipeline, read side.
103
- `captureSnapshot` gives you the clone before any stripping; `captureForSave` gives you the
104
- bytes a save would send.
105
- - `addDocumentTransform(fn)` — the same registry as `clay.addDocumentTransform`. Core only
106
- publishes that name in edit mode, so reach for this one from a view-mode page or from the
107
- satellite with no core loaded.
108
- - `region.addRegionToken(el, token)`, `region.resolveRegionPolicy(node)`, `region.isInert(node)`,
109
- `region.isSnapshotRemoved(el)`, `region.PERSIST`, `region.REGION_ATTRS`, and
110
- `region.selectors.stripFromSave` / `.stripFromComparison` / `.stripFromDirtyCheck` /
111
- `.noTriggerAutosave` / `.noDirty` / `.snapshotRemove` / `.freeze` — write your own attribute
112
- without hardcoding our selectors.
113
- - `save.saveHtml(html, cb, opts)`, `save.replacePageWith(url, cb)`, `save.isSaveInProgress()` — the save
114
- lane under `clay.save`. **`saveHtml` writes the bytes you hand it straight to the file**, bypassing the
115
- snapshot pipeline entirely; check `isSaveInProgress()` first.
99
+ Every member, every attribute, and the endpoint spec: **[docs/reference.md](https://github.com/panphora/clayjs/blob/main/docs/reference.md)**,
100
+ also served at [clayjs.com/llms.txt](https://clayjs.com/llms.txt) and rendered as
101
+ [clayjs.com/docs](https://clayjs.com/docs).
116
102
 
117
103
  ## Regions
118
104
 
@@ -133,41 +119,46 @@ and live sync will not overwrite it, while a `no-dirty` region renders itself fr
133
119
  so it never warns and an incoming sync frame may replace it. Use `no-dirty` for filter bars,
134
120
  projections and drag previews; use `no-trigger-autosave` for a heavy editor you save by hand.
135
121
 
122
+ ## Repo map
123
+
124
+ **Everything in `entries/` is served under a version prefix on clayjs.com.**
125
+ `entries/clay.js` is `https://clayjs.com/v1/clay.js`, which rolls forward within major
126
+ version 1, and `https://clayjs.com/1.0.0/clay.js`, which never changes. Same for each
127
+ satellite. A saved document hardcodes its script URL and has no update channel, which is
128
+ why the version is in the URL and why a served path, once published, is permanent. Repo
129
+ paths are not: `build.js` flattens `entries/` into each prefix on the way out. `clay.js`
130
+ derives its own base URL from its script tag and imports `<base>/src/loader.js`, so `src/`
131
+ is public surface too and ships inside every prefix as its sibling.
132
+
133
+ | | |
134
+ |---|---|
135
+ | `entries/clay.js` | the bootstrap: everything starts here |
136
+ | `entries/clay-ui.js`, `-dom`, `-events`, `-options`, `-utils`, `-internals` | satellites, one script tag each |
137
+ | `entries/all.js` | `All(selector)`, a chainable `querySelectorAll` wrapper |
138
+ | `entries/sap.js` | reactive templating, **generated** from the `sapjs` repo |
139
+ | `entries/clay-data.js` | the HTML data API, **generated** from the `hyper-html-api` repo |
140
+ | `src/` | the implementation, and public surface by direct import |
141
+ | `conformance/` | the byte-exact gate: real browser, pinned Chromium, goldens from the spec |
142
+ | `tests/` | jest unit suite, fixtures, and a stub save server |
143
+ | `website/` | the source of clayjs.com |
144
+ | `docs/reference.md` | the full reference, served as `/llms.txt` |
145
+ | `build.js`, `wrangler.jsonc` | assemble and deploy `public/`, which is a gitignored build output |
146
+
136
147
  ## Develop
137
148
 
138
149
  ```bash
139
150
  npm test # jest unit suite
140
151
  npm run test:conformance # byte-for-byte fixture gate, in a real browser
141
152
  npm run dev # stub save server on :4601 for the tests/fixtures pages
153
+ npm run build # rebuild public/, the deploy output
142
154
  ```
143
155
 
144
- ## Deploy
145
-
146
- `public/` is what wrangler serves to clayjs.com. It is a build output: gitignored,
147
- disposable, and rebuilt from scratch every time.
148
-
149
- ```bash
150
- npm run build # rebuild public/ from source
151
- npm run deploy # rebuild, then wrangler deploy
152
- ```
153
-
154
- Never edit `public/` by hand, and never copy a file into it. Its contents are
155
- **derived**:
156
-
157
- ```
158
- public/ = package.json "files" (minus THIRD-PARTY-NOTICES.md, which nothing requests)
159
- + website/* (flattened to the root, so /docs.html works)
160
- ```
161
-
162
- That derivation is the point. `files` is the list npm publishes, so it is already
163
- the line you edit to ship a new satellite, and deriving from it means a file cannot
164
- reach npm and miss the site. Before this script existed the mirror was hand-copied,
165
- and it drifted exactly that way: `src/core/host-attrs.js` never made it across, and
166
- since `loader.js` imports `is-edit-mode.js`, which imports it, the deployed
167
- `clay.js` could not boot at all.
156
+ Setup, the pinned-browser requirement, and the things about this repo that will surprise
157
+ you are in [CONTRIBUTING.md](https://github.com/panphora/clayjs/blob/main/CONTRIBUTING.md).
168
158
 
169
159
  ## License
170
160
 
171
161
  Our code is MIT-0 (MIT No Attribution): use it, remix it, ship it, no attribution
172
162
  needed. Vendored third-party files keep their original permissive licenses; see
173
- [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
163
+ [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md). Security reports go through
164
+ [the security policy](https://github.com/panphora/clayjs/blob/main/.github/SECURITY.md).
@@ -12,15 +12,13 @@ or carry third-party code, and keep their original licenses.
12
12
 
13
13
  ## Squire 2.4.8 (MIT)
14
14
 
15
- - Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
16
- (richclay bundles Squire)
15
+ - Carried inside src/vendor/richclay.vendor.js (richclay bundles Squire)
17
16
  - Copyright (c) 2011-2023 by Neil Jenkins
18
17
  - https://github.com/fastmail/Squire
19
18
 
20
19
  ## DOMPurify 3.4.11 (Apache-2.0 OR MPL-2.0)
21
20
 
22
- - Carried inside src/vendor/richclay.vendor.js and website/vendor/richclay.min.js
23
- (richclay bundles DOMPurify)
21
+ - Carried inside src/vendor/richclay.vendor.js (richclay bundles DOMPurify)
24
22
  - (c) Cure53 and other contributors
25
23
  - https://github.com/cure53/DOMPurify
26
24
 
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.all = import(base + "/src/dom/all.js")
15
21
  .then(function (m) {
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.dom = import(base + "/src/dom/dom-helpers.js")
15
21
  .catch(function (err) { console.error("clay-dom failed to load:", err); throw err; });
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // The mutation-backed attrs subscribe to a hub that observes document.body, so
15
21
  // wait for DOM ready before importing (same gate as clay.js).
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // No DOM gate: nothing here touches document.body at import time, unlike clay-ui.
15
21
  clay.loaded.internals = import(base + "/src/internals/index.js")
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // Subscribes to the mutation hub (observes document.body); wait for DOM ready.
15
21
  function domReady() {
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // Toasts and dialogs append to document.body, so `loaded.ui` must not resolve
15
21
  // before the DOM is ready (same gate as clay-events).
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.utils = import(base + "/src/utils/index.js")
15
21
  .catch(function (err) { console.error("clay-utils failed to load:", err); throw err; });
@@ -31,7 +31,21 @@
31
31
  clay.__booted = true;
32
32
 
33
33
  var url = new URL(script.src, location.href);
34
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
34
+ // The query and the fragment come off before the last slash is found. Leaving them
35
+ // on split the base inside them, so clay.js?next=/a/b imported ".../clay.js?next=/a/src/loader.js"
36
+ // and the library never booted.
37
+ var path = url.href.split("#")[0].split("?")[0];
38
+ var base = path.slice(0, path.lastIndexOf("/"));
39
+ // In the npm tarball the entry scripts sit in entries/ while src/ stays beside it
40
+ // at the package root, so a CDN that serves package paths literally (jsDelivr,
41
+ // unpkg) would look for src/ one directory too deep and load nothing. Stepping out
42
+ // of an "entries" segment makes those URLs resolve. clayjs.com never has the
43
+ // segment: build.js flattens entries/ into each version prefix, so this is a no-op
44
+ // there. The question is asked of the pathname, because asking it of the whole URL
45
+ // matched a host merely named "entries" and sent the import to another origin.
46
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
47
+ base = base.slice(0, -8);
48
+ }
35
49
  import(base + "/src/loader.js")
36
50
  .then(function (m) { return m.boot(base, url.searchParams, clay.__readyResolve); })
37
51
  .catch(function (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "0.7.4",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
6
6
  "license": "MIT-0",
@@ -9,18 +9,32 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/panphora/clayjs.git"
11
11
  },
12
+ "bugs": {
13
+ "url": "https://github.com/panphora/clayjs/issues"
14
+ },
15
+ "keywords": [
16
+ "html",
17
+ "self-saving",
18
+ "malleable",
19
+ "local-first",
20
+ "no-build",
21
+ "contenteditable",
22
+ "dom",
23
+ "autosave",
24
+ "single-file",
25
+ "static-site",
26
+ "vanilla-javascript",
27
+ "html-editor"
28
+ ],
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ "./THIRD-PARTY-NOTICES.md": "./THIRD-PARTY-NOTICES.md",
32
+ "./src/*": "./src/*",
33
+ "./entries/*": "./entries/*",
34
+ "./*": "./entries/*"
35
+ },
12
36
  "files": [
13
- "clay.js",
14
- "clay-ui.js",
15
- "clay-internals.js",
16
- "clay-events.js",
17
- "clay-options.js",
18
- "clay-dom.js",
19
- "all.js",
20
- "clay-utils.js",
21
- "sap.js",
22
- "clay-data.js",
23
- "_headers",
37
+ "entries/",
24
38
  "THIRD-PARTY-NOTICES.md",
25
39
  "src/"
26
40
  ],
@@ -29,8 +43,8 @@
29
43
  },
30
44
  "scripts": {
31
45
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
32
- "test:conformance": "UPDATE_GOLDENS=0 web-test-runner",
33
- "conformance:update": "UPDATE_GOLDENS=1 web-test-runner",
46
+ "test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
47
+ "conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
34
48
  "prepublishOnly": "npm test && npm run test:conformance",
35
49
  "dev": "node tests/stub-server.mjs",
36
50
  "build": "node build.js",
@@ -49,5 +63,15 @@
49
63
  "kind": "lib",
50
64
  "status": "active",
51
65
  "url": "https://clayjs.com"
66
+ },
67
+ "jest": {
68
+ "testEnvironment": "jsdom",
69
+ "testMatch": [
70
+ "**/tests/unit/**/*.test.js"
71
+ ],
72
+ "transform": {},
73
+ "setupFiles": [
74
+ "<rootDir>/tests/jest.setup.js"
75
+ ]
52
76
  }
53
77
  }
@@ -15,6 +15,7 @@ import { isEditMode } from "./is-edit-mode.js";
15
15
  import { savePageThrottled } from "./save.js";
16
16
  import { markUserDriven } from "../lib/user-gesture.js";
17
17
  import { resolveRegionPolicy, skipForPolicy } from "../lib/region-policy.js";
18
+ import { PERSIST, AUTOSAVE } from "../lib/attr-aliases.js";
18
19
 
19
20
  /**
20
21
  * Initialize auto-save on DOM changes
@@ -47,7 +48,7 @@ function initSavePageOnChange() {
47
48
  let inputSaveTimer = null;
48
49
  function initSaveOnPersistInput() {
49
50
  document.addEventListener('input', (e) => {
50
- if (!e.target.closest('[persist]')) return;
51
+ if (!e.target.closest(PERSIST)) return;
51
52
  // A trusted input on a [persist] field is itself a user-driven change the
52
53
  // Mutation hub can't see (form values aren't DOM mutations). Attribute it,
53
54
  // but only when the field is inside the dirty domain: a [persist] control in
@@ -62,7 +63,7 @@ function initSaveOnPersistInput() {
62
63
  }
63
64
 
64
65
  function init() {
65
- if (!document.documentElement.hasAttribute("autosave")) return;
66
+ if (!document.documentElement.matches(AUTOSAVE)) return;
66
67
  if (!isEditMode) return;
67
68
  // initUserGesture moved to save.js's init: gesture provenance belongs to every
68
69
  // editable page, not only the ones with <html autosave>.
@@ -1,5 +1,6 @@
1
1
  import { onSnapshot } from './snapshot.js';
2
2
  import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/control-serialize.vendor.js';
3
+ import { PERSIST } from '../lib/attr-aliases.js';
3
4
 
4
5
  // Persistent Form Input Values
5
6
  //
@@ -51,7 +52,7 @@ import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/
51
52
  // When event-attrs is loaded, its cloneNode() intercept (onclone.js) patches
52
53
  // data-value into textContent on cloned textareas automatically.
53
54
 
54
- export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
55
+ export default function enablePersistentFormInputValues(filterBySelector = PERSIST) {
55
56
  const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
56
57
  const textareaSelector = `textarea${filterBySelector}`;
57
58
  const selectSelector = `select${filterBySelector}`;
@@ -106,7 +107,7 @@ export default function enablePersistentFormInputValues(filterBySelector = "[per
106
107
 
107
108
  // Auto-initialize with default selector
108
109
  export function init() {
109
- enablePersistentFormInputValues("[persist]");
110
+ enablePersistentFormInputValues(PERSIST);
110
111
  }
111
112
 
112
113
  // Auto-init when module is imported
@@ -0,0 +1,13 @@
1
+ // clayjs spells its attributes without a prefix, which is the whole pitch:
2
+ // <h1 editable> reads like HTML rather than like a framework. No attribute in the
3
+ // HTML standard uses these names and no proposal does either, so the risk of the
4
+ // spelling being taken is small. It is not zero, and a saved document hardcodes
5
+ // the attribute and can never be reached to migrate it.
6
+ //
7
+ // These clay- spellings are read everywhere the bare name is read, and are left
8
+ // out of the documentation on purpose. Their only job is to already exist in every
9
+ // 1.x build: if a bare name ever stops being ours, a file written today can be
10
+ // repaired by adding one attribute instead of needing a migration that cannot
11
+ // reach it. An escape hatch added after the collision would be worthless.
12
+ export const PERSIST = ":is([persist], [clay-persist])";
13
+ export const AUTOSAVE = ":is([autosave], [clay-autosave])";
@@ -28,13 +28,14 @@
28
28
  import Mutation from './mutation.js';
29
29
  import { isEditMode } from '../core/is-edit-mode.js';
30
30
  import { STRIP_FROM_DIRTY_CHECK, SNAPSHOT_REMOVE_SELECTOR } from './region-policy.js';
31
+ import { PERSIST } from './attr-aliases.js';
31
32
 
32
33
  let changes = 0;
33
34
  let clearedAt = 0;
34
35
  let paused = false;
35
36
  let started = false;
36
37
 
37
- const PERSIST_CONTROLS = 'input[persist], textarea[persist], select[persist]';
38
+ const PERSIST_CONTROLS = `input${PERSIST}, textarea${PERSIST}, select${PERSIST}`;
38
39
 
39
40
  // Regions the loss oracle never sees. The hub feed already skips them, through
40
41
  // `require: 'dirty'`, and the input feed has to skip them for the same reason:
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // <html autosave demo-key="my-page-v1"> <!-- both attributes optional -->
5
5
  // <script>window.clayEditMode = true;</script> <!-- before the clay.js tag -->
6
- // <script src="https://clayjs.com/clay.js?plugins=demo"></script>
6
+ // <script src="https://clayjs.com/v1/clay.js?plugins=demo"></script>
7
7
  //
8
8
  // The fetch shim answers POST /_/save the way a clayjs server would, so the
9
9
  // whole real pipeline (savestatus, events, autosave, ⌘S) runs unchanged. The
@@ -112,7 +112,7 @@ async function restore() {
112
112
  // attribute, never the element: [contenteditable] cannot join CHROME_SELECTOR,
113
113
  // whose matches are removed outright, and doing that deletes real content.
114
114
  if (!isEditMode) {
115
- for (const el of doc.querySelectorAll("[editable][contenteditable]")) {
115
+ for (const el of doc.querySelectorAll(":is([editable], [clay-editable])[contenteditable]")) {
116
116
  el.removeAttribute("contenteditable");
117
117
  }
118
118
  }