@panphora/clayjs 1.16.0 → 1.17.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 +7917 -5621
- package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
- package/dist/m/c-2YYDMLNZ.js +1 -0
- package/dist/m/c-336SGU5W.js +1 -0
- package/dist/m/c-3H6BZDNJ.js +6 -0
- package/dist/m/{c-LR4CGEPT.js → c-3RUU4TPX.js} +1 -1
- package/dist/m/c-3XIRD4NU.js +1 -0
- package/dist/m/c-4DFEJ2PO.js +3 -0
- package/dist/m/c-56QRZTZH.js +1 -0
- package/dist/m/c-5J4E7O4Y.js +4 -0
- package/dist/m/c-5LKDWQTL.js +19 -0
- package/dist/m/c-5QWQ7A5L.js +1 -0
- package/dist/m/c-5RJFTLSF.js +1 -0
- package/dist/m/c-6AZOGAPE.js +1 -0
- package/dist/m/{c-Y6USGTS5.js → c-6CRUUXOX.js} +5 -5
- package/dist/m/{c-54RCDCBU.js → c-6KEVEQ7L.js} +1 -1
- package/dist/m/{c-LTGMXXKO.js → c-6NC5I2TV.js} +1 -1
- package/dist/m/{c-YDH7Q3NY.js → c-74MGYWZF.js} +1 -1
- package/dist/m/{c-Z36J63PP.js → c-7OLLJPWP.js} +1 -1
- package/dist/m/c-A3E3NIMC.js +1 -0
- package/dist/m/{c-5R4PTRG2.js → c-AMSNPOCX.js} +1 -1
- package/dist/m/c-B3JM5ZXC.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/c-BQ6LOBEE.js +1 -0
- package/dist/m/c-CJ3A7ZKE.js +1 -0
- package/dist/m/c-CQHPGKX2.js +1 -0
- package/dist/m/c-DCXOXBF3.js +1 -0
- package/dist/m/c-DFS4XZJV.js +2 -0
- package/dist/m/{c-F3CKJTJS.js → c-DNENWY4O.js} +1 -1
- package/dist/m/c-E4LIR5X5.js +1 -0
- package/dist/m/c-EVCQ3BKA.js +1 -0
- package/dist/m/{c-5Y6TNX5W.js → c-EZU56NQV.js} +2 -2
- package/dist/m/c-FDNHHZY5.js +1 -0
- package/dist/m/{c-SWRJFCNN.js → c-FRLHSY7E.js} +1 -1
- package/dist/m/c-FROJUZMV.js +1 -0
- package/dist/m/c-G2JPKCTP.js +1 -0
- package/dist/m/c-GB6TU7WV.js +1 -0
- package/dist/m/{c-GB3JTV6E.js → c-GFJJEBTU.js} +1 -1
- package/dist/m/{c-J7CA54IP.js → c-GSFIGKOP.js} +1 -1
- package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
- package/dist/m/c-HAWFNBYG.js +1 -0
- package/dist/m/{c-33HUYEDI.js → c-HDDEEL77.js} +1 -1
- package/dist/m/c-HFKUVACH.js +1 -0
- package/dist/m/c-HHDFCNHV.js +1 -0
- package/dist/m/c-I2ZATQCD.js +2 -0
- package/dist/m/c-IEM4NTCS.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/{c-SGCCC4FW.js → c-JDCX26UP.js} +2 -2
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-JU3NK25H.js +1 -0
- package/dist/m/{c-VFX45IZQ.js → c-KAU6E43D.js} +1 -1
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGVGROZT.js +1 -0
- package/dist/m/{c-YG2NJYS6.js → c-KQ6TYJAP.js} +2 -2
- package/dist/m/{c-2JL6E75N.js → c-KU3ZFADW.js} +1 -1
- package/dist/m/c-LWLBHLVC.js +3 -0
- package/dist/m/c-M2L47S2X.js +7 -0
- package/dist/m/c-MFP2ZWD4.js +1 -0
- package/dist/m/{c-4YSCRICO.js → c-MKSQ4Y57.js} +1 -1
- package/dist/m/{c-2M73252O.js → c-NUFFJVVJ.js} +1 -1
- package/dist/m/c-NUHSPT5P.js +1 -0
- package/dist/m/c-O72HVYSA.js +1 -0
- package/dist/m/c-OW5QIGG3.js +1 -0
- package/dist/m/c-PGRCK73F.js +1 -0
- package/dist/m/{c-4DRMS4QG.js → c-PS5ARVWR.js} +1 -1
- package/dist/m/c-Q622NS2Y.js +1 -0
- package/dist/m/c-Q7NQD65V.js +1 -0
- package/dist/m/c-QH63LQOS.js +1 -0
- package/dist/m/{c-TQZ2JYHD.js → c-QICP2YR5.js} +1 -1
- package/dist/m/{c-7VGZ7RU5.js → c-QJ4OM3G6.js} +1 -1
- package/dist/m/{c-MMUIUUZG.js → c-R3L7ZJGB.js} +1 -1
- package/dist/m/c-R7IWVINQ.js +1 -0
- package/dist/m/{c-HVYXIS72.js → c-RSMOODDU.js} +1 -1
- package/dist/m/{c-KKWLDOPM.js → c-RYUXUUBX.js} +1 -1
- package/dist/m/c-S4ZEH46B.js +9 -0
- package/dist/m/c-SDAXDM2D.js +1 -0
- package/dist/m/c-SMBZG5X4.js +1 -0
- package/dist/m/{c-UXBMRDZB.js → c-SMGU7RT3.js} +1 -1
- package/dist/m/c-SPIORRYS.js +1 -0
- package/dist/m/c-SWLRT66R.js +1 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-TCJTVAFN.js +1 -0
- package/dist/m/{c-HRDCD3WN.js → c-TJ6WOJL3.js} +1 -1
- package/dist/m/c-TYLXX4NF.js +21 -0
- package/dist/m/c-U62OB4BT.js +1 -0
- package/dist/m/{c-NMJRXKPC.js → c-U6YWUAR3.js} +1 -1
- package/dist/m/c-UDCSU5G5.js +2 -0
- package/dist/m/{c-NZMNXVF6.js → c-UDUUJPFZ.js} +1 -1
- package/dist/m/c-VE6ZMYMC.js +1 -0
- package/dist/m/c-VG5XIN4T.js +1 -0
- package/dist/m/c-VHM5XQHK.js +1 -0
- package/dist/m/c-VI5RMSVY.js +1 -0
- package/dist/m/c-VRZN56XY.js +1 -0
- package/dist/m/c-W5MCHEGA.js +1 -0
- package/dist/m/c-WDC5GVKU.js +1 -0
- package/dist/m/c-WWXIDH4O.js +28 -0
- package/dist/m/{c-QIX5BJYN.js → c-XCNTK76U.js} +1 -1
- package/dist/m/c-XMS6DPBR.js +1 -0
- package/dist/m/c-Y5HBBIQ4.js +1 -0
- package/dist/m/{c-5M4YEKCX.js → c-ZVPVXTJF.js} +1 -1
- package/dist/m/c-ZVRKXKM7.js +1 -0
- package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
- package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
- package/dist/m/core/autosave-HRYGUWV6.js +1 -0
- package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
- package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
- package/dist/m/core/{persist-HKHA66IY.js → persist-JFYRHMRJ.js} +1 -1
- package/dist/m/core/recovery-FLTCC2OC.js +1 -0
- package/dist/m/core/save-HQZ4S667.js +1 -0
- package/dist/m/core/save-core-KGQWFUNH.js +1 -0
- package/dist/m/core/snapshot-EGQZNT45.js +1 -0
- package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
- package/dist/m/events/index-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-PVXGPUDA.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- package/dist/m/loader-S4I7GKXR.js +1 -0
- package/dist/m/manifest-4C419D67.js +1 -0
- package/dist/m/manifest-edit-B077F288.js +1 -0
- package/dist/m/manifest.json +1555 -1058
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/{editor-XUTB2HB2.js → editor-NNJA4OKD.js} +21 -8
- package/dist/m/plugins/{ai-edit-ZQ4CGWIP.js → ai-edit-QN4PMMNL.js} +1 -1
- package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
- package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
- package/dist/m/plugins/live-HFWD4OIH.js +1 -0
- package/dist/m/plugins/{menu-HCKO3DSP.js → menu-3HIEQSI6.js} +2 -2
- package/dist/m/plugins/page-JOKB6XPR.js +2 -0
- package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
- package/dist/m/plugins/people-SGPJKANK.js +1 -0
- package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-NRQDSWX6.js} +2 -2
- package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
- package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
- package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
- package/dist/m/plugins/{upgrade-ZNYODP5Y.js → upgrade-W7FG5VFG.js} +1 -1
- package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
- package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
- package/dist/m/{reader-core-ILWLXVMP.js → reader-core-DXYCURFF.js} +2 -2
- package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
- package/dist/m/ui/{index-EVHKMKBL.js → index-3JLVI27O.js} +1 -1
- package/dist/offline-manifest.json +857 -623
- package/dist/page/themes/light-1.css +25 -1
- package/dist/server/core.cjs +115 -24
- package/package.json +1 -1
- package/src/core/boot-phase.js +2 -1
- package/src/core/conflict-download.js +3 -29
- package/src/core/download.js +32 -0
- package/src/core/host-meta.js +18 -0
- package/src/core/save-core.js +8 -4
- package/src/core/save.js +4 -4
- package/src/core/snapshot.js +39 -1
- package/src/lib/autosave-scheduler.js +3 -4
- package/src/page/blocks.js +34 -1
- package/src/page/board-views.js +2 -16
- package/src/page/chart-tables.js +11 -0
- package/src/page/chart.js +6 -10
- package/src/page/checkpoint-proof.js +289 -0
- package/src/page/chrome.js +24 -6
- package/src/page/document-title-dom.js +12 -0
- package/src/page/document-title-runtime.js +15 -1
- package/src/page/document-title.js +4 -1
- package/src/page/editor.js +77 -12
- package/src/page/emoji-data.js +162 -0
- package/src/page/features/board.js +12 -3
- package/src/page/features/chart.js +11 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -0
- package/src/page/features/mentions.js +188 -0
- package/src/page/features/registry.js +6 -1
- package/src/page/features/table.js +2 -2
- package/src/page/format-command.js +9 -0
- package/src/page/format-host.js +54 -0
- package/src/page/format-native-commands.js +142 -0
- package/src/page/format-native.js +42 -0
- package/src/page/inline.js +32 -8
- package/src/page/items.js +15 -3
- package/src/page/markdown.js +26 -2
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/projection.js +58 -15
- package/src/page/save-projection.js +39 -0
- package/src/page/save-states.js +1 -1
- package/src/page/schema.js +33 -0
- package/src/page/themes/content.css +25 -1
- package/src/page/themes/light-1.css +25 -1
- package/src/page/view-hooks.js +32 -0
- package/src/page/ybind/binding.js +31 -4
- package/src/page/ybind/marks.js +2 -1
- package/src/page/ybind/provider.js +13 -2
- package/src/page/ybind/recovery-replay.js +362 -4
- package/src/plugins/live.js +37 -1
- package/src/plugins/page.js +117 -3
- package/src/plugins/visitor-drafts/intro.js +2 -1
- package/src/server/checkpoint/index.js +2 -2
- package/src/sync/saved-stream.js +17 -8
- package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
- package/dist/m/c-35NBGMZB.js +0 -1
- package/dist/m/c-3IQRECYS.js +0 -1
- package/dist/m/c-4B6H4G5V.js +0 -1
- package/dist/m/c-4FFHZM32.js +0 -1
- package/dist/m/c-66O32MV3.js +0 -28
- package/dist/m/c-7O6AFR65.js +0 -1
- package/dist/m/c-ANQTJ3B5.js +0 -2
- package/dist/m/c-AYVOREBE.js +0 -1
- package/dist/m/c-B2ZXPOS3.js +0 -3
- package/dist/m/c-BMIZKMPV.js +0 -1
- package/dist/m/c-CJZSLMVQ.js +0 -1
- package/dist/m/c-DBE6ORSI.js +0 -1
- package/dist/m/c-EDHUR4MX.js +0 -1
- package/dist/m/c-EY6UOM54.js +0 -1
- package/dist/m/c-F7Y6FLPY.js +0 -1
- package/dist/m/c-FA4DZVTC.js +0 -1
- package/dist/m/c-G2BUZITR.js +0 -1
- package/dist/m/c-GBGWXTX4.js +0 -1
- package/dist/m/c-GHBCCVG4.js +0 -1
- package/dist/m/c-IB52R237.js +0 -1
- package/dist/m/c-IKWNE4R4.js +0 -1
- package/dist/m/c-IU3IYEU6.js +0 -1
- package/dist/m/c-IZ2EBRAJ.js +0 -1
- package/dist/m/c-KR4UW4KW.js +0 -1
- package/dist/m/c-KY3VJGUA.js +0 -8
- package/dist/m/c-LXENEBV7.js +0 -1
- package/dist/m/c-PPOSCAQD.js +0 -1
- package/dist/m/c-QQHEX6HA.js +0 -1
- package/dist/m/c-R7RMANJI.js +0 -9
- package/dist/m/c-RI4NT75Y.js +0 -1
- package/dist/m/c-RUJKSUQW.js +0 -1
- package/dist/m/c-T4VO4OCB.js +0 -1
- package/dist/m/c-TR6RMDQ7.js +0 -3
- package/dist/m/c-UKHTLWUW.js +0 -1
- package/dist/m/c-UNN7LWCT.js +0 -1
- package/dist/m/c-V7PZZRG7.js +0 -1
- package/dist/m/c-VL52ADDR.js +0 -1
- package/dist/m/c-WWDBYXLK.js +0 -1
- package/dist/m/c-XNNW2CWK.js +0 -1
- package/dist/m/c-Z4VRC7GU.js +0 -7
- package/dist/m/c-Z6K52OLP.js +0 -19
- package/dist/m/c-ZDY2MDAA.js +0 -21
- package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
- package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
- package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
- package/dist/m/core/recovery-TBBSICE2.js +0 -1
- package/dist/m/core/save-WUCHTQBE.js +0 -1
- package/dist/m/core/save-core-FJVHT5M3.js +0 -1
- package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
- package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
- package/dist/m/events/index-23DPTUT4.js +0 -1
- package/dist/m/internals/index-WOVAP3RU.js +0 -1
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
- package/dist/m/loader-HXX6YNH5.js +0 -1
- package/dist/m/manifest-F22DEDAC.js +0 -1
- package/dist/m/manifest-edit-C3C7376B.js +0 -1
- package/dist/m/options/options-2LH76JQ3.js +0 -1
- package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
- package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
- package/dist/m/plugins/live-NUTTXSBS.js +0 -1
- package/dist/m/plugins/page-KSUH7G6J.js +0 -2
- package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
- package/dist/m/plugins/people-AJJ32YCG.js +0 -1
- package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
- package/dist/m/plugins/source-KHK5UUSK.js +0 -1
- package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
- package/dist/m/plugins/upload-XSGNILUY.js +0 -1
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
- package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
- package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
- /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-NRQDSWX6.js.LEGAL.txt} +0 -0
package/src/page/blocks.js
CHANGED
|
@@ -8,6 +8,7 @@ import { REMOTE, newId } from "./model.js";
|
|
|
8
8
|
import { safeHref, safeSrc } from "./link.js";
|
|
9
9
|
import { flatten, runsOf, unflatten, unitsOf } from "./inline.js";
|
|
10
10
|
import { OPAQUE_MARK, opaqueMark } from "./format-codec.js";
|
|
11
|
+
import { NATIVE_NAMES, nativeAttrs } from "./format-native.js";
|
|
11
12
|
import { DEFAULTS, MARKS, OPAQUE, envelopeAttrs, envelopeRecord, marksAttrs, marksEntries } from "./projection.js";
|
|
12
13
|
|
|
13
14
|
const alignOf = (a) => (a === "left" || a === "right" ? a : null);
|
|
@@ -1118,6 +1119,10 @@ export const clipboardParser = new model.DOMParser(parseSchema, [
|
|
|
1118
1119
|
{ tag: "del", mark: "strike" },
|
|
1119
1120
|
{ tag: "code", mark: "code" },
|
|
1120
1121
|
{ tag: "a[href]", mark: "link", getAttrs: (d) => { const href = safeHref(d.getAttribute("href")); return href ? { href } : false; } },
|
|
1122
|
+
{ tag: "blockquote", node: "quote", getAttrs: (d) => ({ parent: parentAttr(d) }) },
|
|
1123
|
+
{ tag: "u", mark: "underline" },
|
|
1124
|
+
{ tag: "mark", mark: "highlight", getAttrs: (d) => nativeAttrs("highlight", { color: d.getAttribute("data-color") }) ?? false },
|
|
1125
|
+
{ tag: "span[data-color]", mark: "text_color", getAttrs: (d) => nativeAttrs("text_color", { color: d.getAttribute("data-color") }) ?? false },
|
|
1121
1126
|
]);
|
|
1122
1127
|
|
|
1123
1128
|
// Every pasted block arrives with no id and no pos (identityPlugin mints and
|
|
@@ -1202,10 +1207,38 @@ function pasteIntoClosedSummary(view, _event, slice) {
|
|
|
1202
1207
|
return true;
|
|
1203
1208
|
}
|
|
1204
1209
|
|
|
1210
|
+
// The four formats pasted in from elsewhere are floor-1 content too: a slice
|
|
1211
|
+
// that holds one asks the gate before it is inserted, and text waits in the
|
|
1212
|
+
// clipboard rather than being dropped when the answer is a refusal.
|
|
1213
|
+
function writesFormat(slice) {
|
|
1214
|
+
let found = false;
|
|
1215
|
+
slice?.content?.descendants((node) => {
|
|
1216
|
+
if (found) return false;
|
|
1217
|
+
if (node.type === N.quote) found = true;
|
|
1218
|
+
else if (node.isText && node.marks.some((mark) => NATIVE_NAMES.has(mark.type.name))) found = true;
|
|
1219
|
+
return !found;
|
|
1220
|
+
});
|
|
1221
|
+
return found;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function pasteFormat(view, event, slice) {
|
|
1225
|
+
const from = view.state.selection.from, to = view.state.selection.to;
|
|
1226
|
+
const plain = () => {
|
|
1227
|
+
const text = slice.content.textBetween(0, slice.content.size, "\n", " ");
|
|
1228
|
+
if (!view.isDestroyed && text) view.dispatch(view.state.tr.insertText(text, from, to));
|
|
1229
|
+
return false;
|
|
1230
|
+
};
|
|
1231
|
+
import("./format-native-commands.js").then(({ pasteFormats }) => pasteFormats(view, slice, from, to), plain);
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1205
1235
|
export function pastePlugin() {
|
|
1206
1236
|
return new Plugin({
|
|
1207
1237
|
props: {
|
|
1208
|
-
handlePaste
|
|
1238
|
+
handlePaste(view, event, slice) {
|
|
1239
|
+
if (writesFormat(slice)) return pasteFormat(view, event, slice);
|
|
1240
|
+
return pasteIntoClosedSummary(view, event, slice);
|
|
1241
|
+
},
|
|
1209
1242
|
clipboardSerializer,
|
|
1210
1243
|
transformPastedHTML: (html) => flattenPastedHTML(html),
|
|
1211
1244
|
clipboardParser,
|
package/src/page/board-views.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// The card panels a board plugin can reach, filled by the board view part when it
|
|
4
|
-
// loads. The plugin works, and does nothing, for a board without a panel.
|
|
1
|
+
// The card panels a board part can reach, filled by the board view part when it
|
|
2
|
+
// loads. A board without a panel does nothing on every change.
|
|
5
3
|
export const cardPanels = new WeakMap();
|
|
6
|
-
|
|
7
|
-
export function boardPlugin() {
|
|
8
|
-
return new state.Plugin({
|
|
9
|
-
view: (view) => ({
|
|
10
|
-
update: () => cardPanels.get(view)?.sync(),
|
|
11
|
-
destroy: () => {
|
|
12
|
-
cardPanels.get(view)?.hide(false, false);
|
|
13
|
-
cardPanels.delete(view);
|
|
14
|
-
},
|
|
15
|
-
}),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { schema } from "./schema.js";
|
|
2
|
+
import { orderedIds } from "./model.js";
|
|
3
|
+
|
|
4
|
+
// The tables a chart can be drawn from: a top-level table with at least one
|
|
5
|
+
// column. The insert menu reads this without the chart module itself, which only
|
|
6
|
+
// a document that takes a chart loads.
|
|
7
|
+
export function chartTables(doc) {
|
|
8
|
+
const tables = [];
|
|
9
|
+
doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
|
|
10
|
+
return tables;
|
|
11
|
+
}
|
package/src/page/chart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { state } from "../vendor/prosemirror.vendor.js";
|
|
2
1
|
import { schema } from "./schema.js";
|
|
3
2
|
import { blockJson, keyBetween, newId, orderedIds } from "./model.js";
|
|
3
|
+
import { chartTables } from "./chart-tables.js";
|
|
4
4
|
import { chartModel, renderChartSvg } from "./rich/chart.js";
|
|
5
5
|
import { openPageMenu } from "./chrome.js";
|
|
6
6
|
import { project } from "./projection.js";
|
|
@@ -50,11 +50,7 @@ export function insertChartAfterTable(view, tablePos) {
|
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export
|
|
54
|
-
const tables = [];
|
|
55
|
-
doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
|
|
56
|
-
return tables;
|
|
57
|
-
}
|
|
53
|
+
export { chartTables };
|
|
58
54
|
|
|
59
55
|
// /chart: an empty line becomes the chart, otherwise it goes below the line.
|
|
60
56
|
// One table is charted at once; several open a picker named by their columns.
|
|
@@ -213,8 +209,8 @@ export class ChartView {
|
|
|
213
209
|
}
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
212
|
+
// The hook the chart view part registers for its editor view: every change can
|
|
213
|
+
// have moved a chart's source table.
|
|
214
|
+
export function refreshCharts(view) {
|
|
215
|
+
for (const chart of views.get(view) ?? []) chart.refresh();
|
|
220
216
|
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint-proof.js — the proof one accepted save produces, and the conditional read
|
|
3
|
+
* that answers for bytes this tab did not write.
|
|
4
|
+
*
|
|
5
|
+
* Step 2 built the proof itself: the recorded revisions, the accepted page state, the
|
|
6
|
+
* final article and the receipt, checked by `validatePageCheckpoint` before a store
|
|
7
|
+
* retires anything. What it left here is the producer's half. A proof is made at the
|
|
8
|
+
* accepted-save event, from the page state pinned to THAT capture, never from the live
|
|
9
|
+
* store: an edit made after the capture is in the store and is not in the bytes the host
|
|
10
|
+
* took, and reading the store when the response lands would cover it and clear it.
|
|
11
|
+
*
|
|
12
|
+
* Checklist item 2 lives here too. A state that no longer projects to the article that
|
|
13
|
+
* went to disk proves nothing about those bytes, so an authored transform that changed
|
|
14
|
+
* meaning, a page rebuilt from another article or a stale tag all produce no proof, and
|
|
15
|
+
* the records stay.
|
|
16
|
+
*
|
|
17
|
+
* The second half is the conditional page-state client: the host's own copy of the page
|
|
18
|
+
* state, asked for by the accepted ETag, for the cases a local capture cannot answer —
|
|
19
|
+
* the server's checkpoint, another writer's, and a lost reply that only a verified
|
|
20
|
+
* refetch can reconcile. Nothing here reads a cookie, a room sequence or a version
|
|
21
|
+
* string: a 412, a 428 or a 404 keeps every record, and so does any state whose
|
|
22
|
+
* identity, generation, digest or projection disagrees with what was accepted.
|
|
23
|
+
*
|
|
24
|
+
* It loads only when a recovery channel is registered: nothing else imports it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { saveToken } from "../core/host-attrs.js";
|
|
28
|
+
import { setCapturePageState } from "../core/snapshot.js";
|
|
29
|
+
import { canonicalPageBytes, digestHex, pageProof, validatePageCheckpoint } from "./ybind/recovery-proof.js";
|
|
30
|
+
import { articleOfPageState, pageStateOfClone } from "./save-projection.js";
|
|
31
|
+
|
|
32
|
+
// The capture half. A save capture carries its page state only once something can prove
|
|
33
|
+
// it, so the reader is installed here rather than from the page bundle: a page that
|
|
34
|
+
// journals nothing never pays for it, and a page that does gets the state of the exact
|
|
35
|
+
// clone whose bytes it sends, after every transform.
|
|
36
|
+
setCapturePageState(pageStateOfClone);
|
|
37
|
+
|
|
38
|
+
/** The only page-state resource version this client reads. */
|
|
39
|
+
export const PAGE_STATE_VERSION = 1;
|
|
40
|
+
|
|
41
|
+
export class PageCheckpointError extends Error {
|
|
42
|
+
constructor(code, message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "PageCheckpointError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fail = (code, message) => { throw new PageCheckpointError(code, message); };
|
|
50
|
+
const refused = (code, message) => ({ ok: false, code, message });
|
|
51
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
52
|
+
const GENERATION = /^[0-9a-f]{32}$/;
|
|
53
|
+
|
|
54
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
|
|
56
|
+
// The state as this tab holds it, copied: a pinned capture's value shares its `y` map
|
|
57
|
+
// with the store's at the moment of the copy and must not share it after.
|
|
58
|
+
function copyState(state) {
|
|
59
|
+
return { v: state.v, g: state.g, y: { ...(state.y ?? {}) } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The state the bytes are decoded from, strictly, and its canonical form.
|
|
63
|
+
function stateOfTag(tag) {
|
|
64
|
+
let state;
|
|
65
|
+
try { state = JSON.parse(tag); }
|
|
66
|
+
catch (_) { fail("invalid-state", "The page state is not JSON"); }
|
|
67
|
+
if (!isObject(state) || state.v !== 1) fail("invalid-state", "Expected a version 1 Yjs page value");
|
|
68
|
+
return { state: copyState(state), bytes: canonicalPageBytes(state) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const idKey = (value) => (value === null || value === undefined ? "" : String(value));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ask the host for the page state of one accepted version, conditionally.
|
|
75
|
+
*
|
|
76
|
+
* The ETag is the one the accepted save carried, so the answer is about those exact
|
|
77
|
+
* bytes or about nothing. 412 (another version is stored), 428 (no condition was sent,
|
|
78
|
+
* which no answer of ours can be), 404 (this host keeps no page state) and every other
|
|
79
|
+
* failure are refusals with a code: the caller keeps its records and asks again later.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} options
|
|
82
|
+
* @param {string} options.href - the resource discovery named in `document.pageState.href`
|
|
83
|
+
* @param {string} options.etag - the ETag the accepted bytes were stored under
|
|
84
|
+
* @param {Function} [options.fetch]
|
|
85
|
+
* @param {AbortSignal} [options.signal]
|
|
86
|
+
* @returns {Promise<{ok: true, version: number, documentId: *, generation: string, etag: string, stateHash: string, tag: string}|{ok: false, code: string, message: string}>}
|
|
87
|
+
*/
|
|
88
|
+
export async function readPageState({ href, etag, fetch: doFetch = globalThis.fetch, signal = null } = {}) {
|
|
89
|
+
if (typeof etag !== "string" || !etag) return refused("missing-etag", "A conditional read needs the accepted ETag");
|
|
90
|
+
if (typeof href !== "string" || !href) return refused("no-page-state", "This host names no page-state resource");
|
|
91
|
+
if (typeof doFetch !== "function") return refused("unavailable", "This tab cannot read the page state");
|
|
92
|
+
const base = globalThis.location?.href ?? "http://localhost/";
|
|
93
|
+
let url;
|
|
94
|
+
try { url = new URL(href, base).href; }
|
|
95
|
+
catch (_) { return refused("no-page-state", "The page-state resource is not a URL"); }
|
|
96
|
+
const token = saveToken();
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await doFetch(url, {
|
|
100
|
+
method: "GET",
|
|
101
|
+
credentials: token ? "omit" : "same-origin",
|
|
102
|
+
headers: { "If-Match": etag, "Document-URL": globalThis.location?.href ?? base },
|
|
103
|
+
cache: "no-store",
|
|
104
|
+
redirect: "manual",
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return refused("unavailable", "The page-state request did not complete");
|
|
109
|
+
}
|
|
110
|
+
if (!res || typeof res.status !== "number") return refused("unavailable", "The page-state request did not answer");
|
|
111
|
+
if (res.status === 412) return refused("etag-mismatch", "Another version of this document is stored");
|
|
112
|
+
if (res.status === 428) return refused("etag-required", "The host needs a condition this read cannot send");
|
|
113
|
+
if (res.status === 404) return refused("no-page-state", "This host keeps no page state");
|
|
114
|
+
if (!res.ok) return refused("unavailable", `The host answered ${res.status}`);
|
|
115
|
+
|
|
116
|
+
let body;
|
|
117
|
+
try { body = JSON.parse(await res.text()); }
|
|
118
|
+
catch (_) { return refused("invalid-state", "The page-state answer is not JSON"); }
|
|
119
|
+
if (!isObject(body) || body.version !== PAGE_STATE_VERSION) return refused("unsupported-version", "Unknown page-state version");
|
|
120
|
+
if (typeof body.tag !== "string" || !body.tag) return refused("invalid-state", "The page-state answer carries no page tag");
|
|
121
|
+
if (typeof body.stateHash !== "string" || !HEX64.test(body.stateHash)) return refused("invalid-state", "The page-state answer carries no state digest");
|
|
122
|
+
if (typeof body.generation !== "string" || !GENERATION.test(body.generation)) return refused("invalid-state", "The page-state answer carries no generation");
|
|
123
|
+
if (typeof body.etag !== "string" || !body.etag) return refused("invalid-state", "The page-state answer carries no ETag");
|
|
124
|
+
if (!Object.hasOwn(body, "documentId")) return refused("invalid-state", "The page-state answer carries no document identity");
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
version: body.version,
|
|
128
|
+
documentId: body.documentId,
|
|
129
|
+
generation: body.generation,
|
|
130
|
+
etag: body.etag,
|
|
131
|
+
stateHash: body.stateHash,
|
|
132
|
+
tag: body.tag,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether a conditional read answers for the version this tab accepted.
|
|
138
|
+
*
|
|
139
|
+
* Identity, generation, ETag, digest and projection, in that order: a state for another
|
|
140
|
+
* document, another generation, another version of the same document or another article
|
|
141
|
+
* is refused rather than used, and the receipt names the pair the host persisted.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} read - an answer from readPageState()
|
|
144
|
+
* @param {Object} options
|
|
145
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
146
|
+
* @param {string} options.article - the final outgoing canonical article
|
|
147
|
+
* @param {string} options.etag - the ETag the accepted save carried
|
|
148
|
+
* @returns {Promise<{ok: true, state: Object, receipt: {file: string, pageState: string}}|{ok: false, code: string, message: string}>}
|
|
149
|
+
*/
|
|
150
|
+
export async function verifyPageState(read, { accepted, article, etag } = {}) {
|
|
151
|
+
if (!read || read.ok !== true) return refused(read?.code ?? "unavailable", read?.message ?? "The page state was not read");
|
|
152
|
+
if (idKey(read.documentId) !== idKey(accepted?.authority?.documentId)) return refused("foreign-document", "The page state belongs to another document");
|
|
153
|
+
if (typeof accepted?.lineage?.generation !== "string" || read.generation !== accepted.lineage.generation) return refused("foreign-generation", "The page state belongs to another generation");
|
|
154
|
+
if (read.etag !== etag) return refused("etag-mismatch", "The page state belongs to another version of this document");
|
|
155
|
+
let stateBytes, state;
|
|
156
|
+
try { ({ state, bytes: stateBytes } = stateOfTag(read.tag)); }
|
|
157
|
+
catch (error) { return refused(error?.code ?? "invalid-state", error?.message ?? "The page state does not decode"); }
|
|
158
|
+
if (await digestHex(stateBytes) !== read.stateHash) return refused("state-digest-mismatch", "The page state is not the one the host hashed");
|
|
159
|
+
if (typeof article !== "string") return refused("missing-article", "A proof names the final outgoing article");
|
|
160
|
+
let projected;
|
|
161
|
+
try { projected = articleOfPageState(state); }
|
|
162
|
+
catch (error) { return refused("invalid-state", error?.message ?? "The page state does not project"); }
|
|
163
|
+
if (projected !== article) return refused("article-mismatch", "The page state does not project to the accepted article");
|
|
164
|
+
return { ok: true, state, receipt: { file: read.etag, pageState: read.stateHash } };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A proof is only worth the records the validator confirms, over the revisions the
|
|
168
|
+
// caller observed: a proof that covers nothing keeps everything.
|
|
169
|
+
async function covered(proof, records) {
|
|
170
|
+
const observed = (records || []).map(({ id, revision }) => ({ id, revision }));
|
|
171
|
+
try {
|
|
172
|
+
return { ok: true, proof, covered: await validatePageCheckpoint(proof, observed) };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof does not validate");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The proof of one accepted save, from the page state pinned to its capture.
|
|
180
|
+
*
|
|
181
|
+
* The receipt is the accepted ETag and the digest of the state the proof is made over,
|
|
182
|
+
* which is this tab's own account that this capture was written and hashed. Nothing
|
|
183
|
+
* about a later edit, a store value or an acknowledgement reaches this function.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} options
|
|
186
|
+
* @param {string} options.capture - the capture the save belongs to
|
|
187
|
+
* @param {Array<Object>} options.records - {id, revision, update, context} the caller observed
|
|
188
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
189
|
+
* @param {string} options.article - the final outgoing canonical article
|
|
190
|
+
* @param {Object} options.state - the page state pinned to that capture
|
|
191
|
+
* @param {string} options.etag - the ETag the accepted save carried
|
|
192
|
+
* @returns {Promise<{ok: true, proof: Object, covered: Array}|{ok: false, code: string, message: string}>}
|
|
193
|
+
*/
|
|
194
|
+
export async function captureProof({ capture, records = [], accepted, article, state = null, etag = null } = {}) {
|
|
195
|
+
if (!state) return refused("no-capture-state", "No page state was pinned to this capture");
|
|
196
|
+
if (typeof etag !== "string" || !etag) return refused("missing-etag", "An accepted save with no ETag proves no stored pair");
|
|
197
|
+
if (typeof article !== "string") return refused("missing-article", "The accepted bytes hold no article");
|
|
198
|
+
let projected, stateBytes;
|
|
199
|
+
try {
|
|
200
|
+
projected = articleOfPageState(state);
|
|
201
|
+
stateBytes = canonicalPageBytes(state);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return refused(error?.code ?? "invalid-state", error?.message ?? "The pinned page state does not decode");
|
|
204
|
+
}
|
|
205
|
+
if (projected !== article) return refused("article-mismatch", "The pinned page state does not project to the accepted article");
|
|
206
|
+
let proof;
|
|
207
|
+
try {
|
|
208
|
+
proof = await pageProof({
|
|
209
|
+
capture,
|
|
210
|
+
records,
|
|
211
|
+
accepted,
|
|
212
|
+
article,
|
|
213
|
+
state,
|
|
214
|
+
receipt: { file: etag, pageState: await digestHex(stateBytes) },
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof could not be built");
|
|
218
|
+
}
|
|
219
|
+
return covered(proof, records);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The proof of an accepted save, rebuilt from the page state the host persisted.
|
|
224
|
+
*
|
|
225
|
+
* The answer for the server's checkpoint, another writer's, and a save whose reply was
|
|
226
|
+
* lost: identity, generation and digest are checked against that exact ETag first, and
|
|
227
|
+
* only then is coverage asked. A host that cannot answer leaves the journal alone.
|
|
228
|
+
*
|
|
229
|
+
* @param {Object} options
|
|
230
|
+
* @param {Object} options.read - an answer from readPageState()
|
|
231
|
+
* @returns {Promise<{ok: true, proof: Object, covered: Array}|{ok: false, code: string, message: string}>}
|
|
232
|
+
*/
|
|
233
|
+
export async function refetchedProof({ read, capture, records = [], accepted, article, etag = null } = {}) {
|
|
234
|
+
const verified = await verifyPageState(read, { accepted, article, etag });
|
|
235
|
+
if (!verified.ok) return verified;
|
|
236
|
+
let proof;
|
|
237
|
+
try {
|
|
238
|
+
proof = await pageProof({ capture, records, accepted, article, state: verified.state, receipt: verified.receipt });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof could not be built");
|
|
241
|
+
}
|
|
242
|
+
return covered(proof, records);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Retire exactly the records one accepted save covers.
|
|
247
|
+
*
|
|
248
|
+
* The pinned capture is tried first, because it needs no request and no host. Only a
|
|
249
|
+
* capture that cannot prove these bytes asks the host, conditionally on the accepted
|
|
250
|
+
* ETag. Whichever answers, the channel is handed the caller's observed list and the
|
|
251
|
+
* proof together: it narrows the list to what the proof covers and never widens it, and
|
|
252
|
+
* a refusal here retires nothing.
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} options
|
|
255
|
+
* @param {Object} options.channel - the `page.yjs` channel api
|
|
256
|
+
* @param {Array<Object>} options.records - {id, revision, update, context} the caller observed
|
|
257
|
+
* @param {string} options.capture
|
|
258
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
259
|
+
* @param {string} options.article
|
|
260
|
+
* @param {Object} [options.state] - the page state pinned to the capture
|
|
261
|
+
* @param {string} options.etag
|
|
262
|
+
* @param {?string} [options.href] - the page-state resource, or null
|
|
263
|
+
* @param {Function} [options.read]
|
|
264
|
+
* @param {Function} [options.fetch]
|
|
265
|
+
* @returns {Promise<{ok: boolean, retired: Array, proof: ?Object, code?: string}>}
|
|
266
|
+
*/
|
|
267
|
+
export async function checkpointAccepted({
|
|
268
|
+
channel,
|
|
269
|
+
records = [],
|
|
270
|
+
capture,
|
|
271
|
+
accepted,
|
|
272
|
+
article,
|
|
273
|
+
state = null,
|
|
274
|
+
etag = null,
|
|
275
|
+
href = null,
|
|
276
|
+
read = readPageState,
|
|
277
|
+
fetch: doFetch,
|
|
278
|
+
} = {}) {
|
|
279
|
+
if (!channel || typeof channel.checkpoint !== "function") return { ok: false, code: "no-channel", retired: [], proof: null };
|
|
280
|
+
let proven = await captureProof({ capture, records, accepted, article, state, etag });
|
|
281
|
+
if (!proven.ok) {
|
|
282
|
+
const answer = await read({ href, etag, fetch: doFetch });
|
|
283
|
+
proven = await refetchedProof({ read: answer, capture, records, accepted, article, etag });
|
|
284
|
+
}
|
|
285
|
+
if (!proven.ok) return { ok: false, code: proven.code, retired: [], proof: null };
|
|
286
|
+
const answer = await channel.checkpoint({ records: proven.covered, proof: proven.proof });
|
|
287
|
+
if (!answer || answer.ok !== true) return { ok: false, code: answer?.code ?? "checkpoint-failed", retired: [], proof: proven.proof };
|
|
288
|
+
return { ok: true, retired: answer.retired ?? [], proof: proven.proof };
|
|
289
|
+
}
|
package/src/page/chrome.js
CHANGED
|
@@ -11,9 +11,8 @@ import { currentSpan, selectBlocks, duplicateBlocks, deleteBlocks, asStep, typeI
|
|
|
11
11
|
import { openAltEditor, setImageAttrs } from "./media.js";
|
|
12
12
|
import { BlockDrag } from "./drag.js";
|
|
13
13
|
import { openLinkEditor } from "./link.js";
|
|
14
|
-
import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnInto, insertAfter, rankItems, runItem, retype, canTurnInto, insertItemsFor } from "./items.js";
|
|
14
|
+
import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnInto, insertAfter, rankItems, runItem, retype, canTurnInto, insertItemsFor, loadChartPart } from "./items.js";
|
|
15
15
|
import toast from "../ui/toast-lazy.js";
|
|
16
|
-
import { insertChartAfterTable } from "./chart.js";
|
|
17
16
|
import { pickCsv } from "./csv-import.js";
|
|
18
17
|
import { HISTORY_INTENT, typingIntent } from "./history-intent.js";
|
|
19
18
|
import { composing } from "./markdown.js";
|
|
@@ -86,7 +85,7 @@ export const ACTION_ITEMS = [
|
|
|
86
85
|
{ key: "small", label: "Small", icon: "▫", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 240 }) },
|
|
87
86
|
{ key: "medium", label: "Medium", icon: "◻", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 480 }) },
|
|
88
87
|
{ key: "full", label: "Full width", icon: "⬜", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: null }) },
|
|
89
|
-
{ key: "chart", label: "Chart this table", icon: "📊", only: "table", run: (view, chrome, index) =>
|
|
88
|
+
{ key: "chart", label: "Chart this table", icon: "📊", only: "table", run: (view, chrome, index) => { const at = blockStart(view.state.doc, index); return loadChartPart(view).then(({ insertChartAfterTable }) => insertChartAfterTable(view, at)); } },
|
|
90
89
|
{ key: "csv", label: "Import CSV…", icon: "⇧", only: "table", focus: false, run: (view, chrome, index) => pickCsv(view, null, view.state.doc.child(index).attrs.id) },
|
|
91
90
|
];
|
|
92
91
|
|
|
@@ -217,6 +216,11 @@ function runList(view, kind) {
|
|
|
217
216
|
if (tr.docChanged) view.dispatch(asStep(tr));
|
|
218
217
|
}
|
|
219
218
|
|
|
219
|
+
// The four formats this release adds are commands the format gate may refuse, and
|
|
220
|
+
// a refusal is a notice rather than a change. The menu that offers them, and the
|
|
221
|
+
// commands behind it, load the first time one is chosen (format-native-commands.js).
|
|
222
|
+
const nativeMenu = (view, chrome, name) => import("./format-native-commands.js").then((menu) => menu.nativeMenu(view, name, chrome.more));
|
|
223
|
+
|
|
220
224
|
function runMark(view, name) {
|
|
221
225
|
if (name === "link") return openLinkEditor(view);
|
|
222
226
|
const st = view.state;
|
|
@@ -560,6 +564,9 @@ class Chrome {
|
|
|
560
564
|
const actions = ACTION_ITEMS.filter((a) => MORE_ACTIONS.has(a.key)).map((a) => ({ ...a, blocks: true }));
|
|
561
565
|
return [
|
|
562
566
|
...tools,
|
|
567
|
+
{ key: "underline", label: "Underline", icon: "U", hint: `${MOD}u`, run: (view, chrome) => nativeMenu(view, chrome, "underline") },
|
|
568
|
+
{ key: "text_color", label: "Text color", icon: "A", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "text_color") },
|
|
569
|
+
{ key: "highlight", label: "Highlight", icon: "\u25A0", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "highlight") },
|
|
563
570
|
{ key: "turn", label: "Turn into", icon: "\u21bb", run: (view, chrome) => chrome.openMenu("turn", chrome.more) },
|
|
564
571
|
{ key: "insert", label: "Insert", icon: "+", run: (view, chrome) => chrome.openMenu("insert", chrome.more, null, null, view.state.doc.child(chrome.blockIndex()).attrs.id) },
|
|
565
572
|
...actions,
|
|
@@ -668,9 +675,20 @@ class Chrome {
|
|
|
668
675
|
this.view.focus();
|
|
669
676
|
return;
|
|
670
677
|
}
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
678
|
+
const turn = () => {
|
|
679
|
+
const tr = this.view.state.tr;
|
|
680
|
+
for (const id of wanted) {
|
|
681
|
+
const at = this.idIndex(id);
|
|
682
|
+
if (at >= 0) retype(tr, at, item);
|
|
683
|
+
}
|
|
684
|
+
if (tr.docChanged) this.view.dispatch(asStep(tr).scrollIntoView());
|
|
685
|
+
return true;
|
|
686
|
+
};
|
|
687
|
+
// A block type from a later format asks the gate before it writes.
|
|
688
|
+
if (item.type === "quote") {
|
|
689
|
+
const view = this.view;
|
|
690
|
+
import("./format-native-commands.js").then((menu) => menu.gateTurn(view, turn));
|
|
691
|
+
} else turn();
|
|
674
692
|
this.view.focus();
|
|
675
693
|
} else if (m.mode === "actions" || m.mode === "more" || m.mode === "external") {
|
|
676
694
|
if (item.blocks) this.selectCaretBlock(wanted);
|
|
@@ -15,6 +15,18 @@ export function customTitle(root) {
|
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Mark this document's title as its author's own, the way the SEO editor's custom
|
|
19
|
+
// title does. Returns whether it wrote the marker.
|
|
20
|
+
export function markCustomTitle(root) {
|
|
21
|
+
const head = headOf(root);
|
|
22
|
+
if (!head || customTitle(root)) return false;
|
|
23
|
+
const meta = head.ownerDocument.createElement("meta");
|
|
24
|
+
meta.setAttribute("name", TITLE_MODE);
|
|
25
|
+
meta.setAttribute("content", "custom");
|
|
26
|
+
head.insertBefore(meta, head.querySelector("title") ?? head.firstChild);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export function titleText(root) {
|
|
19
31
|
return headOf(root)?.querySelector("title")?.textContent ?? "";
|
|
20
32
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { state } from "../vendor/prosemirror.vendor.js";
|
|
2
2
|
import Mutation from "../lib/mutation.js";
|
|
3
|
-
import {
|
|
3
|
+
import { UNTITLED } from "./document-title.js";
|
|
4
|
+
import { customTitle, markCustomTitle, titleFor, titleText, writeTitle } from "./document-title-dom.js";
|
|
4
5
|
|
|
5
6
|
const ATOM = "\uFFFC";
|
|
6
7
|
|
|
@@ -38,6 +39,18 @@ export function refreshDocumentTitle(view) {
|
|
|
38
39
|
return title;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// A file written before titles followed the first H1 can carry a title its author
|
|
43
|
+
// typed with no H1 behind it. The first time an editor opens such a file, that title
|
|
44
|
+
// becomes the author's own (the SEO custom marker), so deriving never replaces it;
|
|
45
|
+
// a file that has an H1 keeps following it, and deleting that H1 later reads Untitled.
|
|
46
|
+
export function adoptAuthoredTitle(view) {
|
|
47
|
+
const doc = view?.dom?.ownerDocument ?? null;
|
|
48
|
+
if (!doc || customTitle(doc) || titleItemsOfDoc(view.state.doc).length || [ "", UNTITLED ].includes(titleText(doc).trim())) return false;
|
|
49
|
+
Mutation.pause();
|
|
50
|
+
try { return markCustomTitle(doc); }
|
|
51
|
+
finally { Mutation.resume(); }
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function titlePlugin() {
|
|
42
55
|
return new state.Plugin({
|
|
43
56
|
view(editorView) {
|
|
@@ -46,6 +59,7 @@ export function titlePlugin() {
|
|
|
46
59
|
doc = editorView.state.doc;
|
|
47
60
|
refreshDocumentTitle(editorView);
|
|
48
61
|
};
|
|
62
|
+
adoptAuthoredTitle(editorView);
|
|
49
63
|
refresh();
|
|
50
64
|
return {
|
|
51
65
|
update(next) {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export const UNTITLED = "Untitled";
|
|
2
2
|
|
|
3
|
-
export function documentTitle(items, { custom = false, currentTitle = "" } = {}) {
|
|
3
|
+
export function documentTitle(items, { custom = false, currentTitle = "", keepWithoutHeading = false } = {}) {
|
|
4
4
|
if (custom) return currentTitle;
|
|
5
5
|
const first = items.find(item =>
|
|
6
6
|
item.t === "heading" && (item.level ?? 2) === 1 && item.parent == null
|
|
7
7
|
);
|
|
8
|
+
// A file the editor has not adopted yet (see adoptAuthoredTitle) may carry a title
|
|
9
|
+
// its author wrote with no H1 behind it; a renderer that cannot ask keeps it.
|
|
10
|
+
if (!first && keepWithoutHeading && !["", UNTITLED].includes(String(currentTitle).trim())) return currentTitle;
|
|
8
11
|
return String(first?.c ?? "").replace(/[\s\uFFFC]+/gu, " ").trim() || UNTITLED;
|
|
9
12
|
}
|