mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 10:03:31 +00:00
Support code themes customization option, refactor built-in code themes (#3949)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
"@gitbook/react-openapi": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support code themes customization
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
"rison": "^0.1.1",
|
"rison": "^0.1.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shiki": "^3.2.0",
|
"shiki": "^3.21.0",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwind-shades": "^1.1.2",
|
"tailwind-shades": "^1.1.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
@@ -345,7 +345,7 @@
|
|||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@gitbook/api": "0.158.0",
|
"@gitbook/api": "0.159.0",
|
||||||
"@scalar/api-client-react": "^1.3.46",
|
"@scalar/api-client-react": "^1.3.46",
|
||||||
"@tsconfig/node20": "^20.1.6",
|
"@tsconfig/node20": "^20.1.6",
|
||||||
"@tsconfig/strictest": "^2.0.6",
|
"@tsconfig/strictest": "^2.0.6",
|
||||||
@@ -724,7 +724,7 @@
|
|||||||
|
|
||||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||||
|
|
||||||
"@gitbook/api": ["@gitbook/api@0.158.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-oWrUY0gQ4rVKyNciHaXQrvi+EoiLCWHhAD0QyAhsHJp9+ndaLRMV6WfK2KbiGQmH8kiWwwBmwmQfMkZFmm1eyQ=="],
|
"@gitbook/api": ["@gitbook/api@0.159.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-IRZXT2NAcOlKlCcGnzRH4w82AuBXam5I7lw84qfCRbNDDycvFNBwIVMHu3JMOcGUTCkzj7QDElaNKYirsWDwFQ=="],
|
||||||
|
|
||||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||||
|
|
||||||
@@ -1320,17 +1320,17 @@
|
|||||||
|
|
||||||
"@scalar/workspace-store": ["@scalar/workspace-store@0.20.0", "", { "dependencies": { "@scalar/code-highlight": "0.2.0", "@scalar/helpers": "0.1.1", "@scalar/json-magic": "0.8.1", "@scalar/openapi-upgrader": "0.1.4", "@scalar/snippetz": "0.5.2", "@scalar/themes": "0.13.23", "@scalar/typebox": "0.1.1", "@scalar/types": "0.4.0", "github-slugger": "^2.0.0", "type-fest": "5.0.0", "vue": "^3.5.21", "yaml": "2.8.0" } }, "sha512-RklQSKjz5V/69rKjxQV70q4u34qzmUZVtWGUJLVvHxPsPxCtxPPRLYY+gdQDkGjTQYol/dR9xyvi2QGk3OWTKA=="],
|
"@scalar/workspace-store": ["@scalar/workspace-store@0.20.0", "", { "dependencies": { "@scalar/code-highlight": "0.2.0", "@scalar/helpers": "0.1.1", "@scalar/json-magic": "0.8.1", "@scalar/openapi-upgrader": "0.1.4", "@scalar/snippetz": "0.5.2", "@scalar/themes": "0.13.23", "@scalar/typebox": "0.1.1", "@scalar/types": "0.4.0", "github-slugger": "^2.0.0", "type-fest": "5.0.0", "vue": "^3.5.21", "yaml": "2.8.0" } }, "sha512-RklQSKjz5V/69rKjxQV70q4u34qzmUZVtWGUJLVvHxPsPxCtxPPRLYY+gdQDkGjTQYol/dR9xyvi2QGk3OWTKA=="],
|
||||||
|
|
||||||
"@shikijs/core": ["@shikijs/core@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-+5dPz8q6HgNqfQ28ycm/vA8dIVd2lNFOUqVRFCQLbs0KZ6emYI+1apLpX+wuL/aDSPLOkMgARwNjkA5UjGKS1Q=="],
|
"@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
|
||||||
|
|
||||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.1.0" } }, "sha512-1WrYfaz5YT5aTAIMbYQhxlSHc8ArX+hCDNAIdKRqJHzfWQ3xDgh3PTvrAly+RWGuvi5Q4NlvPlTBdlSAXN6Stg=="],
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
|
||||||
|
|
||||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-3V7ko+YUAP02I4rUbDjCgvyM/H85hUIZBQAS19FjDcJMKL5SbjWTiG7TRKxX1V4ddxLxt2RO64wZinElp/3ngQ=="],
|
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
|
||||||
|
|
||||||
"@shikijs/langs": ["@shikijs/langs@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0" } }, "sha512-Qze5YIsp223AmC69VZDQolcrcYPrVa9wV6cW2kVqsDrSWlwhW2EQZEn1Iw2oQU1tGYVg8Hj/xdp8mOv+9zI0vg=="],
|
"@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
|
||||||
|
|
||||||
"@shikijs/themes": ["@shikijs/themes@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0" } }, "sha512-XfzMSTu6iMl2FZIwKykld2OzFKDDlm4KbZrzW6sbKXEeJ1xq61HX4x4bE4+REBFqbbrvAQM8EAH11m/E3cxYDg=="],
|
"@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
|
||||||
|
|
||||||
"@shikijs/types": ["@shikijs/types@3.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-1uOwfEO0vV+G8n/AO/6Yth7zshNdXvQ1pc4ygTrfE3cyuzVLukrZq72YkFUlsRijam7LvRTvnqL4aT5wx1X2Vw=="],
|
"@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
|
||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
@@ -1898,8 +1898,6 @@
|
|||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
|
||||||
|
|
||||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
@@ -2552,9 +2550,9 @@
|
|||||||
|
|
||||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||||
|
|
||||||
"oniguruma-parser": ["oniguruma-parser@0.5.4", "", {}, "sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA=="],
|
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||||
|
|
||||||
"oniguruma-to-es": ["oniguruma-to-es@4.1.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "oniguruma-parser": "^0.5.4", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA=="],
|
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||||
|
|
||||||
"openapi-fetch": ["openapi-fetch@0.13.5", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q=="],
|
"openapi-fetch": ["openapi-fetch@0.13.5", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q=="],
|
||||||
|
|
||||||
@@ -2774,7 +2772,7 @@
|
|||||||
|
|
||||||
"shell-quote": ["shell-quote@1.8.1", "", {}, "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="],
|
"shell-quote": ["shell-quote@1.8.1", "", {}, "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="],
|
||||||
|
|
||||||
"shiki": ["shiki@3.2.0", "", { "dependencies": { "@shikijs/core": "3.2.0", "@shikijs/engine-javascript": "3.2.0", "@shikijs/engine-oniguruma": "3.2.0", "@shikijs/langs": "3.2.0", "@shikijs/themes": "3.2.0", "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lOF6wkvZCRVQrdfGilyXclTKIjCWKujPAjD6fddLwtQ6eSmgj43pFDbjUmxivtElDRlsGO8G2dLeeRpwNY4wWg=="],
|
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
|
||||||
|
|
||||||
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
|
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@
|
|||||||
"catalog": {
|
"catalog": {
|
||||||
"@tsconfig/strictest": "^2.0.6",
|
"@tsconfig/strictest": "^2.0.6",
|
||||||
"@tsconfig/node20": "^20.1.6",
|
"@tsconfig/node20": "^20.1.6",
|
||||||
"@gitbook/api": "0.158.0",
|
"@gitbook/api": "0.159.0",
|
||||||
"@scalar/api-client-react": "^1.3.46",
|
"@scalar/api-client-react": "^1.3.46",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { argosScreenshot } from '@argos-ci/playwright';
|
|||||||
import {
|
import {
|
||||||
CustomizationAIMode,
|
CustomizationAIMode,
|
||||||
CustomizationBackground,
|
CustomizationBackground,
|
||||||
|
CustomizationCodeTheme,
|
||||||
CustomizationCorners,
|
CustomizationCorners,
|
||||||
CustomizationDefaultFont,
|
CustomizationDefaultFont,
|
||||||
CustomizationDefaultMonospaceFont,
|
CustomizationDefaultMonospaceFont,
|
||||||
@@ -298,6 +299,16 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
|||||||
background: CustomizationBackground.Plain,
|
background: CustomizationBackground.Plain,
|
||||||
icons: CustomizationIconsStyle.Regular,
|
icons: CustomizationIconsStyle.Regular,
|
||||||
links: CustomizationLinksStyle.Default,
|
links: CustomizationLinksStyle.Default,
|
||||||
|
codeTheme: {
|
||||||
|
default: {
|
||||||
|
light: CustomizationCodeTheme.DefaultLight,
|
||||||
|
dark: CustomizationCodeTheme.DefaultDark,
|
||||||
|
},
|
||||||
|
openapi: {
|
||||||
|
light: CustomizationCodeTheme.DefaultLight,
|
||||||
|
dark: CustomizationCodeTheme.DefaultDark,
|
||||||
|
},
|
||||||
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
background: CustomizationSidebarBackgroundStyle.Default,
|
background: CustomizationSidebarBackgroundStyle.Default,
|
||||||
list: CustomizationSidebarListStyle.Default,
|
list: CustomizationSidebarListStyle.Default,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
"rison": "^0.1.1",
|
"rison": "^0.1.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shiki": "^3.2.0",
|
"shiki": "^3.21.0",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwind-shades": "^1.1.2",
|
"tailwind-shades": "^1.1.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { DocumentBlockCode } from '@gitbook/api';
|
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
|
||||||
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||||
@@ -8,18 +8,19 @@ import { useInViewportListener } from '@/components/hooks/useInViewportListener'
|
|||||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||||
import { Button, ToggleChevron } from '@/components/primitives';
|
import { Button, ToggleChevron } from '@/components/primitives';
|
||||||
import { t, useLanguage } from '@/intl/client';
|
import { t, useLanguage } from '@/intl/client';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||||
import { useDebounceCallback } from 'usehooks-ts';
|
import { useDebounceCallback } from 'usehooks-ts';
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
||||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||||
import type { HighlightLine, RenderedInline } from './highlight';
|
import type { HighlightTheme, RenderedInline } from './highlight';
|
||||||
import { plainHighlight } from './plain-highlight';
|
import { plainHighlight } from './plain-highlight';
|
||||||
|
|
||||||
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||||
inlines: RenderedInline[];
|
inlines: RenderedInline[];
|
||||||
inlineExprVariables: InlineExpressionVariables;
|
inlineExprVariables: InlineExpressionVariables;
|
||||||
mode: BlockProps<DocumentBlockCode>['context']['mode'];
|
mode: BlockProps<DocumentBlockCode>['context']['mode'];
|
||||||
|
themes?: CustomizationThemedCodeTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||||
@@ -29,7 +30,7 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
|||||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||||
*/
|
*/
|
||||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||||
const { block, mode, style, inlines, inlineExprVariables } = props;
|
const { block, mode, style, inlines, inlineExprVariables, themes } = props;
|
||||||
const blockRef = useRef<HTMLDivElement>(null);
|
const blockRef = useRef<HTMLDivElement>(null);
|
||||||
const isInViewportRef = useRef(false);
|
const isInViewportRef = useRef(false);
|
||||||
const [isInViewport, setIsInViewport] = useState(false);
|
const [isInViewport, setIsInViewport] = useState(false);
|
||||||
@@ -40,17 +41,17 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
visitorClaims,
|
visitorClaims,
|
||||||
variables: inlineExprVariables,
|
variables: inlineExprVariables,
|
||||||
});
|
});
|
||||||
const plainLines = useMemo(
|
const plainTheme = useMemo(
|
||||||
() => plainHighlight(block, inlines, { evaluateInlineExpression }),
|
() => plainHighlight(block, inlines, { evaluateInlineExpression, themes }),
|
||||||
[block, inlines, evaluateInlineExpression]
|
[block, inlines, evaluateInlineExpression, themes]
|
||||||
);
|
);
|
||||||
const [lines, setLines] = useState<null | HighlightLine[]>(null);
|
const [theme, setTheme] = useState<null | HighlightTheme>(null);
|
||||||
const [highlighting, setHighlighting] = useState(false);
|
const [highlighting, setHighlighting] = useState(false);
|
||||||
|
|
||||||
// Preload the highlighter when the block is mounted.
|
// Preload the highlighter when the block is mounted.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
|
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block, themes));
|
||||||
}, [block]);
|
}, [block, themes]);
|
||||||
|
|
||||||
// When user scrolls, we need to wait for the scroll to finish before running the highlight
|
// When user scrolls, we need to wait for the scroll to finish before running the highlight
|
||||||
const isScrollingRef = useRef(false);
|
const isScrollingRef = useRef(false);
|
||||||
@@ -99,14 +100,16 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
setHighlighting(true);
|
setHighlighting(true);
|
||||||
import('./highlight').then(({ highlight }) => {
|
import('./highlight').then(({ highlight }) => {
|
||||||
highlight(block, inlines, { evaluateInlineExpression }).then((lines) => {
|
highlight(block, inlines, { evaluateInlineExpression, themes }).then(
|
||||||
if (cancelled) {
|
(theme) => {
|
||||||
return;
|
if (cancelled) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLines(lines);
|
setTheme(theme);
|
||||||
setHighlighting(false);
|
setHighlighting(false);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +119,12 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise if the block is not in viewport, we reset to the plain lines
|
// Otherwise if the block is not in viewport, we reset to the plain lines
|
||||||
setLines(null);
|
setTheme(null);
|
||||||
}, [isInViewport, block, inlines, evaluateInlineExpression]);
|
}, [isInViewport, block, inlines, evaluateInlineExpression, themes]);
|
||||||
|
|
||||||
const expandable = block.data.expandable;
|
const expandable = block.data.expandable;
|
||||||
|
|
||||||
const numberOfLinesOfCode = lines?.length ?? plainLines.length;
|
const numberOfLinesOfCode = theme?.lines.length ?? plainTheme.lines.length;
|
||||||
const collapsedLineCount =
|
const collapsedLineCount =
|
||||||
block.data.collapsedLineCount || CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT;
|
block.data.collapsedLineCount || CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT;
|
||||||
const isExpandable = Boolean(
|
const isExpandable = Boolean(
|
||||||
@@ -136,16 +139,17 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
aria-busy={highlighting}
|
aria-busy={highlighting}
|
||||||
block={block}
|
block={block}
|
||||||
style={style}
|
style={style}
|
||||||
lines={lines ?? plainLines}
|
theme={theme ?? plainTheme}
|
||||||
id={codeBlockBodyId}
|
id={codeBlockBodyId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isExpandable ? (
|
return isExpandable ? (
|
||||||
<CodeBlockExpandable
|
<CodeBlockExpandable
|
||||||
lines={lines ?? plainLines}
|
theme={theme ?? plainTheme}
|
||||||
controls={codeBlockBodyId}
|
controls={codeBlockBodyId}
|
||||||
collapsedLineCount={collapsedLineCount}
|
collapsedLineCount={collapsedLineCount}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
{renderer}
|
{renderer}
|
||||||
</CodeBlockExpandable>
|
</CodeBlockExpandable>
|
||||||
@@ -156,21 +160,25 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
|
|
||||||
function CodeBlockExpandable(props: {
|
function CodeBlockExpandable(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
lines: HighlightLine[];
|
theme: HighlightTheme;
|
||||||
collapsedLineCount: number;
|
collapsedLineCount: number;
|
||||||
controls?: string;
|
controls?: string;
|
||||||
|
style?: ClassValue;
|
||||||
}) {
|
}) {
|
||||||
const { children, controls, lines = [], collapsedLineCount } = props;
|
const { children, controls, theme, collapsedLineCount, style } = props;
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
return (
|
return (
|
||||||
<div className="group/codeblock-expandable relative">
|
<div
|
||||||
|
className={tcls('group/codeblock-expandable relative', style)}
|
||||||
|
data-follow-color-scheme="true"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
isExpanded
|
!isExpanded
|
||||||
? '[&_pre]:after:opacity-0'
|
? '[&_pre]:h-[calc(2rem+var(--line-count)*var(--line-height))] [&_pre]:overflow-y-hidden'
|
||||||
: '[&_pre]:h-[calc(2rem+var(--line-count)*var(--line-height))] [&_pre]:overflow-y-hidden [&_pre]:after:opacity-100',
|
: ''
|
||||||
'[&_pre]:after:pointer-events-none [&_pre]:after:absolute [&_pre]:after:inset-0 [&_pre]:after:z-1 [&_pre]:after:bg-gradient-to-t [&_pre]:after:from-0% [&_pre]:after:from-tint-2 [&_pre]:after:to-70% [&_pre]:after:to-transparent [&_pre]:after:content-[""]'
|
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -181,7 +189,7 @@ function CodeBlockExpandable(props: {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none absolute bottom-0 flex w-full justify-center">
|
<div className="pointer-events-none absolute bottom-2 flex w-full justify-center">
|
||||||
<Button
|
<Button
|
||||||
icon={<ToggleChevron open={isExpanded} />}
|
icon={<ToggleChevron open={isExpanded} />}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
@@ -194,7 +202,7 @@ function CodeBlockExpandable(props: {
|
|||||||
>
|
>
|
||||||
{isExpanded
|
{isExpanded
|
||||||
? t(language, 'code_block_expanded')
|
? t(language, 'code_block_expanded')
|
||||||
: t(language, 'code_block_collapsed', lines.length)}
|
: t(language, 'code_block_collapsed', theme.lines.length)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { DocumentBlockCode } from '@gitbook/api';
|
import type {
|
||||||
|
CustomizationThemedCodeTheme,
|
||||||
|
DocumentBlockCode,
|
||||||
|
SiteCustomizationSettings,
|
||||||
|
} from '@gitbook/api';
|
||||||
|
|
||||||
import { getNodeFragmentByType } from '@/lib/document';
|
import { getNodeFragmentByType } from '@/lib/document';
|
||||||
|
|
||||||
@@ -13,8 +17,21 @@ import { type RenderedInline, getInlines, highlight } from './highlight';
|
|||||||
/**
|
/**
|
||||||
* Render a code block, can be client-side or server-side.
|
* Render a code block, can be client-side or server-side.
|
||||||
*/
|
*/
|
||||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
export async function CodeBlock(
|
||||||
const { block, document, style, isEstimatedOffscreen, context } = props;
|
props: BlockProps<DocumentBlockCode> & {
|
||||||
|
themeKey?: keyof SiteCustomizationSettings['styling']['codeTheme'];
|
||||||
|
themes?: CustomizationThemedCodeTheme;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
block,
|
||||||
|
document,
|
||||||
|
style,
|
||||||
|
isEstimatedOffscreen,
|
||||||
|
context,
|
||||||
|
themeKey = 'default',
|
||||||
|
themes: providedThemes,
|
||||||
|
} = props;
|
||||||
const inlines = getInlines(block);
|
const inlines = getInlines(block);
|
||||||
|
|
||||||
let hasInlineExpression = false;
|
let hasInlineExpression = false;
|
||||||
@@ -50,10 +67,19 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
|||||||
return { inline, body };
|
return { inline, body };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get code themes from customization
|
||||||
|
const themes =
|
||||||
|
providedThemes ??
|
||||||
|
(context.contentContext && 'customization' in context.contentContext
|
||||||
|
? context.contentContext.customization.styling.codeTheme[themeKey]
|
||||||
|
: undefined);
|
||||||
|
|
||||||
if (!isEstimatedOffscreen && !hasInlineExpression && !block.data.expandable) {
|
if (!isEstimatedOffscreen && !hasInlineExpression && !block.data.expandable) {
|
||||||
// In v2, we render the code block server-side
|
// In v2, we render the code block server-side
|
||||||
const lines = await highlight(block, richInlines);
|
const theme = await highlight(block, richInlines, {
|
||||||
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
themes: themes,
|
||||||
|
});
|
||||||
|
return <CodeBlockRenderer block={block} style={style} theme={theme} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variables = context.contentContext
|
const variables = context.contentContext
|
||||||
@@ -74,6 +100,7 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
|||||||
inlines={richInlines}
|
inlines={richInlines}
|
||||||
inlineExprVariables={variables}
|
inlineExprVariables={variables}
|
||||||
mode={context.mode}
|
mode={context.mode}
|
||||||
|
themes={themes}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import { AnnotationPopover } from '../Annotation/AnnotationPopover';
|
import { AnnotationPopover } from '../Annotation/AnnotationPopover';
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { CopyCodeButton } from './CopyCodeButton';
|
import { CopyCodeButton } from './CopyCodeButton';
|
||||||
import type { HighlightLine, HighlightToken } from './highlight';
|
import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight';
|
||||||
|
|
||||||
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||||
lines: HighlightLine[];
|
theme: HighlightTheme;
|
||||||
'aria-busy'?: boolean;
|
'aria-busy'?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
@@ -22,7 +22,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
|||||||
props: CodeBlockRendererProps,
|
props: CodeBlockRendererProps,
|
||||||
ref: React.ForwardedRef<HTMLDivElement>
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
) {
|
) {
|
||||||
const { block, style, lines, 'aria-busy': ariaBusy } = props;
|
const { block, style, theme, 'aria-busy': ariaBusy } = props;
|
||||||
|
|
||||||
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
|
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
|
||||||
const withWrap = block.data.overflow === 'wrap';
|
const withWrap = block.data.overflow === 'wrap';
|
||||||
@@ -30,15 +30,35 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
|||||||
|
|
||||||
const id = useId();
|
const id = useId();
|
||||||
const codeId = props.id || id;
|
const codeId = props.id || id;
|
||||||
|
|
||||||
|
/* Shiki returns a color + CSS variables in a single string, which isn't supported in React.
|
||||||
|
So we parse the foreground & background into objects that can be passed into React's `style` prop. */
|
||||||
|
const bg = parseShikiColorString(theme.bg);
|
||||||
|
const fg = parseShikiColorString(theme.fg);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-busy={ariaBusy}
|
aria-busy={ariaBusy}
|
||||||
className={tcls('group/codeblock grid shrink grid-flow-col overflow-hidden', style)}
|
className={tcls(
|
||||||
|
'group/codeblock shiki grid shrink grid-flow-col overflow-hidden',
|
||||||
|
style
|
||||||
|
)}
|
||||||
|
/* Sets the code theme's mode (light or dark) for the site's theme mode (light or dark).
|
||||||
|
* Used to style UI elements (scrollbars, form controls) correctly and apply the right default to "plain" code blocks. */
|
||||||
|
data-color-scheme={`${theme.themes.light.type} ${theme.themes.dark.type}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
||||||
{title ? (
|
{title ? (
|
||||||
<div className="relative top-px z-20 inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base">
|
<div
|
||||||
|
className="relative top-px z-20 inline-flex items-center justify-center circular-corners:rounded-t-xl rounded-corners:rounded-t-lg straight-corners:rounded-t-xs border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base"
|
||||||
|
style={{
|
||||||
|
backgroundColor: bg?.color,
|
||||||
|
...bg?.vars,
|
||||||
|
color: fg?.color,
|
||||||
|
...fg?.vars,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -49,23 +69,32 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
|||||||
/>
|
/>
|
||||||
<pre
|
<pre
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base p-2 [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
|
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base p-2 text-tint-strong [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
|
||||||
'rounded-md straight-corners:rounded-xs shadow-xs',
|
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs shadow-xs',
|
||||||
title && 'rounded-ss-none'
|
title && 'rounded-ss-none!'
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: bg?.color,
|
||||||
|
...bg?.vars,
|
||||||
|
color: fg?.color,
|
||||||
|
...fg?.vars,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
id={codeId}
|
id={codeId}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'inline-grid min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
'inline-grid max-h-full min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
||||||
withWrap && 'whitespace-pre-wrap'
|
withWrap && 'whitespace-pre-wrap',
|
||||||
|
'[[aria-expanded=false]_&]:mask-b-from-50%'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{lines.map((line, index) => (
|
{theme.lines.map((line, index) => (
|
||||||
<CodeHighlightLine
|
<CodeHighlightLine
|
||||||
|
bg={bg}
|
||||||
|
fg={fg}
|
||||||
key={index}
|
key={index}
|
||||||
line={line}
|
line={line}
|
||||||
isLast={index === lines.length - 1}
|
isLast={index === theme.lines.length - 1}
|
||||||
withLineNumbers={withLineNumbers}
|
withLineNumbers={withLineNumbers}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -77,13 +106,35 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
|||||||
|
|
||||||
function CodeHighlightLine(props: {
|
function CodeHighlightLine(props: {
|
||||||
line: HighlightLine;
|
line: HighlightLine;
|
||||||
|
bg?: ShikiColorDefinition;
|
||||||
|
fg?: ShikiColorDefinition;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
withLineNumbers: boolean;
|
withLineNumbers: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { line, isLast, withLineNumbers } = props;
|
const { line, isLast, withLineNumbers, bg, fg } = props;
|
||||||
return (
|
return (
|
||||||
<span className={tcls('highlight-line', line.highlighted && 'highlighted')}>
|
<span
|
||||||
{withLineNumbers && <span className="highlight-line-number" />}
|
className={tcls('highlight-line', line.highlighted && 'highlighted')}
|
||||||
|
style={
|
||||||
|
line.highlighted
|
||||||
|
? {
|
||||||
|
backgroundColor: bg?.color,
|
||||||
|
...bg?.vars,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{withLineNumbers && (
|
||||||
|
<span
|
||||||
|
className="highlight-line-number"
|
||||||
|
style={{
|
||||||
|
color: fg?.color,
|
||||||
|
...fg?.vars,
|
||||||
|
backgroundColor: bg?.color,
|
||||||
|
...bg?.vars,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<span className="highlight-line-content">
|
<span className="highlight-line-content">
|
||||||
<CodeHighlightTokens tokens={line.tokens} />
|
<CodeHighlightTokens tokens={line.tokens} />
|
||||||
{!isLast && '\n'}
|
{!isLast && '\n'}
|
||||||
@@ -112,13 +163,53 @@ function CodeHighlightToken(props: { token: HighlightToken }) {
|
|||||||
return token.content;
|
return token.content;
|
||||||
}
|
}
|
||||||
case 'shiki': {
|
case 'shiki': {
|
||||||
if (!token.token.color) {
|
if (!token.token.htmlStyle) {
|
||||||
return token.token.content;
|
return token.token.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span style={{ color: token.token.color }}>{token.token.content}</span>;
|
return <span style={token.token.htmlStyle}>{token.token.content}</span>;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
assertNever(token);
|
assertNever(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShikiColorDefinition = {
|
||||||
|
color: string | undefined;
|
||||||
|
vars: Record<string, string> | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Shiki color string format into separate color and CSS variables.
|
||||||
|
*
|
||||||
|
* Shiki returns colors in a format that combines a default color with CSS variables:
|
||||||
|
* "defaultColor;--shiki-light:value1;--shiki-dark:value2"
|
||||||
|
*
|
||||||
|
* React does not parse these strings as valid CSS because they contain values & properties in one.
|
||||||
|
* We split these so React can apply them separately via the style prop.
|
||||||
|
*/
|
||||||
|
function parseShikiColorString(
|
||||||
|
originalColor: string | undefined
|
||||||
|
): ShikiColorDefinition | undefined {
|
||||||
|
if (!originalColor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = originalColor.split(';');
|
||||||
|
const color = parts[0];
|
||||||
|
const vars = parts.slice(1).reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
const [key, value] = item.split(':');
|
||||||
|
if (key && value) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
vars: Object.keys(vars).length > 0 ? vars : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { JSONDocument } from '@gitbook/api';
|
import type {
|
||||||
|
CustomizationThemedCodeTheme,
|
||||||
|
JSONDocument,
|
||||||
|
SiteCustomizationSettings,
|
||||||
|
} from '@gitbook/api';
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
|
|
||||||
import { CodeBlock } from './CodeBlock';
|
import { CodeBlock } from './CodeBlock';
|
||||||
@@ -8,8 +12,13 @@ import { convertCodeStringToBlock } from './utils';
|
|||||||
* Plain code block with syntax highlighting.
|
* Plain code block with syntax highlighting.
|
||||||
* For simplicity, this is just a wrapper around the CodeBlock component, emulating a document.
|
* For simplicity, this is just a wrapper around the CodeBlock component, emulating a document.
|
||||||
*/
|
*/
|
||||||
export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
export function PlainCodeBlock(props: {
|
||||||
const { code, syntax } = props;
|
code: string;
|
||||||
|
syntax: string;
|
||||||
|
themeKey?: keyof SiteCustomizationSettings['styling']['codeTheme'];
|
||||||
|
themes?: CustomizationThemedCodeTheme;
|
||||||
|
}) {
|
||||||
|
const { code, syntax, themeKey, themes } = props;
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
||||||
@@ -26,6 +35,8 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
|||||||
context={{
|
context={{
|
||||||
mode: 'default',
|
mode: 'default',
|
||||||
}}
|
}}
|
||||||
|
themeKey={themeKey}
|
||||||
|
themes={themes}
|
||||||
block={block}
|
block={block}
|
||||||
ancestorBlocks={[]}
|
ancestorBlocks={[]}
|
||||||
// We optimize perf by default
|
// We optimize perf by default
|
||||||
|
|||||||
@@ -0,0 +1,832 @@
|
|||||||
|
import type { ThemeRegistration } from 'shiki/core';
|
||||||
|
|
||||||
|
export const customThemes: Record<string, ThemeRegistration> = {
|
||||||
|
'default-light': createTheme({
|
||||||
|
name: 'default-light',
|
||||||
|
type: 'light',
|
||||||
|
bg: 'rgb(var(--tint-2))',
|
||||||
|
fg: 'rgb(var(--tint-12))',
|
||||||
|
colors: {
|
||||||
|
basic: {
|
||||||
|
comment: 'rgb(var(--neutral-9))',
|
||||||
|
string: 'rgb(var(--success-10))',
|
||||||
|
constant: 'rgb(var(--warning-10))',
|
||||||
|
escape: 'rgb(var(--success-10))',
|
||||||
|
},
|
||||||
|
keyword: {
|
||||||
|
default: 'rgb(var(--danger-10))',
|
||||||
|
operator: 'rgb(var(--tint-11))',
|
||||||
|
regexpOperator: 'rgb(var(--primary-10))',
|
||||||
|
regexpQuantifier: 'rgb(var(--warning-10))',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
default: 'rgb(var(--tint-12))',
|
||||||
|
language: 'rgb(var(--danger-10))',
|
||||||
|
parameter: 'rgb(var(--warning-10))',
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
default: 'rgb(var(--primary-10))',
|
||||||
|
support: 'rgb(var(--primary-10))',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
custom: 'rgb(var(--primary-10))',
|
||||||
|
primitive: 'rgb(var(--warning-10))',
|
||||||
|
other: 'rgb(var(--tint-12))',
|
||||||
|
classMeta: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
tag: 'rgb(var(--warning-10))',
|
||||||
|
attributeId: 'rgb(var(--primary-10))',
|
||||||
|
label: 'rgb(var(--danger-10))',
|
||||||
|
},
|
||||||
|
markup: {
|
||||||
|
default: 'rgb(var(--primary-10))',
|
||||||
|
code: 'rgb(var(--success-10))',
|
||||||
|
link: 'rgb(var(--primary-10))',
|
||||||
|
linkUrl: 'rgb(var(--warning-10))',
|
||||||
|
separator: 'rgb(var(--tint-7))',
|
||||||
|
},
|
||||||
|
diff: {
|
||||||
|
inserted: 'rgb(var(--success-10))',
|
||||||
|
deleted: 'rgb(var(--danger-10))',
|
||||||
|
changed: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
punctuation: 'rgb(var(--tint-11))',
|
||||||
|
preprocessor: 'rgb(var(--danger-10))',
|
||||||
|
default: 'rgb(var(--tint-11))',
|
||||||
|
invalid: 'rgb(var(--danger-10))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'default-dark': createTheme({
|
||||||
|
name: 'default-dark',
|
||||||
|
type: 'dark',
|
||||||
|
bg: 'rgb(var(--tint-2))',
|
||||||
|
fg: 'rgb(var(--tint-12))',
|
||||||
|
colors: {
|
||||||
|
basic: {
|
||||||
|
comment: 'rgb(var(--neutral-9))',
|
||||||
|
string: 'rgb(var(--success-11))',
|
||||||
|
constant: 'rgb(var(--warning-11))',
|
||||||
|
escape: 'rgb(var(--success-11))',
|
||||||
|
},
|
||||||
|
keyword: {
|
||||||
|
default: 'rgb(var(--danger-11))',
|
||||||
|
operator: 'rgb(var(--tint-11))',
|
||||||
|
regexpOperator: 'rgb(var(--primary-11))',
|
||||||
|
regexpQuantifier: 'rgb(var(--warning-11))',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
default: 'rgb(var(--tint-12))',
|
||||||
|
language: 'rgb(var(--danger-11))',
|
||||||
|
parameter: 'rgb(var(--warning-11))',
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
default: 'rgb(var(--primary-11))',
|
||||||
|
support: 'rgb(var(--primary-11))',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
custom: 'rgb(var(--primary-11))',
|
||||||
|
primitive: 'rgb(var(--warning-11))',
|
||||||
|
other: 'rgb(var(--tint-12))',
|
||||||
|
classMeta: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
tag: 'rgb(var(--warning-11))',
|
||||||
|
attributeId: 'rgb(var(--primary-11))',
|
||||||
|
label: 'rgb(var(--danger-11))',
|
||||||
|
},
|
||||||
|
markup: {
|
||||||
|
default: 'rgb(var(--primary-11))',
|
||||||
|
code: 'rgb(var(--success-11))',
|
||||||
|
link: 'rgb(var(--primary-11))',
|
||||||
|
linkUrl: 'rgb(var(--warning-11))',
|
||||||
|
separator: 'rgb(var(--tint-7))',
|
||||||
|
},
|
||||||
|
diff: {
|
||||||
|
inserted: 'rgb(var(--success-11))',
|
||||||
|
deleted: 'rgb(var(--danger-11))',
|
||||||
|
changed: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
punctuation: 'rgb(var(--tint-12))',
|
||||||
|
preprocessor: 'rgb(var(--danger-11))',
|
||||||
|
default: 'rgb(var(--tint-12))',
|
||||||
|
invalid: 'rgb(var(--danger-11))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'monochrome-light': createTheme({
|
||||||
|
name: 'monochrome-light',
|
||||||
|
type: 'light',
|
||||||
|
bg: 'rgb(var(--tint-2))',
|
||||||
|
fg: 'rgb(var(--tint-12))',
|
||||||
|
colors: {
|
||||||
|
basic: {
|
||||||
|
comment: 'rgb(var(--tint-9))',
|
||||||
|
string: 'rgb(var(--primary-12))',
|
||||||
|
constant: 'rgb(var(--primary-11))',
|
||||||
|
escape: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
keyword: {
|
||||||
|
default: 'rgb(var(--primary-10))',
|
||||||
|
operator: 'rgb(var(--primary-11))',
|
||||||
|
regexpOperator: 'rgb(var(--primary-10))',
|
||||||
|
regexpQuantifier: 'rgb(var(--primary-11))',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
default: 'rgb(var(--tint-12))',
|
||||||
|
language: 'rgb(var(--primary-10))',
|
||||||
|
parameter: 'rgb(var(--tint-11))',
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
default: 'rgb(var(--primary-9))',
|
||||||
|
support: 'rgb(var(--primary-9))',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
custom: 'rgb(var(--primary-10))',
|
||||||
|
primitive: 'rgb(var(--primary-11))',
|
||||||
|
other: 'rgb(var(--primary-10))',
|
||||||
|
classMeta: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
tag: 'rgb(var(--primary-11))',
|
||||||
|
attributeId: 'rgb(var(--primary-9))',
|
||||||
|
label: 'rgb(var(--primary-10))',
|
||||||
|
},
|
||||||
|
markup: {
|
||||||
|
default: 'rgb(var(--primary-10))',
|
||||||
|
code: 'rgb(var(--tint-12))',
|
||||||
|
link: 'rgb(var(--primary-10))',
|
||||||
|
linkUrl: 'rgb(var(--primary-11))',
|
||||||
|
separator: 'rgb(var(--tint-7))',
|
||||||
|
},
|
||||||
|
diff: {
|
||||||
|
inserted: 'rgb(var(--success-11))',
|
||||||
|
deleted: 'rgb(var(--danger-11))',
|
||||||
|
changed: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
punctuation: 'rgb(var(--tint-10))',
|
||||||
|
preprocessor: 'rgb(var(--primary-10))',
|
||||||
|
default: 'rgb(var(--primary-11))',
|
||||||
|
invalid: 'rgb(var(--danger-11))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'monochrome-dark': createTheme({
|
||||||
|
name: 'monochrome-dark',
|
||||||
|
type: 'dark',
|
||||||
|
bg: 'rgb(var(--tint-2))',
|
||||||
|
fg: 'rgb(var(--tint-12))',
|
||||||
|
colors: {
|
||||||
|
basic: {
|
||||||
|
comment: 'rgb(var(--tint-9))',
|
||||||
|
string: 'rgb(var(--primary-12))',
|
||||||
|
constant: 'rgb(var(--primary-11))',
|
||||||
|
escape: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
keyword: {
|
||||||
|
default: 'rgb(var(--primary-11))',
|
||||||
|
operator: 'rgb(var(--primary-11))',
|
||||||
|
regexpOperator: 'rgb(var(--primary-10))',
|
||||||
|
regexpQuantifier: 'rgb(var(--primary-11))',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
default: 'rgb(var(--tint-12))',
|
||||||
|
language: 'rgb(var(--primary-10))',
|
||||||
|
parameter: 'rgb(var(--tint-9))',
|
||||||
|
},
|
||||||
|
function: {
|
||||||
|
default: 'rgb(var(--primary-9))',
|
||||||
|
support: 'rgb(var(--primary-9))',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
custom: 'rgb(var(--primary-10))',
|
||||||
|
primitive: 'rgb(var(--primary-11))',
|
||||||
|
other: 'rgb(var(--primary-10))',
|
||||||
|
classMeta: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
tag: 'rgb(var(--primary-11))',
|
||||||
|
attributeId: 'rgb(var(--primary-9))',
|
||||||
|
label: 'rgb(var(--primary-10))',
|
||||||
|
},
|
||||||
|
markup: {
|
||||||
|
default: 'rgb(var(--primary-10))',
|
||||||
|
code: 'rgb(var(--tint-12))',
|
||||||
|
link: 'rgb(var(--primary-10))',
|
||||||
|
linkUrl: 'rgb(var(--primary-11))',
|
||||||
|
separator: 'rgb(var(--tint-7))',
|
||||||
|
},
|
||||||
|
diff: {
|
||||||
|
inserted: 'rgb(var(--success-11))',
|
||||||
|
deleted: 'rgb(var(--danger-11))',
|
||||||
|
changed: 'rgb(var(--tint-12))',
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
punctuation: 'rgb(var(--tint-10))',
|
||||||
|
preprocessor: 'rgb(var(--primary-10))',
|
||||||
|
default: 'rgb(var(--primary-11))',
|
||||||
|
invalid: 'rgb(var(--danger-11))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color category mappings for syntax highlighting.
|
||||||
|
* Organized into logical groups for better readability.
|
||||||
|
*/
|
||||||
|
export type ColorCategories = {
|
||||||
|
/** Basic syntax elements */
|
||||||
|
basic: {
|
||||||
|
/** Comments (//, /*, #, etc.) */
|
||||||
|
comment: string;
|
||||||
|
/** String literals ("...", '...', `...`) */
|
||||||
|
string: string;
|
||||||
|
/** Constants: numeric, language constants (true, false, null), character constants */
|
||||||
|
constant: string;
|
||||||
|
/** Escape sequences in strings (\n, \t, etc.) */
|
||||||
|
escape: string;
|
||||||
|
};
|
||||||
|
/** Keywords and operators */
|
||||||
|
keyword: {
|
||||||
|
/** Control flow (if, else, for, while, return), storage (const, let, var, function, class) */
|
||||||
|
default: string;
|
||||||
|
/** Operators (+, -, *, /, =, ==, etc.) */
|
||||||
|
operator: string;
|
||||||
|
/** Regexp operators (|, ^, $) */
|
||||||
|
regexpOperator: string;
|
||||||
|
/** Regexp quantifiers (*, +, ?, {n}) */
|
||||||
|
regexpQuantifier: string;
|
||||||
|
};
|
||||||
|
/** Variables and parameters */
|
||||||
|
variable: {
|
||||||
|
/** Regular variables */
|
||||||
|
default: string;
|
||||||
|
/** Language variables (this, self, super) */
|
||||||
|
language: string;
|
||||||
|
/** Function/method parameters */
|
||||||
|
parameter: string;
|
||||||
|
};
|
||||||
|
/** Functions */
|
||||||
|
function: {
|
||||||
|
/** User-defined function/method invocations */
|
||||||
|
default: string;
|
||||||
|
/** Built-in/library support functions */
|
||||||
|
support: string;
|
||||||
|
};
|
||||||
|
/** Types and classes */
|
||||||
|
type: {
|
||||||
|
/** Custom type names (DocumentBlockCode, HighlightLine, etc.) */
|
||||||
|
custom: string;
|
||||||
|
/** Primitive types (number, boolean, string, etc.) */
|
||||||
|
primitive: string;
|
||||||
|
/** Other types (support.type, support.class, namespaces) */
|
||||||
|
other: string;
|
||||||
|
/** Class meta contexts (meta.class) */
|
||||||
|
classMeta: string;
|
||||||
|
};
|
||||||
|
/** Entities (tags, attributes, labels) */
|
||||||
|
entity: {
|
||||||
|
/** HTML/XML tags and attributes */
|
||||||
|
tag: string;
|
||||||
|
/** Attribute IDs (entity.other.attribute-name.id) */
|
||||||
|
attributeId: string;
|
||||||
|
/** Labels (entity.name.label) */
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
/** Markup elements */
|
||||||
|
markup: {
|
||||||
|
/** Markup elements (bold, italic, headings, lists, quotes, etc.) */
|
||||||
|
default: string;
|
||||||
|
/** Inline code in markup */
|
||||||
|
code: string;
|
||||||
|
/** Links in markup */
|
||||||
|
link: string;
|
||||||
|
/** Link URLs in markup */
|
||||||
|
linkUrl: string;
|
||||||
|
/** Separators (meta.separator) */
|
||||||
|
separator: string;
|
||||||
|
};
|
||||||
|
/** Diff highlighting */
|
||||||
|
diff: {
|
||||||
|
/** Inserted lines */
|
||||||
|
inserted: string;
|
||||||
|
/** Deleted lines */
|
||||||
|
deleted: string;
|
||||||
|
/** Changed lines */
|
||||||
|
changed: string;
|
||||||
|
};
|
||||||
|
/** Other syntax elements */
|
||||||
|
other: {
|
||||||
|
/** Punctuation (., ,, ;, :, {}, [], (), etc.) */
|
||||||
|
punctuation: string;
|
||||||
|
/** Preprocessor directives (#include, #define, etc.) */
|
||||||
|
preprocessor: string;
|
||||||
|
/** Other/miscellaneous elements */
|
||||||
|
default: string;
|
||||||
|
/** Invalid syntax/errors */
|
||||||
|
invalid: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Shiki theme registration from semantic color categories.
|
||||||
|
*
|
||||||
|
* Maps our semantic color categories (comment, keyword, function, etc.) to Shiki's TextMate scopes.
|
||||||
|
* TextMate scopes are hierarchical selectors (e.g., "keyword.control", "variable.parameter") that
|
||||||
|
* Shiki uses to identify different syntax elements. This abstraction allows us to define themes
|
||||||
|
* using semantic categories rather than low-level scope patterns.
|
||||||
|
*/
|
||||||
|
function createTheme(options: {
|
||||||
|
name: string;
|
||||||
|
type: 'light' | 'dark';
|
||||||
|
bg: string;
|
||||||
|
fg: string;
|
||||||
|
colors: ColorCategories;
|
||||||
|
}): ThemeRegistration {
|
||||||
|
const { name, type, bg, fg, colors } = options;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
settings: [
|
||||||
|
// Comments
|
||||||
|
{
|
||||||
|
scope: ['comment'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.comment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: language constants (true, false, null, etc.)
|
||||||
|
{
|
||||||
|
scope: ['constant.language'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: numeric
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'constant.numeric',
|
||||||
|
'variable.other.enummember',
|
||||||
|
'keyword.operator.plus.exponent',
|
||||||
|
'keyword.operator.minus.exponent',
|
||||||
|
'keyword.other.unit',
|
||||||
|
'constant.sha.git-rebase',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: regexp
|
||||||
|
{
|
||||||
|
scope: ['constant.regexp'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: character
|
||||||
|
{
|
||||||
|
scope: ['constant.character', 'constant.other.option'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: escape sequences
|
||||||
|
{
|
||||||
|
scope: ['constant.character.escape'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.escape,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: other
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'constant.other',
|
||||||
|
'constant.other.color',
|
||||||
|
'constant.other.symbol',
|
||||||
|
'constant.other.placeholder',
|
||||||
|
'constant.other.character-class.regexp',
|
||||||
|
'constant.character.set.regexp',
|
||||||
|
'constant.character.character-class.regexp',
|
||||||
|
'constant.other.character-class.set.regexp',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Constants: variable constants
|
||||||
|
{
|
||||||
|
scope: ['variable.other.constant'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.constant,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Strings
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'string',
|
||||||
|
'meta.embedded.assembly',
|
||||||
|
'string.tag',
|
||||||
|
'string.value',
|
||||||
|
'string.regexp',
|
||||||
|
'meta.preprocessor.string',
|
||||||
|
'support.constant.property-value',
|
||||||
|
'support.constant.font-name',
|
||||||
|
'support.constant.media-type',
|
||||||
|
'support.constant.media',
|
||||||
|
'constant.other.color.rgb-value',
|
||||||
|
'constant.other.rgb-value',
|
||||||
|
'support.constant.color',
|
||||||
|
'punctuation.definition.group.regexp',
|
||||||
|
'punctuation.definition.group.assertion.regexp',
|
||||||
|
'punctuation.definition.character-class.regexp',
|
||||||
|
'punctuation.character.set.begin.regexp',
|
||||||
|
'punctuation.character.set.end.regexp',
|
||||||
|
'keyword.operator.negation.regexp',
|
||||||
|
'support.other.parenthesis.regexp',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.basic.string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// String expressions (template literals, interpolated)
|
||||||
|
// Only color the punctuation, not the expression content
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'punctuation.definition.template-expression.begin',
|
||||||
|
'punctuation.definition.template-expression.end',
|
||||||
|
'punctuation.section.embedded',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.variable.parameter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: control flow
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'keyword.control',
|
||||||
|
'source.cpp keyword.operator.new',
|
||||||
|
'keyword.operator.delete',
|
||||||
|
'keyword.other.using',
|
||||||
|
'keyword.other.directive.using',
|
||||||
|
'keyword.other.operator',
|
||||||
|
'entity.name.operator',
|
||||||
|
'keyword.operator.new',
|
||||||
|
'keyword.operator.expression',
|
||||||
|
'keyword.operator.cast',
|
||||||
|
'keyword.operator.sizeof',
|
||||||
|
'keyword.operator.alignof',
|
||||||
|
'keyword.operator.typeid',
|
||||||
|
'keyword.operator.alignas',
|
||||||
|
'keyword.operator.instanceof',
|
||||||
|
'keyword.operator.logical.python',
|
||||||
|
'keyword.operator.wordlike',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: storage (const, let, var, function, class, etc.)
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'storage',
|
||||||
|
'storage.type',
|
||||||
|
'storage.modifier',
|
||||||
|
'keyword.operator.noexcept',
|
||||||
|
'storage.type.numeric.go',
|
||||||
|
'storage.type.byte.go',
|
||||||
|
'storage.type.boolean.go',
|
||||||
|
'storage.type.string.go',
|
||||||
|
'storage.type.uintptr.go',
|
||||||
|
'storage.type.error.go',
|
||||||
|
'storage.type.rune.go',
|
||||||
|
'storage.type.cs',
|
||||||
|
'storage.type.generic.cs',
|
||||||
|
'storage.type.modifier.cs',
|
||||||
|
'storage.type.variable.cs',
|
||||||
|
'storage.type.annotation.java',
|
||||||
|
'storage.type.generic.java',
|
||||||
|
'storage.type.java',
|
||||||
|
'storage.type.object.array.java',
|
||||||
|
'storage.type.primitive.array.java',
|
||||||
|
'storage.type.primitive.java',
|
||||||
|
'storage.type.token.java',
|
||||||
|
'storage.type.groovy',
|
||||||
|
'storage.type.annotation.groovy',
|
||||||
|
'storage.type.parameters.groovy',
|
||||||
|
'storage.type.generic.groovy',
|
||||||
|
'storage.type.object.array.groovy',
|
||||||
|
'storage.type.primitive.array.groovy',
|
||||||
|
'storage.type.primitive.groovy',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: general
|
||||||
|
{
|
||||||
|
scope: ['keyword'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: operators (default)
|
||||||
|
{
|
||||||
|
scope: ['keyword.operator'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.operator,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: regexp operators
|
||||||
|
{
|
||||||
|
scope: ['keyword.operator.or.regexp', 'keyword.control.anchor.regexp'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.regexpOperator,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keywords: regexp quantifiers
|
||||||
|
{
|
||||||
|
scope: ['keyword.operator.quantifier.regexp'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.keyword.regexpQuantifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variable language (this, self, super, etc.)
|
||||||
|
{
|
||||||
|
scope: ['variable.language'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.variable.language,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Parameters
|
||||||
|
{
|
||||||
|
scope: ['variable.parameter'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.variable.parameter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variables (regular variables, not parameters)
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'variable',
|
||||||
|
'meta.definition.variable.name',
|
||||||
|
'support.variable',
|
||||||
|
'entity.name.variable',
|
||||||
|
'meta.object-literal.key',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.variable.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Functions: invocations (calls) - user-defined functions
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'meta.function-call',
|
||||||
|
'meta.method-call',
|
||||||
|
'entity.name.function',
|
||||||
|
'entity.name.method',
|
||||||
|
'support.constant.handlebars',
|
||||||
|
'source.powershell variable.other.member',
|
||||||
|
'entity.name.operator.custom-literal',
|
||||||
|
'keyword.other.special-method',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.function.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Support functions: built-in/library functions
|
||||||
|
{
|
||||||
|
scope: ['support.function', 'support.function.git-rebase'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.function.support,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Types: custom type names (DocumentBlockCode, HighlightLine, etc.) - must come first
|
||||||
|
{
|
||||||
|
scope: ['entity.name.type', 'entity.name.class', 'entity.other.inherited-class'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.type.custom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Types: primitive (number, boolean, string, etc.) - must come before support.type
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'support.type.primitive',
|
||||||
|
'storage.type.primitive',
|
||||||
|
'support.type.builtin',
|
||||||
|
'entity.name.type.primitive',
|
||||||
|
'support.type',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.type.primitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Types: other (support.class, etc.)
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'support.class',
|
||||||
|
'entity.name.namespace',
|
||||||
|
'entity.other.attribute',
|
||||||
|
'entity.name.scope-resolution',
|
||||||
|
'meta.type.cast.expr',
|
||||||
|
'meta.type.new.expr',
|
||||||
|
'support.constant.math',
|
||||||
|
'support.constant.json',
|
||||||
|
'punctuation.separator.namespace.ruby',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.type.other,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Class meta contexts
|
||||||
|
{
|
||||||
|
scope: ['meta.class'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.type.classMeta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// DOM constants and namespace references (ReactDOM, window.document, etc.)
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'support.constant.dom',
|
||||||
|
'variable.other.object',
|
||||||
|
'variable.other.object.property',
|
||||||
|
'entity.name.class.jsx',
|
||||||
|
'support.class.builtin',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.variable.parameter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Entity names: tags, attributes
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'entity.name.tag',
|
||||||
|
'entity.other.attribute-name',
|
||||||
|
'support.type.vendored.property-name',
|
||||||
|
'support.type.property-name',
|
||||||
|
'source.css variable',
|
||||||
|
'source.coffee.embedded',
|
||||||
|
'meta.structure.dictionary.key.python',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.entity.tag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Attribute IDs (typically styled like functions)
|
||||||
|
{
|
||||||
|
scope: ['entity.other.attribute-name.id', 'punctuation.definition.entity'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.entity.attributeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Punctuation
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'punctuation',
|
||||||
|
'punctuation.definition.tag',
|
||||||
|
'punctuation.definition.quote.begin.markdown',
|
||||||
|
'punctuation.definition.list.begin.markdown',
|
||||||
|
'punctuation.section.embedded.begin.php',
|
||||||
|
'punctuation.section.embedded.end.php',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.other.punctuation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Links: link text
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'markup.underline.link',
|
||||||
|
'string.other.link',
|
||||||
|
'punctuation.definition.string.end.markdown',
|
||||||
|
'punctuation.definition.string.begin.markdown',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.link,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Links: link URLs
|
||||||
|
{
|
||||||
|
scope: ['meta.link'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.linkUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Preprocessor
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'meta.preprocessor',
|
||||||
|
'entity.name.function.preprocessor',
|
||||||
|
'meta.preprocessor.numeric',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.other.preprocessor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Diff: inserted
|
||||||
|
{
|
||||||
|
scope: ['markup.inserted', 'markup.inserted.diff'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.diff.inserted,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Diff: deleted
|
||||||
|
{
|
||||||
|
scope: ['markup.deleted', 'markup.deleted.diff'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.diff.deleted,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Diff: changed
|
||||||
|
{
|
||||||
|
scope: ['markup.changed', 'markup.changed.diff', 'meta.diff.header'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.diff.changed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Markup: bold (with bold font style)
|
||||||
|
{
|
||||||
|
scope: ['markup.bold', 'punctuation.definition.bold'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.default,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Markup: italic (with italic font style)
|
||||||
|
{
|
||||||
|
scope: ['markup.italic', 'punctuation.definition.italic'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.default,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Markup: inline code
|
||||||
|
{
|
||||||
|
scope: ['markup.raw.inline'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Markup: headings, lists, quotes, underline, strikethrough
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'markup.heading',
|
||||||
|
'markup.heading punctuation.definition.heading',
|
||||||
|
'entity.name.section',
|
||||||
|
'header',
|
||||||
|
'markup.list',
|
||||||
|
'markup.quote',
|
||||||
|
'markup.underline',
|
||||||
|
'markup.strikethrough',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.markup.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Separator
|
||||||
|
{
|
||||||
|
scope: ['meta.separator'],
|
||||||
|
settings: {
|
||||||
|
background: colors.markup.separator,
|
||||||
|
foreground: fg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Labels
|
||||||
|
{
|
||||||
|
scope: ['entity.name.label'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.entity.label,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Other
|
||||||
|
{
|
||||||
|
scope: [
|
||||||
|
'meta.embedded',
|
||||||
|
'source.groovy.embedded',
|
||||||
|
'string meta.image.inline.markdown',
|
||||||
|
'variable.legacy.builtin.python',
|
||||||
|
'storage.modifier.import.java',
|
||||||
|
'variable.language.wildcard.java',
|
||||||
|
'storage.modifier.package.java',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.other.default,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Invalid
|
||||||
|
{
|
||||||
|
scope: ['invalid'],
|
||||||
|
settings: {
|
||||||
|
foreground: colors.other.invalid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ async function highlightWithInlines(block: DocumentBlockCode) {
|
|||||||
inline,
|
inline,
|
||||||
body: null,
|
body: null,
|
||||||
}));
|
}));
|
||||||
return highlight(block, inlines);
|
const result = await highlight(block, inlines);
|
||||||
|
return result.lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should parse plain code', async () => {
|
it('should parse plain code', async () => {
|
||||||
@@ -224,7 +225,19 @@ it('should parse code with an inline on a single line', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '"Hello World"',
|
content: '"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'Hello World',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: '"',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -375,7 +388,19 @@ it('should parse code with an inline on a multiple line', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '"Hello World"',
|
content: '"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'Hello World',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: '"',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -631,7 +656,13 @@ it('should support multiple code tokens in an annotation', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '.world',
|
content: '.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'world',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -645,7 +676,13 @@ it('should support multiple code tokens in an annotation', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: ');',
|
content: ')',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: ';',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -699,7 +736,13 @@ it('should handle \\r', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '.log',
|
content: '.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'log',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -711,7 +754,19 @@ it('should handle \\r', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '"Hello"',
|
content: '"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'Hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: '"',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -734,7 +789,13 @@ it('should handle \\r', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '.log',
|
content: '.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'log',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -746,7 +807,19 @@ it('should handle \\r', async () => {
|
|||||||
{
|
{
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
token: {
|
token: {
|
||||||
content: '"World"',
|
content: '"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: 'World',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shiki',
|
||||||
|
token: {
|
||||||
|
content: '"',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
import type {
|
import {
|
||||||
DocumentBlockCode,
|
CustomizationCodeTheme,
|
||||||
DocumentBlockCodeLine,
|
type CustomizationThemedCodeTheme,
|
||||||
DocumentInlineAnnotation,
|
type DocumentBlockCode,
|
||||||
|
type DocumentBlockCodeLine,
|
||||||
|
type DocumentInlineAnnotation,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import {
|
import {
|
||||||
|
type ThemeRegistrationAny,
|
||||||
type ThemedToken,
|
type ThemedToken,
|
||||||
createCssVariablesTheme,
|
|
||||||
createSingletonShorthands,
|
createSingletonShorthands,
|
||||||
createdBundledHighlighter,
|
createdBundledHighlighter,
|
||||||
} from 'shiki/core';
|
} from 'shiki/core';
|
||||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||||
import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
|
import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
|
||||||
|
import { bundledThemes } from 'shiki/themes';
|
||||||
|
|
||||||
import { nullIfNever } from '@/lib/typescript';
|
import { nullIfNever } from '@/lib/typescript';
|
||||||
|
import { customThemes } from './customThemes';
|
||||||
import { plainHighlight } from './plain-highlight';
|
import { plainHighlight } from './plain-highlight';
|
||||||
|
|
||||||
|
export const DEFAULT_THEMES: CustomizationThemedCodeTheme = {
|
||||||
|
light: CustomizationCodeTheme.DefaultLight,
|
||||||
|
dark: CustomizationCodeTheme.DefaultDark,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HighlightTheme = {
|
||||||
|
bg?: string;
|
||||||
|
fg?: string;
|
||||||
|
themes: {
|
||||||
|
light: ThemeRegistrationAny;
|
||||||
|
dark: ThemeRegistrationAny;
|
||||||
|
};
|
||||||
|
lines: HighlightLine[];
|
||||||
|
};
|
||||||
|
|
||||||
export type HighlightLine = {
|
export type HighlightLine = {
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
tokens: HighlightToken[];
|
tokens: HighlightToken[];
|
||||||
@@ -36,12 +55,12 @@ export type RenderedInline = {
|
|||||||
|
|
||||||
const isSafari =
|
const isSafari =
|
||||||
typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
const theme = createCssVariablesTheme();
|
|
||||||
|
|
||||||
|
// Merge bundled Shiki themes with our custom themes so both are available to the highlighter
|
||||||
const { getSingletonHighlighter } = createSingletonShorthands(
|
const { getSingletonHighlighter } = createSingletonShorthands(
|
||||||
createdBundledHighlighter<any, any>({
|
createdBundledHighlighter<any, any>({
|
||||||
langs: bundledLanguages,
|
langs: bundledLanguages,
|
||||||
themes: {},
|
themes: { ...bundledThemes, ...customThemes },
|
||||||
engine: () => createJavaScriptRegexEngine({ forgiving: true, target: 'ES2018' }),
|
engine: () => createJavaScriptRegexEngine({ forgiving: true, target: 'ES2018' }),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -49,12 +68,15 @@ const { getSingletonHighlighter } = createSingletonShorthands(
|
|||||||
/**
|
/**
|
||||||
* Preload the highlighter for a code block.
|
* Preload the highlighter for a code block.
|
||||||
*/
|
*/
|
||||||
export async function preloadHighlight(block: DocumentBlockCode) {
|
export async function preloadHighlight(
|
||||||
|
block: DocumentBlockCode,
|
||||||
|
themes: CustomizationThemedCodeTheme = DEFAULT_THEMES
|
||||||
|
) {
|
||||||
const langName = getBlockLang(block);
|
const langName = getBlockLang(block);
|
||||||
if (langName) {
|
if (langName) {
|
||||||
await getSingletonHighlighter({
|
await getSingletonHighlighter({
|
||||||
langs: [langName],
|
langs: [langName],
|
||||||
themes: [theme],
|
themes: [themes.light, themes.dark],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +89,9 @@ export async function highlight(
|
|||||||
inlines: RenderedInline[],
|
inlines: RenderedInline[],
|
||||||
options?: {
|
options?: {
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
evaluateInlineExpression?: (expr: string) => string;
|
||||||
|
themes?: CustomizationThemedCodeTheme;
|
||||||
}
|
}
|
||||||
): Promise<HighlightLine[]> {
|
): Promise<HighlightTheme> {
|
||||||
const langName = getBlockLang(block);
|
const langName = getBlockLang(block);
|
||||||
|
|
||||||
if (!langName || (isSafari && ['powershell', 'cpp'].includes(langName))) {
|
if (!langName || (isSafari && ['powershell', 'cpp'].includes(langName))) {
|
||||||
@@ -80,13 +103,20 @@ export async function highlight(
|
|||||||
return plainHighlight(block, inlines, options);
|
return plainHighlight(block, inlines, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const themes = options?.themes ?? DEFAULT_THEMES;
|
||||||
|
|
||||||
const code = getPlainCodeBlock(block, undefined, options);
|
const code = getPlainCodeBlock(block, undefined, options);
|
||||||
|
|
||||||
const highlighter = await getSingletonHighlighter({
|
const highlighter = await getSingletonHighlighter({
|
||||||
langs: [langName],
|
langs: [langName],
|
||||||
themes: [theme],
|
themes: [themes.light, themes.dark],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolvedThemes = {
|
||||||
|
light: highlighter.getTheme(themes.light),
|
||||||
|
dark: highlighter.getTheme(themes.dark),
|
||||||
|
};
|
||||||
|
|
||||||
let tokenizeMaxLineLength = 400;
|
let tokenizeMaxLineLength = 400;
|
||||||
// In some cases, people will use unindented code blocks with a single line.
|
// In some cases, people will use unindented code blocks with a single line.
|
||||||
// In this case, we can safely increase the max line length to avoid not highlighting the code.
|
// In this case, we can safely increase the max line length to avoid not highlighting the code.
|
||||||
@@ -94,38 +124,47 @@ export async function highlight(
|
|||||||
tokenizeMaxLineLength = 5000;
|
tokenizeMaxLineLength = 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = highlighter.codeToTokensBase(code, {
|
const result = highlighter.codeToTokens(code, {
|
||||||
lang: langName,
|
lang: langName,
|
||||||
theme,
|
themes: resolvedThemes,
|
||||||
|
// Shiki's light-dark() CSS function provides different colors for light/dark modes based on the resolved themes
|
||||||
|
defaultColor: 'light-dark()',
|
||||||
tokenizeMaxLineLength,
|
tokenizeMaxLineLength,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lines = result.tokens;
|
||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
return lines.map((tokens, index) => {
|
return {
|
||||||
const lineBlock = block.nodes[index];
|
bg: result.bg,
|
||||||
const result: HighlightToken[] = [];
|
fg: result.fg,
|
||||||
|
themes: resolvedThemes,
|
||||||
|
lines: lines.map((tokens, index) => {
|
||||||
|
const lineBlock = block.nodes[index];
|
||||||
|
const result: HighlightToken[] = [];
|
||||||
|
|
||||||
const eatToken = (): PositionedToken | null => {
|
const eatToken = (): PositionedToken | null => {
|
||||||
const token = tokens.shift();
|
const token = tokens.shift();
|
||||||
if (token) {
|
if (token) {
|
||||||
currentIndex += token.content.length;
|
currentIndex += token.content.length;
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
? { ...token, start: currentIndex - token.content.length, end: currentIndex }
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (tokens.length > 0) {
|
||||||
|
result.push(...matchTokenAndInlines(eatToken, inlines));
|
||||||
}
|
}
|
||||||
return token
|
|
||||||
? { ...token, start: currentIndex - token.content.length, end: currentIndex }
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
while (tokens.length > 0) {
|
currentIndex += 1; // for the \n
|
||||||
result.push(...matchTokenAndInlines(eatToken, inlines));
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex += 1; // for the \n
|
return {
|
||||||
|
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||||
return {
|
tokens: result,
|
||||||
highlighted: Boolean(lineBlock?.data.highlighted),
|
};
|
||||||
tokens: result,
|
}),
|
||||||
};
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { DocumentBlockCode } from '@gitbook/api';
|
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
|
||||||
|
|
||||||
import { getNodeText } from '@/lib/document';
|
import { getNodeText } from '@/lib/document';
|
||||||
|
import { bundledThemesInfo } from 'shiki/themes';
|
||||||
import type { HighlightLine, HighlightToken, RenderedInline } from './highlight';
|
import { customThemes } from './customThemes';
|
||||||
|
import type { HighlightTheme, HighlightToken, RenderedInline } from './highlight';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a code block without highlighting it.
|
* Parse a code block without highlighting it.
|
||||||
@@ -12,42 +13,59 @@ export function plainHighlight(
|
|||||||
inlines: RenderedInline[],
|
inlines: RenderedInline[],
|
||||||
options?: {
|
options?: {
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
evaluateInlineExpression?: (expr: string) => string;
|
||||||
|
themes?: CustomizationThemedCodeTheme;
|
||||||
}
|
}
|
||||||
): HighlightLine[] {
|
): HighlightTheme {
|
||||||
const inlinesCopy = Array.from(inlines);
|
const inlinesCopy = Array.from(inlines);
|
||||||
|
|
||||||
return block.nodes.map((lineBlock) => {
|
// Resolve theme objects: first check bundled Shiki themes, then custom themes, finally fallback to type-only
|
||||||
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
// The type-only fallback ensures HighlightTheme always has valid theme objects even when theme names don't match
|
||||||
if (node.object === 'text') {
|
const themes = {
|
||||||
return {
|
light: bundledThemesInfo.find((theme) => theme.id === options?.themes?.light) ??
|
||||||
type: 'plain',
|
Object.values(customThemes).find((theme) => theme.name === options?.themes?.light) ?? {
|
||||||
content: getNodeText(node),
|
type: 'light',
|
||||||
};
|
},
|
||||||
}
|
dark: bundledThemesInfo.find((theme) => theme.id === options?.themes?.dark) ??
|
||||||
|
Object.values(customThemes).find((theme) => theme.name === options?.themes?.dark) ?? {
|
||||||
|
type: 'dark',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (node.type === 'expression') {
|
return {
|
||||||
return {
|
themes: themes,
|
||||||
type: 'plain',
|
lines: block.nodes.map((lineBlock) => {
|
||||||
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
||||||
};
|
if (node.object === 'text') {
|
||||||
}
|
return {
|
||||||
|
|
||||||
const inline = inlinesCopy.shift();
|
|
||||||
return {
|
|
||||||
type: 'annotation',
|
|
||||||
body: inline?.body ?? null,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'plain',
|
type: 'plain',
|
||||||
content: getNodeText(node),
|
content: getNodeText(node),
|
||||||
},
|
};
|
||||||
],
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
if (node.type === 'expression') {
|
||||||
highlighted: Boolean(lineBlock.data.highlighted),
|
return {
|
||||||
tokens,
|
type: 'plain',
|
||||||
};
|
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inline = inlinesCopy.shift();
|
||||||
|
return {
|
||||||
|
type: 'annotation',
|
||||||
|
body: inline?.body ?? null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'plain',
|
||||||
|
content: getNodeText(node),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
highlighted: Boolean(lineBlock.data.highlighted),
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,17 @@ export function getOpenAPIContext(args: {
|
|||||||
check: <Icon icon="check" />,
|
check: <Icon icon="check" />,
|
||||||
lock: <Icon icon="lock" />,
|
lock: <Icon icon="lock" />,
|
||||||
},
|
},
|
||||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
renderCodeBlock: (codeProps) => (
|
||||||
|
<PlainCodeBlock
|
||||||
|
{...codeProps}
|
||||||
|
themeKey="openapi"
|
||||||
|
themes={
|
||||||
|
context && 'customization' in context
|
||||||
|
? context.customization.styling.codeTheme.openapi
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
renderDocument: (documentProps) => (
|
renderDocument: (documentProps) => (
|
||||||
<DocumentView
|
<DocumentView
|
||||||
document={documentProps.document as JSONDocument}
|
document={documentProps.document as JSONDocument}
|
||||||
|
|||||||
@@ -527,7 +527,7 @@
|
|||||||
.openapi-panel,
|
.openapi-panel,
|
||||||
.openapi-codesample,
|
.openapi-codesample,
|
||||||
.openapi-response-examples {
|
.openapi-response-examples {
|
||||||
@apply border shrink min-h-40 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
@apply border shrink min-h-32 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-response-examples-panel {
|
.openapi-response-examples-panel {
|
||||||
@@ -541,7 +541,9 @@
|
|||||||
.openapi-panel pre,
|
.openapi-panel pre,
|
||||||
.openapi-codesample pre,
|
.openapi-codesample pre,
|
||||||
.openapi-response-examples pre {
|
.openapi-response-examples pre {
|
||||||
@apply bg-transparent border-none rounded-none shrink shadow-none;
|
@apply border-none rounded-none shrink shadow-none;
|
||||||
|
/* Override the code block's background to transparent so it matches with the header and footer */
|
||||||
|
@apply bg-transparent!
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-panel-heading {
|
.openapi-panel-heading {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export async function CustomizationRootLayout(props: {
|
|||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
>{`
|
>{`
|
||||||
:root {
|
:root, .light, .dark [data-color-scheme$="light"], .dark [data-follow-color-scheme="true"]:has([data-color-scheme$="light"]) {
|
||||||
${generateColorVariable('primary', customization.styling.primaryColor.light)}
|
${generateColorVariable('primary', customization.styling.primaryColor.light)}
|
||||||
${generateColorVariable('tint', tintColor ? tintColor.light : DEFAULT_TINT_COLOR, { mix: mixColor && { color: mixColor.color.light, ratio: mixColor.ratio.light } })}
|
${generateColorVariable('tint', tintColor ? tintColor.light : DEFAULT_TINT_COLOR, { mix: mixColor && { color: mixColor.color.light, ratio: mixColor.ratio.light } })}
|
||||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR)}
|
${generateColorVariable('neutral', DEFAULT_TINT_COLOR)}
|
||||||
@@ -165,7 +165,7 @@ export async function CustomizationRootLayout(props: {
|
|||||||
${generateColorVariable('success', successColor.light)}
|
${generateColorVariable('success', successColor.light)}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark, :root:not(.dark) [data-color-scheme^="dark"], :root:not(.dark) [data-follow-color-scheme="true"]:has([data-color-scheme^="dark"]) {
|
||||||
${generateColorVariable('primary', customization.styling.primaryColor.dark, { darkMode: true })}
|
${generateColorVariable('primary', customization.styling.primaryColor.dark, { darkMode: true })}
|
||||||
${generateColorVariable('tint', tintColor ? tintColor.dark : DEFAULT_TINT_COLOR, { darkMode: true, mix: mixColor && { color: mixColor?.color.dark, ratio: mixColor.ratio.dark } })}
|
${generateColorVariable('tint', tintColor ? tintColor.dark : DEFAULT_TINT_COLOR, { darkMode: true, mix: mixColor && { color: mixColor?.color.dark, ratio: mixColor.ratio.dark } })}
|
||||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
||||||
|
|||||||
@@ -296,88 +296,36 @@ html.announcement-hidden #announcement-banner {
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shiki theme */
|
|
||||||
:root {
|
|
||||||
--shiki-color-text: rgb(var(--tint-11));
|
|
||||||
--shiki-token-punctuation: rgb(var(--tint-11));
|
|
||||||
--shiki-token-comment: rgb(var(--neutral-9));
|
|
||||||
--shiki-token-link: rgb(var(--primary-10));
|
|
||||||
|
|
||||||
--shiki-token-constant: rgb(var(--warning-10));
|
|
||||||
--shiki-token-string: rgb(var(--warning-10));
|
|
||||||
--shiki-token-string-expression: rgb(var(--success-10));
|
|
||||||
--shiki-token-keyword: rgb(var(--danger-10));
|
|
||||||
--shiki-token-parameter: rgb(var(--warning-10));
|
|
||||||
--shiki-token-function: rgb(var(--primary-10));
|
|
||||||
|
|
||||||
--shiki-token-inserted: rgb(var(--success-10));
|
|
||||||
--shiki-token-deleted: rgb(var(--danger-10));
|
|
||||||
--shiki-token-changed: rgb(var(--tint-12));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
:root {
|
|
||||||
--shiki-color-text: rgb(var(--tint-12));
|
|
||||||
--shiki-token-punctuation: rgb(var(--tint-12));
|
|
||||||
--shiki-token-comment: rgb(var(--neutral-11));
|
|
||||||
--shiki-token-link: rgb(var(--primary-11));
|
|
||||||
|
|
||||||
--shiki-token-constant: rgb(var(--warning-11));
|
|
||||||
--shiki-token-string: rgb(var(--warning-11));
|
|
||||||
--shiki-token-string-expression: rgb(var(--success-11));
|
|
||||||
--shiki-token-keyword: rgb(var(--danger-11));
|
|
||||||
--shiki-token-parameter: rgb(var(--warning-11));
|
|
||||||
--shiki-token-function: rgb(var(--primary-11));
|
|
||||||
|
|
||||||
--shiki-token-inserted: rgb(var(--success-11));
|
|
||||||
--shiki-token-deleted: rgb(var(--danger-11));
|
|
||||||
--shiki-token-changed: rgb(var(--tint-12));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark {
|
|
||||||
/* Override select colors to have more contrast */
|
|
||||||
--shiki-token-comment: rgb(var(--neutral-9));
|
|
||||||
|
|
||||||
--shiki-token-constant: rgb(var(--warning-11));
|
|
||||||
--shiki-token-string: rgb(var(--warning-11));
|
|
||||||
--shiki-token-string-expression: rgb(var(--success-11));
|
|
||||||
--shiki-token-keyword: rgb(var(--danger-11));
|
|
||||||
--shiki-token-parameter: rgb(var(--warning-11));
|
|
||||||
--shiki-token-function: rgb(var(--primary-11));
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-monochrome {
|
|
||||||
--shiki-token-constant: rgb(var(--tint-11));
|
|
||||||
--shiki-token-string: rgb(var(--tint-12));
|
|
||||||
--shiki-token-string-expression: rgb(var(--tint-12));
|
|
||||||
--shiki-token-keyword: rgb(var(--primary-10));
|
|
||||||
--shiki-token-parameter: rgb(var(--tint-9));
|
|
||||||
--shiki-token-function: rgb(var(--primary-9));
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark.code-monochrome {
|
|
||||||
--shiki-token-constant: rgb(var(--tint-11));
|
|
||||||
--shiki-token-string: rgb(var(--tint-12));
|
|
||||||
--shiki-token-string-expression: rgb(var(--tint-12));
|
|
||||||
--shiki-token-keyword: rgb(var(--primary-11));
|
|
||||||
--shiki-token-parameter: rgb(var(--tint-10));
|
|
||||||
--shiki-token-function: rgb(var(--primary-10));
|
|
||||||
}
|
|
||||||
|
|
||||||
html.font-Lato {
|
html.font-Lato {
|
||||||
/* Lato's default ligatures impact readability, so we turn them off */
|
/* Lato's default ligatures impact readability, so we turn them off */
|
||||||
font-variant-ligatures: no-common-ligatures;
|
font-variant-ligatures: no-common-ligatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code blocks */
|
/* Code blocks */
|
||||||
|
/* Shiki themes can define font styling (style, weight, decoration) via CSS variables.
|
||||||
|
* These variables are prefixed with --shiki-{mode}-{property} and allow themes to customize
|
||||||
|
* typography beyond just colors. We apply them here so code blocks respect theme font styling. */
|
||||||
|
.shiki, .shiki span {
|
||||||
|
font-style: var(--shiki-light-font-style) !important;
|
||||||
|
font-weight: var(--shiki-light-font-weight) !important;
|
||||||
|
text-decoration: var(--shiki-light-text-decoration) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .shiki,
|
||||||
|
html.dark .shiki span {
|
||||||
|
font-style: var(--shiki-dark-font-style) !important;
|
||||||
|
font-weight: var(--shiki-dark-font-weight) !important;
|
||||||
|
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight-line {
|
.highlight-line {
|
||||||
@apply grid grid-cols-subgrid col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-1 rounded-sm;
|
@apply text-tint-strong grid grid-cols-subgrid col-span-2 relative hover:invert-5 hover:z-1 rounded-sm;
|
||||||
@apply only:hover:ring-transparent;
|
@apply only:hover:ring-transparent;
|
||||||
@apply [counter-increment:line];
|
@apply [counter-increment:line];
|
||||||
|
|
||||||
&.highlighted {
|
&.highlighted {
|
||||||
@apply bg-tint-active;
|
/* Use `invert-` to get a dynamic color that contrasts with the background, regardless of the codeblock's theme. */
|
||||||
|
@apply bg-tint-base invert-10 hover:invert-15;
|
||||||
@apply first:rounded-t-md first:*:mt-1;
|
@apply first:rounded-t-md first:*:mt-1;
|
||||||
@apply last:rounded-b-md last:*:mb-1;
|
@apply last:rounded-b-md last:*:mb-1;
|
||||||
@apply rounded-none;
|
@apply rounded-none;
|
||||||
@@ -397,12 +345,8 @@ html.font-Lato {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight-line-number {
|
.highlight-line-number {
|
||||||
@apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-linear-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
|
@apply text-sm text-tint text-right pr-3.5 bg-tint-base rounded-l pl-2 sticky left-[-3px] mask-r-from-80%;
|
||||||
@apply before:text-tint before:content-[counter(line)];
|
@apply before:content-[counter(line)] not-contrast-more:before:opacity-6;
|
||||||
|
|
||||||
.highlight-line.highlighted > & {
|
|
||||||
@apply before:text-tint bg-linear-to-r from-80% from-tint-active to-transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-line-content {
|
.highlight-line-content {
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
|
|||||||
icons: api.CustomizationIconsStyle.Regular,
|
icons: api.CustomizationIconsStyle.Regular,
|
||||||
links: api.CustomizationLinksStyle.Default,
|
links: api.CustomizationLinksStyle.Default,
|
||||||
depth: api.CustomizationDepth.Subtle,
|
depth: api.CustomizationDepth.Subtle,
|
||||||
|
codeTheme: {
|
||||||
|
default: {
|
||||||
|
light: api.CustomizationCodeTheme.DefaultLight,
|
||||||
|
dark: api.CustomizationCodeTheme.DefaultDark,
|
||||||
|
},
|
||||||
|
openapi: {
|
||||||
|
light: api.CustomizationCodeTheme.DefaultLight,
|
||||||
|
dark: api.CustomizationCodeTheme.DefaultDark,
|
||||||
|
},
|
||||||
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
background: api.CustomizationSidebarBackgroundStyle.Default,
|
background: api.CustomizationSidebarBackgroundStyle.Default,
|
||||||
list: api.CustomizationSidebarListStyle.Default,
|
list: api.CustomizationSidebarListStyle.Default,
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import clsx from 'classnames';
|
|||||||
import { type ComponentPropsWithoutRef, forwardRef } from 'react';
|
import { type ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||||
|
|
||||||
export function Section(props: ComponentPropsWithoutRef<'div'>) {
|
export function Section(props: ComponentPropsWithoutRef<'div'>) {
|
||||||
return <div {...props} className={clsx('openapi-section', props.className)} />;
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
data-follow-color-scheme="true"
|
||||||
|
className={clsx('openapi-section', props.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
|
export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user