diff --git a/.changeset/nasty-ladybugs-whisper.md b/.changeset/nasty-ladybugs-whisper.md new file mode 100644 index 00000000..6c58facb --- /dev/null +++ b/.changeset/nasty-ladybugs-whisper.md @@ -0,0 +1,5 @@ +--- +"vocs": major +--- + +**Breaking:** Removed the `::snip` directive. Use the `// [!include]` marker instead. [See more.](https://vocs.dev/docs/guides/code-snippets) \ No newline at end of file diff --git a/.changeset/strange-lions-kick.md b/.changeset/strange-lions-kick.md new file mode 100644 index 00000000..0ff356ad --- /dev/null +++ b/.changeset/strange-lions-kick.md @@ -0,0 +1,5 @@ +--- +"vocs": minor +--- + +Added support for [regions in code snippets](https://vocs.dev/docs/guides/code-snippets#regions). \ No newline at end of file diff --git a/package.json b/package.json index 6a89ef83..b5bdd073 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "tsx": "^4.6.1", "typescript": "^5.2.2", "vercel": "^32.5.6", + "viem": "^2.1.1", "vocs": "workspace:*" }, "simple-git-hooks": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e94a0c9a..92308b20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: vercel: specifier: ^32.5.6 version: 32.5.6 + viem: + specifier: ^2.1.1 + version: 2.1.1(typescript@5.2.2) vocs: specifier: workspace:* version: link:src @@ -444,6 +447,10 @@ importers: packages: + /@adraffy/ens-normalize@1.10.0: + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + dev: true + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1808,10 +1815,15 @@ packages: - supports-color dev: false + /@noble/curves@1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + dependencies: + '@noble/hashes': 1.3.2 + dev: true + /@noble/hashes@1.3.2: resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} - dev: false /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -2682,6 +2694,25 @@ packages: dev: false optional: true + /@scure/base@1.1.5: + resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} + dev: true + + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.5 + dev: true + + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.5 + dev: true + /@shuding/opentype.js@1.4.0-beta.0: resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} @@ -3301,6 +3332,20 @@ packages: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true + /abitype@0.10.0(typescript@5.2.2): + resolution: {integrity: sha512-QvMHEUzgI9nPj9TWtUGnS2scas80/qaL5PBxGdwWhhvzqXfOph+IEiiiWrzuisu3U3JgDQVruW9oLbJoQ3oZ3A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + dependencies: + typescript: 5.2.2 + dev: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5594,6 +5639,14 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isows@1.0.3(ws@8.13.0): + resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.13.0 + dev: true + /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -8618,6 +8671,29 @@ packages: vfile-message: 4.0.2 dev: false + /viem@2.1.1(typescript@5.2.2): + resolution: {integrity: sha512-gJiwYceD7Dsjioglr+85GQS3u5Gp9XGG8oJqGsauBaEPFlkmbRx7cxD2Q5RZXFToVvEbarOWtITZtGHBsGv4MQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 0.10.0(typescript@5.2.2) + isows: 1.0.3(ws@8.13.0) + typescript: 5.2.2 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: true + /vite-node@0.28.5(@types/node@20.8.9): resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==} engines: {node: '>=v14.16.0'} @@ -8822,6 +8898,19 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /xdg-app-paths@5.1.0: resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} engines: {node: '>=6'} diff --git a/site/pages/docs/guides/code-snippets.mdx b/site/pages/docs/guides/code-snippets.mdx index 967078fb..72120368 100644 --- a/site/pages/docs/guides/code-snippets.mdx +++ b/site/pages/docs/guides/code-snippets.mdx @@ -1,105 +1,213 @@ # Code Snippets [Including code snippets in Markdown] -You can include code snippets in a Markdown file by making use of the `::snip` directive. +You can include code snippets in a Markdown file by making use of the `// [!include ...]` marker. -:::tip[Tip] -The `"::"` syntax refers to the [Directives Syntax Proposal](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444). -::: +## Quick Start ::::steps -## Create the code snippet +### Create the code snippet + +First, we will create a snippet called `example.ts`. Let's put this under a `snippets/` directory. -First, we will create a snippet called `snippet.ts` that we will import into a Markdown file. +```ts [docs/snippets/example.ts] +// [!include ~/snippets/example.ts:import] -```ts [docs/snippets/snippet.ts] -import { writeFileSync } from "fs" +// [!include ~/snippets/example.ts:setup] -writeFileSync("myfile.txt", "Hello world!") +// [!include ~/snippets/example.ts:usage-1] ``` -## Import the snippet +### Import the snippet + +Next, we will import the snippet into our Markdown file using the `// [!include ...]` marker. -Next, we will import the snippet into our Markdown file using the `::snip` directive. +````mdx [example.md] +#### Usage -```mdx [example.md] -### Usage +```ts +import { writeFileSync } from 'node:fs' +//$ [!include ~/snippets/example.ts] // [!code hl] -::snip{path="~/snippets/snippet.ts"} +writeFileSync('test.txt', blockNumber.toString()) ``` +```` :::info The `"~"` in the path refers to the [root (`config.rootDir`) directory](/docs/structure#root-directory) of the project. ::: -## Output +### Output The resulting output will look like this:
-### Usage +#### Usage ```ts -import { writeFileSync } from "fs" +import { writeFileSync } from 'node:fs' +// [!include ~/snippets/example.ts:import] -writeFileSync("myfile.txt", "Hello world!") // [!code focus] +// [!include ~/snippets/example.ts:setup] + +// [!include ~/snippets/example.ts:usage-1] + +writeFileSync('test.txt', blockNumber.toString()) ```
-## Bonus: Use code block features +:::: + +## Regions + +You can also include a specific region of a code snippet by using the `// [!region]` and `// [!endregion]` markers. + +```ts [docs/snippets/example.ts] +//$ [!region import] // [!code focus] +import { http, createPublicClient } from 'viem' +import { mainnet } from 'viem/chains' +//$ [!endregion import] // [!code focus] + +//$ [!region setup] // [!code focus] +const client = createPublicClient({ + chain: mainnet, + transport: http(), +}) +//$ [!endregion setup] // [!code focus] + +//$ [!region usage] // [!code focus] +const blockNumber = await client.getBlockNumber() +//$ [!endregion usage] // [!code focus] +``` + +Then, we can include the regions in the Markdown with the `// [!include]` marker: + +````md [example.md] +```ts +import { writeFileSync } from 'node:fs' +//$ [!include ~/snippets/example.ts:import] // [!code focus] -We can include features like [line focus (`// [!code focus]`)](/docs/api/markdown#line-focus) in our code snippets. +//$ [!include ~/snippets/example.ts:setup] // [!code focus] -```ts [docs/snippets/snippet.ts] -import { writeFileSync } from "fs" +//$ [!include ~/snippets/example.ts:usage] // [!code focus] -writeFileSync("myfile.txt", "Hello world!") !! [!code focus] +writeFileSync('test.txt', blockNumber.toString()) ``` +```` Which will result in the snippet being rendered like this: -```ts -import { writeFileSync } from "fs" +```ts +import { writeFileSync } from 'node:fs' +// [!include ~/snippets/example.ts:import] -writeFileSync("myfile.txt", "Hello world!") // [!code focus] +// [!include ~/snippets/example.ts:setup] + +// [!include ~/snippets/example.ts:usage-1] + +writeFileSync('test.txt', blockNumber.toString()) ``` -## Bonus: Add a title +### Duplicate variable declarations + +When writing snippets, you may run into a scenario where you want to define multiple regions that share the same variable name. -We can include a title for the code snippet by adding a title to the `::snip` directive. +To avoid type errors, you can use the `_$` suffix to discriminate the variable name. -```mdx [example.md] -### Usage +The rendered snippet will still use the original variable name (ie. the name before the `_$` suffix). -::snip[Example.ts]{path="~/snippets/snippet.ts"} +```ts +// [!region import] +import { http, createPublicClient } from 'viem' +import { mainnet } from 'viem/chains' +// [!endregion import] + +// [!region setup] +const client = createPublicClient({ + chain: mainnet, + transport: http(), +}) +// [!endregion setup] + +// [!region usage-1] // [!code focus] +const block_$1 = await client.getBlock() // [!code focus] +// [!endregion usage-1] // [!code focus] + +// [!region usage-2] // [!code focus] +const block_$2 = await client.getBlock({ blockNumber: 42069n }) // [!code focus] +// [!endregion usage-2] // [!code focus] + +// [!region usage-3] // [!code focus] +const block_$3 = await client.getBlock({ blockTag: 'latest' }) // [!code focus] +// [!endregion usage-3] // [!code focus] ``` +## Tip: Code Block Markers + +We can also include markers like [line highlight (`// [!code hl]`)](/docs/api/markdown#line-highlights) in our code snippets. + +:::code-group + +```ts [docs/snippets/example.ts] +// [!include ~/snippets/example.ts:import] + +// [!include ~/snippets/example.ts:setup] + +// [!include ~/snippets/example.ts:usage-2-docs] +``` + +````md [example.md] +```ts +import { writeFileSync } from 'node:fs' +//$ [!include ~/snippets/example.ts] + +writeFileSync('test.txt', blockNumber.toString()) +``` +```` + +::: + Which will result in the snippet being rendered like this: -```ts [Example.ts] -import { writeFileSync } from "fs" +```ts +import { writeFileSync } from 'node:fs' +// [!include ~/snippets/example.ts:import] + +// [!include ~/snippets/example.ts:setup] + +// [!include ~/snippets/example.ts:usage-2] -writeFileSync("myfile.txt", "Hello world!") // [!code focus] +writeFileSync('test.txt', blockNumber.toString()) ``` -## Bonus: Add Twoslash +## Tip: Twoslash + +We can also include Twoslash markers in our code snippets. + +:::code-group -We can include [Twoslash](/docs/guides/twoslash) for the code snippet by using the `::snip-twoslash` directive. +```ts [docs/snippets/example.ts] +// [!include ~/snippets/example.ts:import] -```mdx [example.md] -### Usage +// [!include ~/snippets/example.ts:setup] -::snip-twoslash[Example.ts]{path="~/snippets/snippet.ts"} +// [!include ~/snippets/example.ts:usage-4] ``` +````md [example.md] +```ts twoslash +//$ [!include ~/snippets/example.ts] +``` +```` + +::: + Which will result in the snippet being rendered like this: -```ts [Example.ts] twoslash -/// -// ---cut--- -import { writeFileSync } from "fs" +```ts twoslash +// [!include ~/snippets/example.ts:import] -writeFileSync("myfile.txt", "Hello world!") // [!code focus] -``` +// [!include ~/snippets/example.ts:setup] -:::: \ No newline at end of file +// [!include ~/snippets/example.ts:usage-4] +``` \ No newline at end of file diff --git a/site/pages/docs/guides/markdown-snippets.mdx b/site/pages/docs/guides/markdown-snippets.mdx index 24d727d0..0f06fdea 100644 --- a/site/pages/docs/guides/markdown-snippets.mdx +++ b/site/pages/docs/guides/markdown-snippets.mdx @@ -6,19 +6,21 @@ You can include other Markdown files in a Markdown file by making use of the `im This only works with MDX (`.mdx`) files. If you want to take advantage of this feature and you are using a `.md` file, rename the file extension to `.mdx`. ::: +## Quick Start + ::::steps -## Create a snippet +### Create a snippet First, we will create a snippet called `snippet.mdx` that we will import into another Markdown file. ```mdx [snippet.mdx] -## Hello world +### Hello world This is my snippet. ``` -## Import the snippet +### Import the snippet Next, we will import the snippet into our MDX file using an `import{:ts}` statement. It compiles to a React component, so we can render it with `{:tsx}`. @@ -32,7 +34,7 @@ This is an example of including a snippet in a Markdown file. ``` -## Output +### Output The resulting output will look like this: @@ -46,7 +48,9 @@ This is an example of including a snippet in a Markdown file. This is my snippet. -## Bonus: Use Props! +:::: + +## Tip: Passing Props As we are just rendering a React component, we can also pass props to `{:tsx}`. We can access those props in the MDX file with the `props` global variable. @@ -66,6 +70,4 @@ This is an example of including a snippet in a Markdown file. {props.content} ``` -::: - -:::: \ No newline at end of file +::: \ No newline at end of file diff --git a/site/snippets/example.ts b/site/snippets/example.ts new file mode 100644 index 00000000..f66b76f3 --- /dev/null +++ b/site/snippets/example.ts @@ -0,0 +1,38 @@ +// [!region import] +import { http, createPublicClient } from 'viem' +import { mainnet } from 'viem/chains' +// [!endregion import] + +// [!region setup] +const client = createPublicClient({ + chain: mainnet, + transport: http(), +}) +// [!endregion setup] + +// [!region usage-1] +const blockNumber_$1 = await client.getBlockNumber() +// [!endregion usage-1] + +// [!region usage-2-docs] +const blockNumber_$2 = await client.getBlockNumber() //$ [!code hl] // [!code focus] +// [!endregion usage-2-docs] + +// [!region usage-2] +const blockNumber_$3 = await client.getBlockNumber() // [!code hl] +// [!endregion usage-2] + +// [!region usage-3-docs] +//$ [!code word:getBlockNumber] // [!code focus] +const blockNumber_$4 = await client.getBlockNumber() +// [!endregion usage-3-docs] + +// [!region usage-3] +// [!code word:getBlockNumber] +const blockNumber_$5 = await client.getBlockNumber() +// [!endregion usage-3] + +// [!region usage-4] +const blockNumber_$6 = await client.getBlockNumber() +// ^? +// [!endregion usage-4] diff --git a/src/app/components/mdx/Pre.tsx b/src/app/components/mdx/Pre.tsx index a2a6fa86..b169924a 100644 --- a/src/app/components/mdx/Pre.tsx +++ b/src/app/components/mdx/Pre.tsx @@ -13,10 +13,7 @@ export function Pre({ function recurseChildren(children: ReactNode): ReactNode { if (!children) return children - if (typeof children !== 'object') { - if (typeof children === 'string') return children.replace('!!', '//') - return children - } + if (typeof children !== 'object') return children if ('props' in children) return { ...children, diff --git a/src/app/styles/vars.css.ts b/src/app/styles/vars.css.ts index b1d6588f..80bf9324 100644 --- a/src/app/styles/vars.css.ts +++ b/src/app/styles/vars.css.ts @@ -36,6 +36,7 @@ export const primitiveColorVars = createGlobalThemeContract( borderRed: 'borderRed', borderYellow: 'borderYellow', heading: 'heading', + inverted: 'inverted', shadow: 'shadow', shadow2: 'shadow2', text: 'text', @@ -86,6 +87,7 @@ createGlobalTheme(':root', primitiveColorVars, { borderRed: globalColors.redA.redA4, borderYellow: globalColors.yellowA.yellowA5, heading: globalColors.gray.gray12, + inverted: black, shadow: globalColors.grayA.grayA3, shadow2: globalColors.grayA.grayA2, text: '#4c4c4c', @@ -134,6 +136,7 @@ createGlobalTheme(':root.dark', primitiveColorVars, { borderRed: globalColors.redA.redA4, borderYellow: globalColors.yellowA.yellowA2, heading: '#e9e9ea', + inverted: white, shadow: globalColors.grayDarkA.grayA1, shadow2: globalColors.blackA.blackA1, text: '#cfcfcf', diff --git a/src/vite/plugins/mdx.ts b/src/vite/plugins/mdx.ts index 5ac860ca..f3352259 100644 --- a/src/vite/plugins/mdx.ts +++ b/src/vite/plugins/mdx.ts @@ -30,12 +30,13 @@ import { remarkCode } from './remark/code.js' import { remarkDetails } from './remark/details.js' import { remarkInferFrontmatter } from './remark/inferred-frontmatter.js' import { remarkLinks } from './remark/links.js' -import { remarkSnippets } from './remark/snippets.js' import { remarkSponsors } from './remark/sponsors.js' import { remarkSteps } from './remark/steps.js' import { remarkStrongBlock } from './remark/strong-block.js' import { remarkSubheading } from './remark/subheading.js' import { remarkTwoslash } from './remark/twoslash.js' +import { transformerDisplayNotation } from './shikiji/transformerDisplayNotation.js' +import { transformerNotationInclude } from './shikiji/transformerNotationInclude.js' import { transformerSplitIdentifiers } from './shikiji/transformerSplitIdentifiers.js' import { twoslashRenderer } from './shikiji/twoslashRenderer.js' import { twoslasher } from './shikiji/twoslasher.js' @@ -57,7 +58,6 @@ export const getRemarkPlugins = ({ markdown }: RemarkPluginsParameters = {}) => remarkBlogPosts, remarkCallout, remarkCode, - remarkSnippets, remarkCodeGroup, remarkDetails, remarkSponsors, @@ -72,10 +72,15 @@ export const remarkPlugins = getRemarkPlugins() type RehypePluginsParameters = { markdown?: ParsedConfig['markdown'] + rootDir?: ParsedConfig['rootDir'] twoslash?: ParsedConfig['twoslash'] } -export const getRehypePlugins = ({ markdown, twoslash = {} }: RehypePluginsParameters = {}) => +export const getRehypePlugins = ({ + markdown, + rootDir = '', + twoslash = {}, +}: RehypePluginsParameters = {}) => [ rehypeSlug, [ @@ -87,6 +92,8 @@ export const getRehypePlugins = ({ markdown, twoslash = {} }: RehypePluginsParam transformerNotationFocus(), transformerNotationHighlight(), transformerNotationWordHighlight(), + transformerNotationInclude({ rootDir }), + transformerDisplayNotation(), transformerTwoslash({ explicitTrigger: true, renderer: twoslashRenderer(), @@ -123,9 +130,9 @@ export const rehypePlugins = getRehypePlugins() export async function mdx(): Promise { const { config } = await resolveVocsConfig() - const { markdown, twoslash } = config + const { markdown, rootDir, twoslash } = config const remarkPlugins = getRemarkPlugins({ markdown }) - const rehypePlugins = getRehypePlugins({ markdown, twoslash }) + const rehypePlugins = getRehypePlugins({ markdown, rootDir, twoslash }) return [ mdxPlugin({ providerImportSource: '@mdx-js/react', diff --git a/src/vite/plugins/remark/snippets.ts b/src/vite/plugins/remark/snippets.ts deleted file mode 100644 index ca532c86..00000000 --- a/src/vite/plugins/remark/snippets.ts +++ /dev/null @@ -1,45 +0,0 @@ -/// -/// - -import { readFileSync } from 'fs' -import { resolve } from 'node:path' -import type { Root, Text } from 'mdast' -import { visit } from 'unist-util-visit' - -import type { ContainerDirective } from 'mdast-util-directive' -import { resolveVocsConfig } from '../../utils/resolveVocsConfig.js' - -export function remarkSnippets() { - return async (tree: Root) => { - const { config } = await resolveVocsConfig() - const { rootDir } = config - - visit(tree, (node, index, parent) => { - if (node.type !== 'leafDirective') return - if (node.name !== 'snip' && node.name !== 'snip-twoslash') return - if (typeof index !== 'number') return - - const fileName = node.attributes?.path - if (!fileName) return - - const path = resolve(rootDir, fileName.replace('~', '.')) - const contents = readFileSync(path, { encoding: 'utf-8' }).replace(/\n$/, '') - - const title = (node.children[0] as Text)?.value - const lang = path.split('.').pop() as string - - parent?.children.splice(index, 1, { - type: 'code', - lang, - meta: title - ? `${ - (parent as ContainerDirective).name === 'code-group' - ? `[${title}]` - : `title="${title}"` - } ${node.name === 'snip-twoslash' ? 'twoslash' : ''}}` - : '', - value: contents, - }) - }) - } -} diff --git a/src/vite/plugins/shikiji/transformerDisplayNotation.ts b/src/vite/plugins/shikiji/transformerDisplayNotation.ts new file mode 100644 index 00000000..b5834e88 --- /dev/null +++ b/src/vite/plugins/shikiji/transformerDisplayNotation.ts @@ -0,0 +1,8 @@ +import type { ShikijiTransformer } from 'shikiji' + +export const transformerDisplayNotation = (): ShikijiTransformer => ({ + name: 'display-notation', + postprocess(html) { + return html.replaceAll(/\/\/\$ \[(.*)\]/g, '// [$1]') + }, +}) diff --git a/src/vite/plugins/shikiji/transformerNotationInclude.ts b/src/vite/plugins/shikiji/transformerNotationInclude.ts new file mode 100644 index 00000000..16855bdf --- /dev/null +++ b/src/vite/plugins/shikiji/transformerNotationInclude.ts @@ -0,0 +1,68 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import type { ShikijiTransformer } from 'shikiji' + +const includeRegex = /\/\/ \[!include (.*)\]/ +const regionRegex = /\/\/ \[!region (.*)\]/ +const endRegionRegex = /\/\/ \[!endregion (.*)\]/ + +export type TransformerNotationIncludeOptions = { + rootDir: string +} + +export const transformerNotationInclude = ({ + rootDir, +}: TransformerNotationIncludeOptions): ShikijiTransformer => ({ + name: 'includes', + preprocess(code) { + if (!code) return code + + const includes = code.includes('// [!include') + if (!includes) return code + + const lines = code.split('\n') + let i = 0 + while (i < lines.length) { + const line = lines[i] + const match = line.match(includeRegex) + if (match) { + const [, value] = match + const [fileName, region] = value.split(':') + const path = resolve(rootDir, fileName.replace('~', '.')) + const contents = readFileSync(path, { encoding: 'utf-8' }).replace(/\n$/, '') + lines.splice(i, 1, extractRegion(contents, region)) + } + i++ + } + return lines.join('\n') + }, +}) + +function extractRegion(code: string, region: string | undefined) { + const lines = [] + + let inRegion = !region + for (const line of code.split('\n')) { + const startRegionMatch = line.match(regionRegex) + const endRegionMatch = line.match(endRegionRegex) + + // If we are in a region, ignore any other region markers. + if (inRegion && !startRegionMatch && !endRegionMatch) lines.push(line.replaceAll(/_\$\d*/g, '')) + // If we have have found a `// [!region ${region}]` marker, start including lines. + else if (startRegionMatch) { + const [, regionName] = startRegionMatch + if (regionName === region) { + inRegion = true + } + } + // If we have have found a `// [!endregion ${region}]` marker, stop including lines. + else if (endRegionMatch) { + const [, regionName] = endRegionMatch + if (regionName === region) { + inRegion = false + } + } + } + + return lines.join('\n') +}