From e8b1a8e167a53b50ea9ae5279fa6cd72f416b3d5 Mon Sep 17 00:00:00 2001 From: dawn Date: Thu, 30 Jul 2026 16:15:12 +0300 Subject: [PATCH] web/components: add pierre file and diff viewers Signed-off-by: dawn --- web/package.json | 1 + web/pnpm-lock.yaml | 461 +++++++++++++++++- web/src/app.css | 1 + web/src/lib/api/diff.ts | 74 +++ web/src/lib/api/rawdiff.ts | 132 +++++ web/src/lib/api/repo.ts | 2 +- .../components/repo/PierreDiff.stories.svelte | 191 ++++++++ web/src/lib/components/repo/PierreDiff.svelte | 74 +++ .../components/repo/PierreFile.stories.svelte | 50 ++ web/src/lib/components/repo/PierreFile.svelte | 73 +++ web/src/lib/components/repo/fileDiff.test.ts | 142 ++++++ web/src/lib/components/repo/fileDiff.ts | 146 ++++++ web/src/lib/components/repo/pierre.ts | 100 ++++ web/src/pierre.css | 55 +++ 14 files changed, 1476 insertions(+), 26 deletions(-) create mode 100644 web/src/lib/api/diff.ts create mode 100644 web/src/lib/api/rawdiff.ts create mode 100644 web/src/lib/components/repo/PierreDiff.stories.svelte create mode 100644 web/src/lib/components/repo/PierreDiff.svelte create mode 100644 web/src/lib/components/repo/PierreFile.stories.svelte create mode 100644 web/src/lib/components/repo/PierreFile.svelte create mode 100644 web/src/lib/components/repo/fileDiff.test.ts create mode 100644 web/src/lib/components/repo/fileDiff.ts create mode 100644 web/src/lib/components/repo/pierre.ts create mode 100644 web/src/pierre.css diff --git a/web/package.json b/web/package.json index 3efbd66e..41983753 100644 --- a/web/package.json +++ b/web/package.json @@ -76,6 +76,7 @@ "@mdit/plugin-alert": "^1.0.1", "@mdit/plugin-footnote": "^1.0.1", "@mdit/plugin-tasklist": "^1.0.1", + "@pierre/diffs": "^1.2.12", "markdown-it": "^14.3.0", "markdown-it-anchor": "^9.2.1", "markdown-it-emoji": "^3.1.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4bac628a..048ceb39 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@mdit/plugin-tasklist': specifier: ^1.0.1 version: 1.0.1(markdown-it@14.3.0) + '@pierre/diffs': + specifier: ^1.2.12 + version: 1.2.12(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) markdown-it: specifier: ^14.3.0 version: 14.3.0 @@ -128,7 +131,7 @@ importers: version: 4.7.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(tailwindcss@4.3.2)(typescript@6.0.3) eslint-plugin-storybook: specifier: ^10.5.0 - version: 10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@7.2.0)(typescript@6.0.3) + version: 10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@10.2.2)(typescript@6.0.3) eslint-plugin-svelte: specifier: ^3.20.0 version: 3.20.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) @@ -164,7 +167,7 @@ importers: version: 6.0.3 typescript-eslint: specifier: ^8.64.0 - version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) unplugin-icons: specifier: ^23.0.1 version: 23.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)) @@ -1108,6 +1111,36 @@ packages: cpu: [x64] os: [win32] + '@pierre/diffs@1.2.12': + resolution: {integrity: sha512-pY/gmgWL03WnagqCyCnBi3QtRXUv4hCIY6FYqd5b1ZGaoI6a4Bsji8j+yRl2RfzPh/8Hf19rCl1GE80G6a1cLQ==} + peerDependencies: + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 + + '@pierre/theme@1.1.0': + resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} + engines: {vscode: ^1.0.0} + + '@pierre/theming@0.0.2': + resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} + peerDependencies: + '@pierre/theme': ^1.1.0 + '@shikijs/themes': ^3.0.0 || ^4.0.0 + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 + shiki: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@pierre/theme': + optional: true + '@shikijs/themes': + optional: true + react: + optional: true + react-dom: + optional: true + shiki: + optional: true + '@pkgr/core@0.3.6': resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1410,6 +1443,41 @@ packages: cpu: [x64] os: [win32] + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + + '@shikijs/transformers@4.3.1': + resolution: {integrity: sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A==} + engines: {node: '>=20'} + + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -1703,6 +1771,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1715,6 +1786,9 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} @@ -1736,6 +1810,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.64.0': resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1795,6 +1872,9 @@ packages: resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@valibot/to-json-schema@1.7.1': resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} peerDependencies: @@ -1941,6 +2021,9 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1953,6 +2036,12 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -1987,6 +2076,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -2079,6 +2171,13 @@ packages: devalue@5.8.1: resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -2349,9 +2448,18 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -2574,6 +2682,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru_map@0.4.1: + resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -2601,12 +2712,30 @@ packages: resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdn-data@2.29.0: resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} mdurl@2.1.0: resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2654,6 +2783,12 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -2838,6 +2973,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -2873,6 +3011,15 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexparam@3.0.0: resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} engines: {node: '>=8'} @@ -2930,6 +3077,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2945,6 +3096,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2966,6 +3120,9 @@ packages: vite-plus: optional: true + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} @@ -3090,6 +3247,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -3159,6 +3319,21 @@ packages: unicode-segmenter@0.14.5: resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3203,6 +3378,12 @@ packages: typescript: optional: true + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.1.4: resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3384,6 +3565,9 @@ packages: zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.5.0': {} @@ -4140,6 +4324,30 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true + '@pierre/diffs@1.2.12(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@pierre/theme': 1.1.0 + '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(shiki@4.3.1) + '@shikijs/transformers': 4.3.1 + diff: 9.0.0 + hast-util-to-html: 9.0.5 + lru_map: 0.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + shiki: 4.3.1 + transitivePeerDependencies: + - '@shikijs/themes' + + '@pierre/theme@1.1.0': {} + + '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(shiki@4.3.1)': + optionalDependencies: + '@pierre/theme': 1.1.0 + '@shikijs/themes': 4.3.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + shiki: 4.3.1 + '@pkgr/core@0.3.6': {} '@playwright/test@1.61.1': @@ -4329,6 +4537,51 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/transformers@4.3.1': + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/types': 4.3.1 + + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@7.2.0': {} '@speed-highlight/core@1.2.17': {} @@ -4628,6 +4881,10 @@ snapshots: '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/linkify-it@5.0.0': {} @@ -4641,6 +4898,10 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} '@types/mdx@2.0.14': {} @@ -4661,13 +4922,15 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.64.0 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.6 @@ -4677,11 +4940,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3(supports-color@7.2.0) eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) @@ -4689,11 +4952,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.64.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) '@typescript-eslint/types': 8.64.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4707,11 +4970,11 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) debug: 4.4.3(supports-color@7.2.0) eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@6.0.3) @@ -4721,13 +4984,13 @@ snapshots: '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.64.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/project-service': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) '@typescript-eslint/types': 8.64.0 '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 @@ -4736,12 +4999,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: @@ -4752,6 +5015,8 @@ snapshots: '@typescript-eslint/types': 8.64.0 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@6.0.3))': dependencies: valibot: 1.4.2(typescript@6.0.3) @@ -4928,6 +5193,8 @@ snapshots: dependencies: run-applescript: 7.1.0 + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -4943,6 +5210,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + check-error@2.1.3: {} chokidar@4.0.3: @@ -4961,6 +5232,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commondir@1.0.1: {} confbox@0.1.8: {} @@ -4987,6 +5260,12 @@ snapshots: dayjs@1.11.21: {} + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 @@ -5018,6 +5297,12 @@ snapshots: devalue@5.8.1: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@9.0.0: {} + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -5134,10 +5419,10 @@ snapshots: - '@eslint/css' - typescript - eslint-plugin-storybook@10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-storybook@10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@10.2.2)(typescript@6.0.3): dependencies: '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) storybook: 10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7) transitivePeerDependencies: @@ -5320,8 +5605,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -5501,6 +5806,8 @@ snapshots: loupe@3.2.1: {} + lru_map@0.4.1: {} + lz-string@1.5.0: {} magic-string@0.30.21: @@ -5533,10 +5840,39 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdn-data@2.29.0: {} mdurl@2.1.0: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + min-indent@1.0.1: {} miniflare@4.20260722.1: @@ -5578,6 +5914,14 @@ snapshots: obug@2.1.3: {} + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@10.2.0: dependencies: default-browser: 5.5.0 @@ -5738,6 +6082,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + property-information@7.2.0: {} + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -5768,6 +6114,16 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexparam@3.0.0: {} resolve@1.22.12: @@ -5891,6 +6247,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + siginfo@2.0.0: {} sirv@3.0.2: @@ -5903,6 +6270,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} std-env@4.2.0: {} @@ -5934,6 +6303,11 @@ snapshots: - react - utf-8-validate + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -6050,6 +6424,8 @@ snapshots: totalist@3.0.1: {} + trim-lines@3.0.1: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -6081,12 +6457,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: @@ -6110,6 +6486,29 @@ snapshots: unicode-segmenter@0.14.5: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} unplugin-icons@23.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)): @@ -6143,6 +6542,16 @@ snapshots: optionalDependencies: typescript: 6.0.3 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -6263,3 +6672,5 @@ snapshots: zimmerframe@1.1.2: {} zimmerframe@1.1.4: {} + + zwitch@2.0.4: {} diff --git a/web/src/app.css b/web/src/app.css index 3629ac98..6e5644a2 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "./markup.css"; +@import "./pierre.css"; @theme { --font-sans: "InterVariable", "system-ui", sans-serif, ui-sans-serif; diff --git a/web/src/lib/api/diff.ts b/web/src/lib/api/diff.ts new file mode 100644 index 00000000..a28ab05c --- /dev/null +++ b/web/src/lib/api/diff.ts @@ -0,0 +1,74 @@ +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { diff as knotDiff } from "./knot"; +import type { GitSignature } from "./repo"; + +export interface NiceCommit { + hash?: number[]; + // hex form of the hash bytes above + this?: string; + // hex of the first parent, empty for a root commit + parent?: string; + parent_hashes?: number[][]; + author?: GitSignature; + committer?: GitSignature; + message?: string; + tree?: string; + change_id?: string; + pgp_signature?: string; +} + +export interface DiffStat { + insertions: number; + deletions: number; + files_changed: number; +} + +export const LineOp = { Context: 0, Delete: 1, Add: 2 } as const; +export type LineOp = (typeof LineOp)[keyof typeof LineOp]; + +export interface DiffLine { + Op: LineOp; + Line: string; +} + +export interface TextFragment { + Comment?: string; + OldPosition?: number; + OldLines?: number; + NewPosition?: number; + NewLines?: number; + LinesAdded?: number; + LinesDeleted?: number; + LeadingContext?: number; + TrailingContext?: number; + Lines?: DiffLine[]; +} + +export interface DiffFile { + name: { old: string; new: string }; + text_fragments?: TextFragment[]; + is_binary?: boolean; + is_new?: boolean; + is_delete?: boolean; + is_copy?: boolean; + is_rename?: boolean; +} + +export interface NiceDiff { + commit?: NiceCommit; + stat?: DiffStat; + diff?: DiffFile[]; +} + +// the new path, or the old one for deletions +export const diffFileName = (file: DiffFile): string => file.name.new || file.name.old; + +export interface RepoCommitResponse { + ref?: string; + diff?: NiceDiff; +} + +// the mirror's temp surface has no diff endpoint, diffs go through +// bobbin's knot proxy +export const diffFor = (ctx: BobbinContext, repo: string, ref: string, init?: XrpcRequestInit) => + knotDiff(ctx, { repo, ref }, init); diff --git a/web/src/lib/api/rawdiff.ts b/web/src/lib/api/rawdiff.ts new file mode 100644 index 00000000..3e26aeea --- /dev/null +++ b/web/src/lib/api/rawdiff.ts @@ -0,0 +1,132 @@ +import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "./diff"; +import { hexFromBytes } from "./repo"; + +// port of appview/repo/rawdiff.go + +const OP_CHARS = [" ", "-", "+"] as const; + +const opChar = (op: number): string => OP_CHARS[op] ?? "?"; + +const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n"); + +const fragmentHeader = (fragment: TextFragment): string => { + const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`; + return fragment.Comment ? `${header} ${fragment.Comment}` : header; +}; + +const fragmentString = (fragment: TextFragment): string => { + let out = `${fragmentHeader(fragment)}\n`; + for (const line of fragment.Lines ?? []) { + out += opChar(line.Op) + line.Line; + if (noEol(line.Line)) out += "\n\\ No newline at end of file\n"; + } + return out; +}; + +export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => { + let insertions = 0; + let deletions = 0; + for (const fragment of file.text_fragments ?? []) { + insertions += fragment.LinesAdded ?? 0; + deletions += fragment.LinesDeleted ?? 0; + } + return { insertions, deletions }; +}; + +export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => { + if (!niceDiff) return ""; + let out = ""; + for (const file of niceDiff.diff ?? []) { + const oldName = file.name.old || file.name.new; + const newName = file.name.new || file.name.old; + + out += `diff --git a/${oldName} b/${newName}\n`; + if (file.is_new) { + out += "new file mode 100644\n"; + out += "--- /dev/null\n"; + out += `+++ b/${newName}\n`; + } else if (file.is_delete) { + out += "deleted file mode 100644\n"; + out += `--- a/${oldName}\n`; + out += "+++ /dev/null\n"; + } else if (file.is_rename) { + out += `rename from ${oldName}\n`; + out += `rename to ${newName}\n`; + out += `--- a/${oldName}\n`; + out += `+++ b/${newName}\n`; + } else { + out += `--- a/${oldName}\n`; + out += `+++ b/${newName}\n`; + } + + for (const fragment of file.text_fragments ?? []) { + out += fragmentString(fragment); + } + } + return out; +}; + +const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const; +const MONTHS = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" +] as const; + +const pad = (value: number): string => String(value).padStart(2, "0"); + +// RFC1123Z, always utc +const formatPatchDate = (input: string): string => { + const date = new Date(input); +// a missing When is go's zero time, formats to this exact string + if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000"; + return ( + `${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` + + `${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000` + ); +}; + +export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => { + if (!niceDiff) return ""; + const commit = niceDiff.commit ?? {}; + const message = commit.message ?? ""; + + // format-patch takes the first LINE as the subject, unlike splitMessage's + // first paragraph + const newline = message.indexOf("\n"); + const subject = newline === -1 ? message : message.slice(0, newline); + + const separator = message.indexOf("\n\n"); + const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, ""); + + const hash = commit.this || hexFromBytes(commit.hash ?? []); + + let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`; + out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`; + out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`; + out += `Subject: [PATCH] ${subject}\n`; + out += "\n"; + if (body !== "") out += `${body}\n`; + out += "---\n"; + + for (const file of niceDiff.diff ?? []) { + const name = diffFileName(file); + const { insertions, deletions } = fileStats(file); + out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`; + } + const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 }; + out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`; + + out += renderUnifiedDiff(niceDiff); + out += "\n--\ntangled.sh\n"; + return out; +}; diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index 821340b7..d09d68d8 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -168,7 +168,7 @@ export interface TagSummary { message?: string; } -const hexFromBytes = (bytes: number[]): string => +export const hexFromBytes = (bytes: number[]): string => bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); export const toTagSummary = (tag: TagEntry): TagSummary => { diff --git a/web/src/lib/components/repo/PierreDiff.stories.svelte b/web/src/lib/components/repo/PierreDiff.stories.svelte new file mode 100644 index 00000000..32e423ea --- /dev/null +++ b/web/src/lib/components/repo/PierreDiff.stories.svelte @@ -0,0 +1,191 @@ + + + { + await appviewChrome(ctx); + await expectSplitColumns(false)(ctx); + await separatorNotClipped(ctx); + }} +/> + + { + await expectSplitColumns(true)(ctx); + await separatorNotClipped(ctx); + }} +/> + + { + await waitFor(() => expect(shadowText(canvasElement)).toContain("19 unmodified lines")); + const host = hostsOf(canvasElement)[0]; + const separator = host.shadowRoot?.querySelector("[data-separator='line-info']"); + expect(separator).not.toBeNull(); + expect(separator?.hasAttribute("data-separator-first")).toBe(true); + }} +/> diff --git a/web/src/lib/components/repo/PierreDiff.svelte b/web/src/lib/components/repo/PierreDiff.svelte new file mode 100644 index 00000000..14fbdb69 --- /dev/null +++ b/web/src/lib/components/repo/PierreDiff.svelte @@ -0,0 +1,74 @@ + + +
+ {#each files as file, index (index)} + + {#if prerenderedHTML[index]} + + + {/if} + + {/each} +
diff --git a/web/src/lib/components/repo/PierreFile.stories.svelte b/web/src/lib/components/repo/PierreFile.stories.svelte new file mode 100644 index 00000000..693d133a --- /dev/null +++ b/web/src/lib/components/repo/PierreFile.stories.svelte @@ -0,0 +1,50 @@ + + + + + { + await rendersContents(context); + const host = context.canvasElement.querySelector("diffs-container"); + await waitFor(() => + expect(host?.shadowRoot?.querySelector("[data-overflow='wrap']")).toBeTruthy() + ); + await waitFor(() => + expect(host?.shadowRoot?.querySelector("[data-line] span[style]")).toBeTruthy() + ); + }} +/> diff --git a/web/src/lib/components/repo/PierreFile.svelte b/web/src/lib/components/repo/PierreFile.svelte new file mode 100644 index 00000000..39c5d206 --- /dev/null +++ b/web/src/lib/components/repo/PierreFile.svelte @@ -0,0 +1,73 @@ + + + + {#if prerenderedHTML} + + + {/if} + diff --git a/web/src/lib/components/repo/fileDiff.test.ts b/web/src/lib/components/repo/fileDiff.test.ts new file mode 100644 index 00000000..f4013686 --- /dev/null +++ b/web/src/lib/components/repo/fileDiff.test.ts @@ -0,0 +1,142 @@ +import { processFile, type FileDiffMetadata } from "@pierre/diffs"; +import { describe, expect, it } from "vitest"; +import { LineOp, type DiffFile } from "$lib/api/diff"; +import { renderUnifiedDiff } from "$lib/api/rawdiff"; +import { toFileDiffMetadata } from "./fileDiff"; + +// the mapper must produce exactly what pierre's own parser +// produces from the rendered text! +// +// excluded: type/prevName, the parser +// can only get it from git header lines the renderer doesn't emit (mode too), +// cacheKey, only worker pools use it, and hunkSpecs +const matchesParsedPatch = (file: DiffFile) => { + const parsed = processFile(renderUnifiedDiff({ diff: [file] })); + expect(parsed).toBeDefined(); + const stripKeys = (value: object, keys: string[]): Record => + Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key))); + const strip = (meta: FileDiffMetadata) => ({ + ...stripKeys(meta, ["type", "prevName", "mode", "cacheKey"]), + hunks: meta.hunks.map((hunk) => stripKeys(hunk, ["hunkSpecs"])) + }); + expect(strip(toFileDiffMetadata(file))).toEqual(strip(parsed!)); +}; + +describe("toFileDiffMetadata", () => { + it("maps a modified file", () => { + matchesParsedPatch({ + name: { old: "foo.go", new: "foo.go" }, + text_fragments: [ + { + Comment: "func main()", + OldPosition: 1, + OldLines: 3, + NewPosition: 1, + NewLines: 3, + Lines: [ + { Op: LineOp.Context, Line: "package main\n" }, + { Op: LineOp.Delete, Line: "// old comment\n" }, + { Op: LineOp.Add, Line: "// new comment\n" }, + { Op: LineOp.Context, Line: "func main() {}\n" } + ] + } + ] + }); + }); + + it("maps a new file", () => { + matchesParsedPatch({ + name: { old: "", new: "new.go" }, + is_new: true, + text_fragments: [ + { + OldPosition: 0, + OldLines: 0, + NewPosition: 1, + NewLines: 2, + Lines: [ + { Op: LineOp.Add, Line: "package main\n" }, + { Op: LineOp.Add, Line: "func main() {}\n" } + ] + } + ] + }); + }); + + it("maps a deleted file", () => { + matchesParsedPatch({ + name: { old: "old.go", new: "" }, + is_delete: true, + text_fragments: [ + { + OldPosition: 1, + OldLines: 2, + NewPosition: 0, + NewLines: 0, + Lines: [ + { Op: LineOp.Delete, Line: "package main\n" }, + { Op: LineOp.Delete, Line: "func main() {}\n" } + ] + } + ] + }); + }); + + it("maps a renamed file with multiple fragments", () => { + matchesParsedPatch({ + name: { old: "old.go", new: "renamed.go" }, + is_rename: true, + text_fragments: [ + { + OldPosition: 1, + OldLines: 2, + NewPosition: 1, + NewLines: 2, + Lines: [ + { Op: LineOp.Context, Line: "package main\n" }, + { Op: LineOp.Delete, Line: "func old() {}\n" }, + { Op: LineOp.Add, Line: "func renamed() {}\n" } + ] + }, + { + Comment: "func init()", + OldPosition: 10, + OldLines: 1, + NewPosition: 10, + NewLines: 1, + Lines: [{ Op: LineOp.Context, Line: "var x = 1\n" }] + } + ] + }); + }); + + it("maps a missing trailing newline", () => { + matchesParsedPatch({ + name: { old: "foo.go", new: "foo.go" }, + text_fragments: [ + { + OldPosition: 1, + OldLines: 2, + NewPosition: 1, + NewLines: 2, + Lines: [ + { Op: LineOp.Delete, Line: "old" }, + { Op: LineOp.Add, Line: "new\n" }, + { Op: LineOp.Context, Line: "tail" } + ] + } + ] + }); + }); + + it("marks the file type from the wire flags", () => { + expect(toFileDiffMetadata({ name: { old: "", new: "n.go" }, is_new: true }).type).toBe("new"); + expect(toFileDiffMetadata({ name: { old: "o.go", new: "" }, is_delete: true }).type).toBe( + "deleted" + ); + const renamed = toFileDiffMetadata({ name: { old: "o.go", new: "n.go" }, is_rename: true }); + expect(renamed.type).toBe("rename-changed"); + expect(renamed.prevName).toBe("o.go"); + expect(toFileDiffMetadata({ name: { old: "n.go", new: "n.go" } }).prevName).toBeUndefined(); + }); +}); diff --git a/web/src/lib/components/repo/fileDiff.ts b/web/src/lib/components/repo/fileDiff.ts new file mode 100644 index 00000000..68b712e9 --- /dev/null +++ b/web/src/lib/components/repo/fileDiff.ts @@ -0,0 +1,146 @@ +import type { ChangeContent, ContextContent, FileDiffMetadata, Hunk } from "@pierre/diffs"; +import { LineOp, type DiffFile } from "$lib/api/diff"; + +// old alone collides when one file is deleted and another renamed onto its +// path in the same commit so we need new too +export const diffRowKey = (file: DiffFile): string => `${file.name.old}→${file.name.new}`; + +// pierre parses patch text into FileDiffMetadata, but the wire already hands +// us hunks, so we can skip the text roundtrip by mapping +export const toFileDiffMetadata = (file: DiffFile): FileDiffMetadata => { + const fragments = file.text_fragments ?? []; + + const hunks: Hunk[] = []; + const deletionLines: string[] = []; + const additionLines: string[] = []; + let splitLineCount = 0; + let unifiedLineCount = 0; + let lastHunkEnd = 0; + + for (const fragment of fragments) { + const additionStart = fragment.NewPosition ?? 0; + const additionCount = fragment.NewLines ?? 0; + const deletionStart = fragment.OldPosition ?? 0; + const deletionCount = fragment.OldLines ?? 0; + + const collapsedBefore = Math.max(additionStart - 1 - lastHunkEnd, 0); + lastHunkEnd = additionStart + additionCount - 1; + + const hunk: Hunk = { + collapsedBefore, + additionStart, + additionCount, + additionLines: 0, + additionLineIndex: additionLines.length, + deletionStart, + deletionCount, + deletionLines: 0, + deletionLineIndex: deletionLines.length, + hunkContent: [], + hunkContext: fragment.Comment, + hunkSpecs: `@@ -${deletionStart},${deletionCount} +${additionStart},${additionCount} @@`, + splitLineStart: 0, + splitLineCount: 0, + unifiedLineStart: 0, + unifiedLineCount: 0, + noEOFCRDeletions: false, + noEOFCRAdditions: false + }; + + let current: ContextContent | ChangeContent | null = null; + let lastOp: LineOp | null = null; + for (const { Op: op, Line: raw } of fragment.Lines ?? []) { + if (op === LineOp.Context) { + if (current?.type !== "context") { + current = { + type: "context", + lines: 0, + additionLineIndex: additionLines.length, + deletionLineIndex: deletionLines.length + }; + hunk.hunkContent.push(current); + } + current.lines++; + additionLines.push(raw); + deletionLines.push(raw); + } else { + if (current?.type !== "change") { + current = { + type: "change", + additions: 0, + deletions: 0, + additionLineIndex: additionLines.length, + deletionLineIndex: deletionLines.length + }; + hunk.hunkContent.push(current); + } + if (op === LineOp.Add) { + current.additions++; + hunk.additionLines++; + additionLines.push(raw); + } else { + current.deletions++; + hunk.deletionLines++; + deletionLines.push(raw); + } + } + lastOp = op; + } + + // a line without a trailing newline is the patch's "\ No newline" + // marker, only possible at the end of the file + const last = fragment.Lines?.[fragment.Lines.length - 1]; + if (last && !last.Line.endsWith("\n")) { + if (last.Op === LineOp.Context) { + hunk.noEOFCRAdditions = true; + hunk.noEOFCRDeletions = true; + } else if (last.Op === LineOp.Delete) { + hunk.noEOFCRDeletions = true; + } else { + hunk.noEOFCRAdditions = true; + } + if (lastOp === LineOp.Add || lastOp === LineOp.Context) { + additionLines[additionLines.length - 1] = last.Line; + } + if (lastOp === LineOp.Delete || lastOp === LineOp.Context) { + deletionLines[deletionLines.length - 1] = last.Line; + } + } + + for (const content of hunk.hunkContent) { + if (content.type === "context") { + hunk.splitLineCount += content.lines; + hunk.unifiedLineCount += content.lines; + } else { + hunk.splitLineCount += Math.max(content.additions, content.deletions); + hunk.unifiedLineCount += content.deletions + content.additions; + } + } + hunk.splitLineStart = splitLineCount + collapsedBefore; + hunk.unifiedLineStart = unifiedLineCount + collapsedBefore; + splitLineCount += collapsedBefore + hunk.splitLineCount; + unifiedLineCount += collapsedBefore + hunk.unifiedLineCount; + + hunks.push(hunk); + } + + const type: FileDiffMetadata["type"] = file.is_new + ? "new" + : file.is_delete + ? "deleted" + : file.is_rename + ? "rename-changed" + : "change"; + + return { + name: file.name.new || file.name.old, + prevName: type === "rename-changed" ? file.name.old : undefined, + type, + hunks, + splitLineCount, + unifiedLineCount, + isPartial: true, + deletionLines, + additionLines + }; +}; diff --git a/web/src/lib/components/repo/pierre.ts b/web/src/lib/components/repo/pierre.ts new file mode 100644 index 00000000..fb428b77 --- /dev/null +++ b/web/src/lib/components/repo/pierre.ts @@ -0,0 +1,100 @@ +// the css side of pierre theming lives in src/pierre.css + +import { + getSharedHighlighter, + registerCustomCSSVariableTheme, + type FileDiffOptions, + type FileOptions +} from "@pierre/diffs"; +import { browser } from "$app/environment"; + +// pierre's two diff layouts, threaded from the ?diff= url param down to the +// FileDiff instances +export type DiffStyle = NonNullable["diffStyle"]>; + +// pierre's theming hook is a registered shiki css-variable theme. the token +// colors come from --diffs-token-* properties in pierre.css, they flip +// with the media query. the name is pinned by app.css's @utility or tailwind +// tree-shakes those rules away +export const PIERRE_THEME_NAME = "tangled"; + +registerCustomCSSVariableTheme(PIERRE_THEME_NAME, { + // pierre.css defines the real tokens on diffs-container, these are just + // fallbacks for the generated bindings + background: "#ffffff", + foreground: "#111827", + "token-comment": "#6b7280", + "token-string": "#16a34a", + "token-string-expression": "#16a34a", + "token-keyword": "#c084fc", + "token-function": "#60a5fa", + "token-constant": "#f59e0b", + "token-parameter": "#111827", + "token-punctuation": "#4b5563", + "token-link": "#4338ca", + "token-inserted": "#16a34a", + "token-deleted": "#dc2626", + "token-changed": "#d97706" +}); + +// pierre skips highlighting lines past 1e3 chars by default. +// lockfiles however do not care... +export const PIERRE_TOKENIZE_MAX_LINE_LENGTH = 5000; + +// "line-info" separators carry pierre's "N unmodified lines" text, bare +// "simple" ones get a "···" +export const PIERRE_SEPARATOR_CSS = + // flush against our own file header, pierre's is disabled + ":host{--diffs-gap-block:0}" + + // overflow auto so the bottom scrollbar only exists when a line overflows + ":host{--diffs-overflow-override:auto}" + + "[data-separator]{block-size:21px;font-size:14px;line-height:21px;" + + "color:var(--diffs-fg-number-override,#9ca3af)}" + + // kill pierre's block gap, padding and rounded chip, the separator reads + // as one full-width row + '[data-separator="line-info"]{margin-block:0}' + + '[data-unified] [data-separator="line-info"] [data-separator-wrapper]{padding-inline:0}' + + '[data-gutter] [data-separator="line-info"] [data-separator-wrapper]{padding-left:0}' + + // pierre full-widths the unified separator and the split additions bar but + // not the split deletions one, that one carries the count text and clips in + // the 44px gutter + '[data-deletions] [data-gutter] [data-separator="line-info"] [data-separator-wrapper]{width:100cqi}' + + '[data-separator="line-info"] [data-separator-content]{border-radius:0}' + + '[data-separator="line-info"] [data-expand-button]{border-radius:0}' + + '[data-additions] [data-gutter] [data-separator="line-info"] [data-separator-wrapper]{border-radius:0}' + + "[data-separator-content]{justify-content:center}" + + '[data-separator="simple"]{display:flex;align-items:center;justify-content:center}' + + '[data-separator="simple"]::after{content:"···"}'; + +export const PIERRE_BASE_OPTIONS = { + theme: PIERRE_THEME_NAME, + themeType: "system", + tokenizeMaxLineLength: PIERRE_TOKENIZE_MAX_LINE_LENGTH +} as const; + +// shared by the components and the ssr preloads, the prerendered markup is +// only adoptable when both sides build the same options +export const pierreDiffOptions = (style: DiffStyle): FileDiffOptions => ({ + ...PIERRE_BASE_OPTIONS, + diffStyle: style, + diffIndicators: "classic", + lineDiffType: "word-alt", + // "line-info" separators size the bar to span the whole diff. + // "line-info-basic" never gets that, its count text clips in the 44px + // gutter cell + hunkSeparators: "line-info", + disableFileHeader: true, + overflow: "scroll", + unsafeCSS: PIERRE_SEPARATOR_CSS +}); + +export const pierreFileOptions = (wrap: boolean): FileOptions => ({ + ...PIERRE_BASE_OPTIONS, + disableFileHeader: true, + overflow: wrap ? "wrap" : "scroll" +}); + +// a renderer with a missing language paints plain text first and swaps in +// the highlight, but only once the shared highlighter is up. before that the +// first render waits on the shiki import and paints nothing, so warm it here +if (browser) void getSharedHighlighter({ themes: [PIERRE_THEME_NAME], langs: [] }); diff --git a/web/src/pierre.css b/web/src/pierre.css new file mode 100644 index 00000000..971b561c --- /dev/null +++ b/web/src/pierre.css @@ -0,0 +1,55 @@ +/* pierre's -override properties must land on the diffs-container element + itself, its :host stylesheet beats inheriting from a wrapper. these rules + must stay UNLAYERED: pierre injects its :host stylesheet unlayered, which + beats anything in @layer. the doubled class lifts specificity above it + regardless of order */ + +diffs-container.diffs-container { + --diffs-font-family: var(--font-mono); + --diffs-font-size: 0.875rem; + --diffs-line-height: 1.5; + /* the "tangled" theme (pierre.ts) binds --diffs-bg/--diffs-fg to these and + forces color-scheme: dark. without them the bg chain goes + guaranteed-invalid and light-dark() is stuck on the dark branch */ + --diffs-background: var(--color-background-default); + --diffs-foreground: var(--color-foreground-default); + color-scheme: light dark; + --diffs-light-bg: var(--color-background-default); + --diffs-dark-bg: var(--color-background-default); + --diffs-bg-separator-override: var(--color-background-inset); + --diffs-fg-number-override: var(--color-foreground-placeholder); + + /* pierre blends line backgrounds 88/12 (light) toward these, so they must + be the saturated tokens. the *-color overrides drive the indicators and + number gutter */ + --diffs-bg-addition-override: var(--color-foreground-success); + --diffs-bg-deletion-override: var(--color-foreground-danger); + --diffs-addition-color-override: var(--color-foreground-success-strong); + --diffs-deletion-color-override: var(--color-foreground-danger-strong); + + /* syntax palette for the "tangled" theme registered in pierre.ts */ + --diffs-token-comment: var(--color-foreground-placeholder); + --diffs-token-string: var(--color-foreground-success); + --diffs-token-string-expression: var(--color-foreground-success); + --diffs-token-keyword: #c084fc; + --diffs-token-function: #60a5fa; + --diffs-token-constant: #f59e0b; + --diffs-token-parameter: var(--color-foreground-default); + --diffs-token-punctuation: var(--color-foreground-muted); + --diffs-token-link: var(--color-foreground-info); + --diffs-token-inserted: var(--color-foreground-success); + --diffs-token-deleted: var(--color-foreground-danger); + --diffs-token-changed: var(--color-foreground-warning); +} + +@media (prefers-color-scheme: dark) { + diffs-container.diffs-container { + --diffs-fg-number-override: var(--color-foreground-subtle); + + /* the dark blend is 80/20, hence the subtle backgrounds */ + --diffs-bg-addition-override: var(--color-background-success-subtle); + --diffs-bg-deletion-override: var(--color-background-danger-subtle); + --diffs-addition-color-override: var(--color-foreground-success); + --diffs-deletion-color-override: var(--color-foreground-danger); + } +} -- 2.51.2