From a9cf8f6c5d03cc96f5f74c5c74421d7bef372df6 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Thu, 2 Apr 2026 23:40:26 -0700 Subject: [PATCH] refactor(web): replace react-syntax-highlighter with shiki (#33473) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 312 ++++++++---------- pnpm-workspace.yaml | 4 +- taze.config.js | 1 - .../__tests__/code-block.spec.tsx | 36 +- .../base/markdown-blocks/code-block.tsx | 86 +++-- .../base/markdown-blocks/shiki-highlight.tsx | 29 ++ .../segment-card/__tests__/index.spec.tsx | 2 +- web/app/styles/globals.css | 15 + web/eslint-suppressions.json | 3 - web/package.json | 4 +- 10 files changed, 291 insertions(+), 201 deletions(-) create mode 100644 web/app/components/base/markdown-blocks/shiki-highlight.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d40cf6279ad..f60206104da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,9 +213,6 @@ catalogs: '@types/react-dom': specifier: 19.2.3 version: 19.2.3 - '@types/react-syntax-highlighter': - specifier: 15.5.13 - version: 15.5.13 '@types/react-window': specifier: 1.8.8 version: 1.8.8 @@ -333,6 +330,9 @@ catalogs: happy-dom: specifier: 20.8.9 version: 20.8.9 + hast-util-to-jsx-runtime: + specifier: 2.3.6 + version: 2.3.6 hono: specifier: 4.12.10 version: 4.12.10 @@ -450,9 +450,6 @@ catalogs: react-sortablejs: specifier: 6.1.4 version: 6.1.4 - react-syntax-highlighter: - specifier: 15.6.6 - version: 15.6.6 react-textarea-autosize: specifier: 8.5.9 version: 8.5.9 @@ -474,6 +471,9 @@ catalogs: sharp: specifier: 0.34.5 version: 0.34.5 + shiki: + specifier: 4.0.2 + version: 4.0.2 sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -812,6 +812,9 @@ importers: foxact: specifier: 'catalog:' version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + hast-util-to-jsx-runtime: + specifier: 'catalog:' + version: 2.3.6 html-entities: specifier: 'catalog:' version: 2.6.0 @@ -914,9 +917,6 @@ importers: react-sortablejs: specifier: 'catalog:' version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) - react-syntax-highlighter: - specifier: 'catalog:' - version: 15.6.6(react@19.2.4) react-textarea-autosize: specifier: 'catalog:' version: 8.5.9(@types/react@19.2.14)(react@19.2.4) @@ -938,6 +938,9 @@ importers: sharp: specifier: 'catalog:' version: 0.34.5 + shiki: + specifier: 'catalog:' + version: 4.0.2 sortablejs: specifier: 'catalog:' version: 1.15.7 @@ -1095,9 +1098,6 @@ importers: '@types/react-dom': specifier: 'catalog:' version: 19.2.3(@types/react@19.2.14) - '@types/react-syntax-highlighter': - specifier: 'catalog:' - version: 15.5.13 '@types/react-window': specifier: 'catalog:' version: 1.8.8 @@ -3621,6 +3621,37 @@ packages: peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} @@ -4252,9 +4283,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/hast@2.3.10': - resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4299,9 +4327,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-syntax-highlighter@15.5.13': - resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} - '@types/react-window@1.8.8': resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} @@ -5058,21 +5083,12 @@ packages: character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -5172,9 +5188,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -6060,9 +6073,6 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fault@1.0.4: - resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} - fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} @@ -6251,9 +6261,6 @@ packages: hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -6266,6 +6273,9 @@ packages: hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -6278,9 +6288,6 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} @@ -6288,12 +6295,6 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - - highlightjs-vue@1.0.0: - resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - hono@4.12.10: resolution: {integrity: sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==} engines: {node: '>=16.9.0'} @@ -6413,15 +6414,9 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} @@ -6429,9 +6424,6 @@ packages: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -6448,9 +6440,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -6781,9 +6770,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lowlight@1.20.0: - resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lru-cache@11.2.7: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} @@ -7234,6 +7220,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -7307,9 +7299,6 @@ packages: parse-css-color@0.2.1: resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -7476,10 +7465,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -7490,9 +7475,6 @@ packages: property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -7670,11 +7652,6 @@ packages: '@types/react': optional: true - react-syntax-highlighter@15.6.6: - resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} - peerDependencies: - react: '>= 0.14.0' - react-textarea-autosize@8.5.9: resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} @@ -7743,8 +7720,14 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - refractor@3.6.0: - resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + 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==} regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} @@ -7941,6 +7924,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -7986,9 +7973,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -8772,10 +8756,6 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -11160,6 +11140,46 @@ snapshots: '@sentry/core': 10.47.0 react: 19.2.4 + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': dependencies: fflate: 0.7.4 @@ -11853,10 +11873,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/hast@2.3.10': - dependencies: - '@types/unist': 2.0.11 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11895,10 +11911,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react-syntax-highlighter@15.5.13': - dependencies: - '@types/react': 19.2.14 - '@types/react-window@1.8.8': dependencies: '@types/react': 19.2.14 @@ -12761,16 +12773,10 @@ snapshots: character-entities-html4@2.1.0: {} - character-entities-legacy@1.1.4: {} - character-entities-legacy@3.0.0: {} - character-entities@1.2.4: {} - character-entities@2.0.2: {} - character-reference-invalid@1.1.4: {} - character-reference-invalid@2.0.1: {} check-error@2.1.3: {} @@ -12884,8 +12890,6 @@ snapshots: color-name@1.1.4: {} - comma-separated-tokens@1.0.8: {} - comma-separated-tokens@2.0.3: {} commander@14.0.0: {} @@ -13991,10 +13995,6 @@ snapshots: dependencies: reusify: 1.1.0 - fault@1.0.4: - dependencies: - format: 0.2.2 - fault@2.0.1: dependencies: format: 0.2.2 @@ -14177,8 +14177,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -14226,6 +14224,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@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.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -14267,14 +14279,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hastscript@6.0.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -14285,10 +14289,6 @@ snapshots: hex-rgb@4.3.0: {} - highlight.js@10.7.3: {} - - highlightjs-vue@1.0.0: {} - hono@4.12.10: {} hosted-git-info@9.0.2: @@ -14385,15 +14385,8 @@ snapshots: intersection-observer@0.12.2: {} - is-alphabetical@1.0.4: {} - is-alphabetical@2.0.1: {} - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-alphanumerical@2.0.1: dependencies: is-alphabetical: 2.0.1 @@ -14403,8 +14396,6 @@ snapshots: dependencies: builtin-modules: 5.0.0 - is-decimal@1.0.4: {} - is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -14415,8 +14406,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-hexadecimal@1.0.4: {} - is-hexadecimal@2.0.1: {} is-in-ssh@1.0.0: {} @@ -14692,11 +14681,6 @@ snapshots: dependencies: tslib: 2.8.1 - lowlight@1.20.0: - dependencies: - fault: 1.0.4 - highlight.js: 10.7.3 - lru-cache@11.2.7: {} lru-cache@5.1.1: @@ -15440,6 +15424,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@10.2.0: dependencies: default-browser: 5.5.0 @@ -15608,15 +15600,6 @@ snapshots: color-name: 1.1.4 hex-rgb: 4.3.0 - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -15799,8 +15782,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prismjs@1.30.0: {} - progress@2.0.3: {} prop-types@15.8.1: @@ -15811,10 +15792,6 @@ snapshots: property-expr@2.0.6: {} - property-information@5.6.0: - dependencies: - xtend: 4.0.2 - property-information@7.1.0: {} pump@3.0.4: @@ -15993,16 +15970,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-syntax-highlighter@15.6.6(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - highlight.js: 10.7.3 - highlightjs-vue: 1.0.0 - lowlight: 1.20.0 - prismjs: 1.30.0 - react: 19.2.4 - refractor: 3.6.0 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 @@ -16107,11 +16074,15 @@ snapshots: reflect-metadata@0.2.2: {} - refractor@3.6.0: + regex-recursion@6.0.2: dependencies: - hastscript: 6.0.0 - parse-entities: 2.0.0 - prismjs: 1.30.0 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 regexp-ast-analysis@0.7.1: dependencies: @@ -16427,6 +16398,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + signal-exit@4.1.0: {} simple-concat@1.0.1: @@ -16470,8 +16452,6 @@ snapshots: source-map@0.7.6: {} - space-separated-tokens@1.1.5: {} - space-separated-tokens@2.0.2: {} spdx-correct@3.2.0: @@ -17341,8 +17321,6 @@ snapshots: xmlbuilder@15.1.1: {} - xtend@4.0.2: {} - yallist@3.1.1: {} yallist@5.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cb29e2f990a..d9f6293222b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -147,7 +147,6 @@ catalog: "@types/qs": 6.15.0 "@types/react": 19.2.14 "@types/react-dom": 19.2.3 - "@types/react-syntax-highlighter": 15.5.13 "@types/react-window": 1.8.8 "@types/sortablejs": 1.15.9 "@typescript-eslint/eslint-plugin": 8.58.0 @@ -188,6 +187,7 @@ catalog: fast-deep-equal: 3.1.3 foxact: 0.3.0 happy-dom: 20.8.9 + hast-util-to-jsx-runtime: 2.3.6 hono: 4.12.10 html-entities: 2.6.0 html-to-image: 1.11.13 @@ -228,7 +228,6 @@ catalog: react-pdf-highlighter: 8.0.0-rc.0 react-server-dom-webpack: 19.2.4 react-sortablejs: 6.1.4 - react-syntax-highlighter: 15.6.6 react-textarea-autosize: 8.5.9 react-window: 1.8.11 reactflow: 11.11.4 @@ -236,6 +235,7 @@ catalog: remark-directive: 4.0.0 scheduler: 0.27.0 sharp: 0.34.5 + shiki: 4.0.2 sortablejs: 1.15.7 std-semver: 1.0.8 storybook: 10.3.4 diff --git a/taze.config.js b/taze.config.js index 73bc0b5abab..96a69acf3e6 100644 --- a/taze.config.js +++ b/taze.config.js @@ -3,7 +3,6 @@ import { defineConfig } from 'taze' export default defineConfig({ exclude: [ // We are going to replace these - 'react-syntax-highlighter', 'react-window', '@types/react-window', ], diff --git a/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx b/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx index a16686801c6..2eebe44af9f 100644 --- a/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx +++ b/web/app/components/base/markdown-blocks/__tests__/code-block.spec.tsx @@ -5,6 +5,10 @@ import { Theme } from '@/types/app' import CodeBlock from '../code-block' +const { mockHighlightCode } = vi.hoisted(() => ({ + mockHighlightCode: vi.fn(), +})) + type UseThemeReturn = { theme: Theme } @@ -70,6 +74,10 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => mockUseTheme(), })) +vi.mock('../shiki-highlight', () => ({ + highlightCode: mockHighlightCode, +})) + vi.mock('echarts', () => ({ getInstanceByDom: mockEcharts.getInstanceByDom, })) @@ -130,6 +138,11 @@ describe('CodeBlock', () => { beforeEach(() => { vi.clearAllMocks() mockUseTheme.mockReturnValue({ theme: Theme.light }) + mockHighlightCode.mockImplementation(async ({ code, language }) => ( +
+        {code}
+      
+ )) consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) clientWidthSpy = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(900) @@ -198,11 +211,13 @@ describe('CodeBlock', () => { expect(container.querySelector('code')?.textContent).toBe('plain text') }) - it('should render syntax-highlighted output when language is standard', () => { + it('should render syntax-highlighted output when language is standard', async () => { render(const x = 1;) expect(screen.getByText('JavaScript')).toBeInTheDocument() - expect(document.querySelector('code.language-javascript')?.textContent).toContain('const x = 1;') + await waitFor(() => { + expect(document.querySelector('code.language-javascript')?.textContent).toContain('const x = 1;') + }) }) it('should format unknown language labels with capitalized fallback when language is not in map', () => { @@ -242,13 +257,26 @@ describe('CodeBlock', () => { expect(screen.queryByText(/Error rendering SVG/i)).not.toBeInTheDocument() }) - it('should render syntax-highlighted output when language is standard and app theme is dark', () => { + it('should render syntax-highlighted output when language is standard and app theme is dark', async () => { mockUseTheme.mockReturnValue({ theme: Theme.dark }) render(const y = 2;) expect(screen.getByText('JavaScript')).toBeInTheDocument() - expect(document.querySelector('code.language-javascript')?.textContent).toContain('const y = 2;') + await waitFor(() => { + expect(document.querySelector('code.language-javascript')?.textContent).toContain('const y = 2;') + }) + }) + + it('should fall back to plain code block when shiki highlighting fails', async () => { + mockHighlightCode.mockRejectedValueOnce(new Error('highlight failed')) + + render(const z = 3;) + + await waitFor(() => { + expect(screen.getByText('const z = 3;')).toBeInTheDocument() + }) + expect(document.querySelector('code.language-javascript')).toBeNull() }) }) diff --git a/web/app/components/base/markdown-blocks/code-block.tsx b/web/app/components/base/markdown-blocks/code-block.tsx index 412c61d52d3..679b1ca0d02 100644 --- a/web/app/components/base/markdown-blocks/code-block.tsx +++ b/web/app/components/base/markdown-blocks/code-block.tsx @@ -1,10 +1,7 @@ +import type { JSX } from 'react' +import type { BundledLanguage, BundledTheme } from 'shiki/bundle/web' import ReactEcharts from 'echarts-for-react' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import SyntaxHighlighter from 'react-syntax-highlighter' -import { - atelierHeathDark, - atelierHeathLight, -} from 'react-syntax-highlighter/dist/esm/styles/hljs' +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import ActionButton from '@/app/components/base/action-button' import CopyIcon from '@/app/components/base/copy-icon' import MarkdownMusic from '@/app/components/base/markdown-blocks/music' @@ -14,6 +11,7 @@ import useTheme from '@/hooks/use-theme' import dynamic from '@/next/dynamic' import { Theme } from '@/types/app' import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory +import { highlightCode } from './shiki-highlight' const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false }) @@ -64,6 +62,61 @@ const getCorrectCapitalizationLanguageName = (language: string) => { // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message // or use the non-minified dev environment for full errors and additional helpful warnings. +const ShikiCodeBlock = memo(({ code, language, theme, initial }: { code: string, language: string, theme: BundledTheme, initial?: JSX.Element }) => { + const [nodes, setNodes] = useState(initial) + + useLayoutEffect(() => { + let cancelled = false + + void highlightCode({ + code, + language: language as BundledLanguage, + theme, + }).then((result) => { + if (!cancelled) + setNodes(result) + }).catch((error) => { + console.error('Shiki highlighting failed:', error) + if (!cancelled) + setNodes(undefined) + }) + + return () => { + cancelled = true + } + }, [code, language, theme]) + + if (!nodes) { + return ( +
+        {code}
+      
+ ) + } + + return ( +
+ {nodes} +
+ ) +}) +ShikiCodeBlock.displayName = 'ShikiCodeBlock' + // Define ECharts event parameter types type EChartsEventParams = { type: string @@ -416,20 +469,11 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any ) default: return ( - - {content} - + ) } }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents]) @@ -440,7 +484,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any return (
-
{languageShowName}
+
{languageShowName}
{language === 'svg' && } diff --git a/web/app/components/base/markdown-blocks/shiki-highlight.tsx b/web/app/components/base/markdown-blocks/shiki-highlight.tsx new file mode 100644 index 00000000000..cfc075827f4 --- /dev/null +++ b/web/app/components/base/markdown-blocks/shiki-highlight.tsx @@ -0,0 +1,29 @@ +import type { JSX } from 'react' +import type { BundledLanguage, BundledTheme } from 'shiki/bundle/web' +import { toJsxRuntime } from 'hast-util-to-jsx-runtime' +import { Fragment } from 'react' +import { jsx, jsxs } from 'react/jsx-runtime' +import { codeToHast } from 'shiki/bundle/web' + +type HighlightCodeOptions = { + code: string + language: BundledLanguage + theme: BundledTheme +} + +export const highlightCode = async ({ + code, + language, + theme, +}: HighlightCodeOptions): Promise => { + const hast = await codeToHast(code, { + lang: language, + theme, + }) + + return toJsxRuntime(hast, { + Fragment, + jsx, + jsxs, + }) as JSX.Element +} diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx index 00cf9769df8..fd431af95dd 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/__tests__/index.spec.tsx @@ -61,7 +61,7 @@ vi.mock('@/app/components/datasets/common/image-list', () => ({ ), })) -// Markdown uses next/dynamic and react-syntax-highlighter (ESM) +// Markdown uses next/dynamic and shiki (ESM) vi.mock('@/app/components/base/markdown', () => ({ Markdown: ({ content, className }: { content: string, className?: string }) => (
{content}
diff --git a/web/app/styles/globals.css b/web/app/styles/globals.css index 0d9c950dec3..a1a0130b87f 100644 --- a/web/app/styles/globals.css +++ b/web/app/styles/globals.css @@ -909,4 +909,19 @@ [data-theme='light'] [data-hide-on-theme='light'] { display: none; } + + /* Shiki code block line numbers */ + .shiki-line-numbers code { + counter-reset: line; + } + .shiki-line-numbers .line::before { + counter-increment: line; + content: counter(line); + display: inline-block; + width: 1rem; + margin-right: 0.75rem; + text-align: right; + color: var(--color-text-quaternary); + user-select: none; + } } diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index f7c0ad36ee1..1a7463b7c65 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -3142,9 +3142,6 @@ "react/set-state-in-effect": { "count": 7 }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, "ts/no-explicit-any": { "count": 9 } diff --git a/web/package.json b/web/package.json index 1dada2bfb58..f822e874253 100644 --- a/web/package.json +++ b/web/package.json @@ -100,6 +100,7 @@ "es-toolkit": "catalog:", "fast-deep-equal": "catalog:", "foxact": "catalog:", + "hast-util-to-jsx-runtime": "catalog:", "html-entities": "catalog:", "html-to-image": "catalog:", "i18next": "catalog:", @@ -134,7 +135,6 @@ "react-papaparse": "catalog:", "react-pdf-highlighter": "catalog:", "react-sortablejs": "catalog:", - "react-syntax-highlighter": "catalog:", "react-textarea-autosize": "catalog:", "react-window": "catalog:", "reactflow": "catalog:", @@ -142,6 +142,7 @@ "remark-directive": "catalog:", "scheduler": "catalog:", "sharp": "catalog:", + "shiki": "catalog:", "sortablejs": "catalog:", "std-semver": "catalog:", "streamdown": "catalog:", @@ -196,7 +197,6 @@ "@types/qs": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "@types/react-syntax-highlighter": "catalog:", "@types/react-window": "catalog:", "@types/sortablejs": "catalog:", "@typescript-eslint/parser": "catalog:",