From 732e0c4db67cb660b19bacde5d5e38c69c1a1306 Mon Sep 17 00:00:00 2001 From: jae kaplan Date: Fri, 17 Oct 2025 17:02:59 -0400 Subject: [PATCH] initial commit --- .gitignore | 3 + LICENSE | 21 + package.json | 77 ++ pnpm-lock.yaml | 2112 +++++++++++++++++++++++++++++++ readme.md | 36 + rollup.config.ts | 11 + src/components/custom-emoji.tsx | 19 + src/components/info-box.tsx | 41 + src/components/mention.tsx | 24 + src/index.ts | 2 + src/lib/emoji.ts | 107 ++ src/lib/mention-parsing.ts | 89 ++ src/lib/other-rendering.ts | 139 ++ src/lib/post-rendering.tsx | 384 ++++++ src/lib/sanitize.tsx | 340 +++++ src/lib/shared-types.ts | 27 + src/lib/unified-processors.ts | 178 +++ src/types/access-result.ts | 10 + src/types/asks.ts | 37 + src/types/attachments.ts | 30 + src/types/ids.ts | 44 + src/types/limits.ts | 5 + src/types/post-blocks.ts | 244 ++++ src/types/posts.ts | 10 + src/types/projects.ts | 69 + src/types/tagged.ts | 7 + src/types/username-verifier.ts | 62 + src/types/wire-models.ts | 183 +++ tsconfig.json | 12 + 29 files changed, 4323 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 readme.md create mode 100644 rollup.config.ts create mode 100644 src/components/custom-emoji.tsx create mode 100644 src/components/info-box.tsx create mode 100644 src/components/mention.tsx create mode 100644 src/index.ts create mode 100644 src/lib/emoji.ts create mode 100644 src/lib/mention-parsing.ts create mode 100644 src/lib/other-rendering.ts create mode 100644 src/lib/post-rendering.tsx create mode 100644 src/lib/sanitize.tsx create mode 100644 src/lib/shared-types.ts create mode 100644 src/lib/unified-processors.ts create mode 100644 src/types/access-result.ts create mode 100644 src/types/asks.ts create mode 100644 src/types/attachments.ts create mode 100644 src/types/ids.ts create mode 100644 src/types/limits.ts create mode 100644 src/types/post-blocks.ts create mode 100644 src/types/posts.ts create mode 100644 src/types/projects.ts create mode 100644 src/types/tagged.ts create mode 100644 src/types/username-verifier.ts create mode 100644 src/types/wire-models.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b4a82e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c00d9eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 anti software software club LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3e53cd --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "cohost-renderer", + "version": "1.0.0", + "description": "", + "module": "dist/index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "rollup -c --configPlugin @rollup/plugin-typescript" + }, + "keywords": [], + "author": "anti software software club llc (https://antisoftware.club)", + "contributors": [ + "jae kaplan (https://jkap.io)", + "colin bayer (https://gameboat.org)" + ], + "license": "MIT", + "files": [ + "dist" + ], + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.4", + "@types/hast": "^2.3.4", + "@types/html-to-text": "^9.0.4", + "@types/lodash": "^4.17.20", + "@types/luxon": "^3.7.1", + "@types/mdast": "^3.0.10", + "@types/react": "^18", + "@types/react-dom": "^18", + "hast-util-sanitize": "^5.0.2", + "html-to-text": "^9.0.5", + "lodash": "^4.17.21", + "rollup": "^4.52.4", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "react": "^18", + "react-dom": "^18" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "dependencies": { + "deepmerge": "^4.3.1", + "luxon": "^3.7.2", + "remark": "^15.0.1", + "style-to-object": "^1.0.11", + "zod": "^4.1.12", + "hast-util-sanitize": "^4.0.0", + "html-to-text": "^9.0.5", + "lodash": "^4.17.21", + "rehype-external-links": "^2.0.0", + "rehype-raw": "^6.1.1", + "rehype-react": "^7.1.1", + "rehype-remark": "^9.1.2", + "rehype-sanitize": "^5.0.1", + "rehype-stringify": "^9.0.3", + "remark-breaks": "^3.0.3", + "remark-gfm": "^3.0.1", + "remark-parse": "^10.0.1", + "remark-rehype": "^10.1.0", + "remark-stringify": "^10.0.2", + "unified": "^10.1.2", + "unist-builder": "^3.0.0", + "unist-util-is": "^5.2.0", + "unist-util-visit": "^4.1.0", + "unist-util-visit-parents": "^5.1.3" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3a9b6a3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2112 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 + hast-util-sanitize: + specifier: ^4.0.0 + version: 4.1.0 + html-to-text: + specifier: ^9.0.5 + version: 9.0.5 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + luxon: + specifier: ^3.7.2 + version: 3.7.2 + rehype-external-links: + specifier: ^2.0.0 + version: 2.1.0 + rehype-raw: + specifier: ^6.1.1 + version: 6.1.1 + rehype-react: + specifier: ^7.1.1 + version: 7.2.0(@types/react@18.3.26) + rehype-remark: + specifier: ^9.1.2 + version: 9.1.2 + rehype-sanitize: + specifier: ^5.0.1 + version: 5.0.1 + rehype-stringify: + specifier: ^9.0.3 + version: 9.0.4 + remark: + specifier: ^15.0.1 + version: 15.0.1 + remark-breaks: + specifier: ^3.0.3 + version: 3.0.3 + remark-gfm: + specifier: ^3.0.1 + version: 3.0.1 + remark-parse: + specifier: ^10.0.1 + version: 10.0.2 + remark-rehype: + specifier: ^10.1.0 + version: 10.1.0 + remark-stringify: + specifier: ^10.0.2 + version: 10.0.3 + style-to-object: + specifier: ^1.0.11 + version: 1.0.11 + unified: + specifier: ^10.1.2 + version: 10.1.2 + unist-builder: + specifier: ^3.0.0 + version: 3.0.1 + unist-util-is: + specifier: ^5.2.0 + version: 5.2.1 + unist-util-visit: + specifier: ^4.1.0 + version: 4.1.2 + unist-util-visit-parents: + specifier: ^5.1.3 + version: 5.1.3 + zod: + specifier: ^4.1.12 + version: 4.1.12 + devDependencies: + '@rollup/plugin-typescript': + specifier: ^12.1.4 + version: 12.1.4(rollup@4.52.4)(tslib@2.8.1)(typescript@5.9.3) + '@types/hast': + specifier: ^2.3.4 + version: 2.3.10 + '@types/html-to-text': + specifier: ^9.0.4 + version: 9.0.4 + '@types/lodash': + specifier: ^4.17.20 + version: 4.17.20 + '@types/luxon': + specifier: ^3.7.1 + version: 3.7.1 + '@types/mdast': + specifier: ^3.0.10 + version: 3.0.15 + '@types/react': + specifier: ^18 + version: 18.3.26 + '@types/react-dom': + specifier: ^18 + version: 18.3.7(@types/react@18.3.26) + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + rollup: + specifier: ^4.52.4 + version: 4.52.4 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@mapbox/hast-util-table-cell-style@0.2.1': + resolution: {integrity: sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==} + engines: {node: '>=12'} + + '@rollup/plugin-typescript@12.1.4': + resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} + cpu: [x64] + os: [win32] + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/extend@3.0.4': + resolution: {integrity: sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/html-to-text@9.0.4': + resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/parse5@6.0.3': + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.26': + resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-to-hyperscript@10.0.3: + resolution: {integrity: sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==} + + hast-util-embedded@2.0.1: + resolution: {integrity: sha512-QUdSOP1/o+/TxXtpPFXR2mUg2P+ySrmlX7QjwHZCXqMFyYk7YmcGSvqRW+4XgXAoHifdE1t2PwFaQK33TqVjSw==} + + hast-util-from-parse5@7.1.2: + resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} + + hast-util-has-property@2.0.1: + resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==} + + hast-util-is-body-ok-link@2.0.0: + resolution: {integrity: sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==} + + hast-util-is-element@2.1.3: + resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-phrasing@2.0.2: + resolution: {integrity: sha512-yGkCfPkkfCyiLfK6KEl/orMDr/zgCnq/NaO9HfULx6/Zga5fso5eqQA5Ov/JZVqACygvw9shRYWgXNcG2ilo7w==} + + hast-util-raw@7.2.3: + resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} + + hast-util-sanitize@4.1.0: + resolution: {integrity: sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==} + + hast-util-to-html@8.0.4: + resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==} + + hast-util-to-mdast@8.4.1: + resolution: {integrity: sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==} + + hast-util-to-parse5@7.1.0: + resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} + + hast-util-to-text@3.1.2: + resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} + + hast-util-whitespace@2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + is-absolute-url@4.0.1: + resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@5.1.2: + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + + mdast-util-find-and-replace@2.2.2: + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@1.0.3: + resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} + + mdast-util-gfm-footnote@1.0.2: + resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} + + mdast-util-gfm-strikethrough@1.0.3: + resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} + + mdast-util-gfm-table@1.0.7: + resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} + + mdast-util-gfm-task-list-item@1.0.2: + resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} + + mdast-util-gfm@2.0.2: + resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} + + mdast-util-newline-to-break@1.0.0: + resolution: {integrity: sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==} + + mdast-util-phrasing@3.0.1: + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@12.3.0: + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} + + mdast-util-to-markdown@1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@1.0.5: + resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} + + micromark-extension-gfm-footnote@1.1.2: + resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} + + micromark-extension-gfm-strikethrough@1.0.7: + resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} + + micromark-extension-gfm-table@1.0.7: + resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} + + micromark-extension-gfm-tagfilter@1.0.2: + resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} + + micromark-extension-gfm-task-list-item@1.0.5: + resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} + + micromark-extension-gfm@2.0.3: + resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + rehype-external-links@2.1.0: + resolution: {integrity: sha512-2YMJZVM1hxZnwl9IPkbN5Pjn78kXkAX7lq9VEtlaGA29qIls25vZN+ucNIJdbQUe+9NNFck17BiOhGmsD6oLIg==} + + rehype-minify-whitespace@5.0.1: + resolution: {integrity: sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==} + + rehype-raw@6.1.1: + resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==} + + rehype-react@7.2.0: + resolution: {integrity: sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==} + peerDependencies: + '@types/react': '>=17' + + rehype-remark@9.1.2: + resolution: {integrity: sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==} + + rehype-sanitize@5.0.1: + resolution: {integrity: sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==} + + rehype-stringify@9.0.4: + resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==} + + remark-breaks@3.0.3: + resolution: {integrity: sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==} + + remark-gfm@3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + + remark-parse@10.0.2: + resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + + remark-stringify@10.0.3: + resolution: {integrity: sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + + style-to-object@1.0.11: + resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trim-trailing-lines@2.1.0: + resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-builder@3.0.1: + resolution: {integrity: sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==} + + unist-util-find-after@4.0.1: + resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} + + unist-util-generated@2.0.1: + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} + + unist-util-is@3.0.0: + resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@4.0.4: + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@2.1.2: + resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@1.4.1: + resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + vfile-location@4.1.0: + resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} + + vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@mapbox/hast-util-table-cell-style@0.2.1': + dependencies: + unist-util-visit: 1.4.1 + + '@rollup/plugin-typescript@12.1.4(rollup@4.52.4)(tslib@2.8.1)(typescript@5.9.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.52.4) + resolve: 1.22.10 + typescript: 5.9.3 + optionalDependencies: + rollup: 4.52.4 + tslib: 2.8.1 + + '@rollup/pluginutils@5.3.0(rollup@4.52.4)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.4 + + '@rollup/rollup-android-arm-eabi@4.52.4': + optional: true + + '@rollup/rollup-android-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-x64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.4': + optional: true + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/extend@3.0.4': {} + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/html-to-text@9.0.4': {} + + '@types/lodash@4.17.20': {} + + '@types/luxon@3.7.1': {} + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/parse5@6.0.3': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.26)': + dependencies: + '@types/react': 18.3.26 + + '@types/react@18.3.26': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + bail@2.0.2: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + comma-separated-tokens@2.0.3: {} + + csstype@3.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + deepmerge@4.3.1: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@5.2.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + entities@4.5.0: {} + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + extend@3.0.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-to-hyperscript@10.0.3: + dependencies: + '@types/unist': 2.0.11 + comma-separated-tokens: 2.0.3 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + web-namespaces: 2.0.1 + + hast-util-embedded@2.0.1: + dependencies: + hast-util-is-element: 2.1.3 + + hast-util-from-parse5@7.1.2: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + hastscript: 7.2.0 + property-information: 6.5.0 + vfile: 5.3.7 + vfile-location: 4.1.0 + web-namespaces: 2.0.1 + + hast-util-has-property@2.0.1: {} + + hast-util-is-body-ok-link@2.0.0: + dependencies: + '@types/hast': 2.3.10 + hast-util-has-property: 2.0.1 + hast-util-is-element: 2.1.3 + + hast-util-is-element@2.1.3: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.10 + + hast-util-phrasing@2.0.2: + dependencies: + '@types/hast': 2.3.10 + hast-util-embedded: 2.0.1 + hast-util-has-property: 2.0.1 + hast-util-is-body-ok-link: 2.0.0 + hast-util-is-element: 2.1.3 + + hast-util-raw@7.2.3: + dependencies: + '@types/hast': 2.3.10 + '@types/parse5': 6.0.3 + hast-util-from-parse5: 7.1.2 + hast-util-to-parse5: 7.1.0 + html-void-elements: 2.0.1 + parse5: 6.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@4.1.0: + dependencies: + '@types/hast': 2.3.10 + + hast-util-to-html@8.0.4: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-raw: 7.2.3 + hast-util-whitespace: 2.0.1 + html-void-elements: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-mdast@8.4.1: + dependencies: + '@types/extend': 3.0.4 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + extend: 3.0.2 + hast-util-has-property: 2.0.1 + hast-util-is-element: 2.1.3 + hast-util-phrasing: 2.0.2 + hast-util-to-text: 3.1.2 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + rehype-minify-whitespace: 5.0.1 + trim-trailing-lines: 2.1.0 + unist-util-is: 5.2.1 + unist-util-visit: 4.1.2 + + hast-util-to-parse5@7.1.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@3.1.2: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + hast-util-is-element: 2.1.3 + unist-util-find-after: 4.0.1 + + hast-util-whitespace@2.0.1: {} + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + html-void-elements@2.0.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + inline-style-parser@0.1.1: {} + + inline-style-parser@0.2.4: {} + + is-absolute-url@4.0.1: {} + + is-buffer@2.0.5: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-plain-obj@4.1.0: {} + + js-tokens@4.0.0: {} + + kleur@4.1.5: {} + + leac@0.6.0: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + luxon@3.7.2: {} + + markdown-table@3.0.4: {} + + mdast-util-definitions@5.1.2: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + unist-util-visit: 4.1.2 + + mdast-util-find-and-replace@2.2.2: + dependencies: + '@types/mdast': 3.0.15 + escape-string-regexp: 5.0.0 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + decode-named-character-reference: 1.2.0 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.2 + micromark-util-character: 1.2.0 + + mdast-util-gfm-footnote@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.1.0 + + mdast-util-gfm-strikethrough@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-table@1.0.7: + dependencies: + '@types/mdast': 3.0.15 + markdown-table: 3.0.4 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm@2.0.2: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-gfm-autolink-literal: 1.0.3 + mdast-util-gfm-footnote: 1.0.2 + mdast-util-gfm-strikethrough: 1.0.3 + mdast-util-gfm-table: 1.0.7 + mdast-util-gfm-task-list-item: 1.0.2 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-newline-to-break@1.0.0: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-find-and-replace: 2.2.2 + + mdast-util-phrasing@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + unist-util-is: 5.2.1 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@12.3.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-definitions: 5.1.2 + micromark-util-sanitize-uri: 1.2.0 + trim-lines: 3.0.1 + unist-util-generated: 2.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + + mdast-util-to-markdown@1.5.0: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + micromark-util-decode-string: 1.1.0 + unist-util-visit: 4.1.2 + zwitch: 2.0.4 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.15 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@1.0.5: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm-footnote@1.1.2: + dependencies: + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-strikethrough@1.0.7: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-table@1.0.7: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-tagfilter@1.0.2: + dependencies: + micromark-util-types: 1.1.0 + + micromark-extension-gfm-task-list-item@1.0.5: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm@2.0.3: + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.5 + micromark-extension-gfm-footnote: 1.1.2 + micromark-extension-gfm-strikethrough: 1.0.7 + micromark-extension-gfm-table: 1.0.7 + micromark-extension-gfm-tagfilter: 1.0.2 + micromark-extension-gfm-task-list-item: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@1.1.0: {} + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mri@1.2.0: {} + + ms@2.1.3: {} + + parse5@6.0.1: {} + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + path-parse@1.0.7: {} + + peberminta@0.9.0: {} + + picomatch@4.0.3: {} + + property-information@6.5.0: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + rehype-external-links@2.1.0: + dependencies: + '@types/hast': 2.3.10 + extend: 3.0.2 + hast-util-is-element: 2.1.3 + is-absolute-url: 4.0.1 + space-separated-tokens: 2.0.2 + unified: 10.1.2 + unist-util-visit: 4.1.2 + + rehype-minify-whitespace@5.0.1: + dependencies: + '@types/hast': 2.3.10 + hast-util-embedded: 2.0.1 + hast-util-is-element: 2.1.3 + hast-util-whitespace: 2.0.1 + unified: 10.1.2 + unist-util-is: 5.2.1 + + rehype-raw@6.1.1: + dependencies: + '@types/hast': 2.3.10 + hast-util-raw: 7.2.3 + unified: 10.1.2 + + rehype-react@7.2.0(@types/react@18.3.26): + dependencies: + '@mapbox/hast-util-table-cell-style': 0.2.1 + '@types/hast': 2.3.10 + '@types/react': 18.3.26 + hast-to-hyperscript: 10.0.3 + hast-util-whitespace: 2.0.1 + unified: 10.1.2 + + rehype-remark@9.1.2: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + hast-util-to-mdast: 8.4.1 + unified: 10.1.2 + + rehype-sanitize@5.0.1: + dependencies: + '@types/hast': 2.3.10 + hast-util-sanitize: 4.1.0 + unified: 10.1.2 + + rehype-stringify@9.0.4: + dependencies: + '@types/hast': 2.3.10 + hast-util-to-html: 8.0.4 + unified: 10.1.2 + + remark-breaks@3.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-newline-to-break: 1.0.0 + unified: 10.1.2 + + remark-gfm@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-gfm: 2.0.2 + micromark-extension-gfm: 2.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-parse@10.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@10.1.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-to-hast: 12.3.0 + unified: 10.1.2 + + remark-stringify@10.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + unified: 10.1.2 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.52.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + style-to-object@0.4.4: + dependencies: + inline-style-parser: 0.1.1 + + style-to-object@1.0.11: + dependencies: + inline-style-parser: 0.2.4 + + supports-preserve-symlinks-flag@1.0.0: {} + + trim-lines@3.0.1: {} + + trim-trailing-lines@2.1.0: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.11 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 5.3.7 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-builder@3.0.1: + dependencies: + '@types/unist': 2.0.11 + + unist-util-find-after@4.0.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-generated@2.0.1: {} + + unist-util-is@3.0.0: {} + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.11 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@4.0.4: + dependencies: + '@types/unist': 2.0.11 + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@2.1.2: + dependencies: + unist-util-is: 3.0.0 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@1.4.1: + dependencies: + unist-util-visit-parents: 2.1.2 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + + vfile-location@4.1.0: + dependencies: + '@types/unist': 2.0.11 + vfile: 5.3.7 + + vfile-message@3.1.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 3.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@5.3.7: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + web-namespaces@2.0.1: {} + + zod@4.1.12: {} + + zwitch@2.0.4: {} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2ac846f --- /dev/null +++ b/readme.md @@ -0,0 +1,36 @@ +# cohost renderer + +> No effort has been made to validate that this actually works outside of typechecking and bundling! + +bare-minimum version that builds. no package yet, no instructions yet, this is +just the bare minimum extraction to make this build outside the cohost codebase. +i strongly recommend against using this for anything serious. + +things that are missing that will not be added: + +- `InfoBox` is a bare-minimum implementation that doesn't use any of our styling + because i didn't want to make tailwind a dependency here +- custom emoji loading is not implemented as it was extremely dependent on the + cohost build system. if you can populate the arrays then it should render, but + doing that is left as an exercise to the reader. +- probably other things i'm not thinking about + +things that are missing that will be added: + +- instructions +- an example +- an npm package + +things that are here that will probably be removed at some point: + +- extraneous IDs, types, etc. this was pulled straight out of the cohost + codebase and while i removed the obviously unnecessary bits (things related to + invites, for example) there might be some bonus bullshit in here. +- code related to AST caching. it's entirely unnecessary for non-production use. + +# license + +this release is licensed under the MIT license. the remaining cohost frontend +codebase is source-available (if you know where to look) but not open-source, +the backend remains private. we have no intention to open-source the remainder +of cohost at this time. diff --git a/rollup.config.ts b/rollup.config.ts new file mode 100644 index 0000000..1263d64 --- /dev/null +++ b/rollup.config.ts @@ -0,0 +1,11 @@ +import typescript from "@rollup/plugin-typescript"; + +export default { + input: "src/index.ts", + output: { + dir: "dist", + format: "es", + sourcemap: true, + }, + plugins: [typescript()], +}; diff --git a/src/components/custom-emoji.tsx b/src/components/custom-emoji.tsx new file mode 100644 index 0000000..184f9f9 --- /dev/null +++ b/src/components/custom-emoji.tsx @@ -0,0 +1,19 @@ +import React, { FunctionComponent } from "react"; + +export const CustomEmoji: FunctionComponent<{ + name: string; + url: string; +}> = React.memo(({ name = "missing", url = "" }) => { + return ( + {`:${name}:`} + ); +}); +CustomEmoji.displayName = "CustomEmoji"; diff --git a/src/components/info-box.tsx b/src/components/info-box.tsx new file mode 100644 index 0000000..c851a5c --- /dev/null +++ b/src/components/info-box.tsx @@ -0,0 +1,41 @@ +import React, { FunctionComponent, ReactElement, useContext } from "react"; +import { z } from "zod"; + +export const InfoBoxLevel = z.enum([ + "info", + "warning", + "done", + "post-box-info", + "post-box-warning", +]); +export type InfoBoxLevel = z.infer; + +type InfoBoxProps = { + level: InfoBoxLevel; +}; + +export const InfoBox: FunctionComponent< + React.PropsWithChildren +> = ({ level, children }) => { + let bgClasses: string; + + switch (level) { + case "info": + bgClasses = "co-info-box co-info"; + break; + case "warning": + bgClasses = "co-info-box co-warning"; + break; + case "done": + bgClasses = "co-info-box co-done"; + break; + case "post-box-info": + bgClasses = "co-info-box co-post-info"; + break; + case "post-box-warning": + bgClasses = "co-info-box co-post-warning"; + break; + } + + return
{children}
; +}; diff --git a/src/components/mention.tsx b/src/components/mention.tsx new file mode 100644 index 0000000..d8d2c38 --- /dev/null +++ b/src/components/mention.tsx @@ -0,0 +1,24 @@ +import { ProjectHandle } from "../types/ids"; +import React, { FunctionComponent } from "react"; + +export const Mention: FunctionComponent<{ handle: ProjectHandle }> = ({ + handle, +}) => { + return ( + + @{handle} + + ); +}; + +/** + * Default props included because Mention is used outside of typescript and we + * need an easy way to see when it's fucked instead of just crashing + */ +Mention.defaultProps = { + handle: "ERROR" as ProjectHandle, +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8379d87 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from "./lib/post-rendering"; +export * from "./lib/other-rendering"; diff --git a/src/lib/emoji.ts b/src/lib/emoji.ts new file mode 100644 index 0000000..b95eb06 --- /dev/null +++ b/src/lib/emoji.ts @@ -0,0 +1,107 @@ +import path from "path"; +import { SKIP } from "unist-util-visit"; +import { processMatches } from "./unified-processors"; +import type { Plugin, Compiler } from "unified"; +import { Element, Text, type Root } from "hast"; + +const EMOJI_REGEX = /:[a-zA-Z\d-_]+:/gims; + +export type CustomEmoji = { + id: string; + name: string; + keywords: string[]; + skins: { src: string }[]; + native?: undefined; + // added by the library; we don't need to set it + shortcodes?: string; +}; +type CustomEmojiCategory = { + id: string; + name: string; + emojis: CustomEmoji[]; +}; +export type CustomEmojiSet = CustomEmojiCategory[]; + +type NativeEmoji = { + id: string; + keywords: string[]; + name: string; + native: string; + shortcodes: string; + unified: string; +}; + +export type Emoji = NativeEmoji | CustomEmoji; + +export const customEmoji: CustomEmoji[] = []; + +export const cohostPlusCustomEmoji: CustomEmoji[] = []; + +export const indexableCustomEmoji = new Map( + customEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => { + return [...collector, [emoji.name, emoji]]; + }, []) +); + +export const indexableCohostPlusCustomEmoji = new Map( + cohostPlusCustomEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => { + return [...collector, [emoji.name, emoji]]; + }, []) +); + +type ParseOptions = { + cohostPlus: boolean; +}; + +export const parseEmoji = (options: ParseOptions) => { + const compiler: Compiler = processMatches( + EMOJI_REGEX, + (matches, splits, node, index, parent) => { + const els = splits.reduce>( + (collector, curr, index) => { + const currNode: Text = { + type: "text", + value: curr, + }; + + const pending = [...collector, currNode]; + + if (index < matches.length) { + const emojiName = matches[index].slice( + 1, + matches[index].length - 1 + ); + let emoji = indexableCustomEmoji.get(emojiName); + if (!emoji && options.cohostPlus) { + emoji = indexableCohostPlusCustomEmoji.get(emojiName); + } + + if (emoji) { + pending.push({ + type: "element", + tagName: "CustomEmoji", + properties: { + name: emoji.name, + url: emoji.skins[0].src, + }, + children: [], + } as Element); + } else { + pending.push({ + type: "text", + value: matches[index], + }); + } + } + + return pending; + }, + [] as Array + ); + parent.children.splice(index, 1, ...els); + // skip over all the new elements we just created + return [SKIP, index + els.length]; + } + ); + return compiler; +}; diff --git a/src/lib/mention-parsing.ts b/src/lib/mention-parsing.ts new file mode 100644 index 0000000..262f092 --- /dev/null +++ b/src/lib/mention-parsing.ts @@ -0,0 +1,89 @@ +// Adapted from https://github.com/twitter/twitter-text + +function regexSupplant( + regex: RegExp | string, + map: Record, + flags = "" +) { + if (typeof regex !== "string") { + if (regex.global && flags.indexOf("g") < 0) { + flags += "g"; + } + if (regex.ignoreCase && flags.indexOf("i") < 0) { + flags += "i"; + } + if (regex.multiline && flags.indexOf("m") < 0) { + flags += "m"; + } + + regex = regex.source; + } + + return new RegExp( + regex.replace(/#\{(\w+)\}/g, function (match, name: string) { + let newRegex = map[name] || ""; + if (typeof newRegex !== "string") { + newRegex = newRegex.source; + } + return newRegex; + }), + flags + ); +} + +const latinAccentChars = + /\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u024F\u0253\u0254\u0256\u0257\u0259\u025B\u0263\u0268\u026F\u0272\u0289\u028B\u02BB\u0300-\u036F\u1E00-\u1EFF/; +const atSigns = /[@@]/; +const validMentionPrecedingChars = + /(?:^|[^a-zA-Z0-9_!#$%&*@@\\/]|(?:^|[^a-zA-Z0-9_+~.-\\/]))/; + +const validMention = regexSupplant( + "(#{validMentionPrecedingChars})" + // $1: Preceding character + "(#{atSigns})" + // $2: At mark + "([a-zA-Z0-9-]{3,})", // $3: handle + { validMentionPrecedingChars, atSigns }, + "g" +); +const endMentionMatch = regexSupplant( + /^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/, + { atSigns, latinAccentChars } +); + +type MentionToken = { + handle: string; + indices: [startPosition: number, endPosition: number]; +}; +export function extractMentions(text: string): MentionToken[] { + if (!text.match(atSigns)) { + return []; + } + + const possibleNames: MentionToken[] = []; + + text.replace( + validMention, + function ( + match, + before: string, + atSign: string, + handle: string, + offset: number, + chunk: string + ) { + const after = chunk.slice(offset + match.length); + + if (!after.match(endMentionMatch)) { + const startPosition = offset + before.length; + const endPosition = startPosition + handle.length + 1; + possibleNames.push({ + handle, + indices: [startPosition, endPosition], + }); + } + + return ""; + } + ); + + return possibleNames; +} diff --git a/src/lib/other-rendering.ts b/src/lib/other-rendering.ts new file mode 100644 index 0000000..92132e4 --- /dev/null +++ b/src/lib/other-rendering.ts @@ -0,0 +1,139 @@ +/** + * For comments, page descriptions, etc. Everything that accepts markdown and + * isn't a post. + */ + +import { CustomEmoji } from "../components/custom-emoji"; +import { compile } from "html-to-text"; +import { createElement, Fragment } from "react"; +import rehypeExternalLinks from "rehype-external-links"; +import rehypeReact from "rehype-react"; +import rehypeSanitize from "rehype-sanitize"; +import rehypeStringify from "rehype-stringify"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; +import { Mention } from "../components/mention"; +import { parseEmoji } from "./emoji"; +import { chooseAgeRuleset } from "./sanitize"; +import { MAX_GFM_LINES, RenderingOptions } from "./shared-types"; +import { + cleanUpFootnotes, + convertMentions, + copyImgAltToTitle, +} from "./unified-processors"; +import remarkBreaks from "remark-breaks"; +import _ from "lodash"; + +const convert = compile({ + wordwrap: false, +}); + +/** + * Used in places like comments, page descriptions, etc. places we don't want to + * support arbitrary HTML + * @returns + */ +const markdownRenderStackNoHTML = ( + postDate: Date, + lineLength: number, + options: RenderingOptions +) => { + let stack = unified().use(remarkParse); + + const ruleset = chooseAgeRuleset(postDate); + + if (ruleset.singleLineBreaks) { + stack = stack.use(remarkBreaks); + } + + const externalRel = ["nofollow"]; + if (options.externalLinksInNewTab) { + externalRel.push("noopener"); + } + + if (lineLength < MAX_GFM_LINES) { + stack = stack.use(remarkGfm, { + singleTilde: false, + }); + } + + const effectiveSchema = { ...ruleset.schema }; + + if (options.renderingContext === "ask" && !ruleset.ask.allowEmbeddedMedia) { + effectiveSchema.tagNames = _.filter( + effectiveSchema.tagNames, + (tagName) => !["img", "picture", "audio", "video"].includes(tagName) + ); + } + + return stack + .use(remarkRehype) + .use(() => copyImgAltToTitle) + .use(() => cleanUpFootnotes) + .use(rehypeSanitize, effectiveSchema) + .use(() => ruleset.additionalVisitor) + .use(rehypeExternalLinks, { + rel: externalRel, + target: options.externalLinksInNewTab ? "_blank" : "_self", + }); +}; + +export function renderMarkdownNoHTML( + src: string, + publishDate: Date, + options: RenderingOptions +): string { + const lineLength = src.split("\n", MAX_GFM_LINES).length; + return markdownRenderStackNoHTML(publishDate, lineLength, options) + .use(rehypeStringify) + .processSync(src) + .toString(); +} + +export function renderSummaryNoHTML( + src: string, + publishDate: Date, + options: RenderingOptions +): string { + const renderedBody = renderMarkdownNoHTML(src, publishDate, options); + return convert(renderedBody); +} + +export function renderMarkdownReactNoHTML( + src: string, + publishDate: Date, + options: RenderingOptions +) { + const components = { + Mention, + CustomEmoji, + }; + + if (options.renderingContext === "artistAlley") { + // remove headers for artist alley + Object.assign(components, { + h1: "strong", + h2: "strong", + h3: "strong", + h4: "strong", + h5: "strong", + h6: "strong", + }); + } + + const lineLength = src.split("\n", MAX_GFM_LINES).length; + return ( + markdownRenderStackNoHTML(publishDate, lineLength, options) + .use(() => convertMentions) + .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) + // @ts-expect-error rehype-react types are broken + .use(rehypeReact, { + createElement, + Fragment, + components, + }) + .processSync(src).result + ); +} diff --git a/src/lib/post-rendering.tsx b/src/lib/post-rendering.tsx new file mode 100644 index 0000000..abd561e --- /dev/null +++ b/src/lib/post-rendering.tsx @@ -0,0 +1,384 @@ +import { InfoBox } from "../components/info-box"; +import { CustomEmoji } from "../components/custom-emoji"; +import { + isAskViewBlock, + isMarkdownViewBlock, + MarkdownViewBlock, + summaryContent, + ViewBlock, +} from "../types/post-blocks"; +import { PostASTMap, WirePostViewModel } from "../types/wire-models"; +import { compile } from "html-to-text"; +import { DateTime } from "luxon"; +import React, { createElement, Fragment, JSX } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import rehypeExternalLinks from "rehype-external-links"; +import rehypeRaw from "rehype-raw"; +import rehypeReact from "rehype-react"; +import rehypeSanitize from "rehype-sanitize"; +import rehypeStringify from "rehype-stringify"; +import remarkBreaks from "remark-breaks"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { Plugin, Processor, unified } from "unified"; +import { Mention } from "../components/mention"; +import { parseEmoji } from "./emoji"; +import { chooseAgeRuleset } from "./sanitize"; +import { MAX_GFM_LINES, RenderingOptions } from "./shared-types"; +import { + cleanUpFootnotes, + compileHastAST, + convertMentions, + copyImgAltToTitle, + parseHastAST, +} from "./unified-processors"; +import _ from "lodash"; +import type { Root as HASTRoot } from "hast"; +import type { Root as MDASTRoot } from "mdast"; + +const convert = compile({ + wordwrap: false, +}); + +/** + * Used for posts only, supports arbitrary HTML + * @returns + */ +const markdownRenderStack = ( + postDate: Date, + lineLength: number, + options: Pick +) => { + let stack = unified().use(remarkParse); + + const ruleset = chooseAgeRuleset(postDate); + + if (ruleset.singleLineBreaks) { + stack = stack.use(remarkBreaks); + } + + if (lineLength < MAX_GFM_LINES) { + stack = stack.use(remarkGfm, { + singleTilde: false, + }); + } + + // make a copy so we don't accidentally modify it in-place + const effectiveSchema = { ...ruleset.schema }; + + if (options.renderingContext === "ask" && !ruleset.ask.allowEmbeddedMedia) { + effectiveSchema.tagNames = _.filter( + effectiveSchema.tagNames, + (tagName) => !["img", "picture", "audio", "video"].includes(tagName) + ); + } + + return stack + .use( + remarkRehype as Plugin< + [{ allowDangerousHtml: boolean }], + MDASTRoot, + HASTRoot + >, + { + allowDangerousHtml: true, + } + ) + .use(copyImgAltToTitle) + .use(() => cleanUpFootnotes) + .use(rehypeRaw) + .use(rehypeSanitize, effectiveSchema) + .use(() => ruleset.additionalVisitor); +}; + +const ERROR_BOX_NODE = ( + +

+ There was an issue rendering the HTML for this post! This usually means + you've messed up syntax on a style attribute. Please check + your syntax! +

+
+); +const ERROR_BOX_HTML = renderToStaticMarkup(ERROR_BOX_NODE); + +async function renderMarkdownAst( + blocks: MarkdownViewBlock[], + publishDate: Date, + options: Pick +): Promise { + const src = blocks.map((block) => block.markdown.content).join("\n\n"); + let lineLength = 0; + + // get the max line length among the blocks. while we group all blocks + // together for rendering, the performance regression associated with GFM + // tables only occurs with single line breaks, which can only exist within a + // single block. if the total number of line breaks ACROSS THE ENTIRE POST + // is >256, this isn't an issue. we're only impacted if it's in a single + // block. + for (const block of blocks) { + if (lineLength >= MAX_GFM_LINES) { + break; + } + + lineLength = Math.max( + lineLength, + block.markdown.content.split("\n", MAX_GFM_LINES).length + ); + } + + return markdownRenderStack(publishDate, lineLength, options) + .use(() => convertMentions) + .use(parseEmoji, { cohostPlus: options.hasCohostPlus }) + .use(compileHastAST) + .process(src) + .then((result) => result.value.toString()) + .catch((e) => { + // re-run the renderer with our static error box. we only get errors + // when a user has an invalid style tag that fails parsing. our error + // box is Known Good so this is not a concern for us. + return renderMarkdownAst( + [ + { + type: "markdown", + markdown: { + content: ERROR_BOX_HTML, + }, + }, + ], + publishDate, + options + ); + }); +} + +export function renderReactFromAst( + astString: string, + options: Omit +) { + let stack = unified().use(parseHastAST); + + const externalRel = ["nofollow"]; + if (options.externalLinksInNewTab) { + externalRel.push("noopener"); + } + + try { + return ( + stack + .use(rehypeExternalLinks, { + rel: externalRel, + target: options.externalLinksInNewTab ? "_blank" : "_self", + }) + // @ts-expect-error rehype-react types are broken + .use(rehypeReact, { + createElement, + Fragment, + components: { + Mention, + CustomEmoji, + }, + }) + .processSync(astString).result + ); + } catch (e) { + return ERROR_BOX_NODE; + } +} + +function renderMarkdown(src: string, publishDate: Date): string { + const lineLength = src.split("\n", MAX_GFM_LINES).length; + return markdownRenderStack(publishDate, lineLength, { + renderingContext: "post", + }) + .use(rehypeStringify) + .processSync(src) + .toString(); +} + +export function renderPostSummary( + viewModel: WirePostViewModel, + options: { myPost: boolean; rss?: boolean; skipHeadline?: boolean } +): string { + // invocations with either options.rss set true, or options.skipHeadline + // set true, have a second field they can use to carry the headline of the + // post and don't need to have it embedded herein. + const effectiveSkipHeadline = options.skipHeadline || options.rss; + + if (!options.myPost) { + if (viewModel.effectiveAdultContent && viewModel.cws.length > 0) { + const cwList = viewModel.cws.join(", "); + + return `18+ content; content warnings: ${cwList}`; + } else if (viewModel.cws.length > 0) { + const cwList = viewModel.cws.join(", "); + + return `(content warning: ${cwList})`; + } else if (viewModel.effectiveAdultContent) { + return "this post contains 18+ content"; + } + } + + if (viewModel.transparentShareOfPostId) { + // transparent share; find the opaque post above it + const originalPost = viewModel.shareTree.find( + (vm) => vm.postId === viewModel.transparentShareOfPostId + ); + + if (options.rss) { + // RSS: just include a link in the summary + if (originalPost) { + return `Share from @${originalPost.postingProject.handle}: ${originalPost.singlePostPageUrl}`; + } + } else { + // on-site: nest opaque parent's preview inside this one + if (originalPost) { + return `Share from @${ + originalPost.postingProject.handle + }: ${renderPostSummary(originalPost, options)}`; + } + } + } + + let summary: string = ""; + + if (viewModel.headline && !effectiveSkipHeadline) { + summary = viewModel.headline; + } else { + const effectiveDate = viewModel.publishedAt + ? DateTime.fromISO(viewModel.publishedAt).toJSDate() + : new Date(); + + const askBlocks = viewModel.blocks.filter(isAskViewBlock); + const markdownBlocks = viewModel.blocks.filter(isMarkdownViewBlock); + const textBlocks = [...askBlocks, ...markdownBlocks]; + const textContent = (textBlocks.length > 0 ? textBlocks : viewModel.blocks) + .map((block) => summaryContent(block)) + .join("\n\n"); + + const renderedBody = renderMarkdown(textContent, effectiveDate); + + summary = convert(renderedBody); + } + + if (options.rss && viewModel.shareOfPostId) { + // contentful share, include the link at the start of the summary + const originalPost = viewModel.shareTree.find( + (vm) => vm.postId === viewModel.shareOfPostId + ); + if (originalPost) { + summary = `Share from @${originalPost.postingProject.handle}: ${originalPost.singlePostPageUrl}\n\n${summary}`; + } + } + + return summary; +} + +export async function generatePostAst( + viewBlocks: ViewBlock[], + publishDate: Date, + options: Pick +): Promise { + // identify markdown spans + const spans: { + startIndex: number; + endIndex: number; + }[] = []; + let currentSpanStartIndex: number | null = null; + let readMoreIndex: number | null = null; + + for (let i = 0; i < viewBlocks.length; i++) { + const block = viewBlocks[i]; + const isMarkdownBlock = isMarkdownViewBlock(block); + + if (isMarkdownBlock) { + if ( + currentSpanStartIndex !== null && + block.markdown.content === "---" && + readMoreIndex === null + ) { + // inside a span, content is "---", no read-more yet: end the + // span, set read-more index, and start a new one + spans.push({ + startIndex: currentSpanStartIndex, + endIndex: i, + }); + currentSpanStartIndex = i; + readMoreIndex = i; + } else if (currentSpanStartIndex !== null) { + // inside a span, any other content: keep it going + continue; + } else { + // outside a span: start a new span + currentSpanStartIndex = i; + } + } else { + if (currentSpanStartIndex !== null) { + // inside a span: end the span + spans.push({ + startIndex: currentSpanStartIndex, + endIndex: i, + }); + currentSpanStartIndex = null; + } else { + // outside a span: do nothing + continue; + } + } + } + + // if we ended the post in a span, finish the one we were in + if (currentSpanStartIndex !== null) { + spans.push({ + startIndex: currentSpanStartIndex, + endIndex: viewBlocks.length, + }); + } + + // render each span and return AST map + return { + spans: await Promise.all( + spans.map(async (span) => ({ + startIndex: span.startIndex, + endIndex: span.endIndex, + ast: await renderMarkdownAst( + viewBlocks.slice( + span.startIndex, + span.endIndex + ) as MarkdownViewBlock[], + publishDate, + options + ), + })) + ), + readMoreIndex, + }; +} + +// interim rendering method for until we get the rest of the inline attachments +// changes done. render a sequence of markdown spans all joined together. +export function renderReactFromSpans( + spans: PostASTMap["spans"], + options: Omit +) { + const rendered: JSX.Element[] = []; + + for (let i = 0; i < spans.length; i++) { + // throw a warning if there are any missing blocks in the middle of the + // rendered spans. this should only happen if we're attempting to + // render the markdown in a post with attachments in the middle, which + // shouldn't exist yet. + if (i != 0 && spans[i].startIndex !== spans[i - 1].endIndex) { + console.error("renderReactFromSpans: span interval is sparse?"); + } + + rendered.push( + + {renderReactFromAst(spans[i].ast, options)} + + ); + } + + return <>{rendered}; +} diff --git a/src/lib/sanitize.tsx b/src/lib/sanitize.tsx new file mode 100644 index 0000000..6019f22 --- /dev/null +++ b/src/lib/sanitize.tsx @@ -0,0 +1,340 @@ +// this file is a .tsx so that tailwind will pick up on it + +import deepmerge from "deepmerge"; +import type { Root } from "hast"; +import { Schema } from "hast-util-sanitize"; +import { noop } from "lodash"; +import { defaultSchema } from "rehype-sanitize"; +import parseStyle from "style-to-object"; +import { visit } from "unist-util-visit"; + +type AgeRuleset = { + schema: Schema; + cutoffDate: Date | null; + className: string; + additionalVisitor: (hast: Root) => void; + singleLineBreaks: boolean; + forceAttachmentsToTop: boolean; + attachmentLayoutBehavior: "v1" | "v2"; + ask: { + allowEmbeddedMedia: boolean; + }; +}; + +const FIRST_AGE: AgeRuleset = { + schema: deepmerge(defaultSchema, { + attributes: { + "*": ["style"], + }, + tagNames: ["video", "audio", "aside"], // consistency with current rules, + }), + // Wednesday, June 29, 2022 6:00:00 PM GMT + cutoffDate: new Date(1656525600000), + className: "", + additionalVisitor: noop, + singleLineBreaks: false, + forceAttachmentsToTop: true, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const SECOND_AGE: AgeRuleset = { + schema: deepmerge(defaultSchema, { + attributes: { + "*": ["style"], + }, + tagNames: ["video", "audio", "aside"], // consistency with current rules, + }), + // Monday, November 14, 2022 6:00:00 AM GMT + cutoffDate: new Date(1668405600000), + className: "isolate", + additionalVisitor(hast) { + visit(hast, "element", (node, index, parent) => { + if (parent === null || index === null) return; + + if (node.properties?.style && typeof node.properties.style === "string") { + try { + let changed = false; + const parsed = parseStyle(node.properties.style); + if ( + parsed && + parsed["position"] && + [ + // every valid value of `position` _except_ `fixed` + // (https://developer.mozilla.org/en-US/docs/Web/CSS/position), + // which we disallow + "static", + "relative", + "absolute", + "sticky", + "inherit", + "initial", + "revert", + "revert-layer", + "unset", + ].indexOf(parsed["position"].toLowerCase()) === -1 + ) { + parsed.position = "static"; + changed = true; + } + + if (parsed && changed) { + node.properties.style = Object.entries(parsed) + .map(([k, v]) => `${k}:${v}`) + .join(";"); + } + } catch (e) { + // couldn't parse, don't worry about it + return; + } + } + }); + }, + singleLineBreaks: false, + forceAttachmentsToTop: true, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const THIRD_AGE: AgeRuleset = { + schema: deepmerge(defaultSchema, { + attributes: { + "*": ["style"], + }, + tagNames: ["video", "audio", "aside"], // consistency with current rules, + }), + // may 10 2023, 3pm EDT + cutoffDate: new Date("2023-05-10T15:00:00-04:00"), + className: "isolate co-contain-paint", + additionalVisitor(hast) { + // run the previous age's visitor first + SECOND_AGE.additionalVisitor(hast); + + visit(hast, "element", (node, index, parent) => { + if (parent === null || index === null) return; + + if (node.properties?.style && typeof node.properties.style === "string") { + try { + let changed = false; + const parsed = parseStyle(node.properties.style); + + if (parsed) { + for (const key in parsed) { + // drop all CSS variables + if (key.startsWith("--")) { + delete parsed[key]; + changed = true; + } + } + } + + if (parsed && changed) { + node.properties.style = Object.entries(parsed) + .map(([k, v]) => `${k}:${v}`) + .join(";"); + } + } catch (e) { + // couldn't parse, don't worry about it + return; + } + } + }); + }, + singleLineBreaks: false, + forceAttachmentsToTop: true, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const FOURTH_AGE: AgeRuleset = { + // no schema changes from third age + schema: THIRD_AGE.schema, + // july 17 2023, noon EDT + cutoffDate: new Date("2023-07-17T12:00:00-04:00"), + className: THIRD_AGE.className, + additionalVisitor: THIRD_AGE.additionalVisitor, + singleLineBreaks: true, + forceAttachmentsToTop: true, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +// list pulled from dompurify +const MATH_ML_SCHEMA: Schema = { + // allow mathml + tagNames: [ + "math", + "menclose", + "merror", + "mfenced", + "mfrac", + "mglyph", + "mi", + "mlabeledtr", + "mmultiscripts", + "mn", + "mo", + "mover", + "mpadded", + "mphantom", + "mroot", + "mrow", + "ms", + "mspace", + "msqrt", + "mstyle", + "msub", + "msup", + "msubsup", + "mtable", + "mtd", + "mtext", + "mtr", + "munder", + "munderover", + "mprescripts", + ], + attributes: { + "*": [ + "accent", + "accentunder", + "align", + "bevelled", + "close", + "columnsalign", + "columnlines", + "columnspan", + "denomalign", + "depth", + "dir", + "display", + "displaystyle", + "encoding", + "fence", + "frame", + "height", + "href", + "id", + "largeop", + "length", + "linethickness", + "lspace", + "lquote", + "mathbackground", + "mathcolor", + "mathsize", + "mathvariant", + "maxsize", + "minsize", + "movablelimits", + "notation", + "numalign", + "open", + "rowalign", + "rowlines", + "rowspacing", + "rowspan", + "rspace", + "rquote", + "scriptlevel", + "scriptminsize", + "scriptsizemultiplier", + "selection", + "separator", + "separators", + "stretchy", + "subscriptshift", + "supscriptshift", + "symmetric", + "voffset", + "width", + "xmlns", + ], + span: [["className", "math-inline"]], + div: [["className", "math-display"]], + }, +}; + +const FIFTH_AGE: AgeRuleset = { + // we allow mathml now + schema: deepmerge(FOURTH_AGE.schema, MATH_ML_SCHEMA), + cutoffDate: new Date("2024-02-12T12:00:00-08:00"), // 2024/02/12 12:00 PST + className: FOURTH_AGE.className, + additionalVisitor: FOURTH_AGE.additionalVisitor, + singleLineBreaks: true, + forceAttachmentsToTop: true, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const SIXTH_AGE: AgeRuleset = { + schema: FIFTH_AGE.schema, + cutoffDate: new Date("2024-03-27T12:00:00-08:00"), // 2024/03/27 12:00 PDT + className: FIFTH_AGE.className, + additionalVisitor: FIFTH_AGE.additionalVisitor, + singleLineBreaks: true, + // attachments now render in post order instead of being pushed to the + // top of the post + forceAttachmentsToTop: false, + attachmentLayoutBehavior: "v1", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const SEVENTH_AGE: AgeRuleset = { + schema: SIXTH_AGE.schema, + cutoffDate: new Date("2024-03-29T12:00:00-08:00"), // 2024/03/29 12:00 PDT + className: SIXTH_AGE.className, + additionalVisitor: SIXTH_AGE.additionalVisitor, + singleLineBreaks: true, + forceAttachmentsToTop: false, + attachmentLayoutBehavior: "v2", + ask: { + allowEmbeddedMedia: true, + }, +}; + +const EIGHTH_AGE: AgeRuleset = { + schema: SEVENTH_AGE.schema, + cutoffDate: null, // current age + className: SEVENTH_AGE.className, + additionalVisitor: SEVENTH_AGE.additionalVisitor, + singleLineBreaks: true, + forceAttachmentsToTop: false, + attachmentLayoutBehavior: "v2", + // asks no longer allow images, video, or audio to be embedded. + ask: { + allowEmbeddedMedia: false, + }, +}; + +export const AGE_LIST = [ + FIRST_AGE, + SECOND_AGE, + THIRD_AGE, + FOURTH_AGE, + FIFTH_AGE, + SIXTH_AGE, + SEVENTH_AGE, + EIGHTH_AGE, +] as const; + +export const chooseAgeRuleset = (postDate: Date) => + AGE_LIST.find((ruleset) => { + if (ruleset.cutoffDate) { + return postDate < ruleset.cutoffDate; + } + + return true; + }) ?? AGE_LIST[AGE_LIST.length - 1]; diff --git a/src/lib/shared-types.ts b/src/lib/shared-types.ts new file mode 100644 index 0000000..f9acb2b --- /dev/null +++ b/src/lib/shared-types.ts @@ -0,0 +1,27 @@ +export type RenderingContext = + | "activitypub" + | "ask" + | "comment" + | "email" + | "post" + | "profile" + | "rss" + | "artistAlley" + | null; + +export type RenderingOptions = { + /** the context in which this content is being rendered */ + renderingContext: RenderingContext; + /** + * whether or not the posting user has cohost plus (currently determines + * emoji rendering behavior) + */ + hasCohostPlus: boolean; + externalLinksInNewTab: boolean; +}; + +/** + * line length limit for enabling GFM tables. workaround for a performance issue + * with long profile descriptions. + */ +export const MAX_GFM_LINES = 256; diff --git a/src/lib/unified-processors.ts b/src/lib/unified-processors.ts new file mode 100644 index 0000000..56af594 --- /dev/null +++ b/src/lib/unified-processors.ts @@ -0,0 +1,178 @@ +import type { Element, Root, Text } from "hast"; +import _ from "lodash"; +import { Plugin } from "unified"; +import type { Compiler, Parser } from "unified"; +import { is } from "unist-util-is"; +import { CONTINUE, SKIP, visit } from "unist-util-visit"; +import { EXIT, visitParents } from "unist-util-visit-parents"; +import { extractMentions } from "./mention-parsing"; + +export const processMatches = + ( + regex: RegExp, + callback: ( + matches: string[], + splits: string[], + node: Text, + index: number, + parent: Element | Root + ) => void + ) => + (hast: Root) => { + // we only want to check on text nodes for this + visit(hast, "text", (node, index, parent) => { + // there is no such thing as a text node without a parent. + // but if there is we want nothing to do with it. + if (parent == null || index == null) return; + + const matches = node.value.match(regex); + + // if this text has mentions, process them + if (matches) { + const splits = node.value.split(regex); + if (splits.length - 1 !== matches.length) { + // something isn't how it should be. bail. + return; + } + + return callback(matches, splits, node, index, parent); + } + }); + }; + +export const convertMentions = (hast: Root) => { + // we only want to check on text nodes for this + visit(hast, "text", (node, index, parent) => { + // there is no such thing as a text node without a parent. + // but if there is we want nothing to do with it. + if (parent == null || index == null) return; + + const text = node.value; + const names = extractMentions(text); + + // if this text has mentions, consider processing them + if (names.length) { + // if we have an `a` in our parent tree, we don't want to process + // the mention so that links still work. + let hasAnchorParent = false; + visitParents(parent, { type: "text" }, (newNode, ancestors) => { + // since we have to traverse for all text nodes on the parent, + // we will pretty often get text nodes that _aren't_ the one + // we're trying to operate on. check to make sure they match. + if (!_.isEqual(node, newNode)) return CONTINUE; + + // flag if we've got an anchro parent + hasAnchorParent = !!ancestors.find((el) => + is(el, { type: "element", tagName: "a" }) + ); + + // if we do, we don't need to check everything else so bail out. + if (hasAnchorParent) return EXIT; + }); + + if (hasAnchorParent) return CONTINUE; + + const els: Array = []; + let currentStart = 0; + + names.forEach((token, idx, names) => { + const [startPosition, endPosition] = token.indices; + els.push({ + type: "text", + value: text.slice(currentStart, startPosition), + }); + els.push({ + type: "element", + tagName: "Mention", + properties: { + handle: token.handle, + }, + children: [ + { + type: "text", + value: `@${token.handle}`, + }, + ], + }); + currentStart = endPosition; + + if (idx === names.length - 1) { + // if we're last we need to grab the rest of the string + els.push({ + type: "text", + value: text.slice(currentStart), + }); + } + }); + + parent.children.splice(index, 1, ...els); + // skip over all the new elements we just created + return [SKIP, index + els.length]; + } + }); +}; + +export const cleanUpFootnotes = (hast: Root) => { + visit(hast, "element", (node, index, parent) => { + if (parent == null || index == null) return; + // remove the link from the superscript number + if ( + node.tagName === "a" && + (node.properties?.id as string)?.includes("fnref") + ) { + parent.children.splice(index, 1, ...node.children); + return [SKIP, index]; + } + + // remove the little arrow at the bottom + if ( + node.tagName === "a" && + (node.properties?.href as string)?.includes("fnref") + ) { + parent.children.splice(index, 1); + return [SKIP, index]; + } + + // replace the invisible label with a hr + if ( + node.tagName === "h2" && + (node.properties?.id as string)?.includes("footnote-label") + ) { + const hrEl: Element = { + tagName: "hr", + type: "element", + children: [], + properties: { + "aria-label": "Footnotes", + style: "margin-bottom: -0.5rem;", + }, + }; + parent.children.splice(index, 1, hrEl); + } + }); +}; + +export const copyImgAltToTitle: Plugin<[], Root> = () => (hast: Root) => { + visit(hast, { type: "element", tagName: "img" }, (node) => { + if (node.properties?.alt) { + node.properties.title = node.properties.alt; + } + }); +}; + +export const compileHastAST: Plugin = function () { + const compiler: Compiler = (node: Root) => { + return JSON.stringify(node); + }; + + Object.assign(this, { Compiler: compiler }); +}; + +export const parseHastAST: Plugin = function () { + const parser: Parser = (astString: string) => { + const ast = JSON.parse(astString) as Root; + return ast; + }; + + Object.assign(this, { Parser: parser }); +}; diff --git a/src/types/access-result.ts b/src/types/access-result.ts new file mode 100644 index 0000000..0c548b4 --- /dev/null +++ b/src/types/access-result.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export enum AccessResult { + Allowed = "allowed", + NotAllowed = "not-allowed", + LogInFirst = "log-in-first", + Blocked = "blocked", +} + +export const AccessResultEnum = z.enum(AccessResult); diff --git a/src/types/asks.ts b/src/types/asks.ts new file mode 100644 index 0000000..c86dc59 --- /dev/null +++ b/src/types/asks.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { AskId, ISODateString, ProjectHandle, ProjectId } from "./ids"; +import { AvatarShape, ProjectFlag, ProjectPrivacyEnum } from "./projects"; + +export const AskState = z.enum(["pending", "responded", "deleted"]); +export type AskState = z.infer; + +const FilteredProject = z.object({ + projectId: ProjectId, + handle: ProjectHandle, + avatarURL: z.string().url(), + avatarPreviewURL: z.string().url(), + privacy: ProjectPrivacyEnum, + flags: ProjectFlag.array(), + avatarShape: AvatarShape, + displayName: z.string(), +}); + +export const WireAskModel = z.discriminatedUnion("anon", [ + z.object({ + anon: z.literal(true), + loggedIn: z.boolean(), + askingProject: z.undefined(), + askId: AskId, + content: z.string(), + sentAt: ISODateString, + }), + z.object({ + anon: z.literal(false), + loggedIn: z.literal(true), + askingProject: FilteredProject, + askId: AskId, + content: z.string(), + sentAt: ISODateString, + }), +]); +export type WireAskModel = z.infer; diff --git a/src/types/attachments.ts b/src/types/attachments.ts new file mode 100644 index 0000000..3e9e2c4 --- /dev/null +++ b/src/types/attachments.ts @@ -0,0 +1,30 @@ +import z from "zod"; + +export enum AttachmentState { + Pending = 0, + Finished, +} + +export const AttachmentKind = z.enum(["audio", "image"]); +export type AttachmentKind = z.infer; + +export const WireAudioAttachmentMetadata = z.object({ + title: z.string().optional(), + artist: z.string().optional(), +}); +export type WireAudioAttachmentMetadata = z.infer< + typeof WireAudioAttachmentMetadata +>; + +export const WireImageAttachmentMetadata = z.object({}); +export type WireImageAttachmentMetadata = z.infer< + typeof WireImageAttachmentMetadata +>; + +export const WireAnyAttachmentMetadata = z.union([ + WireAudioAttachmentMetadata, + WireImageAttachmentMetadata, +]); +export type WireAnyAttachmentMetadata = z.infer< + typeof WireAnyAttachmentMetadata +>; diff --git a/src/types/ids.ts b/src/types/ids.ts new file mode 100644 index 0000000..f6ff484 --- /dev/null +++ b/src/types/ids.ts @@ -0,0 +1,44 @@ +import { EXTANT_PAGE_LEGAL_REGEX } from "./username-verifier"; +import { DateTime } from "luxon"; +import z from "zod"; +import { Tagged, refinement } from "./tagged"; + +const BigIntId = z + .string() + .or(z.number().transform((val) => val.toString())) + .refine((val) => { + try { + // BigInt throws on non-integer strings + const num = BigInt(val); + return num > BigInt(0); + } catch (e) { + return false; + } + }); + +export const AttachmentId = z.uuid().brand<"AttachmentId">(); +export type AttachmentId = z.infer; + +export const PostId = z.int().brand<"PostId">(); +export type PostId = z.infer; + +export const ProjectId = z.int().brand<"ProjectId">(); +export type ProjectId = z.infer; + +export const UserId = z.int().brand<"UserId">(); +export type UserId = z.infer; + +export const ProjectHandle = z + .string() + // use the old legal regex to prevent tRPC errors with DNS-illegal usernames + .regex(EXTANT_PAGE_LEGAL_REGEX) + .brand<"ProjectHandle">(); +export type ProjectHandle = z.infer; + +export const CommentId = z.uuid().brand<"CommentId">(); +export type CommentId = z.infer; + +export const ISODateString = z.iso.datetime({ offset: true }); + +export const AskId = BigIntId.brand<"AskId">(); +export type AskId = z.infer; diff --git a/src/types/limits.ts b/src/types/limits.ts new file mode 100644 index 0000000..ab4d759 --- /dev/null +++ b/src/types/limits.ts @@ -0,0 +1,5 @@ +// length limit for usernames and project handles +export const USERNAME_HANDLE_LIMIT = 200; + +// length limit for asks +export const ASK_LENGTH_LIMIT = 1120; diff --git a/src/types/post-blocks.ts b/src/types/post-blocks.ts new file mode 100644 index 0000000..bce6199 --- /dev/null +++ b/src/types/post-blocks.ts @@ -0,0 +1,244 @@ +/** + * Post Blocks + * + * Specifies all the types for the block-based Post system. + * @module + */ + +import { z } from "zod"; +import { WireAskModel } from "./asks"; +import { WireImageAttachmentMetadata } from "./attachments"; +import { AttachmentId } from "./ids"; + +/** + * @internal + */ +const BaseBlock = z.object({ + type: z.string(), +}); +interface BaseBlock { + type: string; +} + +/** + * @category Storage Blocks + * + * Blocks as they are stored in the database. + * Only contains minimal information needed to reproduce content, used as the base to generate [[View Blocks]]. + */ + +/** + * @category Storage Blocks + */ +export const MarkdownStorageBlock = BaseBlock.extend({ + type: z.literal("markdown"), + markdown: z.object({ + /** Raw markdown to be parsed at render-time. */ + content: z.string(), + }), +}); +export type MarkdownStorageBlock = z.infer; + +/** + * @category Storage Blocks + */ +export const AttachmentStorageBlock = BaseBlock.extend({ + type: z.literal("attachment"), + attachment: z.object({ + /** ID for the [[`Attachment`]] to be rendered. */ + attachmentId: AttachmentId, + altText: z.string().optional(), + }), +}); +export type AttachmentStorageBlock = z.infer; + +export const AttachmentRowStorageBlock = BaseBlock.extend({ + type: z.literal("attachment-row"), + attachments: z.array(AttachmentStorageBlock), +}); +export type AttachmentRowStorageBlock = z.infer< + typeof AttachmentRowStorageBlock +>; + +/** + * Union type used on the [[`Post`]] model + * + * @category Storage Blocks + */ +export const StorageBlock = z.union([ + MarkdownStorageBlock, + AttachmentStorageBlock, + AttachmentRowStorageBlock, +]); +export type StorageBlock = z.infer; + +/** + * @category View Blocks + * View Blocks _must_ contain all data needed to render the block. + * This is a wire-safe type and as a result _must_ not contain anything the client can't see. + * + */ + +/** + * No changes are currently required from the [[`MarkdownStorageBlock`]] + * so this is currently a simple alias. + * + * @category View Blocks + * */ +export const MarkdownViewBlock = MarkdownStorageBlock.extend({}); +export type MarkdownViewBlock = z.infer; + +/** + * Adds the image URL for rendering + * + * @category View Blocks + */ +const ImageAttachmentViewModel = AttachmentStorageBlock.shape.attachment + .extend({ + previewURL: z.string(), + fileURL: z.string(), + kind: z.literal("image"), + width: z.number().nullish(), + height: z.number().nullish(), + }) + .extend(WireImageAttachmentMetadata.shape); + +const AudioAttachmentViewModel = AttachmentStorageBlock.shape.attachment.extend( + { + previewURL: z.string(), + fileURL: z.string(), + kind: z.literal("audio"), + artist: z.string().optional(), + title: z.string().optional(), + } +); + +const AttachmentViewModel = z.discriminatedUnion("kind", [ + ImageAttachmentViewModel, + AudioAttachmentViewModel, +]); + +export const AttachmentViewBlock = AttachmentStorageBlock.extend({ + attachment: AttachmentViewModel, +}); + +export type AttachmentViewBlock = z.infer; + +export const AttachmentRowViewBlock = AttachmentRowStorageBlock.extend({ + attachments: z.array(AttachmentViewBlock), +}); +export type AttachmentRowViewBlock = z.infer; + +/** + * NOTE: AskViewBlock DOES NOT have a corresponding storage block. It is + * generated by the server. + */ +export const AskViewBlock = BaseBlock.extend({ + type: z.literal("ask"), + ask: WireAskModel, +}); +export type AskViewBlock = z.infer; + +/** + * Union type used for [[`PostViewModel`]] and component renderers. + * @category View Blocks + */ +export const ViewBlock = z.union([ + MarkdownViewBlock, + AttachmentViewBlock, + AskViewBlock, + AttachmentRowViewBlock, +]); +export type ViewBlock = z.infer; + +export function isAttachmentViewBlock( + test: unknown +): test is AttachmentViewBlock { + return AttachmentViewBlock.safeParse(test).success; +} + +export function isMarkdownViewBlock(test: unknown): test is MarkdownViewBlock { + return MarkdownViewBlock.safeParse(test).success; +} + +export function isAskViewBlock(test: unknown): test is AskViewBlock { + return AskViewBlock.safeParse(test).success; +} + +export function isAttachmentRowViewBlock( + test: unknown +): test is AttachmentRowViewBlock { + return AttachmentRowViewBlock.safeParse(test).success; +} + +export function isAttachmentStorageBlock( + test: unknown +): test is AttachmentStorageBlock { + return AttachmentStorageBlock.safeParse(test).success; +} + +export function isAttachmentRowStorageBlock( + test: unknown +): test is AttachmentRowStorageBlock { + return AttachmentRowStorageBlock.safeParse(test).success; +} + +export function isMarkdownStorageBlock( + test: unknown +): test is MarkdownStorageBlock { + return MarkdownStorageBlock.safeParse(test).success; +} + +// via https://github.com/colinhacks/zod/issues/627#issuecomment-911679836 +// could be worth investigating a factory function? +export function parseAttachmentViewBlocks(originalBlocks: unknown[]) { + return z + .preprocess( + (blocks) => z.array(z.any()).parse(blocks).filter(isAttachmentViewBlock), + z.array(AttachmentViewBlock) + ) + .parse(originalBlocks); +} + +export function summaryContent(block: ViewBlock): string { + switch (block.type) { + case "markdown": + return block.markdown.content; + case "attachment": { + const encodedFilename = block.attachment.fileURL.split("/").pop(); + + return encodedFilename + ? `[${block.attachment.kind}: ${decodeURIComponent(encodedFilename)}]` + : `[${block.attachment.kind}]`; + } + case "attachment-row": + return block.attachments + .map((attachment) => summaryContent(attachment)) + .join("\n"); + case "ask": { + const askerString = block.ask.anon + ? block.ask.loggedIn + ? "Anonymous User asked:" + : "Anonymous Guest asked:" + : `@${block.ask.askingProject.handle} asked:`; + return `${askerString} +> ${block.ask.content.split("\n").join("\n> ")}`; + } + } +} + +export function getAttachmentViewBlocks( + blocks: ViewBlock[] +): AttachmentViewBlock[] { + return blocks.reduce((blocks, block) => { + if (isAttachmentViewBlock(block)) { + return [...blocks, block]; + } + + if (isAttachmentRowViewBlock(block)) { + return [...blocks, ...block.attachments]; + } + + return blocks; + }, []); +} diff --git a/src/types/posts.ts b/src/types/posts.ts new file mode 100644 index 0000000..93db587 --- /dev/null +++ b/src/types/posts.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export enum PostState { + Unpublished = 0, + Published, + Deleted, +} + +export const PostStateEnum = z.enum(PostState); +export type PostStateEnum = z.infer; diff --git a/src/types/projects.ts b/src/types/projects.ts new file mode 100644 index 0000000..407ff7f --- /dev/null +++ b/src/types/projects.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; +import { ISODateString, ProjectHandle, ProjectId } from "./ids"; + +export enum ProjectPrivacy { + Public = "public", + Private = "private", +} +export const ProjectPrivacyEnum = z.enum(ProjectPrivacy); +export type ProjectPrivacyEnum = z.infer; + +// shout out https://stackoverflow.com/a/61129291 +export const ProjectFlag = z.enum([ + "staff", + "staffMember", + "friendOfTheSite", + "noTransparentAvatar", + "suspended", + "automated", // used for the bot badge + "parody", // used for the "un-verified" badge +]); +export type ProjectFlag = z.infer; + +// TODO: the DB also supports "all-ages", but this setting doesn't currently +// work because of an NYI access checker upgrade. +export const LoggedOutPostVisibility = z.enum(["public", "none"]); +export type LoggedOutPostVisibility = z.infer; + +export function isProjectFlag( + maybeProjectFlag: unknown +): maybeProjectFlag is ProjectFlag { + return ProjectFlag.safeParse(maybeProjectFlag).success; +} + +export const AvatarShape = z.enum([ + "circle", + "roundrect", + "squircle", + "capsule-big", + "capsule-small", + "egg", +]); +export type AvatarShape = z.infer; + +export const WireProjectModel = z.object({ + projectId: ProjectId, + handle: ProjectHandle, + displayName: z.string(), + dek: z.string(), + description: z.string(), + avatarURL: z.string().url(), + avatarPreviewURL: z.string().url(), + headerURL: z.string().url().nullable(), + headerPreviewURL: z.string().url().nullable(), + privacy: ProjectPrivacyEnum, + url: z.string().nullable(), + pronouns: z.string().nullable(), + flags: ProjectFlag.array(), + avatarShape: AvatarShape, + loggedOutPostVisibility: LoggedOutPostVisibility, + frequentlyUsedTags: z.string().array(), + askSettings: z.object({ + enabled: z.boolean(), + allowAnon: z.boolean(), + requireLoggedInAnon: z.boolean(), + }), + deleteAfter: ISODateString.nullable(), + isSelfProject: z.boolean().nullable(), +}); +export type WireProjectModel = z.infer; diff --git a/src/types/tagged.ts b/src/types/tagged.ts new file mode 100644 index 0000000..711eed5 --- /dev/null +++ b/src/types/tagged.ts @@ -0,0 +1,7 @@ +// based on https://github.com/colinhacks/zod/issues/678#issuecomment-962387521 +export type Tagged = T & { __tag: Tag }; +export function refinement, T>() { + return function (val: T): val is Type { + return true; + }; +} diff --git a/src/types/username-verifier.ts b/src/types/username-verifier.ts new file mode 100644 index 0000000..670c924 --- /dev/null +++ b/src/types/username-verifier.ts @@ -0,0 +1,62 @@ +import { USERNAME_HANDLE_LIMIT } from "./limits"; + +export type UsernameLegalResult = + | { + legal: true; + } + | { + legal: false; + reason: string; + }; + +export const LEGAL_REGEX_STRING = "^[a-zA-Z0-9][a-zA-Z0-9-]{2,}$"; +export const LEGAL_REGEX = new RegExp(LEGAL_REGEX_STRING); +// separate regular expression for existing pages; fixes tRPC bugs with pages that are invalid DNS names but already exist +export const EXTANT_PAGE_LEGAL_REGEX = /^[a-zA-Z0-9-]{3,}/; +const BANNED_USERNAMES = new Set([ + "rc", + "api", + "www", + "help", + "admin", + "support", + "staff", + "internal", + "status", + "mail", + "mobile", + "search", + "static", +]); + +export function isHandleLegal(username: string): UsernameLegalResult { + const downcasedUsername = username.toLowerCase(); + + // no namespace usernames + if (BANNED_USERNAMES.has(downcasedUsername)) { + return { + legal: false, + reason: `Username can not be "${username}"`, + }; + } + + // legal characters only + if (!LEGAL_REGEX.test(username)) { + return { + legal: false, + + reason: + "Usernames can only contain letters, numbers, and hyphens, and must be at least 3 characters.", + }; + } + + if (downcasedUsername.length > USERNAME_HANDLE_LIMIT) { + return { + legal: false, + reason: + "Your username cannot be longer than 200 characters, but nice try.", + }; + } + + return { legal: true }; +} diff --git a/src/types/wire-models.ts b/src/types/wire-models.ts new file mode 100644 index 0000000..857c4a2 --- /dev/null +++ b/src/types/wire-models.ts @@ -0,0 +1,183 @@ +import { string, z } from "zod"; +import { AccessResult, AccessResultEnum } from "./access-result"; +import { + AskId, + AttachmentId, + CommentId, + ISODateString, + PostId, + ProjectId, + UserId, +} from "./ids"; +import { StorageBlock, ViewBlock } from "./post-blocks"; +import { PostStateEnum } from "./posts"; +import { ProjectFlag, WireProjectModel } from "./projects"; + +// Type data needed for posts on the client +export const WirePostContentCommon = z.object({ + postId: PostId, + headline: z.string(), + publishedAt: z.string().optional(), + filename: z.string(), + transparentShareOfPostId: PostId.nullable(), + shareOfPostId: PostId.nullable(), + state: PostStateEnum, + numComments: z.number(), + cws: z.string().array(), + tags: z.string().array(), + hasCohostPlus: z.boolean(), + pinned: z.boolean(), + commentsLocked: z.boolean(), + sharesLocked: z.boolean(), +}); +export type WirePostContentCommon = z.infer; + +export const WirePostModel = WirePostContentCommon.extend({ + adultContent: z.boolean(), + shareOfPostId: PostId.nullable(), + updatedAt: z.string(), + blocks: StorageBlock.array(), + attachments: z + .object({ attachmentId: AttachmentId, filename: z.string() }) + .array(), +}); +export type WirePostModel = z.infer; + +// double declaration required due to a typescript limitation with recursive types +// see: https://github.com/colinhacks/zod#recursive-types +type WireCommentViewModelInternal = { + comment: { + commentId: CommentId; + postedAtISO: string; + deleted: boolean; + body: string; + children: WireCommentViewModelInternal[]; + postId: PostId; + inReplyTo: CommentId | null; + hasCohostPlus: boolean; + hidden: boolean; + }; + canInteract: AccessResult; + canEdit: AccessResult; + canHide: AccessResult; + poster?: WireProjectModel; +}; + +export const WireCommentViewModel: z.ZodType = + z.lazy(() => + z.object({ + comment: z.object({ + commentId: CommentId, + postedAtISO: ISODateString, + deleted: z.boolean(), + body: z.string(), + children: WireCommentViewModel.array(), + postId: PostId, + inReplyTo: CommentId.nullable(), + hasCohostPlus: z.boolean(), + hidden: z.boolean(), + }), + canInteract: AccessResultEnum, + canEdit: AccessResultEnum, + canHide: AccessResultEnum, + poster: WireProjectModel.optional(), + }) + ); + +export type WireCommentViewModel = z.infer; + +export const WireRenderedPostContent = z.object({ + initial: z.string(), + expanded: z.string().optional(), +}); + +export type WireRenderedPostContent = z.infer; + +export const LimitedVisibilityReason = z.enum([ + "none", + "log-in-first", + "deleted", + "unpublished", + "adult-content", + "blocked", +]); +export type LimitedVisibilityReason = z.infer; + +// rationale for building the post AST this way: originally, the entire post +// shared one AST; this doesn't suffice when attachments can move around. the +// natural way to break the AST down then is per-block, but an inline HTML tag +// can span multiple blocks, so we need to build spans of contiguous markdown +// blocks as a unit or else HTML's tag auto-insertion rules kick in. +export const PostASTMap = z.object({ + spans: z.array( + z.object({ + startIndex: z.number(), + endIndex: z.number(), + ast: z.string(), + }) + ), + readMoreIndex: z.number().nullable(), +}); +export type PostASTMap = z.infer; + +// double declaration required due to a typescript limitation with recursive types +// see: https://github.com/colinhacks/zod#recursive-types +type WirePostViewModelInternal = WirePostContentCommon & { + blocks: ViewBlock[]; + + plainTextBody: string; + + postingProject: WireProjectModel; + shareTree: WirePostViewModelInternal[]; + numSharedComments: number; + relatedProjects: WireProjectModel[]; + singlePostPageUrl: string; + effectiveAdultContent: boolean; + isEditor: boolean; + hasAnyContributorMuted: boolean; + contributorBlockIncomingOrOutgoing: boolean; + postEditUrl: string; + isLiked: boolean; + canShare: boolean; + canPublish: boolean; + limitedVisibilityReason: LimitedVisibilityReason; + astMap: PostASTMap; + + responseToAskId: AskId | null; +}; + +export const WirePostViewModel: z.ZodType = z.lazy( + () => + WirePostContentCommon.extend({ + blocks: ViewBlock.array(), + plainTextBody: z.string(), + postingProject: WireProjectModel, + shareTree: WirePostViewModel.array(), + numSharedComments: z.number(), + relatedProjects: WireProjectModel.array(), + singlePostPageUrl: z.string().url(), + effectiveAdultContent: z.boolean(), + isEditor: z.boolean(), + hasAnyContributorMuted: z.boolean(), + contributorBlockIncomingOrOutgoing: z.boolean(), + postEditUrl: z.string().url(), + isLiked: z.boolean(), + canShare: z.boolean(), + canPublish: z.boolean(), + limitedVisibilityReason: LimitedVisibilityReason, + astMap: PostASTMap, + responseToAskId: AskId.nullable(), + }) +); +export type WirePostViewModel = z.infer; + +export const WireUserModel = z.object({ + userId: UserId, + email: z.string(), + emailVerified: z.boolean(), + collapseAdultContent: z.boolean(), + isAdult: z.boolean(), + twoFactorEnabled: z.boolean(), +}); + +export type WireUserModel = z.infer; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b8798c3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "esModuleInterop": true, + "target": "esnext", + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["./src/**/*"] +}