@panphora/clayjs 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +72474 -62090
- package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
- package/dist/m/c-2GRUM77Q.js +30 -0
- package/dist/m/c-2KIEGNC4.js +1 -0
- package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
- package/dist/m/c-2ZZBC45O.js +1 -0
- package/dist/m/c-372N6H3B.js +1 -0
- package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
- package/dist/m/c-3LDGIBMN.js +1 -0
- package/dist/m/c-3O7OBDCM.js +1 -0
- package/dist/m/c-4ASQPRQL.js +1 -0
- package/dist/m/c-4TXNLX4K.js +6 -0
- package/dist/m/c-4YU3BR4D.js +6 -0
- package/dist/m/c-5734MF2P.js +1 -0
- package/dist/m/c-57XDY6HC.js +1 -0
- package/dist/m/{c-KAU6E43D.js → c-5B4YBZXC.js} +1 -1
- package/dist/m/c-5ONTYIUI.js +1 -0
- package/dist/m/c-5OOVFCJM.js +1 -0
- package/dist/m/{c-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
- package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
- package/dist/m/{c-7OLLJPWP.js → c-6DB2OLRJ.js} +1 -1
- package/dist/m/c-6DNRCPNM.js +1 -0
- package/dist/m/c-6UGWFMQC.js +2 -0
- package/dist/m/c-7K4SCQB7.js +1 -0
- package/dist/m/c-7MG5RU63.js +1 -0
- package/dist/m/c-7PRJCA3E.js +1 -0
- package/dist/m/c-A6L3FMUS.js +1 -0
- package/dist/m/c-AMU3BPPR.js +4 -0
- package/dist/m/{c-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
- package/dist/m/c-AQYCG7QW.js +1 -0
- package/dist/m/c-ATKAOH23.js +1 -0
- package/dist/m/c-B5CS55WR.js +1 -0
- package/dist/m/c-B5UJQT7W.js +1 -0
- package/dist/m/c-B7J4K2AE.js +3 -0
- package/dist/m/c-BHZYSHW4.js +1 -0
- package/dist/m/{c-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
- package/dist/m/c-BJ7YQ5QZ.js +1 -0
- package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
- package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
- package/dist/m/c-C6IPRG34.js +1 -0
- package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
- package/dist/m/{c-PS5ARVWR.js → c-CCQ25GFY.js} +1 -1
- package/dist/m/c-CJFDLD43.js +1 -0
- package/dist/m/c-COPTL5WB.js +1 -0
- package/dist/m/c-D3E4WJ4M.js +166 -0
- package/dist/m/c-D5IKEKXW.js +7 -0
- package/dist/m/c-DHBRA6DL.js +1 -0
- package/dist/m/c-DKLS74XB.js +72 -0
- package/dist/m/c-DVYGFTSK.js +3 -0
- package/dist/m/c-E3ZXARWA.js +1 -0
- package/dist/m/c-EK63G3CG.js +1 -0
- package/dist/m/c-ERQSNR6K.js +13 -0
- package/dist/m/c-EVYAISP3.js +1 -0
- package/dist/m/c-EXKHPOVC.js +3 -0
- package/dist/m/{c-QJ4OM3G6.js → c-FWKHM4E7.js} +1 -1
- package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
- package/dist/m/c-GNDL3TKB.js +1 -0
- package/dist/m/c-GRWMD6O5.js +1 -0
- package/dist/m/c-GRXHHFPO.js +1 -0
- package/dist/m/c-HG2C77BM.js +4 -0
- package/dist/m/c-HHGJBU4D.js +1 -0
- package/dist/m/c-HMV6P4RX.js +1 -0
- package/dist/m/c-HMYWIHFG.js +1 -0
- package/dist/m/c-HT2DKBV2.js +1 -0
- package/dist/m/c-I2SCCUEW.js +1 -0
- package/dist/m/{c-TJ6WOJL3.js → c-ICJYHABF.js} +2 -2
- package/dist/m/c-IMZCWYHR.js +7 -0
- package/dist/m/c-IVMD3FEM.js +1 -0
- package/dist/m/{c-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
- package/dist/m/c-JKZJIUIP.js +1 -0
- package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
- package/dist/m/c-K3ADYNLU.js +1 -0
- package/dist/m/c-KGHVFRR3.js +1 -0
- package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
- package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
- package/dist/m/{c-EZU56NQV.js → c-KP64LT2A.js} +2 -2
- package/dist/m/c-KQ7E3PYY.js +1 -0
- package/dist/m/c-L3F5ZLIV.js +1 -0
- package/dist/m/{c-3RUU4TPX.js → c-L42OLXWM.js} +3 -2
- package/dist/m/c-L5VFATT6.js +1 -0
- package/dist/m/c-L7JL5TL3.js +3 -0
- package/dist/m/c-LKSL4AUJ.js +1 -0
- package/dist/m/c-M6FNQCTQ.js +1 -0
- package/dist/m/c-MCOOHCSG.js +1 -0
- package/dist/m/c-MHI3GYKF.js +1 -0
- package/dist/m/c-MWXFLW4J.js +1 -0
- package/dist/m/{c-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
- package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
- package/dist/m/c-NWX3MFT4.js +1 -0
- package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
- package/dist/m/c-OKXI52N7.js +1 -0
- package/dist/m/{c-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
- package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
- package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
- package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
- package/dist/m/c-PLN76GF3.js +6 -0
- package/dist/m/c-PLP4JKFC.js +1 -0
- package/dist/m/c-Q26MME6N.js +1 -0
- package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
- package/dist/m/{c-FRLHSY7E.js → c-QIKT6SKM.js} +1 -1
- package/dist/m/c-QIXT3Q7C.js +28 -0
- package/dist/m/c-QO4HNGCQ.js +1 -0
- package/dist/m/{c-JU3NK25H.js → c-R27DWQKX.js} +1 -1
- package/dist/m/{c-RSMOODDU.js → c-R2XUODVY.js} +1 -1
- package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
- package/dist/m/c-RND3G2S2.js +1 -0
- package/dist/m/{c-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
- package/dist/m/c-SPZPGD3R.js +1 -0
- package/dist/m/c-SW4YEG2D.js +8 -0
- package/dist/m/c-T637T7SN.js +1 -0
- package/dist/m/c-TJAXGIRV.js +1 -0
- package/dist/m/{c-RYUXUUBX.js → c-TNEM3RNM.js} +1 -1
- package/dist/m/c-TTB5EK4S.js +32 -0
- package/dist/m/c-U7SHNWDH.js +1 -0
- package/dist/m/c-UDHIVLYH.js +20 -0
- package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
- package/dist/m/{c-QICP2YR5.js → c-UORXLSD3.js} +1 -1
- package/dist/m/c-UPD33ZPE.js +1 -0
- package/dist/m/c-URSMSFCB.js +1 -0
- package/dist/m/c-UY4QX42B.js +1 -0
- package/dist/m/c-V44KWI2L.js +4 -0
- package/dist/m/c-VILENZTD.js +1 -0
- package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
- package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
- package/dist/m/c-W4JCVLAB.js +1 -0
- package/dist/m/c-W6DQSHXI.js +135 -0
- package/dist/m/c-WBGWGOM7.js +1 -0
- package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
- package/dist/m/c-WGW764WX.js +1 -0
- package/dist/m/c-WMRJKQ2V.js +5 -0
- package/dist/m/c-WNTYKCDG.js +1 -0
- package/dist/m/c-WQWIRWO3.js +72 -0
- package/dist/m/c-X5LNHET5.js +1 -0
- package/dist/m/{c-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
- package/dist/m/c-XGH4GS2T.js +1 -0
- package/dist/m/c-Y6F2FORL.js +1 -0
- package/dist/m/c-YBKHUAK7.js +10 -0
- package/dist/m/c-YBSS55KU.js +20 -0
- package/dist/m/c-YCLU4UNT.js +2 -0
- package/dist/m/c-YVZ2L7CE.js +1 -0
- package/dist/m/c-ZK4GIZH2.js +1 -0
- package/dist/m/c-ZRXJD4G6.js +1 -0
- package/dist/m/c-ZWBI4HYK.js +1 -0
- package/dist/m/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
- package/dist/m/core/autosave-S6OMNDCK.js +1 -0
- package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
- package/dist/m/core/{host-permission-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
- package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
- package/dist/m/core/{persist-JFYRHMRJ.js → persist-UKXV5KLA.js} +1 -1
- package/dist/m/core/recovery-DRFZRJED.js +1 -0
- package/dist/m/core/save-LYY7K6U7.js +1 -0
- package/dist/m/core/save-core-MKRPSEEO.js +1 -0
- package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
- package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
- package/dist/m/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
- package/dist/m/loader-3BBNZZGM.js +1 -0
- package/dist/m/manifest-26C55578.js +1 -0
- package/dist/m/manifest-edit-4396A009.js +1 -0
- package/dist/m/manifest.json +2111 -1573
- package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
- package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
- package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
- package/dist/m/plugins/{demo-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
- package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
- package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
- package/dist/m/plugins/live-AD4H6XSL.js +1 -0
- package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
- package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
- package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
- package/dist/m/plugins/{people-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
- package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
- package/dist/m/plugins/{richclay-NRQDSWX6.js → richclay-KLGETK77.js} +2 -2
- package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
- package/dist/m/plugins/source-TJXOIOYY.js +1 -0
- package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
- package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
- package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
- package/dist/m/plugins/{upload-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
- package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
- package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
- package/dist/m/plugins/{wire-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
- package/dist/m/reader-core-4W2KYEWQ.js +77 -0
- package/dist/m/route/route-62CHXL5B.js +1 -0
- package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
- package/dist/m/ui/index-IVWN2QC4.js +1 -0
- package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
- package/dist/offline-manifest.json +1367 -930
- package/dist/page/themes/light-1.css +8 -7
- package/dist/server/core.cjs +1440 -288
- package/dist/server/offline-runtime.cjs +63 -63
- package/dist/server/offline-runtime.mjs +63 -63
- package/dist/server/page-compaction-worker.cjs +3710 -0
- package/dist/server/rooms.mjs +5 -0
- package/docs/guide.md +653 -101
- package/entries/sap.js +5 -5
- package/package.json +1 -1
- package/src/core/admin-contenteditable.js +5 -2
- package/src/core/admin-inputs.js +1 -0
- package/src/core/admin-onclick.js +2 -1
- package/src/core/conflict-notice-ui.js +4 -7
- package/src/core/download.js +2 -2
- package/src/core/host-meta.js +38 -0
- package/src/core/is-edit-mode.js +17 -1
- package/src/core/mode-key.js +125 -19
- package/src/core/mode-support.js +5 -0
- package/src/core/mode-transition.js +20 -6
- package/src/core/preserve-authored.js +4 -4
- package/src/core/recovery.js +121 -33
- package/src/core/save-accepted.js +33 -0
- package/src/core/save-core.js +106 -31
- package/src/core/save-job.js +108 -0
- package/src/core/save-prepare.js +53 -0
- package/src/core/save-size-notice.js +3 -1
- package/src/core/save-slices.js +15 -0
- package/src/core/save.js +557 -127
- package/src/core/serialize-steps.js +109 -0
- package/src/core/snapshot.js +170 -39
- package/src/core/source-map.js +83 -38
- package/src/core/stale-host-notice.js +1 -0
- package/src/core/upgrade-run.js +59 -8
- package/src/dom/active-element.js +5 -0
- package/src/lib/assets.js +1 -0
- package/src/lib/slice.js +65 -0
- package/src/lib/uimotion.js +774 -0
- package/src/lib/unsaved-state.js +2 -2
- package/src/live/fast-lane.js +10 -3
- package/src/live/local-lane.js +202 -11
- package/src/live/ops.js +16 -0
- package/src/live/page-compaction.js +162 -0
- package/src/live/server-lane.js +1 -0
- package/src/live/store.js +11 -3
- package/src/loader-logic.js +3 -1
- package/src/loader.js +14 -4
- package/src/page/adopt-boot.js +2 -1
- package/src/page/awareness.js +33 -2
- package/src/page/blocks.js +89 -27
- package/src/page/boot-pack.js +1 -1
- package/src/page/chart-tables.js +21 -3
- package/src/page/chart.js +406 -63
- package/src/page/checklist.js +10 -135
- package/src/page/checkpoint-proof.js +7 -1
- package/src/page/chrome.js +250 -51
- package/src/page/clipboard-shape.js +126 -0
- package/src/page/drag.js +33 -3
- package/src/page/editor.js +136 -25
- package/src/page/export-assets.js +357 -0
- package/src/page/export-html.js +209 -0
- package/src/page/export-markdown.js +361 -0
- package/src/page/features/checklist.js +428 -0
- package/src/page/features/export-html.js +189 -0
- package/src/page/features/export-markdown.js +169 -0
- package/src/page/features/import-markdown.js +349 -0
- package/src/page/features/media.js +61 -5
- package/src/page/features/registry.js +18 -0
- package/src/page/features/table.js +373 -83
- package/src/page/features/title-cover.js +331 -0
- package/src/page/features/title-icon.js +213 -0
- package/src/page/features/toc.js +87 -0
- package/src/page/format-command.js +25 -1
- package/src/page/format-gate.js +68 -18
- package/src/page/format-host.js +17 -0
- package/src/page/format-native-commands.js +21 -5
- package/src/page/image-actions.js +64 -0
- package/src/page/import-markdown.js +262 -0
- package/src/page/items.js +135 -17
- package/src/page/markdown.js +6 -2
- package/src/page/media.js +35 -119
- package/src/page/placeholder.js +14 -1
- package/src/page/projection.js +98 -10
- package/src/page/recover.js +10 -12
- package/src/page/recovery-ui.js +464 -0
- package/src/page/rich/chart.js +63 -7
- package/src/page/save-projection.js +28 -0
- package/src/page/save-states.js +13 -3
- package/src/page/schema.js +19 -3
- package/src/page/table-paste.js +3 -1
- package/src/page/table-style.js +4 -4
- package/src/page/table.js +44 -2
- package/src/page/themes/light-1.css +8 -7
- package/src/page/themes/light-1.source.css +8 -7
- package/src/page/toc-link.js +31 -0
- package/src/page/toc.js +5 -0
- package/src/page/view-hooks.js +38 -12
- package/src/page/view.js +13 -0
- package/src/page/ybind/binding.js +88 -5
- package/src/page/ybind/bridge.js +187 -20
- package/src/page/ybind/containment.js +162 -0
- package/src/page/ybind/doc.js +17 -9
- package/src/page/ybind/file-debt.js +63 -0
- package/src/page/ybind/layout.js +7 -2
- package/src/page/ybind/marks.js +1 -1
- package/src/page/ybind/moves.js +5 -4
- package/src/page/ybind/origins.js +106 -4
- package/src/page/ybind/parts.js +8 -1
- package/src/page/ybind/provider.js +113 -17
- package/src/page/ybind/recovery-proof.js +32 -9
- package/src/page/ybind/recovery-replay.js +558 -15
- package/src/page/ybind/recovery.js +174 -45
- package/src/page/ybind/table.js +67 -11
- package/src/page/ybind/walk-delta.js +258 -0
- package/src/page/ybind/walk.js +54 -29
- package/src/page/ybind/writer.js +1 -1
- package/src/plugins/ai-edit.js +13 -9
- package/src/plugins/indicator-chip.js +91 -0
- package/src/plugins/indicator.js +13 -80
- package/src/plugins/live.js +89 -14
- package/src/plugins/menu/actions.js +196 -0
- package/src/plugins/menu/people.js +279 -0
- package/src/plugins/menu/placement.js +121 -0
- package/src/plugins/menu/preferences.js +9 -0
- package/src/plugins/menu/seo.js +316 -397
- package/src/plugins/menu/style.js +171 -0
- package/src/plugins/menu/variants.js +296 -0
- package/src/plugins/menu.js +186 -183
- package/src/plugins/page-view.js +3 -2
- package/src/plugins/page.js +249 -44
- package/src/plugins/sortable.js +8 -1
- package/src/plugins/source.js +6 -0
- package/src/plugins/upgrade.js +51 -17
- package/src/plugins/upload-ui.js +12 -17
- package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
- package/src/plugins/visitor-drafts/intro-style.js +7 -2
- package/src/plugins/visitor-drafts/intro.js +5 -3
- package/src/plugins/visitor-drafts/owner-chip.js +8 -12
- package/src/plugins/visitor-drafts/prompt.js +11 -9
- package/src/plugins/visitor-drafts/source.js +4 -3
- package/src/plugins/visitor-drafts/store.js +138 -9
- package/src/plugins/visitor-drafts/ui-memory.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +4 -6
- package/src/plugins/visitor-drafts.js +5 -3
- package/src/recovery/store.js +1 -1
- package/src/route/route.js +4 -0
- package/src/server/rooms/index.js +5 -1
- package/src/server/rooms/live-data-rooms.js +302 -50
- package/src/server/rooms/page-compaction-worker.js +12 -0
- package/src/server/rooms/page-compactor.js +92 -0
- package/src/server/rooms/page-room-conformance.js +461 -0
- package/src/source/parse-steps.js +94 -0
- package/src/source/session.js +233 -57
- package/src/sync/editing-sender.js +12 -0
- package/src/sync/live-sync-edit.js +22 -2
- package/src/sync/live-sync.js +150 -23
- package/src/sync/page-relay.js +52 -0
- package/src/sync/presence-store.js +44 -4
- package/src/sync/presence.js +6 -3
- package/src/sync/saved-stream.js +11 -6
- package/src/ui/bevel-controls.js +1 -0
- package/src/ui/bevel-dialog.js +5 -2
- package/src/ui/editor-ui.js +77 -0
- package/src/ui/page-colour.js +281 -0
- package/src/ui/toast.js +16 -7
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +9 -9
- package/src/vendor/parse5.vendor.js +2 -2
- package/src/vendor/sap-core.vendor.js +5 -5
- package/dist/m/c-2SPVQSHC.js +0 -1
- package/dist/m/c-336SGU5W.js +0 -1
- package/dist/m/c-3H6BZDNJ.js +0 -6
- package/dist/m/c-4DFEJ2PO.js +0 -3
- package/dist/m/c-56QRZTZH.js +0 -1
- package/dist/m/c-5J4E7O4Y.js +0 -4
- package/dist/m/c-5LKDWQTL.js +0 -19
- package/dist/m/c-5V3MOVJT.js +0 -1
- package/dist/m/c-6AZOGAPE.js +0 -1
- package/dist/m/c-6CRUUXOX.js +0 -5
- package/dist/m/c-6HMB24EV.js +0 -1
- package/dist/m/c-6KEVEQ7L.js +0 -1
- package/dist/m/c-A3E3NIMC.js +0 -1
- package/dist/m/c-BQ6LOBEE.js +0 -1
- package/dist/m/c-BTDI4Y23.js +0 -1
- package/dist/m/c-CJ3A7ZKE.js +0 -1
- package/dist/m/c-CQHPGKX2.js +0 -1
- package/dist/m/c-DFS4XZJV.js +0 -2
- package/dist/m/c-DNENWY4O.js +0 -1
- package/dist/m/c-E4LIR5X5.js +0 -1
- package/dist/m/c-FDNHHZY5.js +0 -1
- package/dist/m/c-FROJUZMV.js +0 -1
- package/dist/m/c-G2JPKCTP.js +0 -1
- package/dist/m/c-GB6TU7WV.js +0 -1
- package/dist/m/c-GFJJEBTU.js +0 -1
- package/dist/m/c-GJAEGGXY.js +0 -1
- package/dist/m/c-GOWBDM6J.js +0 -1
- package/dist/m/c-HFKUVACH.js +0 -1
- package/dist/m/c-HHDFCNHV.js +0 -1
- package/dist/m/c-IEM4NTCS.js +0 -1
- package/dist/m/c-JDCX26UP.js +0 -25
- package/dist/m/c-KGVGROZT.js +0 -1
- package/dist/m/c-KQ6TYJAP.js +0 -135
- package/dist/m/c-LFPHCXY3.js +0 -1
- package/dist/m/c-LRPUO2MX.js +0 -1
- package/dist/m/c-LWLBHLVC.js +0 -3
- package/dist/m/c-M2L47S2X.js +0 -7
- package/dist/m/c-MFP2ZWD4.js +0 -1
- package/dist/m/c-MKSQ4Y57.js +0 -1
- package/dist/m/c-NUFFJVVJ.js +0 -1
- package/dist/m/c-NUHSPT5P.js +0 -1
- package/dist/m/c-O72HVYSA.js +0 -1
- package/dist/m/c-OIBUTDXI.js +0 -1
- package/dist/m/c-OW5QIGG3.js +0 -1
- package/dist/m/c-PGRCK73F.js +0 -1
- package/dist/m/c-Q7NQD65V.js +0 -1
- package/dist/m/c-QH63LQOS.js +0 -1
- package/dist/m/c-R7IWVINQ.js +0 -1
- package/dist/m/c-RM7J7BV2.js +0 -1
- package/dist/m/c-S4ZEH46B.js +0 -9
- package/dist/m/c-SDAXDM2D.js +0 -1
- package/dist/m/c-SMGU7RT3.js +0 -235
- package/dist/m/c-SPIORRYS.js +0 -1
- package/dist/m/c-SWLRT66R.js +0 -1
- package/dist/m/c-TCJTVAFN.js +0 -1
- package/dist/m/c-TQEMVJCS.js +0 -1
- package/dist/m/c-TYLXX4NF.js +0 -21
- package/dist/m/c-U62OB4BT.js +0 -1
- package/dist/m/c-U6YWUAR3.js +0 -8
- package/dist/m/c-UDCSU5G5.js +0 -2
- package/dist/m/c-VI5RMSVY.js +0 -1
- package/dist/m/c-VRZN56XY.js +0 -1
- package/dist/m/c-W5MCHEGA.js +0 -1
- package/dist/m/c-WNYBLIOZ.js +0 -1
- package/dist/m/c-WWXIDH4O.js +0 -28
- package/dist/m/c-XCNTK76U.js +0 -1
- package/dist/m/c-XMS6DPBR.js +0 -1
- package/dist/m/c-ZUCOKOHT.js +0 -1
- package/dist/m/c-ZVPVXTJF.js +0 -4
- package/dist/m/c-ZVRKXKM7.js +0 -1
- package/dist/m/core/autosave-HRYGUWV6.js +0 -1
- package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
- package/dist/m/core/recovery-FLTCC2OC.js +0 -1
- package/dist/m/core/save-HQZ4S667.js +0 -1
- package/dist/m/core/save-core-KGQWFUNH.js +0 -1
- package/dist/m/core/snapshot-EGQZNT45.js +0 -1
- package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
- package/dist/m/loader-S4I7GKXR.js +0 -1
- package/dist/m/manifest-4C419D67.js +0 -1
- package/dist/m/manifest-edit-B077F288.js +0 -1
- package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
- package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
- package/dist/m/plugins/live-HFWD4OIH.js +0 -1
- package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
- package/dist/m/plugins/page-JOKB6XPR.js +0 -2
- package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
- package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
- package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
- package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
- package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
- package/dist/m/reader-core-DXYCURFF.js +0 -68
- package/dist/m/route/route-KOCZSEGN.js +0 -1
- package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
- package/dist/m/ui/index-3JLVI27O.js +0 -1
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
- /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
- /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// The readable snapshot's inventory, made self-contained: every displayed image and
|
|
2
|
+
// included attachment the snapshot names is read once, within limits, and written
|
|
3
|
+
// into the document as a data URL, so the file that comes out needs nothing fetched
|
|
4
|
+
// again. What cannot be carried is named in a loss list rather than dropped in
|
|
5
|
+
// silence: a cross-origin read the other origin refuses, a host that is not
|
|
6
|
+
// reachable, a missing file, a type a reading copy does not carry, a resource over
|
|
7
|
+
// its limit, an embed with no static rendering. Addresses are canonical before the
|
|
8
|
+
// duplicate check, so one picture named twice is one request.
|
|
9
|
+
/** The limits one export reads under: per resource, in total, and how long each waits. */
|
|
10
|
+
export const EXPORT_LIMITS = Object.freeze({
|
|
11
|
+
perResource: 20 * 1024 * 1024,
|
|
12
|
+
total: 50 * 1024 * 1024,
|
|
13
|
+
timeout: 10_000,
|
|
14
|
+
inFlight: 3,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// What a reading copy carries inside itself: images, fonts and media, never anything
|
|
18
|
+
// a browser would treat as a document, a script or a style. An active type such as
|
|
19
|
+
// SVG or HTML is refused by name rather than inlined.
|
|
20
|
+
const ALLOWED_MIME = [
|
|
21
|
+
/^image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon)$/i,
|
|
22
|
+
/^font\/(?:woff2?|ttf|otf|sfnt|collection|eot)$/i,
|
|
23
|
+
/^application\/(?:font-woff2?|vnd\.ms-fontobject|x-font-ttf|x-font-opentype|vnd\.ms-opentype)$/i,
|
|
24
|
+
/^audio\/[a-z0-9][a-z0-9.+-]*$/i,
|
|
25
|
+
/^video\/[a-z0-9][a-z0-9.+-]*$/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// What no attachment may carry: anything a browser would run or render as a document.
|
|
29
|
+
const ACTIVE_MIME = /^(?:text\/(?:html|javascript|css|xml|xsl)|application\/(?:xhtml\+xml|xml|javascript|ecmascript|x-javascript|wasm)|image\/svg\+xml)$|\+xml$/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a response's declared type is one a reading copy carries inside itself. An image
|
|
33
|
+
* is drawn, so only the drawn types; an attachment is a link to a file, so any type that is
|
|
34
|
+
* not active.
|
|
35
|
+
*/
|
|
36
|
+
export function isAllowedMime(type, kind = "image") {
|
|
37
|
+
const base = String(type ?? "").split(";")[0].trim();
|
|
38
|
+
if (base === "") return false;
|
|
39
|
+
if (kind === "attachment") return !ACTIVE_MIME.test(base);
|
|
40
|
+
return ALLOWED_MIME.some((pattern) => pattern.test(base));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One address, as the duplicate check and the request both see it: resolved against
|
|
45
|
+
* the captured base URL, with the fragment gone. A value that names nothing, or that
|
|
46
|
+
* is not an address at all, answers null.
|
|
47
|
+
*/
|
|
48
|
+
export function canonicalUrl(raw, base = globalThis.document?.baseURI ?? null) {
|
|
49
|
+
const text = String(raw ?? "").trim();
|
|
50
|
+
if (!text || text.startsWith("#")) return null;
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(text, base ?? undefined);
|
|
53
|
+
url.hash = "";
|
|
54
|
+
return url.href;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const squash = (value) => String(value ?? "").trim().replace(/[\u0000-\u0020]/g, "");
|
|
61
|
+
const isLocalHost = (hostname) => /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1\]|::1)$/i.test(hostname || "") || /\.local$/i.test(hostname || "");
|
|
62
|
+
const originOf = (href) => { try { return new URL(href).origin; } catch { return null; } };
|
|
63
|
+
const hostOf = (href) => { try { return new URL(href).hostname; } catch { return null; } };
|
|
64
|
+
const mb = (bytes) => (bytes >= 1024 * 1024 ? `${Math.round(bytes / (1024 * 1024))} MiB` : bytes >= 1024 ? `${Math.round(bytes / 1024)} KiB` : `${Math.round(bytes)} B`);
|
|
65
|
+
|
|
66
|
+
const subject = (target) => (target.blockId ? `The ${target.kind} in block ${target.blockId}` : `The ${target.kind}`);
|
|
67
|
+
const loss = (code, target, message) => ({ code, kind: target.kind ?? null, url: target.url ?? null, blockId: target.blockId ?? null, message });
|
|
68
|
+
|
|
69
|
+
function embedLoss(entry, url) {
|
|
70
|
+
const blockId = entry.blockId ?? null;
|
|
71
|
+
const where = blockId ? ` in block ${blockId}` : "";
|
|
72
|
+
return {
|
|
73
|
+
code: "unsupported-embed",
|
|
74
|
+
kind: "embed",
|
|
75
|
+
url: entry.url ?? null,
|
|
76
|
+
blockId,
|
|
77
|
+
message: `The embed${where} is a live frame, which a reading copy cannot carry${url ? ` (${url})` : ""}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function urlLoss(entry) {
|
|
82
|
+
const blockId = entry.blockId ?? null;
|
|
83
|
+
const where = blockId ? ` in block ${blockId}` : "";
|
|
84
|
+
return {
|
|
85
|
+
code: "unsupported-url",
|
|
86
|
+
kind: entry.kind ?? null,
|
|
87
|
+
url: entry.url ?? null,
|
|
88
|
+
blockId,
|
|
89
|
+
message: `The ${entry.kind}${where} points at ${String(entry.url ?? "").trim().slice(0, 120)}, which is not an address a reading copy can fetch`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const bytesOf = (value) => {
|
|
94
|
+
if (!value) return new Uint8Array(0);
|
|
95
|
+
if (value instanceof Uint8Array) return value;
|
|
96
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
97
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
98
|
+
return new Uint8Array(value);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function toBase64(bytes) {
|
|
102
|
+
let binary = "";
|
|
103
|
+
for (let at = 0; at < bytes.length; at += 0x8000) binary += String.fromCharCode(...bytes.subarray(at, at + 0x8000));
|
|
104
|
+
return globalThis.btoa(binary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dataUrlOf = (type, bytes) => `data:${String(type).split(";")[0].trim()};base64,${toBase64(bytes)}`;
|
|
108
|
+
|
|
109
|
+
// The body as bytes, never as a whole file at once: each chunk is counted against
|
|
110
|
+
// both limits, so an oversized resource, or one that would push the file past its
|
|
111
|
+
// total, stops at the chunk that crossed the line instead of after the download.
|
|
112
|
+
async function readBytes(response, task, limits, budget) {
|
|
113
|
+
const declared = Number(response.headers?.get?.("content-length"));
|
|
114
|
+
if (Number.isFinite(declared) && declared > limits.perResource) {
|
|
115
|
+
throw loss("too-large", task.target, `${subject(task.target)} at ${task.target.url} is larger than ${mb(limits.perResource)}`);
|
|
116
|
+
}
|
|
117
|
+
const charge = () => {
|
|
118
|
+
if (task.bytes > limits.perResource) throw loss("too-large", task.target, `${subject(task.target)} at ${task.target.url} is larger than ${mb(limits.perResource)}`);
|
|
119
|
+
if (budget.exhausted || budget.spent + budget.live > budget.total) {
|
|
120
|
+
budget.exhausted = true;
|
|
121
|
+
throw loss("total-too-large", task.target, `The page's resources together are larger than ${mb(budget.total)}`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const reader = response.body?.getReader?.();
|
|
125
|
+
if (!reader) {
|
|
126
|
+
const whole = bytesOf(typeof response.arrayBuffer === "function" ? await response.arrayBuffer() : response.body);
|
|
127
|
+
task.bytes += whole.byteLength;
|
|
128
|
+
budget.live += whole.byteLength;
|
|
129
|
+
try {
|
|
130
|
+
charge();
|
|
131
|
+
} finally {
|
|
132
|
+
budget.live -= whole.byteLength;
|
|
133
|
+
if (task.bytes <= limits.perResource) budget.spent += whole.byteLength;
|
|
134
|
+
}
|
|
135
|
+
return whole;
|
|
136
|
+
}
|
|
137
|
+
const chunks = [];
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
const part = bytesOf(value);
|
|
143
|
+
task.bytes += part.byteLength;
|
|
144
|
+
budget.live += part.byteLength;
|
|
145
|
+
charge();
|
|
146
|
+
chunks.push(part);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
budget.live -= task.bytes;
|
|
150
|
+
task.bytes = 0;
|
|
151
|
+
try { await reader.cancel?.(); } catch {}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
budget.live -= task.bytes;
|
|
155
|
+
budget.spent += task.bytes;
|
|
156
|
+
const out = new Uint8Array(task.bytes);
|
|
157
|
+
let at = 0;
|
|
158
|
+
for (const part of chunks) { out.set(part, at); at += part.byteLength; }
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// One resource: one request with the credentials its origin is owed, one timeout, one
|
|
163
|
+
// cancellation, and one named loss if it cannot be carried. Nothing here throws past
|
|
164
|
+
// the loss: an export reports every failure it found rather than the first.
|
|
165
|
+
async function fetchOne(task, { fetchImpl, baseOrigin, baseHost, limits, budget, run }) {
|
|
166
|
+
const { target, controller } = task;
|
|
167
|
+
if (run.signal.aborted) return { target, loss: loss("cancelled", target, `Reading ${target.url} was cancelled`) };
|
|
168
|
+
const onCancel = () => controller.abort();
|
|
169
|
+
if (run.signal.aborted) controller.abort();
|
|
170
|
+
else run.signal.addEventListener("abort", onCancel, { once: true });
|
|
171
|
+
const timer = setTimeout(() => { task.timedOut = true; controller.abort(); }, limits.timeout);
|
|
172
|
+
const sameOrigin = baseOrigin != null && originOf(target.url) === baseOrigin;
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetchImpl(target.url, {
|
|
175
|
+
credentials: sameOrigin ? "include" : "omit",
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
if (!response || response.ok === false) {
|
|
179
|
+
const status = response?.status ?? 0;
|
|
180
|
+
throw loss("missing-resource", target, `${subject(target)} is not at ${target.url} (the host answered ${status || "nothing"})`);
|
|
181
|
+
}
|
|
182
|
+
const type = response.headers?.get?.("content-type") ?? "";
|
|
183
|
+
if (!isAllowedMime(type, target.kind)) {
|
|
184
|
+
throw loss("mime-refused", target, `${subject(target)} at ${target.url} is ${String(type).split(";")[0].trim() || "of no stated type"}, which a reading copy does not carry`);
|
|
185
|
+
}
|
|
186
|
+
const bytes = await readBytes(response, task, limits, budget);
|
|
187
|
+
return { target, hit: { data: dataUrlOf(type, bytes), type: String(type).split(";")[0].trim(), bytes: bytes.byteLength } };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error && typeof error.code === "string" && typeof error.message === "string") return { target, loss: error };
|
|
190
|
+
if (task.timedOut) return { target, loss: loss("timeout", target, `${subject(target)} at ${target.url} did not arrive within ${Math.round(limits.timeout / 1000)} s`) };
|
|
191
|
+
if (run.signal.aborted || controller.signal.aborted) return { target, loss: loss("cancelled", target, `Reading ${target.url} was cancelled`) };
|
|
192
|
+
if (!sameOrigin) return { target, loss: loss("cross-origin-cors", target, `${subject(target)} at ${target.url} could not be read: ${originOf(target.url) ?? "the other origin"} refused a cross-origin read`) };
|
|
193
|
+
if (isLocalHost(baseHost)) return { target, loss: loss("offline-host", target, `${subject(target)} at ${target.url} could not be read: this copy's host is not reachable`) };
|
|
194
|
+
return { target, loss: loss("unreachable", target, `${subject(target)} at ${target.url} could not be read`) };
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
run.signal.removeEventListener("abort", onCancel);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function pool(tasks, inFlight, worker) {
|
|
202
|
+
const queue = tasks.slice();
|
|
203
|
+
const run = async () => {
|
|
204
|
+
for (;;) {
|
|
205
|
+
const task = queue.shift();
|
|
206
|
+
if (!task) return;
|
|
207
|
+
await worker(task);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(inFlight, queue.length)) }, run));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The document with the fetched bytes inside it and every remote reference gone: a
|
|
214
|
+
// data URL where a picture or an attachment was, and nothing at all where a resource
|
|
215
|
+
// could not be carried. A stylesheet or script element has no place in a reading copy,
|
|
216
|
+
// and a `url()` or `@import` leaves a style declaration alone.
|
|
217
|
+
function rewrite(html, hits, base) {
|
|
218
|
+
const parsed = new DOMParser().parseFromString(String(html ?? ""), "text/html");
|
|
219
|
+
for (const el of [...parsed.querySelectorAll("script, link, base, style[media]")]) el.remove();
|
|
220
|
+
for (const el of [...parsed.querySelectorAll("iframe, embed, object, video, audio, source, track, picture")]) el.remove();
|
|
221
|
+
for (const el of parsed.querySelectorAll("img")) {
|
|
222
|
+
const hit = hits.get(canonicalUrl(el.getAttribute("src"), base));
|
|
223
|
+
if (hit) el.setAttribute("src", hit.data);
|
|
224
|
+
else el.removeAttribute("src");
|
|
225
|
+
el.removeAttribute("srcset");
|
|
226
|
+
el.removeAttribute("poster");
|
|
227
|
+
}
|
|
228
|
+
for (const el of parsed.querySelectorAll('[data-type="file"]')) {
|
|
229
|
+
const hit = hits.get(canonicalUrl(el.getAttribute("data-src"), base)) ?? hits.get(canonicalUrl(el.querySelector("a[href]")?.getAttribute("href"), base));
|
|
230
|
+
if (hit) {
|
|
231
|
+
el.setAttribute("data-src", hit.data);
|
|
232
|
+
const link = el.querySelector("a[href]");
|
|
233
|
+
if (link) link.setAttribute("href", hit.data);
|
|
234
|
+
} else {
|
|
235
|
+
el.removeAttribute("data-src");
|
|
236
|
+
el.querySelector("a[href]")?.removeAttribute("href");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const el of parsed.querySelectorAll("[style]")) {
|
|
240
|
+
const kept = String(el.getAttribute("style")).split(";").filter((part) => !/url\s*\(|@import/i.test(part)).join(";").trim();
|
|
241
|
+
if (kept) el.setAttribute("style", kept);
|
|
242
|
+
else el.removeAttribute("style");
|
|
243
|
+
}
|
|
244
|
+
return `<!DOCTYPE html>\n${parsed.documentElement.outerHTML}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const CANDIDATES = [...String("src srcset poster data data-src xlink:href").split(" "), "href"];
|
|
248
|
+
const carriesRemote = (value) => {
|
|
249
|
+
const s = squash(value);
|
|
250
|
+
if (!s || s.startsWith("#")) return false;
|
|
251
|
+
return !/^data:/i.test(s);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Every reference in a document that would make the browser request something the
|
|
256
|
+
* file does not hold: a source, a source set, a poster, an object's data, a
|
|
257
|
+
* stylesheet's or a script's address, a `url()` or `@import` in a style. An ordinary
|
|
258
|
+
* hyperlink is not one, and neither is a data URL or a fragment.
|
|
259
|
+
*/
|
|
260
|
+
export function remoteDependencies(html) {
|
|
261
|
+
const parsed = new DOMParser().parseFromString(String(html ?? ""), "text/html");
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const el of parsed.querySelectorAll("*")) {
|
|
264
|
+
const tag = el.tagName.toLowerCase();
|
|
265
|
+
for (const name of CANDIDATES) {
|
|
266
|
+
if (!el.hasAttribute(name)) continue;
|
|
267
|
+
if (name === "href" && tag !== "link" && tag !== "script" && tag !== "base") continue;
|
|
268
|
+
if (name === "data" && tag !== "object") continue;
|
|
269
|
+
if (name === "data-src" && !el.closest?.('[data-type="file"]')) continue;
|
|
270
|
+
const value = el.getAttribute(name);
|
|
271
|
+
const candidates = name === "srcset" ? String(value).split(",").map((part) => part.trim().split(/\s+/)[0]) : [value];
|
|
272
|
+
for (const candidate of candidates) if (carriesRemote(candidate)) out.push({ tag, attr: name, value: candidate });
|
|
273
|
+
}
|
|
274
|
+
const style = el.getAttribute("style");
|
|
275
|
+
if (style && /url\s*\(|@import/i.test(style)) out.push({ tag, attr: "style", value: style });
|
|
276
|
+
}
|
|
277
|
+
for (const el of parsed.querySelectorAll("style")) {
|
|
278
|
+
if (/url\s*\(|@import/i.test(el.textContent ?? "")) out.push({ tag: "style", attr: "text", value: el.textContent });
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* One readable snapshot made self-contained: `html` with its images and attachments
|
|
285
|
+
* inside it, the named `losses` that stopped anything else being carried, and the
|
|
286
|
+
* `remote` references that still point off the file (empty when nothing does). The
|
|
287
|
+
* caller downloads only when both lists are empty.
|
|
288
|
+
*/
|
|
289
|
+
export async function inlineExportResources(html, {
|
|
290
|
+
resources = [],
|
|
291
|
+
base = globalThis.document?.baseURI ?? null,
|
|
292
|
+
fetch: fetchImpl = globalThis.fetch,
|
|
293
|
+
signal = null,
|
|
294
|
+
limits = EXPORT_LIMITS,
|
|
295
|
+
} = {}) {
|
|
296
|
+
const capped = { ...EXPORT_LIMITS, ...(limits ?? {}) };
|
|
297
|
+
const losses = [];
|
|
298
|
+
const named = new Set();
|
|
299
|
+
const add = (entry) => {
|
|
300
|
+
const key = `${entry.code}\u0000${entry.blockId ?? ""}\u0000${entry.message}`;
|
|
301
|
+
if (named.has(key)) return;
|
|
302
|
+
named.add(key);
|
|
303
|
+
losses.push(entry);
|
|
304
|
+
};
|
|
305
|
+
const targets = new Map();
|
|
306
|
+
for (const entry of resources ?? []) {
|
|
307
|
+
if (!entry || entry.kind === "chart" || entry.inline) continue;
|
|
308
|
+
const url = entry.url ? canonicalUrl(entry.url, base) : null;
|
|
309
|
+
if (entry.kind === "embed") {
|
|
310
|
+
add(embedLoss(entry, url));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!url) {
|
|
314
|
+
add(urlLoss(entry));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const target = targets.get(url) ?? { url, kind: entry.kind, blockId: entry.blockId ?? null };
|
|
318
|
+
target.blockId ??= entry.blockId ?? null;
|
|
319
|
+
targets.set(url, target);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const listed = [...targets.values()];
|
|
323
|
+
const hits = new Map();
|
|
324
|
+
const budget = { spent: 0, live: 0, total: capped.total, exhausted: false };
|
|
325
|
+
if (listed.length && typeof fetchImpl !== "function") {
|
|
326
|
+
for (const target of listed) add(loss("unavailable", target, `${subject(target)} at ${target.url} could not be read: this page cannot fetch it`));
|
|
327
|
+
} else if (listed.length) {
|
|
328
|
+
const run = new AbortController();
|
|
329
|
+
const onCancel = () => run.abort();
|
|
330
|
+
if (signal) {
|
|
331
|
+
if (signal.aborted) run.abort();
|
|
332
|
+
else signal.addEventListener("abort", onCancel, { once: true });
|
|
333
|
+
}
|
|
334
|
+
const baseOrigin = base ? originOf(canonicalUrl(base, base) ?? base) : null;
|
|
335
|
+
const baseHost = base ? hostOf(canonicalUrl(base, base) ?? base) : null;
|
|
336
|
+
const answers = new Map();
|
|
337
|
+
try {
|
|
338
|
+
const tasks = listed.map((target) => ({ target, controller: new AbortController(), bytes: 0, timedOut: false }));
|
|
339
|
+
await pool(tasks, capped.inFlight, async (task) => {
|
|
340
|
+
if (run.signal.aborted) return;
|
|
341
|
+
answers.set(task.target.url, await fetchOne(task, { fetchImpl, baseOrigin, baseHost, limits: capped, budget, run }));
|
|
342
|
+
});
|
|
343
|
+
} finally {
|
|
344
|
+
signal?.removeEventListener("abort", onCancel);
|
|
345
|
+
}
|
|
346
|
+
for (const target of listed) {
|
|
347
|
+
const answer = answers.get(target.url);
|
|
348
|
+
if (answer?.hit) hits.set(target.url, answer.hit);
|
|
349
|
+
else if (answer?.loss) add(answer.loss);
|
|
350
|
+
else if (run.signal.aborted) add(loss("cancelled", target, `Reading ${target.url} was cancelled`));
|
|
351
|
+
else add(loss("unreachable", target, `${subject(target)} at ${target.url} could not be read`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const out = rewrite(html, hits, base);
|
|
356
|
+
return { html: out, losses, remote: remoteDependencies(out), embedded: hits.size, bytes: budget.spent };
|
|
357
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The readable snapshot: one static HTML document rendered from the live page
|
|
2
|
+
// model through the same projection the saved page carries, plus the inventory
|
|
3
|
+
// of every image, attachment, chart and embed it references. Nothing here
|
|
4
|
+
// fetches, downloads or writes a file: inlining those resources and the export
|
|
5
|
+
// action belong to the feature that calls this.
|
|
6
|
+
//
|
|
7
|
+
// The document keeps the content and drops the machinery: no script, stylesheet,
|
|
8
|
+
// live tag, save hook or block envelope, every toggle open, and a table of
|
|
9
|
+
// contents as a plain list rather than links into a page that is not running.
|
|
10
|
+
import { fragmentCleaner, renderModel } from "./projection.js";
|
|
11
|
+
import { documentTitle } from "./document-title.js";
|
|
12
|
+
|
|
13
|
+
/** A snapshot that cannot be produced without lying about its contents. */
|
|
14
|
+
export class PageExportError extends Error {
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "PageExportError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fail = (code, message) => { throw new PageExportError(code, message); };
|
|
23
|
+
|
|
24
|
+
// A reference the document may carry: bytes already inside the file, an address
|
|
25
|
+
// on the origin the page was read from, or a plain http(s) URL. A script URL or a
|
|
26
|
+
// second document in a data URL is refused by name, never dropped in silence.
|
|
27
|
+
const DATA_IMAGE = /^data:image\/(?:png|jpe?g|gif|webp);/i;
|
|
28
|
+
const SCRIPT_URL = /^(?:javascript|vbscript|data):/i;
|
|
29
|
+
const REMOTE = /^https?:\/\//i;
|
|
30
|
+
const ROOTED = /^\/(?!\/)/;
|
|
31
|
+
const squash = (value) => String(value ?? "").trim().replace(/[\u0000-\u0020]/g, "");
|
|
32
|
+
const carriable = (value) => DATA_IMAGE.test(value) || !SCRIPT_URL.test(value);
|
|
33
|
+
|
|
34
|
+
function reference(raw) {
|
|
35
|
+
const s = squash(raw);
|
|
36
|
+
if (!s || s.startsWith("#")) return null;
|
|
37
|
+
if (DATA_IMAGE.test(s)) return { url: s, inline: true };
|
|
38
|
+
if (/^\/\//.test(s)) return { url: `https:${s}`, inline: false };
|
|
39
|
+
if (ROOTED.test(s) || REMOTE.test(s)) return { url: s, inline: false };
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RESOURCE_TAGS = { img: "image", iframe: "embed", embed: "embed", object: "embed", video: "embed", audio: "embed", source: "embed", track: "embed" };
|
|
44
|
+
const SOURCE_ATTR = { object: "data" };
|
|
45
|
+
const STATIC_MEDIA = /<(?:img|svg|video|audio|canvas|iframe|table)\b/i;
|
|
46
|
+
const STATIC_TEXT = /<(?:meta|style|img|br|hr|input)\b[^>]*>/gi;
|
|
47
|
+
|
|
48
|
+
const blockOf = (el) => el.closest?.("[data-id]")?.getAttribute("data-id") || null;
|
|
49
|
+
const where = (kind, blockId) => (blockId ? `${kind} in block ${blockId}` : kind);
|
|
50
|
+
|
|
51
|
+
// Every reference the document carries, in the order it appears, each named by the
|
|
52
|
+
// block that holds it. Charts are listed too: their drawing is already SVG inside
|
|
53
|
+
// the file, so they name no address and need nothing fetched.
|
|
54
|
+
function resourcesOf(container) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const add = (kind, raw, blockId) => {
|
|
58
|
+
const ref = reference(raw);
|
|
59
|
+
if (ref === undefined) {
|
|
60
|
+
fail("unsafe-url", `The ${where(kind, blockId)} points at ${String(raw).trim().slice(0, 120)}, which a reading copy cannot carry`);
|
|
61
|
+
}
|
|
62
|
+
if (!ref) return;
|
|
63
|
+
const key = `${kind}\u0000${ref.url}`;
|
|
64
|
+
if (seen.has(key)) return;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
out.push({ kind, url: ref.url, blockId: blockId ?? null, inline: ref.inline });
|
|
67
|
+
};
|
|
68
|
+
for (const el of container.querySelectorAll("*")) {
|
|
69
|
+
const tag = el.tagName.toLowerCase();
|
|
70
|
+
const kind = RESOURCE_TAGS[tag];
|
|
71
|
+
if (kind) add(kind, el.getAttribute(SOURCE_ATTR[tag] ?? "src"), blockOf(el));
|
|
72
|
+
if (tag === "img" || tag === "source") {
|
|
73
|
+
for (const candidate of String(el.getAttribute("srcset") ?? "").split(",")) add("image", candidate.trim().split(/\s+/)[0], blockOf(el));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const el of container.querySelectorAll('[data-type="file"]')) {
|
|
77
|
+
add("attachment", el.getAttribute("data-src"), blockOf(el));
|
|
78
|
+
add("attachment", el.querySelector("a[href]")?.getAttribute("href"), blockOf(el));
|
|
79
|
+
}
|
|
80
|
+
for (const el of container.querySelectorAll('figure[data-type="chart"]')) {
|
|
81
|
+
const blockId = blockOf(el);
|
|
82
|
+
const key = `chart\u0000${blockId ?? ""}`;
|
|
83
|
+
if (seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
out.push({ kind: "chart", url: null, blockId: blockId ?? null, inline: true });
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The article as a reader gets it: no script, stylesheet or runtime attribute, and
|
|
91
|
+
// no link to a script URL. A resource the document needs was already checked.
|
|
92
|
+
function scrub(container) {
|
|
93
|
+
for (const el of [...container.querySelectorAll("script, style, link, meta, base")]) el.remove();
|
|
94
|
+
for (const el of container.querySelectorAll("*")) {
|
|
95
|
+
for (const { name, value } of [...el.attributes]) {
|
|
96
|
+
const attr = name.toLowerCase();
|
|
97
|
+
if (attr.startsWith("on") || attr === "clay" || attr === "merge" || attr === "live" || attr === "srcdoc" || attr.startsWith("data-clay") || attr.startsWith("clay-")) {
|
|
98
|
+
el.removeAttribute(name);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (/^(href|src|action|formaction|poster|data-src|xlink:href)$/.test(attr) && !carriable(squash(value))) el.removeAttribute(name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The table of contents reads as a nested list of its entries; its anchors point at
|
|
107
|
+
// a page that is not running here.
|
|
108
|
+
function plainToc(container) {
|
|
109
|
+
for (const nav of container.querySelectorAll('nav[data-type="toc"]')) {
|
|
110
|
+
for (const link of [...nav.querySelectorAll("a")]) link.replaceWith(link.ownerDocument.createTextNode(link.textContent));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const openToggles = (model) => model.map((item) => (item.t === "toggle" ? { ...item, open: true } : item));
|
|
115
|
+
|
|
116
|
+
const hasStatic = (html, clean) => {
|
|
117
|
+
const cleaned = clean.fallback(String(html ?? ""));
|
|
118
|
+
return STATIC_MEDIA.test(cleaned) || cleaned.replace(STATIC_TEXT, "").replace(/<[^>]+>/g, "").trim() !== "";
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const escText = (text) => String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
122
|
+
|
|
123
|
+
const READER_CSS = `:root { color-scheme: light dark; }
|
|
124
|
+
body { margin: 0; background: #ffffff; color: #37352f; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.6; }
|
|
125
|
+
article[data-page] { max-width: 46rem; margin: 0 auto; padding: 3rem 1.25rem 6rem; }
|
|
126
|
+
article[data-page] > * { margin: 0.7em 0; }
|
|
127
|
+
header[data-page-heading] { position: relative; margin-bottom: 1.6em; }
|
|
128
|
+
header[data-page-heading] img[data-page-cover] { display: block; width: 100%; max-height: 18rem; object-fit: cover; border-radius: 6px; margin-bottom: 1em; }
|
|
129
|
+
header[data-page-heading] span[data-page-icon] { display: block; font-size: 2.6em; line-height: 1.2; }
|
|
130
|
+
h1, h2, h3, h4, h5, h6 { line-height: 1.25; font-weight: 650; margin: 1.2em 0 0.3em; }
|
|
131
|
+
h1 { font-size: 2em; }
|
|
132
|
+
h2 { font-size: 1.5em; }
|
|
133
|
+
h3 { font-size: 1.2em; }
|
|
134
|
+
p, li { max-width: 100%; }
|
|
135
|
+
a { color: #0b6bcb; }
|
|
136
|
+
blockquote { margin-left: 0; padding-left: 1em; border-left: 3px solid rgba(55, 53, 47, 0.2); color: #5b5952; }
|
|
137
|
+
aside.page-callout { border: 1px solid rgba(55, 53, 47, 0.14); border-radius: 6px; padding: 0.8em 1em; background: rgba(55, 53, 47, 0.04); }
|
|
138
|
+
pre { overflow-x: auto; padding: 0.9em 1em; border-radius: 6px; background: rgba(55, 53, 47, 0.06); font-size: 0.9em; }
|
|
139
|
+
code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 0.92em; }
|
|
140
|
+
pre code { font-size: inherit; }
|
|
141
|
+
details { margin: 0.4em 0; }
|
|
142
|
+
details > summary { cursor: pointer; font-weight: 600; }
|
|
143
|
+
details > summary h1, details > summary h2, details > summary h3 { display: inline; margin: 0; }
|
|
144
|
+
hr { border: 0; border-top: 1px solid rgba(55, 53, 47, 0.18); }
|
|
145
|
+
table { border-collapse: collapse; width: 100%; }
|
|
146
|
+
th, td { border: 1px solid rgba(55, 53, 47, 0.18); padding: 0.4em 0.6em; text-align: left; vertical-align: top; }
|
|
147
|
+
th { background: rgba(55, 53, 47, 0.04); font-weight: 600; }
|
|
148
|
+
figure { margin: 1em 0; }
|
|
149
|
+
figure img { max-width: 100%; height: auto; border-radius: 4px; }
|
|
150
|
+
figcaption { font-size: 0.88em; color: #78766f; margin-top: 0.4em; }
|
|
151
|
+
figure[data-type="chart"] svg { max-width: 100%; height: auto; }
|
|
152
|
+
section[data-type="board"] { display: flex; gap: 1em; overflow-x: auto; align-items: flex-start; }
|
|
153
|
+
section[data-type="board"] > div { min-width: 12rem; border: 1px solid rgba(55, 53, 47, 0.14); border-radius: 6px; padding: 0.6em 0.8em; }
|
|
154
|
+
section[data-type="board"] h4 { margin: 0 0 0.4em; font-size: 0.95em; }
|
|
155
|
+
section[data-type="board"] ul { list-style: none; margin: 0; padding: 0; }
|
|
156
|
+
section[data-type="board"] li { border: 1px solid rgba(55, 53, 47, 0.14); border-radius: 4px; padding: 0.4em 0.5em; margin-bottom: 0.4em; }
|
|
157
|
+
nav[data-type="toc"] ul { list-style: none; padding-left: 0; }
|
|
158
|
+
nav[data-type="toc"] ul ul { padding-left: 1.2em; }
|
|
159
|
+
li > input[type="checkbox"] { margin-right: 0.4em; }
|
|
160
|
+
[data-type="file"] a { display: inline-block; padding: 0.4em 0.7em; border: 1px solid rgba(55, 53, 47, 0.18); border-radius: 4px; }
|
|
161
|
+
@media (prefers-color-scheme: dark) {
|
|
162
|
+
body { background: #191919; color: #d4d4d4; }
|
|
163
|
+
aside.page-callout, th, pre { background: rgba(255, 255, 255, 0.06); }
|
|
164
|
+
th, td, section[data-type="board"] > div, section[data-type="board"] li, [data-type="file"] a { border-color: rgba(255, 255, 255, 0.16); }
|
|
165
|
+
a { color: #6cb0f5; }
|
|
166
|
+
}`;
|
|
167
|
+
|
|
168
|
+
function standalone(title, body) {
|
|
169
|
+
return `<!DOCTYPE html>
|
|
170
|
+
<html lang="en">
|
|
171
|
+
<head>
|
|
172
|
+
<meta charset="utf-8">
|
|
173
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
174
|
+
<title>${escText(title)}</title>
|
|
175
|
+
<style>
|
|
176
|
+
${READER_CSS}
|
|
177
|
+
</style>
|
|
178
|
+
</head>
|
|
179
|
+
<body>
|
|
180
|
+
<article data-page="1">
|
|
181
|
+
${body}
|
|
182
|
+
</article>
|
|
183
|
+
</body>
|
|
184
|
+
</html>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* One readable HTML document for the page model, and the resources it references.
|
|
190
|
+
* An address the file cannot carry, or a tool with no static rendering, throws a
|
|
191
|
+
* PageExportError naming what stopped it rather than producing a truncated file.
|
|
192
|
+
*/
|
|
193
|
+
export function renderExportHtml(model, { title = null, fallbackOf = null, doc = globalThis.document } = {}) {
|
|
194
|
+
if (!Array.isArray(model)) throw new TypeError("page: an export renders a projection model");
|
|
195
|
+
const clean = fragmentCleaner(doc);
|
|
196
|
+
const fallback = fallbackOf ?? ((id) => model.find((item) => item.id === id)?.artifact?.fallbackHtml ?? "");
|
|
197
|
+
for (const item of model) {
|
|
198
|
+
if (item.t === "tool" && !hasStatic(fallback(item.id), clean)) {
|
|
199
|
+
fail("unsupported-tool", `Block ${item.id} holds a tool with no static rendering, which a reading copy cannot show`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const container = doc.createElement("div");
|
|
203
|
+
container.innerHTML = renderModel(openToggles(model), { fallbackOf: fallback, clean });
|
|
204
|
+
const resources = resourcesOf(container);
|
|
205
|
+
scrub(container);
|
|
206
|
+
plainToc(container);
|
|
207
|
+
const name = title ?? documentTitle(model);
|
|
208
|
+
return { html: standalone(name, container.innerHTML), title: name, resources };
|
|
209
|
+
}
|