tfrere HF Staff commited on
Commit
3171ebe
·
1 Parent(s): b3d2294

feat: add publish pipeline with HTML/PDF/thumbnail generation

Browse files

Publisher pipeline (POST /api/publish):
- Server-side TipTap extensions mirroring frontend (no React)
- generateHTML() produces article HTML with SEO meta tags
- Playwright-based PDF and OG thumbnail generation (optional)
- Upload published artifacts to HF dataset (or local fallback)
- HTML template with light/dark theme toggle, image lightbox

Routing updates:
- Authenticated editors get SPA, visitors get published HTML
- Image proxy for private HF datasets (/api/images/*)
- Serve local published articles from /published/

Also: add type declarations for citation-js and playwright
Made-with: Cursor

backend/package-lock.json CHANGED
@@ -18,10 +18,23 @@
18
  "@hocuspocus/server": "^3.4.4",
19
  "@huggingface/hub": "^2.11.0",
20
  "@openrouter/ai-sdk-provider": "^2.5.1",
 
 
 
 
 
 
 
 
 
 
 
 
21
  "ai": "^6.0.158",
22
  "dotenv": "^17.4.1",
23
  "express": "^4.21.0",
24
  "express-ws": "^5.0.2",
 
25
  "multer": "^2.1.1",
26
  "yjs": "^13.6.0",
27
  "zod": "^4.3.6"
@@ -722,12 +735,510 @@
722
  "node": ">=8.0.0"
723
  }
724
  },
 
 
 
 
 
 
725
  "node_modules/@standard-schema/spec": {
726
  "version": "1.1.0",
727
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
728
  "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
729
  "license": "MIT"
730
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  "node_modules/@types/body-parser": {
732
  "version": "1.19.6",
733
  "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -786,6 +1297,15 @@
786
  "@types/ws": "*"
787
  }
788
  },
 
 
 
 
 
 
 
 
 
789
  "node_modules/@types/http-errors": {
790
  "version": "2.0.5",
791
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
@@ -793,6 +1313,28 @@
793
  "dev": true,
794
  "license": "MIT"
795
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  "node_modules/@types/multer": {
797
  "version": "2.1.0",
798
  "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
@@ -807,7 +1349,6 @@
807
  "version": "22.19.17",
808
  "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
809
  "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
810
- "dev": true,
811
  "license": "MIT",
812
  "dependencies": {
813
  "undici-types": "~6.21.0"
@@ -848,11 +1389,22 @@
848
  "@types/node": "*"
849
  }
850
  },
 
 
 
 
 
 
 
 
 
 
 
 
851
  "node_modules/@types/ws": {
852
  "version": "8.18.1",
853
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
854
  "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
855
- "dev": true,
856
  "license": "MIT",
857
  "dependencies": {
858
  "@types/node": "*"
@@ -915,6 +1467,12 @@
915
  "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
916
  "license": "MIT"
917
  },
 
 
 
 
 
 
918
  "node_modules/array-flatten": {
919
  "version": "1.1.1",
920
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1078,6 +1636,15 @@
1078
  "node": ">=4"
1079
  }
1080
  },
 
 
 
 
 
 
 
 
 
1081
  "node_modules/concat-stream": {
1082
  "version": "2.0.0",
1083
  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -1129,6 +1696,12 @@
1129
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
1130
  "license": "MIT"
1131
  },
 
 
 
 
 
 
1132
  "node_modules/debug": {
1133
  "version": "2.6.9",
1134
  "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1147,6 +1720,15 @@
1147
  "node": ">= 0.8"
1148
  }
1149
  },
 
 
 
 
 
 
 
 
 
1150
  "node_modules/destroy": {
1151
  "version": "1.2.0",
1152
  "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -1157,6 +1739,19 @@
1157
  "npm": "1.2.8000 || >= 1.4.16"
1158
  }
1159
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1160
  "node_modules/dotenv": {
1161
  "version": "17.4.1",
1162
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
@@ -1205,6 +1800,18 @@
1205
  "node": ">= 0.8"
1206
  }
1207
  },
 
 
 
 
 
 
 
 
 
 
 
 
1208
  "node_modules/es-define-property": {
1209
  "version": "1.0.1",
1210
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1283,6 +1890,18 @@
1283
  "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1284
  "license": "MIT"
1285
  },
 
 
 
 
 
 
 
 
 
 
 
 
1286
  "node_modules/etag": {
1287
  "version": "1.8.1",
1288
  "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1515,6 +2134,24 @@
1515
  "url": "https://github.com/sponsors/ljharb"
1516
  }
1517
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1518
  "node_modules/has-symbols": {
1519
  "version": "1.1.0",
1520
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1539,6 +2176,16 @@
1539
  "node": ">= 0.4"
1540
  }
1541
  },
 
 
 
 
 
 
 
 
 
 
1542
  "node_modules/http-errors": {
1543
  "version": "2.0.1",
1544
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -1632,6 +2279,23 @@
1632
  "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
1633
  "license": "(AFL-2.1 OR BSD-3-Clause)"
1634
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1635
  "node_modules/kleur": {
1636
  "version": "4.1.5",
1637
  "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -1662,6 +2326,66 @@
1662
  "url": "https://github.com/sponsors/dmonad"
1663
  }
1664
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1665
  "node_modules/math-intrinsics": {
1666
  "version": "1.1.0",
1667
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1671,6 +2395,12 @@
1671
  "node": ">= 0.4"
1672
  }
1673
  },
 
 
 
 
 
 
1674
  "node_modules/media-typer": {
1675
  "version": "0.3.0",
1676
  "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -1815,6 +2545,12 @@
1815
  "node": ">= 0.8"
1816
  }
1817
  },
 
 
 
 
 
 
1818
  "node_modules/parseurl": {
1819
  "version": "1.3.3",
1820
  "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1830,6 +2566,204 @@
1830
  "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
1831
  "license": "MIT"
1832
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1833
  "node_modules/proxy-addr": {
1834
  "version": "2.0.7",
1835
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1843,6 +2777,15 @@
1843
  "node": ">= 0.10"
1844
  }
1845
  },
 
 
 
 
 
 
 
 
 
1846
  "node_modules/qs": {
1847
  "version": "6.14.2",
1848
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -1906,6 +2849,12 @@
1906
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1907
  }
1908
  },
 
 
 
 
 
 
1909
  "node_modules/safe-buffer": {
1910
  "version": "5.2.1",
1911
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2196,11 +3145,16 @@
2196
  "node": ">=14.17"
2197
  }
2198
  },
 
 
 
 
 
 
2199
  "node_modules/undici-types": {
2200
  "version": "6.21.0",
2201
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2202
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2203
- "dev": true,
2204
  "license": "MIT"
2205
  },
2206
  "node_modules/unpipe": {
@@ -2236,12 +3190,27 @@
2236
  "node": ">= 0.8"
2237
  }
2238
  },
 
 
 
 
 
 
2239
  "node_modules/webidl-conversions": {
2240
  "version": "3.0.1",
2241
  "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
2242
  "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
2243
  "license": "BSD-2-Clause"
2244
  },
 
 
 
 
 
 
 
 
 
2245
  "node_modules/whatwg-url": {
2246
  "version": "5.0.0",
2247
  "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 
18
  "@hocuspocus/server": "^3.4.4",
19
  "@huggingface/hub": "^2.11.0",
20
  "@openrouter/ai-sdk-provider": "^2.5.1",
21
+ "@tiptap/core": "^3.22.3",
22
+ "@tiptap/extension-code-block-lowlight": "^3.22.3",
23
+ "@tiptap/extension-image": "^3.22.3",
24
+ "@tiptap/extension-link": "^3.22.3",
25
+ "@tiptap/extension-mathematics": "^3.22.3",
26
+ "@tiptap/extension-table": "^3.22.3",
27
+ "@tiptap/extension-table-cell": "^3.22.3",
28
+ "@tiptap/extension-table-header": "^3.22.3",
29
+ "@tiptap/extension-table-row": "^3.22.3",
30
+ "@tiptap/html": "^3.22.3",
31
+ "@tiptap/pm": "^3.22.3",
32
+ "@tiptap/starter-kit": "^3.22.3",
33
  "ai": "^6.0.158",
34
  "dotenv": "^17.4.1",
35
  "express": "^4.21.0",
36
  "express-ws": "^5.0.2",
37
+ "lowlight": "^3.3.0",
38
  "multer": "^2.1.1",
39
  "yjs": "^13.6.0",
40
  "zod": "^4.3.6"
 
735
  "node": ">=8.0.0"
736
  }
737
  },
738
+ "node_modules/@remirror/core-constants": {
739
+ "version": "3.0.0",
740
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
741
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
742
+ "license": "MIT"
743
+ },
744
  "node_modules/@standard-schema/spec": {
745
  "version": "1.1.0",
746
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
747
  "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
748
  "license": "MIT"
749
  },
750
+ "node_modules/@tiptap/core": {
751
+ "version": "3.22.3",
752
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.3.tgz",
753
+ "integrity": "sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==",
754
+ "license": "MIT",
755
+ "peer": true,
756
+ "funding": {
757
+ "type": "github",
758
+ "url": "https://github.com/sponsors/ueberdosis"
759
+ },
760
+ "peerDependencies": {
761
+ "@tiptap/pm": "^3.22.3"
762
+ }
763
+ },
764
+ "node_modules/@tiptap/extension-blockquote": {
765
+ "version": "3.22.3",
766
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.3.tgz",
767
+ "integrity": "sha512-IaUx3zh7yLHXzIXKL+fw/jzFhsIImdhJyw0lMhe8FfYrefFqXJFYW/sey6+L/e8B3AWvTksPA6VBwefzbH77JA==",
768
+ "license": "MIT",
769
+ "funding": {
770
+ "type": "github",
771
+ "url": "https://github.com/sponsors/ueberdosis"
772
+ },
773
+ "peerDependencies": {
774
+ "@tiptap/core": "^3.22.3"
775
+ }
776
+ },
777
+ "node_modules/@tiptap/extension-bold": {
778
+ "version": "3.22.3",
779
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.3.tgz",
780
+ "integrity": "sha512-tysipHla2zCWr8XNIWRaW9O+7i7/SoEqnRqSRUUi2ailcJjlia+RBy3RykhkgyThrQDStu5KGBS/UvrXwA+O1A==",
781
+ "license": "MIT",
782
+ "funding": {
783
+ "type": "github",
784
+ "url": "https://github.com/sponsors/ueberdosis"
785
+ },
786
+ "peerDependencies": {
787
+ "@tiptap/core": "^3.22.3"
788
+ }
789
+ },
790
+ "node_modules/@tiptap/extension-bullet-list": {
791
+ "version": "3.22.3",
792
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.3.tgz",
793
+ "integrity": "sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==",
794
+ "license": "MIT",
795
+ "funding": {
796
+ "type": "github",
797
+ "url": "https://github.com/sponsors/ueberdosis"
798
+ },
799
+ "peerDependencies": {
800
+ "@tiptap/extension-list": "^3.22.3"
801
+ }
802
+ },
803
+ "node_modules/@tiptap/extension-code": {
804
+ "version": "3.22.3",
805
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.3.tgz",
806
+ "integrity": "sha512-wafWTDQOuMKtXpZEuk1PFQmzopabBciNLryL90MB9S03MNLaQQZYLnmYkDBlzAaLAbgF5QiC+2XZQEBQuTVjFQ==",
807
+ "license": "MIT",
808
+ "funding": {
809
+ "type": "github",
810
+ "url": "https://github.com/sponsors/ueberdosis"
811
+ },
812
+ "peerDependencies": {
813
+ "@tiptap/core": "^3.22.3"
814
+ }
815
+ },
816
+ "node_modules/@tiptap/extension-code-block": {
817
+ "version": "3.22.3",
818
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.3.tgz",
819
+ "integrity": "sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==",
820
+ "license": "MIT",
821
+ "peer": true,
822
+ "funding": {
823
+ "type": "github",
824
+ "url": "https://github.com/sponsors/ueberdosis"
825
+ },
826
+ "peerDependencies": {
827
+ "@tiptap/core": "^3.22.3",
828
+ "@tiptap/pm": "^3.22.3"
829
+ }
830
+ },
831
+ "node_modules/@tiptap/extension-code-block-lowlight": {
832
+ "version": "3.22.3",
833
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.22.3.tgz",
834
+ "integrity": "sha512-NGFuD9zb1QfnCgD2zW4XaUEdQvd/ydm9FmXXh8eawx/+C8xt3p21DIKRERvxWrCvvVNzIUEpZRRYehPOJiD1eg==",
835
+ "license": "MIT",
836
+ "funding": {
837
+ "type": "github",
838
+ "url": "https://github.com/sponsors/ueberdosis"
839
+ },
840
+ "peerDependencies": {
841
+ "@tiptap/core": "^3.22.3",
842
+ "@tiptap/extension-code-block": "^3.22.3",
843
+ "@tiptap/pm": "^3.22.3",
844
+ "highlight.js": "^11",
845
+ "lowlight": "^2 || ^3"
846
+ }
847
+ },
848
+ "node_modules/@tiptap/extension-document": {
849
+ "version": "3.22.3",
850
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.3.tgz",
851
+ "integrity": "sha512-MCSr1PFPtTd++lA3H1RNgqAczAE59XXJ5wUFIQf2F+/0DPY5q2SU4g5QsNJVxPPft5mrNT4C6ty8xBPrALFEdA==",
852
+ "license": "MIT",
853
+ "funding": {
854
+ "type": "github",
855
+ "url": "https://github.com/sponsors/ueberdosis"
856
+ },
857
+ "peerDependencies": {
858
+ "@tiptap/core": "^3.22.3"
859
+ }
860
+ },
861
+ "node_modules/@tiptap/extension-dropcursor": {
862
+ "version": "3.22.3",
863
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.3.tgz",
864
+ "integrity": "sha512-taXq9Tl5aybdFbptJtFRHX9LFJzbXphAbPp4/vutFyTrBu5meXDxuS+B9pEmE+Or0XcolTlW2nDZB0Tqnr18JQ==",
865
+ "license": "MIT",
866
+ "funding": {
867
+ "type": "github",
868
+ "url": "https://github.com/sponsors/ueberdosis"
869
+ },
870
+ "peerDependencies": {
871
+ "@tiptap/extensions": "^3.22.3"
872
+ }
873
+ },
874
+ "node_modules/@tiptap/extension-gapcursor": {
875
+ "version": "3.22.3",
876
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.3.tgz",
877
+ "integrity": "sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==",
878
+ "license": "MIT",
879
+ "funding": {
880
+ "type": "github",
881
+ "url": "https://github.com/sponsors/ueberdosis"
882
+ },
883
+ "peerDependencies": {
884
+ "@tiptap/extensions": "^3.22.3"
885
+ }
886
+ },
887
+ "node_modules/@tiptap/extension-hard-break": {
888
+ "version": "3.22.3",
889
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.3.tgz",
890
+ "integrity": "sha512-J0v8I99y9tbvVmgKYKzKP/JYNsWaZYS7avn4rzLft2OhnyTfwt3OoY8DtpHmmi6apSUaCtoWHWta/TmoEfK1nQ==",
891
+ "license": "MIT",
892
+ "funding": {
893
+ "type": "github",
894
+ "url": "https://github.com/sponsors/ueberdosis"
895
+ },
896
+ "peerDependencies": {
897
+ "@tiptap/core": "^3.22.3"
898
+ }
899
+ },
900
+ "node_modules/@tiptap/extension-heading": {
901
+ "version": "3.22.3",
902
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.3.tgz",
903
+ "integrity": "sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw==",
904
+ "license": "MIT",
905
+ "funding": {
906
+ "type": "github",
907
+ "url": "https://github.com/sponsors/ueberdosis"
908
+ },
909
+ "peerDependencies": {
910
+ "@tiptap/core": "^3.22.3"
911
+ }
912
+ },
913
+ "node_modules/@tiptap/extension-horizontal-rule": {
914
+ "version": "3.22.3",
915
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.3.tgz",
916
+ "integrity": "sha512-wI2bFzScs+KgWeBH/BtypcVKeYelCyqV0RG8nxsZMWtPrBhqixzNd0Oi3gEKtjSjKUqMQ/kjJAIRuESr5UzlHA==",
917
+ "license": "MIT",
918
+ "funding": {
919
+ "type": "github",
920
+ "url": "https://github.com/sponsors/ueberdosis"
921
+ },
922
+ "peerDependencies": {
923
+ "@tiptap/core": "^3.22.3",
924
+ "@tiptap/pm": "^3.22.3"
925
+ }
926
+ },
927
+ "node_modules/@tiptap/extension-image": {
928
+ "version": "3.22.3",
929
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.22.3.tgz",
930
+ "integrity": "sha512-Qpp8c5LOQaNpHrzjqZtoxtIR+8sSqJ7k8v+8anmYw3nxjvt2kpfT28Vd7aWMX55ZS43LaxMx+MkZqbmgUmMP0w==",
931
+ "license": "MIT",
932
+ "funding": {
933
+ "type": "github",
934
+ "url": "https://github.com/sponsors/ueberdosis"
935
+ },
936
+ "peerDependencies": {
937
+ "@tiptap/core": "^3.22.3"
938
+ }
939
+ },
940
+ "node_modules/@tiptap/extension-italic": {
941
+ "version": "3.22.3",
942
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.3.tgz",
943
+ "integrity": "sha512-LteA4cb4EGCiUtrK2JHvDF/Zg0/YqV4DUyHhAAho+oGEQDupZlsS6m0ia5wQcclkiTLzsoPrwcSNu6RDGQ16wQ==",
944
+ "license": "MIT",
945
+ "funding": {
946
+ "type": "github",
947
+ "url": "https://github.com/sponsors/ueberdosis"
948
+ },
949
+ "peerDependencies": {
950
+ "@tiptap/core": "^3.22.3"
951
+ }
952
+ },
953
+ "node_modules/@tiptap/extension-link": {
954
+ "version": "3.22.3",
955
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.3.tgz",
956
+ "integrity": "sha512-S8/P2o9pv6B3kqLjH2TRWwSAximGbciNc6R8/QcN6HWLYxp0N0JoqN3rZHl9VWIBAGRWc4zkt80dhqrl2xmgfQ==",
957
+ "license": "MIT",
958
+ "dependencies": {
959
+ "linkifyjs": "^4.3.2"
960
+ },
961
+ "funding": {
962
+ "type": "github",
963
+ "url": "https://github.com/sponsors/ueberdosis"
964
+ },
965
+ "peerDependencies": {
966
+ "@tiptap/core": "^3.22.3",
967
+ "@tiptap/pm": "^3.22.3"
968
+ }
969
+ },
970
+ "node_modules/@tiptap/extension-list": {
971
+ "version": "3.22.3",
972
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.3.tgz",
973
+ "integrity": "sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==",
974
+ "license": "MIT",
975
+ "peer": true,
976
+ "funding": {
977
+ "type": "github",
978
+ "url": "https://github.com/sponsors/ueberdosis"
979
+ },
980
+ "peerDependencies": {
981
+ "@tiptap/core": "^3.22.3",
982
+ "@tiptap/pm": "^3.22.3"
983
+ }
984
+ },
985
+ "node_modules/@tiptap/extension-list-item": {
986
+ "version": "3.22.3",
987
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.3.tgz",
988
+ "integrity": "sha512-80CNf4oO5y8+LdckT4CyMe1t01EyhpRrQC9H45JW20P7559Nrchp5my3vvMtIAJbpTPPZtcB7LwdzWGKsG5drg==",
989
+ "license": "MIT",
990
+ "funding": {
991
+ "type": "github",
992
+ "url": "https://github.com/sponsors/ueberdosis"
993
+ },
994
+ "peerDependencies": {
995
+ "@tiptap/extension-list": "^3.22.3"
996
+ }
997
+ },
998
+ "node_modules/@tiptap/extension-list-keymap": {
999
+ "version": "3.22.3",
1000
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.3.tgz",
1001
+ "integrity": "sha512-pKuyj5llu35zd/s2u/H9aydKZjmPRAIK5P1q/YXULhhCNln2RnmuRfQ5NklAqTD3yGciQ2lxDwwf7J6iw3ergA==",
1002
+ "license": "MIT",
1003
+ "funding": {
1004
+ "type": "github",
1005
+ "url": "https://github.com/sponsors/ueberdosis"
1006
+ },
1007
+ "peerDependencies": {
1008
+ "@tiptap/extension-list": "^3.22.3"
1009
+ }
1010
+ },
1011
+ "node_modules/@tiptap/extension-mathematics": {
1012
+ "version": "3.22.3",
1013
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-mathematics/-/extension-mathematics-3.22.3.tgz",
1014
+ "integrity": "sha512-GIkuTkiMLRjKN1Dp/jbOZ/Yvf0OrtmjS19Q+BVbkLJI+FJlEn5m+N2f/sjDq6sVoHccdraEyq1Ag8be3ohK0ng==",
1015
+ "license": "MIT",
1016
+ "funding": {
1017
+ "type": "github",
1018
+ "url": "https://github.com/sponsors/ueberdosis"
1019
+ },
1020
+ "peerDependencies": {
1021
+ "@tiptap/core": "^3.22.3",
1022
+ "@tiptap/pm": "^3.22.3",
1023
+ "katex": "^0.16.4"
1024
+ }
1025
+ },
1026
+ "node_modules/@tiptap/extension-ordered-list": {
1027
+ "version": "3.22.3",
1028
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.3.tgz",
1029
+ "integrity": "sha512-orAghtmd+K4Euu4BgI1hG+iZDXBYOyl5YTwiLBc2mQn+pqtZ9LqaH2us4ETwEwNP3/IWXGSAimUZ19nuL+eM2w==",
1030
+ "license": "MIT",
1031
+ "funding": {
1032
+ "type": "github",
1033
+ "url": "https://github.com/sponsors/ueberdosis"
1034
+ },
1035
+ "peerDependencies": {
1036
+ "@tiptap/extension-list": "^3.22.3"
1037
+ }
1038
+ },
1039
+ "node_modules/@tiptap/extension-paragraph": {
1040
+ "version": "3.22.3",
1041
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.3.tgz",
1042
+ "integrity": "sha512-oO7rhfyhEuwm+50s9K3GZPjYyEEEvFAvm1wXopvZnhbkBLydIWImBfrZoC5IQh4/sRDlTIjosV2C+ji5y0tUSg==",
1043
+ "license": "MIT",
1044
+ "funding": {
1045
+ "type": "github",
1046
+ "url": "https://github.com/sponsors/ueberdosis"
1047
+ },
1048
+ "peerDependencies": {
1049
+ "@tiptap/core": "^3.22.3"
1050
+ }
1051
+ },
1052
+ "node_modules/@tiptap/extension-strike": {
1053
+ "version": "3.22.3",
1054
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.3.tgz",
1055
+ "integrity": "sha512-jY2InoUlKkuk5KHoIDGdML1OCA2n6PRHAtxwHNkAmiYh0Khf0zaVPGFpx4dgQrN7W5Q1WE6oBZnjrvy6qb7w0g==",
1056
+ "license": "MIT",
1057
+ "funding": {
1058
+ "type": "github",
1059
+ "url": "https://github.com/sponsors/ueberdosis"
1060
+ },
1061
+ "peerDependencies": {
1062
+ "@tiptap/core": "^3.22.3"
1063
+ }
1064
+ },
1065
+ "node_modules/@tiptap/extension-table": {
1066
+ "version": "3.22.3",
1067
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.22.3.tgz",
1068
+ "integrity": "sha512-inbQSusJad7H0T++L1APg/anfL5d15cNGp2YG3vwo6TQr71nn2c9pepvmz3xuAIt8eygZDRba+4GT/COP1f9QA==",
1069
+ "license": "MIT",
1070
+ "peer": true,
1071
+ "funding": {
1072
+ "type": "github",
1073
+ "url": "https://github.com/sponsors/ueberdosis"
1074
+ },
1075
+ "peerDependencies": {
1076
+ "@tiptap/core": "^3.22.3",
1077
+ "@tiptap/pm": "^3.22.3"
1078
+ }
1079
+ },
1080
+ "node_modules/@tiptap/extension-table-cell": {
1081
+ "version": "3.22.3",
1082
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.22.3.tgz",
1083
+ "integrity": "sha512-LgaUgNwjHe6J7dq66N7iflC9efjgygsYWkHJtfLzaSU82tXzk8HSwBrMRCaj0PU+AR967/jGixEZIxH9xfXswQ==",
1084
+ "license": "MIT",
1085
+ "funding": {
1086
+ "type": "github",
1087
+ "url": "https://github.com/sponsors/ueberdosis"
1088
+ },
1089
+ "peerDependencies": {
1090
+ "@tiptap/extension-table": "^3.22.3"
1091
+ }
1092
+ },
1093
+ "node_modules/@tiptap/extension-table-header": {
1094
+ "version": "3.22.3",
1095
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.22.3.tgz",
1096
+ "integrity": "sha512-5wQ5rne9ccdbzeqC1AEDaUzlWjnpQNgBctgSPKEv+Da88lRRcGX21H+b/8jhL7fr4/sznr45toKGUg8NmRhboQ==",
1097
+ "license": "MIT",
1098
+ "funding": {
1099
+ "type": "github",
1100
+ "url": "https://github.com/sponsors/ueberdosis"
1101
+ },
1102
+ "peerDependencies": {
1103
+ "@tiptap/extension-table": "^3.22.3"
1104
+ }
1105
+ },
1106
+ "node_modules/@tiptap/extension-table-row": {
1107
+ "version": "3.22.3",
1108
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.22.3.tgz",
1109
+ "integrity": "sha512-dkoHnbOZgb2iQLXsp2FUu0KejAh3LOqDLMixYYU/0lnUwhqGG8kgu6+0YkCiUhedbfMFqVvemdKnRddpTQ4sqg==",
1110
+ "license": "MIT",
1111
+ "funding": {
1112
+ "type": "github",
1113
+ "url": "https://github.com/sponsors/ueberdosis"
1114
+ },
1115
+ "peerDependencies": {
1116
+ "@tiptap/extension-table": "^3.22.3"
1117
+ }
1118
+ },
1119
+ "node_modules/@tiptap/extension-text": {
1120
+ "version": "3.22.3",
1121
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.3.tgz",
1122
+ "integrity": "sha512-Q9R7JsTdomP5uUjtPjNKxHT1xoh/i9OJZnmgJLe7FcgZEaPOQ3bWxmKZoLZQfDfZjyB8BtH+Hc7nUvhCMOePxw==",
1123
+ "license": "MIT",
1124
+ "funding": {
1125
+ "type": "github",
1126
+ "url": "https://github.com/sponsors/ueberdosis"
1127
+ },
1128
+ "peerDependencies": {
1129
+ "@tiptap/core": "^3.22.3"
1130
+ }
1131
+ },
1132
+ "node_modules/@tiptap/extension-underline": {
1133
+ "version": "3.22.3",
1134
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.3.tgz",
1135
+ "integrity": "sha512-Ch6CBWRa5w90yYSPUW6x9Py9JdrXMqk3pZ9OIlMYD8A7BqyZGfiHerX7XDMYDS09KjyK3U9XH60/zxYOzXdDLA==",
1136
+ "license": "MIT",
1137
+ "funding": {
1138
+ "type": "github",
1139
+ "url": "https://github.com/sponsors/ueberdosis"
1140
+ },
1141
+ "peerDependencies": {
1142
+ "@tiptap/core": "^3.22.3"
1143
+ }
1144
+ },
1145
+ "node_modules/@tiptap/extensions": {
1146
+ "version": "3.22.3",
1147
+ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.3.tgz",
1148
+ "integrity": "sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==",
1149
+ "license": "MIT",
1150
+ "peer": true,
1151
+ "funding": {
1152
+ "type": "github",
1153
+ "url": "https://github.com/sponsors/ueberdosis"
1154
+ },
1155
+ "peerDependencies": {
1156
+ "@tiptap/core": "^3.22.3",
1157
+ "@tiptap/pm": "^3.22.3"
1158
+ }
1159
+ },
1160
+ "node_modules/@tiptap/html": {
1161
+ "version": "3.22.3",
1162
+ "resolved": "https://registry.npmjs.org/@tiptap/html/-/html-3.22.3.tgz",
1163
+ "integrity": "sha512-Cc1v2x5HJNDjgkT9vxhP1rJGscIPWTprCRUzRCIVwLuqdnRblntJtOpxMhFUU7yXCqlY27yUqmOBAvyhH1WO9g==",
1164
+ "license": "MIT",
1165
+ "funding": {
1166
+ "type": "github",
1167
+ "url": "https://github.com/sponsors/ueberdosis"
1168
+ },
1169
+ "peerDependencies": {
1170
+ "@tiptap/core": "^3.22.3",
1171
+ "@tiptap/pm": "^3.22.3",
1172
+ "happy-dom": "^20.8.9"
1173
+ }
1174
+ },
1175
+ "node_modules/@tiptap/pm": {
1176
+ "version": "3.22.3",
1177
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.3.tgz",
1178
+ "integrity": "sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==",
1179
+ "license": "MIT",
1180
+ "peer": true,
1181
+ "dependencies": {
1182
+ "prosemirror-changeset": "^2.3.0",
1183
+ "prosemirror-collab": "^1.3.1",
1184
+ "prosemirror-commands": "^1.6.2",
1185
+ "prosemirror-dropcursor": "^1.8.1",
1186
+ "prosemirror-gapcursor": "^1.3.2",
1187
+ "prosemirror-history": "^1.4.1",
1188
+ "prosemirror-inputrules": "^1.4.0",
1189
+ "prosemirror-keymap": "^1.2.2",
1190
+ "prosemirror-markdown": "^1.13.1",
1191
+ "prosemirror-menu": "^1.2.4",
1192
+ "prosemirror-model": "^1.24.1",
1193
+ "prosemirror-schema-basic": "^1.2.3",
1194
+ "prosemirror-schema-list": "^1.5.0",
1195
+ "prosemirror-state": "^1.4.3",
1196
+ "prosemirror-tables": "^1.6.4",
1197
+ "prosemirror-trailing-node": "^3.0.0",
1198
+ "prosemirror-transform": "^1.10.2",
1199
+ "prosemirror-view": "^1.38.1"
1200
+ },
1201
+ "funding": {
1202
+ "type": "github",
1203
+ "url": "https://github.com/sponsors/ueberdosis"
1204
+ }
1205
+ },
1206
+ "node_modules/@tiptap/starter-kit": {
1207
+ "version": "3.22.3",
1208
+ "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.3.tgz",
1209
+ "integrity": "sha512-vdW/Oo1fdwTL1VOQ5YYbTov00ANeHLquBVEZyL/EkV7Xv5io9rXQsCysJfTSHhiQlyr2MtWFB4+CPGuwXjQWOQ==",
1210
+ "license": "MIT",
1211
+ "dependencies": {
1212
+ "@tiptap/core": "^3.22.3",
1213
+ "@tiptap/extension-blockquote": "^3.22.3",
1214
+ "@tiptap/extension-bold": "^3.22.3",
1215
+ "@tiptap/extension-bullet-list": "^3.22.3",
1216
+ "@tiptap/extension-code": "^3.22.3",
1217
+ "@tiptap/extension-code-block": "^3.22.3",
1218
+ "@tiptap/extension-document": "^3.22.3",
1219
+ "@tiptap/extension-dropcursor": "^3.22.3",
1220
+ "@tiptap/extension-gapcursor": "^3.22.3",
1221
+ "@tiptap/extension-hard-break": "^3.22.3",
1222
+ "@tiptap/extension-heading": "^3.22.3",
1223
+ "@tiptap/extension-horizontal-rule": "^3.22.3",
1224
+ "@tiptap/extension-italic": "^3.22.3",
1225
+ "@tiptap/extension-link": "^3.22.3",
1226
+ "@tiptap/extension-list": "^3.22.3",
1227
+ "@tiptap/extension-list-item": "^3.22.3",
1228
+ "@tiptap/extension-list-keymap": "^3.22.3",
1229
+ "@tiptap/extension-ordered-list": "^3.22.3",
1230
+ "@tiptap/extension-paragraph": "^3.22.3",
1231
+ "@tiptap/extension-strike": "^3.22.3",
1232
+ "@tiptap/extension-text": "^3.22.3",
1233
+ "@tiptap/extension-underline": "^3.22.3",
1234
+ "@tiptap/extensions": "^3.22.3",
1235
+ "@tiptap/pm": "^3.22.3"
1236
+ },
1237
+ "funding": {
1238
+ "type": "github",
1239
+ "url": "https://github.com/sponsors/ueberdosis"
1240
+ }
1241
+ },
1242
  "node_modules/@types/body-parser": {
1243
  "version": "1.19.6",
1244
  "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
 
1297
  "@types/ws": "*"
1298
  }
1299
  },
1300
+ "node_modules/@types/hast": {
1301
+ "version": "3.0.4",
1302
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
1303
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
1304
+ "license": "MIT",
1305
+ "dependencies": {
1306
+ "@types/unist": "*"
1307
+ }
1308
+ },
1309
  "node_modules/@types/http-errors": {
1310
  "version": "2.0.5",
1311
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
 
1313
  "dev": true,
1314
  "license": "MIT"
1315
  },
1316
+ "node_modules/@types/linkify-it": {
1317
+ "version": "5.0.0",
1318
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
1319
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
1320
+ "license": "MIT"
1321
+ },
1322
+ "node_modules/@types/markdown-it": {
1323
+ "version": "14.1.2",
1324
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
1325
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
1326
+ "license": "MIT",
1327
+ "dependencies": {
1328
+ "@types/linkify-it": "^5",
1329
+ "@types/mdurl": "^2"
1330
+ }
1331
+ },
1332
+ "node_modules/@types/mdurl": {
1333
+ "version": "2.0.0",
1334
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
1335
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
1336
+ "license": "MIT"
1337
+ },
1338
  "node_modules/@types/multer": {
1339
  "version": "2.1.0",
1340
  "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
 
1349
  "version": "22.19.17",
1350
  "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
1351
  "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
 
1352
  "license": "MIT",
1353
  "dependencies": {
1354
  "undici-types": "~6.21.0"
 
1389
  "@types/node": "*"
1390
  }
1391
  },
1392
+ "node_modules/@types/unist": {
1393
+ "version": "3.0.3",
1394
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1395
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1396
+ "license": "MIT"
1397
+ },
1398
+ "node_modules/@types/whatwg-mimetype": {
1399
+ "version": "3.0.2",
1400
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
1401
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
1402
+ "license": "MIT"
1403
+ },
1404
  "node_modules/@types/ws": {
1405
  "version": "8.18.1",
1406
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
1407
  "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
 
1408
  "license": "MIT",
1409
  "dependencies": {
1410
  "@types/node": "*"
 
1467
  "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
1468
  "license": "MIT"
1469
  },
1470
+ "node_modules/argparse": {
1471
+ "version": "2.0.1",
1472
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1473
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1474
+ "license": "Python-2.0"
1475
+ },
1476
  "node_modules/array-flatten": {
1477
  "version": "1.1.1",
1478
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
 
1636
  "node": ">=4"
1637
  }
1638
  },
1639
+ "node_modules/commander": {
1640
+ "version": "8.3.0",
1641
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
1642
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
1643
+ "license": "MIT",
1644
+ "engines": {
1645
+ "node": ">= 12"
1646
+ }
1647
+ },
1648
  "node_modules/concat-stream": {
1649
  "version": "2.0.0",
1650
  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
 
1696
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
1697
  "license": "MIT"
1698
  },
1699
+ "node_modules/crelt": {
1700
+ "version": "1.0.6",
1701
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
1702
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
1703
+ "license": "MIT"
1704
+ },
1705
  "node_modules/debug": {
1706
  "version": "2.6.9",
1707
  "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
 
1720
  "node": ">= 0.8"
1721
  }
1722
  },
1723
+ "node_modules/dequal": {
1724
+ "version": "2.0.3",
1725
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
1726
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
1727
+ "license": "MIT",
1728
+ "engines": {
1729
+ "node": ">=6"
1730
+ }
1731
+ },
1732
  "node_modules/destroy": {
1733
  "version": "1.2.0",
1734
  "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
 
1739
  "npm": "1.2.8000 || >= 1.4.16"
1740
  }
1741
  },
1742
+ "node_modules/devlop": {
1743
+ "version": "1.1.0",
1744
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
1745
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
1746
+ "license": "MIT",
1747
+ "dependencies": {
1748
+ "dequal": "^2.0.0"
1749
+ },
1750
+ "funding": {
1751
+ "type": "github",
1752
+ "url": "https://github.com/sponsors/wooorm"
1753
+ }
1754
+ },
1755
  "node_modules/dotenv": {
1756
  "version": "17.4.1",
1757
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
 
1800
  "node": ">= 0.8"
1801
  }
1802
  },
1803
+ "node_modules/entities": {
1804
+ "version": "7.0.1",
1805
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
1806
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
1807
+ "license": "BSD-2-Clause",
1808
+ "engines": {
1809
+ "node": ">=0.12"
1810
+ },
1811
+ "funding": {
1812
+ "url": "https://github.com/fb55/entities?sponsor=1"
1813
+ }
1814
+ },
1815
  "node_modules/es-define-property": {
1816
  "version": "1.0.1",
1817
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
 
1890
  "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1891
  "license": "MIT"
1892
  },
1893
+ "node_modules/escape-string-regexp": {
1894
+ "version": "4.0.0",
1895
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1896
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1897
+ "license": "MIT",
1898
+ "engines": {
1899
+ "node": ">=10"
1900
+ },
1901
+ "funding": {
1902
+ "url": "https://github.com/sponsors/sindresorhus"
1903
+ }
1904
+ },
1905
  "node_modules/etag": {
1906
  "version": "1.8.1",
1907
  "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
 
2134
  "url": "https://github.com/sponsors/ljharb"
2135
  }
2136
  },
2137
+ "node_modules/happy-dom": {
2138
+ "version": "20.8.9",
2139
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.9.tgz",
2140
+ "integrity": "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==",
2141
+ "license": "MIT",
2142
+ "peer": true,
2143
+ "dependencies": {
2144
+ "@types/node": ">=20.0.0",
2145
+ "@types/whatwg-mimetype": "^3.0.2",
2146
+ "@types/ws": "^8.18.1",
2147
+ "entities": "^7.0.1",
2148
+ "whatwg-mimetype": "^3.0.0",
2149
+ "ws": "^8.18.3"
2150
+ },
2151
+ "engines": {
2152
+ "node": ">=20.0.0"
2153
+ }
2154
+ },
2155
  "node_modules/has-symbols": {
2156
  "version": "1.1.0",
2157
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
 
2176
  "node": ">= 0.4"
2177
  }
2178
  },
2179
+ "node_modules/highlight.js": {
2180
+ "version": "11.11.1",
2181
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
2182
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
2183
+ "license": "BSD-3-Clause",
2184
+ "peer": true,
2185
+ "engines": {
2186
+ "node": ">=12.0.0"
2187
+ }
2188
+ },
2189
  "node_modules/http-errors": {
2190
  "version": "2.0.1",
2191
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
 
2279
  "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
2280
  "license": "(AFL-2.1 OR BSD-3-Clause)"
2281
  },
2282
+ "node_modules/katex": {
2283
+ "version": "0.16.45",
2284
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
2285
+ "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
2286
+ "funding": [
2287
+ "https://opencollective.com/katex",
2288
+ "https://github.com/sponsors/katex"
2289
+ ],
2290
+ "license": "MIT",
2291
+ "peer": true,
2292
+ "dependencies": {
2293
+ "commander": "^8.3.0"
2294
+ },
2295
+ "bin": {
2296
+ "katex": "cli.js"
2297
+ }
2298
+ },
2299
  "node_modules/kleur": {
2300
  "version": "4.1.5",
2301
  "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
 
2326
  "url": "https://github.com/sponsors/dmonad"
2327
  }
2328
  },
2329
+ "node_modules/linkify-it": {
2330
+ "version": "5.0.0",
2331
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
2332
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
2333
+ "license": "MIT",
2334
+ "dependencies": {
2335
+ "uc.micro": "^2.0.0"
2336
+ }
2337
+ },
2338
+ "node_modules/linkifyjs": {
2339
+ "version": "4.3.2",
2340
+ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
2341
+ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
2342
+ "license": "MIT"
2343
+ },
2344
+ "node_modules/lowlight": {
2345
+ "version": "3.3.0",
2346
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
2347
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
2348
+ "license": "MIT",
2349
+ "peer": true,
2350
+ "dependencies": {
2351
+ "@types/hast": "^3.0.0",
2352
+ "devlop": "^1.0.0",
2353
+ "highlight.js": "~11.11.0"
2354
+ },
2355
+ "funding": {
2356
+ "type": "github",
2357
+ "url": "https://github.com/sponsors/wooorm"
2358
+ }
2359
+ },
2360
+ "node_modules/markdown-it": {
2361
+ "version": "14.1.1",
2362
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
2363
+ "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
2364
+ "license": "MIT",
2365
+ "dependencies": {
2366
+ "argparse": "^2.0.1",
2367
+ "entities": "^4.4.0",
2368
+ "linkify-it": "^5.0.0",
2369
+ "mdurl": "^2.0.0",
2370
+ "punycode.js": "^2.3.1",
2371
+ "uc.micro": "^2.1.0"
2372
+ },
2373
+ "bin": {
2374
+ "markdown-it": "bin/markdown-it.mjs"
2375
+ }
2376
+ },
2377
+ "node_modules/markdown-it/node_modules/entities": {
2378
+ "version": "4.5.0",
2379
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
2380
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
2381
+ "license": "BSD-2-Clause",
2382
+ "engines": {
2383
+ "node": ">=0.12"
2384
+ },
2385
+ "funding": {
2386
+ "url": "https://github.com/fb55/entities?sponsor=1"
2387
+ }
2388
+ },
2389
  "node_modules/math-intrinsics": {
2390
  "version": "1.1.0",
2391
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
 
2395
  "node": ">= 0.4"
2396
  }
2397
  },
2398
+ "node_modules/mdurl": {
2399
+ "version": "2.0.0",
2400
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
2401
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
2402
+ "license": "MIT"
2403
+ },
2404
  "node_modules/media-typer": {
2405
  "version": "0.3.0",
2406
  "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
 
2545
  "node": ">= 0.8"
2546
  }
2547
  },
2548
+ "node_modules/orderedmap": {
2549
+ "version": "2.1.1",
2550
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
2551
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
2552
+ "license": "MIT"
2553
+ },
2554
  "node_modules/parseurl": {
2555
  "version": "1.3.3",
2556
  "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 
2566
  "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
2567
  "license": "MIT"
2568
  },
2569
+ "node_modules/prosemirror-changeset": {
2570
+ "version": "2.4.0",
2571
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
2572
+ "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
2573
+ "license": "MIT",
2574
+ "dependencies": {
2575
+ "prosemirror-transform": "^1.0.0"
2576
+ }
2577
+ },
2578
+ "node_modules/prosemirror-collab": {
2579
+ "version": "1.3.1",
2580
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
2581
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
2582
+ "license": "MIT",
2583
+ "dependencies": {
2584
+ "prosemirror-state": "^1.0.0"
2585
+ }
2586
+ },
2587
+ "node_modules/prosemirror-commands": {
2588
+ "version": "1.7.1",
2589
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
2590
+ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
2591
+ "license": "MIT",
2592
+ "dependencies": {
2593
+ "prosemirror-model": "^1.0.0",
2594
+ "prosemirror-state": "^1.0.0",
2595
+ "prosemirror-transform": "^1.10.2"
2596
+ }
2597
+ },
2598
+ "node_modules/prosemirror-dropcursor": {
2599
+ "version": "1.8.2",
2600
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
2601
+ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
2602
+ "license": "MIT",
2603
+ "dependencies": {
2604
+ "prosemirror-state": "^1.0.0",
2605
+ "prosemirror-transform": "^1.1.0",
2606
+ "prosemirror-view": "^1.1.0"
2607
+ }
2608
+ },
2609
+ "node_modules/prosemirror-gapcursor": {
2610
+ "version": "1.4.1",
2611
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
2612
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
2613
+ "license": "MIT",
2614
+ "dependencies": {
2615
+ "prosemirror-keymap": "^1.0.0",
2616
+ "prosemirror-model": "^1.0.0",
2617
+ "prosemirror-state": "^1.0.0",
2618
+ "prosemirror-view": "^1.0.0"
2619
+ }
2620
+ },
2621
+ "node_modules/prosemirror-history": {
2622
+ "version": "1.5.0",
2623
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
2624
+ "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
2625
+ "license": "MIT",
2626
+ "dependencies": {
2627
+ "prosemirror-state": "^1.2.2",
2628
+ "prosemirror-transform": "^1.0.0",
2629
+ "prosemirror-view": "^1.31.0",
2630
+ "rope-sequence": "^1.3.0"
2631
+ }
2632
+ },
2633
+ "node_modules/prosemirror-inputrules": {
2634
+ "version": "1.5.1",
2635
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
2636
+ "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
2637
+ "license": "MIT",
2638
+ "dependencies": {
2639
+ "prosemirror-state": "^1.0.0",
2640
+ "prosemirror-transform": "^1.0.0"
2641
+ }
2642
+ },
2643
+ "node_modules/prosemirror-keymap": {
2644
+ "version": "1.2.3",
2645
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
2646
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
2647
+ "license": "MIT",
2648
+ "dependencies": {
2649
+ "prosemirror-state": "^1.0.0",
2650
+ "w3c-keyname": "^2.2.0"
2651
+ }
2652
+ },
2653
+ "node_modules/prosemirror-markdown": {
2654
+ "version": "1.13.4",
2655
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
2656
+ "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
2657
+ "license": "MIT",
2658
+ "dependencies": {
2659
+ "@types/markdown-it": "^14.0.0",
2660
+ "markdown-it": "^14.0.0",
2661
+ "prosemirror-model": "^1.25.0"
2662
+ }
2663
+ },
2664
+ "node_modules/prosemirror-menu": {
2665
+ "version": "1.3.0",
2666
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
2667
+ "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
2668
+ "license": "MIT",
2669
+ "dependencies": {
2670
+ "crelt": "^1.0.0",
2671
+ "prosemirror-commands": "^1.0.0",
2672
+ "prosemirror-history": "^1.0.0",
2673
+ "prosemirror-state": "^1.0.0"
2674
+ }
2675
+ },
2676
+ "node_modules/prosemirror-model": {
2677
+ "version": "1.25.4",
2678
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
2679
+ "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
2680
+ "license": "MIT",
2681
+ "peer": true,
2682
+ "dependencies": {
2683
+ "orderedmap": "^2.0.0"
2684
+ }
2685
+ },
2686
+ "node_modules/prosemirror-schema-basic": {
2687
+ "version": "1.2.4",
2688
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
2689
+ "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
2690
+ "license": "MIT",
2691
+ "dependencies": {
2692
+ "prosemirror-model": "^1.25.0"
2693
+ }
2694
+ },
2695
+ "node_modules/prosemirror-schema-list": {
2696
+ "version": "1.5.1",
2697
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
2698
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
2699
+ "license": "MIT",
2700
+ "dependencies": {
2701
+ "prosemirror-model": "^1.0.0",
2702
+ "prosemirror-state": "^1.0.0",
2703
+ "prosemirror-transform": "^1.7.3"
2704
+ }
2705
+ },
2706
+ "node_modules/prosemirror-state": {
2707
+ "version": "1.4.4",
2708
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
2709
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
2710
+ "license": "MIT",
2711
+ "peer": true,
2712
+ "dependencies": {
2713
+ "prosemirror-model": "^1.0.0",
2714
+ "prosemirror-transform": "^1.0.0",
2715
+ "prosemirror-view": "^1.27.0"
2716
+ }
2717
+ },
2718
+ "node_modules/prosemirror-tables": {
2719
+ "version": "1.8.5",
2720
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
2721
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
2722
+ "license": "MIT",
2723
+ "dependencies": {
2724
+ "prosemirror-keymap": "^1.2.3",
2725
+ "prosemirror-model": "^1.25.4",
2726
+ "prosemirror-state": "^1.4.4",
2727
+ "prosemirror-transform": "^1.10.5",
2728
+ "prosemirror-view": "^1.41.4"
2729
+ }
2730
+ },
2731
+ "node_modules/prosemirror-trailing-node": {
2732
+ "version": "3.0.0",
2733
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
2734
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
2735
+ "license": "MIT",
2736
+ "dependencies": {
2737
+ "@remirror/core-constants": "3.0.0",
2738
+ "escape-string-regexp": "^4.0.0"
2739
+ },
2740
+ "peerDependencies": {
2741
+ "prosemirror-model": "^1.22.1",
2742
+ "prosemirror-state": "^1.4.2",
2743
+ "prosemirror-view": "^1.33.8"
2744
+ }
2745
+ },
2746
+ "node_modules/prosemirror-transform": {
2747
+ "version": "1.12.0",
2748
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
2749
+ "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
2750
+ "license": "MIT",
2751
+ "dependencies": {
2752
+ "prosemirror-model": "^1.21.0"
2753
+ }
2754
+ },
2755
+ "node_modules/prosemirror-view": {
2756
+ "version": "1.41.8",
2757
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
2758
+ "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
2759
+ "license": "MIT",
2760
+ "peer": true,
2761
+ "dependencies": {
2762
+ "prosemirror-model": "^1.20.0",
2763
+ "prosemirror-state": "^1.0.0",
2764
+ "prosemirror-transform": "^1.1.0"
2765
+ }
2766
+ },
2767
  "node_modules/proxy-addr": {
2768
  "version": "2.0.7",
2769
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 
2777
  "node": ">= 0.10"
2778
  }
2779
  },
2780
+ "node_modules/punycode.js": {
2781
+ "version": "2.3.1",
2782
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
2783
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
2784
+ "license": "MIT",
2785
+ "engines": {
2786
+ "node": ">=6"
2787
+ }
2788
+ },
2789
  "node_modules/qs": {
2790
  "version": "6.14.2",
2791
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
 
2849
  "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
2850
  }
2851
  },
2852
+ "node_modules/rope-sequence": {
2853
+ "version": "1.3.4",
2854
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
2855
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
2856
+ "license": "MIT"
2857
+ },
2858
  "node_modules/safe-buffer": {
2859
  "version": "5.2.1",
2860
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 
3145
  "node": ">=14.17"
3146
  }
3147
  },
3148
+ "node_modules/uc.micro": {
3149
+ "version": "2.1.0",
3150
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
3151
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
3152
+ "license": "MIT"
3153
+ },
3154
  "node_modules/undici-types": {
3155
  "version": "6.21.0",
3156
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
3157
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
 
3158
  "license": "MIT"
3159
  },
3160
  "node_modules/unpipe": {
 
3190
  "node": ">= 0.8"
3191
  }
3192
  },
3193
+ "node_modules/w3c-keyname": {
3194
+ "version": "2.2.8",
3195
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
3196
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
3197
+ "license": "MIT"
3198
+ },
3199
  "node_modules/webidl-conversions": {
3200
  "version": "3.0.1",
3201
  "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
3202
  "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
3203
  "license": "BSD-2-Clause"
3204
  },
3205
+ "node_modules/whatwg-mimetype": {
3206
+ "version": "3.0.0",
3207
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
3208
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
3209
+ "license": "MIT",
3210
+ "engines": {
3211
+ "node": ">=12"
3212
+ }
3213
+ },
3214
  "node_modules/whatwg-url": {
3215
  "version": "5.0.0",
3216
  "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
backend/package.json CHANGED
@@ -19,10 +19,23 @@
19
  "@hocuspocus/server": "^3.4.4",
20
  "@huggingface/hub": "^2.11.0",
21
  "@openrouter/ai-sdk-provider": "^2.5.1",
 
 
 
 
 
 
 
 
 
 
 
 
22
  "ai": "^6.0.158",
23
  "dotenv": "^17.4.1",
24
  "express": "^4.21.0",
25
  "express-ws": "^5.0.2",
 
26
  "multer": "^2.1.1",
27
  "yjs": "^13.6.0",
28
  "zod": "^4.3.6"
 
19
  "@hocuspocus/server": "^3.4.4",
20
  "@huggingface/hub": "^2.11.0",
21
  "@openrouter/ai-sdk-provider": "^2.5.1",
22
+ "@tiptap/core": "^3.22.3",
23
+ "@tiptap/extension-code-block-lowlight": "^3.22.3",
24
+ "@tiptap/extension-image": "^3.22.3",
25
+ "@tiptap/extension-link": "^3.22.3",
26
+ "@tiptap/extension-mathematics": "^3.22.3",
27
+ "@tiptap/extension-table": "^3.22.3",
28
+ "@tiptap/extension-table-cell": "^3.22.3",
29
+ "@tiptap/extension-table-header": "^3.22.3",
30
+ "@tiptap/extension-table-row": "^3.22.3",
31
+ "@tiptap/html": "^3.22.3",
32
+ "@tiptap/pm": "^3.22.3",
33
+ "@tiptap/starter-kit": "^3.22.3",
34
  "ai": "^6.0.158",
35
  "dotenv": "^17.4.1",
36
  "express": "^4.21.0",
37
  "express-ws": "^5.0.2",
38
+ "lowlight": "^3.3.0",
39
  "multer": "^2.1.1",
40
  "yjs": "^13.6.0",
41
  "zod": "^4.3.6"
backend/src/hf-storage.ts CHANGED
@@ -95,3 +95,67 @@ export async function flushAll(): Promise<void> {
95
  const names = [...dirtyDocs.keys()];
96
  await Promise.allSettled(names.map((n) => pushDocument(n)));
97
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  const names = [...dirtyDocs.keys()];
96
  await Promise.allSettled(names.map((n) => pushDocument(n)));
97
  }
98
+
99
+ // ---------- Published Assets ----------
100
+
101
+ interface PublishedPayload {
102
+ html: string;
103
+ pdf: Buffer | null;
104
+ thumbnail: Buffer | null;
105
+ meta: { title: string; description: string; authors: string[]; date: string; [key: string]: unknown };
106
+ }
107
+
108
+ export async function uploadPublishedAssets(
109
+ docName: string,
110
+ payload: PublishedPayload
111
+ ): Promise<{ htmlUrl: string; pdfUrl: string | null; thumbUrl: string | null }> {
112
+ const safeName = sanitizeName(docName);
113
+ const base = `published/${safeName}`;
114
+
115
+ // Upload HTML
116
+ await uploadFile({
117
+ repo,
118
+ file: { path: `${base}/index.html`, content: new Blob([payload.html]) },
119
+ accessToken: HF_TOKEN,
120
+ commitTitle: `publish ${safeName}: HTML`,
121
+ });
122
+
123
+ const baseUrl = `https://huggingface.co/datasets/${HF_DATASET_ID}/resolve/main/${base}`;
124
+ let pdfUrl: string | null = null;
125
+ let thumbUrl: string | null = null;
126
+
127
+ // Upload PDF
128
+ if (payload.pdf) {
129
+ await uploadFile({
130
+ repo,
131
+ file: { path: `${base}/article.pdf`, content: new Blob([new Uint8Array(payload.pdf)]) },
132
+ accessToken: HF_TOKEN,
133
+ commitTitle: `publish ${safeName}: PDF`,
134
+ });
135
+ pdfUrl = `${baseUrl}/article.pdf`;
136
+ }
137
+
138
+ // Upload thumbnail
139
+ if (payload.thumbnail) {
140
+ await uploadFile({
141
+ repo,
142
+ file: { path: `${base}/thumb.jpg`, content: new Blob([new Uint8Array(payload.thumbnail)]) },
143
+ accessToken: HF_TOKEN,
144
+ commitTitle: `publish ${safeName}: thumbnail`,
145
+ });
146
+ thumbUrl = `${baseUrl}/thumb.jpg`;
147
+ }
148
+
149
+ // Upload meta.json
150
+ await uploadFile({
151
+ repo,
152
+ file: {
153
+ path: `${base}/meta.json`,
154
+ content: new Blob([JSON.stringify(payload.meta, null, 2)]),
155
+ },
156
+ accessToken: HF_TOKEN,
157
+ commitTitle: `publish ${safeName}: metadata`,
158
+ });
159
+
160
+ return { htmlUrl: `${baseUrl}/index.html`, pdfUrl, thumbUrl };
161
+ }
backend/src/publisher/extensions.ts ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Server-side TipTap extensions for generateHTML().
3
+ *
4
+ * Mirrors the frontend extension list but without:
5
+ * - React NodeViews (not available server-side)
6
+ * - Collaboration / CollaborationCursor / CollaborationUndo
7
+ * - Placeholder, SlashCommands, ImageUpload, Comment (editor-only)
8
+ */
9
+
10
+ import { Node, mergeAttributes, type Extensions } from "@tiptap/core";
11
+ import StarterKit from "@tiptap/starter-kit";
12
+ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
13
+ import Link from "@tiptap/extension-link";
14
+ import Mathematics from "@tiptap/extension-mathematics";
15
+ import Image from "@tiptap/extension-image";
16
+ import { Table } from "@tiptap/extension-table";
17
+ import { TableRow } from "@tiptap/extension-table-row";
18
+ import { TableCell } from "@tiptap/extension-table-cell";
19
+ import { TableHeader } from "@tiptap/extension-table-header";
20
+ import { common, createLowlight } from "lowlight";
21
+
22
+ const lowlight = createLowlight(common);
23
+
24
+ // ---- Custom inline nodes (server versions, no NodeView) ----
25
+
26
+ const CitationServer = Node.create({
27
+ name: "citation",
28
+ group: "inline",
29
+ inline: true,
30
+ atom: true,
31
+ addAttributes() {
32
+ return { key: { default: "" } };
33
+ },
34
+ parseHTML() {
35
+ return [{ tag: 'span[data-type="citation"]' }];
36
+ },
37
+ renderHTML({ HTMLAttributes }) {
38
+ return [
39
+ "span",
40
+ mergeAttributes(HTMLAttributes, {
41
+ "data-type": "citation",
42
+ class: "citation-node",
43
+ }),
44
+ ];
45
+ },
46
+ });
47
+
48
+ const BibliographyServer = Node.create({
49
+ name: "bibliography",
50
+ group: "block",
51
+ atom: true,
52
+ parseHTML() {
53
+ return [{ tag: 'div[data-type="bibliography"]' }];
54
+ },
55
+ renderHTML({ HTMLAttributes }) {
56
+ return [
57
+ "div",
58
+ mergeAttributes(HTMLAttributes, {
59
+ "data-type": "bibliography",
60
+ class: "bibliography-block",
61
+ }),
62
+ ];
63
+ },
64
+ });
65
+
66
+ const GlossaryServer = Node.create({
67
+ name: "glossary",
68
+ group: "inline",
69
+ inline: true,
70
+ atom: true,
71
+ addAttributes() {
72
+ return {
73
+ term: { default: "" },
74
+ definition: { default: "" },
75
+ };
76
+ },
77
+ parseHTML() {
78
+ return [{ tag: 'span[data-type="glossary"]' }];
79
+ },
80
+ renderHTML({ HTMLAttributes, node }) {
81
+ return [
82
+ "span",
83
+ mergeAttributes(HTMLAttributes, {
84
+ "data-type": "glossary",
85
+ class: "glossary-node",
86
+ title: node.attrs.definition,
87
+ tabindex: "0",
88
+ }),
89
+ node.attrs.term || "",
90
+ ];
91
+ },
92
+ });
93
+
94
+ const FootnoteServer = Node.create({
95
+ name: "footnote",
96
+ group: "inline",
97
+ inline: true,
98
+ atom: true,
99
+ addAttributes() {
100
+ return { content: { default: "" } };
101
+ },
102
+ parseHTML() {
103
+ return [{ tag: 'span[data-type="footnote"]' }];
104
+ },
105
+ renderHTML({ HTMLAttributes }) {
106
+ return [
107
+ "span",
108
+ mergeAttributes(HTMLAttributes, {
109
+ "data-type": "footnote",
110
+ class: "footnote-node",
111
+ }),
112
+ ];
113
+ },
114
+ });
115
+
116
+ // ---- Stack ----
117
+
118
+ const StackColumnServer = Node.create({
119
+ name: "stackColumn",
120
+ group: "stackColumn",
121
+ content: "block+",
122
+ defining: true,
123
+ isolating: true,
124
+ parseHTML() {
125
+ return [{ tag: 'div[data-type="stack-column"]' }];
126
+ },
127
+ renderHTML({ HTMLAttributes }) {
128
+ return [
129
+ "div",
130
+ mergeAttributes(HTMLAttributes, { "data-type": "stack-column" }),
131
+ 0,
132
+ ];
133
+ },
134
+ });
135
+
136
+ const StackServer = Node.create({
137
+ name: "stack",
138
+ group: "block",
139
+ content: "stackColumn{2,4}",
140
+ defining: true,
141
+ isolating: true,
142
+ addAttributes() {
143
+ return {
144
+ layout: { default: "2-column" },
145
+ gap: { default: "medium" },
146
+ };
147
+ },
148
+ parseHTML() {
149
+ return [{ tag: 'div[data-component="stack"]' }];
150
+ },
151
+ renderHTML({ HTMLAttributes }) {
152
+ return [
153
+ "div",
154
+ mergeAttributes(HTMLAttributes, { "data-component": "stack" }),
155
+ 0,
156
+ ];
157
+ },
158
+ });
159
+
160
+ // ---- Generic wrapper/atomic components from the registry ----
161
+
162
+ interface ComponentDefLite {
163
+ name: string;
164
+ kind: "wrapper" | "atomic";
165
+ fields: { name: string; type: string; default?: unknown }[];
166
+ content?: string;
167
+ }
168
+
169
+ const COMPONENT_DEFS: ComponentDefLite[] = [
170
+ {
171
+ name: "accordion",
172
+ kind: "wrapper",
173
+ fields: [
174
+ { name: "title", type: "string", default: "Details" },
175
+ { name: "open", type: "boolean", default: false },
176
+ ],
177
+ },
178
+ {
179
+ name: "note",
180
+ kind: "wrapper",
181
+ fields: [
182
+ { name: "title", type: "string", default: "" },
183
+ { name: "emoji", type: "string", default: "" },
184
+ { name: "variant", type: "select", default: "neutral" },
185
+ ],
186
+ },
187
+ {
188
+ name: "quoteBlock",
189
+ kind: "wrapper",
190
+ fields: [
191
+ { name: "author", type: "string", default: "" },
192
+ { name: "source", type: "string", default: "" },
193
+ ],
194
+ },
195
+ { name: "wide", kind: "wrapper", fields: [] },
196
+ { name: "fullWidth", kind: "wrapper", fields: [] },
197
+ { name: "sidenote", kind: "wrapper", fields: [] },
198
+ {
199
+ name: "reference",
200
+ kind: "wrapper",
201
+ fields: [
202
+ { name: "id", type: "string", default: "" },
203
+ { name: "caption", type: "string", default: "" },
204
+ ],
205
+ },
206
+ {
207
+ name: "htmlEmbed",
208
+ kind: "atomic",
209
+ fields: [
210
+ { name: "src", type: "string", default: "" },
211
+ { name: "title", type: "string", default: "" },
212
+ { name: "desc", type: "string", default: "" },
213
+ { name: "wide", type: "boolean", default: false },
214
+ { name: "downloadable", type: "boolean", default: false },
215
+ ],
216
+ },
217
+ {
218
+ name: "hfUser",
219
+ kind: "atomic",
220
+ fields: [
221
+ { name: "username", type: "string", default: "" },
222
+ { name: "name", type: "string", default: "" },
223
+ { name: "url", type: "string", default: "" },
224
+ ],
225
+ },
226
+ {
227
+ name: "rawHtml",
228
+ kind: "atomic",
229
+ fields: [
230
+ { name: "html", type: "string", default: "" },
231
+ ],
232
+ },
233
+ ];
234
+
235
+ function makeServerWrapperExt(def: ComponentDefLite) {
236
+ return Node.create({
237
+ name: def.name,
238
+ group: "block",
239
+ content: def.content || "block+",
240
+ defining: true,
241
+ isolating: true,
242
+ addAttributes() {
243
+ const attrs: Record<string, { default: unknown }> = {};
244
+ for (const f of def.fields) attrs[f.name] = { default: f.default ?? null };
245
+ return attrs;
246
+ },
247
+ parseHTML() {
248
+ return [{ tag: `div[data-component="${def.name}"]` }];
249
+ },
250
+ renderHTML({ HTMLAttributes }) {
251
+ return [
252
+ "div",
253
+ mergeAttributes(HTMLAttributes, { "data-component": def.name }),
254
+ 0,
255
+ ];
256
+ },
257
+ });
258
+ }
259
+
260
+ function makeServerAtomicExt(def: ComponentDefLite) {
261
+ return Node.create({
262
+ name: def.name,
263
+ group: "block",
264
+ atom: true,
265
+ addAttributes() {
266
+ const attrs: Record<string, { default: unknown }> = {};
267
+ for (const f of def.fields) attrs[f.name] = { default: f.default ?? null };
268
+ return attrs;
269
+ },
270
+ parseHTML() {
271
+ return [{ tag: `div[data-component="${def.name}"]` }];
272
+ },
273
+ renderHTML({ HTMLAttributes }) {
274
+ return [
275
+ "div",
276
+ mergeAttributes(HTMLAttributes, { "data-component": def.name }),
277
+ ];
278
+ },
279
+ });
280
+ }
281
+
282
+ export function getServerExtensions(): Extensions {
283
+ const wrappers = COMPONENT_DEFS.filter((d) => d.kind === "wrapper").map(makeServerWrapperExt);
284
+ const atomics = COMPONENT_DEFS.filter((d) => d.kind === "atomic").map(makeServerAtomicExt);
285
+
286
+ return [
287
+ StarterKit.configure({
288
+ codeBlock: false,
289
+ } as any),
290
+ CodeBlockLowlight.configure({ lowlight }),
291
+ Link.configure({ openOnClick: false }),
292
+ Mathematics.configure({ katexOptions: { throwOnError: false } }),
293
+ Image.configure({ allowBase64: true }),
294
+ Table.configure({ resizable: false }),
295
+ TableRow,
296
+ TableCell,
297
+ TableHeader,
298
+ CitationServer,
299
+ BibliographyServer,
300
+ GlossaryServer,
301
+ FootnoteServer,
302
+ StackServer,
303
+ StackColumnServer,
304
+ ...wrappers,
305
+ ...atomics,
306
+ ];
307
+ }
backend/src/publisher/html-renderer.ts ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Server-side HTML renderer.
3
+ *
4
+ * Takes TipTap JSON content and generates a full static HTML page
5
+ * with SEO tags, dark/light theming, and minimal inline JS.
6
+ */
7
+
8
+ import { generateHTML } from "@tiptap/html";
9
+ import { getServerExtensions } from "./extensions.js";
10
+
11
+ export interface PublishMeta {
12
+ title: string;
13
+ description: string;
14
+ authors: string[];
15
+ date: string;
16
+ ogImage?: string;
17
+ }
18
+
19
+ /**
20
+ * Render TipTap JSON document into a complete, self-contained HTML page.
21
+ */
22
+ export function renderArticleHTML(
23
+ json: Record<string, unknown>,
24
+ meta: PublishMeta,
25
+ cssTokens: string,
26
+ cssArticle: string
27
+ ): string {
28
+ const extensions = getServerExtensions();
29
+ const bodyHtml = generateHTML(json as any, extensions);
30
+ const enrichedBody = postProcess(bodyHtml);
31
+
32
+ const authorsStr = meta.authors.join(", ");
33
+ const safeTitle = escapeHtml(meta.title);
34
+ const safeDesc = escapeHtml(meta.description);
35
+
36
+ return `<!DOCTYPE html>
37
+ <html lang="en" data-theme="dark">
38
+ <head>
39
+ <meta charset="UTF-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
41
+ <title>${safeTitle}</title>
42
+ <meta name="description" content="${safeDesc}">
43
+ <meta name="author" content="${escapeHtml(authorsStr)}">
44
+
45
+ <!-- Open Graph -->
46
+ <meta property="og:type" content="article">
47
+ <meta property="og:title" content="${safeTitle}">
48
+ <meta property="og:description" content="${safeDesc}">
49
+ ${meta.ogImage ? `<meta property="og:image" content="${escapeHtml(meta.ogImage)}">` : ""}
50
+ <meta property="article:published_time" content="${escapeHtml(meta.date)}">
51
+ ${meta.authors.map((a) => `<meta property="article:author" content="${escapeHtml(a)}">`).join("\n ")}
52
+
53
+ <!-- Twitter Card -->
54
+ <meta name="twitter:card" content="summary_large_image">
55
+ <meta name="twitter:title" content="${safeTitle}">
56
+ <meta name="twitter:description" content="${safeDesc}">
57
+ ${meta.ogImage ? `<meta name="twitter:image" content="${escapeHtml(meta.ogImage)}">` : ""}
58
+
59
+ <!-- KaTeX CSS -->
60
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"
61
+ integrity="sha384-zh0CIslj3dQfMxK3pZnPJAb3bprHitCE2vBz9TyOTICAn3fso5GYa90qPNMBclov"
62
+ crossorigin="anonymous">
63
+
64
+ <!-- highlight.js theme -->
65
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
66
+
67
+ <style>
68
+ ${cssTokens}
69
+ ${cssArticle}
70
+
71
+ /* Published article layout */
72
+ body {
73
+ margin: 0;
74
+ padding: 0;
75
+ background: var(--bg-primary);
76
+ color: var(--text-primary);
77
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
78
+ }
79
+
80
+ .article-container {
81
+ max-width: 780px;
82
+ margin: 0 auto;
83
+ padding: 2rem 32px 4rem;
84
+ }
85
+
86
+ .article-header {
87
+ margin-bottom: 2rem;
88
+ padding-bottom: 1.5rem;
89
+ border-bottom: 1px solid var(--border);
90
+ }
91
+
92
+ .article-header h1 {
93
+ font-size: 2.5rem;
94
+ font-weight: 700;
95
+ color: var(--text-heading);
96
+ line-height: 1.2;
97
+ margin: 0 0 0.5rem;
98
+ letter-spacing: -0.02em;
99
+ }
100
+
101
+ .article-header .authors {
102
+ color: var(--text-secondary);
103
+ font-size: 0.9375rem;
104
+ margin-bottom: 0.25rem;
105
+ }
106
+
107
+ .article-header .date {
108
+ color: var(--text-tertiary);
109
+ font-size: 0.8125rem;
110
+ }
111
+
112
+ /* Theme toggle */
113
+ .theme-toggle {
114
+ position: fixed;
115
+ top: 1rem;
116
+ right: 1rem;
117
+ background: var(--bg-secondary);
118
+ border: 1px solid var(--border);
119
+ border-radius: 8px;
120
+ padding: 0.5rem;
121
+ cursor: pointer;
122
+ font-size: 1.2rem;
123
+ line-height: 1;
124
+ z-index: 100;
125
+ }
126
+
127
+ /* Accordion as details/summary */
128
+ details[data-component="accordion"] {
129
+ border: 1px solid var(--border);
130
+ border-radius: 8px;
131
+ padding: 0;
132
+ margin: 1em 0;
133
+ }
134
+
135
+ details[data-component="accordion"] > summary {
136
+ padding: 0.75rem 1rem;
137
+ cursor: pointer;
138
+ font-weight: 600;
139
+ color: var(--text-heading-secondary);
140
+ list-style: none;
141
+ user-select: none;
142
+ }
143
+
144
+ details[data-component="accordion"] > summary::-webkit-details-marker {
145
+ display: none;
146
+ }
147
+
148
+ details[data-component="accordion"] > summary::before {
149
+ content: "▸";
150
+ display: inline-block;
151
+ margin-right: 0.5rem;
152
+ transition: transform 0.2s;
153
+ }
154
+
155
+ details[data-component="accordion"][open] > summary::before {
156
+ transform: rotate(90deg);
157
+ }
158
+
159
+ details[data-component="accordion"] > .accordion-content {
160
+ padding: 0 1rem 1rem;
161
+ }
162
+
163
+ /* Image lightbox dialog */
164
+ dialog.lightbox {
165
+ border: none;
166
+ background: transparent;
167
+ padding: 0;
168
+ max-width: 95vw;
169
+ max-height: 95vh;
170
+ }
171
+
172
+ dialog.lightbox::backdrop {
173
+ background: rgba(0, 0, 0, 0.85);
174
+ }
175
+
176
+ dialog.lightbox img {
177
+ max-width: 95vw;
178
+ max-height: 90vh;
179
+ object-fit: contain;
180
+ border-radius: 4px;
181
+ }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <button class="theme-toggle" aria-label="Toggle theme" onclick="toggleTheme()">🌓</button>
186
+
187
+ <article class="article-container">
188
+ <header class="article-header">
189
+ <h1>${safeTitle}</h1>
190
+ ${authorsStr ? `<p class="authors">${escapeHtml(authorsStr)}</p>` : ""}
191
+ ${meta.date ? `<p class="date">${escapeHtml(formatDate(meta.date))}</p>` : ""}
192
+ </header>
193
+
194
+ <div class="tiptap">
195
+ ${enrichedBody}
196
+ </div>
197
+ </article>
198
+
199
+ <!-- Image lightbox -->
200
+ <dialog class="lightbox" id="lightbox">
201
+ <img id="lightbox-img" src="" alt="">
202
+ </dialog>
203
+
204
+ <script>
205
+ // Theme toggle
206
+ function toggleTheme() {
207
+ const html = document.documentElement;
208
+ const current = html.getAttribute('data-theme');
209
+ html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
210
+ }
211
+
212
+ // Image lightbox
213
+ document.querySelectorAll('.tiptap img').forEach(function(img) {
214
+ img.style.cursor = 'zoom-in';
215
+ img.addEventListener('click', function() {
216
+ var d = document.getElementById('lightbox');
217
+ document.getElementById('lightbox-img').src = img.src;
218
+ d.showModal();
219
+ });
220
+ });
221
+ document.getElementById('lightbox').addEventListener('click', function(e) {
222
+ if (e.target === this) this.close();
223
+ });
224
+
225
+ // Glossary tooltips
226
+ document.querySelectorAll('[data-type="glossary"]').forEach(function(el) {
227
+ el.setAttribute('tabindex', '0');
228
+ });
229
+ </script>
230
+ </body>
231
+ </html>`;
232
+ }
233
+
234
+ /**
235
+ * Post-process the raw generateHTML output:
236
+ * - Transform accordion divs into <details><summary>
237
+ * - Transform htmlEmbed into iframes
238
+ * - Add semantic structure where possible
239
+ */
240
+ function postProcess(html: string): string {
241
+ let result = html;
242
+
243
+ // Accordion: div[data-component="accordion"] → <details><summary>
244
+ result = result.replace(
245
+ /<div[^>]*data-component="accordion"[^>]*>([\s\S]*?)<\/div>/g,
246
+ (_match, inner) => {
247
+ const titleMatch = inner.match(/data-title="([^"]*)"/);
248
+ const openMatch = inner.match(/data-open="true"/);
249
+ const title = titleMatch ? titleMatch[1] : "Details";
250
+ return `<details data-component="accordion"${openMatch ? " open" : ""}>
251
+ <summary>${escapeHtml(title)}</summary>
252
+ <div class="accordion-content">${inner}</div>
253
+ </details>`;
254
+ }
255
+ );
256
+
257
+ // HtmlEmbed: div[data-component="htmlEmbed"] → iframe with srcdoc
258
+ result = result.replace(
259
+ /<div[^>]*data-component="htmlEmbed"[^>]*data-src="([^"]*)"[^>]*><\/div>/g,
260
+ (_match, src) => {
261
+ return `<div class="html-embed-container">
262
+ <iframe srcdoc="" data-embed-src="${escapeHtml(src)}"
263
+ style="width:100%;border:none;min-height:400px;"
264
+ loading="lazy" sandbox="allow-scripts"></iframe>
265
+ </div>`;
266
+ }
267
+ );
268
+
269
+ return result;
270
+ }
271
+
272
+ function escapeHtml(str: string): string {
273
+ return str
274
+ .replace(/&/g, "&amp;")
275
+ .replace(/</g, "&lt;")
276
+ .replace(/>/g, "&gt;")
277
+ .replace(/"/g, "&quot;")
278
+ .replace(/'/g, "&#039;");
279
+ }
280
+
281
+ function formatDate(dateStr: string): string {
282
+ try {
283
+ return new Date(dateStr).toLocaleDateString("en-US", {
284
+ year: "numeric",
285
+ month: "long",
286
+ day: "numeric",
287
+ });
288
+ } catch {
289
+ return dateStr;
290
+ }
291
+ }
backend/src/publisher/index.ts ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Publish pipeline entry point.
3
+ *
4
+ * Orchestrates: Y.Doc → JSON → HTML → PDF/Thumbnail → Upload to HF
5
+ */
6
+
7
+ import { readFileSync, existsSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import * as Y from "yjs";
11
+ import { renderArticleHTML, type PublishMeta } from "./html-renderer.js";
12
+ import { isPdfEnabled, generatePdfAndThumbnail } from "./pdf-generator.js";
13
+ import {
14
+ isHfStorageEnabled,
15
+ uploadPublishedAssets,
16
+ } from "../hf-storage.js";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const DATA_DIR = join(__dirname, "..", "..", "data");
20
+
21
+ function docPath(name: string) {
22
+ return join(DATA_DIR, `${name.replace(/[^a-zA-Z0-9_-]/g, "_")}.yjs`);
23
+ }
24
+
25
+ /**
26
+ * Load CSS files for injection into the published HTML.
27
+ */
28
+ function loadCSS(): { tokens: string; article: string } {
29
+ const frontendStyles = join(__dirname, "..", "..", "..", "frontend", "src", "styles");
30
+ const distStyles = join(__dirname, "..", "frontend-dist", "assets");
31
+
32
+ let tokens = "";
33
+ let article = "";
34
+
35
+ // Development: read source files directly
36
+ const tokensPath = join(frontendStyles, "tokens.css");
37
+ const articlePath = join(frontendStyles, "article.css");
38
+
39
+ if (existsSync(tokensPath)) {
40
+ tokens = readFileSync(tokensPath, "utf-8");
41
+ article = readFileSync(articlePath, "utf-8");
42
+ }
43
+
44
+ return { tokens, article };
45
+ }
46
+
47
+ /**
48
+ * Extract TipTap JSON and frontmatter from a Yjs document.
49
+ */
50
+ function extractFromYDoc(ydoc: Y.Doc): {
51
+ json: Record<string, unknown>;
52
+ frontmatter: Record<string, unknown>;
53
+ } {
54
+ const fragment = ydoc.getXmlFragment("default");
55
+
56
+ // The Hocuspocus/TipTap collaboration stores the ProseMirror document
57
+ // inside a Y.XmlFragment. We need to extract it as JSON.
58
+ // For now, we'll try reading the JSON from the shared map if available.
59
+ const metaMap = ydoc.getMap("frontmatter");
60
+ const frontmatter: Record<string, unknown> = {};
61
+
62
+ metaMap.forEach((value, key) => {
63
+ frontmatter[key] = value;
64
+ });
65
+
66
+ // TipTap JSON is stored as the XmlFragment's prosemirror representation
67
+ // We need to reconstruct it. The simplest approach: store the JSON
68
+ // in a shared type when the editor saves.
69
+ const jsonMap = ydoc.getMap("tiptap-json");
70
+ const stored = jsonMap.get("doc");
71
+
72
+ if (stored && typeof stored === "object") {
73
+ return { json: stored as Record<string, unknown>, frontmatter };
74
+ }
75
+
76
+ // Fallback: return empty doc
77
+ return {
78
+ json: { type: "doc", content: [{ type: "paragraph" }] },
79
+ frontmatter,
80
+ };
81
+ }
82
+
83
+ export interface PublishResult {
84
+ htmlUrl: string | null;
85
+ pdfUrl: string | null;
86
+ thumbUrl: string | null;
87
+ success: boolean;
88
+ error?: string;
89
+ }
90
+
91
+ /**
92
+ * Run the full publish pipeline for a document.
93
+ */
94
+ export async function publishDocument(docName: string): Promise<PublishResult> {
95
+ const p = docPath(docName);
96
+
97
+ if (!existsSync(p)) {
98
+ return { htmlUrl: null, pdfUrl: null, thumbUrl: null, success: false, error: "Document not found" };
99
+ }
100
+
101
+ // Load Yjs document
102
+ const ydoc = new Y.Doc();
103
+ const data = readFileSync(p);
104
+ Y.applyUpdate(ydoc, new Uint8Array(data));
105
+
106
+ // Extract content
107
+ const { json, frontmatter } = extractFromYDoc(ydoc);
108
+
109
+ // Build metadata
110
+ const meta: PublishMeta = {
111
+ title: (frontmatter.title as string) || docName,
112
+ description: (frontmatter.description as string) || "",
113
+ authors: Array.isArray(frontmatter.authors)
114
+ ? (frontmatter.authors as string[])
115
+ : [],
116
+ date: (frontmatter.date as string) || new Date().toISOString(),
117
+ };
118
+
119
+ // Load CSS
120
+ const css = loadCSS();
121
+
122
+ // Generate HTML
123
+ const html = renderArticleHTML(json, meta, css.tokens, css.article);
124
+
125
+ // Generate PDF + thumbnail
126
+ let pdf: Buffer | null = null;
127
+ let thumbnail: Buffer | null = null;
128
+
129
+ if (isPdfEnabled()) {
130
+ try {
131
+ const assets = await generatePdfAndThumbnail(html);
132
+ pdf = assets.pdf;
133
+ thumbnail = assets.thumbnail;
134
+ } catch (err) {
135
+ console.error("[publish] PDF generation failed:", err);
136
+ }
137
+ }
138
+
139
+ // Upload to HF dataset
140
+ if (isHfStorageEnabled()) {
141
+ try {
142
+ const urls = await uploadPublishedAssets(docName, {
143
+ html,
144
+ pdf,
145
+ thumbnail,
146
+ meta: meta as any,
147
+ });
148
+ return { ...urls, success: true };
149
+ } catch (err) {
150
+ console.error("[publish] Upload failed:", err);
151
+ return {
152
+ htmlUrl: null,
153
+ pdfUrl: null,
154
+ thumbUrl: null,
155
+ success: false,
156
+ error: (err as Error).message,
157
+ };
158
+ }
159
+ }
160
+
161
+ // Local-only: save to data/published/
162
+ const { writeFileSync, mkdirSync } = await import("fs");
163
+ const publishDir = join(DATA_DIR, "published", docName.replace(/[^a-zA-Z0-9_-]/g, "_"));
164
+ mkdirSync(publishDir, { recursive: true });
165
+ writeFileSync(join(publishDir, "index.html"), html);
166
+ if (pdf) writeFileSync(join(publishDir, "article.pdf"), pdf);
167
+ if (thumbnail) writeFileSync(join(publishDir, "thumb.jpg"), thumbnail);
168
+ writeFileSync(join(publishDir, "meta.json"), JSON.stringify(meta, null, 2));
169
+
170
+ return {
171
+ htmlUrl: `/published/${docName}/index.html`,
172
+ pdfUrl: pdf ? `/published/${docName}/article.pdf` : null,
173
+ thumbUrl: thumbnail ? `/published/${docName}/thumb.jpg` : null,
174
+ success: true,
175
+ };
176
+ }
backend/src/publisher/pdf-generator.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * PDF and OG thumbnail generation via Playwright.
3
+ *
4
+ * Launches headless Chromium, loads the published HTML,
5
+ * waits for rendering, then captures PDF + screenshot.
6
+ */
7
+
8
+ let playwrightAvailable = false;
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ let chromium: any;
11
+
12
+ try {
13
+ // Dynamic import: playwright is optional (only in Docker production image)
14
+ const pw = await import(/* @vite-ignore */ "playwright");
15
+ chromium = pw.chromium;
16
+ playwrightAvailable = true;
17
+ } catch {
18
+ console.warn("[publisher] Playwright not installed, PDF/thumbnail generation disabled");
19
+ }
20
+
21
+ export function isPdfEnabled(): boolean {
22
+ return playwrightAvailable && process.env.ENABLE_PDF !== "false";
23
+ }
24
+
25
+ export interface GeneratedAssets {
26
+ pdf: Buffer | null;
27
+ thumbnail: Buffer | null;
28
+ }
29
+
30
+ /**
31
+ * Generate PDF and OG thumbnail from HTML string.
32
+ */
33
+ export async function generatePdfAndThumbnail(
34
+ html: string
35
+ ): Promise<GeneratedAssets> {
36
+ if (!playwrightAvailable) {
37
+ return { pdf: null, thumbnail: null };
38
+ }
39
+
40
+ const browser = await chromium.launch({
41
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
42
+ });
43
+
44
+ try {
45
+ const page = await browser.newPage();
46
+
47
+ await page.setContent(html, { waitUntil: "networkidle" });
48
+
49
+ // Wait a bit for fonts, KaTeX, images
50
+ await page.waitForTimeout(2000);
51
+
52
+ // OG Thumbnail (1200x630)
53
+ await page.setViewportSize({ width: 1200, height: 630 });
54
+ const thumbnail = await page.screenshot({
55
+ type: "jpeg",
56
+ quality: 85,
57
+ clip: { x: 0, y: 0, width: 1200, height: 630 },
58
+ });
59
+
60
+ // PDF
61
+ const pdf = await page.pdf({
62
+ format: "A4",
63
+ printBackground: true,
64
+ margin: { top: "1cm", bottom: "1cm", left: "1.5cm", right: "1.5cm" },
65
+ });
66
+
67
+ return {
68
+ pdf: Buffer.from(pdf),
69
+ thumbnail: Buffer.from(thumbnail),
70
+ };
71
+ } finally {
72
+ await browser.close();
73
+ }
74
+ }
backend/src/server.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
  flushAll,
19
  } from "./hf-storage.js";
20
  import { resolveUser, extractToken, isOAuthEnabled } from "./auth.js";
 
21
 
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
  const DATA_DIR = join(__dirname, "..", "data");
@@ -137,6 +138,38 @@ app.post("/api/chat", handleChat);
137
 
138
  app.use("/api/citations", citationsRouter);
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  // ---------- Image Upload ----------
141
 
142
  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
@@ -172,6 +205,48 @@ app.post("/api/upload", upload.single("file"), async (req, res) => {
172
  // Serve locally uploaded images
173
  app.use("/uploads", express.static(join(DATA_DIR, "uploads")));
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  // ---------- Static frontend ----------
176
 
177
  const staticDir =
@@ -181,7 +256,46 @@ const staticDir =
181
 
182
  if (existsSync(staticDir)) {
183
  app.use(express.static(staticDir));
184
- app.get("*", (_req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  res.sendFile(join(staticDir, "index.html"));
186
  });
187
  }
 
18
  flushAll,
19
  } from "./hf-storage.js";
20
  import { resolveUser, extractToken, isOAuthEnabled } from "./auth.js";
21
+ import { publishDocument } from "./publisher/index.js";
22
 
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
  const DATA_DIR = join(__dirname, "..", "data");
 
138
 
139
  app.use("/api/citations", citationsRouter);
140
 
141
+ // ---------- Publish ----------
142
+
143
+ app.post("/api/publish", async (req, res) => {
144
+ // Auth check: only editors can publish
145
+ if (oauthEnabled) {
146
+ const token = extractToken(req.headers.cookie);
147
+ const user = await resolveUser(token);
148
+ if (!user || !user.canEdit) {
149
+ res.status(403).json({ error: "Unauthorized: write access required" });
150
+ return;
151
+ }
152
+ }
153
+
154
+ const { docName } = req.body as { docName?: string };
155
+ if (!docName) {
156
+ res.status(400).json({ error: "docName is required" });
157
+ return;
158
+ }
159
+
160
+ try {
161
+ const result = await publishDocument(docName);
162
+ if (!result.success) {
163
+ res.status(500).json({ error: result.error || "Publish failed" });
164
+ return;
165
+ }
166
+ res.json(result);
167
+ } catch (err: any) {
168
+ console.error("[publish] error:", err);
169
+ res.status(500).json({ error: err.message || "Publish failed" });
170
+ }
171
+ });
172
+
173
  // ---------- Image Upload ----------
174
 
175
  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
 
205
  // Serve locally uploaded images
206
  app.use("/uploads", express.static(join(DATA_DIR, "uploads")));
207
 
208
+ // Serve locally published articles
209
+ app.use("/published", express.static(join(DATA_DIR, "published")));
210
+
211
+ // ---------- Image proxy for private datasets ----------
212
+
213
+ app.get("/api/images/*", async (req, res) => {
214
+ const imagePath = (req.params as any)[0] as string;
215
+ if (!imagePath || imagePath.includes("..")) {
216
+ res.status(400).send("Invalid path");
217
+ return;
218
+ }
219
+
220
+ const HF_DATASET_ID = process.env.HF_DATASET_ID || "";
221
+ const HF_TOKEN = process.env.HF_TOKEN || "";
222
+
223
+ if (!HF_DATASET_ID || !HF_TOKEN) {
224
+ res.status(404).send("Not configured");
225
+ return;
226
+ }
227
+
228
+ try {
229
+ const url = `https://huggingface.co/datasets/${HF_DATASET_ID}/resolve/main/images/${imagePath}`;
230
+ const response = await fetch(url, {
231
+ headers: { Authorization: `Bearer ${HF_TOKEN}` },
232
+ });
233
+
234
+ if (!response.ok) {
235
+ res.status(response.status).send("Not found");
236
+ return;
237
+ }
238
+
239
+ const contentType = response.headers.get("content-type");
240
+ if (contentType) res.setHeader("Content-Type", contentType);
241
+ res.setHeader("Cache-Control", "public, max-age=86400");
242
+
243
+ const buffer = Buffer.from(await response.arrayBuffer());
244
+ res.send(buffer);
245
+ } catch {
246
+ res.status(500).send("Proxy error");
247
+ }
248
+ });
249
+
250
  // ---------- Static frontend ----------
251
 
252
  const staticDir =
 
256
 
257
  if (existsSync(staticDir)) {
258
  app.use(express.static(staticDir));
259
+
260
+ app.get("*", async (req, res) => {
261
+ // If OAuth is enabled, serve published HTML to unauthenticated visitors
262
+ if (oauthEnabled) {
263
+ const token = extractToken(req.headers.cookie);
264
+ const user = await resolveUser(token);
265
+
266
+ if (!user || !user.canEdit) {
267
+ // Try to serve published version
268
+ const docMatch = req.query.doc as string | undefined;
269
+ const docName = docMatch || "default";
270
+ const safeName = docName.replace(/[^a-zA-Z0-9_-]/g, "_");
271
+ const publishedPath = join(DATA_DIR, "published", safeName, "index.html");
272
+
273
+ if (existsSync(publishedPath)) {
274
+ res.sendFile(publishedPath);
275
+ return;
276
+ }
277
+
278
+ // No published version: show a simple message
279
+ if (!user) {
280
+ res.status(200).send(`
281
+ <!DOCTYPE html>
282
+ <html>
283
+ <head><title>Collab Editor</title></head>
284
+ <body style="display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:system-ui;background:#0a0a0a;color:#aaa;">
285
+ <div style="text-align:center;">
286
+ <h1 style="color:#fff;">This article is not yet published</h1>
287
+ <p>Log in with your Hugging Face account to edit.</p>
288
+ <a href="/oauth/authorize" style="color:#958DF1;">Log in</a>
289
+ </div>
290
+ </body>
291
+ </html>
292
+ `);
293
+ return;
294
+ }
295
+ }
296
+ }
297
+
298
+ // Authenticated editors get the SPA
299
  res.sendFile(join(staticDir, "index.html"));
300
  });
301
  }
backend/src/types.d.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module "@citation-js/core";
2
+ declare module "@citation-js/plugin-bibtex";
3
+ declare module "@citation-js/plugin-csl";
4
+ declare module "@citation-js/plugin-doi";
5
+
6
+ // Playwright is optional - only available in Docker production image
7
+ declare module "playwright" {
8
+ export const chromium: {
9
+ launch(options?: Record<string, unknown>): Promise<any>;
10
+ };
11
+ }