@heycys/gallery 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +50 -2
- package/THIRD-PARTY-NOTICES.md +3253 -0
- package/assets/writing-preferences.md +107 -0
- package/bin/gallery.mjs +10 -0
- package/browser/editor/components/bookmark/icons.mjs +6 -0
- package/browser/editor/components/bookmark/index.mjs +80 -0
- package/browser/editor/components/bookmark/page.mjs +147 -0
- package/browser/editor/components/builders.mjs +19 -0
- package/browser/editor/components/callout/index.mjs +39 -0
- package/browser/editor/components/callout/page.mjs +13 -0
- package/browser/editor/components/code/index.mjs +327 -0
- package/browser/editor/components/code/page.mjs +561 -0
- package/browser/editor/components/colors.mjs +50 -0
- package/browser/editor/components/columns/index.mjs +104 -0
- package/browser/editor/components/columns/page.mjs +297 -0
- package/browser/editor/components/details/index.mjs +72 -0
- package/browser/editor/components/details/page.mjs +86 -0
- package/browser/editor/components/dialog/index.mjs +83 -0
- package/browser/editor/components/dialog/page.mjs +219 -0
- package/browser/editor/components/divider/index.mjs +48 -0
- package/browser/editor/components/embed/icons.mjs +11 -0
- package/browser/editor/components/embed/index.mjs +123 -0
- package/browser/editor/components/embed/page.mjs +309 -0
- package/browser/editor/components/figure/index.mjs +87 -0
- package/browser/editor/components/figure/page.mjs +33 -0
- package/browser/editor/components/file/icons.mjs +14 -0
- package/browser/editor/components/file/index.mjs +77 -0
- package/browser/editor/components/file/page.mjs +114 -0
- package/browser/editor/components/flowchart/add.mjs +123 -0
- package/browser/editor/components/flowchart/index.mjs +246 -0
- package/browser/editor/components/flowchart/page.mjs +453 -0
- package/browser/editor/components/heading/index.mjs +51 -0
- package/browser/editor/components/highlight/index.mjs +79 -0
- package/browser/editor/components/icon/index.mjs +57 -0
- package/browser/editor/components/icon/page.mjs +286 -0
- package/browser/editor/components/icons.mjs +45 -0
- package/browser/editor/components/image/crop.mjs +189 -0
- package/browser/editor/components/image/index.mjs +104 -0
- package/browser/editor/components/image/page.mjs +385 -0
- package/browser/editor/components/image/viewer.mjs +213 -0
- package/browser/editor/components/installed/index.mjs +133 -0
- package/browser/editor/components/installed/page.mjs +422 -0
- package/browser/editor/components/installed/served.mjs +35 -0
- package/browser/editor/components/kit.mjs +64 -0
- package/browser/editor/components/list/index.mjs +194 -0
- package/browser/editor/components/list/join.mjs +68 -0
- package/browser/editor/components/list/numbers.mjs +107 -0
- package/browser/editor/components/list/page.mjs +632 -0
- package/browser/editor/components/math/index.mjs +88 -0
- package/browser/editor/components/math/page.mjs +236 -0
- package/browser/editor/components/math/tex-logo.mjs +6 -0
- package/browser/editor/components/media/icons.mjs +19 -0
- package/browser/editor/components/media/page.mjs +162 -0
- package/browser/editor/components/media/parts.mjs +60 -0
- package/browser/editor/components/media/size.mjs +20 -0
- package/browser/editor/components/media/toolbar.mjs +37 -0
- package/browser/editor/components/media/write.mjs +7 -0
- package/browser/editor/components/pages.mjs +46 -0
- package/browser/editor/components/paragraph/index.mjs +43 -0
- package/browser/editor/components/paragraph/page.mjs +52 -0
- package/browser/editor/components/placeholder-bar.mjs +17 -0
- package/browser/editor/components/placeholder.mjs +128 -0
- package/browser/editor/components/quote/index.mjs +33 -0
- package/browser/editor/components/registry.mjs +833 -0
- package/browser/editor/components/rows.mjs +16 -0
- package/browser/editor/components/section/index.mjs +41 -0
- package/browser/editor/components/section/page.mjs +141 -0
- package/browser/editor/components/sheets/index.mjs +137 -0
- package/browser/editor/components/sheets/page.mjs +227 -0
- package/browser/editor/components/sheets/pills.mjs +226 -0
- package/browser/editor/components/table/grid.mjs +73 -0
- package/browser/editor/components/table/index.mjs +306 -0
- package/browser/editor/components/table/page.mjs +672 -0
- package/browser/editor/components/table/slots.mjs +41 -0
- package/browser/editor/components/widget/index.mjs +50 -0
- package/browser/editor/page/annotate/anchors.mjs +229 -0
- package/browser/editor/page/annotate/card.mjs +102 -0
- package/browser/editor/page/annotate/index.mjs +1629 -0
- package/browser/editor/page/at-menu/index.mjs +317 -0
- package/browser/editor/page/autosave.mjs +977 -0
- package/browser/editor/page/beside.mjs +21 -0
- package/browser/editor/page/block-info.mjs +63 -0
- package/browser/editor/page/block-menu/colors.mjs +82 -0
- package/browser/editor/page/block-menu/index.mjs +215 -0
- package/browser/editor/page/blocks.mjs +246 -0
- package/browser/editor/page/caret.mjs +679 -0
- package/browser/editor/page/chip-fills.mjs +126 -0
- package/browser/editor/page/clipboard/import/colors.mjs +192 -0
- package/browser/editor/page/clipboard/import/html.mjs +792 -0
- package/browser/editor/page/clipboard/import/inline.mjs +232 -0
- package/browser/editor/page/clipboard/import/languages.mjs +42 -0
- package/browser/editor/page/clipboard/import/markdown.mjs +760 -0
- package/browser/editor/page/clipboard/import/math.mjs +100 -0
- package/browser/editor/page/clipboard/import/office.mjs +245 -0
- package/browser/editor/page/clipboard/import/read.mjs +120 -0
- package/browser/editor/page/clipboard/import/rich.mjs +29 -0
- package/browser/editor/page/clipboard/import/to-blocks.mjs +290 -0
- package/browser/editor/page/clipboard/index.mjs +797 -0
- package/browser/editor/page/clipboard/own/blocks.mjs +202 -0
- package/browser/editor/page/clipboard/own/chart-keys.mjs +58 -0
- package/browser/editor/page/clipboard/own/describe.mjs +115 -0
- package/browser/editor/page/clipboard/own/export.mjs +167 -0
- package/browser/editor/page/clipboard/own/ids.mjs +39 -0
- package/browser/editor/page/clipboard/own/index.mjs +13 -0
- package/browser/editor/page/clipboard/own/rules.mjs +44 -0
- package/browser/editor/page/clipboard/own/selection.mjs +159 -0
- package/browser/editor/page/clipboard/own/writers.mjs +326 -0
- package/browser/editor/page/columns/index.mjs +360 -0
- package/browser/editor/page/commit.mjs +1798 -0
- package/browser/editor/page/conflicts.mjs +269 -0
- package/browser/editor/page/convert.mjs +830 -0
- package/browser/editor/page/core.mjs +1655 -0
- package/browser/editor/page/drag/drop.mjs +307 -0
- package/browser/editor/page/drag/index.mjs +335 -0
- package/browser/editor/page/drag/preview.mjs +121 -0
- package/browser/editor/page/edge-scroll.mjs +50 -0
- package/browser/editor/page/editor.mjs +1725 -0
- package/browser/editor/page/emoji-picker/emoji-tab.mjs +278 -0
- package/browser/editor/page/emoji-picker/glyph-html.mjs +26 -0
- package/browser/editor/page/emoji-picker/grid.mjs +79 -0
- package/browser/editor/page/emoji-picker/icon-tab.mjs +217 -0
- package/browser/editor/page/emoji-picker/icons.mjs +32 -0
- package/browser/editor/page/emoji-picker/index.mjs +164 -0
- package/browser/editor/page/emoji-picker/styles.mjs +2 -0
- package/browser/editor/page/emoji-picker/support.mjs +202 -0
- package/browser/editor/page/emoji-picker/upload.mjs +235 -0
- package/browser/editor/page/export-report.mjs +23 -0
- package/browser/editor/page/fills.mjs +60 -0
- package/browser/editor/page/find/index.mjs +107 -0
- package/browser/editor/page/flags.mjs +70 -0
- package/browser/editor/page/format-bar/bar.mjs +448 -0
- package/browser/editor/page/format-bar/brush.mjs +77 -0
- package/browser/editor/page/format-bar/colors.mjs +46 -0
- package/browser/editor/page/format-bar/index.mjs +404 -0
- package/browser/editor/page/formula/index.mjs +236 -0
- package/browser/editor/page/formula/panel.mjs +336 -0
- package/browser/editor/page/formula/symbols.mjs +156 -0
- package/browser/editor/page/frame.mjs +74 -0
- package/browser/editor/page/frames.mjs +196 -0
- package/browser/editor/page/geometry.mjs +400 -0
- package/browser/editor/page/gesture.mjs +45 -0
- package/browser/editor/page/handle/index.mjs +639 -0
- package/browser/editor/page/handle-sizes.mjs +14 -0
- package/browser/editor/page/hints/index.mjs +84 -0
- package/browser/editor/page/history.mjs +401 -0
- package/browser/editor/page/insert/index.mjs +243 -0
- package/browser/editor/page/kept.mjs +38 -0
- package/browser/editor/page/links/address.mjs +40 -0
- package/browser/editor/page/links/index.mjs +866 -0
- package/browser/editor/page/links/pages.mjs +131 -0
- package/browser/editor/page/links/views.mjs +369 -0
- package/browser/editor/page/look.mjs +65 -0
- package/browser/editor/page/made.mjs +24 -0
- package/browser/editor/page/margin.mjs +352 -0
- package/browser/editor/page/markdown/index.mjs +88 -0
- package/browser/editor/page/markdown/rules.mjs +33 -0
- package/browser/editor/page/marks.mjs +377 -0
- package/browser/editor/page/media-menu/index.mjs +158 -0
- package/browser/editor/page/menu.mjs +500 -0
- package/browser/editor/page/merge.mjs +100 -0
- package/browser/editor/page/notices.mjs +142 -0
- package/browser/editor/page/offered.mjs +32 -0
- package/browser/editor/page/outline/folds.mjs +52 -0
- package/browser/editor/page/outline/index.mjs +697 -0
- package/browser/editor/page/outline/list.mjs +172 -0
- package/browser/editor/page/outline/read.mjs +201 -0
- package/browser/editor/page/outline/strip.mjs +281 -0
- package/browser/editor/page/outline/tree.mjs +248 -0
- package/browser/editor/page/overlay-layer.mjs +227 -0
- package/browser/editor/page/overlays.mjs +205 -0
- package/browser/editor/page/own.mjs +28 -0
- package/browser/editor/page/pads.mjs +71 -0
- package/browser/editor/page/page-objects.mjs +105 -0
- package/browser/editor/page/panel.mjs +64 -0
- package/browser/editor/page/placement.mjs +58 -0
- package/browser/editor/page/post.mjs +41 -0
- package/browser/editor/page/present-links.mjs +50 -0
- package/browser/editor/page/quiet.mjs +48 -0
- package/browser/editor/page/resize/index.mjs +327 -0
- package/browser/editor/page/select-all/index.mjs +262 -0
- package/browser/editor/page/selection/delete.mjs +59 -0
- package/browser/editor/page/selection/index.mjs +157 -0
- package/browser/editor/page/selection/marquee.mjs +174 -0
- package/browser/editor/page/session.mjs +117 -0
- package/browser/editor/page/slash-menu/index.mjs +291 -0
- package/browser/editor/page/storage.mjs +48 -0
- package/browser/editor/page/sync-state.mjs +267 -0
- package/browser/editor/page/table/aligns.mjs +29 -0
- package/browser/editor/page/table/cell-bar.mjs +138 -0
- package/browser/editor/page/table/cells.mjs +527 -0
- package/browser/editor/page/table/icons.mjs +40 -0
- package/browser/editor/page/table/index.mjs +985 -0
- package/browser/editor/page/table/lists.mjs +187 -0
- package/browser/editor/page/table/widths.mjs +308 -0
- package/browser/editor/page/tex.mjs +32 -0
- package/browser/editor/page/text-select/index.mjs +656 -0
- package/browser/editor/page/tooltip.mjs +98 -0
- package/browser/editor/page/type-grid.mjs +82 -0
- package/browser/editor/page/typing/code-edges.mjs +228 -0
- package/browser/editor/page/typing/index.mjs +276 -0
- package/browser/editor/page/typing/leave.mjs +68 -0
- package/browser/editor/page/ui.mjs +160 -0
- package/browser/shared/annotations.mjs +571 -0
- package/browser/shared/code-folds.mjs +10 -0
- package/browser/shared/code-languages.mjs +70 -0
- package/browser/shared/code-lines.mjs +30 -0
- package/browser/shared/colors.mjs +28 -0
- package/browser/shared/component-catalog.mjs +559 -0
- package/browser/shared/components/bookmark.mjs +26 -0
- package/browser/shared/components/callout.mjs +7 -0
- package/browser/shared/components/chart.mjs +20 -0
- package/browser/shared/components/code.mjs +37 -0
- package/browser/shared/components/columns.mjs +16 -0
- package/browser/shared/components/details.mjs +15 -0
- package/browser/shared/components/dialog.mjs +28 -0
- package/browser/shared/components/diff.mjs +18 -0
- package/browser/shared/components/divider.mjs +6 -0
- package/browser/shared/components/embed.mjs +25 -0
- package/browser/shared/components/figure.mjs +14 -0
- package/browser/shared/components/file.mjs +23 -0
- package/browser/shared/components/heading.mjs +6 -0
- package/browser/shared/components/highlight.mjs +9 -0
- package/browser/shared/components/icon.mjs +18 -0
- package/browser/shared/components/image.mjs +39 -0
- package/browser/shared/components/index.mjs +342 -0
- package/browser/shared/components/installed.mjs +162 -0
- package/browser/shared/components/list.mjs +146 -0
- package/browser/shared/components/math.mjs +49 -0
- package/browser/shared/components/mention.mjs +33 -0
- package/browser/shared/components/page.mjs +37 -0
- package/browser/shared/components/paragraph.mjs +9 -0
- package/browser/shared/components/quote.mjs +6 -0
- package/browser/shared/components/section.mjs +8 -0
- package/browser/shared/components/sheets.mjs +80 -0
- package/browser/shared/components/state.mjs +20 -0
- package/browser/shared/components/table.mjs +49 -0
- package/browser/shared/components/text.mjs +14 -0
- package/browser/shared/components/widget.mjs +10 -0
- package/browser/shared/copy-ids.mjs +72 -0
- package/browser/shared/css-references.mjs +110 -0
- package/browser/shared/emoji/custom.mjs +91 -0
- package/browser/shared/emoji/data/LICENSE-mingcute.txt +202 -0
- package/browser/shared/emoji/data/LICENSE-tabler.txt +21 -0
- package/browser/shared/emoji/data/README.md +26 -0
- package/browser/shared/emoji/data/categories.json +1 -0
- package/browser/shared/emoji/data/icons.json +1 -0
- package/browser/shared/emoji/data/keywords-en.json +1 -0
- package/browser/shared/emoji/data/keywords-zh.json +1 -0
- package/browser/shared/emoji/data/names.json +1 -0
- package/browser/shared/emoji/data/positions.json +1 -0
- package/browser/shared/emoji/data/twemoji-index.json +1 -0
- package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
- package/browser/shared/emoji/data.mjs +196 -0
- package/browser/shared/emoji/emoji.mjs +37 -0
- package/browser/shared/emoji/glyphs.mjs +134 -0
- package/browser/shared/emoji/icon-value.mjs +90 -0
- package/browser/shared/escape.mjs +20 -0
- package/browser/shared/export-wrap.mjs +89 -0
- package/browser/shared/files.mjs +33 -0
- package/browser/shared/html-tree.mjs +53 -0
- package/browser/shared/http.mjs +22 -0
- package/browser/shared/i18n/en.mjs +1164 -0
- package/browser/shared/i18n/index.mjs +132 -0
- package/browser/shared/ids.mjs +24 -0
- package/browser/shared/import-types.mjs +23 -0
- package/browser/shared/inline-spacing.mjs +97 -0
- package/browser/shared/layers.mjs +42 -0
- package/browser/shared/media.mjs +162 -0
- package/browser/shared/new-names.mjs +64 -0
- package/browser/shared/number-reading.mjs +92 -0
- package/browser/shared/old-names.mjs +143 -0
- package/browser/shared/page-address.mjs +140 -0
- package/browser/shared/page-links.mjs +86 -0
- package/browser/shared/paths.mjs +73 -0
- package/browser/shared/preferences.mjs +110 -0
- package/browser/shared/preview-server.mjs +247 -0
- package/browser/shared/qa-levels.mjs +93 -0
- package/browser/shared/reading-place.mjs +91 -0
- package/browser/shared/renamed-storage.mjs +220 -0
- package/browser/shared/reports.mjs +46 -0
- package/browser/shared/search.mjs +85 -0
- package/browser/shared/session.mjs +76 -0
- package/browser/shared/table-chart.mjs +113 -0
- package/browser/shared/table-hides.mjs +120 -0
- package/browser/shared/table-slots.mjs +75 -0
- package/browser/shared/words.mjs +23 -0
- package/browser/shared/workspace.mjs +535 -0
- package/browser/shared/writing-preferences.mjs +68 -0
- package/browser/shared/zoom.mjs +57 -0
- package/dist/chunk-2B5E4CDJ.mjs +2 -0
- package/dist/chunk-2PBALTR3.mjs +57 -0
- package/dist/chunk-342HXE3Q.mjs +2 -0
- package/dist/chunk-3LA3EMDE.mjs +15 -0
- package/dist/chunk-43B75J5T.mjs +2 -0
- package/dist/chunk-4K5KC6V2.mjs +2 -0
- package/dist/chunk-5FRMCDPY.mjs +16 -0
- package/dist/chunk-5L7RYZCL.mjs +2 -0
- package/dist/chunk-5LEK2CBS.mjs +2 -0
- package/dist/chunk-5OFN5AHC.mjs +2 -0
- package/dist/chunk-5SIYS5HN.mjs +2 -0
- package/dist/chunk-5TEK3RAD.mjs +2 -0
- package/dist/chunk-6CYG3Z4V.mjs +2 -0
- package/dist/chunk-6U2SLSDN.mjs +3 -0
- package/dist/chunk-742D2WWJ.mjs +19 -0
- package/dist/chunk-7GHT2UCA.mjs +15 -0
- package/dist/chunk-7QQBR7PB.mjs +2 -0
- package/dist/chunk-A55HN5OK.mjs +2 -0
- package/dist/chunk-ACOPDCNB.mjs +2 -0
- package/dist/chunk-AGDILNWB.mjs +2 -0
- package/dist/chunk-ATCJYAR4.mjs +2 -0
- package/dist/chunk-B3VTYBYR.mjs +960 -0
- package/dist/chunk-C564NU2H.mjs +77 -0
- package/dist/chunk-C7HWOBIF.mjs +2 -0
- package/dist/chunk-DB4WKGYF.mjs +7 -0
- package/dist/chunk-E7VYDG2X.mjs +2 -0
- package/dist/chunk-EUX7IC5J.mjs +48 -0
- package/dist/chunk-EVJ3BX3M.mjs +2 -0
- package/dist/chunk-EZY3WC6Z.mjs +15 -0
- package/dist/chunk-FQGQH4KM.mjs +2 -0
- package/dist/chunk-G7TOXONE.mjs +6 -0
- package/dist/chunk-GPF4YWWB.mjs +9 -0
- package/dist/chunk-GRAXKKG4.mjs +2 -0
- package/dist/chunk-GYQV7Y4J.mjs +120 -0
- package/dist/chunk-HMHK3EQ6.mjs +2 -0
- package/dist/chunk-HQQMJCQJ.mjs +10 -0
- package/dist/chunk-HYMEEXKN.mjs +4 -0
- package/dist/chunk-IERUOGC3.mjs +6 -0
- package/dist/chunk-ITNLYXXV.mjs +3 -0
- package/dist/chunk-L4DVONOG.mjs +2 -0
- package/dist/chunk-LAE5NQ4B.mjs +2 -0
- package/dist/chunk-LC7YNQFG.mjs +2 -0
- package/dist/chunk-LHTFLRB6.mjs +4 -0
- package/dist/chunk-LKX55H76.mjs +2 -0
- package/dist/chunk-MAFIGVXY.mjs +2 -0
- package/dist/chunk-NFIBI4IP.mjs +26 -0
- package/dist/chunk-NSKUTQ5J.mjs +5 -0
- package/dist/chunk-O52JMOCZ.mjs +2 -0
- package/dist/chunk-P2CAZPDP.mjs +3 -0
- package/dist/chunk-QAIHRH3I.mjs +13 -0
- package/dist/chunk-QMQUXTU4.mjs +12 -0
- package/dist/chunk-QR6A54KJ.mjs +3 -0
- package/dist/chunk-QVWVFC5K.mjs +6 -0
- package/dist/chunk-QYKCJ7FG.mjs +8 -0
- package/dist/chunk-QZY6Q64C.mjs +2 -0
- package/dist/chunk-TDQP3DKQ.mjs +10 -0
- package/dist/chunk-TMYZB6AV.mjs +2 -0
- package/dist/chunk-TX6JH7TR.mjs +2 -0
- package/dist/chunk-U3SSPL4K.mjs +8 -0
- package/dist/chunk-UH4SIXTR.mjs +2 -0
- package/dist/chunk-UNUEWH4S.mjs +2 -0
- package/dist/chunk-UXUK4PMC.mjs +27 -0
- package/dist/chunk-V4CPWKMZ.mjs +2 -0
- package/dist/chunk-V6XZ4VIR.mjs +2 -0
- package/dist/chunk-VACRB2NE.mjs +3 -0
- package/dist/chunk-W63L7367.mjs +7 -0
- package/dist/chunk-W7CXPCRV.mjs +6 -0
- package/dist/chunk-W7XHE4CT.mjs +2 -0
- package/dist/chunk-XINNG4F2.mjs +4 -0
- package/dist/chunk-Y34NR5JL.mjs +2 -0
- package/dist/chunk-YEY3RSFQ.mjs +2 -0
- package/dist/chunk-YL2DPWVS.mjs +4 -0
- package/dist/chunk-YXAUQPVK.mjs +8 -0
- package/dist/chunk-YZ5Z6H4W.mjs +4 -0
- package/dist/chunk-Z52HTUW7.mjs +4 -0
- package/dist/chunk-ZBIM6ARY.mjs +216 -0
- package/dist/cli.mjs +2 -0
- package/dist/server.mjs +2 -0
- package/dist/worker.mjs +5 -0
- package/kit/charts.js +355 -0
- package/kit/code.js +164 -0
- package/kit/dialogs.js +157 -0
- package/kit/math.js +1988 -0
- package/kit/report.css +2821 -0
- package/kit/runtime.js +1707 -0
- package/kit/sheets.js +123 -0
- package/kit/state.js +217 -0
- package/kit/tables.js +1449 -0
- package/kit/tokens.js +29 -0
- package/kit/ui.js +561 -0
- package/package.json +41 -4
- package/release.json +25 -0
- package/sdk/column-widths.d.ts +56 -0
- package/sdk/doc-charts.d.ts +82 -0
- package/sdk/doc-code.d.ts +28 -0
- package/sdk/doc-dialogs.d.ts +4 -0
- package/sdk/doc-kit.d.ts +141 -0
- package/sdk/doc-math.d.ts +3 -0
- package/sdk/doc-sheets.d.ts +15 -0
- package/sdk/doc-state.d.ts +41 -0
- package/sdk/doc-tables.d.ts +24 -0
- package/sdk/doc-tokens.d.ts +22 -0
- package/sdk/index.d.ts +30 -0
- package/skill/SKILL.md +138 -0
- package/skill/agents/openai.yaml +5 -0
- package/skill/references/building-components.md +327 -0
- package/skill/references/example-report.html +356 -0
- package/skill/references/writing-html.md +187 -0
- package/skill/sdk/column-widths.d.ts +56 -0
- package/skill/sdk/doc-charts.d.ts +82 -0
- package/skill/sdk/doc-code.d.ts +28 -0
- package/skill/sdk/doc-dialogs.d.ts +4 -0
- package/skill/sdk/doc-kit.d.ts +141 -0
- package/skill/sdk/doc-math.d.ts +3 -0
- package/skill/sdk/doc-sheets.d.ts +15 -0
- package/skill/sdk/doc-state.d.ts +41 -0
- package/skill/sdk/doc-tables.d.ts +24 -0
- package/skill/sdk/doc-tokens.d.ts +22 -0
- package/skill/sdk/index.d.ts +30 -0
- package/starter/doc.html +21 -0
- package/starter/doc.json +5 -0
- package/vendor/d3.v7.9.0.min.js +2 -0
- package/vendor/katex/auto-render.min.js +1 -0
- package/vendor/katex/contrib/mhchem.min.js +1 -0
- package/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/katex/katex.min.css +1 -0
- package/vendor/katex/katex.min.js +1 -0
- package/vendor/pdfjs/LICENSE +177 -0
- package/vendor/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Add-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Add-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
- package/vendor/pdfjs/cmaps/B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/B5pc-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/B5pc-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS1-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS1-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS2-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/CNS2-V.bcmap +3 -0
- package/vendor/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
- package/vendor/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Ext-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Ext-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GB-H.bcmap +4 -0
- package/vendor/pdfjs/cmaps/GB-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBT-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBT-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Hankaku.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Hiragana.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Katakana.bcmap +0 -0
- package/vendor/pdfjs/cmaps/LICENSE +36 -0
- package/vendor/pdfjs/cmaps/NWP-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/NWP-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/Roman.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
- package/vendor/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/V.bcmap +0 -0
- package/vendor/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
- package/vendor/pdfjs/legacy/build/pdf.min.mjs +27 -0
- package/vendor/pdfjs/legacy/build/pdf.worker.min.mjs +27 -0
- package/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
- package/vendor/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
- package/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION +363 -0
- package/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
- package/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
- package/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
- package/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
- package/vendor/types/@types/d3/LICENSE +21 -0
- package/vendor/types/@types/d3/index.d.ts +39 -0
- package/vendor/types/@types/d3/package.json +81 -0
- package/vendor/types/@types/d3-array/LICENSE +21 -0
- package/vendor/types/@types/d3-array/index.d.ts +1118 -0
- package/vendor/types/@types/d3-array/package.json +56 -0
- package/vendor/types/@types/d3-axis/LICENSE +21 -0
- package/vendor/types/@types/d3-axis/index.d.ts +293 -0
- package/vendor/types/@types/d3-axis/package.json +47 -0
- package/vendor/types/@types/d3-brush/LICENSE +21 -0
- package/vendor/types/@types/d3-brush/index.d.ts +255 -0
- package/vendor/types/@types/d3-brush/package.json +42 -0
- package/vendor/types/@types/d3-chord/LICENSE +21 -0
- package/vendor/types/@types/d3-chord/index.d.ts +471 -0
- package/vendor/types/@types/d3-chord/package.json +40 -0
- package/vendor/types/@types/d3-color/LICENSE +21 -0
- package/vendor/types/@types/d3-color/index.d.ts +669 -0
- package/vendor/types/@types/d3-color/package.json +55 -0
- package/vendor/types/@types/d3-contour/LICENSE +21 -0
- package/vendor/types/@types/d3-contour/index.d.ts +222 -0
- package/vendor/types/@types/d3-contour/package.json +38 -0
- package/vendor/types/@types/d3-delaunay/LICENSE +21 -0
- package/vendor/types/@types/d3-delaunay/index.d.ts +316 -0
- package/vendor/types/@types/d3-delaunay/package.json +30 -0
- package/vendor/types/@types/d3-dispatch/LICENSE +21 -0
- package/vendor/types/@types/d3-dispatch/index.d.ts +96 -0
- package/vendor/types/@types/d3-dispatch/package.json +51 -0
- package/vendor/types/@types/d3-drag/LICENSE +21 -0
- package/vendor/types/@types/d3-drag/index.d.ts +395 -0
- package/vendor/types/@types/d3-drag/package.json +42 -0
- package/vendor/types/@types/d3-dsv/LICENSE +21 -0
- package/vendor/types/@types/d3-dsv/index.d.ts +482 -0
- package/vendor/types/@types/d3-dsv/package.json +45 -0
- package/vendor/types/@types/d3-ease/LICENSE +21 -0
- package/vendor/types/@types/d3-ease/index.d.ts +316 -0
- package/vendor/types/@types/d3-ease/package.json +40 -0
- package/vendor/types/@types/d3-fetch/LICENSE +21 -0
- package/vendor/types/@types/d3-fetch/index.d.ts +282 -0
- package/vendor/types/@types/d3-fetch/package.json +37 -0
- package/vendor/types/@types/d3-force/LICENSE +21 -0
- package/vendor/types/@types/d3-force/index.d.ts +899 -0
- package/vendor/types/@types/d3-force/package.json +45 -0
- package/vendor/types/@types/d3-format/LICENSE +21 -0
- package/vendor/types/@types/d3-format/index.d.ts +337 -0
- package/vendor/types/@types/d3-format/package.json +45 -0
- package/vendor/types/@types/d3-geo/LICENSE +21 -0
- package/vendor/types/@types/d3-geo/index.d.ts +1371 -0
- package/vendor/types/@types/d3-geo/package.json +48 -0
- package/vendor/types/@types/d3-hierarchy/LICENSE +21 -0
- package/vendor/types/@types/d3-hierarchy/index.d.ts +928 -0
- package/vendor/types/@types/d3-hierarchy/package.json +50 -0
- package/vendor/types/@types/d3-interpolate/LICENSE +21 -0
- package/vendor/types/@types/d3-interpolate/index.d.ts +387 -0
- package/vendor/types/@types/d3-interpolate/package.json +47 -0
- package/vendor/types/@types/d3-path/LICENSE +21 -0
- package/vendor/types/@types/d3-path/index.d.ts +108 -0
- package/vendor/types/@types/d3-path/package.json +41 -0
- package/vendor/types/@types/d3-polygon/LICENSE +21 -0
- package/vendor/types/@types/d3-polygon/index.d.ts +41 -0
- package/vendor/types/@types/d3-polygon/package.json +40 -0
- package/vendor/types/@types/d3-quadtree/LICENSE +21 -0
- package/vendor/types/@types/d3-quadtree/index.d.ts +204 -0
- package/vendor/types/@types/d3-quadtree/package.json +45 -0
- package/vendor/types/@types/d3-random/LICENSE +21 -0
- package/vendor/types/@types/d3-random/index.d.ts +313 -0
- package/vendor/types/@types/d3-random/package.json +41 -0
- package/vendor/types/@types/d3-scale/LICENSE +21 -0
- package/vendor/types/@types/d3-scale/index.d.ts +2669 -0
- package/vendor/types/@types/d3-scale/package.json +53 -0
- package/vendor/types/@types/d3-scale-chromatic/LICENSE +21 -0
- package/vendor/types/@types/d3-scale-chromatic/index.d.ts +520 -0
- package/vendor/types/@types/d3-scale-chromatic/package.json +46 -0
- package/vendor/types/@types/d3-selection/LICENSE +21 -0
- package/vendor/types/@types/d3-selection/index.d.ts +1210 -0
- package/vendor/types/@types/d3-selection/package.json +56 -0
- package/vendor/types/@types/d3-shape/LICENSE +21 -0
- package/vendor/types/@types/d3-shape/index.d.ts +2792 -0
- package/vendor/types/@types/d3-shape/package.json +53 -0
- package/vendor/types/@types/d3-time/LICENSE +21 -0
- package/vendor/types/@types/d3-time/index.d.ts +697 -0
- package/vendor/types/@types/d3-time/package.json +46 -0
- package/vendor/types/@types/d3-time-format/LICENSE +21 -0
- package/vendor/types/@types/d3-time-format/index.d.ts +202 -0
- package/vendor/types/@types/d3-time-format/package.json +40 -0
- package/vendor/types/@types/d3-timer/LICENSE +21 -0
- package/vendor/types/@types/d3-timer/index.d.ts +68 -0
- package/vendor/types/@types/d3-timer/package.json +45 -0
- package/vendor/types/@types/d3-transition/LICENSE +21 -0
- package/vendor/types/@types/d3-transition/index.d.ts +664 -0
- package/vendor/types/@types/d3-transition/package.json +47 -0
- package/vendor/types/@types/d3-zoom/LICENSE +21 -0
- package/vendor/types/@types/d3-zoom/index.d.ts +611 -0
- package/vendor/types/@types/d3-zoom/package.json +48 -0
- package/vendor/types/@types/geojson/LICENSE +21 -0
- package/vendor/types/@types/geojson/index.d.ts +202 -0
- package/vendor/types/@types/geojson/package.json +46 -0
- package/vendor/typescript/LICENSE.txt +55 -0
- package/vendor/typescript/lib.decorators.d.ts +384 -0
- package/vendor/typescript/lib.decorators.legacy.d.ts +22 -0
- package/vendor/typescript/lib.dom.d.ts +39429 -0
- package/vendor/typescript/lib.dom.iterable.d.ts +571 -0
- package/vendor/typescript/lib.es2015.collection.d.ts +147 -0
- package/vendor/typescript/lib.es2015.core.d.ts +597 -0
- package/vendor/typescript/lib.es2015.d.ts +28 -0
- package/vendor/typescript/lib.es2015.generator.d.ts +77 -0
- package/vendor/typescript/lib.es2015.iterable.d.ts +605 -0
- package/vendor/typescript/lib.es2015.promise.d.ts +81 -0
- package/vendor/typescript/lib.es2015.proxy.d.ts +128 -0
- package/vendor/typescript/lib.es2015.reflect.d.ts +144 -0
- package/vendor/typescript/lib.es2015.symbol.d.ts +46 -0
- package/vendor/typescript/lib.es2015.symbol.wellknown.d.ts +326 -0
- package/vendor/typescript/lib.es2016.array.include.d.ts +116 -0
- package/vendor/typescript/lib.es2016.d.ts +21 -0
- package/vendor/typescript/lib.es2016.intl.d.ts +31 -0
- package/vendor/typescript/lib.es2017.arraybuffer.d.ts +21 -0
- package/vendor/typescript/lib.es2017.d.ts +26 -0
- package/vendor/typescript/lib.es2017.date.d.ts +31 -0
- package/vendor/typescript/lib.es2017.intl.d.ts +44 -0
- package/vendor/typescript/lib.es2017.object.d.ts +49 -0
- package/vendor/typescript/lib.es2017.sharedmemory.d.ts +135 -0
- package/vendor/typescript/lib.es2017.string.d.ts +45 -0
- package/vendor/typescript/lib.es2017.typedarrays.d.ts +53 -0
- package/vendor/typescript/lib.es2018.asyncgenerator.d.ts +77 -0
- package/vendor/typescript/lib.es2018.asynciterable.d.ts +53 -0
- package/vendor/typescript/lib.es2018.d.ts +24 -0
- package/vendor/typescript/lib.es2018.intl.d.ts +83 -0
- package/vendor/typescript/lib.es2018.promise.d.ts +30 -0
- package/vendor/typescript/lib.es2018.regexp.d.ts +37 -0
- package/vendor/typescript/lib.es2019.array.d.ts +79 -0
- package/vendor/typescript/lib.es2019.d.ts +24 -0
- package/vendor/typescript/lib.es2019.intl.d.ts +23 -0
- package/vendor/typescript/lib.es2019.object.d.ts +33 -0
- package/vendor/typescript/lib.es2019.string.d.ts +37 -0
- package/vendor/typescript/lib.es2019.symbol.d.ts +24 -0
- package/vendor/typescript/lib.es2020.bigint.d.ts +765 -0
- package/vendor/typescript/lib.es2020.d.ts +27 -0
- package/vendor/typescript/lib.es2020.date.d.ts +42 -0
- package/vendor/typescript/lib.es2020.intl.d.ts +474 -0
- package/vendor/typescript/lib.es2020.number.d.ts +28 -0
- package/vendor/typescript/lib.es2020.promise.d.ts +47 -0
- package/vendor/typescript/lib.es2020.sharedmemory.d.ts +99 -0
- package/vendor/typescript/lib.es2020.string.d.ts +44 -0
- package/vendor/typescript/lib.es2020.symbol.wellknown.d.ts +41 -0
- package/vendor/typescript/lib.es2021.d.ts +23 -0
- package/vendor/typescript/lib.es2021.intl.d.ts +166 -0
- package/vendor/typescript/lib.es2021.promise.d.ts +48 -0
- package/vendor/typescript/lib.es2021.string.d.ts +33 -0
- package/vendor/typescript/lib.es2021.weakref.d.ts +78 -0
- package/vendor/typescript/lib.es2022.array.d.ts +121 -0
- package/vendor/typescript/lib.es2022.d.ts +25 -0
- package/vendor/typescript/lib.es2022.error.d.ts +75 -0
- package/vendor/typescript/lib.es2022.intl.d.ts +145 -0
- package/vendor/typescript/lib.es2022.object.d.ts +26 -0
- package/vendor/typescript/lib.es2022.regexp.d.ts +39 -0
- package/vendor/typescript/lib.es2022.string.d.ts +25 -0
- package/vendor/typescript/lib.es5.d.ts +4601 -0
- package/vendor/typescript/package.json +4 -0
- package/vendor/typescript/typescript.js +446 -0
- package/web/assets/index-B6Ai3wsa.js +13 -0
- package/web/assets/index-SYPC3EZy.css +1 -0
- package/web/index.html +14 -0
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
import { escapeAttribute, escapeText, escapeTyped, unescapeText } from "../../shared/escape.mjs";
|
|
2
|
+
import { formatRange, isInkClass, isUnderlineClass, markValue } from "./marks.mjs";
|
|
3
|
+
import { ZWSP, inlineItems, spaceAround } from "../../shared/inline-spacing.mjs";
|
|
4
|
+
import { ATTRIBUTES, BLOCK_TYPES, HOLDS_ONE, HOSTS, HOST_NAMES, PART_SELECTOR, REMOVED_EMPTY, SIZES, TEXT_BLOCKS, TOGGLES, attributeValue, blockTypeOf, capsFor, firstBlockOf, leadOf, removedFromAnywhere, sizeValue, textTypeOf } from "../components/registry.mjs";
|
|
5
|
+
import { isNoteTab } from "../components/paragraph/index.mjs";
|
|
6
|
+
import { DIALOG_TRIGGER, cardNamed } from "../components/dialog/index.mjs";
|
|
7
|
+
import { COPY_KEYS } from "../components/figure/index.mjs";
|
|
8
|
+
import { panelOf } from "../components/sheets/index.mjs";
|
|
9
|
+
import { gridOf, tableChange } from "../components/table/grid.mjs";
|
|
10
|
+
import { HELD_TAGS, heldListType, listType } from "../components/list/index.mjs";
|
|
11
|
+
import { isFlowchart, isStep } from "../components/flowchart/index.mjs";
|
|
12
|
+
import { markEmpty } from "../components/placeholder.mjs";
|
|
13
|
+
import { unread } from "./tex.mjs";
|
|
14
|
+
import { MENTION, MENTION_ICON, MENTION_PAGE } from "../../shared/components/mention.mjs";
|
|
15
|
+
import { FLOW_BLOCKS } from "../../shared/components/index.mjs";
|
|
16
|
+
import { giveLine } from "../../shared/components/list.mjs";
|
|
17
|
+
import { isFileLink } from "../../shared/page-address.mjs";
|
|
18
|
+
import { ownInline } from "./own.mjs";
|
|
19
|
+
import { pageOfUnit } from "./page-objects.mjs";
|
|
20
|
+
import { t } from "../../shared/i18n/index.mjs";
|
|
21
|
+
|
|
22
|
+
// Browser core of in-place text editing (DOM only; tested with jsdom).
|
|
23
|
+
// The editing DOM of a block is rebuilt from the block's source, never from the
|
|
24
|
+
// component-enhanced runtime DOM. Inline formulas stay rendered as non-editable chips and are
|
|
25
|
+
// written back as their exact source text. Markers are data-ge-* attributes, so a block can be
|
|
26
|
+
// restored from an innerHTML snapshot (undo/redo) without losing them; they are never saved.
|
|
27
|
+
|
|
28
|
+
// Atoms: inline formulas, and links shown as their page's icon and title (图标视图: links/views.mjs),
|
|
29
|
+
// and inline elements of the report's own (own.mjs: a badge, a citation), kept whole as the file
|
|
30
|
+
// writes them (data-ge-own: none of a formula's looks).
|
|
31
|
+
const FORMULA = ".doc-math-inline";
|
|
32
|
+
const ATOM_SELECTOR = `${FORMULA}, .${MENTION}`;
|
|
33
|
+
const ATOM_SOURCE = new Map(); // data-ge-atom value -> its source (a formula's delimiters and escapes included)
|
|
34
|
+
const ATOM_PREFIX = Math.random().toString(36).slice(2, 7); // keys of another page never collide with this page's
|
|
35
|
+
let atomCount = 0;
|
|
36
|
+
const TAGS = { STRONG: "strong", B: "strong", EM: "em", I: "em", U: "u", S: "s", STRIKE: "s", DEL: "s", CODE: "code", A: "a", BR: "br", SUP: "sup", SUB: "sub" };
|
|
37
|
+
// (and data-rs-*, their name before 2026-10-28: a file may keep one an older page left in it)
|
|
38
|
+
const MARKER = /^(contenteditable|data-ge-|data-rs-)/;
|
|
39
|
+
const WRAPPERS = new Set(["UL", "OL", "BLOCKQUOTE"]);
|
|
40
|
+
|
|
41
|
+
// An inline element of the report's own, as the scanner judges it (source/blocks.mjs): its
|
|
42
|
+
// attributes as the file writes them (not the page's data-ge-* markers).
|
|
43
|
+
const isOwnAtom = (node) => ownInline(node.localName, [...node.attributes].filter(({ name }) => !MARKER.test(name)), node.textContent.trim() !== "");
|
|
44
|
+
|
|
45
|
+
// A block's atoms in `root` (its source parsed), in the order of the file, as the scanner lists
|
|
46
|
+
// them (source/blocks.mjs inlineMode): the outermost formulas, links shown with their icon and inline
|
|
47
|
+
// elements of the report's own; `all`, every element (the text of an inline element of the report's own).
|
|
48
|
+
function atomsIn(root, all = false) {
|
|
49
|
+
const atom = (node) => all || node.matches(ATOM_SELECTOR) || isOwnAtom(node);
|
|
50
|
+
const inAtom = (node) => { for (let at = node.parentElement; at; at = at.parentElement) if (atom(at)) return true; return false; };
|
|
51
|
+
return [...root.querySelectorAll("*")].filter((node) => atom(node) && !inAtom(node));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What prepareBlock makes editable or not of a block, set ahead of it (editor.mjs enter: every block
|
|
56
|
+
* first, then every block rebuilt): the host, the lists and blocks an item holds (not its text), the
|
|
57
|
+
* formulas the kit drew in it. Chrome recalculates the page's style and lays it out at once whenever an
|
|
58
|
+
* element's contenteditable changes; once blocks have been rebuilt (their content put in again, not
|
|
59
|
+
* laid out yet) that is most of the page each time, a report of 800 blocks taking 12s to open. Set
|
|
60
|
+
* while the page is as it was served, each costs nothing, and content put into a host already
|
|
61
|
+
* editable forces nothing.
|
|
62
|
+
*/
|
|
63
|
+
export function editableAhead(element, { kind = "flow" } = {}) {
|
|
64
|
+
const lists = nestedBlocks(element);
|
|
65
|
+
lists.forEach((list) => list.setAttribute("contenteditable", "false"));
|
|
66
|
+
for (const atom of element.querySelectorAll(ATOM_SELECTOR)) if (!lists.some((list) => list.contains(atom))) atom.setAttribute("contenteditable", "false");
|
|
67
|
+
if (!HOSTS[kind]?.wrapped) element.setAttribute("contenteditable", "true");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// contenteditable set only where it changes (setting it again to what it is costs as a change: editableAhead).
|
|
71
|
+
const editable = (element, value) => { if (element.getAttribute("contenteditable") !== value) element.setAttribute("contenteditable", value); };
|
|
72
|
+
|
|
73
|
+
/** A text block the reader may not write in from now on, and why (`reason`, which hints/ says on a hover). */
|
|
74
|
+
export function lockHost(host, reason) {
|
|
75
|
+
editable(host, "false");
|
|
76
|
+
host.setAttribute("data-ge-lock", reason);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rebuild one [data-ge-block] element for editing from its source. Returns its baseline
|
|
81
|
+
* serialization. A host whose element must keep its own behaviour (`wrapped`: a Sheet tab, a button
|
|
82
|
+
* that must keep switching Sheets) gets its text in a span of its own, which becomes the block;
|
|
83
|
+
* the editor makes that span contenteditable only while the tab is renamed.
|
|
84
|
+
*/
|
|
85
|
+
export function prepareBlock(element, { inner, atoms, kind = "flow", caps = [] }) {
|
|
86
|
+
element.setAttribute("data-ge-kind", kind);
|
|
87
|
+
element.setAttribute("data-ge-caps", caps.join(" "));
|
|
88
|
+
const template = element.ownerDocument.createElement("template");
|
|
89
|
+
template.innerHTML = inner;
|
|
90
|
+
template.content.querySelectorAll("*").forEach((node) => node.setAttribute("data-ge-src", ""));
|
|
91
|
+
const sourceAtoms = atomsIn(template.content, kind === "text" && caps.includes("plain"));
|
|
92
|
+
const drawnAtoms = sourceAtoms.filter((atom) => atom.matches(ATOM_SELECTOR)); // a formula as the kit drew it
|
|
93
|
+
// The blocks a list item holds are blocks of their own: only its text before them is rebuilt.
|
|
94
|
+
const lists = nestedBlocks(element);
|
|
95
|
+
const rendered = [...element.querySelectorAll(ATOM_SELECTOR)].filter((atom) => !lists.some((list) => list.contains(atom)));
|
|
96
|
+
sourceAtoms.forEach((atom, index) => {
|
|
97
|
+
const drawn = drawnAtoms.indexOf(atom);
|
|
98
|
+
const chip = drawn >= 0 && rendered.length === drawnAtoms.length ? rendered[drawn] : atom; // mismatch: show the raw TeX
|
|
99
|
+
if (drawn < 0) chip.setAttribute("data-ge-own", ""); // the report's own: as the file has it
|
|
100
|
+
// The breaks the kit put between the boxes of a formula it held with the marks after it go with
|
|
101
|
+
// the span that held them (runtime/spacing.js: the block's text is rebuilt from the file).
|
|
102
|
+
chip.querySelectorAll(".doc-math-break").forEach((gap) => gap.remove());
|
|
103
|
+
const key = `${ATOM_PREFIX}-${atomCount++}`;
|
|
104
|
+
editable(chip, "false");
|
|
105
|
+
chip.setAttribute("data-ge-atom", key);
|
|
106
|
+
const source = inner.slice(atoms[index][0], atoms[index][1]);
|
|
107
|
+
ATOM_SOURCE.set(key, source);
|
|
108
|
+
// One left empty is its gray chip (as served already, components/placeholder.mjs).
|
|
109
|
+
const empty = emptyKind(source);
|
|
110
|
+
if (empty) markEmpty(chip, empty);
|
|
111
|
+
else if (drawn >= 0 && chip.matches(FORMULA) && unread(chip, formulaParts(chip)?.tex ?? "")) markEmpty(chip, "invalid");
|
|
112
|
+
atom.replaceWith(chip);
|
|
113
|
+
});
|
|
114
|
+
if (lists.length > 0) {
|
|
115
|
+
ownNodes(element).forEach((node) => node.remove());
|
|
116
|
+
lists[0].before(template.content);
|
|
117
|
+
// Nothing after the blocks it holds (the source's line breaks), where a caret could put text that is not the item's.
|
|
118
|
+
while (lists.at(-1).nextSibling) lists.at(-1).nextSibling.remove();
|
|
119
|
+
lists.forEach((list) => editable(list, "false"));
|
|
120
|
+
keepLine(element);
|
|
121
|
+
element.setAttribute("data-ge-nested", ""); // it stays an item (convert.mjs)
|
|
122
|
+
} else element.replaceChildren(template.content);
|
|
123
|
+
// Inline code and formulas stand a space apart from the words beside them, where the report's
|
|
124
|
+
// text has none (the rule the kit shows it by, shared/inline-spacing.mjs); an empty formula and a
|
|
125
|
+
// link shown with its icon do not. What the commits compare with (the baseline returned) has the
|
|
126
|
+
// spaces, and what they send as the block's source does not: the file takes them only with the
|
|
127
|
+
// reader's own edit of the block.
|
|
128
|
+
const wrapped = Boolean(HOSTS[kind]?.wrapped);
|
|
129
|
+
if (!wrapped) {
|
|
130
|
+
editable(element, "true"); // the edge of the line the rule looks along (as shownAs, on a detached element, finds it)
|
|
131
|
+
for (const inline of inlineItems(element)) if (inline !== element && !lists.some((list) => list.contains(inline))) spaceAround(inline);
|
|
132
|
+
}
|
|
133
|
+
if (wrapped) {
|
|
134
|
+
const name = element.ownerDocument.createElement("span");
|
|
135
|
+
name.append(...element.childNodes);
|
|
136
|
+
element.append(name);
|
|
137
|
+
for (const marker of ["data-ge-block", "data-ge-kind", "data-ge-caps"]) {
|
|
138
|
+
name.setAttribute(marker, element.getAttribute(marker));
|
|
139
|
+
element.removeAttribute(marker);
|
|
140
|
+
}
|
|
141
|
+
return serializeInner(name);
|
|
142
|
+
}
|
|
143
|
+
return serializeInner(element);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The html the page compares a block with (its baseline) when the file has `inner` for it (its
|
|
148
|
+
* formulas at `atoms`): what prepareBlock makes of that text as the page opens, worked out on an
|
|
149
|
+
* element of its own, so that `host` is not touched. Null when the formulas are not given.
|
|
150
|
+
*/
|
|
151
|
+
export function shownAs(host, inner, atoms = []) {
|
|
152
|
+
const template = host.ownerDocument.createElement("template");
|
|
153
|
+
template.innerHTML = inner;
|
|
154
|
+
const kind = host.getAttribute("data-ge-kind") ?? "flow";
|
|
155
|
+
const caps = (host.getAttribute("data-ge-caps") ?? "").split(" ");
|
|
156
|
+
if (atomsIn(template.content, kind === "text" && caps.includes("plain")).length !== atoms.length) return null;
|
|
157
|
+
return prepareBlock(host.ownerDocument.createElement(host.localName), { inner, atoms, kind, caps });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A new inline formula (formula/): a chip showing `rendered` (KaTeX's markup of `tex`), written
|
|
162
|
+
* back as the kit's `<span class="doc-math-inline">\(tex\)</span>`; `parts` (formulaParts of
|
|
163
|
+
* the formula it replaces): its own tags kept, with whatever attributes the file gave them.
|
|
164
|
+
*/
|
|
165
|
+
export function newFormula(doc, tex, rendered, parts = null) {
|
|
166
|
+
const chip = doc.createElement("span");
|
|
167
|
+
chip.className = "doc-math-inline";
|
|
168
|
+
chip.innerHTML = rendered;
|
|
169
|
+
const key = `${ATOM_PREFIX}-${atomCount++}`;
|
|
170
|
+
chip.setAttribute("contenteditable", "false");
|
|
171
|
+
chip.setAttribute("data-ge-atom", key);
|
|
172
|
+
ATOM_SOURCE.set(key, `${parts?.open ?? '<span class="doc-math-inline">'}\\(${spaced(tex)}\\)${parts?.close ?? "</span>"}`);
|
|
173
|
+
if (!tex.trim()) markEmpty(chip, "formula");
|
|
174
|
+
return chip;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A link left empty (the "/" menu's 链接 given no address): a gray chip, 链接 (data-ge-empty="link",
|
|
179
|
+
* components/placeholder.mjs), written as an empty <a class="doc-link-mention"></a>, which the kit
|
|
180
|
+
* shows to no reader; a click on it asks for its address (links/). With `page` a page block left
|
|
181
|
+
* empty, 页面, written with data-doc-page. An empty formula is newFormula's of no TeX (\(\)). Each
|
|
182
|
+
* stays as an empty block does, a whole as a character is (its icon and name are in it, so the browser
|
|
183
|
+
* keeps a caret beside it and deletes it as a formula).
|
|
184
|
+
*/
|
|
185
|
+
export function newEmptyLink(doc, page = false) {
|
|
186
|
+
const chip = doc.createElement("a");
|
|
187
|
+
chip.className = MENTION;
|
|
188
|
+
const key = `${ATOM_PREFIX}-${atomCount++}`;
|
|
189
|
+
chip.setAttribute("contenteditable", "false");
|
|
190
|
+
chip.setAttribute("data-ge-atom", key);
|
|
191
|
+
markEmpty(chip, page ? "page" : "link");
|
|
192
|
+
ATOM_SOURCE.set(key, page ? `<a class="${MENTION}" ${MENTION_PAGE}></a>` : `<a class="${MENTION}"></a>`);
|
|
193
|
+
return chip;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const EMPTY_FORMULA = /^<span\b[^>]*>\s*\\\(\s*\\\)\s*<\/span>$/;
|
|
197
|
+
const EMPTY_LINK = new RegExp(`^<a class="${MENTION}">\\s*</a>$`);
|
|
198
|
+
const EMPTY_PAGE = new RegExp(`^<a\\b[^>]*\\b${MENTION_PAGE}\\b[^>]*>\\s*</a>$`);
|
|
199
|
+
|
|
200
|
+
/** What an atom's source leaves empty: "formula" (\(\)), "link" (an empty link shown with its icon), "page" (an empty page block), or null. */
|
|
201
|
+
export const emptyKind = (source) => (EMPTY_FORMULA.test(source) ? "formula" : EMPTY_LINK.test(source) ? "link" : EMPTY_PAGE.test(source) ? "page" : null);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A chip standing for `source` (written back as it is): a display formula's KaTeX in its block
|
|
205
|
+
* (components/math/page.mjs), `nodes` its content. Like an inline formula's, its source lives in
|
|
206
|
+
* the chip's key, so undo, which puts the chip back, puts the source back too.
|
|
207
|
+
*/
|
|
208
|
+
export function newAtom(doc, source, nodes, tag = "span") {
|
|
209
|
+
const chip = doc.createElement(tag);
|
|
210
|
+
chip.append(...nodes);
|
|
211
|
+
const key = `${ATOM_PREFIX}-${atomCount++}`;
|
|
212
|
+
chip.setAttribute("contenteditable", "false");
|
|
213
|
+
chip.setAttribute("data-ge-atom", key);
|
|
214
|
+
ATOM_SOURCE.set(key, source);
|
|
215
|
+
return chip;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Whether `html` holds a chip of this page (data-ge-atom: a formula, a link shown with its icon, one left empty). */
|
|
219
|
+
export function holdsAtoms(doc, html) {
|
|
220
|
+
const template = doc.createElement("template");
|
|
221
|
+
template.innerHTML = html;
|
|
222
|
+
return [...template.content.querySelectorAll("[data-ge-atom]")].some((node) => ATOM_SOURCE.has(node.getAttribute("data-ge-atom")));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What a chip is written back as (its source), or null for no chip of this page. */
|
|
226
|
+
export const atomSource = (chip) => ATOM_SOURCE.get(chip?.getAttribute?.("data-ge-atom")) ?? null;
|
|
227
|
+
/** The sources of the chips in `roots`, by key: what another page needs to write them back (sync-state.mjs pack). */
|
|
228
|
+
export function atomSources(roots) {
|
|
229
|
+
const sources = {};
|
|
230
|
+
for (const root of roots) {
|
|
231
|
+
for (const chip of root.querySelectorAll("[data-ge-atom]")) {
|
|
232
|
+
const key = chip.getAttribute("data-ge-atom");
|
|
233
|
+
if (ATOM_SOURCE.has(key)) sources[key] = ATOM_SOURCE.get(key);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return sources;
|
|
237
|
+
}
|
|
238
|
+
/** Chips of another page (atomSources) known to this one, so that a text holding them writes them back; its keys never collide with this page's. */
|
|
239
|
+
export function knowAtoms(sources) {
|
|
240
|
+
for (const [key, source] of Object.entries(sources ?? {})) {
|
|
241
|
+
if (typeof source === "string" && !ATOM_SOURCE.has(key)) ATOM_SOURCE.set(key, source);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The words the file gives a chip, which the page may show otherwise (a page block's title now), or null for no chip of this page. */
|
|
246
|
+
export function sourceWords(chip) {
|
|
247
|
+
const source = atomSource(chip);
|
|
248
|
+
if (source === null) return null;
|
|
249
|
+
const template = chip.ownerDocument.createElement("template");
|
|
250
|
+
template.innerHTML = source;
|
|
251
|
+
return template.content.textContent;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A new link shown as its page's icon and title (图标视图, links/views.mjs), or a page block: a chip
|
|
256
|
+
* of the page (`assetUrl(path)`: where the preview serves the icon, a file of the report's folder),
|
|
257
|
+
* written back as shared/components/mention.mjs says, its icon an empty span (the kit's globe or page)
|
|
258
|
+
* when it has none. `drawn`: the image shown for a page block's icon, which no file holds.
|
|
259
|
+
*/
|
|
260
|
+
export function newMention(doc, { href, title, icon = null, drawn = null }, assetUrl = (path) => path) {
|
|
261
|
+
const chip = doc.createElement("a");
|
|
262
|
+
chip.className = MENTION;
|
|
263
|
+
chip.setAttribute("href", href);
|
|
264
|
+
if (icon || drawn) {
|
|
265
|
+
const img = doc.createElement("img");
|
|
266
|
+
img.className = MENTION_ICON;
|
|
267
|
+
img.src = drawn ?? assetUrl(icon);
|
|
268
|
+
img.alt = "";
|
|
269
|
+
img.draggable = false;
|
|
270
|
+
if (icon) img.setAttribute("data-ge-src", icon);
|
|
271
|
+
chip.append(img);
|
|
272
|
+
} else {
|
|
273
|
+
const globe = doc.createElement("span");
|
|
274
|
+
globe.className = MENTION_ICON;
|
|
275
|
+
chip.append(globe);
|
|
276
|
+
}
|
|
277
|
+
chip.append(doc.createTextNode(title));
|
|
278
|
+
const key = `${ATOM_PREFIX}-${atomCount++}`;
|
|
279
|
+
chip.setAttribute("contenteditable", "false");
|
|
280
|
+
chip.setAttribute("data-ge-atom", key);
|
|
281
|
+
const picture = icon ? `<img class="${MENTION_ICON}" src="${escapeAttribute(icon)}" alt="">` : `<span class="${MENTION_ICON}"></span>`;
|
|
282
|
+
ATOM_SOURCE.set(key, `<a class="${MENTION}" href="${escapeAttribute(href)}">${picture}${spaced(title)}</a>`);
|
|
283
|
+
return chip;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* What a link shown with its icon is: { href, title, icon } (its icon a file of the report's folder,
|
|
288
|
+
* or null), from the chip on the page, or from one pasted (its icon where the preview served it,
|
|
289
|
+
* when it was: `slug`, the report's folder it was served from; `drawn`: the image a page block shows,
|
|
290
|
+
* by the icon route).
|
|
291
|
+
*/
|
|
292
|
+
export function mentionOf(chip) {
|
|
293
|
+
const img = chip.querySelector(`img.${MENTION_ICON}`);
|
|
294
|
+
const src = img?.getAttribute("data-ge-src") ?? img?.getAttribute("src") ?? "";
|
|
295
|
+
const served = /^\/__gallery\/edit\/asset\/([^/]+)\/(assets\/[^/?#]+)$/.exec(src);
|
|
296
|
+
return {
|
|
297
|
+
href: chip.getAttribute("href") ?? "",
|
|
298
|
+
title: chip.textContent.replace(/\s+/g, " ").trim(),
|
|
299
|
+
icon: served ? served[2] : /^assets\/[^/]+$/.test(src) ? src : null,
|
|
300
|
+
slug: served ? decodeURIComponent(served[1]) : null,
|
|
301
|
+
drawn: src.startsWith("/__gallery/edit/icon?") ? src : null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The words the page shows for a link shown with its icon to `href`, which those of the file may be behind (a page block's title), or null for none on the page. */
|
|
306
|
+
export function shownWords(doc, href) {
|
|
307
|
+
return [...doc.querySelectorAll(`main .${MENTION}`)].find((chip) => chip.getAttribute("href") === href)?.textContent ?? null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Whether a node is a link shown with its icon (a chip of the page, or one pasted). */
|
|
311
|
+
export const isMention = (node) => node?.nodeType === 1 && node.matches(`a.${MENTION}`);
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* An inline formula's source in parts (formula/): the tag that opens it, its TeX and the tag that
|
|
315
|
+
* closes it; null when it is not written as \(…\) in one span (then the page leaves it alone).
|
|
316
|
+
*/
|
|
317
|
+
export function formulaParts(chip) {
|
|
318
|
+
const source = ATOM_SOURCE.get(chip.getAttribute("data-ge-atom"));
|
|
319
|
+
const match = source?.match(/^(<span\b[^>]*>)\s*\\\(([\s\S]*)\\\)\s*(<\/span>)$/);
|
|
320
|
+
return match ? { open: match[1], tex: unescapeText(match[2]), close: match[3] } : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Whether the page still shows the block's source text. Scripts fill some elements at runtime
|
|
325
|
+
* (status lines, counters); rebuilding those from source would wipe what the script wrote, so the
|
|
326
|
+
* editor leaves them alone. Formulas and decorative (aria-hidden) parts are not compared, nor
|
|
327
|
+
* whitespace (the kit spaces inline code and formulas apart from the words beside them).
|
|
328
|
+
*/
|
|
329
|
+
export function matchesSource(element, inner) {
|
|
330
|
+
// The blocks a list item holds are blocks of their own, not its text (a table cell's lists are its text).
|
|
331
|
+
const text = (root, nested) => {
|
|
332
|
+
const copy = root.cloneNode(true);
|
|
333
|
+
if (nested) nestedBlocks(copy).forEach((node) => node.remove());
|
|
334
|
+
copy.querySelectorAll(`${ATOM_SELECTOR}, [aria-hidden="true"]`).forEach((node) => node.remove());
|
|
335
|
+
return copy.textContent.replace(/\s+/g, "");
|
|
336
|
+
};
|
|
337
|
+
const template = element.ownerDocument.createElement("template");
|
|
338
|
+
template.innerHTML = inner;
|
|
339
|
+
return text(element, element.tagName === "LI") === text(template.content, false);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function serializeInner(element) {
|
|
343
|
+
const inner = HOSTS[element.getAttribute?.("data-ge-kind")]?.inner;
|
|
344
|
+
if (inner) return inner(element);
|
|
345
|
+
// A table cell's lists are its text (单元格里的列表); any other block's are blocks of their own.
|
|
346
|
+
const lists = element.getAttribute?.("data-ge-kind") === "cell";
|
|
347
|
+
return ownNodes(element).map((node) => serializeNode(node, lists)).join("").replace(/<br>\s*$/, "");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// A list in a table cell, its items' inline content (a line break holding an empty item's caret left out).
|
|
351
|
+
function serializeList(list) {
|
|
352
|
+
const tag = list.tagName.toLowerCase();
|
|
353
|
+
const items = [...list.children].filter((item) => item.tagName === "LI").map((item) => `<li>${[...item.childNodes].map((child) => serializeNode(child)).join("").replace(/<br>\s*$/, "")}</li>`);
|
|
354
|
+
return items.length > 0 ? `<${tag}>${items.join("")}</${tag}>` : "";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------- list items holding blocks ----------
|
|
358
|
+
// A list item's text comes first, the blocks it holds after it (notion-lite's children of a list
|
|
359
|
+
// item): its nested lists (with items of their own) and any other block, a paragraph, an image, a
|
|
360
|
+
// code block…: <li>text<p>…</p><ul><li>…</li></ul></li> (components/list/ HELD_TAGS). The item's text
|
|
361
|
+
// block is only the text; the blocks it holds are never part of its content, its snapshots or its
|
|
362
|
+
// caret positions. A flowchart's step is an li too, but no list item: a node holding blocks and no
|
|
363
|
+
// text (components/flowchart/), which the lines and the items below never go through (parentItem,
|
|
364
|
+
// holderItem, previousLine and nextLine stop at it), as they never go through a highlight block.
|
|
365
|
+
|
|
366
|
+
const isList = (node) => node?.nodeType === 1 && (node.tagName === "UL" || node.tagName === "OL");
|
|
367
|
+
const isHeld = (node) => node?.nodeType === 1 && HELD_TAGS.has(node.localName);
|
|
368
|
+
|
|
369
|
+
/** The blocks a list item holds after its text, in order ([] for any other element). */
|
|
370
|
+
export const nestedBlocks = (element) => element?.tagName === "LI" ? [...element.children].filter(isHeld) : [];
|
|
371
|
+
|
|
372
|
+
/** The lists nested in a list item, in order ([] for any other element). */
|
|
373
|
+
export const nestedLists = (element) => nestedBlocks(element).filter(isList);
|
|
374
|
+
|
|
375
|
+
/** The list item holding a block (a list nested in it, a paragraph under it), or null; a flowchart's step holds blocks as a container does, no item. */
|
|
376
|
+
export const holderItem = (block) => (block?.parentElement?.tagName === "LI" && !isStep(block.parentElement) && isHeld(block) ? block.parentElement : null);
|
|
377
|
+
|
|
378
|
+
/** The child offset where an element's own content ends: before the first block a list item holds. */
|
|
379
|
+
export function textEnd(element) {
|
|
380
|
+
const index = element.tagName === "LI" ? [...element.childNodes].findIndex(isHeld) : -1;
|
|
381
|
+
return index < 0 ? element.childNodes.length : index;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const ownNodes = (element) => [...element.childNodes].slice(0, textEnd(element));
|
|
385
|
+
|
|
386
|
+
/** A block's own content as HTML (innerHTML, without the blocks a list item holds). */
|
|
387
|
+
export function ownHTML(element) {
|
|
388
|
+
if (nestedBlocks(element).length === 0) return element.innerHTML;
|
|
389
|
+
const copy = element.ownerDocument.createElement("div");
|
|
390
|
+
copy.append(...ownNodes(element).map((node) => node.cloneNode(true)));
|
|
391
|
+
return copy.innerHTML;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Replace a block's own content (the blocks a list item holds stay as they are). */
|
|
395
|
+
export function setOwnHTML(element, html) {
|
|
396
|
+
const lists = nestedBlocks(element);
|
|
397
|
+
if (lists.length === 0) {
|
|
398
|
+
element.innerHTML = html;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const template = element.ownerDocument.createElement("template");
|
|
402
|
+
template.innerHTML = html;
|
|
403
|
+
ownNodes(element).forEach((node) => node.remove());
|
|
404
|
+
lists[0].before(template.content);
|
|
405
|
+
keepLine(element);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* An item with no text over the blocks it holds keeps a line of its own, as notion-lite's item keeps
|
|
410
|
+
* its title line however empty: a line break ahead of them, which Backspace leaves there too and its
|
|
411
|
+
* text leaves out (serializeInner), so that a click on its marker or an arrow key puts the caret in it.
|
|
412
|
+
* Which items, and the line break's class, are the kit's wherever it draws the page (keepsLine of
|
|
413
|
+
* src/shared/components/list.mjs, src/render/kit/runtime/spacing.js): an item of a list whose markers
|
|
414
|
+
* the kit draws in a flow, never one of a widget's list or a list with a class of its own; an export
|
|
415
|
+
* of the view takes the line break off.
|
|
416
|
+
*/
|
|
417
|
+
export function keepLine(element) {
|
|
418
|
+
giveLine(element, FLOW_BLOCKS); // none but an item of a list keeps one
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The list item a nested item's list hangs from, or null for an item of a top-level list (one of a list in a flowchart's step among them: the step is no item). */
|
|
422
|
+
export const parentItem = (item) => item?.tagName === "LI" && item.parentElement?.parentElement?.tagName === "LI" && !isStep(item.parentElement.parentElement) ? item.parentElement.parentElement : null;
|
|
423
|
+
|
|
424
|
+
// Spaces are written as the page shows them. The non-breaking spaces a browser types to keep a
|
|
425
|
+
// space visible while it is edited (at the end of a text, before a mark, next to another space),
|
|
426
|
+
// with the spaces beside them, become one ordinary space: left in the file they would glue the
|
|
427
|
+
// words around them, and lines would break early. Runs of ordinary spaces (the source's own line
|
|
428
|
+
// indentation) stay as they are. Reports write no of their own; one in a text the reader
|
|
429
|
+
// edits becomes a space too.
|
|
430
|
+
const spaced = (text) => escapeTyped(text).replace(/ *\u00a0[ \u00a0]*/g, " ");
|
|
431
|
+
|
|
432
|
+
function serializeNode(node, lists = false) {
|
|
433
|
+
// A text node holding nothing but a zero-width space keeps the caret after a mark the editor
|
|
434
|
+
// made (markdown/, typing shortcuts); it is never content.
|
|
435
|
+
if (node.nodeType === 3) return node.data === ZWSP ? "" : spaced(node.data);
|
|
436
|
+
if (node.nodeType !== 1) return "";
|
|
437
|
+
if (isList(node)) return lists ? serializeList(node) : "";
|
|
438
|
+
const atom = node.getAttribute("data-ge-atom");
|
|
439
|
+
if (atom !== null && ATOM_SOURCE.has(atom)) return ATOM_SOURCE.get(atom);
|
|
440
|
+
// Elements from the source keep the author's tag and attributes; only elements the browser
|
|
441
|
+
// created while editing are normalized (<b> -> <strong>). Text colors and highlights are spans
|
|
442
|
+
// with the kit's classes only. Unknown wrappers such as <span style> or <font> are unwrapped
|
|
443
|
+
// and their text kept.
|
|
444
|
+
const fromSource = node.hasAttribute("data-ge-src") && node.tagName in TAGS;
|
|
445
|
+
const tag = fromSource ? node.tagName.toLowerCase() : TAGS[node.tagName];
|
|
446
|
+
const inner = [...node.childNodes].map((child) => serializeNode(child)).join("");
|
|
447
|
+
const ink = node.tagName === "SPAN" ? [...node.classList].filter(isInkClass) : [];
|
|
448
|
+
if (ink.length > 0) return inner ? `<span class="${ink.join(" ")}">${inner}</span>` : "";
|
|
449
|
+
// Small text's end part (its Tab: components/paragraph/page.mjs), at the line's right end.
|
|
450
|
+
if (isNoteTab(node)) return inner ? `<span class="doc-tab">${inner}</span>` : "";
|
|
451
|
+
if (!tag) return inner;
|
|
452
|
+
if (tag === "br") return "<br>";
|
|
453
|
+
if (!inner) return "";
|
|
454
|
+
// Made on the page: a link's address (and a detail card's class), an underline's kit color and weight.
|
|
455
|
+
const attributes = [...node.attributes].filter((attribute) =>
|
|
456
|
+
fromSource ? !MARKER.test(attribute.name)
|
|
457
|
+
: tag === "a" ? attribute.name === "href" || (attribute.name === "class" && attribute.value === DIALOG_TRIGGER)
|
|
458
|
+
: tag === "u" && attribute.name === "class" && attribute.value.trim().split(/\s+/).every(isUnderlineClass));
|
|
459
|
+
const attributeText = attributes.map((attribute) => ` ${attribute.name}="${escapeAttribute(attribute.value)}"`).join("");
|
|
460
|
+
return `<${tag}${attributeText}>${inner}</${tag}>`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** An editable host: an original block or one created while editing. */
|
|
464
|
+
export function isHost(node) {
|
|
465
|
+
return node?.nodeType === 1 && node.getAttribute("contenteditable") === "true"
|
|
466
|
+
&& (node.hasAttribute("data-ge-block") || node.hasAttribute("data-ge-new") || node.hasAttribute("data-ge-part"));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Capabilities: split, merge, remove, empty, plain, links (see src/editor/source/). */
|
|
470
|
+
export function can(element, cap) {
|
|
471
|
+
return (element.getAttribute("data-ge-caps") ?? "").split(" ").includes(cap);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Why a text block refuses an action ("split", "remove", "merge", "links", "format", "formula"), in
|
|
476
|
+
* the reader's terms (what each kind of text block is called: HOST_NAMES).
|
|
477
|
+
*/
|
|
478
|
+
export function limitText(host, action) {
|
|
479
|
+
const kind = host.getAttribute("data-ge-kind");
|
|
480
|
+
if (kind === "flow" && !can(host, "remove")) {
|
|
481
|
+
return t("页面其他地方引用了这一段(链接、无障碍关联或文档的脚本),不能删除,也不能并入上一段。");
|
|
482
|
+
}
|
|
483
|
+
if (kind === "text" && action === "remove") return t("这段文字可以清空,但不能单独删除;要删掉整块,点左侧的手柄后按 Delete。");
|
|
484
|
+
const name = HOST_NAMES[kind] ?? t("这一段");
|
|
485
|
+
if (action === "split") return can(host, "plain") ? t("{0}不能分段或换行。", name) : t("{0}不能分段;需要换行可以用 Shift+Enter。", name);
|
|
486
|
+
if (action === "remove") return can(host, "empty") ? t("{0}不能删除,但可以清空。", name) : t("{0}不能清空或删除,只能改字。", name);
|
|
487
|
+
if (action === "links") return t("{0}里不能加链接。", name);
|
|
488
|
+
if (action === "format") return t("{0}不能设置格式。", name);
|
|
489
|
+
if (action === "formula") return t("{0}不能放公式。", name);
|
|
490
|
+
return t("{0}不能与相邻段落合并。", name);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** No text and no formula (a trailing <br> or spaces do not count). */
|
|
494
|
+
export function isEmpty(element) {
|
|
495
|
+
return serializeInner(element).replace(/ /g, " ").trim() === "";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const siblingTag = (element) => element.tagName === "LI" ? "li" : "p";
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A text block made on the page: data-ge-new, with its type and unit kind. Inside a component it
|
|
502
|
+
* leaves the frame to the component, as `from` (the block it was split off) does.
|
|
503
|
+
*/
|
|
504
|
+
export function newHost(doc, type, from = null) {
|
|
505
|
+
const spec = BLOCK_TYPES[type];
|
|
506
|
+
const block = doc.createElement(spec.tag);
|
|
507
|
+
if (spec.className) block.className = spec.className;
|
|
508
|
+
block.setAttribute("contenteditable", "true");
|
|
509
|
+
block.setAttribute("data-ge-new", "");
|
|
510
|
+
block.setAttribute("data-ge-type", type);
|
|
511
|
+
block.setAttribute("data-ge-unit-kind", spec.kind);
|
|
512
|
+
const kind = spec.host ?? "flow";
|
|
513
|
+
block.setAttribute("data-ge-kind", kind);
|
|
514
|
+
block.setAttribute("data-ge-caps", capsFor(kind));
|
|
515
|
+
if (from?.hasAttribute("data-ge-framed")) block.setAttribute("data-ge-framed", "");
|
|
516
|
+
return block;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** A container made on the page (a list, a quote, a section): a box of its own that goes once empty. */
|
|
520
|
+
export function newContainer(doc, type) {
|
|
521
|
+
const spec = BLOCK_TYPES[type];
|
|
522
|
+
const box = doc.createElement(spec.tag);
|
|
523
|
+
if (spec.className) box.className = spec.className;
|
|
524
|
+
for (const [name, value] of [["data-ge-new", ""], ["data-ge-type", type], ["data-ge-unit-kind", spec.kind], ["data-ge-box", ""], ["data-ge-box-kind", spec.box], ["data-ge-pure", ""]]) {
|
|
525
|
+
box.setAttribute(name, value);
|
|
526
|
+
}
|
|
527
|
+
if (spec.box === "list") box.setAttribute("contenteditable", "false"); // nested in an item, it is not the item's text
|
|
528
|
+
return box;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* A new container of `type` for the blocks after the place where `container` (a list, a quote, a
|
|
533
|
+
* panel) splits in two, as newContainer makes it, with what its type keeps of the whole (`onSplit`); a
|
|
534
|
+
* numbered list's starts at 1 again (notion-lite's runs).
|
|
535
|
+
*/
|
|
536
|
+
export function newTail(container, type) {
|
|
537
|
+
const tail = newContainer(container.ownerDocument, type);
|
|
538
|
+
BLOCK_TYPES[type].onSplit?.(container, tail);
|
|
539
|
+
return tail;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// New blocks split off `from` are paragraphs, or list items in a list.
|
|
543
|
+
const newBlock = (doc, tag, from) => newHost(doc, tag === "li" ? "item" : "paragraph", from);
|
|
544
|
+
|
|
545
|
+
// The element a text block made on the page is part of when it is not a block of its own: the
|
|
546
|
+
// summary of a new disclosure, a new code block (its code's pre, its title's block), the header of a
|
|
547
|
+
// new detail card.
|
|
548
|
+
const partOf = (host) => host.hasAttribute("data-ge-new") ? host.parentElement?.closest(PART_SELECTOR) ?? null : null;
|
|
549
|
+
|
|
550
|
+
function extractTail(element, range) {
|
|
551
|
+
const tail = element.ownerDocument.createRange();
|
|
552
|
+
tail.setStart(range.startContainer, range.startOffset);
|
|
553
|
+
tail.setEnd(element, textEnd(element));
|
|
554
|
+
return tail.extractContents();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Enter: at the block start insert an empty block before it; otherwise move the content after
|
|
559
|
+
* the caret into a new block (after a list item holding lists, the first item of its first list).
|
|
560
|
+
* Returns the block that should receive the caret. The caller skips IME composition and checks
|
|
561
|
+
* that the block may split. A box's first line (a flowchart node's title: firstLineOf) has nothing
|
|
562
|
+
* before it: it always splits, what follows the caret (all of it at its start) a plain paragraph
|
|
563
|
+
* below it, its bold taken off when the line was bold throughout (its title's <strong>: the second
|
|
564
|
+
* half is a plain paragraph), the line left empty at its start (bold armed there again: format-bar/).
|
|
565
|
+
*/
|
|
566
|
+
export function splitBlock(element, range) {
|
|
567
|
+
if (!range.collapsed) range.deleteContents();
|
|
568
|
+
const first = isFirstLine(element);
|
|
569
|
+
const head = element.ownerDocument.createRange();
|
|
570
|
+
head.setStart(element, 0);
|
|
571
|
+
head.setEnd(range.startContainer, range.startOffset);
|
|
572
|
+
if (!first && head.toString().trim() === "" && !isEmpty(element)) {
|
|
573
|
+
element.before(newBlock(element.ownerDocument, siblingTag(element), element));
|
|
574
|
+
return element;
|
|
575
|
+
}
|
|
576
|
+
const bold = first && boldThroughout(element);
|
|
577
|
+
const next = splitBelow(element, extractTail(element, range), bold);
|
|
578
|
+
// An empty mark left at either end of the split (the title's <strong> the caret stood at an end of) holds no caret: gone.
|
|
579
|
+
if (first) for (const node of [element, next]) if (isEmpty(node)) node.replaceChildren();
|
|
580
|
+
return next;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* What stood after a caret (`rest`, taken out of `element`) as a new block on the line below it (after
|
|
585
|
+
* a list item holding lists, the first item of its first list). `plain`: its bold taken off, a
|
|
586
|
+
* title's. Returns the new block.
|
|
587
|
+
*/
|
|
588
|
+
export function splitBelow(element, rest, plain = false) {
|
|
589
|
+
const next = newBlock(element.ownerDocument, siblingTag(element), element);
|
|
590
|
+
next.append(rest);
|
|
591
|
+
placeAfterText(element, [next]);
|
|
592
|
+
if (plain && !isEmpty(next)) {
|
|
593
|
+
const all = element.ownerDocument.createRange();
|
|
594
|
+
all.selectNodeContents(next);
|
|
595
|
+
formatRange(next, all, "bold", false);
|
|
596
|
+
}
|
|
597
|
+
return next;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Whether a text block's text is bold throughout (some text, all of it in <strong> or <b>): a node's title as written. */
|
|
601
|
+
export function boldThroughout(element) {
|
|
602
|
+
if (isEmpty(element)) return false;
|
|
603
|
+
const all = element.ownerDocument.createRange();
|
|
604
|
+
all.selectNodeContents(element);
|
|
605
|
+
return markValue(element, all, "bold") === true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The first line of a box whose first block is always of one type (`firstBlock`: a flowchart node's
|
|
610
|
+
* title, its first paragraph after its icon), when it is one, or null.
|
|
611
|
+
*/
|
|
612
|
+
export function firstLineOf(box) {
|
|
613
|
+
const type = box ? firstBlockOf(boxKind(box)) : null;
|
|
614
|
+
const first = type ? heldIn(box)[0] : null;
|
|
615
|
+
return first && textTypeOf(first) === type ? first : null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Whether a text block is the first line of its box (firstLineOf): turned into nothing, nothing goes before it. */
|
|
619
|
+
export const isFirstLine = (host) => Boolean(host?.parentElement) && firstLineOf(host.parentElement) === host;
|
|
620
|
+
|
|
621
|
+
// New blocks split off a block go right after it; after a list item holding blocks, they open them
|
|
622
|
+
// (the line right below the item's text): its first list's first items, or, when another block comes
|
|
623
|
+
// first, a new list of the item's own kind put before it (a numbered one in a 流程图's step: heldListType).
|
|
624
|
+
function placeAfterText(element, blocks) {
|
|
625
|
+
const first = nestedBlocks(element)[0];
|
|
626
|
+
if (!first) element.after(...blocks);
|
|
627
|
+
else if (isList(first)) first.prepend(...blocks);
|
|
628
|
+
else {
|
|
629
|
+
const list = newContainer(element.ownerDocument, heldListType(listType(element.parentElement)));
|
|
630
|
+
list.append(...blocks);
|
|
631
|
+
first.before(list);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---------- pasting formatted text ----------
|
|
636
|
+
|
|
637
|
+
const PASTE_BLOCKS = new Set([
|
|
638
|
+
"P", "DIV", "LI", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "PRE", "TR", "TABLE", "TBODY", "THEAD", "UL", "OL", "DL", "DT", "DD",
|
|
639
|
+
"SECTION", "ARTICLE", "ASIDE", "HEADER", "FOOTER", "MAIN", "NAV", "FIGURE", "FIGCAPTION", "DETAILS", "SUMMARY",
|
|
640
|
+
]);
|
|
641
|
+
|
|
642
|
+
// The mark of the format bar a pasted element stands for (bold, italic, inline code, a link, the
|
|
643
|
+
// kit's text color or highlight, super- and subscript), as a new element; null for any other.
|
|
644
|
+
// A pasted u keeps the kit's underline color and weight.
|
|
645
|
+
function underlineOf(doc, from) {
|
|
646
|
+
const mark = doc.createElement("u");
|
|
647
|
+
const kept = [...from.classList].filter(isUnderlineClass);
|
|
648
|
+
if (kept.length > 0) mark.className = kept.join(" ");
|
|
649
|
+
return mark;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function markOf(doc, node, links) {
|
|
653
|
+
const tag = node.tagName;
|
|
654
|
+
const style = node.getAttribute("style") ?? "";
|
|
655
|
+
if (tag === "STRONG" || (tag === "B" && !/font-weight:\s*(normal|[1-5]00)\b/i.test(style))) return doc.createElement("strong");
|
|
656
|
+
if (tag === "EM" || tag === "I") return doc.createElement("em");
|
|
657
|
+
if (tag === "U" || tag === "INS") return underlineOf(doc, node);
|
|
658
|
+
if (tag === "S" || tag === "STRIKE" || tag === "DEL") return doc.createElement("s");
|
|
659
|
+
if (tag === "CODE" || tag === "KBD" || tag === "SAMP") return doc.createElement("code");
|
|
660
|
+
if (tag === "SUP" || tag === "SUB") return doc.createElement(tag.toLowerCase());
|
|
661
|
+
if (tag === "A") {
|
|
662
|
+
const href = node.getAttribute("href") ?? "";
|
|
663
|
+
if (!links || !href || /^(javascript|vbscript|data):/i.test(href.replace(/[\u0000- ]/g, ""))) return null;
|
|
664
|
+
const link = doc.createElement("a");
|
|
665
|
+
// A detail card's entry pasted in the page of its card stays its entry (its look, its card).
|
|
666
|
+
if (node.classList.contains(DIALOG_TRIGGER) && cardNamed(doc, href)) link.className = DIALOG_TRIGGER;
|
|
667
|
+
link.setAttribute("href", href);
|
|
668
|
+
return link;
|
|
669
|
+
}
|
|
670
|
+
if (tag !== "SPAN") return null;
|
|
671
|
+
const ink = [...node.classList].filter(isInkClass);
|
|
672
|
+
if (ink.length > 0) {
|
|
673
|
+
const span = doc.createElement("span");
|
|
674
|
+
span.className = ink.join(" ");
|
|
675
|
+
return span;
|
|
676
|
+
}
|
|
677
|
+
if (/font-weight:\s*(bold|[6-9]00)\b/i.test(style)) return doc.createElement("strong");
|
|
678
|
+
if (/font-style:\s*italic/i.test(style)) return doc.createElement("em");
|
|
679
|
+
if (/text-decoration[^;]*line-through/i.test(style)) return doc.createElement("s");
|
|
680
|
+
if (/text-decoration[^;]*underline/i.test(style)) return doc.createElement("u");
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const isFormula = (node) => node.matches(".doc-math-inline");
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* The lines of pasted HTML (the clipboard's text/html), each an array of nodes for a text block:
|
|
688
|
+
* text with the format bar's marks (marks: false drops them, links: false drops links), a formula
|
|
689
|
+
* of this page as its chip (another one, KaTeX's or the kit's \(…\), as the chip formula(tex) makes,
|
|
690
|
+
* or else as its TeX source; `formulas` false, each as its source), a link shown with its icon as the
|
|
691
|
+
* chip mention(mentionOf(it)) makes (else a link of its title); blocks and line breaks start new
|
|
692
|
+
* lines, and everything else gives only its text.
|
|
693
|
+
*/
|
|
694
|
+
export function pastedLines(doc, html, { marks = true, links = true, formula = null, formulas = true, mention = null } = {}) {
|
|
695
|
+
const template = doc.createElement("template");
|
|
696
|
+
template.innerHTML = html;
|
|
697
|
+
const lines = [[]];
|
|
698
|
+
const newLine = () => {
|
|
699
|
+
if (lines.at(-1).length > 0) lines.push([]);
|
|
700
|
+
};
|
|
701
|
+
const push = (target, node) => (target ? target.append(node) : lines.at(-1).push(node));
|
|
702
|
+
const walk = (node, target) => {
|
|
703
|
+
for (const child of node.childNodes) {
|
|
704
|
+
if (child.nodeType === 3) {
|
|
705
|
+
const text = child.data.replace(/[\t\n\r ]+/g, " ");
|
|
706
|
+
if (text) push(target, doc.createTextNode(text));
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (child.nodeType !== 1) continue;
|
|
710
|
+
const atom = child.getAttribute("data-ge-atom");
|
|
711
|
+
if (atom !== null && ATOM_SOURCE.has(atom) && (formulas || !isFormula(child))) {
|
|
712
|
+
// A chip of this page: a copy of it as the page shows it (the browser's copy of a selection
|
|
713
|
+
// writes its look into inline styles), else as it came.
|
|
714
|
+
const shown = [...doc.querySelectorAll("[data-ge-atom]")].find((node) => node.getAttribute("data-ge-atom") === atom);
|
|
715
|
+
const chip = shown ? shown.cloneNode(true) : doc.importNode(child, true);
|
|
716
|
+
chip.removeAttribute("data-ge-formula-edit");
|
|
717
|
+
chip.removeAttribute("data-ge-link-edit");
|
|
718
|
+
chip.setAttribute("contenteditable", "false");
|
|
719
|
+
push(target, chip);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
// A formula of another page (KaTeX's, or the kit's \(…\) not drawn yet): a formula of this page
|
|
723
|
+
// where `formula` makes one, else its TeX between \( \).
|
|
724
|
+
const foreign = child.matches(".katex") ? child.querySelector('annotation[encoding="application/x-tex"]')?.textContent
|
|
725
|
+
: isFormula(child) ? /^\s*\\\(([\s\S]*)\\\)\s*$/.exec(child.textContent)?.[1] : undefined;
|
|
726
|
+
if (foreign !== undefined) {
|
|
727
|
+
const made = foreign.trim() && formula ? formula(foreign.trim()) : null;
|
|
728
|
+
if (made) push(target, made);
|
|
729
|
+
else if (foreign.trim()) push(target, doc.createTextNode(`\\(${foreign}\\)`));
|
|
730
|
+
else if (formula) push(target, newFormula(doc, "", "")); // one left empty stays so
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
// A link left empty, and a link shown with its icon (of another page, or its source from the
|
|
734
|
+
// clipboard's own blocks).
|
|
735
|
+
if (marks && links && mention && isMention(child) && !child.hasAttribute("href")) {
|
|
736
|
+
push(target, newEmptyLink(doc, child.hasAttribute(MENTION_PAGE)));
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (marks && links && mention && isMention(child)) {
|
|
740
|
+
const parts = mentionOf(child);
|
|
741
|
+
const made = (/^https?:\/\//i.test(parts.href) || isFileLink(parts.href)) && parts.title ? mention(parts) : null;
|
|
742
|
+
if (made) {
|
|
743
|
+
push(target, made);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (/^(STYLE|SCRIPT|TEMPLATE|META|TITLE|LINK|NOSCRIPT|BUTTON|SVG)$/i.test(child.tagName) || child.matches('[aria-hidden="true"], .ge-code-gutter, [data-doc-dev]')) continue;
|
|
748
|
+
if (child.tagName === "BR") {
|
|
749
|
+
if (target) push(target, doc.createTextNode(" "));
|
|
750
|
+
else newLine();
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (!target && PASTE_BLOCKS.has(child.tagName)) {
|
|
754
|
+
newLine();
|
|
755
|
+
walk(child, null);
|
|
756
|
+
newLine();
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const mark = marks ? markOf(doc, child, links) : null;
|
|
760
|
+
if (!mark) {
|
|
761
|
+
walk(child, target);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
walk(child, mark);
|
|
765
|
+
if (mark.textContent.trim() || mark.querySelector("[data-ge-atom]")) push(target, mark);
|
|
766
|
+
else if (mark.textContent) push(target, doc.createTextNode(mark.textContent));
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
walk(template.content, null);
|
|
770
|
+
// Each line without the spaces at its ends.
|
|
771
|
+
const trim = (line) => {
|
|
772
|
+
const edge = (node, end) => {
|
|
773
|
+
if (node?.nodeType !== 3) return;
|
|
774
|
+
node.data = end ? node.data.replace(/\s+$/, "") : node.data.replace(/^\s+/, "");
|
|
775
|
+
};
|
|
776
|
+
edge(line[0], false);
|
|
777
|
+
edge(line.at(-1), true);
|
|
778
|
+
return line.filter((node) => node.nodeType !== 3 || node.data !== "");
|
|
779
|
+
};
|
|
780
|
+
// Inline code and formulas stand a space apart from the words beside them in the line, as everywhere
|
|
781
|
+
// (shared/inline-spacing.mjs); the ones at its ends are spaced from the text they join (the paste).
|
|
782
|
+
const spaced = (line) => {
|
|
783
|
+
const holder = doc.createElement("div");
|
|
784
|
+
holder.append(...line);
|
|
785
|
+
inlineItems(holder).forEach((item) => spaceAround(item));
|
|
786
|
+
return [...holder.childNodes];
|
|
787
|
+
};
|
|
788
|
+
return lines.map(trim).filter((line) => line.some((node) => node.nodeType !== 3 || node.data.trim())).map(spaced);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// An item holding blocks merges into an item that may hold them too (they go under it: Delete at the end
|
|
792
|
+
// of the line above, Feishu's and Notion's).
|
|
793
|
+
const holdsLists = (item) => item.getAttribute("data-ge-box-kind") === "nest" || (item.hasAttribute("data-ge-new") && !item.hasAttribute("data-ge-unit"));
|
|
794
|
+
// `sameKind`: an item joins only an item and a paragraph only a paragraph (Backspace, Delete); else
|
|
795
|
+
// any text joins any (a selection across blocks deleted: the last block's rest joins the first).
|
|
796
|
+
const joinable = (into, from, sameKind) => isHost(into) && can(into, "merge") && can(from, "merge") && can(from, "remove")
|
|
797
|
+
&& !from.hasAttribute("data-ge-keep") && (!sameKind || (into.tagName === "LI") === (from.tagName === "LI"))
|
|
798
|
+
&& (nestedBlocks(from).length === 0 || (into.tagName === "LI" && holdsLists(into) && nestedBlocks(from).every((block) => block.matches(UNIT))));
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* The block on the line right above a block: its previous sibling, or for a list item, the last
|
|
802
|
+
* block held by the one before it (at any depth: an item, a paragraph…), or the item its list (or
|
|
803
|
+
* the block) hangs from. A flowchart is a container (its steps are nodes): the line above the block
|
|
804
|
+
* after one is the flowchart itself, as a highlight block is the line above the block after it; and
|
|
805
|
+
* nothing in a node has a line above it outside the node.
|
|
806
|
+
*/
|
|
807
|
+
export function previousLine(element) {
|
|
808
|
+
let previous = element.previousElementSibling;
|
|
809
|
+
if (!previous) return parentItem(element) ?? holderItem(element);
|
|
810
|
+
// A block right after a list: the list's last item (Notion's and Feishu's line above it).
|
|
811
|
+
if (isList(previous) && !isFlowchart(previous) && previous.lastElementChild?.tagName === "LI") previous = previous.lastElementChild;
|
|
812
|
+
if (isStep(previous)) return previous;
|
|
813
|
+
for (let held = nestedBlocks(previous).at(-1); held; held = nestedBlocks(previous).at(-1)) {
|
|
814
|
+
if (!isList(held)) return held; // the last block it holds: a paragraph, an image…
|
|
815
|
+
if (held.lastElementChild?.tagName !== "LI") break;
|
|
816
|
+
previous = held.lastElementChild;
|
|
817
|
+
}
|
|
818
|
+
return previous;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* The block on the line right below a text block: the first item it holds, its next sibling, or
|
|
823
|
+
* for the last item of a list, the item after the one its list hangs from, or the block after the
|
|
824
|
+
* outermost list (previousLine's counterpart).
|
|
825
|
+
*/
|
|
826
|
+
export function nextLine(element) {
|
|
827
|
+
const held = nestedBlocks(element)[0];
|
|
828
|
+
const first = isList(held) ? held.firstElementChild : held;
|
|
829
|
+
if (first) return first;
|
|
830
|
+
let at = element;
|
|
831
|
+
// The last block an item holds: the line after the item.
|
|
832
|
+
if (!at.nextElementSibling && holderItem(at)) at = holderItem(at);
|
|
833
|
+
while (!at.nextElementSibling && at.tagName === "LI" && !isStep(at)) {
|
|
834
|
+
const list = at.parentElement;
|
|
835
|
+
// Out of a list a node holds, its line below is the next block in the node, or none: the line
|
|
836
|
+
// after a node's last block is not the next node's, nor the block after the flowchart.
|
|
837
|
+
if (list?.parentElement?.tagName === "LI" && !isStep(list.parentElement)) at = list.parentElement;
|
|
838
|
+
else return list?.nextElementSibling ?? null;
|
|
839
|
+
}
|
|
840
|
+
return at.nextElementSibling;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Backspace at a block start: append it to the block on the line above (a block right after a list
|
|
845
|
+
* to the list's last item, whatever its kind). Returns that block, or null.
|
|
846
|
+
*/
|
|
847
|
+
export const mergeWithPrevious = (element) => mergeInto(previousLine(element), element, { anyKind: isList(element.previousElementSibling) || (!element.previousElementSibling && Boolean(holderItem(element))) });
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* A text block appended to another's text (the lists it holds going under that one); it goes.
|
|
851
|
+
* `anyKind`: whatever their kinds (see joinable). Returns `into`, or null when they do not join.
|
|
852
|
+
*/
|
|
853
|
+
export function mergeInto(into, from, { anyKind = false } = {}) {
|
|
854
|
+
if (!into || into === from || !joinable(into, from, !anyKind)) return null;
|
|
855
|
+
const end = into.childNodes[textEnd(into)] ?? null;
|
|
856
|
+
const lists = nestedBlocks(from);
|
|
857
|
+
for (const node of [...from.childNodes].filter((node) => !lists.includes(node))) into.insertBefore(node, end);
|
|
858
|
+
into.append(...lists); // the blocks it held, under the line it joined
|
|
859
|
+
const list = from.parentElement;
|
|
860
|
+
from.remove();
|
|
861
|
+
if (isList(list) && list.children.length === 0 && list.parentElement?.tagName === "LI") list.remove(); // a nested list left empty goes
|
|
862
|
+
return into;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ---------- identity ----------
|
|
866
|
+
// Every element the commits know (a unit, a text block, a box, a block made on the page) carries
|
|
867
|
+
// an identity of its own, data-ge-id, given once and never changed (docs/editor.md, "Sync state"):
|
|
868
|
+
// what of the file it is lives in the commits' table under that identity, and the marks below
|
|
869
|
+
// (data-ge-unit, data-ge-block, data-ge-box, data-ge-new…) are written from that table. A block
|
|
870
|
+
// turned into another type keeps it (convert.mjs), as does a list turned into the other kind; a
|
|
871
|
+
// copy never has one (it is taken off with every other mark), and undo brings an element back
|
|
872
|
+
// with its own.
|
|
873
|
+
|
|
874
|
+
export const ELEMENT_ID = "data-ge-id";
|
|
875
|
+
const ELEMENT_ID_PREFIX = Math.random().toString(36).slice(2, 6); // a page's own, as formulas' keys
|
|
876
|
+
let elementIds = 0;
|
|
877
|
+
|
|
878
|
+
/** The identity of an element, given now when it has none. */
|
|
879
|
+
export function identityOf(element) {
|
|
880
|
+
let id = element.getAttribute(ELEMENT_ID);
|
|
881
|
+
if (!id) {
|
|
882
|
+
id = `${ELEMENT_ID_PREFIX}${(elementIds++).toString(36)}`;
|
|
883
|
+
element.setAttribute(ELEMENT_ID, id);
|
|
884
|
+
}
|
|
885
|
+
return id;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** The elements of `root` the commits know. */
|
|
889
|
+
export const knownElements = (root) => root.querySelectorAll("[data-ge-unit], [data-ge-block], [data-ge-box], [data-ge-new], [data-ge-id]");
|
|
890
|
+
|
|
891
|
+
// ---------- units: the blocks the reader can select and delete ----------
|
|
892
|
+
// The preview server marks them (see findUnits in src/editor/source/): data-ge-unit on each,
|
|
893
|
+
// data-ge-lead on a table's scope line that goes with it, data-ge-keep on those the report's
|
|
894
|
+
// script or other elements refer to, data-ge-box (and data-ge-pure) on the containers.
|
|
895
|
+
|
|
896
|
+
/** What marks a block: a unit the file has (data-ge-unit) or one made on the page (data-ge-new). */
|
|
897
|
+
export const UNIT = "[data-ge-unit], [data-ge-new]";
|
|
898
|
+
|
|
899
|
+
// The containers Backspace deletes once nothing is written in them (notion-lite's callout goes with
|
|
900
|
+
// its only empty line; so do these), the kinds of block whose component says so (`removedEmpty`):
|
|
901
|
+
// text blocks may stand in them, empty, and their own parts (a highlight block's icon, a code block's
|
|
902
|
+
// language); nothing else.
|
|
903
|
+
const EMPTIED = REMOVED_EMPTY.map((kind) => `[data-ge-unit-kind="${kind}"]`).join(", ");
|
|
904
|
+
|
|
905
|
+
/** The container `host` stands first in (or anywhere in, where it says so: `removedEmpty`) when nothing is written in it, or null. */
|
|
906
|
+
export function emptyContainer(host) {
|
|
907
|
+
const container = host.parentElement?.closest(EMPTIED);
|
|
908
|
+
if (!container || !container.matches(UNIT)) return null;
|
|
909
|
+
const own = [...container.querySelectorAll('[contenteditable="true"]')].filter(isHost);
|
|
910
|
+
if (own.length === 0 || !own.every(isEmpty) || (own[0] !== host && !removedFromAnywhere(container.getAttribute("data-ge-unit-kind")))) return null;
|
|
911
|
+
const units = [...container.querySelectorAll(UNIT)];
|
|
912
|
+
const lead = leadOf(boxKind(container));
|
|
913
|
+
return units.every((unit) => isHost(unit) || unit.matches(TEXT_BLOCKS) || (lead !== null && unit.getAttribute("data-ge-unit-kind") === lead)) ? container : null;
|
|
914
|
+
}
|
|
915
|
+
// ...and a table's scope line, which goes with its table.
|
|
916
|
+
const UNIT_OR_LEAD = "[data-ge-unit], [data-ge-lead], [data-ge-new]";
|
|
917
|
+
// The block around a node (outerOf): a text host made on the page (contenteditable) is no block around anything.
|
|
918
|
+
export const OUTER = "[data-ge-unit], [data-ge-new]:not([contenteditable])";
|
|
919
|
+
|
|
920
|
+
/** The blocks directly in a container: the file's and those made on the page (not a section's title row). */
|
|
921
|
+
export const unitsOf = (container) => [...container.children].filter((child) => child.matches(UNIT));
|
|
922
|
+
|
|
923
|
+
// A row or cell made on the page (components/table/page.mjs): part of its table, written by its operation.
|
|
924
|
+
export const inTable = (node) => /^(TR|TD|TH)$/.test(node?.tagName ?? "") && node.hasAttribute("data-ge-new");
|
|
925
|
+
|
|
926
|
+
/** The unit an element belongs to (a new, unsaved block counts as one), or null. */
|
|
927
|
+
export function unitElement(node) {
|
|
928
|
+
let found = node?.closest?.(UNIT_OR_LEAD) ?? null;
|
|
929
|
+
while (inTable(found)) found = found.parentElement?.closest(UNIT_OR_LEAD) ?? null;
|
|
930
|
+
// Text that is part of a block made on the page (a new disclosure's title, a new code block's
|
|
931
|
+
// code) stands for that block.
|
|
932
|
+
const part = found && !found.hasAttribute("data-ge-unit") ? partOf(found) : null;
|
|
933
|
+
if (part) found = (part.matches("pre, .doc-code") ? part : part.parentElement).closest(UNIT);
|
|
934
|
+
if (!found?.hasAttribute("data-ge-lead")) return found;
|
|
935
|
+
const id = found.getAttribute("data-ge-lead");
|
|
936
|
+
let next = found.nextElementSibling;
|
|
937
|
+
while (next && next.getAttribute("data-ge-unit") !== id) next = next.nextElementSibling;
|
|
938
|
+
return next;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** A unit and the elements right before it that go with it (a table's scope line), in document order. */
|
|
942
|
+
export function withLeads(node) {
|
|
943
|
+
const id = node.getAttribute("data-ge-unit");
|
|
944
|
+
const nodes = [node];
|
|
945
|
+
for (let previous = node.previousElementSibling; id !== null && previous?.getAttribute("data-ge-lead") === id; previous = previous.previousElementSibling) {
|
|
946
|
+
nodes.unshift(previous);
|
|
947
|
+
}
|
|
948
|
+
return nodes;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** Everything that goes away with a unit: its leads, itself and, for a Sheet tab, its panel. */
|
|
952
|
+
export function partsOf(node) {
|
|
953
|
+
const panel = panelOf(node);
|
|
954
|
+
return [...withLeads(node), ...(panel ? [panel] : [])];
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** A unit taken out of the page with what goes with it (partsOf): the one step of every deletion. */
|
|
958
|
+
export function takeOut(node) {
|
|
959
|
+
partsOf(node).forEach((part) => part.remove());
|
|
960
|
+
return node;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const sizesOf = (node, kind) => Object.fromEntries(Object.keys(SIZES[kind] ?? {}).map((name) => [name, node.style.getPropertyValue(name).trim()]));
|
|
964
|
+
const attributesOf = (node, kind) => Object.fromEntries(Object.keys(ATTRIBUTES[kind] ?? {}).map((name) => [name, node.getAttribute(name)]));
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Where the dashed frame of what is being edited goes (editor.mjs shows it only then): text that
|
|
968
|
+
* is a block of its own has its own outline; any other block (a card, a table and its scope line,
|
|
969
|
+
* a disclosure, code, a formula, a Sheet tab, a report widget...) a frame around the whole
|
|
970
|
+
* (data-ge-frame, shown while text inside it is edited), and the text inside it (`hosts` among
|
|
971
|
+
* them) none (data-ge-framed). Chapters (sections, Sheet areas) and the groups in them (rows of
|
|
972
|
+
* columns and their columns, lists, plain divs) have none of their own.
|
|
973
|
+
*/
|
|
974
|
+
export function markFrames(root, layout, hosts) {
|
|
975
|
+
const frames = [];
|
|
976
|
+
for (const [id, node] of layout.elements) {
|
|
977
|
+
if (!isHost(node) && !node.hasAttribute("data-ge-box") && layout.kinds.get(id) !== "sheets") frames.push(node);
|
|
978
|
+
}
|
|
979
|
+
frames.push(...root.querySelectorAll("[data-ge-lead]"));
|
|
980
|
+
for (const node of frames) {
|
|
981
|
+
node.setAttribute("data-ge-frame", "");
|
|
982
|
+
const round = cornersOf(node);
|
|
983
|
+
if (round) node.setAttribute("data-ge-round", round);
|
|
984
|
+
}
|
|
985
|
+
for (const host of hosts) if (frameOf(host)) host.setAttribute("data-ge-framed", "");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// A frame follows its element's corners. One with square corners gets rounded ones while the page
|
|
989
|
+
// is edited: a block that draws a box (a fill or a border: a report widget, say) the kit's, as
|
|
990
|
+
// every component has them ("box"); one that draws none (a formula) or only a line (a divider)
|
|
991
|
+
// the corners of a text block's outline ("text"), its frame being all there is to round. Not
|
|
992
|
+
// media, nor a box that clips its content, whose look that would change.
|
|
993
|
+
const MEDIA_TAGS = new Set(["img", "picture", "video", "audio", "canvas", "iframe", "object", "svg"]);
|
|
994
|
+
function cornersOf(node) {
|
|
995
|
+
if (MEDIA_TAGS.has(node.localName)) return null;
|
|
996
|
+
if (node.localName === "hr") return "text";
|
|
997
|
+
const style = node.ownerDocument.defaultView.getComputedStyle(node);
|
|
998
|
+
const corners = [style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius];
|
|
999
|
+
if (corners.some((corner) => parseFloat(corner) > 0)) return null;
|
|
1000
|
+
const clips = [style.overflowX, style.overflowY].some((value) => value && value !== "visible");
|
|
1001
|
+
const boxed = style.backgroundImage !== "none" || !/^(transparent|rgba\(0, 0, 0, 0\))$/.test(style.backgroundColor)
|
|
1002
|
+
|| [style.borderTopWidth, style.borderRightWidth, style.borderBottomWidth, style.borderLeftWidth].some((width) => parseFloat(width) > 0);
|
|
1003
|
+
if (clips && boxed) return null;
|
|
1004
|
+
return boxed ? "box" : "text";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** The frame that carries the outline of text inside a component: the nearest around it, unless a container comes first. */
|
|
1008
|
+
export function frameOf(host) {
|
|
1009
|
+
for (let node = host?.parentElement; node && node.tagName !== "MAIN"; node = node.parentElement) {
|
|
1010
|
+
if (node.hasAttribute("data-ge-frame")) return node;
|
|
1011
|
+
if (node.hasAttribute("data-ge-box")) return null;
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** Whether a child of a box is a block (a unit, a table's scope line, a block made on the page). */
|
|
1017
|
+
export const holdsBlocks = (node) => node.matches(UNIT_OR_LEAD);
|
|
1018
|
+
|
|
1019
|
+
/** The blocks of a box, in order. */
|
|
1020
|
+
export const blocksIn = (box) => [...box.children].filter(holdsBlocks);
|
|
1021
|
+
|
|
1022
|
+
/** The kind of box an element is (data-ge-box-kind), or null. */
|
|
1023
|
+
export const boxKind = (node) => node?.getAttribute?.("data-ge-box-kind") ?? null;
|
|
1024
|
+
|
|
1025
|
+
/** The blocks a box holds as its own: its blocks without its lead (a highlight block's icon, `lead`). */
|
|
1026
|
+
export function heldIn(box) {
|
|
1027
|
+
const lead = leadOf(boxKind(box));
|
|
1028
|
+
return lead === null ? blocksIn(box) : blocksIn(box).filter((child) => child.getAttribute("data-ge-unit-kind") !== lead);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* What a structural step (history.mjs) does to the boxes that always hold a block (`holdsOne`: a
|
|
1033
|
+
* disclosure's content, a highlight block, a flowchart's node), as a tidier: one of the containers
|
|
1034
|
+
* it changes that it leaves empty (its blocks deleted, cut, dragged or turned out of it) gets an
|
|
1035
|
+
* empty paragraph, in the same step (undo takes it out with the rest), the step's `caret`: what is
|
|
1036
|
+
* deleted leaves it there to write in; and one whose first block is always of one type
|
|
1037
|
+
* (`firstBlock`: a node's title) that it leaves with another first gets an empty one of that type
|
|
1038
|
+
* before it (a node's title deleted or dragged away: an empty title, bold armed there, format-bar/).
|
|
1039
|
+
* A box the step takes off the page is left alone.
|
|
1040
|
+
*/
|
|
1041
|
+
export function tidyEmptied(parents) {
|
|
1042
|
+
const boxes = parents.filter((node) => HOLDS_ONE.has(boxKind(node)) || firstBlockOf(boxKind(node)));
|
|
1043
|
+
return {
|
|
1044
|
+
parents: [],
|
|
1045
|
+
settle: (result) => boxes.filter((box) => box.isConnected).forEach((box) => {
|
|
1046
|
+
const held = heldIn(box);
|
|
1047
|
+
const type = firstBlockOf(boxKind(box));
|
|
1048
|
+
if (held.length === 0 && HOLDS_ONE.has(boxKind(box))) box.append(result.caret = newHost(box.ownerDocument, type ?? "paragraph"));
|
|
1049
|
+
else if (type && held.length > 0 && textTypeOf(held[0]) !== type) withLeads(held[0])[0].before(newHost(box.ownerDocument, type));
|
|
1050
|
+
}),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Whether deleting `node` would change nothing: it is what tidyEmptied puts back, the one block of a
|
|
1056
|
+
* box that always holds one, an empty paragraph with no look of its own (no color, not small text).
|
|
1057
|
+
*/
|
|
1058
|
+
export const keptAnyway = (node) => node?.localName === "p" && node.classList.length === 0 && isHost(node) && isEmpty(node)
|
|
1059
|
+
&& HOLDS_ONE.has(boxKind(node.parentElement)) && heldIn(node.parentElement).length === 1;
|
|
1060
|
+
|
|
1061
|
+
/** An emptied container goes when it may: made on the page, or a pure unit nothing refers to. */
|
|
1062
|
+
export function dropIfEmpty(node) {
|
|
1063
|
+
if (!node?.isConnected || blocksIn(node).length > 0) return;
|
|
1064
|
+
if (node.hasAttribute("data-ge-pure") && node.matches(UNIT) && !node.hasAttribute("data-ge-keep")) node.remove();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** The first text block of a container that shows (Enter in a title, Enter on a selected block). */
|
|
1068
|
+
export const firstHost = (container) => [...container.querySelectorAll("[data-ge-block], [data-ge-new]")].find((node) => isHost(node) && node.getClientRects().length > 0);
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* The node that goes away with a block: the block, or its container once nothing else is left in
|
|
1072
|
+
* it, when that container is itself a unit that may go (a list, quote or plain div, a column, or
|
|
1073
|
+
* any container made on the page). Outside marked containers (a list nested in a list item) a
|
|
1074
|
+
* list or quote left empty goes too. `gone`: elements going at the same time (other selected
|
|
1075
|
+
* blocks), which leave nothing behind either.
|
|
1076
|
+
*/
|
|
1077
|
+
export function removalTarget(element, gone = new Set()) {
|
|
1078
|
+
let node = element;
|
|
1079
|
+
for (let parent = node.parentElement; parent; parent = node.parentElement) {
|
|
1080
|
+
const own = new Set(withLeads(node));
|
|
1081
|
+
const others = [...parent.children].filter((child) => !own.has(child) && !gone.has(child));
|
|
1082
|
+
if (parent.hasAttribute("data-ge-box")) {
|
|
1083
|
+
const free = parent.hasAttribute("data-ge-pure") && parent.matches(UNIT) && !parent.hasAttribute("data-ge-keep");
|
|
1084
|
+
if (!free || others.some(holdsBlocks)) return node;
|
|
1085
|
+
} else if (!WRAPPERS.has(parent.tagName) || others.length > 0) return node;
|
|
1086
|
+
node = parent;
|
|
1087
|
+
}
|
|
1088
|
+
return node;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// The page's units by id: the elements marked as units (or a snapshot's), the last of an id kept.
|
|
1092
|
+
function unitElements(root, snapshot = null, unitOf = markedUnit) {
|
|
1093
|
+
const nodes = (snapshot ? snapshot.units : [...root.querySelectorAll("[data-ge-unit]")]).filter((node) => unitOf(node) !== null);
|
|
1094
|
+
return new Map(nodes.map((node) => [unitOf(node), node]));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Every unit of the page marked with its kind from the map (data-ge-unit-kind), as captureLayout marks
|
|
1099
|
+
* them (it calls this): what reads the page by its units' kinds before the editor has started finds
|
|
1100
|
+
* them as it will after (the outline, drawn before enter(): editor.mjs start). A mark already right is
|
|
1101
|
+
* not set again (the outline's observer would take it for a change of a heading and read the page
|
|
1102
|
+
* again). `elements`: the units by id (unitElements). Returns the kinds by unit id.
|
|
1103
|
+
*/
|
|
1104
|
+
export function markKinds(root, map, elements = unitElements(root)) {
|
|
1105
|
+
const kinds = new Map((map.units ?? []).map((unit) => [unit.id, unit.kind]));
|
|
1106
|
+
for (const [id, node] of elements) {
|
|
1107
|
+
if (kinds.has(id) && node.getAttribute("data-ge-unit-kind") !== kinds.get(id)) node.setAttribute("data-ge-unit-kind", kinds.get(id));
|
|
1108
|
+
}
|
|
1109
|
+
return kinds;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* The page's units as the editor found them when it started (or as the last commit left them,
|
|
1114
|
+
* commit.mjs): elements by id (a deleted unit is no longer in the document, but its element is),
|
|
1115
|
+
* which unit each text block lies in, each unit's source hash, and the switchable classes it had. Every
|
|
1116
|
+
* unit and box is also marked with its kind (data-ge-unit-kind, data-ge-box-kind), so blocks made
|
|
1117
|
+
* or converted on the page, which the map does not know, read the same way. `data`: what the file
|
|
1118
|
+
* holds of what each unit's component writes, by unit id, as the commits say it (commit.mjs apply);
|
|
1119
|
+
* without it, what the page holds of that now.
|
|
1120
|
+
*/
|
|
1121
|
+
export function captureLayout(root, map, snapshot = null, { unitOf = markedUnit, boxOf = markedBox, typeOf = null, data = null } = {}) {
|
|
1122
|
+
const elements = unitElements(root, snapshot, unitOf);
|
|
1123
|
+
const kinds = markKinds(root, map, elements);
|
|
1124
|
+
const boxKinds = new Map((map.boxes ?? []).map((box) => [box.id, box.kind]));
|
|
1125
|
+
const boxes = (snapshot ? snapshot.boxes : [...root.querySelectorAll("[data-ge-box]")].map((box) => [box, [...box.children]])).filter(([box]) => boxOf(box) !== null);
|
|
1126
|
+
for (const [box] of boxes) {
|
|
1127
|
+
const kind = boxKinds.get(boxOf(box));
|
|
1128
|
+
if (kind) box.setAttribute("data-ge-box-kind", kind);
|
|
1129
|
+
}
|
|
1130
|
+
// Classes, sizes and attributes as they were when the snapshot was taken, else as they are now.
|
|
1131
|
+
const look = (node) => snapshot?.looks.get(node) ?? node;
|
|
1132
|
+
const toggles = new Map([...elements].map(([id, node]) => [id, new Set((TOGGLES[kinds.get(id)] ?? []).filter((name) => look(node).classList.contains(name)))]));
|
|
1133
|
+
const sizes = new Map([...elements].map(([id, node]) => [id, sizesOf(look(node), kinds.get(id))]));
|
|
1134
|
+
const attributes = new Map([...elements].map(([id, node]) => [id, attributesOf(look(node), kinds.get(id))]));
|
|
1135
|
+
return {
|
|
1136
|
+
root,
|
|
1137
|
+
elements,
|
|
1138
|
+
kinds,
|
|
1139
|
+
// The type each showed as (convert.mjs ownTypeOf, when given): turned into another, it is converted.
|
|
1140
|
+
types: new Map(typeOf ? [...elements].map(([id, node]) => [id, typeOf(look(node))]) : []),
|
|
1141
|
+
toggles,
|
|
1142
|
+
sizes,
|
|
1143
|
+
attributes,
|
|
1144
|
+
// By box id: a list turned into the other kind of list is a new element with the same id.
|
|
1145
|
+
orders: new Map(boxes.map(([box, children]) => [boxOf(box), children.map(unitOf).filter((id) => id !== null)])),
|
|
1146
|
+
blockUnit: new Map((map.blocks ?? []).map((block) => [block.id, block.unit ?? null])),
|
|
1147
|
+
hashes: new Map((map.units ?? []).map((unit) => [unit.id, unit.hash])),
|
|
1148
|
+
// Each table's rows and cells (components/table/grid.mjs): a change of them is one operation.
|
|
1149
|
+
tables: new Map([...elements].filter(([id]) => kinds.get(id) === "table").map(([id, node]) => [id, snapshot?.tables?.get(node) ?? gridOf(node)])),
|
|
1150
|
+
// What the file holds of what each block's component writes, so that a part left as it was is never
|
|
1151
|
+
// written: as the page opened on it (ownData), then what its commits say (`data`, commit.mjs apply).
|
|
1152
|
+
data: new Map([...elements].flatMap(([id, node]) => {
|
|
1153
|
+
const held = data ? data.get(id) : ownData(node);
|
|
1154
|
+
return held ? [[id, held]] : [];
|
|
1155
|
+
})),
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// What of a block its component writes, by part (`dataOf` of its page object: an instance's data
|
|
1160
|
+
// parts), as the file is to hold it, or nothing where it writes nothing.
|
|
1161
|
+
const ownData = (node) => pageOfUnit(node)?.dataOf?.(node) ?? null;
|
|
1162
|
+
|
|
1163
|
+
// What a unit or box of the map an element is, as its marks say (the commits' table says it once
|
|
1164
|
+
// they run: commit.mjs).
|
|
1165
|
+
const markedUnit = (node) => (node.getAttribute("data-ge-unit") ?? "") === "" ? null : Number(node.getAttribute("data-ge-unit"));
|
|
1166
|
+
const markedBox = (node) => (node.getAttribute("data-ge-box") ?? "") === "" ? null : Number(node.getAttribute("data-ge-box"));
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* The page's structure at one moment, by element: every unit (and every block made on the page,
|
|
1170
|
+
* which a commit may make one), the blocks of each box in order, and a copy of each one's start
|
|
1171
|
+
* tag (its classes and sizes). Taken when a commit is built; captureLayout(root, map, snapshot)
|
|
1172
|
+
* turns it into the layout the next commit is found against, so what changes on the page while a
|
|
1173
|
+
* commit is on its way is not taken as committed.
|
|
1174
|
+
*/
|
|
1175
|
+
export function snapshotLayout(root) {
|
|
1176
|
+
const units = [...root.querySelectorAll(UNIT)];
|
|
1177
|
+
const boxes = [...root.querySelectorAll("[data-ge-box]")].map((box) => [box, unitsOf(box)]);
|
|
1178
|
+
const tables = new Map(units.filter((node) => node.getAttribute("data-ge-unit-kind") === "table").map((node) => [node, gridOf(node)]));
|
|
1179
|
+
return { units, boxes, looks: new Map(units.map((node) => [node, node.cloneNode(false)])), tables };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Whether a text block may be written with no text: it is of a type whose component says so (its
|
|
1183
|
+
// `empty`: a paragraph, a heading, a list item, a callout; the server's emptyLeaf reads the same)
|
|
1184
|
+
// and can be emptied (can "empty"), an item holding blocks too, its line kept over them (keepLine). A
|
|
1185
|
+
// field that may be emptied (a cell, a caption) is no block written empty; a new code block is
|
|
1186
|
+
// written empty by its type (describeNode).
|
|
1187
|
+
const clearable = (element) => can(element, "empty");
|
|
1188
|
+
/** Whether an empty text block stays, written with no text (a paragraph, a heading, a list item, a callout, a code block). */
|
|
1189
|
+
const keepsWhenEmpty = (element) => BLOCK_TYPES[textTypeOf(element)]?.empty === true && clearable(element);
|
|
1190
|
+
|
|
1191
|
+
/** Whether a text block may go: it can be removed, is not kept (data-ge-keep) and holds no blocks (it would show no line of its own). */
|
|
1192
|
+
export const removable = (element) => can(element, "remove") && !element.hasAttribute("data-ge-keep") && nestedBlocks(element).length === 0;
|
|
1193
|
+
/** Whether a text block left empty goes with its save: it may go and does not stay empty. */
|
|
1194
|
+
const dropsWhenEmpty = (element) => removable(element) && !keepsWhenEmpty(element);
|
|
1195
|
+
|
|
1196
|
+
// A block converted on the page (data-ge-convert) and then emptied goes, like an emptied block
|
|
1197
|
+
// that cannot be empty.
|
|
1198
|
+
const gone = (node) => node.hasAttribute("data-ge-convert") && !BLOCK_TYPES[node.getAttribute("data-ge-type")]?.box && isEmpty(textOf(node)) && !keepsWhenEmpty(textOf(node));
|
|
1199
|
+
|
|
1200
|
+
// Whether a unit goes with this collect: one a conversion emptied (gone), or one of `emptied`, the
|
|
1201
|
+
// units of the file whose own text collectEdits took out, with nothing split off them (`[]`): a
|
|
1202
|
+
// block that cannot be empty (can "empty": a free paragraph, heading or list item can) goes with
|
|
1203
|
+
// that commit. Like a block converted and then emptied, such a block is placed nowhere (no move of
|
|
1204
|
+
// it, no block placed next to it) and taken along into no block made on the page. collectEdits
|
|
1205
|
+
// finds the set and hands it to every function that asks.
|
|
1206
|
+
const leaves = (node, emptied) => gone(node) || emptied.has(node);
|
|
1207
|
+
|
|
1208
|
+
// The text block of a block: the block itself, or where its component says (a code block's code).
|
|
1209
|
+
const textOf = (node) => BLOCK_TYPES[blockTypeOf(node)]?.textHost?.(node) ?? node;
|
|
1210
|
+
|
|
1211
|
+
// What a component's description is given to say what goes with a block (`extras`).
|
|
1212
|
+
const HELPERS = { sizeValue, serializeInner, unitsOf };
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* The first element in `root` (in document order, `root` itself left out) carrying each value of
|
|
1216
|
+
* the attributes `names`, by value: what root.querySelector(`[name="value"]`) (one of them, for
|
|
1217
|
+
* several names) finds, for every value at once. Attribute selectors are not indexed, so a search
|
|
1218
|
+
* per value walks the page until it matches: one per block of the report made every collect
|
|
1219
|
+
* O(blocks × elements), 35-90 ms on a large report each time the reader paused, and many times
|
|
1220
|
+
* over whenever the page was left. One pass over the elements that carry them instead.
|
|
1221
|
+
*/
|
|
1222
|
+
export function firstByAttribute(root, ...names) {
|
|
1223
|
+
const found = new Map();
|
|
1224
|
+
for (const element of root.querySelectorAll(names.map((name) => `[${name}]`).join(", "))) {
|
|
1225
|
+
for (const name of names) {
|
|
1226
|
+
const value = element.getAttribute(name);
|
|
1227
|
+
if (value !== null && !found.has(value)) found.set(value, element);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return found;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// An index of the page made the first time it is asked for (firstByAttribute), for one pass of a
|
|
1234
|
+
// function that does not change the page: most collects never need some of them.
|
|
1235
|
+
const lazyIndex = (root, ...names) => {
|
|
1236
|
+
let index = null;
|
|
1237
|
+
return () => (index ??= firstByAttribute(root, ...names));
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
// The element a unit has on the page now: its own, or the one it was converted into. `units`: the
|
|
1241
|
+
// page's elements by data-ge-unit (firstByAttribute), when the caller has them.
|
|
1242
|
+
function live(root, id, units = null) {
|
|
1243
|
+
const node = units ? units.get(String(id)) ?? null : root.querySelector(`[data-ge-unit="${id}"]`);
|
|
1244
|
+
return node && !gone(node) ? node : null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Deleted units: on the page neither as their element nor as one they were converted into, and
|
|
1249
|
+
* outermost only: out of the document with no parent, or inside a container made on the page
|
|
1250
|
+
* that was deleted (and no unit around it).
|
|
1251
|
+
*/
|
|
1252
|
+
export function removedUnits(layout) {
|
|
1253
|
+
if (!layout) return [];
|
|
1254
|
+
const top = (node) => {
|
|
1255
|
+
while (node.parentElement) node = node.parentElement;
|
|
1256
|
+
return node;
|
|
1257
|
+
};
|
|
1258
|
+
const units = lazyIndex(layout.root, "data-ge-unit"); // asked only for the units off the page
|
|
1259
|
+
return [...layout.elements].filter(([id, node]) => {
|
|
1260
|
+
if (node.isConnected || live(layout.root, id, units())) return false;
|
|
1261
|
+
if (!node.parentNode) return true;
|
|
1262
|
+
return !node.parentElement?.closest("[data-ge-unit]") && top(node).hasAttribute?.("data-ge-new") === true;
|
|
1263
|
+
}).map(([id]) => id);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// The ids of the units directly in a box, in page order.
|
|
1267
|
+
const unitsIn = (box) => [...box.children].map((child) => child.getAttribute("data-ge-unit")).filter((id) => id !== null).map(Number);
|
|
1268
|
+
|
|
1269
|
+
// Longest common subsequence of two lists of ids.
|
|
1270
|
+
function common(xs, ys) {
|
|
1271
|
+
const table = Array.from({ length: xs.length + 1 }, () => new Array(ys.length + 1).fill(0));
|
|
1272
|
+
for (let i = xs.length - 1; i >= 0; i--) {
|
|
1273
|
+
for (let j = ys.length - 1; j >= 0; j--) table[i][j] = xs[i] === ys[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
1274
|
+
}
|
|
1275
|
+
const kept = new Set();
|
|
1276
|
+
for (let i = 0, j = 0; i < xs.length && j < ys.length;) {
|
|
1277
|
+
if (xs[i] === ys[j]) {
|
|
1278
|
+
kept.add(xs[i]);
|
|
1279
|
+
i++;
|
|
1280
|
+
j++;
|
|
1281
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) i++;
|
|
1282
|
+
else j++;
|
|
1283
|
+
}
|
|
1284
|
+
return kept;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// A new paragraph or list item split off an original text block (Enter, a multi-line paste) is
|
|
1288
|
+
// written with that block, as `parts` of its edit, when only such blocks lie between them.
|
|
1289
|
+
// One with no text (Enter at a block's start or end) is a block of its own, placed next to it: the
|
|
1290
|
+
// block it came from is not edited by it (an empty part once made the reader's untouched paragraph
|
|
1291
|
+
// conflict with the Agent deleting it).
|
|
1292
|
+
const splitOff = (node) => node.hasAttribute("data-ge-new") && !node.hasAttribute("data-ge-unit")
|
|
1293
|
+
&& (node.tagName === "P" || node.tagName === "LI") && !node.parentElement?.closest("[data-ge-new]") && nestedBlocks(node).length === 0
|
|
1294
|
+
&& !BLOCK_TYPES[textTypeOf(node)]?.className && !isEmpty(node);
|
|
1295
|
+
|
|
1296
|
+
// An original block accepts new sibling blocks of the type it splits into (li -> li, p/h3/h4 -> p).
|
|
1297
|
+
const takesNew = (original, node) => Boolean(original?.hasAttribute("data-ge-block"))
|
|
1298
|
+
&& can(original, "split")
|
|
1299
|
+
&& (original.tagName === "LI") === (node.tagName === "LI");
|
|
1300
|
+
|
|
1301
|
+
/** The original text block a new block is written with ({ owner, where: "after" | "before" }), or null. */
|
|
1302
|
+
export function partOwner(node) {
|
|
1303
|
+
if (!splitOff(node)) return null;
|
|
1304
|
+
for (const [direction, where] of [["previousElementSibling", "after"], ["nextElementSibling", "before"]]) {
|
|
1305
|
+
let sibling = node[direction];
|
|
1306
|
+
while (sibling && splitOff(sibling)) sibling = sibling[direction];
|
|
1307
|
+
if (takesNew(sibling, node)) return { owner: sibling, where };
|
|
1308
|
+
}
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* A block made on the page as the server writes it: a leaf { type, html }, a container
|
|
1314
|
+
* { type, children } (a disclosure also has its `title`, a detail card its `id`, a column its
|
|
1315
|
+
* `ratio` when it has one), an original unit placed in a container made on the page, { unit: id }, or a copy of
|
|
1316
|
+
* one, { copy: id }. Null when there is nothing to write (no text, no blocks; a row of columns
|
|
1317
|
+
* until each of its columns has something). `emptied`: the units collectEdits takes out (leaves).
|
|
1318
|
+
*/
|
|
1319
|
+
export function nodeOf(element, emptied) {
|
|
1320
|
+
const node = describeNode(element, emptied);
|
|
1321
|
+
if (node) madeFrom.set(node, element);
|
|
1322
|
+
return node;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// The class switches a block made or turned on the page carries (its color: TOGGLES) and the
|
|
1326
|
+
// attributes its kind sets (a to-do item's checked state: ATTRIBUTES), as the file is to have them:
|
|
1327
|
+
// { classes, attributes } or nothing. A bare attribute (one whose value is "": a to-do item's
|
|
1328
|
+
// checked state) that the file wrote with a value (data-checked="true", which the kit reads as
|
|
1329
|
+
// there) goes bare, as the file takes it: an element the file had, put back by undo, writes again.
|
|
1330
|
+
const fileValue = (kind, name, value) => (!attributeValue(kind, name, value) && attributeValue(kind, name, "") ? "" : value);
|
|
1331
|
+
const switched = (element, kind) => {
|
|
1332
|
+
const classes = (TOGGLES[kind] ?? []).filter((name) => element.classList.contains(name));
|
|
1333
|
+
const attributes = Object.fromEntries(Object.keys(ATTRIBUTES[kind] ?? {}).filter((name) => element.hasAttribute(name)).map((name) => [name, fileValue(kind, name, element.getAttribute(name))]));
|
|
1334
|
+
return { ...(classes.length > 0 ? { classes } : {}), ...(Object.keys(attributes).length > 0 ? { attributes } : {}) };
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
function describeNode(element, emptied) {
|
|
1338
|
+
if (element.hasAttribute("data-ge-unit")) return leaves(element, emptied) ? null : { unit: Number(element.getAttribute("data-ge-unit")) };
|
|
1339
|
+
const type = element.getAttribute("data-ge-type");
|
|
1340
|
+
// A copy of a card or a table (clipboard/own/): the file copies that unit, with the keys of the
|
|
1341
|
+
// charts drawn from tables in it that the paste gave it (clipboard/own/chart-keys.mjs).
|
|
1342
|
+
if (type === "copy") {
|
|
1343
|
+
const keys = element.hasAttribute(COPY_KEYS) ? JSON.parse(element.getAttribute(COPY_KEYS)) : null;
|
|
1344
|
+
return { copy: Number(element.getAttribute("data-ge-copy")), ...(keys ? { keys } : {}) };
|
|
1345
|
+
}
|
|
1346
|
+
const spec = BLOCK_TYPES[type];
|
|
1347
|
+
if (!spec) return null;
|
|
1348
|
+
// Everything about a type is its component's (registry.mjs, `types`): what goes with it besides
|
|
1349
|
+
// its text and blocks (`extras`: a code block's language and title, a column's share of its row),
|
|
1350
|
+
// whether it has text at all (`bare`: a divider), where its text is (`textHost`) and its blocks
|
|
1351
|
+
// (`content`: a disclosure's content region), its title (`title`), its id (`keyed`: a detail
|
|
1352
|
+
// card's), when it is written at all (`written`: a row once each of its columns has something, a
|
|
1353
|
+
// detail card while a link opens it; else once it holds something).
|
|
1354
|
+
const extras = spec.extras?.(element, HELPERS) ?? {};
|
|
1355
|
+
if (spec.bare) return spec.written?.(element, []) === false ? null : { type, ...extras };
|
|
1356
|
+
if (!spec.box) {
|
|
1357
|
+
const text = textOf(element);
|
|
1358
|
+
if (isEmpty(text) && !keepsWhenEmpty(text) && !spec.empty) return null; // a code block is written empty too (its text host has no type of its own)
|
|
1359
|
+
const lists = nestedBlocks(element).map((block) => nodeOf(block, emptied)).filter(Boolean);
|
|
1360
|
+
return { type, html: serializeInner(text), ...extras, ...switched(element, spec.kind), ...(lists.length > 0 ? { children: lists } : {}) };
|
|
1361
|
+
}
|
|
1362
|
+
const box = spec.content?.(element) ?? element;
|
|
1363
|
+
const children = (box ? unitsOf(box) : []).map((child) => nodeOf(child, emptied)).filter(Boolean);
|
|
1364
|
+
const title = spec.title?.(element) ?? null;
|
|
1365
|
+
const written = spec.written ? spec.written(element, children, HELPERS) : children.length > 0 || Boolean(title && !isEmpty(title));
|
|
1366
|
+
if (!written) return null;
|
|
1367
|
+
// Its color too, as a text block's (switched): a highlight block or a disclosure made of a colored
|
|
1368
|
+
// node, or pasted with its color, is written so (the type's own class is no switch of it: a to-do
|
|
1369
|
+
// list's doc-todo).
|
|
1370
|
+
const classes = switched(element, spec.kind).classes?.filter((name) => name !== spec.className) ?? [];
|
|
1371
|
+
return { type, ...(spec.keyed ? { id: element.id } : {}), ...(spec.title ? { title: title ? serializeInner(title) : "" } : {}), children, ...(classes.length > 0 ? { classes } : {}), ...extras };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// The blocks directly in a box: original ones (a converted one too) and new ones written as
|
|
1375
|
+
// blocks of their own; not one that goes (emptied, or a conversion emptied), which is placed nowhere.
|
|
1376
|
+
const placedIn = (box, emptied) => [...box.children].filter((child) => !leaves(child, emptied)
|
|
1377
|
+
&& (child.hasAttribute("data-ge-unit") || (child.hasAttribute("data-ge-new") && !partOwner(child))));
|
|
1378
|
+
|
|
1379
|
+
/** What a part, a node or an insert of collectEdits was made from: its element on the page (commits). */
|
|
1380
|
+
export const madeFrom = new WeakMap();
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Moved and new blocks, box by box: [{ id, move }] and [{ id: "n<k>", insert, place }]. In each
|
|
1384
|
+
* box the longest run of units still in their original order stays; every other unit there (moved
|
|
1385
|
+
* within the box or from another one) and every new block goes { after } the unit that stayed
|
|
1386
|
+
* before it or { before } the one after it. In a box where no original unit stayed, blocks go
|
|
1387
|
+
* { into } the box, in order. In page order, so blocks placed next to the same one keep their order.
|
|
1388
|
+
* `emptied`: the units collectEdits takes out (leaves).
|
|
1389
|
+
*/
|
|
1390
|
+
export function placedUnits(layout, emptied) {
|
|
1391
|
+
if (!layout) return [];
|
|
1392
|
+
const ops = [];
|
|
1393
|
+
let fresh = 0;
|
|
1394
|
+
const boxes = firstByAttribute(layout.root, "data-ge-box"); // each box as a search for it would find it, the first of that id
|
|
1395
|
+
for (const [boxId, before] of layout.orders) {
|
|
1396
|
+
const box = boxes.get(String(boxId));
|
|
1397
|
+
if (!box) continue;
|
|
1398
|
+
const children = placedIn(box, emptied);
|
|
1399
|
+
const idOf = (child) => child.hasAttribute("data-ge-unit") ? Number(child.getAttribute("data-ge-unit")) : null;
|
|
1400
|
+
const now = children.map(idOf).filter((id) => id !== null);
|
|
1401
|
+
const kept = before.filter((id) => now.includes(id));
|
|
1402
|
+
if (children.length === now.length && kept.join() === now.join()) continue;
|
|
1403
|
+
const stay = common(kept, now);
|
|
1404
|
+
children.forEach((child, index) => {
|
|
1405
|
+
const id = idOf(child);
|
|
1406
|
+
if (id !== null && stay.has(id)) return;
|
|
1407
|
+
const previous = children.slice(0, index).reverse().map(idOf).find((other) => other !== null && stay.has(other));
|
|
1408
|
+
const next = children.slice(index + 1).map(idOf).find((other) => other !== null && stay.has(other));
|
|
1409
|
+
const place = previous !== undefined ? { after: previous } : next !== undefined ? { before: next } : { into: boxId };
|
|
1410
|
+
if (id !== null) {
|
|
1411
|
+
ops.push({ id, move: place });
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const node = nodeOf(child, emptied);
|
|
1415
|
+
if (!node) return;
|
|
1416
|
+
const op = { id: `n${fresh++}`, insert: node, place };
|
|
1417
|
+
madeFrom.set(op, child);
|
|
1418
|
+
ops.push(op);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return ops;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Units converted on the page into another type: [{ id, replace: { type, html } }] for a text
|
|
1426
|
+
* block, [{ id, retag }] for a list turned into the other kind of list.
|
|
1427
|
+
*/
|
|
1428
|
+
export function convertedUnits(layout) {
|
|
1429
|
+
if (!layout) return [];
|
|
1430
|
+
const ops = [];
|
|
1431
|
+
for (const node of layout.root.querySelectorAll("[data-ge-convert]")) {
|
|
1432
|
+
const id = Number(node.getAttribute("data-ge-convert"));
|
|
1433
|
+
const type = node.getAttribute("data-ge-type");
|
|
1434
|
+
if (BLOCK_TYPES[type]?.box) {
|
|
1435
|
+
if (node.localName !== layout.elements.get(id)?.localName) ops.push({ id, retag: node.localName });
|
|
1436
|
+
// Its class switches too (a bulleted list turned into a to-do list keeps its tag).
|
|
1437
|
+
const had = layout.toggles.get(id) ?? new Set();
|
|
1438
|
+
for (const name of TOGGLES[layout.kinds.get(id)] ?? []) if (node.classList.contains(name) !== had.has(name)) ops.push({ id, toggle: name, on: !had.has(name) });
|
|
1439
|
+
} else if (!gone(node)) ops.push({ id, replace: { type, html: serializeInner(textOf(node)), ...(BLOCK_TYPES[type]?.extras?.(node, HELPERS) ?? {}), ...switched(node, BLOCK_TYPES[type]?.kind) } });
|
|
1440
|
+
}
|
|
1441
|
+
return ops;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/** Classes switched on units still on the page: [{ id, toggle, on }]. */
|
|
1445
|
+
export function toggledUnits(layout) {
|
|
1446
|
+
if (!layout) return [];
|
|
1447
|
+
const changes = [];
|
|
1448
|
+
for (const [id, node] of layout.elements) {
|
|
1449
|
+
if (!node.isConnected) continue;
|
|
1450
|
+
for (const name of TOGGLES[layout.kinds.get(id)] ?? []) {
|
|
1451
|
+
const on = node.classList.contains(name);
|
|
1452
|
+
if (on !== layout.toggles.get(id).has(name)) changes.push({ id, toggle: name, on });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return changes;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/** Sizes changed on units still on the page: [{ id, size, value }], value null when removed. */
|
|
1459
|
+
export function sizedUnits(layout) {
|
|
1460
|
+
if (!layout) return [];
|
|
1461
|
+
const changes = [];
|
|
1462
|
+
for (const [id, node] of layout.elements) {
|
|
1463
|
+
if (!node.isConnected) continue;
|
|
1464
|
+
const now = sizesOf(node, layout.kinds.get(id));
|
|
1465
|
+
for (const [name, value] of Object.entries(now)) {
|
|
1466
|
+
if (value !== (layout.sizes.get(id)?.[name] ?? "")) changes.push({ id, size: name, value: value || null });
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return changes;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** Attributes set or removed on units still on the page: [{ id, attribute, value }], value null when removed. */
|
|
1473
|
+
export function attributedUnits(layout) {
|
|
1474
|
+
if (!layout) return [];
|
|
1475
|
+
const changes = [];
|
|
1476
|
+
for (const [id, node] of layout.elements) {
|
|
1477
|
+
if (!node.isConnected) continue;
|
|
1478
|
+
const now = attributesOf(node, layout.kinds.get(id));
|
|
1479
|
+
for (const [name, value] of Object.entries(now)) {
|
|
1480
|
+
if (value !== (layout.attributes.get(id)?.[name] ?? null)) changes.push({ id, attribute: name, value });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return changes;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Tables whose rows or columns changed on the page (components/table/grid.mjs): [{ id, table }],
|
|
1488
|
+
* each cell's text with it where it changed against `baselines` (a new cell's always).
|
|
1489
|
+
*/
|
|
1490
|
+
export function tableChanges(layout, baselines = new Map()) {
|
|
1491
|
+
if (!layout?.tables) return [];
|
|
1492
|
+
const ops = [];
|
|
1493
|
+
for (const [id, before] of layout.tables) {
|
|
1494
|
+
const node = layout.elements.get(id);
|
|
1495
|
+
if (!node?.isConnected || !before) continue;
|
|
1496
|
+
// A cell of the file not edited on the page (no baseline: locked, a script changed what it shows)
|
|
1497
|
+
// keeps what the file has.
|
|
1498
|
+
const table = tableChange(before, node, (cell) => {
|
|
1499
|
+
const html = serializeInner(cell);
|
|
1500
|
+
const block = cell.getAttribute("data-ge-block");
|
|
1501
|
+
const baseline = block === null ? undefined : baselines.get(Number(block));
|
|
1502
|
+
return { html, changed: block === null || (baseline !== undefined && html !== baseline) };
|
|
1503
|
+
});
|
|
1504
|
+
if (table) ops.push({ id, table });
|
|
1505
|
+
}
|
|
1506
|
+
return ops;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Parts a block's component wrote on the page (ownData): [{ id, source, definition }], `source` the
|
|
1511
|
+
* parts that differ from what the layout holds of them, by name, `definition` the stamp of what it
|
|
1512
|
+
* was written by. A part left as it was is written by nothing, so what a component cannot read and
|
|
1513
|
+
* leaves alone stays as the file has it.
|
|
1514
|
+
*/
|
|
1515
|
+
export function dataChanges(layout) {
|
|
1516
|
+
if (!layout?.data) return [];
|
|
1517
|
+
const ops = [];
|
|
1518
|
+
for (const [id, before] of layout.data) {
|
|
1519
|
+
const node = layout.elements.get(id);
|
|
1520
|
+
if (!node?.isConnected) continue;
|
|
1521
|
+
const source = Object.fromEntries(Object.entries(ownData(node) ?? {}).filter(([name, html]) => html !== before[name]));
|
|
1522
|
+
if (Object.keys(source).length > 0) ops.push({ id, source, definition: pageOfUnit(node).definitionOf(node) });
|
|
1523
|
+
}
|
|
1524
|
+
return ops;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Every change of units: conversions before placements, which may name a converted unit. `tables`:
|
|
1528
|
+
// the tables' operations (tableChanges), asked for once by whoever collects the edits; `emptied`: the
|
|
1529
|
+
// units that go (leaves).
|
|
1530
|
+
const unitChanges = (layout, removal, tables, emptied) => [
|
|
1531
|
+
...removedUnits(layout).map(removal), ...toggledUnits(layout), ...sizedUnits(layout), ...attributedUnits(layout), ...convertedUnits(layout), ...tables, ...dataChanges(layout), ...placedUnits(layout, emptied),
|
|
1532
|
+
];
|
|
1533
|
+
|
|
1534
|
+
// A text block that went away with the unit around it (deleted, or converted: the conversion
|
|
1535
|
+
// carries its text) is not a text edit of its own. Converted, an element stands for the unit
|
|
1536
|
+
// (data-ge-convert), also when the layout knows that one as the unit's element (a unit an undo put
|
|
1537
|
+
// back, converted before the commit putting it back was answered: model seed 5087, where the old
|
|
1538
|
+
// text went as emptied with the conversion, and the file took the block out). `converted`: the
|
|
1539
|
+
// page's elements by data-ge-convert (firstByAttribute).
|
|
1540
|
+
const covered = (layout, id, converted) => {
|
|
1541
|
+
const unit = layout?.blockUnit.get(id);
|
|
1542
|
+
if (unit === null || unit === undefined) return false;
|
|
1543
|
+
return layout.elements.get(unit)?.isConnected === false || converted().has(String(unit));
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Save: turn the DOM into the edits the server expects. Deleted units (with `layout`, see
|
|
1548
|
+
* captureLayout) become `units: [{ id, remove: true }]`, and the text blocks inside them no edit
|
|
1549
|
+
* of their own; switched classes become `units: [{ id, toggle, on }]`, changed sizes
|
|
1550
|
+
* `[{ id, size, value }]`, set or removed attributes `[{ id, attribute, value }]`, converted units `[{ id, replace }]` or `[{ id, retag }]`
|
|
1551
|
+
* (convertedUnits), what a component wrote `[{ id, source, definition }]` (dataChanges), moved units `[{ id, move }]` and new blocks `[{ id, insert, place }]`
|
|
1552
|
+
* (placedUnits); other removed original blocks become empty `parts`. A new paragraph or list
|
|
1553
|
+
* item split off an original block is written with it (partOwner). `unplaced` lists new,
|
|
1554
|
+
* non-empty blocks that have no place (not a block of a box, next to no block that can take
|
|
1555
|
+
* them) and `blank` lists blocks emptied that must keep some text (a new disclosure's title
|
|
1556
|
+
* among them); the caller must not save while either is non-empty.
|
|
1557
|
+
*/
|
|
1558
|
+
export function collectEdits(root, baselines, layout = null) {
|
|
1559
|
+
const edits = [];
|
|
1560
|
+
const unplaced = [];
|
|
1561
|
+
const blank = [];
|
|
1562
|
+
// The cells of a table whose rows or columns changed go with its operation (their text in it).
|
|
1563
|
+
const tables = tableChanges(layout, baselines);
|
|
1564
|
+
const restructured = new Set(tables.map((op) => op.id));
|
|
1565
|
+
const withTable = (id) => restructured.has(layout?.blockUnit.get(id));
|
|
1566
|
+
const edit = (id, parts) => { if (!withTable(id)) edits.push({ id, parts }); };
|
|
1567
|
+
// A code block not opened for editing (data-ge-code) is unchanged. Whether each block is on the
|
|
1568
|
+
// page is looked up in one index of the page (firstByAttribute), as nothing here changes it.
|
|
1569
|
+
const present = firstByAttribute(root, "data-ge-block", "data-ge-code");
|
|
1570
|
+
const converted = lazyIndex(layout?.root ?? root, "data-ge-convert"); // asked only for the blocks off the page
|
|
1571
|
+
for (const [id] of baselines) {
|
|
1572
|
+
if (!present.has(String(id)) && !covered(layout, id, converted)) edit(id, []);
|
|
1573
|
+
}
|
|
1574
|
+
const groups = new Map();
|
|
1575
|
+
const group = (element) => groups.get(element) ?? groups.set(element, { before: [], after: [] }).get(element);
|
|
1576
|
+
for (const node of root.querySelectorAll("[data-ge-new]:not([data-ge-unit])")) {
|
|
1577
|
+
if (inTable(node)) continue; // a new row or cell: its table's operation writes it
|
|
1578
|
+
const owner = partOwner(node);
|
|
1579
|
+
const parent = node.parentElement;
|
|
1580
|
+
// A block of a box is a block of its own (placedUnits), or part of a container made on the
|
|
1581
|
+
// page (nodeOf), as a new disclosure's title is; a block held by an item made on the page is
|
|
1582
|
+
// part of that item.
|
|
1583
|
+
const placed = (parent?.hasAttribute("data-ge-box") && (layout || parent.hasAttribute("data-ge-new")))
|
|
1584
|
+
|| (isHeld(node) && parent?.tagName === "LI" && parent.hasAttribute("data-ge-new") && !parent.hasAttribute("data-ge-unit"));
|
|
1585
|
+
if (owner) group(owner.owner)[owner.where].push(node);
|
|
1586
|
+
else if (!owner && !placed && !partOf(node) && !isEmpty(node)) unplaced.push(node);
|
|
1587
|
+
}
|
|
1588
|
+
const emptied = new WeakSet([...root.querySelectorAll("[data-ge-unit][data-ge-block]")].filter((element) => {
|
|
1589
|
+
const id = Number(element.getAttribute("data-ge-block"));
|
|
1590
|
+
return baselines.has(id) && !groups.has(element) && isEmpty(element) && dropsWhenEmpty(element) && serializeInner(element) !== baselines.get(id);
|
|
1591
|
+
}));
|
|
1592
|
+
// Parts a new block's component says the reader must fill before it is written (`blanks`: a row
|
|
1593
|
+
// of columns, one of which holds something).
|
|
1594
|
+
for (const block of root.querySelectorAll("[data-ge-new][data-ge-type]")) {
|
|
1595
|
+
const spec = BLOCK_TYPES[block.getAttribute("data-ge-type")];
|
|
1596
|
+
if (spec?.blanks) blank.push(...spec.blanks(block, { nodeOf: (node) => nodeOf(node, emptied), unitsOf }));
|
|
1597
|
+
}
|
|
1598
|
+
const part = (node) => {
|
|
1599
|
+
// Its class switches go with it (a new paragraph given a color: TOGGLES), as with a block made whole.
|
|
1600
|
+
const entry = { tag: node.tagName.toLowerCase(), html: serializeInner(node), ...switched(node, node.getAttribute("data-ge-unit-kind")) };
|
|
1601
|
+
madeFrom.set(entry, node);
|
|
1602
|
+
return entry;
|
|
1603
|
+
};
|
|
1604
|
+
for (const element of root.querySelectorAll("[data-ge-block]")) {
|
|
1605
|
+
const id = Number(element.getAttribute("data-ge-block"));
|
|
1606
|
+
if (!baselines.has(id)) continue;
|
|
1607
|
+
const { before = [], after = [] } = groups.get(element) ?? {};
|
|
1608
|
+
const html = serializeInner(element);
|
|
1609
|
+
const empty = isEmpty(element);
|
|
1610
|
+
// Spaces alone are written as nothing (`keep`), so they are no change of a block the file has empty.
|
|
1611
|
+
if ((html === baselines.get(id) || (empty && baselines.get(id) === "")) && before.length + after.length === 0) continue;
|
|
1612
|
+
if (empty && !removable(element) && !clearable(element)) blank.push(element);
|
|
1613
|
+
const keep = empty && dropsWhenEmpty(element) ? [] : [{ keep: true, html: empty ? "" : html }];
|
|
1614
|
+
edit(id, [...before.map(part), ...keep, ...after.map(part)]);
|
|
1615
|
+
}
|
|
1616
|
+
return { edits, units: unitChanges(layout, (id) => ({ id, remove: true }), tables, emptied), unplaced, blank };
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ---------- moving nodes ----------
|
|
1620
|
+
|
|
1621
|
+
/** Move nodes before `reference` (null: to the end), keeping their state where the browser can. */
|
|
1622
|
+
export function placeBefore(parent, nodes, reference) {
|
|
1623
|
+
for (const node of nodes) {
|
|
1624
|
+
if (node === reference) continue;
|
|
1625
|
+
try {
|
|
1626
|
+
if (typeof parent.moveBefore === "function" && node.isConnected && parent.isConnected) {
|
|
1627
|
+
parent.moveBefore(node, reference);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
} catch {}
|
|
1631
|
+
parent.insertBefore(node, reference);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// ---------- plain text ----------
|
|
1636
|
+
|
|
1637
|
+
/** Readable text of block source: tags dropped, formulas shown as their TeX source. */
|
|
1638
|
+
export function plainText(html, doc = globalThis.document) {
|
|
1639
|
+
const template = doc.createElement("template");
|
|
1640
|
+
template.innerHTML = html;
|
|
1641
|
+
return template.content.textContent.replace(/\s+/g, " ").trim();
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* The text of the editable blocks in `element` (itself among them), one a line, an item's lists
|
|
1646
|
+
* apart from its own text: what the reader wrote there, closed disclosures included (innerText
|
|
1647
|
+
* leaves out what is not shown).
|
|
1648
|
+
*/
|
|
1649
|
+
export function blocksText(element) {
|
|
1650
|
+
const hosts = [element, ...element.querySelectorAll("[contenteditable='true']")].filter((node) => node.getAttribute?.("contenteditable") === "true");
|
|
1651
|
+
return hosts.map(ownText).filter(Boolean).join("\n");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/** A text block's own text: what the reader wrote in it, the blocks an item holds apart, on one line. */
|
|
1655
|
+
export const ownText = (host) => [...host.childNodes].filter((node) => !(host.tagName === "LI" && isHeld(node))).map((node) => node.textContent).join("").replace(/\s+/g, " ").trim();
|