@panphora/clayjs 1.17.0 → 1.18.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 +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-F449FF98.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
package/src/page/format-gate.js
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
// lands between `capabilities()` and `activate()` is refused rather than missed.
|
|
30
30
|
|
|
31
31
|
import { PAGE_FORMAT, PAGE_FORMAT_EXTENSION, PAGE_FORMAT_UPGRADE, formatCapability, notePageFormatFloor, pageFormatDecision, pageFormatFloor } from "./format-level.js";
|
|
32
|
+
import { registerUnsavedState } from "../lib/unsaved-state.js";
|
|
33
|
+
|
|
34
|
+
const PENDING_COMMAND = "page-format-command";
|
|
32
35
|
|
|
33
36
|
const UPGRADE_MESSAGE = "This document uses a newer page format; update the editor in the other tabs to keep writing";
|
|
34
37
|
|
|
@@ -46,6 +49,10 @@ export function createFormatGate({
|
|
|
46
49
|
let floor = 0;
|
|
47
50
|
let pending = null;
|
|
48
51
|
let lanePending = false;
|
|
52
|
+
let laneReceipt = null;
|
|
53
|
+
let laneAsked = false;
|
|
54
|
+
let laneJoin = Promise.resolve(false);
|
|
55
|
+
let laneJoining = null;
|
|
49
56
|
let disposed = false;
|
|
50
57
|
|
|
51
58
|
// The floor this gate knows the document at. A save or a room answer can tell
|
|
@@ -54,9 +61,22 @@ export function createFormatGate({
|
|
|
54
61
|
function note(level) {
|
|
55
62
|
floor = Math.max(floor, formatCapability(level));
|
|
56
63
|
notePageFormatFloor(floor);
|
|
64
|
+
if (floor >= PAGE_FORMAT) requestLane();
|
|
57
65
|
return floor;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
// A raised floor is one lane switch per tab, however this tab learned of it:
|
|
69
|
+
// its own accepted save (activateNow joins with that receipt), another tab's
|
|
70
|
+
// write that came in with the document, or a relay's meta. A switch the lane
|
|
71
|
+
// has not accepted yet stays pending, and the retry path covers it.
|
|
72
|
+
function requestLane() {
|
|
73
|
+
if (laneAsked || disposed) return laneJoin;
|
|
74
|
+
laneAsked = true;
|
|
75
|
+
lanePending = true;
|
|
76
|
+
laneJoin = joinLane(null);
|
|
77
|
+
return laneJoin;
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
async function read() {
|
|
61
81
|
try {
|
|
62
82
|
const answer = await host.read();
|
|
@@ -88,7 +108,10 @@ export function createFormatGate({
|
|
|
88
108
|
if (disposed) return { status: "disposed", retry: false };
|
|
89
109
|
if (!(await supportedHere())) return { status: "unsupported-host", code: "page-format-unsupported", extension: PAGE_FORMAT_EXTENSION };
|
|
90
110
|
await read();
|
|
91
|
-
if (floor >= PAGE_FORMAT)
|
|
111
|
+
if (floor >= PAGE_FORMAT) {
|
|
112
|
+
await requestLane();
|
|
113
|
+
return { status: "active", floor };
|
|
114
|
+
}
|
|
92
115
|
const admitted = await admittance();
|
|
93
116
|
const early = pageFormatDecision({ floor, required: PAGE_FORMAT, capability, admitted });
|
|
94
117
|
if (!early.ok) return upgrade(early.minimum);
|
|
@@ -106,28 +129,45 @@ export function createFormatGate({
|
|
|
106
129
|
if (disposed) {
|
|
107
130
|
// The save was accepted, so the document is at floor 1 either way; this
|
|
108
131
|
// tab is gone and must not act on it.
|
|
132
|
+
laneAsked = true;
|
|
109
133
|
note(PAGE_FORMAT);
|
|
110
134
|
return { status: "disposed", retry: false };
|
|
111
135
|
}
|
|
136
|
+
laneAsked = true;
|
|
112
137
|
note(PAGE_FORMAT);
|
|
113
138
|
lanePending = true;
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
laneReceipt = answer.receipt ?? null;
|
|
140
|
+
await joinLane(laneReceipt);
|
|
141
|
+
return { status: "active", floor, receipt: laneReceipt };
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
// Joining the new lane scope happens after the receipt is accepted, and only
|
|
119
145
|
// then: an interrupted switch leaves this tab on floor 0's scope with every
|
|
120
|
-
// store intact and the switch still pending, never a lane it cannot cover.
|
|
146
|
+
// store intact and the switch still pending, never a lane it cannot cover. The
|
|
147
|
+
// receipt is kept, so the retry of a refused switch is covered by the same write.
|
|
121
148
|
async function joinLane(receipt) {
|
|
122
149
|
if (!lane || !lanePending) return false;
|
|
150
|
+
if (laneJoining) return laneJoining;
|
|
151
|
+
const covering = receipt ?? laneReceipt;
|
|
152
|
+
laneJoining = (async () => {
|
|
153
|
+
try {
|
|
154
|
+
// The receipt is what says the pending work went with an accepted write.
|
|
155
|
+
const result = await lane.switchScope(PAGE_FORMAT, { covered: Boolean(covering), receipt: covering });
|
|
156
|
+
// "already" is the lane answering that this tab is on that scope: the
|
|
157
|
+
// join held, and there is nothing left pending.
|
|
158
|
+
if (result && (result.status === "switched" || result.status === "already")) {
|
|
159
|
+
lanePending = false;
|
|
160
|
+
laneReceipt = null;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
return false;
|
|
165
|
+
})();
|
|
123
166
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
} catch {}
|
|
130
|
-
return false;
|
|
167
|
+
return await laneJoining;
|
|
168
|
+
} finally {
|
|
169
|
+
laneJoining = null;
|
|
170
|
+
}
|
|
131
171
|
}
|
|
132
172
|
|
|
133
173
|
function activate() {
|
|
@@ -150,16 +190,25 @@ export function createFormatGate({
|
|
|
150
190
|
// same attempt, and a retry after a failure starts a new one.
|
|
151
191
|
activate,
|
|
152
192
|
// A new-format command: capture what it applies to, ask the gate, and apply
|
|
153
|
-
// only when activation succeeded and nothing moved underneath it.
|
|
193
|
+
// only when activation succeeded and nothing moved underneath it. While it waits on
|
|
194
|
+
// activation the work is unsaved and belongs to the person who asked for it: the
|
|
195
|
+
// exit asks the unsaved-state registry, and this command answers to its own name
|
|
196
|
+
// until it applies or fails, so an exit is refused rather than committing and
|
|
197
|
+
// disposing the editor underneath it.
|
|
154
198
|
async run(command) {
|
|
155
199
|
const stamp = { revision: revision(), installed: installed() };
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return { status: "
|
|
200
|
+
const unsaved = registerUnsavedState({ id: PENDING_COMMAND, isPending: () => true });
|
|
201
|
+
try {
|
|
202
|
+
const result = await activate();
|
|
203
|
+
if (result.status !== "active") return result;
|
|
204
|
+
if (disposed) return { status: "disposed", retry: false };
|
|
205
|
+
if (revision() !== stamp.revision || installed() !== stamp.installed) {
|
|
206
|
+
return { status: "retry", code: "format-gate-stale", retry: true };
|
|
207
|
+
}
|
|
208
|
+
return { status: "applied", floor, result: await command?.apply?.(stamp) };
|
|
209
|
+
} finally {
|
|
210
|
+
unsaved.dispose();
|
|
161
211
|
}
|
|
162
|
-
return { status: "applied", floor, result: await command?.apply?.(stamp) };
|
|
163
212
|
},
|
|
164
213
|
// Retry what is still pending: a lane switch whose receipt is in hand, or an
|
|
165
214
|
// activation that failed on the wire. Unsaved work is never part of this.
|
|
@@ -173,6 +222,7 @@ export function createFormatGate({
|
|
|
173
222
|
dispose() {
|
|
174
223
|
disposed = true;
|
|
175
224
|
lanePending = false;
|
|
225
|
+
laneReceipt = null;
|
|
176
226
|
return { status: "disposed" };
|
|
177
227
|
},
|
|
178
228
|
};
|
package/src/page/format-host.js
CHANGED
|
@@ -27,8 +27,21 @@ function servedFloor() {
|
|
|
27
27
|
// never uses a newer format never downloads the lane to raise one.
|
|
28
28
|
const savePage = async (options) => (await import("../core/save.js")).savePageForce(() => {}, options);
|
|
29
29
|
|
|
30
|
+
// A forced save asked for while another is on the wire is skipped rather than queued.
|
|
31
|
+
// The activation is the person's own request, so it waits for the save ahead of it to
|
|
32
|
+
// settle and asks again, instead of answering "try again" for a save it never sent.
|
|
33
|
+
const SKIPPED_ATTEMPTS = 4;
|
|
34
|
+
const SKIPPED_WAIT_MS = 10000;
|
|
35
|
+
const skippedBehindSave = (result) => result?.ok === false && result?.msgType === "skipped" && /in progress/i.test(result?.msg ?? "");
|
|
36
|
+
const saveSettles = async () => {
|
|
37
|
+
const { isSaveInProgress } = await import("../core/save-core.js");
|
|
38
|
+
const until = Date.now() + SKIPPED_WAIT_MS;
|
|
39
|
+
while (isSaveInProgress() && Date.now() < until) await new Promise((r) => setTimeout(r, 50));
|
|
40
|
+
};
|
|
41
|
+
|
|
30
42
|
export function createFormatHost({
|
|
31
43
|
save = savePage,
|
|
44
|
+
settles = saveSettles,
|
|
32
45
|
supported = () => hostPageFormat().then((floor) => floor === PAGE_FORMAT),
|
|
33
46
|
} = {}) {
|
|
34
47
|
return {
|
|
@@ -43,6 +56,10 @@ export function createFormatHost({
|
|
|
43
56
|
let result;
|
|
44
57
|
try {
|
|
45
58
|
result = await save({ pageFormatFloor: floor });
|
|
59
|
+
for (let attempt = 1; attempt < SKIPPED_ATTEMPTS && skippedBehindSave(result); attempt += 1) {
|
|
60
|
+
await settles();
|
|
61
|
+
result = await save({ pageFormatFloor: floor });
|
|
62
|
+
}
|
|
46
63
|
} catch (error) {
|
|
47
64
|
return { accepted: false, code: error?.code ?? FAILED };
|
|
48
65
|
}
|
|
@@ -12,6 +12,7 @@ import { history } from "../vendor/prosemirror.vendor.js";
|
|
|
12
12
|
import { schema } from "./schema.js";
|
|
13
13
|
import { PALETTE, markable } from "./format-native.js";
|
|
14
14
|
import { announce } from "./announce.js";
|
|
15
|
+
import toast from "../ui/toast-lazy.js";
|
|
15
16
|
import { openPageMenu } from "./chrome.js";
|
|
16
17
|
|
|
17
18
|
// blocks.js's STEP, read here rather than imported from it: this module is part
|
|
@@ -72,8 +73,11 @@ export function formatMark(view, name, attrs = {}, { reset = false } = {}) {
|
|
|
72
73
|
// The four formats pasted in from elsewhere are floor-1 content too: the slice is
|
|
73
74
|
// inserted only once the gate answered applied. A refusal keeps the text and
|
|
74
75
|
// writes no descriptor; a selection that moved while the answer came back takes
|
|
75
|
-
// neither
|
|
76
|
+
// neither, and neither does a document that changed underneath the paste: the
|
|
77
|
+
// plain fallback would write at a place a peer edit has moved, so the text stays
|
|
78
|
+
// in the clipboard instead.
|
|
76
79
|
export function pasteFormats(view, slice, from, to) {
|
|
80
|
+
const before = view.state.doc;
|
|
77
81
|
const place = () => {
|
|
78
82
|
if (view.isDestroyed) return false;
|
|
79
83
|
const st = view.state;
|
|
@@ -83,12 +87,12 @@ export function pasteFormats(view, slice, from, to) {
|
|
|
83
87
|
};
|
|
84
88
|
const plain = () => {
|
|
85
89
|
const text = slice.content.textBetween(0, slice.content.size, "\n", " ");
|
|
86
|
-
if (!view.isDestroyed && text) view.dispatch(view.state.tr.insertText(text, from, to));
|
|
90
|
+
if (!view.isDestroyed && text && view.state.doc === before) view.dispatch(view.state.tr.insertText(text, from, to));
|
|
87
91
|
return false;
|
|
88
92
|
};
|
|
89
93
|
runFormat(view, place).then((result) => {
|
|
90
94
|
if (result?.status === "applied") return;
|
|
91
|
-
|
|
95
|
+
refuse(view, result?.message ?? "That format isn\u2019t available here.");
|
|
92
96
|
plain();
|
|
93
97
|
}, plain);
|
|
94
98
|
}
|
|
@@ -106,7 +110,11 @@ export function quoteFromSpace(plugin, view, { at, from, to }, plain) {
|
|
|
106
110
|
view.dispatch(asStep(tr.setMeta(plugin, { transform: tr, from, to, text: " " })));
|
|
107
111
|
return true;
|
|
108
112
|
};
|
|
109
|
-
view.runFormatCommand(convert).then((result) => {
|
|
113
|
+
view.runFormatCommand(convert).then((result) => {
|
|
114
|
+
if (result?.status === "applied") return;
|
|
115
|
+
if (result) refuse(view, refusalText(result));
|
|
116
|
+
plain();
|
|
117
|
+
}, plain);
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
// The More menu's rows: the underline command, the palette of a color mark and
|
|
@@ -115,8 +123,16 @@ export function quoteFromSpace(plugin, view, { at, from, to }, plain) {
|
|
|
115
123
|
const CANNOT_MARK = "This text can\u2019t take that format.";
|
|
116
124
|
const refusalText = (result) => (result.status === "upgrade-required" ? result.message : result.code === "mark-not-allowed" ? CANNOT_MARK : "That format isn\u2019t available here.");
|
|
117
125
|
|
|
126
|
+
// A refusal is said to a screen reader and shown, since a sighted reader whose
|
|
127
|
+
// click did nothing otherwise has no way to know why.
|
|
128
|
+
export const refuse = (view, text) => {
|
|
129
|
+
if (view.isDestroyed) return;
|
|
130
|
+
announce(view, text);
|
|
131
|
+
toast(text, "error");
|
|
132
|
+
};
|
|
133
|
+
|
|
118
134
|
const notice = (view, result) => {
|
|
119
|
-
if (result?.status !== "applied"
|
|
135
|
+
if (result?.status !== "applied") refuse(view, refusalText(result ?? {}));
|
|
120
136
|
};
|
|
121
137
|
|
|
122
138
|
export const runNative = (view, name, attrs = {}, options = undefined) => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The image menu's own actions: the alt text field and the width presets, reached
|
|
2
|
+
// by the chrome's actions menu. Its own module, so a document that never sets
|
|
3
|
+
// either downloads neither.
|
|
4
|
+
import { asStep } from "./blocksel.js";
|
|
5
|
+
|
|
6
|
+
const indexOfId = (doc, id) => {
|
|
7
|
+
let found = -1;
|
|
8
|
+
doc.forEach((n, _, i) => { if (n.attrs.id === id) found = i; });
|
|
9
|
+
return found;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function setImageAttrs(view, id, patch) {
|
|
13
|
+
const st = view.state;
|
|
14
|
+
const index = indexOfId(st.doc, id);
|
|
15
|
+
if (index < 0) return false;
|
|
16
|
+
let at = 0;
|
|
17
|
+
for (let i = 0; i < index; i++) at += st.doc.child(i).nodeSize;
|
|
18
|
+
const node = st.doc.child(index);
|
|
19
|
+
view.dispatch(asStep(st.tr.setNodeMarkup(at, null, { ...node.attrs, ...patch })));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function openAltEditor(view, id) {
|
|
24
|
+
const index = indexOfId(view.state.doc, id);
|
|
25
|
+
if (index < 0) return false;
|
|
26
|
+
const shell = view.dom.parentElement;
|
|
27
|
+
const doc = shell.ownerDocument;
|
|
28
|
+
let at = 0;
|
|
29
|
+
for (let i = 0; i < index; i++) at += view.state.doc.child(i).nodeSize;
|
|
30
|
+
const box = doc.createElement("div");
|
|
31
|
+
box.className = "page-link";
|
|
32
|
+
box.dataset.mode = "alt";
|
|
33
|
+
const input = doc.createElement("input");
|
|
34
|
+
input.className = "page-link-input";
|
|
35
|
+
input.type = "text";
|
|
36
|
+
input.value = view.state.doc.child(index).attrs.alt || "";
|
|
37
|
+
input.placeholder = "Describe the image";
|
|
38
|
+
input.setAttribute("aria-label", "Alt text");
|
|
39
|
+
box.append(input);
|
|
40
|
+
const rect = view.nodeDOM(at)?.getBoundingClientRect?.();
|
|
41
|
+
const shellBox = shell.getBoundingClientRect();
|
|
42
|
+
if (rect) {
|
|
43
|
+
box.style.setProperty("--page-pop-x", `${rect.left - shellBox.left}px`);
|
|
44
|
+
box.style.setProperty("--page-pop-y", `${rect.bottom - shellBox.top + 6}px`);
|
|
45
|
+
}
|
|
46
|
+
shell.append(box);
|
|
47
|
+
let done = false;
|
|
48
|
+
const close = (commit, refocus = true) => {
|
|
49
|
+
if (done) return;
|
|
50
|
+
done = true;
|
|
51
|
+
const value = input.value.trim();
|
|
52
|
+
box.remove();
|
|
53
|
+
const i = indexOfId(view.state.doc, id);
|
|
54
|
+
if (commit && i >= 0 && value !== view.state.doc.child(i).attrs.alt) setImageAttrs(view, id, { alt: value });
|
|
55
|
+
if (refocus) view.focus();
|
|
56
|
+
};
|
|
57
|
+
input.addEventListener("keydown", (e) => {
|
|
58
|
+
if (e.key === "Enter") { e.preventDefault(); close(true); }
|
|
59
|
+
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); close(false); }
|
|
60
|
+
});
|
|
61
|
+
input.addEventListener("blur", () => close(true, false));
|
|
62
|
+
input.focus();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Markdown import: the tokens marked's lexer returns become the page's flat block
|
|
2
|
+
// records (model.js), so a file arrives without marked rendering anything. Raw HTML
|
|
3
|
+
// stays inert: a block of it is a preserved block, cleaned of scripts, event handlers
|
|
4
|
+
// and script URLs, and inline markup is literal text. A construct the schema cannot
|
|
5
|
+
// express is kept as a literal code block and named in `unsupported`; a link or
|
|
6
|
+
// image address the editor refuses is left out and named in `refused`, its text
|
|
7
|
+
// kept. Nothing is dropped, and a limit refuses with a code rather than mutating.
|
|
8
|
+
import { schema } from "./schema.js";
|
|
9
|
+
import { markKey, runsOf } from "./inline.js";
|
|
10
|
+
import { keysBetween, newId } from "./model.js";
|
|
11
|
+
import { fragmentCleaner } from "./projection.js";
|
|
12
|
+
import { safeHref, safeSrc } from "./address.js";
|
|
13
|
+
|
|
14
|
+
// HTML in a file someone sent is kept for its formatting, never for what it can load or
|
|
15
|
+
// run: anything that embeds or fetches another document goes, and so does every address
|
|
16
|
+
// that runs script, whichever attribute holds it.
|
|
17
|
+
const EMBEDS = "iframe, frame, frameset, object, embed, applet, meta, base, link, form, style, animate, set, animateMotion, animateTransform, portal";
|
|
18
|
+
const LOADERS = /^(srcdoc|data|formaction|codebase|archive|ping|background|poster|lowsrc|dynsrc)$/i;
|
|
19
|
+
const SCRIPTED = /^\s*(javascript|vbscript|data):/i;
|
|
20
|
+
function cleanImported(html) {
|
|
21
|
+
const t = document.createElement("template");
|
|
22
|
+
t.innerHTML = html;
|
|
23
|
+
for (const el of t.content.querySelectorAll(EMBEDS)) el.remove();
|
|
24
|
+
for (const el of t.content.querySelectorAll("*")) {
|
|
25
|
+
for (const { name, value } of [...el.attributes]) {
|
|
26
|
+
if (LOADERS.test(name) || SCRIPTED.test(value.replace(/[\u0000-\u0020]/g, ""))) el.removeAttribute(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return t.innerHTML;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const MAX_BYTES = 2 * 1024 * 1024;
|
|
33
|
+
export const MAX_DEPTH = 32;
|
|
34
|
+
export const MAX_BLOCKS = 10000;
|
|
35
|
+
|
|
36
|
+
export class MarkdownImportError extends Error {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "MarkdownImportError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ATOM = "\uFFFC";
|
|
45
|
+
const BREAK = { type: "hard_break" };
|
|
46
|
+
const BLOCK = new Set(["paragraph", "heading", "list", "blockquote", "code", "table", "hr", "html", "def", "space"]);
|
|
47
|
+
const encoder = new TextEncoder();
|
|
48
|
+
const M = schema.marks;
|
|
49
|
+
|
|
50
|
+
const sorted = (marks) => marks.slice().sort((a, b) => a.type.rank - b.type.rank);
|
|
51
|
+
|
|
52
|
+
function added(marks, name, attrs) {
|
|
53
|
+
if (marks.some((mark) => mark.type === M[name])) return marks;
|
|
54
|
+
return sorted([...marks, M[name].create(attrs)]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// One text block's inline content as the record keeps it: c, and m as runs (one
|
|
58
|
+
// key per UTF-16 unit, packed), exactly as flatten and runsOf write it; `parts`
|
|
59
|
+
// splits around an image, which the schema only carries as a block of its own.
|
|
60
|
+
function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
let c = "";
|
|
63
|
+
let m = [];
|
|
64
|
+
const flush = () => {
|
|
65
|
+
if (c) parts.push({ c, m });
|
|
66
|
+
c = "";
|
|
67
|
+
m = [];
|
|
68
|
+
};
|
|
69
|
+
const text = (value, marks) => {
|
|
70
|
+
const str = String(value ?? "");
|
|
71
|
+
if (!str) return;
|
|
72
|
+
const key = markKey(sorted(marks));
|
|
73
|
+
c += str;
|
|
74
|
+
for (let i = 0; i < str.length; i++) m.push(key);
|
|
75
|
+
};
|
|
76
|
+
const atom = (json) => { c += ATOM; m.push("@" + JSON.stringify(json)); };
|
|
77
|
+
const walk = (list, marks) => {
|
|
78
|
+
for (const token of list ?? []) {
|
|
79
|
+
const type = token?.type;
|
|
80
|
+
if (typeof type !== "string") throw new MarkdownImportError("malformed-input", "A Markdown token has no type.");
|
|
81
|
+
switch (type) {
|
|
82
|
+
case "text":
|
|
83
|
+
if (Array.isArray(token.tokens)) walk(token.tokens.filter((child) => typeof child?.type === "string" && !BLOCK.has(child.type)), marks);
|
|
84
|
+
else text(token.text, marks);
|
|
85
|
+
break;
|
|
86
|
+
case "escape": text(token.text, marks); break;
|
|
87
|
+
case "strong": walk(token.tokens, added(marks, "strong")); break;
|
|
88
|
+
case "em": walk(token.tokens, added(marks, "em")); break;
|
|
89
|
+
case "del":
|
|
90
|
+
case "strike": walk(token.tokens, added(marks, "strike")); break;
|
|
91
|
+
case "codespan": text(token.text, added(marks, "code")); break;
|
|
92
|
+
case "br": atom(BREAK); break;
|
|
93
|
+
case "link": {
|
|
94
|
+
const href = safeHref(token.href);
|
|
95
|
+
if (href) walk(token.tokens, added(marks, "link", { href }));
|
|
96
|
+
else {
|
|
97
|
+
refused.push({ type: "link", href: String(token.href ?? "") });
|
|
98
|
+
walk(token.tokens, marks);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "image": {
|
|
103
|
+
const image = { src: String(token.href ?? ""), alt: String(token.text ?? ""), title: token.title ?? "" };
|
|
104
|
+
if (!images) { text(image.alt, marks); break; }
|
|
105
|
+
flush();
|
|
106
|
+
parts.push({ image });
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
// Raw HTML is inert: its text stays text, never a node or a mark.
|
|
110
|
+
case "html": text(token.text ?? token.raw, marks); break;
|
|
111
|
+
case "checkbox": break;
|
|
112
|
+
default:
|
|
113
|
+
unsupported.push({ type, text: String(token.raw ?? token.text ?? "") });
|
|
114
|
+
text(token.raw ?? token.text, marks);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
walk(tokens, []);
|
|
120
|
+
flush();
|
|
121
|
+
return { parts };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function importMarkdownTokens(tokens) {
|
|
125
|
+
if (!Array.isArray(tokens)) throw new MarkdownImportError("malformed-input", "Markdown import needs the lexer's token list.");
|
|
126
|
+
let size = 0;
|
|
127
|
+
for (const token of tokens) {
|
|
128
|
+
if (!token || typeof token !== "object" || typeof token.type !== "string") throw new MarkdownImportError("malformed-input", "A Markdown token has no type.");
|
|
129
|
+
size += encoder.encode(String(token.raw ?? "")).length;
|
|
130
|
+
}
|
|
131
|
+
if (size > MAX_BYTES) throw new MarkdownImportError("input-too-large", `Markdown import is limited to ${MAX_BYTES} bytes; this file is ${size}.`);
|
|
132
|
+
|
|
133
|
+
const blocks = [];
|
|
134
|
+
const unsupported = [];
|
|
135
|
+
const refused = [];
|
|
136
|
+
const used = new Set();
|
|
137
|
+
const fresh = (prefix) => {
|
|
138
|
+
let id = newId(prefix);
|
|
139
|
+
while (used.has(id)) id = newId(prefix);
|
|
140
|
+
used.add(id);
|
|
141
|
+
return id;
|
|
142
|
+
};
|
|
143
|
+
const push = (value) => {
|
|
144
|
+
if (blocks.length + 1 > MAX_BLOCKS) throw new MarkdownImportError("too-many-blocks", `Markdown import is limited to ${MAX_BLOCKS} blocks.`);
|
|
145
|
+
blocks.push(value);
|
|
146
|
+
};
|
|
147
|
+
const deeper = (ctx) => {
|
|
148
|
+
const depth = ctx.depth + 1;
|
|
149
|
+
if (depth > MAX_DEPTH) throw new MarkdownImportError("nesting-too-deep", `Markdown import is limited to ${MAX_DEPTH} levels of nesting.`);
|
|
150
|
+
return depth;
|
|
151
|
+
};
|
|
152
|
+
const block = (t, attrs, { c, m }) => {
|
|
153
|
+
const value = { t, ...attrs, c };
|
|
154
|
+
if (m.some(Boolean)) value.m = runsOf(m);
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
const imageOf = (image) => {
|
|
158
|
+
const src = safeSrc(image.src);
|
|
159
|
+
if (src) return { t: "image", src, ...(image.alt ? { alt: image.alt } : {}), ...(image.title ? { caption: image.title } : {}) };
|
|
160
|
+
refused.push({ type: "image", href: image.src });
|
|
161
|
+
return image.alt ? block("paragraph", {}, { c: image.alt, m: [] }) : null;
|
|
162
|
+
};
|
|
163
|
+
const textBlocks = (t, attrs, inline) => {
|
|
164
|
+
for (const part of inline.parts) {
|
|
165
|
+
if (!part.image) { push(block(t, attrs, part)); continue; }
|
|
166
|
+
const value = imageOf(part.image);
|
|
167
|
+
if (value) push(value);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const paragraph = (inline, ctx) => textBlocks(ctx.quote ? "quote" : "paragraph", {}, inline);
|
|
171
|
+
const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, ...options });
|
|
172
|
+
const literal = (token) => {
|
|
173
|
+
const raw = String(token.raw ?? token.text ?? "");
|
|
174
|
+
unsupported.push({ type: token.type, text: raw });
|
|
175
|
+
push({ t: "code_block", lang: "markdown", c: raw });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const walk = (list, ctx) => {
|
|
179
|
+
for (const token of list ?? []) {
|
|
180
|
+
const type = token?.type;
|
|
181
|
+
if (typeof type !== "string") throw new MarkdownImportError("malformed-input", "A Markdown token has no type.");
|
|
182
|
+
switch (type) {
|
|
183
|
+
case "space": break;
|
|
184
|
+
case "heading": {
|
|
185
|
+
const level = Math.min(6, Math.max(1, Number.parseInt(token.depth, 10) || 1));
|
|
186
|
+
textBlocks("heading", level === 2 ? {} : { level }, inline(token.tokens, { images: false }));
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "paragraph":
|
|
190
|
+
case "text": paragraph(inline(token.tokens ?? [token]), ctx); break;
|
|
191
|
+
case "hr": push({ t: "divider" }); break;
|
|
192
|
+
case "code": push({ t: "code_block", ...(token.lang ? { lang: String(token.lang) } : {}), c: String(token.text ?? "") }); break;
|
|
193
|
+
case "html": {
|
|
194
|
+
const raw = String(token.text ?? token.raw ?? "");
|
|
195
|
+
unsupported.push({ type: "HTML block cleaned", text: raw });
|
|
196
|
+
push({ t: "preserved", html: cleanImported(fragmentCleaner(document).fallback(raw)) });
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case "blockquote": {
|
|
200
|
+
const depth = deeper(ctx);
|
|
201
|
+
walk(token.tokens, { ...ctx, depth, quote: true });
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "list": {
|
|
205
|
+
const depth = deeper(ctx);
|
|
206
|
+
const level = ctx.indent ?? 0;
|
|
207
|
+
const kind = token.ordered ? "ordered" : "bullet";
|
|
208
|
+
for (const item of token.items ?? []) {
|
|
209
|
+
let own = { c: "", m: [] };
|
|
210
|
+
const rest = [];
|
|
211
|
+
for (const child of item?.tokens ?? []) {
|
|
212
|
+
const childType = child?.type;
|
|
213
|
+
if (childType === "checkbox" || childType === "space") continue;
|
|
214
|
+
if (childType === "text" || childType === "paragraph") {
|
|
215
|
+
if (own.c) own = { c: own.c + ATOM, m: [...own.m, "@" + JSON.stringify(BREAK)] };
|
|
216
|
+
const tokens = childType === "paragraph" ? child.tokens ?? [] : [child];
|
|
217
|
+
for (const part of inline(tokens, { images: false }).parts) {
|
|
218
|
+
own = { c: own.c + part.c, m: [...own.m, ...part.m] };
|
|
219
|
+
}
|
|
220
|
+
rest.push(...(child.tokens ?? []).filter((nested) => typeof nested?.type === "string" && BLOCK.has(nested.type) && nested.type !== "space"));
|
|
221
|
+
} else rest.push(child);
|
|
222
|
+
}
|
|
223
|
+
const itemKind = item?.task ? "check" : kind;
|
|
224
|
+
const attrs = {
|
|
225
|
+
...(itemKind === "bullet" ? {} : { kind: itemKind }),
|
|
226
|
+
...(level ? { indent: level } : {}),
|
|
227
|
+
...(item?.task && item.checked ? { checked: true } : {}),
|
|
228
|
+
};
|
|
229
|
+
push(block("list_item", attrs, own));
|
|
230
|
+
if (rest.length) walk(rest, { ...ctx, depth, indent: level + 1, quote: false });
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "table": {
|
|
235
|
+
const header = token.header ?? [];
|
|
236
|
+
if (!header.length) { literal(token); break; }
|
|
237
|
+
const cell = (tokens) => inline(tokens, { images: false }).parts[0] ?? { c: "", m: [] };
|
|
238
|
+
const colIds = header.map(() => fresh("c"));
|
|
239
|
+
const colPos = keysBetween(null, null, header.length);
|
|
240
|
+
const cols = Object.fromEntries(colIds.map((id, i) => [id, { pos: colPos[i], name: cell(header[i]?.tokens).c }]));
|
|
241
|
+
// The header line is the column names, drawn as <thead>; the body holds the data rows.
|
|
242
|
+
const rows = token.rows?.length ? token.rows : [[]];
|
|
243
|
+
const rowPos = keysBetween(null, null, rows.length);
|
|
244
|
+
const values = rows.map((row) => Object.fromEntries(colIds.map((id, i) => {
|
|
245
|
+
const part = cell(row?.[i]?.tokens);
|
|
246
|
+
return [id, part.m.some(Boolean) ? { c: part.c, m: runsOf(part.m) } : part.c];
|
|
247
|
+
})));
|
|
248
|
+
push({ t: "table", cols, rows: Object.fromEntries(values.map((cells, i) => [fresh("r"), { pos: rowPos[i], cells }])) });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
default: literal(token); break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
walk(tokens, { depth: 0, indent: 0, quote: false });
|
|
257
|
+
|
|
258
|
+
const positions = keysBetween(null, null, blocks.length);
|
|
259
|
+
const out = {};
|
|
260
|
+
blocks.forEach((value, i) => { out[fresh("b")] = { ...value, pos: positions[i] }; });
|
|
261
|
+
return { blocks: out, unsupported, refused };
|
|
262
|
+
}
|