src
webapp
app
components
features
reader
components
ReaderButton
ReaderHeaderButton
lib
lib
···
113
113
"lru-cache": "^10.4.3"
114
114
}
115
115
},
116
116
+
"node_modules/@asamuzakjp/dom-selector": {
117
117
+
"version": "7.1.1",
118
118
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
119
119
+
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
120
120
+
"license": "MIT",
121
121
+
"dependencies": {
122
122
+
"@asamuzakjp/generational-cache": "^1.0.1",
123
123
+
"@asamuzakjp/nwsapi": "^2.3.9",
124
124
+
"bidi-js": "^1.0.3",
125
125
+
"css-tree": "^3.2.1",
126
126
+
"is-potential-custom-element-name": "^1.0.1"
127
127
+
},
128
128
+
"engines": {
129
129
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
130
130
+
}
131
131
+
},
132
132
+
"node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": {
133
133
+
"version": "3.2.1",
134
134
+
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
135
135
+
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
136
136
+
"license": "MIT",
137
137
+
"dependencies": {
138
138
+
"mdn-data": "2.27.1",
139
139
+
"source-map-js": "^1.2.1"
140
140
+
},
141
141
+
"engines": {
142
142
+
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
143
143
+
}
144
144
+
},
145
145
+
"node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": {
146
146
+
"version": "2.27.1",
147
147
+
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
148
148
+
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
149
149
+
"license": "CC0-1.0"
150
150
+
},
151
151
+
"node_modules/@asamuzakjp/generational-cache": {
152
152
+
"version": "1.0.1",
153
153
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
154
154
+
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
155
155
+
"license": "MIT",
156
156
+
"engines": {
157
157
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
158
158
+
}
159
159
+
},
160
160
+
"node_modules/@asamuzakjp/nwsapi": {
161
161
+
"version": "2.3.9",
162
162
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
163
163
+
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
164
164
+
"license": "MIT"
165
165
+
},
116
166
"node_modules/@atproto-labs/did-resolver": {
117
167
"version": "0.1.13",
118
168
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.13.tgz",
···
1616
1666
"dev": true,
1617
1667
"license": "MIT"
1618
1668
},
1669
1669
+
"node_modules/@bramus/specificity": {
1670
1670
+
"version": "2.4.2",
1671
1671
+
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
1672
1672
+
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
1673
1673
+
"license": "MIT",
1674
1674
+
"dependencies": {
1675
1675
+
"css-tree": "^3.0.0"
1676
1676
+
},
1677
1677
+
"bin": {
1678
1678
+
"specificity": "bin/cli.js"
1679
1679
+
}
1680
1680
+
},
1681
1681
+
"node_modules/@bramus/specificity/node_modules/css-tree": {
1682
1682
+
"version": "3.2.1",
1683
1683
+
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
1684
1684
+
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
1685
1685
+
"license": "MIT",
1686
1686
+
"dependencies": {
1687
1687
+
"mdn-data": "2.27.1",
1688
1688
+
"source-map-js": "^1.2.1"
1689
1689
+
},
1690
1690
+
"engines": {
1691
1691
+
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
1692
1692
+
}
1693
1693
+
},
1694
1694
+
"node_modules/@bramus/specificity/node_modules/mdn-data": {
1695
1695
+
"version": "2.27.1",
1696
1696
+
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
1697
1697
+
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
1698
1698
+
"license": "CC0-1.0"
1699
1699
+
},
1619
1700
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
1620
1701
"version": "2.2.0",
1621
1702
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
···
1830
1911
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
1831
1912
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
1832
1913
"dev": true
1914
1914
+
},
1915
1915
+
"node_modules/@emnapi/wasi-threads": {
1916
1916
+
"version": "1.2.1",
1917
1917
+
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
1918
1918
+
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
1919
1919
+
"dev": true,
1920
1920
+
"license": "MIT",
1921
1921
+
"optional": true,
1922
1922
+
"dependencies": {
1923
1923
+
"tslib": "^2.4.0"
1924
1924
+
}
1833
1925
},
1834
1926
"node_modules/@esbuild-kit/core-utils": {
1835
1927
"version": "3.3.2",
···
2870
2962
},
2871
2963
"engines": {
2872
2964
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2965
2965
+
}
2966
2966
+
},
2967
2967
+
"node_modules/@exodus/bytes": {
2968
2968
+
"version": "1.15.0",
2969
2969
+
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
2970
2970
+
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
2971
2971
+
"license": "MIT",
2972
2972
+
"engines": {
2973
2973
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
2974
2974
+
},
2975
2975
+
"peerDependencies": {
2976
2976
+
"@noble/hashes": "^1.8.0 || ^2.0.0"
2977
2977
+
},
2978
2978
+
"peerDependenciesMeta": {
2979
2979
+
"@noble/hashes": {
2980
2980
+
"optional": true
2981
2981
+
}
2873
2982
}
2874
2983
},
2875
2984
"node_modules/@expo/spawn-async": {
···
4760
4869
"node": ">=12.0.0"
4761
4870
}
4762
4871
},
4872
4872
+
"node_modules/@mozilla/readability": {
4873
4873
+
"version": "0.6.0",
4874
4874
+
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
4875
4875
+
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
4876
4876
+
"license": "Apache-2.0",
4877
4877
+
"engines": {
4878
4878
+
"node": ">=14.0.0"
4879
4879
+
}
4880
4880
+
},
4763
4881
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
4764
4882
"version": "3.0.3",
4765
4883
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
···
5065
5183
"version": "1.8.0",
5066
5184
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
5067
5185
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
5186
5186
+
"peer": true,
5068
5187
"engines": {
5069
5188
"node": "^14.21.3 || >=16"
5070
5189
},
···
12129
12248
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
12130
12249
"dev": true
12131
12250
},
12251
12251
+
"node_modules/@types/mozilla__readability": {
12252
12252
+
"version": "0.4.2",
12253
12253
+
"resolved": "https://registry.npmjs.org/@types/mozilla__readability/-/mozilla__readability-0.4.2.tgz",
12254
12254
+
"integrity": "sha512-mXHoZZ/Knps7GABAG9D512yDLCMQNOvQBqzIZpFJnRJveHc528rltnXh6EZTi1RiB1I2Z8yV35Dzs6DhE2WaYw==",
12255
12255
+
"dev": true,
12256
12256
+
"license": "MIT"
12257
12257
+
},
12132
12258
"node_modules/@types/ms": {
12133
12259
"version": "2.1.0",
12134
12260
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
···
12330
12456
"version": "2.0.7",
12331
12457
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
12332
12458
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
12333
12333
-
"dev": true
12459
12459
+
"devOptional": true
12334
12460
},
12335
12461
"node_modules/@types/unist": {
12336
12462
"version": "3.0.3",
···
14352
14478
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
14353
14479
}
14354
14480
},
14481
14481
+
"node_modules/bidi-js": {
14482
14482
+
"version": "1.0.3",
14483
14483
+
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
14484
14484
+
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
14485
14485
+
"license": "MIT",
14486
14486
+
"dependencies": {
14487
14487
+
"require-from-string": "^2.0.2"
14488
14488
+
}
14489
14489
+
},
14355
14490
"node_modules/binary-extensions": {
14356
14491
"version": "2.3.0",
14357
14492
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
···
16231
16366
"node_modules/decimal.js": {
16232
16367
"version": "10.6.0",
16233
16368
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
16234
16234
-
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
16235
16235
-
"dev": true
16369
16369
+
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="
16236
16370
},
16237
16371
"node_modules/decode-named-character-reference": {
16238
16372
"version": "1.2.0",
···
16733
16867
}
16734
16868
},
16735
16869
"node_modules/dompurify": {
16736
16736
-
"version": "3.3.1",
16737
16737
-
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
16738
16738
-
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
16739
16739
-
"dev": true,
16870
16870
+
"version": "3.4.2",
16871
16871
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
16872
16872
+
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
16873
16873
+
"license": "(MPL-2.0 OR Apache-2.0)",
16740
16874
"optionalDependencies": {
16741
16875
"@types/trusted-types": "^2.0.7"
16742
16876
}
···
20689
20823
"node_modules/is-potential-custom-element-name": {
20690
20824
"version": "1.0.1",
20691
20825
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
20692
20692
-
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
20693
20693
-
"dev": true
20826
20826
+
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
20694
20827
},
20695
20828
"node_modules/is-promise": {
20696
20829
"version": "4.0.0",
···
20905
21038
"resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz",
20906
21039
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
20907
21040
},
21041
21041
+
"node_modules/isomorphic-dompurify": {
21042
21042
+
"version": "3.11.0",
21043
21043
+
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.11.0.tgz",
21044
21044
+
"integrity": "sha512-il0sNhLnfawc6vKoMqnZX1JXzEzw0pP3DZWg7mM7VJNJ8cq6DCxeAjEcfjTazyBF8dkUn+rBsBPS5NQs4ZaI3g==",
21045
21045
+
"license": "MIT",
21046
21046
+
"dependencies": {
21047
21047
+
"dompurify": "^3.4.1",
21048
21048
+
"jsdom": "^29.1.0"
21049
21049
+
},
21050
21050
+
"engines": {
21051
21051
+
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
21052
21052
+
}
21053
21053
+
},
21054
21054
+
"node_modules/isomorphic-dompurify/node_modules/@asamuzakjp/css-color": {
21055
21055
+
"version": "5.1.11",
21056
21056
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
21057
21057
+
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
21058
21058
+
"license": "MIT",
21059
21059
+
"dependencies": {
21060
21060
+
"@asamuzakjp/generational-cache": "^1.0.1",
21061
21061
+
"@csstools/css-calc": "^3.2.0",
21062
21062
+
"@csstools/css-color-parser": "^4.1.0",
21063
21063
+
"@csstools/css-parser-algorithms": "^4.0.0",
21064
21064
+
"@csstools/css-tokenizer": "^4.0.0"
21065
21065
+
},
21066
21066
+
"engines": {
21067
21067
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
21068
21068
+
}
21069
21069
+
},
21070
21070
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/color-helpers": {
21071
21071
+
"version": "6.0.2",
21072
21072
+
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
21073
21073
+
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
21074
21074
+
"funding": [
21075
21075
+
{
21076
21076
+
"type": "github",
21077
21077
+
"url": "https://github.com/sponsors/csstools"
21078
21078
+
},
21079
21079
+
{
21080
21080
+
"type": "opencollective",
21081
21081
+
"url": "https://opencollective.com/csstools"
21082
21082
+
}
21083
21083
+
],
21084
21084
+
"license": "MIT-0",
21085
21085
+
"engines": {
21086
21086
+
"node": ">=20.19.0"
21087
21087
+
}
21088
21088
+
},
21089
21089
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/css-calc": {
21090
21090
+
"version": "3.2.0",
21091
21091
+
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz",
21092
21092
+
"integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==",
21093
21093
+
"funding": [
21094
21094
+
{
21095
21095
+
"type": "github",
21096
21096
+
"url": "https://github.com/sponsors/csstools"
21097
21097
+
},
21098
21098
+
{
21099
21099
+
"type": "opencollective",
21100
21100
+
"url": "https://opencollective.com/csstools"
21101
21101
+
}
21102
21102
+
],
21103
21103
+
"license": "MIT",
21104
21104
+
"engines": {
21105
21105
+
"node": ">=20.19.0"
21106
21106
+
},
21107
21107
+
"peerDependencies": {
21108
21108
+
"@csstools/css-parser-algorithms": "^4.0.0",
21109
21109
+
"@csstools/css-tokenizer": "^4.0.0"
21110
21110
+
}
21111
21111
+
},
21112
21112
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/css-color-parser": {
21113
21113
+
"version": "4.1.0",
21114
21114
+
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz",
21115
21115
+
"integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==",
21116
21116
+
"funding": [
21117
21117
+
{
21118
21118
+
"type": "github",
21119
21119
+
"url": "https://github.com/sponsors/csstools"
21120
21120
+
},
21121
21121
+
{
21122
21122
+
"type": "opencollective",
21123
21123
+
"url": "https://opencollective.com/csstools"
21124
21124
+
}
21125
21125
+
],
21126
21126
+
"license": "MIT",
21127
21127
+
"dependencies": {
21128
21128
+
"@csstools/color-helpers": "^6.0.2",
21129
21129
+
"@csstools/css-calc": "^3.2.0"
21130
21130
+
},
21131
21131
+
"engines": {
21132
21132
+
"node": ">=20.19.0"
21133
21133
+
},
21134
21134
+
"peerDependencies": {
21135
21135
+
"@csstools/css-parser-algorithms": "^4.0.0",
21136
21136
+
"@csstools/css-tokenizer": "^4.0.0"
21137
21137
+
}
21138
21138
+
},
21139
21139
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/css-parser-algorithms": {
21140
21140
+
"version": "4.0.0",
21141
21141
+
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
21142
21142
+
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
21143
21143
+
"funding": [
21144
21144
+
{
21145
21145
+
"type": "github",
21146
21146
+
"url": "https://github.com/sponsors/csstools"
21147
21147
+
},
21148
21148
+
{
21149
21149
+
"type": "opencollective",
21150
21150
+
"url": "https://opencollective.com/csstools"
21151
21151
+
}
21152
21152
+
],
21153
21153
+
"license": "MIT",
21154
21154
+
"peer": true,
21155
21155
+
"engines": {
21156
21156
+
"node": ">=20.19.0"
21157
21157
+
},
21158
21158
+
"peerDependencies": {
21159
21159
+
"@csstools/css-tokenizer": "^4.0.0"
21160
21160
+
}
21161
21161
+
},
21162
21162
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/css-syntax-patches-for-csstree": {
21163
21163
+
"version": "1.1.3",
21164
21164
+
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz",
21165
21165
+
"integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==",
21166
21166
+
"funding": [
21167
21167
+
{
21168
21168
+
"type": "github",
21169
21169
+
"url": "https://github.com/sponsors/csstools"
21170
21170
+
},
21171
21171
+
{
21172
21172
+
"type": "opencollective",
21173
21173
+
"url": "https://opencollective.com/csstools"
21174
21174
+
}
21175
21175
+
],
21176
21176
+
"license": "MIT-0",
21177
21177
+
"peerDependencies": {
21178
21178
+
"css-tree": "^3.2.1"
21179
21179
+
},
21180
21180
+
"peerDependenciesMeta": {
21181
21181
+
"css-tree": {
21182
21182
+
"optional": true
21183
21183
+
}
21184
21184
+
}
21185
21185
+
},
21186
21186
+
"node_modules/isomorphic-dompurify/node_modules/@csstools/css-tokenizer": {
21187
21187
+
"version": "4.0.0",
21188
21188
+
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
21189
21189
+
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
21190
21190
+
"funding": [
21191
21191
+
{
21192
21192
+
"type": "github",
21193
21193
+
"url": "https://github.com/sponsors/csstools"
21194
21194
+
},
21195
21195
+
{
21196
21196
+
"type": "opencollective",
21197
21197
+
"url": "https://opencollective.com/csstools"
21198
21198
+
}
21199
21199
+
],
21200
21200
+
"license": "MIT",
21201
21201
+
"peer": true,
21202
21202
+
"engines": {
21203
21203
+
"node": ">=20.19.0"
21204
21204
+
}
21205
21205
+
},
21206
21206
+
"node_modules/isomorphic-dompurify/node_modules/css-tree": {
21207
21207
+
"version": "3.2.1",
21208
21208
+
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
21209
21209
+
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
21210
21210
+
"license": "MIT",
21211
21211
+
"peer": true,
21212
21212
+
"dependencies": {
21213
21213
+
"mdn-data": "2.27.1",
21214
21214
+
"source-map-js": "^1.2.1"
21215
21215
+
},
21216
21216
+
"engines": {
21217
21217
+
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
21218
21218
+
}
21219
21219
+
},
21220
21220
+
"node_modules/isomorphic-dompurify/node_modules/data-urls": {
21221
21221
+
"version": "7.0.0",
21222
21222
+
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
21223
21223
+
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
21224
21224
+
"license": "MIT",
21225
21225
+
"dependencies": {
21226
21226
+
"whatwg-mimetype": "^5.0.0",
21227
21227
+
"whatwg-url": "^16.0.0"
21228
21228
+
},
21229
21229
+
"engines": {
21230
21230
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
21231
21231
+
}
21232
21232
+
},
21233
21233
+
"node_modules/isomorphic-dompurify/node_modules/entities": {
21234
21234
+
"version": "8.0.0",
21235
21235
+
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
21236
21236
+
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
21237
21237
+
"license": "BSD-2-Clause",
21238
21238
+
"engines": {
21239
21239
+
"node": ">=20.19.0"
21240
21240
+
},
21241
21241
+
"funding": {
21242
21242
+
"url": "https://github.com/fb55/entities?sponsor=1"
21243
21243
+
}
21244
21244
+
},
21245
21245
+
"node_modules/isomorphic-dompurify/node_modules/html-encoding-sniffer": {
21246
21246
+
"version": "6.0.0",
21247
21247
+
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
21248
21248
+
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
21249
21249
+
"license": "MIT",
21250
21250
+
"dependencies": {
21251
21251
+
"@exodus/bytes": "^1.6.0"
21252
21252
+
},
21253
21253
+
"engines": {
21254
21254
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
21255
21255
+
}
21256
21256
+
},
21257
21257
+
"node_modules/isomorphic-dompurify/node_modules/jsdom": {
21258
21258
+
"version": "29.1.1",
21259
21259
+
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
21260
21260
+
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
21261
21261
+
"license": "MIT",
21262
21262
+
"dependencies": {
21263
21263
+
"@asamuzakjp/css-color": "^5.1.11",
21264
21264
+
"@asamuzakjp/dom-selector": "^7.1.1",
21265
21265
+
"@bramus/specificity": "^2.4.2",
21266
21266
+
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
21267
21267
+
"@exodus/bytes": "^1.15.0",
21268
21268
+
"css-tree": "^3.2.1",
21269
21269
+
"data-urls": "^7.0.0",
21270
21270
+
"decimal.js": "^10.6.0",
21271
21271
+
"html-encoding-sniffer": "^6.0.0",
21272
21272
+
"is-potential-custom-element-name": "^1.0.1",
21273
21273
+
"lru-cache": "^11.3.5",
21274
21274
+
"parse5": "^8.0.1",
21275
21275
+
"saxes": "^6.0.0",
21276
21276
+
"symbol-tree": "^3.2.4",
21277
21277
+
"tough-cookie": "^6.0.1",
21278
21278
+
"undici": "^7.25.0",
21279
21279
+
"w3c-xmlserializer": "^5.0.0",
21280
21280
+
"webidl-conversions": "^8.0.1",
21281
21281
+
"whatwg-mimetype": "^5.0.0",
21282
21282
+
"whatwg-url": "^16.0.1",
21283
21283
+
"xml-name-validator": "^5.0.0"
21284
21284
+
},
21285
21285
+
"engines": {
21286
21286
+
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
21287
21287
+
},
21288
21288
+
"peerDependencies": {
21289
21289
+
"canvas": "^3.0.0"
21290
21290
+
},
21291
21291
+
"peerDependenciesMeta": {
21292
21292
+
"canvas": {
21293
21293
+
"optional": true
21294
21294
+
}
21295
21295
+
}
21296
21296
+
},
21297
21297
+
"node_modules/isomorphic-dompurify/node_modules/lru-cache": {
21298
21298
+
"version": "11.3.5",
21299
21299
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
21300
21300
+
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
21301
21301
+
"license": "BlueOak-1.0.0",
21302
21302
+
"engines": {
21303
21303
+
"node": "20 || >=22"
21304
21304
+
}
21305
21305
+
},
21306
21306
+
"node_modules/isomorphic-dompurify/node_modules/mdn-data": {
21307
21307
+
"version": "2.27.1",
21308
21308
+
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
21309
21309
+
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
21310
21310
+
"license": "CC0-1.0"
21311
21311
+
},
21312
21312
+
"node_modules/isomorphic-dompurify/node_modules/parse5": {
21313
21313
+
"version": "8.0.1",
21314
21314
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
21315
21315
+
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
21316
21316
+
"license": "MIT",
21317
21317
+
"dependencies": {
21318
21318
+
"entities": "^8.0.0"
21319
21319
+
},
21320
21320
+
"funding": {
21321
21321
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
21322
21322
+
}
21323
21323
+
},
21324
21324
+
"node_modules/isomorphic-dompurify/node_modules/tldts": {
21325
21325
+
"version": "7.0.29",
21326
21326
+
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz",
21327
21327
+
"integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==",
21328
21328
+
"license": "MIT",
21329
21329
+
"dependencies": {
21330
21330
+
"tldts-core": "^7.0.29"
21331
21331
+
},
21332
21332
+
"bin": {
21333
21333
+
"tldts": "bin/cli.js"
21334
21334
+
}
21335
21335
+
},
21336
21336
+
"node_modules/isomorphic-dompurify/node_modules/tldts-core": {
21337
21337
+
"version": "7.0.29",
21338
21338
+
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz",
21339
21339
+
"integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==",
21340
21340
+
"license": "MIT"
21341
21341
+
},
21342
21342
+
"node_modules/isomorphic-dompurify/node_modules/tough-cookie": {
21343
21343
+
"version": "6.0.1",
21344
21344
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
21345
21345
+
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
21346
21346
+
"license": "BSD-3-Clause",
21347
21347
+
"dependencies": {
21348
21348
+
"tldts": "^7.0.5"
21349
21349
+
},
21350
21350
+
"engines": {
21351
21351
+
"node": ">=16"
21352
21352
+
}
21353
21353
+
},
21354
21354
+
"node_modules/isomorphic-dompurify/node_modules/tr46": {
21355
21355
+
"version": "6.0.0",
21356
21356
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
21357
21357
+
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
21358
21358
+
"license": "MIT",
21359
21359
+
"dependencies": {
21360
21360
+
"punycode": "^2.3.1"
21361
21361
+
},
21362
21362
+
"engines": {
21363
21363
+
"node": ">=20"
21364
21364
+
}
21365
21365
+
},
21366
21366
+
"node_modules/isomorphic-dompurify/node_modules/undici": {
21367
21367
+
"version": "7.25.0",
21368
21368
+
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
21369
21369
+
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
21370
21370
+
"license": "MIT",
21371
21371
+
"engines": {
21372
21372
+
"node": ">=20.18.1"
21373
21373
+
}
21374
21374
+
},
21375
21375
+
"node_modules/isomorphic-dompurify/node_modules/webidl-conversions": {
21376
21376
+
"version": "8.0.1",
21377
21377
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
21378
21378
+
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
21379
21379
+
"license": "BSD-2-Clause",
21380
21380
+
"engines": {
21381
21381
+
"node": ">=20"
21382
21382
+
}
21383
21383
+
},
21384
21384
+
"node_modules/isomorphic-dompurify/node_modules/whatwg-mimetype": {
21385
21385
+
"version": "5.0.0",
21386
21386
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
21387
21387
+
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
21388
21388
+
"license": "MIT",
21389
21389
+
"engines": {
21390
21390
+
"node": ">=20"
21391
21391
+
}
21392
21392
+
},
21393
21393
+
"node_modules/isomorphic-dompurify/node_modules/whatwg-url": {
21394
21394
+
"version": "16.0.1",
21395
21395
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
21396
21396
+
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
21397
21397
+
"license": "MIT",
21398
21398
+
"dependencies": {
21399
21399
+
"@exodus/bytes": "^1.11.0",
21400
21400
+
"tr46": "^6.0.0",
21401
21401
+
"webidl-conversions": "^8.0.1"
21402
21402
+
},
21403
21403
+
"engines": {
21404
21404
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
21405
21405
+
}
21406
21406
+
},
20908
21407
"node_modules/istanbul-lib-coverage": {
20909
21408
"version": "3.2.2",
20910
21409
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
···
21730
22229
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
21731
22230
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
21732
22231
"dev": true,
22232
22232
+
"license": "MIT",
21733
22233
"peer": true,
21734
22234
"dependencies": {
21735
22235
"cssstyle": "^4.2.1",
···
26425
26925
"version": "2.3.1",
26426
26926
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
26427
26927
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
26428
26428
-
"dev": true,
26429
26928
"engines": {
26430
26929
"node": ">=6"
26431
26930
}
···
27210
27709
"node": ">=0.10.0"
27211
27710
}
27212
27711
},
27712
27712
+
"node_modules/require-from-string": {
27713
27713
+
"version": "2.0.2",
27714
27714
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
27715
27715
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
27716
27716
+
"license": "MIT",
27717
27717
+
"engines": {
27718
27718
+
"node": ">=0.10.0"
27719
27719
+
}
27720
27720
+
},
27213
27721
"node_modules/require-in-the-middle": {
27214
27722
"version": "8.0.1",
27215
27723
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
···
27610
28118
"version": "6.0.0",
27611
28119
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
27612
28120
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
27613
27613
-
"dev": true,
27614
28121
"dependencies": {
27615
28122
"xmlchars": "^2.2.0"
27616
28123
},
···
29284
29791
"node_modules/symbol-tree": {
29285
29792
"version": "3.2.4",
29286
29793
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
29287
29287
-
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
29288
29288
-
"dev": true
29794
29794
+
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
29289
29795
},
29290
29796
"node_modules/synckit": {
29291
29797
"version": "0.11.11",
···
31385
31891
"version": "5.0.0",
31386
31892
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
31387
31893
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
31388
31388
-
"dev": true,
31389
31894
"dependencies": {
31390
31895
"xml-name-validator": "^5.0.0"
31391
31896
},
···
31815
32320
"version": "5.0.0",
31816
32321
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
31817
32322
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
31818
31818
-
"dev": true,
31819
32323
"engines": {
31820
32324
"node": ">=18"
31821
32325
}
···
31848
32352
"node_modules/xmlchars": {
31849
32353
"version": "2.2.0",
31850
32354
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
31851
31851
-
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
31852
31852
-
"dev": true
32355
32355
+
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
31853
32356
},
31854
32357
"node_modules/xtend": {
31855
32358
"version": "4.0.2",
···
32116
32619
"@mantine/notifications": "9.0.0",
32117
32620
"@mdx-js/loader": "^3.1.1",
32118
32621
"@mdx-js/react": "^3.1.1",
32622
32622
+
"@mozilla/readability": "^0.6.0",
32119
32623
"@next/mdx": "16.2.3",
32120
32624
"@semble/types": "*",
32121
32625
"@tanstack/react-query": "^5.85.5",
···
32124
32628
"capnweb": "^0.6.1",
32125
32629
"date-fns": "^4.1.0",
32126
32630
"dayjs": "^1.11.13",
32631
32631
+
"isomorphic-dompurify": "^3.11.0",
32127
32632
"next": "16.2.3",
32128
32633
"react": "19.2.5",
32129
32634
"react-dom": "19.2.5",
···
32146
32651
"@testing-library/react": "^16.3.2",
32147
32652
"@testing-library/user-event": "^14.6.1",
32148
32653
"@types/chrome": "^0.0.332",
32654
32654
+
"@types/jsdom": "^21.1.7",
32655
32655
+
"@types/mozilla__readability": "^0.4.2",
32149
32656
"@types/node": "^20.10.4",
32150
32657
"@types/react": "19.2.14",
32151
32658
"@types/react-dom": "19.2.3",
···
1
1
import BackButton from '@/components/navigation/backButton/BackButton';
2
2
import Header from '@/components/navigation/header/Header';
3
3
-
import { Fragment } from 'react';
3
3
+
import ReaderHeaderButton from '@/features/reader/components/ReaderHeaderButton/ReaderHeaderButton';
4
4
+
import { Fragment, Suspense } from 'react';
4
5
5
6
interface Props {
6
7
children: React.ReactNode;
···
11
12
<Fragment>
12
13
<Header>
13
14
<BackButton />
15
15
+
<Suspense fallback={null}>
16
16
+
<ReaderHeaderButton />
17
17
+
</Suspense>
14
18
</Header>
15
19
{props.children}
16
20
</Fragment>
···
17
17
following: false,
18
18
connections: false,
19
19
graphView: false,
20
20
+
readerMode: false,
20
21
});
21
22
}
22
23
}
···
1
1
+
import { NextRequest, NextResponse } from 'next/server';
2
2
+
import { JSDOM } from 'jsdom';
3
3
+
import { Readability } from '@mozilla/readability';
4
4
+
import DOMPurify from 'isomorphic-dompurify';
5
5
+
6
6
+
export interface ReaderContent {
7
7
+
title: string | null;
8
8
+
content: string;
9
9
+
byline: string | null;
10
10
+
siteName: string | null;
11
11
+
excerpt: string | null;
12
12
+
}
13
13
+
14
14
+
export async function GET(request: NextRequest) {
15
15
+
const { searchParams } = request.nextUrl;
16
16
+
const url = searchParams.get('url');
17
17
+
18
18
+
if (!url) {
19
19
+
return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 });
20
20
+
}
21
21
+
22
22
+
let targetUrl: URL;
23
23
+
try {
24
24
+
targetUrl = new URL(url);
25
25
+
} catch {
26
26
+
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
27
27
+
}
28
28
+
29
29
+
let html: string;
30
30
+
try {
31
31
+
const response = await fetch(targetUrl.toString(), {
32
32
+
headers: {
33
33
+
// Mimic a real browser to avoid blocks
34
34
+
'User-Agent':
35
35
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
36
36
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
37
37
+
'Accept-Language': 'en-US,en;q=0.5',
38
38
+
},
39
39
+
// 10 second timeout
40
40
+
signal: AbortSignal.timeout(10_000),
41
41
+
});
42
42
+
43
43
+
if (!response.ok) {
44
44
+
return NextResponse.json(
45
45
+
{ error: `Failed to fetch URL: ${response.status} ${response.statusText}` },
46
46
+
{ status: 502 },
47
47
+
);
48
48
+
}
49
49
+
50
50
+
html = await response.text();
51
51
+
} catch (err) {
52
52
+
const message = err instanceof Error ? err.message : 'Unknown error';
53
53
+
return NextResponse.json({ error: `Failed to fetch URL: ${message}` }, { status: 502 });
54
54
+
}
55
55
+
56
56
+
// Parse with jsdom, passing the URL so Readability can resolve relative links
57
57
+
const dom = new JSDOM(html, { url: targetUrl.toString() });
58
58
+
const reader = new Readability(dom.window.document);
59
59
+
const article = reader.parse();
60
60
+
61
61
+
if (!article) {
62
62
+
return NextResponse.json(
63
63
+
{ error: 'Could not extract readable content from this page' },
64
64
+
{ status: 422 },
65
65
+
);
66
66
+
}
67
67
+
68
68
+
// Sanitize the extracted HTML to prevent XSS
69
69
+
const sanitizedContent = DOMPurify.sanitize(article.content ?? '', {
70
70
+
USE_PROFILES: { html: true },
71
71
+
// Allow common article elements but strip scripts/iframes/etc.
72
72
+
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input', 'button'],
73
73
+
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
74
74
+
});
75
75
+
76
76
+
const result: ReaderContent = {
77
77
+
title: article.title ?? null,
78
78
+
content: sanitizedContent,
79
79
+
byline: article.byline ?? null,
80
80
+
siteName: article.siteName ?? null,
81
81
+
excerpt: article.excerpt ?? null,
82
82
+
};
83
83
+
84
84
+
return NextResponse.json(result);
85
85
+
}
···
1
1
+
/* ── Bottom bar ── */
2
2
+
3
3
+
.bottomBar {
4
4
+
position: relative;
5
5
+
flex-shrink: 0;
6
6
+
background: color-mix(in srgb, var(--mantine-color-body) 85%, transparent);
7
7
+
backdrop-filter: blur(10px);
8
8
+
-webkit-backdrop-filter: blur(10px);
9
9
+
border-top: 1px solid var(--mantine-color-gray-2);
10
10
+
}
11
11
+
12
12
+
/* ── Article body typography ── */
13
13
+
14
14
+
.readerContent :global(p) {
15
15
+
margin: 0 0 1.3em;
16
16
+
}
17
17
+
18
18
+
.readerContent :global(h1),
19
19
+
.readerContent :global(h2),
20
20
+
.readerContent :global(h3),
21
21
+
.readerContent :global(h4),
22
22
+
.readerContent :global(h5),
23
23
+
.readerContent :global(h6) {
24
24
+
font-weight: 700;
25
25
+
line-height: 1.3;
26
26
+
margin: 1.75em 0 0.5em;
27
27
+
}
28
28
+
29
29
+
.readerContent :global(h1) { font-size: 1.6em; }
30
30
+
.readerContent :global(h2) { font-size: 1.35em; }
31
31
+
.readerContent :global(h3) { font-size: 1.15em; }
32
32
+
.readerContent :global(h4) { font-size: 1.0em; }
33
33
+
34
34
+
.readerContent :global(a) {
35
35
+
color: var(--mantine-color-blue-6);
36
36
+
text-decoration: underline;
37
37
+
text-underline-offset: 3px;
38
38
+
}
39
39
+
40
40
+
.readerContent :global(a:hover) {
41
41
+
color: var(--mantine-color-blue-8);
42
42
+
}
43
43
+
44
44
+
.readerContent :global(blockquote) {
45
45
+
border-left: 3px solid var(--mantine-color-gray-4);
46
46
+
margin: 1.5em 0;
47
47
+
padding: 0.4em 1.25em;
48
48
+
color: var(--mantine-color-dimmed);
49
49
+
font-style: italic;
50
50
+
}
51
51
+
52
52
+
.readerContent :global(pre) {
53
53
+
background: var(--mantine-color-gray-0);
54
54
+
border: 1px solid var(--mantine-color-gray-2);
55
55
+
border-radius: 8px;
56
56
+
padding: 1em 1.25em;
57
57
+
overflow-x: auto;
58
58
+
font-size: 0.875em;
59
59
+
line-height: 1.6;
60
60
+
margin: 1.5em 0;
61
61
+
}
62
62
+
63
63
+
.readerContent :global(code) {
64
64
+
background: var(--mantine-color-gray-1);
65
65
+
border-radius: 4px;
66
66
+
padding: 0.15em 0.4em;
67
67
+
font-size: 0.875em;
68
68
+
font-family: var(--mantine-font-family-monospace, monospace);
69
69
+
}
70
70
+
71
71
+
.readerContent :global(pre code) {
72
72
+
background: none;
73
73
+
padding: 0;
74
74
+
font-size: inherit;
75
75
+
}
76
76
+
77
77
+
.readerContent :global(ul),
78
78
+
.readerContent :global(ol) {
79
79
+
padding-left: 1.75em;
80
80
+
margin: 0 0 1.3em;
81
81
+
}
82
82
+
83
83
+
.readerContent :global(li) {
84
84
+
margin-bottom: 0.4em;
85
85
+
}
86
86
+
87
87
+
.readerContent :global(li > ul),
88
88
+
.readerContent :global(li > ol) {
89
89
+
margin-top: 0.3em;
90
90
+
margin-bottom: 0;
91
91
+
}
92
92
+
93
93
+
.readerContent :global(img) {
94
94
+
max-width: 100%;
95
95
+
height: auto;
96
96
+
border-radius: 8px;
97
97
+
margin: 1.5em 0;
98
98
+
display: block;
99
99
+
}
100
100
+
101
101
+
.readerContent :global(figure) {
102
102
+
margin: 1.75em 0;
103
103
+
}
104
104
+
105
105
+
.readerContent :global(figcaption) {
106
106
+
font-size: 0.8125em;
107
107
+
color: var(--mantine-color-dimmed);
108
108
+
text-align: center;
109
109
+
margin-top: 0.6em;
110
110
+
}
111
111
+
112
112
+
.readerContent :global(hr) {
113
113
+
border: none;
114
114
+
border-top: 1px solid var(--mantine-color-gray-3);
115
115
+
margin: 2em 0;
116
116
+
}
117
117
+
118
118
+
.readerContent :global(table) {
119
119
+
width: 100%;
120
120
+
border-collapse: collapse;
121
121
+
margin: 1.5em 0;
122
122
+
font-size: 0.9375em;
123
123
+
}
124
124
+
125
125
+
.readerContent :global(th),
126
126
+
.readerContent :global(td) {
127
127
+
border: 1px solid var(--mantine-color-gray-3);
128
128
+
padding: 0.5em 0.75em;
129
129
+
text-align: left;
130
130
+
}
131
131
+
132
132
+
.readerContent :global(th) {
133
133
+
background: var(--mantine-color-gray-0);
134
134
+
font-weight: 600;
135
135
+
}
···
1
1
+
'use client';
2
2
+
3
3
+
import { useState } from 'react';
4
4
+
import {
5
5
+
ActionIcon,
6
6
+
Drawer,
7
7
+
Tooltip,
8
8
+
Stack,
9
9
+
Title,
10
10
+
Text,
11
11
+
Alert,
12
12
+
ScrollArea,
13
13
+
Box,
14
14
+
Group,
15
15
+
CloseButton,
16
16
+
Skeleton,
17
17
+
Button,
18
18
+
Anchor,
19
19
+
Container,
20
20
+
} from '@mantine/core';
21
21
+
import { TbBook2, TbExternalLink, TbLink, TbLinkOff } from 'react-icons/tb';
22
22
+
import { MdErrorOutline } from 'react-icons/md';
23
23
+
import { fetchReaderContent, type ReaderState } from '../../lib/fetchReaderContent';
24
24
+
import styles from './ReaderButton.module.css';
25
25
+
26
26
+
interface Props {
27
27
+
url: string;
28
28
+
}
29
29
+
30
30
+
const FONT_SIZE_MIN = 14;
31
31
+
const FONT_SIZE_MAX = 24;
32
32
+
const FONT_SIZE_STEP = 2;
33
33
+
const FONT_SIZE_DEFAULT = 17;
34
34
+
35
35
+
function ArticleSkeleton() {
36
36
+
return (
37
37
+
<Stack gap="sm">
38
38
+
<Skeleton height={12} width="15%" radius="sm" />
39
39
+
<Skeleton height={36} width="90%" radius="sm" />
40
40
+
<Skeleton height={36} width="70%" radius="sm" />
41
41
+
<Skeleton height={12} width="25%" radius="sm" mt={4} />
42
42
+
<Box mt="xl">
43
43
+
{[100, 95, 88, 100, 92, 78, 100, 96, 83, 60].map((w, i) => (
44
44
+
<Skeleton key={i} height={14} width={`${w}%`} radius="sm" mb={10} />
45
45
+
))}
46
46
+
</Box>
47
47
+
<Box mt="xs">
48
48
+
{[100, 91, 100, 85, 100, 72].map((w, i) => (
49
49
+
<Skeleton key={i} height={14} width={`${w}%`} radius="sm" mb={10} />
50
50
+
))}
51
51
+
</Box>
52
52
+
</Stack>
53
53
+
);
54
54
+
}
55
55
+
56
56
+
export default function ReaderButton({ url }: Props) {
57
57
+
const [opened, setOpened] = useState(false);
58
58
+
const [state, setState] = useState<ReaderState>({ status: 'idle' });
59
59
+
const [fontSize, setFontSize] = useState(FONT_SIZE_DEFAULT);
60
60
+
const [removeLinks, setRemoveLinks] = useState(false);
61
61
+
62
62
+
const displayedHtml =
63
63
+
state.status === 'success'
64
64
+
? removeLinks
65
65
+
? state.data.content.replace(/<a[^>]*>([\s\S]*?)<\/a>/g, '$1')
66
66
+
: state.data.content
67
67
+
: '';
68
68
+
69
69
+
function openReader() {
70
70
+
setOpened(true);
71
71
+
if (state.status === 'success' || state.status === 'loading') return;
72
72
+
fetchReaderContent(url, setState);
73
73
+
}
74
74
+
75
75
+
function handleClose() {
76
76
+
setOpened(false);
77
77
+
}
78
78
+
79
79
+
return (
80
80
+
<>
81
81
+
<Button
82
82
+
variant="light"
83
83
+
color="gray"
84
84
+
radius="xl"
85
85
+
leftSection={<TbBook2 size={18} />}
86
86
+
onClick={openReader}
87
87
+
aria-label="Open reader mode"
88
88
+
>
89
89
+
Reader
90
90
+
</Button>
91
91
+
92
92
+
<Drawer
93
93
+
opened={opened}
94
94
+
onClose={handleClose}
95
95
+
position="bottom"
96
96
+
size="full"
97
97
+
padding={0}
98
98
+
withCloseButton={false}
99
99
+
styles={{
100
100
+
content: { display: 'flex', flexDirection: 'column' },
101
101
+
body: {
102
102
+
flex: 1,
103
103
+
display: 'flex',
104
104
+
flexDirection: 'column',
105
105
+
overflow: 'hidden',
106
106
+
},
107
107
+
}}
108
108
+
>
109
109
+
{/* ── Scrollable article area ── */}
110
110
+
<ScrollArea style={{ flex: 1 }}>
111
111
+
<Container
112
112
+
size={'sm'}
113
113
+
px="xl"
114
114
+
style={{
115
115
+
paddingTop: '2.5rem',
116
116
+
paddingBottom: '3rem',
117
117
+
}}
118
118
+
>
119
119
+
{state.status === 'loading' && <ArticleSkeleton />}
120
120
+
121
121
+
{state.status === 'error' && (
122
122
+
<Stack gap="md">
123
123
+
<Alert
124
124
+
icon={<MdErrorOutline size={20} />}
125
125
+
title="Could not load reader"
126
126
+
color="red"
127
127
+
variant="light"
128
128
+
>
129
129
+
{state.message}
130
130
+
</Alert>
131
131
+
<Button
132
132
+
variant="default"
133
133
+
size="sm"
134
134
+
radius="xl"
135
135
+
onClick={() => fetchReaderContent(url, setState)}
136
136
+
>
137
137
+
Try again
138
138
+
</Button>
139
139
+
</Stack>
140
140
+
)}
141
141
+
142
142
+
{state.status === 'success' && (
143
143
+
<Stack gap="xs">
144
144
+
{state.data.siteName && (
145
145
+
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
146
146
+
{state.data.siteName}
147
147
+
</Text>
148
148
+
)}
149
149
+
150
150
+
{state.data.title && (
151
151
+
<Title
152
152
+
order={1}
153
153
+
style={{ lineHeight: 1.2, fontSize: '1.75rem' }}
154
154
+
>
155
155
+
{state.data.title}
156
156
+
</Title>
157
157
+
)}
158
158
+
159
159
+
{state.data.byline && (
160
160
+
<Text size="sm" c="dimmed" mt={2}>
161
161
+
{state.data.byline}
162
162
+
</Text>
163
163
+
)}
164
164
+
165
165
+
<Box
166
166
+
component="article"
167
167
+
dangerouslySetInnerHTML={{ __html: displayedHtml }}
168
168
+
className={styles.readerContent}
169
169
+
style={{
170
170
+
fontSize: `${fontSize}px`,
171
171
+
lineHeight: 1.8,
172
172
+
color: 'var(--mantine-color-text)',
173
173
+
marginTop: '1.5rem',
174
174
+
}}
175
175
+
/>
176
176
+
</Stack>
177
177
+
)}
178
178
+
</Container>
179
179
+
</ScrollArea>
180
180
+
181
181
+
{/* ── Sticky bottom bar ── */}
182
182
+
<Box className={styles.bottomBar}>
183
183
+
<Group px="lg" py="xs" justify="space-between" align="center">
184
184
+
{/* Left: font size + link toggle */}
185
185
+
<Group gap="xs">
186
186
+
<Tooltip label="Decrease font size" withArrow position="top">
187
187
+
<ActionIcon
188
188
+
variant="light"
189
189
+
color="gray"
190
190
+
size="md"
191
191
+
radius="xl"
192
192
+
onClick={() =>
193
193
+
setFontSize((s) =>
194
194
+
Math.max(FONT_SIZE_MIN, s - FONT_SIZE_STEP),
195
195
+
)
196
196
+
}
197
197
+
disabled={fontSize <= FONT_SIZE_MIN}
198
198
+
aria-label="Decrease font size"
199
199
+
>
200
200
+
<Text size="xs" fw={700}>
201
201
+
A-
202
202
+
</Text>
203
203
+
</ActionIcon>
204
204
+
</Tooltip>
205
205
+
<Tooltip label="Increase font size" withArrow position="top">
206
206
+
<ActionIcon
207
207
+
variant="light"
208
208
+
color="gray"
209
209
+
size="md"
210
210
+
radius="xl"
211
211
+
onClick={() =>
212
212
+
setFontSize((s) =>
213
213
+
Math.min(FONT_SIZE_MAX, s + FONT_SIZE_STEP),
214
214
+
)
215
215
+
}
216
216
+
disabled={fontSize >= FONT_SIZE_MAX}
217
217
+
aria-label="Increase font size"
218
218
+
>
219
219
+
<Text size="xs" fw={700}>
220
220
+
A+
221
221
+
</Text>
222
222
+
</ActionIcon>
223
223
+
</Tooltip>
224
224
+
<Tooltip
225
225
+
label={removeLinks ? 'Show links' : 'Remove links'}
226
226
+
withArrow
227
227
+
position="top"
228
228
+
>
229
229
+
<ActionIcon
230
230
+
variant={removeLinks ? 'filled' : 'light'}
231
231
+
color={'gray'}
232
232
+
size="md"
233
233
+
radius="xl"
234
234
+
onClick={() => setRemoveLinks((v) => !v)}
235
235
+
aria-label={removeLinks ? 'Show links' : 'Remove links'}
236
236
+
>
237
237
+
{removeLinks ? <TbLinkOff size={16} /> : <TbLink size={16} />}
238
238
+
</ActionIcon>
239
239
+
</Tooltip>
240
240
+
</Group>
241
241
+
242
242
+
{/* Right: open original + close */}
243
243
+
<Group gap="xs">
244
244
+
<Anchor
245
245
+
href={url}
246
246
+
target="_blank"
247
247
+
rel="noopener noreferrer"
248
248
+
underline="never"
249
249
+
>
250
250
+
<Button
251
251
+
variant="light"
252
252
+
color="gray"
253
253
+
size="xs"
254
254
+
radius="xl"
255
255
+
rightSection={<TbExternalLink size={14} />}
256
256
+
>
257
257
+
Open original
258
258
+
</Button>
259
259
+
</Anchor>
260
260
+
261
261
+
<CloseButton
262
262
+
size="lg"
263
263
+
radius={'xl'}
264
264
+
onClick={handleClose}
265
265
+
aria-label="Close reader mode"
266
266
+
/>
267
267
+
</Group>
268
268
+
</Group>
269
269
+
</Box>
270
270
+
</Drawer>
271
271
+
</>
272
272
+
);
273
273
+
}
···
1
1
+
import type { ReaderContent } from '@/app/api/reader/route';
2
2
+
3
3
+
export type ReaderState =
4
4
+
| { status: 'idle' }
5
5
+
| { status: 'loading' }
6
6
+
| { status: 'error'; message: string }
7
7
+
| { status: 'success'; data: ReaderContent };
8
8
+
9
9
+
export async function fetchReaderContent(
10
10
+
url: string,
11
11
+
setState: (state: ReaderState) => void,
12
12
+
): Promise<void> {
13
13
+
setState({ status: 'loading' });
14
14
+
try {
15
15
+
const res = await fetch(`/api/reader?url=${encodeURIComponent(url)}`);
16
16
+
const json = await res.json();
17
17
+
if (!res.ok) {
18
18
+
setState({ status: 'error', message: json.error ?? 'Unknown error' });
19
19
+
return;
20
20
+
}
21
21
+
setState({ status: 'success', data: json as ReaderContent });
22
22
+
} catch (err) {
23
23
+
const message =
24
24
+
err instanceof Error ? err.message : 'Failed to load reader content';
25
25
+
setState({ status: 'error', message });
26
26
+
}
27
27
+
}
···
11
11
following: boolean;
12
12
connections: boolean;
13
13
graphView: boolean;
14
14
+
readerMode: boolean;
14
15
}
15
16
16
17
async function fetchFeatureFlags(): Promise<FeatureFlags> {
···
26
26
following: true,
27
27
connections: true,
28
28
graphView: show,
29
29
+
readerMode: showForTeam,
29
30
};
30
31
}
···
1
1
/// <reference types="next" />
2
2
/// <reference types="next/image-types/global" />
3
3
-
import "./.next/types/routes.d.ts";
3
3
+
import "./.next/dev/types/routes.d.ts";
4
4
5
5
// NOTE: This file should not be edited
6
6
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
···
38
38
"@mantine/notifications": "9.0.0",
39
39
"@mdx-js/loader": "^3.1.1",
40
40
"@mdx-js/react": "^3.1.1",
41
41
+
"@mozilla/readability": "^0.6.0",
41
42
"@next/mdx": "16.2.3",
42
43
"@semble/types": "*",
43
44
"@tanstack/react-query": "^5.85.5",
···
46
47
"capnweb": "^0.6.1",
47
48
"date-fns": "^4.1.0",
48
49
"dayjs": "^1.11.13",
50
50
+
"isomorphic-dompurify": "^3.11.0",
49
51
"next": "16.2.3",
50
52
"react": "19.2.5",
51
53
"react-dom": "19.2.5",
···
57
59
"web-haptics": "^0.0.6"
58
60
},
59
61
"devDependencies": {
62
62
+
"@chromatic-com/storybook": "^5.1.2",
60
63
"@playwright/test": "^1.59.1",
64
64
+
"@storybook/addon-a11y": "^10.3.6",
65
65
+
"@storybook/addon-docs": "^10.3.6",
66
66
+
"@storybook/addon-onboarding": "^10.3.6",
67
67
+
"@storybook/addon-vitest": "^10.3.6",
68
68
+
"@storybook/nextjs-vite": "^10.3.6",
61
69
"@testing-library/dom": "^10.4.1",
62
70
"@testing-library/react": "^16.3.2",
63
71
"@testing-library/user-event": "^14.6.1",
64
72
"@types/chrome": "^0.0.332",
73
73
+
"@types/jsdom": "^21.1.7",
74
74
+
"@types/mozilla__readability": "^0.4.2",
65
75
"@types/node": "^20.10.4",
66
76
"@types/react": "19.2.14",
67
77
"@types/react-dom": "19.2.3",
68
78
"@vitejs/plugin-react": "^6.0.1",
79
79
+
"@vitest/browser-playwright": "^4.1.5",
80
80
+
"@vitest/coverage-v8": "^4.1.5",
69
81
"autoprefixer": "^10.4.16",
70
82
"eslint": "^9.0.0",
71
83
"eslint-config-next": "16.2.3",
84
84
+
"eslint-plugin-storybook": "^10.3.6",
72
85
"jsdom": "^26.1.0",
73
86
"plasmo": "^0.90.5",
74
87
"postcss": "^8.5.6",
75
88
"postcss-preset-mantine": "^1.18.0",
76
89
"postcss-simple-vars": "^7.0.1",
77
77
-
"typescript": "^5.3.3",
78
78
-
"vitest": "^4.1.5",
79
90
"storybook": "^10.3.6",
80
80
-
"@storybook/nextjs-vite": "^10.3.6",
81
81
-
"@chromatic-com/storybook": "^5.1.2",
82
82
-
"@storybook/addon-vitest": "^10.3.6",
83
83
-
"@storybook/addon-a11y": "^10.3.6",
84
84
-
"@storybook/addon-docs": "^10.3.6",
85
85
-
"@storybook/addon-onboarding": "^10.3.6",
91
91
+
"typescript": "^5.3.3",
86
92
"vite": "^8.0.10",
87
87
-
"eslint-plugin-storybook": "^10.3.6",
88
88
-
"@vitest/browser-playwright": "^4.1.5",
89
89
-
"@vitest/coverage-v8": "^4.1.5"
93
93
+
"vitest": "^4.1.5"
90
94
},
91
95
"overrides": {
92
96
"@types/react": "19.2.14",