diff --git a/astro.config.mjs b/astro.config.mjs index 8525479..6b5327c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,6 +6,7 @@ import starlightUtils from "@lorenzo_lewis/starlight-utils"; import wasm from "vite-plugin-wasm"; import shikiConfig from "./src/utils/shiki"; +import remarkVersionBadges from "./src/remark-plugins/version-badges"; const playgroundSidebarEntry = { label: 'Playground', @@ -67,6 +68,9 @@ export default defineConfig({ Pagination: "./src/components/override/Pagination.astro", SocialIcons: './src/components/override/SocialIcons.astro', }, + customCss: [ + './src/remark-plugins/version-badges.css', + ], sidebar: [ { label: "leadingNavLinks", @@ -131,5 +135,10 @@ export default defineConfig({ plugins: [ wasm(), ], - } + }, + markdown: { + remarkPlugins: [ + remarkVersionBadges, + ], + }, }); diff --git a/package.json b/package.json index d5a331d..1b4c7e5 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,10 @@ "@mui/icons-material": "^6.1.1", "@mui/material": "^6.1.1", "@shikijs/monaco": "^1.7.0", + "@types/mdast": "^4.0.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/unist": "^3.0.3", "astro": "^4.15.9", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -32,6 +34,8 @@ "starlight-links-validator": "^0.12.1", "tm-themes": "^1.4.3", "typescript": "^5.4.5", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", "use-immer": "^0.10.0", "vite-plugin-wasm": "^3.3.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5061e6c..e59d22d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,12 +38,18 @@ importers: '@shikijs/monaco': specifier: ^1.7.0 version: 1.7.0 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 '@types/react': specifier: ^18.3.3 version: 18.3.3 '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@types/unist': + specifier: ^3.0.3 + version: 3.0.3 astro: specifier: ^4.15.9 version: 4.15.9(rollup@4.22.5)(sass@1.77.6)(typescript@5.4.5) @@ -71,6 +77,12 @@ importers: typescript: specifier: ^5.4.5 version: 5.4.5 + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 use-immer: specifier: ^0.10.0 version: 0.10.0(immer@10.1.1)(react@18.3.1) @@ -1014,8 +1026,8 @@ packages: '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - '@types/unist@3.0.2': - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -3754,11 +3766,11 @@ snapshots: '@types/hast@3.0.4': dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 '@types/mdast@4.0.4': dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 '@types/mdx@2.0.13': {} @@ -3766,7 +3778,7 @@ snapshots: '@types/nlcst@2.0.3': dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 '@types/node@17.0.45': {} @@ -3795,7 +3807,7 @@ snapshots: '@types/unist@2.0.10': {} - '@types/unist@3.0.2': {} + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.2.0': {} @@ -4277,7 +4289,7 @@ snapshots: estree-util-visit@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 estree-walker@2.0.2: {} @@ -4387,7 +4399,7 @@ snapshots: hast-util-from-parse5@8.0.1: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 8.0.0 property-information: 6.5.0 @@ -4422,7 +4434,7 @@ snapshots: hast-util-raw@9.0.3: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 '@ungap/structured-clone': 1.2.0 hast-util-from-parse5: 8.0.1 hast-util-to-parse5: 8.0.0 @@ -4438,7 +4450,7 @@ snapshots: hast-util-select@6.0.2: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 bcp-47-match: 2.0.3 comma-separated-tokens: 2.0.3 css-selector-parser: 3.0.5 @@ -4478,7 +4490,7 @@ snapshots: hast-util-to-html@9.0.1: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 hast-util-raw: 9.0.3 @@ -4493,7 +4505,7 @@ snapshots: hast-util-to-html@9.0.3: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 hast-util-whitespace: 3.0.0 @@ -4508,7 +4520,7 @@ snapshots: dependencies: '@types/estree': 1.0.5 '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -4541,7 +4553,7 @@ snapshots: hast-util-to-text@4.0.2: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 hast-util-is-element: 3.0.0 unist-util-find-after: 5.0.0 @@ -4731,13 +4743,13 @@ snapshots: mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-visit: 5.0.0 mdast-util-directive@3.0.0: dependencies: '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 devlop: 1.1.0 mdast-util-from-markdown: 2.0.1 mdast-util-to-markdown: 2.1.0 @@ -4757,7 +4769,7 @@ snapshots: mdast-util-from-markdown@2.0.1: dependencies: '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 decode-named-character-reference: 1.0.2 devlop: 1.1.0 mdast-util-to-string: 4.0.0 @@ -4844,7 +4856,7 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 mdast-util-from-markdown: 2.0.1 @@ -4898,7 +4910,7 @@ snapshots: mdast-util-to-markdown@2.1.0: dependencies: '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 longest-streak: 3.1.0 mdast-util-phrasing: 4.1.0 mdast-util-to-string: 4.0.0 @@ -5130,7 +5142,7 @@ snapshots: dependencies: '@types/acorn': 4.0.6 '@types/estree': 1.0.5 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 devlop: 1.1.0 estree-util-visit: 2.0.0 micromark-util-symbol: 2.0.0 @@ -5302,7 +5314,7 @@ snapshots: parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 nlcst-to-string: 4.0.0 unist-util-modify-children: 4.0.0 unist-util-visit-children: 3.0.0 @@ -5812,7 +5824,7 @@ snapshots: unified@11.0.5: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 bail: 2.0.2 devlop: 1.1.0 extend: 3.0.2 @@ -5822,47 +5834,47 @@ snapshots: unist-util-find-after@5.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-is: 6.0.0 unist-util-is@6.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-modify-children@4.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 array-iterate: 2.0.1 unist-util-position-from-estree@2.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-position@5.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-remove-position@5.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-visit: 5.0.0 unist-util-stringify-position@4.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-visit-children@3.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-visit-parents@6.0.1: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-is: 6.0.0 unist-util-visit@5.0.0: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 @@ -5881,17 +5893,17 @@ snapshots: vfile-location@5.0.2: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 vfile: 6.0.3 vfile-message@4.0.2: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 vfile@6.0.3: dependencies: - '@types/unist': 3.0.2 + '@types/unist': 3.0.3 vfile-message: 4.0.2 vite-plugin-wasm@3.3.0(vite@5.4.8(sass@1.77.6)): diff --git a/src/remark-plugins/version-badges.css b/src/remark-plugins/version-badges.css new file mode 100644 index 0000000..d779a50 --- /dev/null +++ b/src/remark-plugins/version-badges.css @@ -0,0 +1,55 @@ +.version-badges { + display: flex; + gap: 0.5rem; + margin: 0.5em 0 !important; + margin-bottom: 0 !important; + flex-wrap: wrap; +} + +.version-badges + p { + margin-top: 0.5rem !important; +} + +.version-badges span { + font-size: 0.75rem; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + border: 1px solid transparent; +} + +html[data-theme='light'] .version-badges .since-badge { + background-color: #e0f7fa; + color: #00796b; + border-color: #b2ebf2; +} + +html[data-theme='light'] .version-badges .changed-badge { + background-color: #fff8e1; + color: #795548; + border-color: #ffe082; +} + +html[data-theme='light'] .version-badges .deprecated-badge { + background-color: #ffebee; + color: #b71c1c; + border-color: #ffcdd2; +} + +html[data-theme='dark'] .version-badges .since-badge { + background-color: #004d40; + color: #b2dfdb; + border-color: #00695c; +} + +html[data-theme='dark'] .version-badges .changed-badge { + background-color: #3e2723; + color: #ffe0b2; + border-color: #6d5037; +} + +html[data-theme='dark'] .version-badges .deprecated-badge { + background-color: #4a0000; + color: #ffcdd2; + border-color: #b71c1c; +} diff --git a/src/remark-plugins/version-badges.ts b/src/remark-plugins/version-badges.ts new file mode 100644 index 0000000..d597428 --- /dev/null +++ b/src/remark-plugins/version-badges.ts @@ -0,0 +1,64 @@ +import { visit } from 'unist-util-visit'; +import type { Plugin } from 'unified'; +import type { Parent } from 'unist'; +import type { Paragraph, Text } from 'mdast'; + +const INLINE_REGEX = /!(since|changed|deprecated)\[([^\]]+)\]/g; + +const remarkVersionBadges: Plugin = () => { + return (tree) => { + visit(tree, 'paragraph', (node, index, parent) => { + const paragraph = node as Paragraph; + const parentNode = parent as Parent; + + if (!paragraph.children || paragraph.children.length !== 1) return; + + const child = paragraph.children[0]; + if(child.type !== 'text') return; + + const value = child.value; + + const matches = [...value.matchAll(INLINE_REGEX)]; + if(matches.length === 0) return; + + const isOnlyBadges = matches.reduce((allMatched, match) => { + return allMatched && match[0] === value.slice(match.index!, match.index! + match[0].length) + }, true); + + if (!isOnlyBadges) return; + + const badges: string[] = matches.map(([_, type, version]) => { + let label = ''; + let className = ''; + let title = ''; + switch(type) { + case 'since': + label = `Since v${version}`; + className = 'since-badge'; + title = `This feature was added in v${version}`; + break; + case 'changed': + label = `Changed in v${version}`; + className = 'changed-badge'; + title = `This feature was changed in v${version}`; + break; + case 'deprecated': + label = `Deprecated since v${version}`; + className = 'deprecated-badge' + title = `This feature is deprecated since v${version}`; + break; + } + return `${label}`; + }); + + const htmlNode = { + type: 'html', + value: `