Remove small markdown helper dependencies (#2467)
This commit is contained in:
parent
56cb5237a1
commit
942bcf7065
|
|
@ -98,14 +98,10 @@
|
|||
"dompurify": "^3.4.2",
|
||||
"electron-updater": "^6.8.3",
|
||||
"emoji-picker-react": "^4.19.1",
|
||||
"entities": "^6.0.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"hosted-git-info": "^9.0.3",
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.16.45",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"node-pty": "^1.1.0",
|
||||
|
|
|
|||
141
pnpm-lock.yaml
141
pnpm-lock.yaml
|
|
@ -142,15 +142,6 @@ importers:
|
|||
emoji-picker-react:
|
||||
specifier: ^4.19.1
|
||||
version: 4.19.1(react@19.2.5)
|
||||
entities:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
github-slugger:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
hosted-git-info:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
html-to-image:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
|
|
@ -163,9 +154,6 @@ importers:
|
|||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@19.2.5)
|
||||
mdast-util-to-string:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
mermaid:
|
||||
specifier: ^11.15.0
|
||||
version: 11.15.0
|
||||
|
|
@ -331,7 +319,7 @@ importers:
|
|||
version: 19.2.5(react@19.2.5)
|
||||
react-grab:
|
||||
specifier: ^0.1.33
|
||||
version: 0.1.33(react@19.2.5)
|
||||
version: 0.1.34(react@19.2.5)
|
||||
tailwindcss:
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4
|
||||
|
|
@ -369,11 +357,6 @@ packages:
|
|||
'@antfu/install-pkg@1.1.0':
|
||||
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
|
||||
|
||||
'@antfu/ni@30.1.0':
|
||||
resolution: {integrity: sha512-3VuAbPjgY52rQNn4wABaXMhBU2Oq91uy6L8nX49eJ35OLI68CyckGU+HZxcaHix4ymuGM2nFL1D6sLpgODK5xw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -553,8 +536,8 @@ packages:
|
|||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dotenvx/dotenvx@1.65.0':
|
||||
resolution: {integrity: sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg==}
|
||||
'@dotenvx/dotenvx@1.66.0':
|
||||
resolution: {integrity: sha512-qlQFhHUjhRDybrinqLAD0MClVZDOrsq80O8eD5iSjz3Qa/4f3Jg7SQrOaSobrRyP1QaWIYLGtGpj2c7H0D8NUw==}
|
||||
hasBin: true
|
||||
|
||||
'@ecies/ciphers@0.2.6':
|
||||
|
|
@ -2228,8 +2211,8 @@ packages:
|
|||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@react-grab/cli@0.1.33':
|
||||
resolution: {integrity: sha512-UOc3PwN11Osw0NzaxRLK8trP4X+5iW1Dst3gvHRCafe3wXHyadzHYH8H1hdkcXdlIx3gsoD9ASJ+G/JH+A/jqA==}
|
||||
'@react-grab/cli@0.1.34':
|
||||
resolution: {integrity: sha512-L2eAxN46Vq2Ss3nDegrH7wQVMeWH03ahawp+OdzUtQWqL3cq6Bt149q9XhY3cWc9fJsxuWjLfCn+3T9uApIlBA==}
|
||||
hasBin: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.3':
|
||||
|
|
@ -3215,8 +3198,8 @@ packages:
|
|||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bippy@0.5.39:
|
||||
resolution: {integrity: sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w==}
|
||||
bippy@0.5.40:
|
||||
resolution: {integrity: sha512-3QPSDG5tgd7FCIkcKsUqmbGdlHxBxP7II5drXPNp1I0rpA9+4+/VjQOigDTbzeqTi6Xkaf1Dq7x4E8vbOuwOkA==}
|
||||
peerDependencies:
|
||||
react: '>=17.0.1'
|
||||
|
||||
|
|
@ -3463,6 +3446,10 @@ packages:
|
|||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
content-type@2.0.0:
|
||||
resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
|
|
@ -3999,8 +3986,8 @@ packages:
|
|||
exponential-backoff@3.1.3:
|
||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
|
||||
|
||||
express-rate-limit@8.5.0:
|
||||
resolution: {integrity: sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==}
|
||||
express-rate-limit@8.5.2:
|
||||
resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
|
@ -4160,9 +4147,6 @@ packages:
|
|||
fuzzysort@3.1.0:
|
||||
resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
|
||||
|
||||
fzf@0.5.2:
|
||||
resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
|
||||
|
||||
gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -4314,18 +4298,14 @@ packages:
|
|||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hono@4.12.17:
|
||||
resolution: {integrity: sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==}
|
||||
hono@4.12.19:
|
||||
resolution: {integrity: sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
hosted-git-info@9.0.3:
|
||||
resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
|
|
@ -4415,8 +4395,8 @@ packages:
|
|||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
ip-address@10.2.0:
|
||||
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
|
|
@ -4747,10 +4727,6 @@ packages:
|
|||
lowlight@3.3.0:
|
||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||
|
||||
lru-cache@11.3.6:
|
||||
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
|
|
@ -5442,8 +5418,8 @@ packages:
|
|||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
qs@6.15.1:
|
||||
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
|
||||
qs@6.15.2:
|
||||
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
|
|
@ -5483,8 +5459,8 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^19.2.5
|
||||
|
||||
react-grab@0.1.33:
|
||||
resolution: {integrity: sha512-ER919JMsE4TTrb2CpEivqsIjNMSycD4HtS8v7mS3pq67U7WL1K3+C8m9AYOwW4dpuYh+EanC2eJBmfuczHJZ0A==}
|
||||
react-grab@0.1.34:
|
||||
resolution: {integrity: sha512-jtdOdv0kb90oqL+pMszSh9DOLgVRaX4ZE6XN4GkDEpNNUqveQfZT014+EeJraljpqQfuWKW+96NrrRqUD93D2g==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
|
|
@ -6077,9 +6053,9 @@ packages:
|
|||
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
type-is@2.1.0:
|
||||
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
|
|
@ -6458,13 +6434,6 @@ snapshots:
|
|||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.1.2
|
||||
|
||||
'@antfu/ni@30.1.0':
|
||||
dependencies:
|
||||
fzf: 0.5.2
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.1.2
|
||||
tinyglobby: 0.2.16
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
|
@ -6700,7 +6669,7 @@ snapshots:
|
|||
react: 19.2.5
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dotenvx/dotenvx@1.65.0':
|
||||
'@dotenvx/dotenvx@1.66.0':
|
||||
dependencies:
|
||||
commander: 11.1.0
|
||||
dotenv: 17.4.2
|
||||
|
|
@ -6999,9 +6968,9 @@ snapshots:
|
|||
dependencies:
|
||||
graphql: 16.13.2
|
||||
|
||||
'@hono/node-server@1.19.14(hono@4.12.17)':
|
||||
'@hono/node-server@1.19.14(hono@4.12.19)':
|
||||
dependencies:
|
||||
hono: 4.12.17
|
||||
hono: 4.12.19
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
|
|
@ -7094,7 +7063,7 @@ snapshots:
|
|||
|
||||
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.14(hono@4.12.17)
|
||||
'@hono/node-server': 1.19.14(hono@4.12.19)
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
content-type: 1.0.5
|
||||
|
|
@ -7103,8 +7072,8 @@ snapshots:
|
|||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.8
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.5.0(express@5.2.1)
|
||||
hono: 4.12.17
|
||||
express-rate-limit: 8.5.2(express@5.2.1)
|
||||
hono: 4.12.19
|
||||
jose: 6.2.3
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
|
|
@ -8160,16 +8129,17 @@ snapshots:
|
|||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@react-grab/cli@0.1.33':
|
||||
'@react-grab/cli@0.1.34':
|
||||
dependencies:
|
||||
'@antfu/ni': 30.1.0
|
||||
commander: 14.0.3
|
||||
ignore: 7.0.5
|
||||
jsonc-parser: 3.3.1
|
||||
ora: 9.4.0
|
||||
package-manager-detector: 1.6.0
|
||||
picocolors: 1.1.1
|
||||
prompts: 2.4.2
|
||||
smol-toml: 1.6.1
|
||||
tinyexec: 1.1.2
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.3': {}
|
||||
|
||||
|
|
@ -9133,7 +9103,7 @@ snapshots:
|
|||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bippy@0.5.39(react@19.2.5):
|
||||
bippy@0.5.40(react@19.2.5):
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
|
|
@ -9151,9 +9121,9 @@ snapshots:
|
|||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
qs: 6.15.1
|
||||
qs: 6.15.2
|
||||
raw-body: 3.0.2
|
||||
type-is: 2.0.1
|
||||
type-is: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -9384,6 +9354,8 @@ snapshots:
|
|||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
content-type@2.0.0: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
|
@ -10021,10 +9993,10 @@ snapshots:
|
|||
|
||||
exponential-backoff@3.1.3: {}
|
||||
|
||||
express-rate-limit@8.5.0(express@5.2.1):
|
||||
express-rate-limit@8.5.2(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
ip-address: 10.1.0
|
||||
ip-address: 10.2.0
|
||||
|
||||
express@5.2.1:
|
||||
dependencies:
|
||||
|
|
@ -10048,13 +10020,13 @@ snapshots:
|
|||
once: 1.4.0
|
||||
parseurl: 1.3.3
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.15.1
|
||||
qs: 6.15.2
|
||||
range-parser: 1.2.1
|
||||
router: 2.2.0
|
||||
send: 1.2.1
|
||||
serve-static: 2.2.1
|
||||
statuses: 2.0.2
|
||||
type-is: 2.0.1
|
||||
type-is: 2.1.0
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -10190,7 +10162,6 @@ snapshots:
|
|||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.2.1
|
||||
universalify: 2.0.1
|
||||
optional: true
|
||||
|
||||
fs-extra@7.0.1:
|
||||
dependencies:
|
||||
|
|
@ -10223,8 +10194,6 @@ snapshots:
|
|||
|
||||
fuzzysort@3.1.0: {}
|
||||
|
||||
fzf@0.5.2: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
|
@ -10463,16 +10432,12 @@ snapshots:
|
|||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hono@4.12.17: {}
|
||||
hono@4.12.19: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
hosted-git-info@9.0.3:
|
||||
dependencies:
|
||||
lru-cache: 11.3.6
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
|
@ -10554,7 +10519,7 @@ snapshots:
|
|||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
ip-address@10.2.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
|
|
@ -10805,8 +10770,6 @@ snapshots:
|
|||
devlop: 1.1.0
|
||||
highlight.js: 11.11.1
|
||||
|
||||
lru-cache@11.3.6: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
|
@ -11812,7 +11775,7 @@ snapshots:
|
|||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
qs@6.15.1:
|
||||
qs@6.15.2:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
|
|
@ -11904,10 +11867,10 @@ snapshots:
|
|||
react: 19.2.5
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-grab@0.1.33(react@19.2.5):
|
||||
react-grab@0.1.34(react@19.2.5):
|
||||
dependencies:
|
||||
'@react-grab/cli': 0.1.33
|
||||
bippy: 0.5.39(react@19.2.5)
|
||||
'@react-grab/cli': 0.1.34
|
||||
bippy: 0.5.40(react@19.2.5)
|
||||
optionalDependencies:
|
||||
react: 19.2.5
|
||||
|
||||
|
|
@ -12245,7 +12208,7 @@ snapshots:
|
|||
'@babel/parser': 7.29.3
|
||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@dotenvx/dotenvx': 1.65.0
|
||||
'@dotenvx/dotenvx': 1.66.0
|
||||
'@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76)
|
||||
'@types/validate-npm-package-name': 4.0.2
|
||||
browserslist: 4.28.2
|
||||
|
|
@ -12256,7 +12219,7 @@ snapshots:
|
|||
diff: 8.0.4
|
||||
execa: 9.6.1
|
||||
fast-glob: 3.3.3
|
||||
fs-extra: 11.3.4
|
||||
fs-extra: 11.3.5
|
||||
fuzzysort: 3.1.0
|
||||
https-proxy-agent: 7.0.6
|
||||
kleur: 4.1.5
|
||||
|
|
@ -12624,9 +12587,9 @@ snapshots:
|
|||
dependencies:
|
||||
tagged-tag: 1.0.0
|
||||
|
||||
type-is@2.0.1:
|
||||
type-is@2.1.0:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
content-type: 2.0.0
|
||||
media-typer: 1.1.0
|
||||
mime-types: 3.0.2
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { buildHostedRemoteFileUrl, parseHostedRemote } from './hosted-remote-url'
|
||||
|
||||
describe('hosted remote URLs', () => {
|
||||
it('parses common GitHub remote formats', () => {
|
||||
expect(parseHostedRemote('https://github.com/Org/Repo.git')).toEqual({
|
||||
host: 'github.com',
|
||||
path: 'Org/Repo',
|
||||
provider: 'github'
|
||||
})
|
||||
expect(parseHostedRemote('git@github.com:Org/Repo.git')).toEqual({
|
||||
host: 'github.com',
|
||||
path: 'Org/Repo',
|
||||
provider: 'github'
|
||||
})
|
||||
expect(parseHostedRemote('ssh://git@github.com/Org/Repo.git')).toEqual({
|
||||
host: 'github.com',
|
||||
path: 'Org/Repo',
|
||||
provider: 'github'
|
||||
})
|
||||
expect(parseHostedRemote('github:Org/Repo')).toEqual({
|
||||
host: 'github.com',
|
||||
path: 'Org/Repo',
|
||||
provider: 'github'
|
||||
})
|
||||
})
|
||||
|
||||
it('parses nested GitLab and Bitbucket remotes', () => {
|
||||
expect(parseHostedRemote('git@gitlab.com:group/sub/repo.git')).toEqual({
|
||||
host: 'gitlab.com',
|
||||
path: 'group/sub/repo',
|
||||
provider: 'gitlab'
|
||||
})
|
||||
expect(parseHostedRemote('https://bitbucket.org/team/repo.git')).toEqual({
|
||||
host: 'bitbucket.org',
|
||||
path: 'team/repo',
|
||||
provider: 'bitbucket'
|
||||
})
|
||||
})
|
||||
|
||||
it('builds file URLs with encoded branches and paths', () => {
|
||||
expect(
|
||||
buildHostedRemoteFileUrl('git@github.com:Org/Repo.git', 'src/a file.ts', 'feature/x', 42)
|
||||
).toBe('https://github.com/Org/Repo/blob/feature%2Fx/src/a%20file.ts#L42')
|
||||
|
||||
expect(
|
||||
buildHostedRemoteFileUrl('git@gitlab.com:group/sub/repo.git', 'src/a.ts', 'feature/x', 9)
|
||||
).toBe('https://gitlab.com/group/sub/repo/tree/feature%2Fx/src/a.ts#L9')
|
||||
|
||||
expect(
|
||||
buildHostedRemoteFileUrl('git@bitbucket.org:team/repo.git', 'src/a.ts', 'feature/x', 7)
|
||||
).toBe('https://bitbucket.org/team/repo/src/feature%2Fx/src/a.ts#L7')
|
||||
})
|
||||
|
||||
it('rejects unsupported hosts and incomplete repo paths', () => {
|
||||
expect(parseHostedRemote('git@example.com:team/repo.git')).toBeNull()
|
||||
expect(parseHostedRemote('git@github.com:repo.git')).toBeNull()
|
||||
expect(parseHostedRemote('ftp://github.com/Org/Repo.git')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// Scoped local fork of the hosted-git-info@9.0.3 behavior Orca used.
|
||||
// Why: source links only need GitHub/GitLab/Bitbucket remote parsing and file
|
||||
// URL construction, so we keep that small surface local and tested.
|
||||
type HostedRemoteProvider = 'github' | 'gitlab' | 'bitbucket'
|
||||
|
||||
type HostedRemote = {
|
||||
host: string
|
||||
path: string
|
||||
provider: HostedRemoteProvider
|
||||
}
|
||||
|
||||
const shorthandHosts: Record<string, { host: string; provider: HostedRemoteProvider }> = {
|
||||
bitbucket: { host: 'bitbucket.org', provider: 'bitbucket' },
|
||||
github: { host: 'github.com', provider: 'github' },
|
||||
gitlab: { host: 'gitlab.com', provider: 'gitlab' }
|
||||
}
|
||||
|
||||
function providerForHost(host: string): HostedRemoteProvider | null {
|
||||
const normalized = host.toLowerCase()
|
||||
if (normalized === 'github.com') {
|
||||
return 'github'
|
||||
}
|
||||
if (normalized === 'gitlab.com') {
|
||||
return 'gitlab'
|
||||
}
|
||||
if (normalized === 'bitbucket.org') {
|
||||
return 'bitbucket'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function trimGitSuffix(path: string): string {
|
||||
return path.replace(/\.git$/i, '')
|
||||
}
|
||||
|
||||
function decodeRemotePathPart(pathPart: string): string {
|
||||
try {
|
||||
return decodeURIComponent(pathPart)
|
||||
} catch {
|
||||
return pathPart
|
||||
}
|
||||
}
|
||||
|
||||
function cleanRemotePath(path: string): string | null {
|
||||
const normalized = trimGitSuffix(path.replace(/^\/+/, '').replace(/\/+$/, ''))
|
||||
const parts = normalized.split('/').filter(Boolean)
|
||||
if (parts.length < 2) {
|
||||
return null
|
||||
}
|
||||
return parts.map(decodeRemotePathPart).join('/')
|
||||
}
|
||||
|
||||
export function parseHostedRemote(remoteUrl: string): HostedRemote | null {
|
||||
const trimmed = remoteUrl.trim().replace(/^git\+/, '')
|
||||
const shorthand = trimmed.match(/^([a-z]+):([^/].+)$/i)
|
||||
if (shorthand) {
|
||||
const host = shorthandHosts[shorthand[1].toLowerCase()]
|
||||
const path = cleanRemotePath(shorthand[2])
|
||||
return host && path ? { ...host, path } : null
|
||||
}
|
||||
|
||||
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
||||
const scpLike = trimmed.match(/^(?:[^@/:]+@)?([^:\s/]+):([^\s]+)$/)
|
||||
if (scpLike) {
|
||||
const provider = providerForHost(scpLike[1])
|
||||
const path = cleanRemotePath(scpLike[2])
|
||||
return provider && path ? { host: scpLike[1].toLowerCase(), path, provider } : null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed)
|
||||
if (!['git:', 'http:', 'https:', 'ssh:'].includes(url.protocol.toLowerCase())) {
|
||||
return null
|
||||
}
|
||||
const provider = providerForHost(url.hostname)
|
||||
const path = cleanRemotePath(url.pathname)
|
||||
return provider && path ? { host: url.hostname.toLowerCase(), path, provider } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function encodeRemotePath(path: string): string {
|
||||
return path.split('/').map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
function encodeRelativePath(path: string): string {
|
||||
return path.replaceAll('\\', '/').split('/').filter(Boolean).map(encodeURIComponent).join('/')
|
||||
}
|
||||
|
||||
export function buildHostedRemoteFileUrl(
|
||||
remoteUrl: string,
|
||||
relativePath: string,
|
||||
branch: string,
|
||||
line: number
|
||||
): string | null {
|
||||
const remote = parseHostedRemote(remoteUrl)
|
||||
if (!remote) {
|
||||
return null
|
||||
}
|
||||
|
||||
const encodedRepoPath = encodeRemotePath(remote.path)
|
||||
const encodedBranch = encodeURIComponent(branch)
|
||||
const encodedFilePath = encodeRelativePath(relativePath)
|
||||
const filePathSuffix = encodedFilePath ? `/${encodedFilePath}` : ''
|
||||
const baseUrl = `https://${remote.host}/${encodedRepoPath}`
|
||||
|
||||
if (remote.provider === 'github') {
|
||||
return `${baseUrl}/blob/${encodedBranch}${filePathSuffix}#L${line}`
|
||||
}
|
||||
if (remote.provider === 'gitlab') {
|
||||
return `${baseUrl}/tree/${encodedBranch}${filePathSuffix}#L${line}`
|
||||
}
|
||||
return `${baseUrl}/src/${encodedBranch}${filePathSuffix}#L${line}`
|
||||
}
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
import { execSync } from 'child_process'
|
||||
import { existsSync, statSync } from 'fs'
|
||||
import { join, basename } from 'path'
|
||||
import hostedGitInfo from 'hosted-git-info'
|
||||
import { gitExecFileSync, gitExecFileAsync } from './runner'
|
||||
import type { BaseRefSearchResult } from '../../shared/types'
|
||||
import { buildHostedRemoteFileUrl } from './hosted-remote-url'
|
||||
|
||||
const GH_LOGIN_TIMEOUT_MS = 2500
|
||||
|
||||
|
|
@ -672,9 +672,6 @@ function isAllowedRemoteBaseRef(refName: string, allowedBaseRef: string | undefi
|
|||
/**
|
||||
* Build a hosted URL (e.g. GitHub, GitLab, Bitbucket) for a specific file
|
||||
* and line in the repo. Returns null when the remote isn't a recognized host.
|
||||
*
|
||||
* Why hosted-git-info: it handles SSH, HTTPS, and shorthand remote URLs
|
||||
* across multiple providers, so we don't have to maintain our own URL parser.
|
||||
*/
|
||||
export function getRemoteFileUrl(
|
||||
repoPath: string,
|
||||
|
|
@ -686,22 +683,11 @@ export function getRemoteFileUrl(
|
|||
return null
|
||||
}
|
||||
|
||||
const info = hostedGitInfo.fromUrl(remoteUrl)
|
||||
if (!info) {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaultBaseRef = getDefaultBaseRef(repoPath)
|
||||
if (!defaultBaseRef) {
|
||||
return null
|
||||
}
|
||||
const defaultBranch = defaultBaseRef.replace(/^origin\//, '')
|
||||
const browseUrl = info.browseFile(relativePath, { committish: defaultBranch })
|
||||
if (!browseUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: hosted-git-info lowercases the fragment, but GitHub convention
|
||||
// uses uppercase L for line links (e.g. #L42). Append manually.
|
||||
return `${browseUrl}#L${line}`
|
||||
return buildHostedRemoteFileUrl(remoteUrl, relativePath, defaultBranch, line)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1183,9 +1183,8 @@ export function registerFilesystemHandlers(
|
|||
_event,
|
||||
args: { worktreePath: string; relativePath: string; line: number; connectionId?: string }
|
||||
): Promise<string | null> => {
|
||||
// Why: remote repos can't use the local hosted-git-info approach because
|
||||
// the .git/config lives on the remote. Route through the relay's git.exec
|
||||
// to fetch the remote URL and build the file link server-side.
|
||||
// Why: remote repos can't read relay-side .git/config locally. Delegate
|
||||
// URL construction to the SSH provider, which can fetch remote metadata.
|
||||
if (args.connectionId) {
|
||||
const provider = getSshGitProvider(args.connectionId)
|
||||
if (!provider) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
small amount of param plumbing. */
|
||||
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
|
||||
import type { IGitProvider } from './types'
|
||||
import hostedGitInfo from 'hosted-git-info'
|
||||
import type {
|
||||
GitStatusResult,
|
||||
GitDiffResult,
|
||||
|
|
@ -16,6 +15,7 @@ import type {
|
|||
GitWorktreeInfo
|
||||
} from '../../shared/types'
|
||||
import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history'
|
||||
import { buildHostedRemoteFileUrl } from '../git/hosted-remote-url'
|
||||
import type { CommitMessageDraftContext } from '../../shared/commit-message-generation'
|
||||
import type { CommitMessagePlan } from '../../shared/commit-message-plan'
|
||||
import type { RemoteCommitMessageExecResult } from '../text-generation/commit-message-text-generation'
|
||||
|
|
@ -274,9 +274,8 @@ export class SshGitProvider implements IGitProvider {
|
|||
return true
|
||||
}
|
||||
|
||||
// Why: the local getRemoteFileUrl uses hosted-git-info which requires the
|
||||
// remote URL from .git/config. For SSH connections we must fetch the remote
|
||||
// URL from the relay, then apply the same hosted-git-info logic locally.
|
||||
// Why: SSH worktrees need the remote URL from the relay-side .git/config
|
||||
// before local code can map it to a hosted source link.
|
||||
async getRemoteFileUrl(
|
||||
worktreePath: string,
|
||||
relativePath: string,
|
||||
|
|
@ -293,11 +292,6 @@ export class SshGitProvider implements IGitProvider {
|
|||
return null
|
||||
}
|
||||
|
||||
const info = hostedGitInfo.fromUrl(remoteUrl)
|
||||
if (!info) {
|
||||
return null
|
||||
}
|
||||
|
||||
let defaultBranch = 'main'
|
||||
try {
|
||||
const refResult = await this.exec(
|
||||
|
|
@ -312,13 +306,6 @@ export class SshGitProvider implements IGitProvider {
|
|||
// Fall back to 'main'
|
||||
}
|
||||
|
||||
const browseUrl = info.browseFile(relativePath, { committish: defaultBranch })
|
||||
if (!browseUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: hosted-git-info lowercases the fragment, but GitHub convention
|
||||
// uses uppercase L for line links (e.g. #L42). Append manually.
|
||||
return `${browseUrl}#L${line}`
|
||||
return buildHostedRemoteFileUrl(remoteUrl, relativePath, defaultBranch, line)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
declare module 'hosted-git-info' {
|
||||
type HostedGitInfo = {
|
||||
browseFile(path: string, opts?: { committish?: string; fragment?: string }): string | undefined
|
||||
browse(path?: string, opts?: { committish?: string; fragment?: string }): string | undefined
|
||||
}
|
||||
|
||||
type HostedGitInfoStatic = {
|
||||
fromUrl(url: string): HostedGitInfo | undefined
|
||||
}
|
||||
|
||||
const hostedGitInfo: HostedGitInfoStatic
|
||||
export default hostedGitInfo
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import rehypeKatex from 'rehype-katex'
|
|||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
import { extractFrontMatter } from './markdown-frontmatter'
|
||||
import {
|
||||
Check,
|
||||
|
|
@ -230,27 +229,6 @@ const markdownPreviewSanitizeSchema = {
|
|||
}
|
||||
}
|
||||
|
||||
function getMarkdownPreviewNodeText(node: React.ReactNode): string {
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
return String(node)
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((child) => getMarkdownPreviewNodeText(child)).join('')
|
||||
}
|
||||
if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
|
||||
return getMarkdownPreviewNodeText(node.props.children)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Why: use the same GithubSlugger that rehype-slug uses internally so
|
||||
// heading IDs match standard GitHub/VS Code anchor links. The custom
|
||||
// slugger previously stripped punctuation differently, breaking links
|
||||
// like `#a--b` for headings containing `A & B`.
|
||||
function createMarkdownPreviewHeadingId(headingText: string, slugger: GithubSlugger): string {
|
||||
return slugger.slug(headingText)
|
||||
}
|
||||
|
||||
function parseLineTarget(hash: string): { line: number; column?: number } | null {
|
||||
if (!hash) {
|
||||
return null
|
||||
|
|
@ -471,7 +449,6 @@ export default function MarkdownPreview({
|
|||
.replace(/\r?\n(?:---|\+\+\+)\r?\n?$/, '')
|
||||
.trim()
|
||||
}, [frontMatter])
|
||||
const sluggerRef = useRef(new GithubSlugger())
|
||||
const [activeAnnotationBlockKey, setActiveAnnotationBlockKey] = useState<string | null>(null)
|
||||
const [reviewPanelOpen, setReviewPanelOpen] = useState(false)
|
||||
const [reviewNotesCopied, setReviewNotesCopied] = useState(false)
|
||||
|
|
@ -884,8 +861,6 @@ export default function MarkdownPreview({
|
|||
)
|
||||
|
||||
const components: Components = useMemo(() => {
|
||||
sluggerRef.current.reset()
|
||||
const slugger = sluggerRef.current
|
||||
return {
|
||||
a: ({ href, children, className, ...props }) => {
|
||||
const docLinkTarget = parseMarkdownDocLinkHref(href)
|
||||
|
|
@ -1239,61 +1214,55 @@ export default function MarkdownPreview({
|
|||
)
|
||||
},
|
||||
h1: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h1',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h1 {...props} id={id} tabIndex={-1}>
|
||||
<h1 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h1>
|
||||
)
|
||||
},
|
||||
h2: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h2',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h2 {...props} id={id} tabIndex={-1}>
|
||||
<h2 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
},
|
||||
h3: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h3',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h3 {...props} id={id} tabIndex={-1}>
|
||||
<h3 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h3>
|
||||
)
|
||||
},
|
||||
h4: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h4',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h4 {...props} id={id} tabIndex={-1}>
|
||||
<h4 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h4>
|
||||
)
|
||||
},
|
||||
h5: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h5',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h5 {...props} id={id} tabIndex={-1}>
|
||||
<h5 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h5>
|
||||
)
|
||||
},
|
||||
h6: ({ node, children, ...props }) => {
|
||||
const id = createMarkdownPreviewHeadingId(getMarkdownPreviewNodeText(children), slugger)
|
||||
return wrapAnnotatedBlock(
|
||||
'h6',
|
||||
node as MarkdownPreviewPositionNode,
|
||||
<h6 {...props} id={id} tabIndex={-1}>
|
||||
<h6 {...props} tabIndex={-1}>
|
||||
{children}
|
||||
</h6>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import GithubSlugger from 'github-slugger'
|
||||
import { MarkdownHeadingSlugger } from './markdown-heading-slug'
|
||||
|
||||
// Why: rehype-slug generates heading ids using a stateful GithubSlugger that
|
||||
// appends numeric suffixes to duplicate headings (foo, foo-1, foo-2). To keep
|
||||
// the editor's anchor matching in parity with the preview, we must use the
|
||||
// same stateful slugger — the stateless `slug()` helper would miss suffixes
|
||||
// and silently land on the wrong heading.
|
||||
// Why: duplicate headings need the same stateful suffixes as the preview
|
||||
// renderer (foo, foo-1, foo-2) or anchor links can jump to the wrong heading.
|
||||
export function scrollToAnchorInEditor(root: HTMLElement | null, anchor: string): void {
|
||||
if (!root || !anchor) {
|
||||
return
|
||||
|
|
@ -16,7 +13,7 @@ export function scrollToAnchorInEditor(root: HTMLElement | null, anchor: string)
|
|||
// Malformed %-escapes: fall back to the raw fragment.
|
||||
}
|
||||
const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
const slugger = new GithubSlugger()
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
for (const heading of headings) {
|
||||
if (slugger.slug(heading.textContent ?? '') === decoded) {
|
||||
heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { MarkdownHeadingSlugger, slugMarkdownHeading } from './markdown-heading-slug'
|
||||
|
||||
describe('markdown heading slugger', () => {
|
||||
it('matches GitHub-style punctuation and space handling used by markdown anchors', () => {
|
||||
expect(slugMarkdownHeading('A & B')).toBe('a--b')
|
||||
expect(slugMarkdownHeading('https://example.com')).toBe('httpsexamplecom')
|
||||
expect(slugMarkdownHeading('Keep_under-score')).toBe('keep_under-score')
|
||||
})
|
||||
|
||||
it('adds stable duplicate suffixes', () => {
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
|
||||
expect([slugger.slug('Repeat'), slugger.slug('Repeat'), slugger.slug('Repeat')]).toEqual([
|
||||
'repeat',
|
||||
'repeat-1',
|
||||
'repeat-2'
|
||||
])
|
||||
})
|
||||
|
||||
it('can reset duplicate state between render passes', () => {
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
|
||||
expect(slugger.slug('Repeat')).toBe('repeat')
|
||||
expect(slugger.slug('Repeat')).toBe('repeat-1')
|
||||
slugger.reset()
|
||||
expect(slugger.slug('Repeat')).toBe('repeat')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Scoped local fork of github-slugger@2.0.0 behavior.
|
||||
// Why: TOC/editor anchors need to match rehype-slug@6.0.0 preview IDs without
|
||||
// adding a direct github-slugger runtime dependency.
|
||||
const markdownSlugPunctuationPattern = /[^\p{L}\p{M}\p{N} _-]/gu
|
||||
|
||||
export class MarkdownHeadingSlugger {
|
||||
private readonly occurrences = new Map<string, number>()
|
||||
|
||||
reset(): void {
|
||||
this.occurrences.clear()
|
||||
}
|
||||
|
||||
slug(value: string): string {
|
||||
const baseSlug = slugMarkdownHeading(value)
|
||||
let nextSlug = baseSlug
|
||||
|
||||
while (this.occurrences.has(nextSlug)) {
|
||||
const nextCount = (this.occurrences.get(baseSlug) ?? 0) + 1
|
||||
this.occurrences.set(baseSlug, nextCount)
|
||||
nextSlug = `${baseSlug}-${nextCount}`
|
||||
}
|
||||
|
||||
this.occurrences.set(nextSlug, 0)
|
||||
return nextSlug
|
||||
}
|
||||
}
|
||||
|
||||
export function slugMarkdownHeading(value: string): string {
|
||||
return value.toLowerCase().replace(markdownSlugPunctuationPattern, '').replace(/ /g, '-')
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import GithubSlugger from 'github-slugger'
|
||||
import { decodeHTML } from 'entities'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import remarkFrontmatter from 'remark-frontmatter'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkParse from 'remark-parse'
|
||||
import { unified } from 'unified'
|
||||
import { MarkdownHeadingSlugger } from './markdown-heading-slug'
|
||||
|
||||
export type MarkdownTocLevel = 1 | 2 | 3
|
||||
|
||||
|
|
@ -15,12 +13,43 @@ export type MarkdownTocItem = {
|
|||
title: string
|
||||
}
|
||||
|
||||
const htmlEntitiesForToc = new Map([
|
||||
['amp', '&'],
|
||||
['apos', "'"],
|
||||
['gt', '>'],
|
||||
['lt', '<'],
|
||||
['nbsp', ' '],
|
||||
['quot', '"']
|
||||
])
|
||||
|
||||
function isMarkdownTocLevel(value: number): value is MarkdownTocLevel {
|
||||
return value === 1 || value === 2 || value === 3
|
||||
}
|
||||
|
||||
// Scoped local fork of the tiny entities@6.0.1 surface Orca used here.
|
||||
// Why: TOC labels only need common/numeric entity decoding before inline
|
||||
// Markdown stripping, not the full entity database.
|
||||
function decodeTocHtmlEntities(text: string): string {
|
||||
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);/gi, (match, entity: string) => {
|
||||
const normalized = entity.toLowerCase()
|
||||
if (normalized.startsWith('#x')) {
|
||||
const codePoint = Number.parseInt(normalized.slice(2), 16)
|
||||
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff
|
||||
? String.fromCodePoint(codePoint)
|
||||
: match
|
||||
}
|
||||
if (normalized.startsWith('#')) {
|
||||
const codePoint = Number.parseInt(normalized.slice(1), 10)
|
||||
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff
|
||||
? String.fromCodePoint(codePoint)
|
||||
: match
|
||||
}
|
||||
return htmlEntitiesForToc.get(normalized) ?? match
|
||||
})
|
||||
}
|
||||
|
||||
export function stripInlineMarkdownForToc(text: string): string {
|
||||
return decodeHTML(text)
|
||||
return decodeTocHtmlEntities(text)
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/\[\[[^|\]]+\|([^\]]+)\]\]/g, '$1')
|
||||
|
|
@ -48,18 +77,33 @@ function appendTocItem(stack: MarkdownTocItem[], item: MarkdownTocItem): void {
|
|||
}
|
||||
|
||||
type MarkdownAstNode = {
|
||||
alt?: string
|
||||
children?: MarkdownAstNode[]
|
||||
depth?: number
|
||||
type?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
// Scoped local fork of mdast-util-to-string@4.0.0 for heading nodes.
|
||||
// Why: TOC generation only needs text/alt/child concatenation from parsed
|
||||
// Markdown headings, so a local walker keeps the dependency boundary smaller.
|
||||
function markdownAstNodeToText(node: MarkdownAstNode): string {
|
||||
if (typeof node.value === 'string') {
|
||||
return node.value
|
||||
}
|
||||
if (typeof node.alt === 'string') {
|
||||
return node.alt
|
||||
}
|
||||
return (node.children ?? []).map(markdownAstNodeToText).join('')
|
||||
}
|
||||
|
||||
export function buildMarkdownTableOfContents(markdown: string): MarkdownTocItem[] {
|
||||
const slugger = new GithubSlugger()
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
const root: MarkdownTocItem = { id: 'toc-root', level: 1, title: '', children: [] }
|
||||
const stack: MarkdownTocItem[] = [root]
|
||||
|
||||
// Why: the TOC must produce the same heading text/ids as react-markdown plus
|
||||
// rehype-slug; parsing Markdown avoids drift on setext, GFM, and entities.
|
||||
// Why: parsing Markdown keeps the TOC aligned with rendered setext/GFM/entity
|
||||
// headings without carrying separate mdast stringifier/entity packages.
|
||||
const tree = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
|
|
@ -72,7 +116,7 @@ export function buildMarkdownTableOfContents(markdown: string): MarkdownTocItem[
|
|||
typeof node.depth === 'number' &&
|
||||
isMarkdownTocLevel(node.depth)
|
||||
) {
|
||||
const title = toString(node).replace(/\s+/g, ' ').trim()
|
||||
const title = markdownAstNodeToText(node).replace(/\s+/g, ' ').trim()
|
||||
if (title) {
|
||||
appendTocItem(stack, {
|
||||
children: [],
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { createRoot } from 'react-dom/client'
|
|||
import App from './App'
|
||||
import { applyDocumentTheme } from './lib/document-theme'
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
import('react-grab').then(({ init }) => init())
|
||||
import('react-grab/styles.css')
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_ENABLE_REACT_GRAB === 'true') {
|
||||
void import('react-grab').then(({ init }) => init())
|
||||
void import('react-grab/styles.css')
|
||||
}
|
||||
|
||||
applyDocumentTheme('system', { disableTransitions: false })
|
||||
|
|
|
|||
Loading…
Reference in New Issue