@panphora/clayjs 1.19.0 → 1.20.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/dist/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +8186 -8076
- package/dist/m/attrs/{refetch-on-save-ZUE7PSCI.js → refetch-on-save-DPGJJMGN.js} +1 -1
- package/dist/m/attrs/save-freeze-MTFVVYV5.js +1 -0
- package/dist/m/{c-DHUCEMXE.js → c-2VB555FG.js} +1 -1
- package/dist/m/{c-KGHVFRR3.js → c-3AXJDL2P.js} +1 -1
- package/dist/m/{c-FOOE4GML.js → c-3GJ4XARY.js} +1 -1
- package/dist/m/c-3TWCKFOP.js +1 -0
- package/dist/m/{c-LPCZOYTX.js → c-3YH6JSIL.js} +1 -1
- package/dist/m/{c-AIWN4QN4.js → c-44B5ODN7.js} +2 -2
- package/dist/m/{c-S2U6NJU4.js → c-62HZW5DH.js} +1 -1
- package/dist/m/{c-ZGDYHKG3.js → c-67ILSNPR.js} +1 -1
- package/dist/m/c-67LGZ52V.js +20 -0
- package/dist/m/{c-ZRUQ5FSU.js → c-7SXYQCN4.js} +1 -1
- package/dist/m/{c-LACAAQFB.js → c-AL4RBJF2.js} +1 -1
- package/dist/m/{c-L42OLXWM.js → c-ANNFZFLN.js} +1 -1
- package/dist/m/{c-QO4HNGCQ.js → c-B4VYNXTQ.js} +1 -1
- package/dist/m/{c-7PRJCA3E.js → c-BJPZCWAV.js} +1 -1
- package/dist/m/{c-V52FE5FI.js → c-BTLFLLML.js} +1 -1
- package/dist/m/c-C4IIQZGJ.js +1 -0
- package/dist/m/{c-5T6NZHNM.js → c-CAWNFER6.js} +1 -1
- package/dist/m/c-CSNE5EIF.js +1 -0
- package/dist/m/{c-DVYGFTSK.js → c-DDDOKPI6.js} +2 -2
- package/dist/m/{c-2RARVIAK.js → c-DHXPDLW6.js} +1 -1
- package/dist/m/{c-WCXLG3MV.js → c-E4IBSAQM.js} +1 -1
- package/dist/m/{c-IVTRVENZ.js → c-EDSPKYMA.js} +1 -1
- package/dist/m/{c-2Y4DOHSY.js → c-EFD6QNTE.js} +1 -1
- package/dist/m/{c-7MG5RU63.js → c-EGTIAL6P.js} +1 -1
- package/dist/m/{c-FCNAO2MI.js → c-EOCKLMKX.js} +1 -1
- package/dist/m/{c-UXFUJEZO.js → c-EOCZ2YVL.js} +1 -1
- package/dist/m/{c-K72SY2TL.js → c-EZ7B5AZT.js} +1 -1
- package/dist/m/{c-UPD33ZPE.js → c-FDAXZ2PM.js} +1 -1
- package/dist/m/{c-P3CK66NT.js → c-FNVQAACK.js} +1 -1
- package/dist/m/c-FSWRGBVQ.js +1 -0
- package/dist/m/c-FTWGYI5E.js +1 -0
- package/dist/m/{c-WKNRJX3C.js → c-GKEATME6.js} +1 -1
- package/dist/m/{c-QINHDETP.js → c-GPGYKER7.js} +1 -1
- package/dist/m/{c-FWKHM4E7.js → c-GSRLXYWL.js} +1 -1
- package/dist/m/{c-B7J4K2AE.js → c-GVPNDIKJ.js} +2 -2
- package/dist/m/{c-FTQKI7SP.js → c-I2IFKJX2.js} +1 -1
- package/dist/m/c-ICMAOOGK.js +1 -0
- package/dist/m/{c-HUISJXHF.js → c-J3BXFTZS.js} +1 -1
- package/dist/m/{c-BIJALSRD.js → c-JDZDF2QM.js} +1 -1
- package/dist/m/{c-UDHIVLYH.js → c-JH5FF4NL.js} +1 -1
- package/dist/m/{c-OYRZ7XG2.js → c-JHJR5BQX.js} +1 -1
- package/dist/m/{c-V44KWI2L.js → c-JOH753EJ.js} +2 -2
- package/dist/m/{c-P3OFYUMA.js → c-JUE7B4OX.js} +1 -1
- package/dist/m/c-K76AUL4I.js +7 -0
- package/dist/m/{c-ZWIOJGMT.js → c-LA2BW7UY.js} +1 -1
- package/dist/m/{c-ICJYHABF.js → c-LDUNOR57.js} +1 -1
- package/dist/m/{c-XSZKP646.js → c-LIBE4HPD.js} +1 -1
- package/dist/m/c-LPUJXRXW.js +1 -0
- package/dist/m/{c-JK3PHE7J.js → c-LQU7TXIH.js} +1 -1
- package/dist/m/{c-5ZQV7KC4.js → c-LV5MABYD.js} +1 -1
- package/dist/m/{c-GWNC3UGL.js → c-M4FSPNQ2.js} +1 -1
- package/dist/m/{c-2KIEGNC4.js → c-MEWUGR4D.js} +1 -1
- package/dist/m/c-MIL4B2JQ.js +1 -0
- package/dist/m/{c-IVMD3FEM.js → c-N7Y52HJH.js} +1 -1
- package/dist/m/{c-O4JMKN6U.js → c-NFR673TL.js} +2 -2
- package/dist/m/{c-ZKNBMNN3.js → c-NLZ5AN4L.js} +1 -1
- package/dist/m/{c-RRZJWFLE.js → c-OABVRYOT.js} +1 -1
- package/dist/m/c-PIVIYLTY.js +1 -0
- package/dist/m/{c-L7JL5TL3.js → c-QY7F6EN6.js} +1 -1
- package/dist/m/{c-UY4QX42B.js → c-RG6XUZFA.js} +1 -1
- package/dist/m/c-RHMPKZZ7.js +1 -0
- package/dist/m/{c-TJAXGIRV.js → c-RQRMPZXD.js} +1 -1
- package/dist/m/{c-GY422N3Q.js → c-RS77ODLY.js} +1 -1
- package/dist/m/{c-BAUBNYXK.js → c-RT3C74GY.js} +1 -1
- package/dist/m/{c-CAS3NND4.js → c-S5IEIZCN.js} +1 -1
- package/dist/m/{c-HXPHZN7Q.js → c-SBSMJIPC.js} +1 -1
- package/dist/m/c-SFKI6OLM.js +2 -0
- package/dist/m/{c-4FJTBE6I.js → c-SJPD46ZO.js} +4 -4
- package/dist/m/{c-JCP3XDSJ.js → c-SO63UGPQ.js} +1 -1
- package/dist/m/{c-PLP4JKFC.js → c-TUGVINJR.js} +1 -1
- package/dist/m/{c-Q26MME6N.js → c-TWCXBWS3.js} +1 -1
- package/dist/m/{c-GNDL3TKB.js → c-U5X57T4L.js} +1 -1
- package/dist/m/{c-ZS4US2XV.js → c-UXEVJ63W.js} +1 -1
- package/dist/m/{c-KJJPV77R.js → c-V2J5C5MT.js} +1 -1
- package/dist/m/{c-7PYNAULW.js → c-VBLFAQD2.js} +1 -1
- package/dist/m/{c-ERQSNR6K.js → c-VQCEOGKH.js} +2 -2
- package/dist/m/{c-YVZ2L7CE.js → c-VSPAFOKU.js} +1 -1
- package/dist/m/{c-L6FLIUXP.js → c-VY6XRBOW.js} +1 -1
- package/dist/m/{c-2KL3BD52.js → c-W23KVGUO.js} +1 -1
- package/dist/m/{c-AXGY2WTB.js → c-W3K23S72.js} +1 -1
- package/dist/m/c-WFMRX5RZ.js +1 -0
- package/dist/m/{c-BWFVSBDF.js → c-WN3MXOLT.js} +1 -1
- package/dist/m/{c-MIKZEG5I.js → c-XD5MOROJ.js} +1 -1
- package/dist/m/{c-CQQDL6O4.js → c-XQSXKIKM.js} +1 -1
- package/dist/m/{c-I4MQHOEY.js → c-Y33LUUIQ.js} +1 -1
- package/dist/m/{c-JDARJDOT.js → c-YQUJ74R3.js} +1 -1
- package/dist/m/{c-F6G2OGYG.js → c-YSB6JKV2.js} +2 -2
- package/dist/m/{c-WQWIRWO3.js → c-Z32V4MHK.js} +1 -1
- package/dist/m/{c-BHZYSHW4.js → c-ZJLU56DV.js} +1 -1
- package/dist/m/{c-DKLS74XB.js → c-ZQXNHZZT.js} +2 -2
- package/dist/m/{c-TNEM3RNM.js → c-ZSCBEB7I.js} +1 -1
- package/dist/m/core/{admin-attrs-YY7T5FGU.js → admin-attrs-ZG7R5V3E.js} +1 -1
- package/dist/m/core/{autosave-RVJPYLRT.js → autosave-VPORYJSE.js} +1 -1
- package/dist/m/core/{conflict-notice-2WGT52LH.js → conflict-notice-JMAZLVRX.js} +1 -1
- package/dist/m/core/{persist-UKXV5KLA.js → persist-3OPWJKZS.js} +1 -1
- package/dist/m/core/{recovery-ZOUOJVIX.js → recovery-DRFZRJED.js} +1 -1
- package/dist/m/core/{save-A5JXV6ME.js → save-AOJBDSOK.js} +1 -1
- package/dist/m/core/{save-core-MKRPSEEO.js → save-core-IOCMXO5L.js} +1 -1
- package/dist/m/core/{snapshot-NU3YM6GR.js → snapshot-O7II2AGG.js} +1 -1
- package/dist/m/core/unsaved-warning-U5VUJFOE.js +1 -0
- package/dist/m/events/{index-RN2JNGBV.js → index-B6TGDYMG.js} +1 -1
- package/dist/m/internals/{index-TGLQINN3.js → index-HDRANDWE.js} +1 -1
- package/dist/m/lib/{cache-bust-MUMFXZK6.js → cache-bust-NH7VBWHO.js} +1 -1
- package/dist/m/lib/{region-policy-SB2AWVG5.js → region-policy-KWBPROWF.js} +1 -1
- package/dist/m/{loader-34YPFKOH.js → loader-DZZLUBVB.js} +1 -1
- package/dist/m/manifest-3AEF5191.js +1 -0
- package/dist/m/manifest-edit-29A26F22.js +1 -0
- package/dist/m/manifest.json +1202 -1158
- package/dist/m/options/{options-EJMS3K24.js → options-55LQHSJ4.js} +1 -1
- package/dist/m/page/{editor-WRQGHDSG.js → editor-EY2WAAGB.js} +2 -2
- package/dist/m/plugins/{ai-edit-X2L2KIEP.js → ai-edit-J5VLSW5I.js} +1 -1
- package/dist/m/plugins/{demo-OYO5GCAO.js → demo-42BSYQ7T.js} +1 -1
- package/dist/m/plugins/{live-7C36XEI2.js → live-5EZN2EYP.js} +1 -1
- package/dist/m/plugins/menu-KGWKT6CS.js +1 -0
- package/dist/m/plugins/{page-BSW6MGDO.js → page-LVW5DWA4.js} +2 -2
- package/dist/m/plugins/{page-view-WFZVTPQK.js → page-view-5KQ76IJB.js} +1 -1
- package/dist/m/plugins/{sortable-4S6SQC3D.js → sortable-WJZZBM7R.js} +1 -1
- package/dist/m/plugins/{source-TJXOIOYY.js → source-CCEUM3OR.js} +1 -1
- package/dist/m/plugins/{undo-NLPGCJQY.js → undo-3JEYK5QY.js} +1 -1
- package/dist/m/plugins/{upgrade-HSUNDNJD.js → upgrade-NW556WJD.js} +1 -1
- package/dist/m/plugins/{upload-NHGJXZK2.js → upload-TTCQRIM5.js} +1 -1
- package/dist/m/plugins/{visitor-drafts-BW43DGY3.js → visitor-drafts-2AWNT23K.js} +2 -2
- package/dist/m/plugins/{visitor-drafts-owner-7ODQG7JX.js → visitor-drafts-owner-MZP45XSG.js} +1 -1
- package/dist/m/plugins/{wire-6OSIL3I7.js → wire-VRYPY6EJ.js} +1 -1
- package/dist/m/{reader-core-PLZYLYLT.js → reader-core-2GXY7SHY.js} +2 -2
- package/dist/m/sync/{live-sync-UXNI6ZF6.js → live-sync-SKPMFYOO.js} +1 -1
- package/dist/m/ui/{index-IVWN2QC4.js → index-QNIPDHXQ.js} +1 -1
- package/dist/offline-manifest.json +634 -610
- package/docs/guide.md +24 -8
- package/package.json +3 -2
- package/src/core/conflict-download.js +8 -7
- package/src/core/conflict-notice-ui.js +4 -4
- package/src/core/copy-content.js +103 -0
- package/src/core/download.js +14 -2
- package/src/core/save-size-notice.js +2 -2
- package/src/core/upgrade-run.js +5 -1
- package/src/lib/region-policy.js +1 -1
- package/src/page/features/export-html.js +4 -4
- package/src/page/features/export-markdown.js +2 -2
- package/src/page/recovery-ui.js +2 -2
- package/src/page/save-states.js +4 -4
- package/src/plugins/menu/actions.js +1 -1
- package/src/plugins/page.js +4 -4
- package/src/plugins/visitor-drafts/prompt.js +12 -12
- package/src/plugins/wire.js +3 -1
- package/dist/m/attrs/save-freeze-LWGF6XUN.js +0 -1
- package/dist/m/c-DCFSEJZG.js +0 -1
- package/dist/m/c-EXEDYU4H.js +0 -1
- package/dist/m/c-GRXHHFPO.js +0 -1
- package/dist/m/c-HT2DKBV2.js +0 -1
- package/dist/m/c-MCOOHCSG.js +0 -1
- package/dist/m/c-MMXKAX2W.js +0 -1
- package/dist/m/c-VE7RKNKZ.js +0 -7
- package/dist/m/c-VR2BJ4AN.js +0 -1
- package/dist/m/c-WGW764WX.js +0 -1
- package/dist/m/c-WNTYKCDG.js +0 -1
- package/dist/m/c-YBSS55KU.js +0 -20
- package/dist/m/core/unsaved-warning-4ZPEV7BF.js +0 -1
- package/dist/m/manifest-0D166F4F.js +0 -1
- package/dist/m/manifest-edit-3877153E.js +0 -1
- package/dist/m/plugins/menu-MFC4DI5E.js +0 -1
- /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
- /package/dist/m/{c-5JLFROUB.js → c-X5LNHET5.js} +0 -0
- /package/dist/m/plugins/{people-JCHVPF7P.js → people-C7TVBBMR.js} +0 -0
package/docs/guide.md
CHANGED
|
@@ -4,7 +4,7 @@ This is the complete reference for building malleable documents with ClayJS: one
|
|
|
4
4
|
|
|
5
5
|
It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing ([cms: content-managed pages](https://clayjs.com/docs/guide/components/#cms-content-managed-pages)), because an app whose product is editing should not route editing through a form sidebar.
|
|
6
6
|
|
|
7
|
-
Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/
|
|
7
|
+
Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/hyperspacesystems/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
|
|
8
8
|
|
|
9
9
|
| File | What it shows |
|
|
10
10
|
|---|---|
|
|
@@ -892,7 +892,23 @@ Traps in this table:
|
|
|
892
892
|
- **Fill a scratchpad after `clay.ready`, and insert new ones empty.** The contents are captured when the element first appears. Content written before boot, or present in a cloned element when inserted, becomes its saved contents. Empty the scratchpads in a copied record before inserting it.
|
|
893
893
|
- **Create `editor-ui` roots at runtime from an unmarked `<template>` on every load.** An `editor-ui` root disappears from the saved file, so it cannot be authored in the file. Do not mark the blueprint inside the `<template>` as `editor-ui`, or the first save empties the template and leaves no chrome to clone on the next load. Leave the blueprint unmarked and set `clay="editor-ui"` on each clone as you insert it.
|
|
894
894
|
|
|
895
|
-
### 4.3 `
|
|
895
|
+
### 4.3 Copy policy: `no-copy` and `no-copy-unless:paid-forms`
|
|
896
|
+
|
|
897
|
+
Save and sync tokens govern the working document; copy-policy tokens on `clay` govern what travels into an explicit copy:
|
|
898
|
+
|
|
899
|
+
```html
|
|
900
|
+
<script src="/analytics.js" defer clay="no-copy"></script>
|
|
901
|
+
<header class="intro" clay="no-copy">…</header>
|
|
902
|
+
<form action="/_/message" method="post" clay="no-copy-unless:paid-forms">…</form>
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
- **Normal saves keep the original.** Ordinary saves, live sync, versions, and automatic recovery retain `no-copy` and conditional regions.
|
|
906
|
+
- **Explicit copies strip `no-copy` content, even for the owner.** Copies, duplicates, forks, HTML downloads, and ZIP exports remove `clay="no-copy"` elements (including inside `<template>` contents). User-requested recovery downloads filter visible HTML and embedded earlier HTML pages while preserving recovery records and edits.
|
|
907
|
+
- **Authored loaders vs runtime scripts.** An authored loader marked `clay="no-copy"` stays in original saves and disappears from copies. Any SDK `<script>` added at runtime uses `clay="no-save no-snapshot no-watch no-copy"` so it is not persisted. ClayJS core knows only generic copy policy, not any analytics provider.
|
|
908
|
+
- **Conditional paid forms.** `clay="no-copy-unless:paid-forms"` aliases legacy `data-paid-only="forms"` (still supported). Hosted copy/create checks the destination's existing `paid-forms` (`forms`) entitlement; offline downloads have no destination entitlement and strip optional conditional content. Reusable templates retain conditional regions for creation-time filtering, while unconditional `no-copy` regions are removed before owned template outputs. Exact case-sensitive matching fails closed, so an unknown conditional spelling cannot disguise a required paid feature.
|
|
909
|
+
- **Supporting hosts and raw file limits.** Enforced in ClayJS client downloads and recovery exports, Hyperclay hosted copy/create/download flows, and Hyperclay Local / HTML Clay copies and exports. Finder, `cp`, and browser Save Page As bypass app hooks and copy raw file bytes unchanged.
|
|
910
|
+
|
|
911
|
+
### 4.4 `persist`: form controls
|
|
896
912
|
|
|
897
913
|
Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belongs to the record. A control's typed value is a property, not an attribute, and serialization only reads attributes. Without `persist`, a typed value is lost on save and the file keeps the authored `value`.
|
|
898
914
|
|
|
@@ -913,7 +929,7 @@ Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belon
|
|
|
913
929
|
|
|
914
930
|
**After undo, redo, or a merge, ClayJS puts each control's display back in step with its attributes.** A control the user has touched shows its live property, not its attribute. A merge and the undo plugin copy `checked`, `value`, and `selected` back onto the property (undo does this for `persist` controls outside `no-undo` regions). A merge leaves the focused text field alone, while undo restores it too.
|
|
915
931
|
|
|
916
|
-
### 4.
|
|
932
|
+
### 4.5 Where to keep data that is not visible text
|
|
917
933
|
|
|
918
934
|
In order of preference:
|
|
919
935
|
|
|
@@ -935,7 +951,7 @@ In order of preference:
|
|
|
935
951
|
4. **A live JSON script (`clay="live"`)** for state that changes faster than a save: game boards, scores, cursors ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)).
|
|
936
952
|
5. **`<template>` content: never for mutable data.** It is saved, but edits there never trigger autosave.
|
|
937
953
|
|
|
938
|
-
### 4.
|
|
954
|
+
### 4.6 Element identity: `data-id` and `id`
|
|
939
955
|
|
|
940
956
|
Live sync and undo pair elements across versions by **`data-id`, then `id`**, used only when the value is unique on its side and the tag names match. Without an id, elements pair by content, which works for distinct text and fails for identical new items.
|
|
941
957
|
|
|
@@ -956,11 +972,11 @@ Live sync and undo pair elements across versions by **`data-id`, then `id`**, us
|
|
|
956
972
|
- **A `data-id` is also what presence names.** A record target `{ kind: "record", id }` carries it, so a stable id is what lets a peer's badge find the row again after a reload or a merge ([Presence](https://clayjs.com/docs/guide/live-sync/#presence)).
|
|
957
973
|
- **Use ids to find records in commands:** `` board.querySelector(`.card[data-id="${CSS.escape(id)}"]`) ``. Never find a record by position.
|
|
958
974
|
|
|
959
|
-
### 4.
|
|
975
|
+
### 4.7 Assets
|
|
960
976
|
|
|
961
977
|
ClayJS saves an image through its `src`. Upload the file and write the returned URL ([upload and quickcrop](https://clayjs.com/docs/guide/components/#upload-and-quickcrop)). Every host returns a host path such as `/_/uploads/assets-<name>/photo-ab12.png`, which keeps working on that host when the document is renamed or moved. A document opened straight from disk shows those images broken; Export as zip takes a document with its files. Where the host cannot store files, embed a `data:` URL. Data URLs work anywhere, but every save carries the bytes, so keep embedded images small.
|
|
962
978
|
|
|
963
|
-
### 4.
|
|
979
|
+
### 4.8 Ephemeral state: per-tab state on saved elements
|
|
964
980
|
|
|
965
981
|
Use `ephemeral` for per-tab state on a saved element, such as a selected card, a row being dragged, or an open section. Toggling a normal class or attribute is an edit like any other: it marks the page unsaved, starts an autosave, adds an undo step, reaches every peer, and lands in the file. Mark it `ephemeral` instead:
|
|
966
982
|
|
|
@@ -982,7 +998,7 @@ clay.ephemeral.toggle(card, "selected"); // add, remove, toggle(el, word, forc
|
|
|
982
998
|
|
|
983
999
|
Use `ephemeral` for state on one element. State that comes from the URL hash and spans many records (the open card, a filter) is still painted with a runtime stylesheet ([Per-tab state](https://clayjs.com/docs/guide/architecture/#per-tab-state)).
|
|
984
1000
|
|
|
985
|
-
### 4.
|
|
1001
|
+
### 4.9 A template is an ordinary record with a tag
|
|
986
1002
|
|
|
987
1003
|
A reusable template is a record like any other, marked with a tag. It stays editable, searchable, exported, synced and in a visitor's draft, and it needs no second store and no new format. The app reads the tag off the body it already has:
|
|
988
1004
|
|
|
@@ -992,7 +1008,7 @@ const templates = open => Entries.live().filter(a => a !== open && summary({ bod
|
|
|
992
1008
|
|
|
993
1009
|
The tag is matched outside code spans and fences, so a template entry that shows a `#template` example does not become a template itself. Using one copies its text into the page, and the new record is written by the same save as everything else.
|
|
994
1010
|
|
|
995
|
-
### 4.
|
|
1011
|
+
### 4.10 Keep a helper that reads the text beside the editor, in an `editor-ui` row
|
|
996
1012
|
|
|
997
1013
|
A helper that reacts to the text being written belongs in a row a script builds beside the editor, marked `clay="editor-ui"`, and refreshed on a debounce. Nothing it draws is saved, printed or synced, and no keystroke pays for it beyond the debounce:
|
|
998
1014
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"build:standalone": "node build.js --standalone",
|
|
73
73
|
"prepack": "node build.js --standalone",
|
|
74
74
|
"test:standalone": "node build.js --standalone && web-test-runner --config tests/browser/wtr.config.mjs",
|
|
75
|
+
"browser:install": "node scripts/chrome-for-testing/browser-runtime.cjs install",
|
|
75
76
|
"prepublishOnly": "testbox run",
|
|
76
77
|
"dev": "node tests/stub-server.mjs",
|
|
77
78
|
"build": "npm run build:bevel && npm run build:skins && node build.js",
|
|
@@ -111,7 +112,7 @@
|
|
|
111
112
|
"marked": "18.0.14",
|
|
112
113
|
"overtype": "2.6.1",
|
|
113
114
|
"parse5": "^7.3.0",
|
|
114
|
-
"playwright": "1.
|
|
115
|
+
"playwright": "1.64.1",
|
|
115
116
|
"postcss": "^8.5.28",
|
|
116
117
|
"prosemirror-commands": "1.7.2",
|
|
117
118
|
"prosemirror-dropcursor": "1.8.4",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serializeRecoveryClone, captureRecoveryPage } from "./snapshot.js";
|
|
2
2
|
import { rowOf } from "./conflict-presentation.js";
|
|
3
3
|
import { documentName, downloadHtml } from "./download.js";
|
|
4
|
+
import { stripCopyContent } from "./copy-content.js";
|
|
4
5
|
|
|
5
6
|
export { documentName, downloadHtml };
|
|
6
7
|
|
|
@@ -31,12 +32,12 @@ export function buildRecovery(records, { refused = false } = {}) {
|
|
|
31
32
|
if (apply && !seen.has(apply.id)) { seen.add(apply.id); applies.push(apply); }
|
|
32
33
|
}
|
|
33
34
|
applies.sort((a, b) => (a.ticket || 0) - (b.ticket || 0));
|
|
34
|
-
const pageOf = (a) => serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" });
|
|
35
|
+
const pageOf = (a) => stripCopyContent(serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" }));
|
|
35
36
|
|
|
36
37
|
let html;
|
|
37
38
|
let older;
|
|
38
39
|
if (refused || !applies.length) {
|
|
39
|
-
html = captureRecoveryPage();
|
|
40
|
+
html = stripCopyContent(captureRecoveryPage());
|
|
40
41
|
older = applies;
|
|
41
42
|
} else {
|
|
42
43
|
html = pageOf(applies[applies.length - 1]);
|
|
@@ -66,9 +67,9 @@ export function buildRecovery(records, { refused = false } = {}) {
|
|
|
66
67
|
// The review panel's own copy: the page a reload kept, when there is one, with every
|
|
67
68
|
// stored row it drew listed beside it as JSON. The rows are already plain, so nothing
|
|
68
69
|
// here reads the ledger the tab no longer has.
|
|
69
|
-
export function downloadStored({ html = null, edits = [] } = {}) {
|
|
70
|
+
export async function downloadStored({ html = null, edits = [] } = {}) {
|
|
70
71
|
try {
|
|
71
|
-
const page = typeof html === "string" ? html : captureRecoveryPage();
|
|
72
|
+
const page = stripCopyContent(typeof html === "string" ? html : captureRecoveryPage());
|
|
72
73
|
const data = {
|
|
73
74
|
format: "clay-lost-edits",
|
|
74
75
|
version: 1,
|
|
@@ -78,17 +79,17 @@ export function downloadStored({ html = null, edits = [] } = {}) {
|
|
|
78
79
|
edits: Array.isArray(edits) ? edits : [],
|
|
79
80
|
earlierPages: [],
|
|
80
81
|
};
|
|
81
|
-
return downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
|
|
82
|
+
return await downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
|
|
82
83
|
} catch (err) {
|
|
83
84
|
console.error("clayjs: could not build the download", err);
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export function downloadRecovery(records, { refused = false } = {}) {
|
|
89
|
+
export async function downloadRecovery(records, { refused = false } = {}) {
|
|
89
90
|
try {
|
|
90
91
|
const { html, name } = buildRecovery(records, { refused });
|
|
91
|
-
return downloadHtml({ html, name });
|
|
92
|
+
return await downloadHtml({ html, name });
|
|
92
93
|
} catch (err) {
|
|
93
94
|
console.error("clayjs: could not build the download", err);
|
|
94
95
|
return false;
|
|
@@ -707,12 +707,12 @@ async function onRevert() {
|
|
|
707
707
|
let downloadedTimer = null;
|
|
708
708
|
|
|
709
709
|
// Download my copy. Nothing here acknowledges or releases a record.
|
|
710
|
-
function onDownload() {
|
|
710
|
+
async function onDownload() {
|
|
711
711
|
const l = ledger();
|
|
712
712
|
// A refusal ends in a reload that takes every record with it, so its copy carries
|
|
713
713
|
// all of them; a replaced notice covers the rows it drew.
|
|
714
714
|
const records = refused ? (l?.list() ?? []) : renderedIds.map((id) => l?.get(id)).filter(Boolean);
|
|
715
|
-
const ok = downloadRecovery(records, { refused: !!refused });
|
|
715
|
+
const ok = await downloadRecovery(records, { refused: !!refused });
|
|
716
716
|
downloadFailed = !ok;
|
|
717
717
|
downloaded = ok;
|
|
718
718
|
clearTimeout(downloadedTimer);
|
|
@@ -801,14 +801,14 @@ async function onCopyStored(entry) {
|
|
|
801
801
|
|
|
802
802
|
// Download my copy, from the stored root: the page as this tab had it, with the
|
|
803
803
|
// losing text in it. Nothing here acknowledges or releases a record.
|
|
804
|
-
function onDownloadRecovered() {
|
|
804
|
+
async function onDownloadRecovered() {
|
|
805
805
|
const record = recovered.find((item) => item.root);
|
|
806
806
|
const edits = [];
|
|
807
807
|
for (const item of recovered) {
|
|
808
808
|
for (const entry of item.conflicts) edits.push({ id: entry.id, kind: entry.kind, name: storedName(entry), yours: entry.yours, now: entry.now, sentence: entry.sentence });
|
|
809
809
|
}
|
|
810
810
|
for (const entry of recoveredUndo) edits.push({ id: entry.id, kind: entry.kind, name: entry.name, yours: entry.yours, now: entry.now, sentence: entry.sentence });
|
|
811
|
-
const ok = downloadStored({ html: record?.root ?? null, edits });
|
|
811
|
+
const ok = await downloadStored({ html: record?.root ?? null, edits });
|
|
812
812
|
downloadFailed = !ok;
|
|
813
813
|
downloaded = ok;
|
|
814
814
|
clearTimeout(downloadedTimer);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copy-only content: marked regions that stay in the file, in normal saves, in snapshots
|
|
2
|
+
// and in recovery storage, and are removed only from the bytes of a copy someone asks for.
|
|
3
|
+
// The removal is a splice on the original string using parse5's source offsets, so every
|
|
4
|
+
// byte outside a removed range survives, templates and implicit end tags included.
|
|
5
|
+
|
|
6
|
+
import { parse } from "../vendor/parse5.vendor.js";
|
|
7
|
+
|
|
8
|
+
const CLAY = "clay";
|
|
9
|
+
const PAID_ONLY = "data-paid-only";
|
|
10
|
+
const NO_COPY = "no-copy";
|
|
11
|
+
const CONDITIONAL_FORMS = "no-copy-unless:paid-forms";
|
|
12
|
+
const CLAY_TOKENS = /[\t\n\f\r ]+/;
|
|
13
|
+
|
|
14
|
+
function attributeOf(node, name) {
|
|
15
|
+
return node.attrs?.find((attr) => attr.name === name)?.value ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function childrenOf(node) {
|
|
19
|
+
return node.content ? [...(node.childNodes || []), node.content] : (node.childNodes || []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function eachElement(node, visit) {
|
|
23
|
+
visit(node);
|
|
24
|
+
for (const child of childrenOf(node)) eachElement(child, visit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clayTokensOf(node) {
|
|
28
|
+
return (attributeOf(node, CLAY) ?? "").split(CLAY_TOKENS);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The marker is compared to a feature key exactly: no trim, no lowercase, so a typo, an
|
|
32
|
+
// empty value or `FORMS` strips nothing and the content stays, which is the direction
|
|
33
|
+
// that fails closed. `clay="no-copy-unless:paid-forms"` is the approved spelling of the
|
|
34
|
+
// same condition.
|
|
35
|
+
function paidMarkersOf(node) {
|
|
36
|
+
const markers = [];
|
|
37
|
+
const value = attributeOf(node, PAID_ONLY);
|
|
38
|
+
if (value !== null) markers.push(value);
|
|
39
|
+
if (clayTokensOf(node).includes(CONDITIONAL_FORMS)) markers.push("forms");
|
|
40
|
+
return [...new Set(markers)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lastDescendantEnd(node) {
|
|
44
|
+
let end = null;
|
|
45
|
+
eachElement(node, (entry) => {
|
|
46
|
+
const entryEnd = entry.sourceCodeLocation?.endOffset;
|
|
47
|
+
if (typeof entryEnd === "number" && entryEnd >= 0 && (end === null || entryEnd > end)) end = entryEnd;
|
|
48
|
+
});
|
|
49
|
+
return end;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function endOffsetFor(node) {
|
|
53
|
+
const location = node.sourceCodeLocation;
|
|
54
|
+
if (typeof location?.endOffset === "number" && location.endOffset >= 0) return location.endOffset;
|
|
55
|
+
const fallback = lastDescendantEnd(node) ?? location?.startTag?.endOffset ?? null;
|
|
56
|
+
if (fallback === null) return null;
|
|
57
|
+
console.warn(`clayjs: <${node.tagName}> has no end offset; removing through ${fallback}`);
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markedRanges(html, features) {
|
|
62
|
+
const ranges = [];
|
|
63
|
+
eachElement(parse(html, { sourceCodeLocationInfo: true }), (node) => {
|
|
64
|
+
const markers = paidMarkersOf(node);
|
|
65
|
+
const unconditional = clayTokensOf(node).includes(NO_COPY);
|
|
66
|
+
const conditional = features === null ? markers.length > 0 : markers.some((marker) => features.includes(marker));
|
|
67
|
+
if (!unconditional && !conditional) return;
|
|
68
|
+
const start = node.sourceCodeLocation?.startOffset;
|
|
69
|
+
const end = typeof start === "number" ? endOffsetFor(node) : null;
|
|
70
|
+
if (typeof start === "number" && start >= 0 && end !== null && end > start) ranges.push({ start, end });
|
|
71
|
+
});
|
|
72
|
+
return ranges.filter((range, index) => !ranges.some((other, otherIndex) =>
|
|
73
|
+
otherIndex !== index && other.start <= range.start && range.end <= other.end));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function leadingRunStart(html, start) {
|
|
77
|
+
const newline = html.lastIndexOf("\n", start - 1);
|
|
78
|
+
if (newline < 0 || /\S/.test(html.slice(newline + 1, start))) return start;
|
|
79
|
+
return html[newline - 1] === "\r" ? newline - 1 : newline;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function spliceRanges(html, ranges) {
|
|
83
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
84
|
+
let stripped = html;
|
|
85
|
+
let limit = html.length;
|
|
86
|
+
for (let index = sorted.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const { start, end } = sorted[index];
|
|
88
|
+
if (end > limit) continue;
|
|
89
|
+
const from = leadingRunStart(html, start);
|
|
90
|
+
if (from >= end) continue;
|
|
91
|
+
stripped = stripped.slice(0, from) + stripped.slice(end);
|
|
92
|
+
limit = from;
|
|
93
|
+
}
|
|
94
|
+
return stripped;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `features` decides the conditional regions: null (the default, and the offline
|
|
98
|
+
// destination) strips every one of them, while a list strips only the missing features
|
|
99
|
+
// it names. Unconditional `no-copy` content always goes.
|
|
100
|
+
export function stripCopyContent(html, features = null) {
|
|
101
|
+
if (typeof html !== "string" || !/clay|data-paid-only/i.test(html)) return html;
|
|
102
|
+
return spliceRanges(html, markedRanges(html, features));
|
|
103
|
+
}
|
package/src/core/download.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Saving a page as a file the browser downloads. Kept apart from the conflict copy
|
|
2
2
|
// builders so an editor that only offers "download this page" loads none of them.
|
|
3
3
|
|
|
4
|
+
const HTML_TYPES = ["text/html", "application/xhtml+xml"];
|
|
5
|
+
|
|
6
|
+
function isHtml(type) {
|
|
7
|
+
if (typeof type !== "string") return false;
|
|
8
|
+
return HTML_TYPES.includes(type.split(";")[0].trim().toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export function documentName(pathname = location.pathname) {
|
|
5
12
|
const last = pathname.split("/").filter(Boolean).pop();
|
|
6
13
|
if (!last) return pathname.endsWith("/") || pathname === "" ? "index" : "page";
|
|
@@ -9,10 +16,15 @@ export function documentName(pathname = location.pathname) {
|
|
|
9
16
|
return name.replace(/\.(html?|htmlclay)$/i, "") || "page";
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
export function downloadHtml({ html, name, type = "text/html" }) {
|
|
19
|
+
export async function downloadHtml({ html, name, type = "text/html" }) {
|
|
13
20
|
let url = null;
|
|
14
21
|
try {
|
|
15
|
-
|
|
22
|
+
let contents = html;
|
|
23
|
+
if (isHtml(type) && typeof html === "string" && /clay|data-paid-only/i.test(html)) {
|
|
24
|
+
const { stripCopyContent } = await import("./copy-content.js");
|
|
25
|
+
contents = stripCopyContent(html);
|
|
26
|
+
}
|
|
27
|
+
url = URL.createObjectURL(new Blob([contents], { type }));
|
|
16
28
|
const a = document.createElement("a");
|
|
17
29
|
a.setAttribute("clay", "no-save no-watch no-snapshot");
|
|
18
30
|
a.href = url;
|
|
@@ -57,8 +57,8 @@ function place() {
|
|
|
57
57
|
// The safe copy the conflict notice offers, for the same reason: a file the person can
|
|
58
58
|
// open has to exist outside a host that will not store these bytes. It already reports
|
|
59
59
|
// its own failure.
|
|
60
|
-
function download() {
|
|
61
|
-
downloadStored({});
|
|
60
|
+
async function download() {
|
|
61
|
+
await downloadStored({});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function build() {
|
package/src/core/upgrade-run.js
CHANGED
|
@@ -348,7 +348,11 @@ export async function pageBytes(clay, { loc = location, fetchFn = (url, init) =>
|
|
|
348
348
|
export async function downloadCopy({ clay, doc = document, loc = location, fetchFn } = {}) {
|
|
349
349
|
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
|
|
350
350
|
try {
|
|
351
|
-
|
|
351
|
+
let bytes = await pageBytes(clay, { loc, fetchFn });
|
|
352
|
+
if (typeof bytes === "string" && /clay|data-paid-only/i.test(bytes)) {
|
|
353
|
+
const { stripCopyContent } = await import("./copy-content.js");
|
|
354
|
+
bytes = stripCopyContent(bytes);
|
|
355
|
+
}
|
|
352
356
|
const url = URL.createObjectURL(new Blob([bytes], { type: "text/html" }));
|
|
353
357
|
const a = doc.createElement("a");
|
|
354
358
|
a.setAttribute("clay", "editor-ui");
|
package/src/lib/region-policy.js
CHANGED
|
@@ -54,7 +54,7 @@ export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-u
|
|
|
54
54
|
// Every canonical token spellable inside the space-separated `clay` attribute.
|
|
55
55
|
// Exported: this list used to exist as a private CLAY_TOKENS that nothing read,
|
|
56
56
|
// while callers hardcoded their own copies.
|
|
57
|
-
export const TOKENS = [...POLICY_TOKENS];
|
|
57
|
+
export const TOKENS = [...POLICY_TOKENS, 'no-copy', 'no-copy-unless:paid-forms'];
|
|
58
58
|
|
|
59
59
|
// True when a region marker is present, whether spelled as a `clay` token
|
|
60
60
|
// (whitespace-token semantics, matching [clay~=token]) or a legacy bare attribute.
|
|
@@ -112,12 +112,12 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function decide(s) {
|
|
115
|
-
listen(s, s.panel, "click", (event) => {
|
|
115
|
+
listen(s, s.panel, "click", async (event) => {
|
|
116
116
|
const b = event.target.closest?.("[data-act]");
|
|
117
117
|
if (!b) return;
|
|
118
118
|
event.preventDefault();
|
|
119
119
|
if (b.dataset.act === "close") close();
|
|
120
|
-
else if (b.dataset.act === "download") save(s);
|
|
120
|
+
else if (b.dataset.act === "download") await save(s);
|
|
121
121
|
else if (b.dataset.act === "retry") run().catch((error) => console.error("clayjs: the reading copy could not be made", error));
|
|
122
122
|
});
|
|
123
123
|
listen(s, doc, "keydown", (event) => {
|
|
@@ -130,9 +130,9 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
|
|
|
130
130
|
|
|
131
131
|
// The one place a reading copy becomes a file. The download is handed the bytes that
|
|
132
132
|
// were shown, never a second render, so what a person saw is what they get.
|
|
133
|
-
function save(s) {
|
|
133
|
+
async function save(s) {
|
|
134
134
|
if (!s.ready || disposed) return { ok: false, code: "not-editing" };
|
|
135
|
-
const saved = download({ html: s.ready.html, name: s.ready.name });
|
|
135
|
+
const saved = await download({ html: s.ready.html, name: s.ready.name });
|
|
136
136
|
close();
|
|
137
137
|
return { ok: saved === true, downloaded: saved === true };
|
|
138
138
|
}
|
|
@@ -83,10 +83,10 @@ export function createFeature({ view = null, download = downloadHtml } = {}) {
|
|
|
83
83
|
s.panel.replaceChildren(...parts);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function save() {
|
|
86
|
+
async function save() {
|
|
87
87
|
const s = session;
|
|
88
88
|
if (!s) return { ok: false, code: "no-export" };
|
|
89
|
-
if (download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
|
|
89
|
+
if (await download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
|
|
90
90
|
close();
|
|
91
91
|
view.focus?.();
|
|
92
92
|
return { ok: true, downloaded: true };
|
package/src/page/recovery-ui.js
CHANGED
|
@@ -228,12 +228,12 @@ export function createRecoveryReview({
|
|
|
228
228
|
button.setAttribute("aria-expanded", "false");
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
const download = () => {
|
|
231
|
+
const download = async () => {
|
|
232
232
|
const edits = [];
|
|
233
233
|
for (const row of rows) edits.push({ id: row.id, kind: "recovered-page", name: row.message, yours: row.text, now: "", sentence: row.message });
|
|
234
234
|
const composing = "Text typed while composing never reached the page.";
|
|
235
235
|
for (const row of journaled) edits.push({ id: row.id, kind: "composing", name: "Typed text", yours: row.text, now: "", sentence: composing });
|
|
236
|
-
if (!downloadStored({ html: null, edits })) failure = "That download could not be made.";
|
|
236
|
+
if (!(await downloadStored({ html: null, edits }))) failure = "That download could not be made.";
|
|
237
237
|
draw();
|
|
238
238
|
};
|
|
239
239
|
|
package/src/page/save-states.js
CHANGED
|
@@ -67,14 +67,14 @@ function withoutPageTag(html) {
|
|
|
67
67
|
// One complete, openable file: the capture the save would have sent, with the
|
|
68
68
|
// page projection already applied by the live store's snapshot listener, and
|
|
69
69
|
// without the page editor's merge state, which no saved file or download carries.
|
|
70
|
-
function downloadPageCopy() {
|
|
70
|
+
async function downloadPageCopy() {
|
|
71
71
|
const { forSave } = captureForSaveAndComparison({ emitForSync: false });
|
|
72
|
-
return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
72
|
+
return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// A copy of the page captured earlier (captureForSaveAndComparison().forSave), as a download.
|
|
76
|
-
function downloadSnapshot(forSave) {
|
|
77
|
-
return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
76
|
+
async function downloadSnapshot(forSave) {
|
|
77
|
+
return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// The article's own plain text, never the editor's chrome: the capture carries
|
|
@@ -146,7 +146,7 @@ export function attachActions({ addItem, note, currentMode }) {
|
|
|
146
146
|
if (typeof html === "string" && /^https?:$/.test(location.protocol)) {
|
|
147
147
|
html = html.replace(/(^|[^\w./:-])\/_\/uploads\//g, `$1${location.origin}/_/uploads/`);
|
|
148
148
|
}
|
|
149
|
-
if (typeof html !== "string" || !downloadHtml({ html, name: `${documentName()}.html` })) {
|
|
149
|
+
if (typeof html !== "string" || !(await downloadHtml({ html, name: `${documentName()}.html` }))) {
|
|
150
150
|
note("Couldn't build the download. Try again.");
|
|
151
151
|
}
|
|
152
152
|
},
|
package/src/plugins/page.js
CHANGED
|
@@ -233,17 +233,17 @@ function stopEditing(inst) {
|
|
|
233
233
|
|
|
234
234
|
// The tab's own page, when the store no longer holds it (another page replaced it, or the
|
|
235
235
|
// room refused it): the saved document with the article rendered from the tab's document.
|
|
236
|
-
function downloadTabCopy(inst) {
|
|
237
|
-
if (!inst.tabValue) return downloadPageCopy();
|
|
236
|
+
async function downloadTabCopy(inst) {
|
|
237
|
+
if (!inst.tabValue) return await downloadPageCopy();
|
|
238
238
|
const { forSave } = captureForSaveAndComparison({ emitForSync: false });
|
|
239
239
|
const doc = new DOMParser().parseFromString(forSave, "text/html");
|
|
240
240
|
const articles = doc.querySelectorAll(ARTICLE_SEL);
|
|
241
|
-
if (articles.length !== 1) return downloadPageCopy();
|
|
241
|
+
if (articles.length !== 1) return await downloadPageCopy();
|
|
242
242
|
const rendered = pageCode.renderFromTags([JSON.stringify(inst.tabValue)]);
|
|
243
243
|
articles[0].innerHTML = rendered.html;
|
|
244
244
|
if (rendered.rev == null) articles[0].removeAttribute("data-rev");
|
|
245
245
|
else articles[0].setAttribute("data-rev", rendered.rev);
|
|
246
|
-
return downloadSnapshot(`<!DOCTYPE html>\n${doc.documentElement.outerHTML}`);
|
|
246
|
+
return await downloadSnapshot(`<!DOCTYPE html>\n${doc.documentElement.outerHTML}`);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function stopWithNotice(inst, text, { reload = true } = {}) {
|
|
@@ -103,22 +103,19 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function downloadCurrent() {
|
|
106
|
+
async function downloadCurrent() {
|
|
107
107
|
const getHTML = window.clay && window.clay.getHTML;
|
|
108
108
|
if (typeof getHTML !== "function" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
|
|
109
109
|
const segment = location.pathname.split("/").filter(Boolean).pop() || "index.html";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
a.remove();
|
|
117
|
-
setTimeout(() => URL.revokeObjectURL?.(objectUrl), 1000);
|
|
118
|
-
return true;
|
|
110
|
+
try {
|
|
111
|
+
const { downloadHtml } = await import("../../core/download.js");
|
|
112
|
+
return await downloadHtml({ html: getHTML(), name: segment.replace(/\.html$/, ".htmlclay") });
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
root.addEventListener("click", (e) => {
|
|
118
|
+
root.addEventListener("click", async (e) => {
|
|
122
119
|
const t = e.target.closest("[data-hcp]");
|
|
123
120
|
if (t) {
|
|
124
121
|
const act = t.getAttribute("data-hcp");
|
|
@@ -133,7 +130,10 @@ export async function mountPrompt({ memory, appName, link }) {
|
|
|
133
130
|
if (e.target.closest("[data-hcp-link]")) {
|
|
134
131
|
if (root.getAttribute("data-hcp-reason") === "error") {
|
|
135
132
|
e.preventDefault();
|
|
136
|
-
downloadCurrent()
|
|
133
|
+
if (!(await downloadCurrent())) {
|
|
134
|
+
root.querySelector(".hcp-body [data-hcp-for='error']").textContent = "The download could not be made. Keep this page open and try Download free again.";
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
137
|
}
|
|
138
138
|
mem.downloaded = true; mem.shown = true; memory.save();
|
|
139
139
|
}
|
package/src/plugins/wire.js
CHANGED
|
@@ -788,7 +788,9 @@ function send(payload, opts = {}) {
|
|
|
788
788
|
// Accepted by the router and taken by nobody. Reporting this as an error
|
|
789
789
|
// rather than a pending request is the difference between a UI that says
|
|
790
790
|
// "start an agent" and one that spins forever.
|
|
791
|
-
finish(rec, "error",
|
|
791
|
+
finish(rec, "error", rec.helper
|
|
792
|
+
? `The local program "${rec.helper}" is not connected to this file.`
|
|
793
|
+
: "No program is connected to this file.");
|
|
792
794
|
}
|
|
793
795
|
};
|
|
794
796
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a}from"../c-OYRZ7XG2.js";import"../c-Q26MME6N.js";import"../c-ZWIOJGMT.js";import"../c-IJUK2GPW.js";import"../c-7MG5RU63.js";import"../c-XQXZAO7N.js";import"../c-S7LK4KUE.js";import"../c-VG5XIN4T.js";import"../c-2YYDMLNZ.js";import"../c-HA6INT6J.js";import"../c-DCFSEJZG.js";import"../c-WRBKEJD4.js";import"../c-VHNSBD2M.js";export{a as default};
|
package/dist/m/c-DCFSEJZG.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as m,c as g,d as c,e as f,f as v}from"./c-WRBKEJD4.js";var w=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],N=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],u=w.join(","),_=new RegExp("^(?:"+N.join("|")+")(?:-|$)");function X(e){if(!(!e||!e.querySelectorAll))try{for(let a of e.querySelectorAll(u))a.remove();let t=a=>{let r=a.attributes;if(!r)return;let n=null;for(let s=0;s<r.length;s++){let o=r[s].name;_.test(o.toLowerCase())&&(n||(n=[])).push(o)}if(n)for(let s of n)a.removeAttribute(s)};t(e);for(let a of e.querySelectorAll("*"))t(a)}catch(t){typeof console<"u"&&console.warn("[clay] stripExtensionNoise skipped:",t)}}var l={FULL:"full",FROZEN:"frozen",NONE:"none"},k=["no-save","no-trigger-autosave","no-dirty","no-undo","no-watch","no-data","freeze","editor-ui","live"],A=[...m],i=g;function z(e,t){let a=new Set((e.getAttribute("clay")||"").trim().split(/\s+/).filter(Boolean));a.has(t)||(a.add(t),e.setAttribute("clay",Array.from(a).join(" ")))}var T=`${f("save")}, [save-remove]`,h='[clay~="freeze"], [freeze], [save-freeze]',y='[clay~="no-save"], [clay~="no-trigger-autosave"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-trigger-autosave], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]',x='[clay~="no-trigger-autosave"], [no-trigger-autosave]',j='[clay~="live"]',R='[clay~="no-dirty"], [no-dirty], [save-ignore]',S='[clay~="no-save"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]',p=`${f("snapshot")}, [snapshot-remove]`,O=f("data");function I(e){let t=E(e);return!!(t&&t.closest&&t.closest(p))}function b(e){return v(e,"data")}var d={full:0,frozen:1,none:2},C=["full","frozen","none"];function E(e){return e&&e.nodeType!==1?e.parentElement:e}function L(e){let t=E(e);if(t&&t.closest&&t.closest(u))return{watched:!1,autosaveTriggered:!1,dirtyTracked:!1,undoable:!1,persist:l.FULL,extension:!0};let a=!0,r=!0,n=!1,s=!1,o=0;for(;t&&t.nodeType===1;)t.hasAttribute&&(c(t,"no-watch")&&(a=!1),i(t,"no-trigger-autosave")&&(n=!0),i(t,"no-dirty")&&(n=!0,s=!0),i(t,"live")&&(n=!0,s=!0,r=!1),c(t,"no-undo")&&(r=!1),c(t,"no-save")&&(o=Math.max(o,d.none)),i(t,"freeze")&&(o=Math.max(o,d.frozen)),i(t,"mutations-ignore")&&(a=!1),i(t,"save-remove")&&(o=Math.max(o,d.none),r=!1),i(t,"save-ignore")&&(n=!0,s=!0,r=!1),i(t,"save-freeze")&&(o=Math.max(o,d.frozen),r=!1)),t=t.parentElement;return a||(n=!0,r=!1,s=!0),o>0&&(n=!0,s=!0),{watched:a,autosaveTriggered:!n,dirtyTracked:!s,undoable:r,persist:C[o],extension:!1}}function P(e){let t=E(e);if(t&&t.closest&&t.closest(u))return!0;for(;t&&t.nodeType===1;){if(t.hasAttribute&&(c(t,"no-watch")||i(t,"mutations-ignore")))return!0;t=t.parentElement}return!1}function F(e,t){return{watched:e.watched&&t.watched,autosaveTriggered:e.autosaveTriggered&&t.autosaveTriggered,dirtyTracked:e.dirtyTracked&&t.dirtyTracked,undoable:e.undoable&&t.undoable,persist:d[e.persist]>=d[t.persist]?e.persist:t.persist,extension:e.extension||t.extension}}var D={"no-watch":e=>!e.watched,"mutations-ignore":e=>!e.watched,"no-save":e=>e.persist===l.NONE,"save-remove":e=>e.persist===l.NONE,freeze:e=>e.persist===l.FROZEN,"save-freeze":e=>e.persist===l.FROZEN,"no-trigger-autosave":e=>!e.autosaveTriggered,"no-dirty":e=>!e.dirtyTracked,"save-ignore":e=>!e.dirtyTracked,"no-undo":e=>!e.undoable};function M(e,t,a){if(e.extension)return!0;if(a&&a.length)return a.some(r=>D[r]?.(e)||!1);switch(t){case"observed":return!e.watched;case"autosave":return!e.autosaveTriggered;case"dirty":return!e.dirtyTracked;case"undo":return!e.undoable;default:return!e.watched||!e.autosaveTriggered||e.persist!==l.FULL}}var K={resolveRegionPolicy:L,isInert:P,isSnapshotRemoved:I,isDataExcluded:b,skipForPolicy:M,strictestPolicy:F,addRegionToken:z,PERSIST:l,TOKENS:A,REGION_ATTRS:k,STRIP_FROM_SAVE:T,STRIP_FROM_COMPARISON:y,STRIP_FROM_DIRTY_CHECK:S,NO_TRIGGER_AUTOSAVE_SELECTOR:x,NO_DIRTY_SELECTOR:R,SNAPSHOT_REMOVE_SELECTOR:p,NO_DATA_SELECTOR:O,FREEZE_SELECTOR:h,selectors:{stripFromSave:T,stripFromComparison:y,stripFromDirtyCheck:S,noTriggerAutosave:x,noDirty:R,snapshotRemove:p,noData:O,freeze:h}},q=K;export{u as a,_ as b,X as c,l as d,k as e,A as f,z as g,T as h,h as i,y as j,x as k,j as l,R as m,S as n,p as o,O as p,I as q,b as r,L as s,P as t,F as u,M as v,K as w,q as x};
|
package/dist/m/c-EXEDYU4H.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var o={table:{types:["table"],views:["table","table_cell"],module:"page/features/table.js",load:()=>import("./c-JK3PHE7J.js")},board:{types:["board"],module:"page/features/board.js",load:()=>import("./c-ZRUQ5FSU.js")},chart:{types:["chart"],views:["chart"],module:"page/features/chart.js",load:()=>import("./c-L6FLIUXP.js")},checklist:{types:[],views:[],match:e=>e.type.name==="list_item"&&e.attrs.kind==="check",module:"page/features/checklist.js",load:()=>import("./c-HXPHZN7Q.js")},code:{types:["code_block"],module:"page/features/code.js",load:()=>import("./c-RRZJWFLE.js")},toc:{types:["toc"],module:"page/features/toc.js",load:()=>import("./c-GWNC3UGL.js")},titleIcon:{types:[],views:["heading"],match:e=>e.attrs.pageIcon!=null||e.attrs.pageCoverSrc!=null,module:"page/features/title-icon.js",load:()=>import("./c-O4JMKN6U.js")},media:{types:["image","file"],module:"page/features/media.js",load:()=>import("./c-DHUCEMXE.js")},tool:{types:["tool"],module:"page/features/tool.js",boot:["page/tools.js","page/tool-data.js"],load:()=>import("./c-WKNRJX3C.js")}},r=e=>Object.keys(o).find(t=>o[t].types.includes(e))??null,s=e=>{for(let t of Object.keys(o)){let a=o[t];if(a.match?a.match(e):a.types.includes(e.type.name))return t}return null},l={mentions:{module:"page/features/mentions.js",load:()=>import("./c-MIKZEG5I.js")},dates:{module:"page/features/dates.js",load:()=>import("./c-FOOE4GML.js")},emoji:{module:"page/features/emoji.js",load:()=>import("./c-UXFUJEZO.js")},titleIcon:{module:"page/features/title-icon.js",load:()=>import("./c-O4JMKN6U.js")},titleCover:{module:"page/features/title-cover.js",load:()=>import("./c-QINHDETP.js")},exportHtml:{module:"page/features/export-html.js",load:()=>import("./c-DKLS74XB.js")},importMarkdown:{module:"page/features/import-markdown.js",load:()=>import("./c-AXGY2WTB.js")},exportMarkdown:{module:"page/features/export-markdown.js",load:()=>import("./c-ERQSNR6K.js")}};export{o as a,r as b,s as c,l as d};
|
package/dist/m/c-GRXHHFPO.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a,b,c}from"./c-UY4QX42B.js";import"./c-L7JL5TL3.js";import"./c-BJ7YQ5QZ.js";import"./c-4JZDZFF6.js";import"./c-VAK7SG37.js";import"./c-MHI3GYKF.js";import"./c-NUHP2XDB.js";import"./c-DCFSEJZG.js";import"./c-WRBKEJD4.js";import"./c-VHNSBD2M.js";export{c as default,b as dismissPersistent,a as toastPersistent};
|
package/dist/m/c-HT2DKBV2.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as d}from"./c-WNTYKCDG.js";import"./c-PZ4XEPUP.js";import{g as w,l as v,n as x,q as s}from"./c-BJ7YQ5QZ.js";import{a as p}from"./c-4JZDZFF6.js";import{e as f}from"./c-VAK7SG37.js";import{b as m}from"./c-MHI3GYKF.js";import"./c-MCOOHCSG.js";import{b as c}from"./c-NUHP2XDB.js";import{f as l}from"./c-3O7OBDCM.js";import"./c-BPF2NSPV.js";import"./c-P7KNDG34.js";import"./c-CYWLPPUS.js";import"./c-N7JPUPGC.js";import"./c-7MG5RU63.js";import"./c-XQXZAO7N.js";import"./c-S7LK4KUE.js";import"./c-VG5XIN4T.js";import"./c-2YYDMLNZ.js";import"./c-HA6INT6J.js";import"./c-DCFSEJZG.js";import"./c-WRBKEJD4.js";import"./c-VHNSBD2M.js";var $="Download a copy, then reduce its size; saving still works.",b="Download a copy to keep your work.",t=null,a=null,i=null,r=null;function h(e){return typeof e!="number"||!Number.isFinite(e)||e<0?"an unknown size":e>=1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:e>=1024?`${Math.max(1,Math.round(e/1024))} KB`:`${e} bytes`}function z({bytes:e,maxBytes:n,state:S}){let u=`At the last save attempt: ${h(e)} of ${h(n)}.`;return S==="over"?`This host can't save a file this size. ${u} ${b}`:`This file is getting large. ${u} ${$}`}function o(){if(!t)return;let e=window.visualViewport,n=e?Math.max(0,window.innerHeight-e.height-e.offsetTop):0;c(t,"bottom",`calc(${16+n}px + env(safe-area-inset-bottom,0px))`)}function y(){d({})}function A(){t=v("div",["position:fixed","left:50%","transform:translateX(-50%)","z-index:2147483000","display:flex","align-items:center","gap:10px 12px","width:max-content","max-width:calc(100vw - 24px)","flex-wrap:wrap","padding:8px 8px 8px 14px","text-align:left",`color-scheme:${p()}`]),t.setAttribute("clay","editor-ui"),t.setAttribute("data-clay-save-size",""),t.setAttribute("role","status"),a=x("span",[],""),i=w("Download my copy",{small:!0,onClick:y}),s(i,!1,"inline-flex"),t.append(a,i),document.body.appendChild(t),o(),window.visualViewport?.addEventListener("resize",o),window.visualViewport?.addEventListener("scroll",o)}function M(e){t||A(),a.textContent=z(e),s(i,e.state==="over","inline-flex"),o(),m(t,{background:f.surface})}function E(){t&&(window.visualViewport?.removeEventListener("resize",o),window.visualViewport?.removeEventListener("scroll",o),t.remove(),t=null,a=null,i=null)}function g(e){let n=e?.detail;n&&(n.state==="near"||n.state==="over"?M(n):E())}function k(){l||L()}function N(e){l&&(r||=new AbortController,document.addEventListener("clay:save-size",g,{signal:r.signal}),document.addEventListener("clay:modechange",k,{signal:r.signal}),e?.type==="clay:save-size"&&g(e))}function L(){r?.abort(),r=null,E()}export{N as start,L as stop};
|
package/dist/m/c-MCOOHCSG.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function r(t=location.pathname){let o=t.split("/").filter(Boolean).pop();if(!o)return t.endsWith("/")||t===""?"index":"page";let l=o;try{l=decodeURIComponent(o)}catch{}return l.replace(/\.(html?|htmlclay)$/i,"")||"page"}function c({html:t,name:o,type:l="text/html"}){let n=null;try{n=URL.createObjectURL(new Blob([t],{type:l}));let e=document.createElement("a");return e.setAttribute("clay","no-save no-watch no-snapshot"),e.href=n,e.download=o,e.style.setProperty("display","none","important"),document.body.appendChild(e),e.click(),e.remove(),setTimeout(()=>URL.revokeObjectURL(n),4e4),!0}catch(e){return console.error("clayjs: could not build the download",e),n&&URL.revokeObjectURL(n),!1}}export{r as a,c as b};
|
package/dist/m/c-MMXKAX2W.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as b,b as c,c as d,d as e,e as f}from"./c-FCNAO2MI.js";import{g as a}from"./c-4FJTBE6I.js";import"./c-EXKHPOVC.js";import"./c-L5VFATT6.js";import"./c-CQQDL6O4.js";import"./c-ZGDYHKG3.js";import"./c-W4JCVLAB.js";import"./c-7K4SCQB7.js";import"./c-WMRJKQ2V.js";import"./c-AIWN4QN4.js";import"./c-QIXT3Q7C.js";import"./c-WCXLG3MV.js";import"./c-57XDY6HC.js";import"./c-I2SCCUEW.js";import"./c-VHNSBD2M.js";export{e as ChartView,d as chartFromSlash,a as chartTables,b as insertChartAfterTable,f as refreshCharts,c as tableLabel};
|