diff --git a/bun.lock b/bun.lock index 739c15ced..9bb42dc40 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,9 @@ "@lexical/history": "^0.51.0", "@lexical/link": "^0.51.0", "@lexical/rich-text": "^0.51.0", + "@tiptap/core": "3.31.4", + "@tiptap/pm": "3.31.4", + "@tiptap/starter-kit": "3.31.4", "@types/bun": "^1.4.2", "@types/chrome": "^0.3.0", "@types/luxon": "^3.7.5", @@ -52,6 +55,7 @@ "react-dom": "19.2.0", "react-dom18": "npm:react-dom@18.3.1", "react18": "npm:react@18.3.1", + "roosterjs": "9.60.0", "slate": "^0.126.2", "slate-dom": "^0.126.0", "slate-history": "^0.113.1", @@ -648,6 +652,56 @@ "@tannin/sprintf": ["@tannin/sprintf@1.3.3", "", {}, "sha512-RwARl+hFwhzy0tg9atWcchLFvoQiOh4rrP7uG2N5E4W80BPCUX0ElcUR9St43fxB9EfjsW2df9Qp+UsTbvQDjA=="], + "@tiptap/core": ["@tiptap/core@3.31.4", "", { "peerDependencies": { "@tiptap/pm": "3.31.4" } }, "sha512-cIVUQYBycighzsk6GbmCef8ZiEDVxoT5EuAe4xg2q9dNkDqCXVqI4YGr9aavCQEiAS4uHiRwqvcsPqJHipFDfg=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-FHnbvCIW0eDKgUUFjmvEB0ax9YUBW7/Unm+38qpuQROvU872yRqhnDtycKT9IKk46jNdjM4A4fAGUbge10CrAA=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-QX1xoXvO0gz6uVsoLegumqQ4MJXL7lcUDQZ4J7la7LBqiIh4efrCmp2EXbhpr37M/YQL7w8q3g7H/VovSh7xIg=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.31.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.31.4" } }, "sha512-gHn1RDR4olCnmKpH+G9x+f8R+iKPNVdLlvbFkIpR/stjabZi7fB4Rr5GiHWsulbtxAjdQrNjnVeNdGM4rBxApw=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-pzi8GNDs5J1DEoO1HNYiJ5WZpgOZzlc/2VIa77mMpEuD4hZxuK0BQzlF/O6Ps/O5P1lGJT4xS0Yowpzb9mD9RA=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-su/B/cfQ1HpiHl6JfJ6wa+RVEptiYsV4dCntE8XQNj2dUcII4y6w6PQbbswOoDrU79uXHsAvpy44215rghG4LA=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-vT20u62RWR+BLIyNJ54e24MPUY4zm8VF7tcgJnxQXi7BgVJWcNMtMWWDIkPJ4JNXwfoIznKh3jzYsaYmbCJgQQ=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.31.4", "", { "peerDependencies": { "@tiptap/extensions": "3.31.4" } }, "sha512-5TbCPCC4RL+QT1NYHjXCYDBHBtVqYOFvFEnk3v98lvXCM2PNL13ofXUfJPuykAVy1OdTAx4DlZOQAD9WKy/IdQ=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.31.4", "", { "peerDependencies": { "@tiptap/extensions": "3.31.4" } }, "sha512-hBogUAepQH6IQUhaePhPWzQ1Oeu+yqlJVMKO06Eue5m36uRy1PZZAQ780pCGT0SC4R5MH2td1N8EUQ7bupQdDA=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-oLNajLyUvRMg1qtzFgKGduygTlpaHFB9PU+M+pUWdA4sa1bRnxS2Y++Ao4lHYopJ7nxatLndrYoCIHkotqkPDQ=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-KqlSeLathpdxCc3kVHBCA/RkHvb+V7u5vgiSEu1Cv3L70r7F19COMGODav/k1AGYBgEXKL6lM9cy8cloE5/28g=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-ws0Kh8kfHL7ESILSXj0HJAXs8G10ewlW1sedXKm8dm0op9L8il+xl8bEJVoqG/jMf77900Fc2CaQldcCGRpF1g=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-pftTQBPYZocNDAj/u6OyDIp7YxmN0+N895cQPogJQ6qMHdLiRhzgawiEewJkniyN1w8k3Wx/Etp8pTKrxaOOBQ=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@3.31.4", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-LLxW3qfeL9gvDdMPVdxYFYZ0zZSjsoIDhOqjJ5U2UfaK3U2iwkKTBgV2qT+FWf3uUHmBRjBZ08q3dRPmWBAD5g=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-Wd4YRYxOdf+TtF33/QsRo1RK1CecVxjYGjUBIKNGHAywfh8tghgV4YOUCiV1iJqYt2IBs+RWlIqTmdIPzBrd4A=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.31.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.31.4" } }, "sha512-ao8XAPvhOrtGFILs+fKtOM91omDSpUC8X19m44gbaq5xJDVpIMZ0rP+NAVUS72G7LskhcHa0bu/Pm022bUVjVw=="], + + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.31.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.31.4" } }, "sha512-etdkl/YnYfwtALDxecJ+4WYgJAez1fn1vzzZqJklZBpgqiZtny3NxpvWS7/JhsltNG84Wo/oovpfJgVmzhddrw=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.31.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.31.4" } }, "sha512-UNWyF+Dhm08znszIMnT8Y2kJ1lvtiCvUGs1aG7m9cPHxDa7VycWnOqdtXWkc+6xRLNiC0zgWCFWgLLHduV8pRQ=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-WZlBgw+XVKXerRlmEsoQ7x1PwSU+dh3X+b70+ViZsLebdfsmmYBIaNDrvIGwG9UQr5eLnddJnZplMSTI2YKs+g=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-4MBr63TJIMqxCjE+gAPdlY1IdELgvbAP516K3Ntn/BkcpEbEoPMEYVz+XTxULyHQEuARDO6jsXPPxRXpA74jaw=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-WL2ZRpFksgQcXJN05/UQH09YCH9LlhdLrSEcviASvEH/aAfsKiJlDrouN8PET+YAWK/F5xFc5/Q1z0xj+I23jw=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4" } }, "sha512-0iFGPIqmqGkCrOFh1OPo7Dzv5s86dVN3bZaugsz0jK04otkb8bn04ZDKP2/6Wq+lcK0M9DfJ0QqfW5hC2nBZxQ=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.31.4", "", { "peerDependencies": { "@tiptap/core": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-nIupIne/sHpV2Kw1M4MOmr4+G454KI8BXM5+LWKOU2EpzfALZWVSy7koF9lVRna4Zp5Gv8hfg5Z8kvmrYQvhkw=="], + + "@tiptap/pm": ["@tiptap/pm@3.31.4", "", { "dependencies": { "prosemirror-changeset": "^2.4.1", "prosemirror-commands": "^1.7.1", "prosemirror-dropcursor": "^1.8.2", "prosemirror-gapcursor": "^1.4.1", "prosemirror-history": "^1.5.0", "prosemirror-inputrules": "^1.5.1", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.11", "prosemirror-schema-list": "^1.5.1", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.5", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.42.3" } }, "sha512-x7dwth79cQp4QGae66/RJqMDJQv0+n7b7dD7cyFQBWqzPFLJxN/4paHY4qbWedZjNLhRyIOAD9a5q4AYM1zmPA=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.31.4", "", { "dependencies": { "@tiptap/core": "3.31.4", "@tiptap/extension-blockquote": "3.31.4", "@tiptap/extension-bold": "3.31.4", "@tiptap/extension-bullet-list": "3.31.4", "@tiptap/extension-code": "3.31.4", "@tiptap/extension-code-block": "3.31.4", "@tiptap/extension-document": "3.31.4", "@tiptap/extension-dropcursor": "3.31.4", "@tiptap/extension-gapcursor": "3.31.4", "@tiptap/extension-hard-break": "3.31.4", "@tiptap/extension-heading": "3.31.4", "@tiptap/extension-horizontal-rule": "3.31.4", "@tiptap/extension-italic": "3.31.4", "@tiptap/extension-link": "3.31.4", "@tiptap/extension-list": "3.31.4", "@tiptap/extension-list-item": "3.31.4", "@tiptap/extension-list-keymap": "3.31.4", "@tiptap/extension-ordered-list": "3.31.4", "@tiptap/extension-paragraph": "3.31.4", "@tiptap/extension-strike": "3.31.4", "@tiptap/extension-text": "3.31.4", "@tiptap/extension-underline": "3.31.4", "@tiptap/extensions": "3.31.4", "@tiptap/pm": "3.31.4" } }, "sha512-u7HIJ9vK6kFe4S4J2CIcDLrhRZzoTMSyU0XxjQ6lUNF7pWW3rMi//pvi8RcsZmxGjyb8CpXAqYgNl7f9WyaUeA=="], + "@types/aws-lambda": ["@types/aws-lambda@8.10.164", "", {}, "sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg=="], "@types/btoa-lite": ["@types/btoa-lite@1.0.2", "", {}, "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg=="], @@ -1000,12 +1054,16 @@ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + "color-convert": ["color-convert@3.1.0", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA=="], "color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], "color-parse": ["color-parse@2.0.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colord": ["colord@2.10.0", "", {}, "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw=="], "colorjs.io": ["colorjs.io@0.7.1", "", {}, "sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA=="], @@ -1320,7 +1378,7 @@ "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], @@ -1394,6 +1452,8 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "linkifyjs": ["linkifyjs@4.3.3", "", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], @@ -1676,18 +1736,30 @@ "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + "prosemirror-changeset": ["prosemirror-changeset@2.4.4", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA=="], + "prosemirror-commands": ["prosemirror-commands@1.7.2", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw=="], + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.4", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="], + "prosemirror-history": ["prosemirror-history@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA=="], + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], "prosemirror-model": ["prosemirror-model@1.25.12", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg=="], "prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.5", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-McBWhfgOfThyu3Nltbs8SOUbM4t/h0pxteibaaHd1QR4S+F1t0u7M1ETaCE6/cveFoF+2fowTIBNkJOyeZQjNg=="], + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + "prosemirror-transform": ["prosemirror-transform@1.12.2", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-PE/aY0HEY4zczvmqrilgkUK/WautF0chvMqkmM/iN5/aPHvwrCWaQZjS6CIZp+K84YrvPFqrL+YArrfJX8xX7g=="], "prosemirror-view": ["prosemirror-view@1.42.6", "", { "dependencies": { "prosemirror-model": "^1.25.12", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ=="], @@ -1792,6 +1864,22 @@ "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "roosterjs": ["roosterjs@9.60.0", "", { "dependencies": { "roosterjs-color-utils": "^1.1.0", "roosterjs-content-model-api": "^9.60.0", "roosterjs-content-model-core": "^9.60.0", "roosterjs-content-model-dom": "^9.60.0", "roosterjs-content-model-markdown": "^9.60.0", "roosterjs-content-model-plugins": "^9.60.0", "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-xiEG2GQXq4UtWEK7VNHvmMUAaUnL003+Q2JJkllMcKXf4TWixtiKioof1cGvxzTyGjhX7b6DJeFHe+57CreeDg=="], + + "roosterjs-color-utils": ["roosterjs-color-utils@1.1.0", "", { "dependencies": { "color": "^3.0.0" } }, "sha512-AdfGX/LSM+wiX6FovBjJLSHDqurIzXfEPCx95aIpEQ/VAvyOAHG9bja4K9aeNX/RpreuQ3kcSaJS0QzxQeFcCw=="], + + "roosterjs-content-model-api": ["roosterjs-content-model-api@9.60.0", "", { "dependencies": { "roosterjs-content-model-dom": "^9.60.0", "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-VbNdYKNBqPY6uviGyAo5iIibyUNMCrFU2gpbPJ5yR2IwmVQu9zq8Taj2Mtcd+Rip+QJNAE+5/z+BB3xvJA4ctQ=="], + + "roosterjs-content-model-core": ["roosterjs-content-model-core@9.60.0", "", { "dependencies": { "roosterjs-content-model-dom": "^9.60.0", "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-VXa4EtI5+hi9MQGVRo3hGiTBvM7u2VxqySe47IuqeUGCGmSAaYP0JtkhRCtZcThljhtlG6mP3VII+o+yx2+Jxw=="], + + "roosterjs-content-model-dom": ["roosterjs-content-model-dom@9.60.0", "", { "dependencies": { "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-qRKx/BoLHODZzExVtz8WEfKEDWVWbnlJ2kgPxa28H32l9n90nIwKP3exm4UjU3gJo/HBPI9pvtw69wVRNJMZcA=="], + + "roosterjs-content-model-markdown": ["roosterjs-content-model-markdown@9.60.0", "", { "dependencies": { "roosterjs-content-model-dom": "^9.60.0", "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-l5f+G4f/QRmvrKoXYKDQKXsc1LJOXcdIFIiP5KMO6MZdCy9LOoV4k632dYws5ltJEk54m5blsiSBL1L4Ydq+HQ=="], + + "roosterjs-content-model-plugins": ["roosterjs-content-model-plugins@9.60.0", "", { "dependencies": { "roosterjs-content-model-api": "^9.60.0", "roosterjs-content-model-core": "^9.60.0", "roosterjs-content-model-dom": "^9.60.0", "roosterjs-content-model-types": "^9.60.0", "tslib": "^2.3.1" } }, "sha512-YG2QjFZmCdu06vDzPIhxfHntm11ppKRMzZUTCSDt6azr0iUsEleeh+ZlQ3Zk+bP8jcaww3f3ctbe7VsZURo6KQ=="], + + "roosterjs-content-model-types": ["roosterjs-content-model-types@9.60.0", "", {}, "sha512-HVTFbhNF1x5VeVJgmKoDeDuCMkL9LZdEGYDf6YmsuXNgXULk0fsRCrmEqQT3sk0k+Tp37GfHkrs/txWNKuymEQ=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], "rungen": ["rungen@0.3.2", "", {}, "sha512-zWl10xu2D7zoR8zSC2U6bg5bYF6T/Wk7rxwp8IPaJH7f0Ge21G03kNHVgHR7tyVkSSfAOG0Rqf/Cl38JftSmtw=="], @@ -1852,6 +1940,8 @@ "simple-html-tokenizer": ["simple-html-tokenizer@0.5.11", "", {}, "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "slate": ["slate@0.126.2", "", {}, "sha512-38C8GU9awiEbroZm+DzXEFUWKptb1QV9xcgcUjseas4PtqQdBnEPITocosYzFHXEVrlkafjZEv8KH8pjhe0esQ=="], "slate-dom": ["slate-dom@0.126.0", "", { "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" }, "peerDependencies": { "slate": ">=0.121.0" } }, "sha512-FdRlaKh0xksnymiRiv8/8c6dYhybwTg1QJIjmvEyeOUKHfdJI55YA4gGKv4vQj4ZKklau2aHY6uX/npJu5jYMA=="], @@ -2108,6 +2198,10 @@ "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], "data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], @@ -2118,6 +2212,8 @@ "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], @@ -2198,6 +2294,8 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/docs/editor-capabilities.md b/docs/editor-capabilities.md index a761a1cfd..da5757a73 100644 --- a/docs/editor-capabilities.md +++ b/docs/editor-capabilities.md @@ -28,13 +28,33 @@ It does no model reads, stores no history, and creates no observers. The runtime checks this record before interaction. Review uses the same metadata gate before its text safety checks. `reviewApply` requires the target's separate `ReviewCapabilities` evidence. Typing permission never grants Review write permission. -The typing adapters support host transactions for ProseMirror and Slate, one model transaction for Draft.js and Trix (their Review writer), a native edit in one host undo step for TinyMCE, CKEditor 4, Froala and Summernote, and verified host input handling for CKEditor 5 and Lexical. -Quill, also a bundled Quill such as Slack's composer, takes typing through native input that Quill applies to its own model. Quill's history puts the changes of the last second into one undo step, as it does for its own typing. +The typing adapters write as follows: + +- ProseMirror and Slate: a host transaction through the MAIN-world bridge. Tiptap renders a ProseMirror view, so it uses the ProseMirror path. +- CKEditor 5: one `model.change` batch through the MAIN-world bridge. The batch replaces the text of the model block at the DOM selection and keeps the attributes of the text at the caret. + An inline object without DOM text has one model offset and no text. An inline image is an example. A `softBreak` is the same. + The bridge refuses an edit that contains an inline object. It also refuses an edit with an inline object between the edit and the caret. + The inserted text never gets the attributes of an object. + An element that shows text, for example a mention, gives no block context. + Then CKEditor 5 takes the write as `beforeinput` into its typing batch. Then Undo also removes the typed text. +- Draft.js and Trix (`MODEL_TYPING_SELECTOR`): one model transaction through their Review writer, with the caret after the accepted word. Draft.js gets one `insert-fragment` push. Trix gets one recorded undo entry. +- TinyMCE, CKEditor 4, Froala and Summernote: a native edit in one host undo step. +- RoosterJS (Outlook on the web): a native edit between two Rooster snapshots, then a `contentChanged` event, as Rooster's own find and replace does. + The bridge finds the editor in `window.__ROOSTERJS_DEVTOOLS_EDITORS__` (roosterjs 9.59 and later). It refuses during an IME composition, in shadow edit, without focus or without a range selection. After the write, Rooster must have an undo step. + A native edit that Rooster does not record is lost on Undo and Redo. Thus an identified Rooster editor without its instance gets no generic write. +- Lexical: no bridge path. FluentTyper sends a synthetic `insertReplacementText` `beforeinput` event on the replaced range. Lexical applies the event to its own model. If no handler takes the event, the write is refused. +- Quill, also a bundled Quill such as Slack's composer: native input that Quill applies to its own model. Quill 2 takes the synthetic `insertReplacementText` `beforeinput` event. Quill 1 ignores that event, so FluentTyper uses the browser's `insertText` command, and Quill reads the DOM change into its model. Quill's history puts the changes of the last second into one undo step. + Thus, before and after the write, the bridge records the pending DOM changes (`quill.update`) and starts a new undo step (`history.cutoff()`). Then Undo removes only the accepted word, not the typed text before it. + The bridge must find the Quill instance: through `window.Quill` (Quill 1 or Quill 2), or through the instance that a bundled Quill 1 keeps on its `.ql-container`. A bundled Quill 2 keeps its instances in a private module map. There, FluentTyper cannot set the boundary, and Undo also removes the text typed in the last second. +- CodeMirror 5: the bridge finds the CodeMirror instance by its methods (`replaceRange`, `getLine`, `getCursor`) on the field or an ancestor. It replaces the caret line range with `replaceRange` and the `+input` origin in one `operation`. This path needs a contenteditable field, so it needs CodeMirror's `contenteditable` input style. +- Notion: the browser's `insertText` command in one block leaf. Notion reads the DOM change into its model on `input`. See [Notion](#notion). + Their typing paths validate each write. Their fingerprints alone do not grant Review writes. -Review writes need the MAIN-world bridge to find the editor itself: Lexical, Draft.js, CKEditor 5 and Trix get a model transaction, and TinyMCE, CKEditor 4, Froala and Summernote get a native edit inside one host undo step. +Review writes need the MAIN-world bridge to find the editor itself: Lexical, Draft.js, CKEditor 5 and Trix get a model transaction, and TinyMCE, CKEditor 4, Froala, Summernote and RoosterJS get a native edit inside one host undo step. A fingerprint without a working bridge gives Review only. It cannot select a generic Review DOM writer. Acceptance handlers consume keys only after a synchronous action succeeds. Tab does not queue acceptance of an unseen inline suggestion. +Escape that closes a visible popup or inline suggestion is consumed, so the page does not also act on it (Notion would select the block, and the next keys would type nothing). With nothing visible, or while acceptance keys yield to a native list, Escape goes to the page. Code/prose transitions invalidate predictions, also when the text stays the same. The early Tab bridge checks the context recorded when the menu rendered. Temporary unknown selection states keep the host reconciliation path. ARIA-disabled and ARIA-readonly ancestors block interaction across shadow boundaries. @@ -42,24 +62,60 @@ Review-only finding cards offer Copy. Clipboard writes require a trusted click a ## Capability and reason matrix -| Context | Prose inspection | Typing suggestions / acceptance | Review / Apply | Reason or limit | -| ---------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------- | -| Ordinary input or textarea, including stale ARIA hints | Yes | Yes, when a valid action is visible | Yes / native transaction required | `available` | -| Associated visible native popup | Yes | Temporarily paused with the preference enabled | Yes / target transaction required | `native-popup` | -| Unrelated visible popup | Yes | Unchanged | Unchanged | No field association | -| Structured purpose | According to the privacy rules | Manual activation | Conservative Review exclusions | `manual-activation` | -| Usable browser datalist | Yes for prose | Manual activation; acceptance keys yield while native preference is enabled | Yes / target transaction required | `manual-activation` (conflict `browser-unknown`) | -| Model-editor fingerprint without its editor | Yes | Disabled | Review and Copy / no Apply | `unverified-writer` | -| Verified Quill, ProseMirror or Slate Review bridge | Yes | Typing transaction path | Yes / verified model transaction | Each edit revalidates model and ranges | -| Verified Lexical, Draft.js, CKEditor 5 or Trix bridge | Yes | Typing path where one exists, else disabled | Yes / verified model transaction | Each edit revalidates model and ranges | -| Verified TinyMCE, CKEditor 4, Froala or Summernote bridge | Yes | Native edit in one host undo step | Yes / native edit in one host undo step | Each edit revalidates DOM, ranges and formatting | -| Mixed prose and code | Prose with protected ranges | Fresh code predictions keep capitalization suppression | Prose only / protected ranges cannot change | Current context is separate from host eligibility | -| Credential, disabled, read-only, hidden, or detached field | No | No | No | `sensitive`, `restricted`, `hidden`, or `detached` | +| Context | Prose inspection | Typing suggestions / acceptance | Review / Apply | Reason or limit | +| ---------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------- | +| Ordinary input or textarea, including stale ARIA hints | Yes | Yes, when a valid action is visible | Yes / native transaction required | `available` | +| Associated visible native popup | Yes | Temporarily paused with the preference enabled | Yes / target transaction required | `native-popup` | +| Unrelated visible popup | Yes | Unchanged | Unchanged | No field association | +| Structured purpose | According to the privacy rules | Manual activation | Conservative Review exclusions | `manual-activation` | +| Usable browser datalist | Yes for prose | Manual activation; acceptance keys yield while native preference is enabled | Yes / target transaction required | `manual-activation` (conflict `browser-unknown`) | +| Model-editor fingerprint without a typing path | Yes | Disabled | Review and Copy / no Apply | `unverified-writer` | +| Typing-path fingerprint without its editor | Yes | Shown; each write is refused | Review and Copy / no Apply | `available`; the record permits only an attempt | +| Verified ProseMirror (also Tiptap) or Slate Review bridge | Yes | Host transaction through the bridge | Yes / verified model transaction | Each edit revalidates model and ranges | +| Quill, also a bundled Quill | Yes | Native input that Quill applies to its own model | Yes / Delta transaction or Quill beforeinput | Own undo step except for a bundled Quill 2 | +| Verified Draft.js or Trix bridge | Yes | One model transaction through the Review writer | Yes / verified model transaction | Each edit revalidates model and ranges | +| Verified CKEditor 5 bridge | Yes | One `model.change` batch through the bridge | Yes / verified model transaction | Each edit revalidates model and ranges | +| Verified Lexical bridge | Yes | `beforeinput` that Lexical applies to its own model | Yes / verified model transaction | Each edit revalidates ranges and result text | +| CodeMirror 5 with the `contenteditable` input style | No | Code predictions; `replaceRange` in one `operation` | No | `code` | +| Notion block leaf | Yes | Native edit in the leaf that holds the caret, checked after Notion's input | Yes, one fix at a time / no Fix all | No editor API; live check passed in Chrome | +| Verified TinyMCE, CKEditor 4, Froala or Summernote bridge | Yes | Native edit in one host undo step | Yes / native edit in one host undo step | Each edit revalidates DOM, ranges and formatting | +| RoosterJS editor in the developer tools list | Yes | Native edit between two Rooster snapshots | Yes / native edit in one Rooster undo step | Each edit revalidates DOM, ranges and formatting | +| RoosterJS identified without its editor instance | Yes | Shown; each write is refused | Review and Copy / no Apply | `available`; the record permits only an attempt | +| `data-ms-editor` field without RoosterJS evidence | Yes | Generic path | Yes / native transaction required | `available` | +| Mixed prose and code | Prose with protected ranges | Fresh code predictions keep capitalization suppression | Prose only / protected ranges cannot change | Current context is separate from host eligibility | +| Credential, disabled, read-only, hidden, or detached field | No | No | No | `sensitive`, `restricted`, `hidden`, or `detached` | + +A typing-path fingerprint is on the field itself. It is one of `.ProseMirror`, `[data-slate-editor]`, `[data-lexical-editor]`, `.ck-editor__editable`, `trix-editor`, `.public-DraftEditor-content`, `.mce-content-body`, `.cke_editable`, `.fr-element` and `.note-editable`, or a Gutenberg field, or a Notion block leaf. +The other fingerprints of `MODEL_EDITOR_SELECTOR` have no typing path, for example `.DraftEditor-root` or `[data-contents]` on the field. A field that is only inside a fingerprint, and has no typing-path fingerprint itself, also has no typing path. +A typing-path fingerprint without its editor never gets a generic DOM write. `ContentEditableAdapter.ts` refuses ProseMirror, Slate and Gutenberg before it sends an event. +It refuses the other model fingerprints when no handler takes the synthetic `beforeinput` event. It refuses the DOM-model editors when the bridge finds no host undo integration. +RoosterJS has no fingerprint of its own: `[contenteditable="true"][data-ms-editor="true"]` is only a candidate, because other pages and Microsoft Editor can use the attribute. The MAIN-world bridge identifies a Rooster editor through the developer tools list or through the marks that Rooster's DOM index puts on the text nodes that it rendered. A candidate without this evidence keeps the generic path. Without the bridge, FluentTyper cannot identify RoosterJS. No field in the record proves that a write will succeed. `displaySuggestions` and `reviewApply` permit only an attempt. Each write goes through a transaction. The transaction must validate its target, range, selection, result, and Undo behavior. The diagnostic record contains no text, identifiers, URLs, or accumulated event history. It is not uploaded or persisted. +## Notion + +Notion has no in-page editor API, so no writer can read its model. A live probe of app.notion.com showed this structure and behavior: + +- One root contenteditable (`[data-content-editable-root]`) holds the page. Each text block has its own nested leaf (`[data-content-editable-leaf="true"]` in `.notion-page-content`). Focus and the events stay on the root. +- Notion reads a leaf's DOM into its model on `input`, with one Notion undo step for each input. It does not take a synthetic `beforeinput` event. +- A native edit right after the selection moved into the leaf (for example right after a click) showed and was then reverted within 1 s. + +Thus FluentTyper handles the Notion root as a canvas, as a Gutenberg canvas (`GutenbergEnvironment.ts`, `NotionEnvironment.ts`). The root gets no session. Each leaf is a field: typing context, block context and Review target are the leaf that holds the selection. +The fingerprint (the root with `.notion-page-content`, and a leaf in it) grants no write. Each write, typing or Review, does these checks: + +1. The root has focus, and the whole selection is already in the leaf. A write never moves the selection into another leaf. Review gives focus back to the root, waits 100 ms for Notion, and refuses when the caret is not in the reviewed leaf. +2. No IME composition runs in the page. +3. The edit range is in the leaf's text and the leaf's text did not change. +4. After the `insertText` command, the leaf holds the expected text. FluentTyper then watches the leaf for 1 s. When Notion reverts the write in that time, Review reports the fix as unverified. A typing write that Notion reverts is logged, not written again. + +Notion's DOM lock removes each foreign attribute of a leaf. Thus FluentTyper keeps the `data-ft-*` state of a leaf on the page root, which Notion does not lock. The root names the entry of the leaf that shows the menu, and the early Tab bridge reads it there. + +Review applies one fix at a time. Each fix is one input, thus one Notion undo step. Fix all is not available ("Apply fixes individually"). +Limits: the checks read the DOM after Notion's input handling, not Notion's model. A revert after 1 s is not seen. The 100 ms and 1 s values come from one live probe. The tests use a synthetic Notion-like page (`tests/e2e/fixtures/review-editors/notion.ts`), not Notion. A live check on Notion in Chrome passed; see the [live check results](editor-surfaces.md#live-check-results). + ## Settings and lifecycle precedence 1. Global and site enablement determine whether the runtime starts. @@ -73,6 +129,8 @@ The session remains attached during a temporary native popup. Requests issued be Closure restores eligibility without a reload. Fresh suggestions use the next input or explicit request, so native choices are not replayed as corrections. Removed fields lose their sessions, listeners, timers, and UI. Replacement fields receive new sessions. If a model mounts on the same host during Review, Apply becomes unavailable. Reopen Review to resolve the new model adapter. +A Lexical, Draft.js, CKEditor 5 or Trix field whose DOM text differs from its model when Review opens is review-only until the model reads. The open Review reads the model again on each read and once a second. +A difference that stays keeps the field review-only. A partial read is not safe: a model write renders the changed block again from the model and can remove text that only the DOM has. There is no negative cache. FluentTyper UI stays outside serialized editor content. ## Limits and manual smoke procedure @@ -84,6 +142,7 @@ The MAIN-world early Tab bridge still sends an asynchronous request. An interven The receiving session revalidates the edit. The context check reduces this race but does not eliminate all asynchronous host changes. The automated tests use synthetic pages and editor fixtures. They do not establish current live-site compatibility. +For Gmail, Outlook on the web and Notion, see the [large editor surfaces report](editor-surfaces.md). For a live smoke check: 1. Open a non-sensitive composer with the production extension. diff --git a/docs/editor-surfaces.md b/docs/editor-surfaces.md new file mode 100644 index 000000000..d234c4394 --- /dev/null +++ b/docs/editor-surfaces.md @@ -0,0 +1,121 @@ +# Large editor surfaces: Gmail, Outlook and Notion + +[FluentTyper](../README.md) / [Editor capability detection](editor-capabilities.md) / Large editor surfaces + +This report covers three high-traffic editors that had no verified writer: Gmail compose, Outlook on the web (RoosterJS) and Notion. +The evidence comes from public sources, open-source code, synthetic fixtures and one live probe in a test session (October 2026). +The live probe used only a new, empty Outlook draft and one new Notion test block. It read no mail and no other page text. +A live check of the writers followed in the same way, in Chrome with a development build (see [Live check results](#live-check-results)). +**[V]** marks a verified fact. **[I]** marks an inference. + +## Summary + +| Surface | Editor model | Generic path safe? | Writer now | Status | +| ------------------- | -------------------------------------------------- | ------------------------------------------- | -------------------------------------------------- | ------------------------------------- | +| Gmail compose | DOM is the model [I] | Yes, with limits [I] | Generic contenteditable path | No change | +| Outlook (RoosterJS) | DOM plus a cached Content Model, snapshot Undo [V] | **No**: Undo and Redo can lose the edit [V] | RoosterJS snapshot transaction | Supported; live check passed (Chrome) | +| Notion | Block tree with server sync, own Undo [V] | Only when the caret is in the block [V] | Native edit in one block leaf, with a revert watch | Supported; live check passed (Chrome) | + +## Gmail compose + +**Editor model.** The body is one `contenteditable` element. No public source shows a separate document model [I]. +InboxSDK, an open-source Gmail SDK, edits the body with plain DOM ranges, and Gmail keeps these edits [V, source]. +Undo is the browser's native Undo for text edits [I]. + +**Generic path.** It is safe for text edits. `execCommand("insertText")` sends `beforeinput` and `input` and goes into the native Undo stack [I]. +A DOM edit without a key press can start the draft save late. Gmail reads the DOM when it sends, so the edit is not lost [I]. + +**Verified writer.** Not necessary. Gmail has no public editor API. A writer would depend on minified internals. + +**Risks.** + +- Smart Compose also accepts its ghost text with Tab. The native popup detector does not see that ghost text [I]. +- Quoted text (`.gmail_quote`) and signatures are in the same editable root. Review treats them as normal text [I]. +- Mail is private data. A live check must use a test account and a new, empty draft. + +**Decision.** The maintainer chose to keep the generic path. + +## Outlook on the web (RoosterJS) + +**Editor model.** RoosterJS keeps the DOM live and caches a Content Model. Undo uses HTML snapshots. Rooster takes Cmd+Z and Ctrl+Z itself, and the browser's Undo stack is not used [V]. +The compose body is a `div` with `role="textbox"`, `contenteditable="true"` and `data-ms-editor="true"`. It is in the top document [V]. + +**Generic path: not safe.** A native `insertText` edit does not tell Rooster that the content changed. After a click, one Undo removed two steps, and Redo could not restore the edit [V, live and with roosterjs 9.60.0]. + +**Verified writer.** A `Transaction` in `ReviewDomEditors.ts` uses the pattern of Rooster's own find and replace: `takeSnapshot()`, the validated native edit, then `takeSnapshot()` and `triggerEvent("contentChanged")` [V, live]. +The MAIN-world bridge finds the editor in `window.__ROOSTERJS_DEVTOOLS_EDITORS__` (roosterjs 9.59 and later). It refuses during IME, during shadow edit, without focus and without a range selection. +An editor that is identified without that list gets Review and Copy only. Typing acceptance and Review Apply are each one Rooster Undo step. +The e2e fixture uses the real `roosterjs` package as a dev dependency. + +**Bug found.** After Undo of an accepted word, the typed prefix stayed selected, and the next key replaced it [V, live]. +The first snapshot recorded the replaced range, and Rooster's Undo restores the selection of that snapshot. +Now the bridge puts the user's caret back before the first snapshot and selects the range again for the edit. TinyMCE, CKEditor 4, Froala and Summernote had the same problem and get the same fix. TinyMCE also records the caret with `beforeChange()`. + +**Risks.** + +- The devtools list is marked internal. If Microsoft removes it, Outlook falls back to Review only. +- Outlook runs extra plugins (legacy bridge, autocorrect, autoformat). They can change text near an edit. The write is then reported as unverified. +- Text in an empty editor has no Rooster mark yet. Without the list, such a field is not identified. + +## Notion + +**Editor model.** One root `contenteditable` holds the page. Each block has its own leaf `[data-content-editable-leaf="true"]` [V]. +Notion keeps a block tree, sends changes to its server, and has its own Undo. It reads the leaf DOM back into its model on `input` [V]. +There is no in-page editor API. Notion ignores synthetic `beforeinput` events [V]. + +**Generic path.** A native `insertText` edit persists, with one Undo step, when the caret is already in the leaf [V]. +Directly after a click, Notion reverted the same edit within one second, with no signal [V]. FluentTyper attached to the whole page root, not to the block. + +**Verified writer.** Each block leaf is a field, as each Gutenberg block is. A write needs the caret already in that leaf, no composition, and a range inside the leaf's text. +After the write, FluentTyper reads the leaf back and watches it for one second. A revert makes a Review fix "unverified". Each Review fix is its own Notion Undo step, so Fix all is not offered. +The e2e fixture is a synthetic page with Notion's DOM shape. It contains no Notion code. + +**Bug found.** Escape closed the FluentTyper popup but also reached the page. In Notion, Escape selects the block, and the next keys typed nothing. Now Escape is consumed only when it closes a visible FluentTyper popup or inline suggestion. + +**Bug found (live check).** Notion's DOM lock removes each foreign attribute of a block leaf at once, also each FluentTyper `data-ft-*` attribute. It logs a "Reverting mutation of attribute" warning for each [V]. +Thus the MAIN-world early Tab bridge did not see FluentTyper's state, and Tab used the slower path. FluentTyper does not write again when an attribute is removed: it writes only on its own events (attach, menu render). This is not a loop. +Now the state of a leaf is on the page root, which Notion does not lock [V]. The root names the entry of the leaf that shows the menu. + +**Risks.** + +- Verification reads the DOM, not Notion's model. A revert later than one second is not seen. +- The 100 ms and one-second limits come from one live probe. +- Each block gets its own session and a hidden popup host. + +## Live check results + +Chrome, development build, October 2026. All text was synthetic. The test blocks were removed after the check. The test draft was cleared and closed, and it was not sent. + +| # | Check | Result | +| --- | ------------------------------------------------------------------------------ | ------ | +| 1 | Outlook: the developer tools list holds the compose editor | Pass | +| 2 | Outlook: Tab acceptance, then one Undo and one Redo | Pass | +| 3 | Outlook: the same after a click at the line end and after a space | Pass | +| 4 | Outlook: Review has no "Review only" note; one fix and Fix all, each one Undo | Pass | +| 5 | Outlook: the "To" field keeps the generic path | Pass | +| 6 | Outlook: Outlook's autocorrect does not change the accepted word | Pass | +| 7 | Notion: Tab acceptance in a block, one Undo, the word stays after a reload | Pass | +| 8 | Notion: acceptance in a second block stays after a reload | Pass | +| 9 | Notion: Escape with the popup open closes only the popup | Pass | +| 10 | Notion: Review note, no Fix all, one fix, one Undo, the fix stays after reload | Pass | +| 11 | Notion: a fix with the caret in another block is refused | Pass | + +The check found two bugs. Both have a fix and a regression test: + +- Outlook: after Undo of an accepted word, the typed prefix stayed selected (see [Outlook](#outlook-on-the-web-roosterjs)). +- Notion: the DOM lock removed FluentTyper's attributes from the leaves (see [Notion](#notion)). + +Limits that stay: + +- Notion: a one-line block shows no Review button. The launcher needs a field of at least 36 px height (`ReviewLauncher.ts`), and a one-line leaf is 28 px high. Open Review with the shortcut or the popup. +- Notion: one Undo can remove the user's typing and a FluentTyper autocorrect together. Notion puts them into one undo step. +- Outlook: the "To" field gets the capitalization of the first word ("hello" becomes "Hello"), as a prose field does. + +## Live checks that are still open + +1. A real IME composition in Outlook and Notion: the writers must refuse. +2. Firefox on the live sites. The synthetic RoosterJS and Notion-like fixtures pass the full e2e suite on Firefox and Chrome. + +--- + +[Editor capability detection](editor-capabilities.md) · [Review reference](review-reference.md) diff --git a/docs/review-reference.md b/docs/review-reference.md index 5c44ccc50..02f7c453c 100644 --- a/docs/review-reference.md +++ b/docs/review-reference.md @@ -852,11 +852,14 @@ signature and invalidate pending fixes. | `