From e5bb3446930162908a5cebfb667beff56b4ef6bc Mon Sep 17 00:00:00 2001 From: Flower-F <78215016+Flower-F@users.noreply.github.com> Date: Tue, 11 Oct 2022 21:21:10 +0800 Subject: [PATCH] feat: code line highlight (#82) * feat: code line highlight * docs: typo * docs: file structure and typo --- docs/en/guide/use-mdx.mdx | 86 ++++++++++++++++++++++ docs/zh/guide/use-mdx.mdx | 86 ++++++++++++++++++++++ src/node/plugin-mdx/rehypePlugins/shiki.ts | 58 ++++++++++++++- 3 files changed, 228 insertions(+), 2 deletions(-) diff --git a/docs/en/guide/use-mdx.mdx b/docs/en/guide/use-mdx.mdx index 870230c1..734c7751 100644 --- a/docs/en/guide/use-mdx.mdx +++ b/docs/en/guide/use-mdx.mdx @@ -52,3 +52,89 @@ The properties defined in front matter will be passed to the component as `meta` ``` You can see more front matter config detail in [config-front-matter](/en/api/config-front-matter). + +## Code Line Highlighting + +Island.js supports highlighting of specified code lines. You can specify line highlighting in any of the following ways. + +**Input:** + +````bash +```js{1} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` +```` + +**Output:** + +```js{1} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` + +In addition to highlight the single line, you can also specify multiple lines to highlight at a time. + +- A Line Range: `{1-10}` +- Multiple single lines: `{1,3,5}` +- Combine Line Range and Single Lines:`{3,5-13,20}` + +**Input:** + +````bash +```js{1,4-7,10} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` +```` + +**Output:** + +```js{1,4-7,10} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` diff --git a/docs/zh/guide/use-mdx.mdx b/docs/zh/guide/use-mdx.mdx index e8f30b19..f9b87116 100644 --- a/docs/zh/guide/use-mdx.mdx +++ b/docs/zh/guide/use-mdx.mdx @@ -56,3 +56,89 @@ title: Hello World ``` 更多的配置详情请参考 [config-front-matter](/zh/api/config-front-matter)。 + +## 指定代码行高亮 + +Island.js 支持指定代码行的高亮显示。你可以用以下任意方式指定代码行高亮。 + +**输入:** + +````bash +```js{1} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` +```` + +**输出:** + +```js{1} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` + +除了单行之外,你还可以同时指定多行代码高亮。 + +- 指定一个范围:`{1-10}` +- 指定多个单行:`{1,3,5}` +- 多行和单行结合:`{3,5-13,20}` + +**输入:** + +````bash +```js{1,4-7,10} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` +```` + +**输出:** + +```js{1,4-7,10} +import { defineConfig } from 'islandjs'; + +export default defineConfig({ + themeConfig: { + navbar: [ + { + text: 'Home', + link: '/', + activeMatch: '^/$|^/' + } + ] + } +}); +``` diff --git a/src/node/plugin-mdx/rehypePlugins/shiki.ts b/src/node/plugin-mdx/rehypePlugins/shiki.ts index a22686b6..f4180fde 100644 --- a/src/node/plugin-mdx/rehypePlugins/shiki.ts +++ b/src/node/plugin-mdx/rehypePlugins/shiki.ts @@ -8,6 +8,29 @@ interface Options { highlighter: shiki.Highlighter; } +function highlightSingleLine( + line: number, + fragmentAst: ReturnType +) { + // Children are composed of span and \n alternately, so we should get the even rows to highlight + if ( + line >= 1 && + line * 2 - 2 < + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + fragmentAst.children[0].children[0].children.length && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + fragmentAst.children[0].children[0].children?.[line * 2 - 2]?.properties + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + fragmentAst.children[0].children[0].children[ + line * 2 - 2 + ].properties.className = 'line highlighted'; + } +} + // https://github.com/leafac/rehype-shiki/blob/41e64054d72ab29d5ad48c4c070499fc075090e9/source/index.ts // The plugin cannot be used directly because it won't reserve the class name `language-xxx` in the code tag // It cause conflict with preWrapper plugin, so we should integrate it manually @@ -25,18 +48,49 @@ export const rehypePluginShiki: Plugin<[Options], import('hast').Root> = ({ const codeNode = node.children[0]; const codeContent = (node.children[0].children[0] as Text).value; const codeClassName = codeNode.properties?.className?.toString() || ''; - const lang = codeClassName.split('-')[1]; + const highlightLinesReg = /language-([a-zA-Z]*)\s*({[\d,-]*})?/i; + const highlightRegExecResult = highlightLinesReg.exec(codeClassName); + + if (!highlightRegExecResult) { + return; + } + + const lang = highlightRegExecResult[1]; if (!lang) { return; } + // Support single line and line range + const highlightLines: (number | number[])[] = []; + highlightRegExecResult[2] + ?.slice(1, highlightRegExecResult[2].length - 1) + ?.split(',') + .forEach((str) => { + if (str.includes('-')) { + const [start, end] = str.split('-'); + highlightLines.push([parseInt(start), parseInt(end)]); + } else { + highlightLines.push(parseInt(str)); + } + }); + const highlightedCode = highlighter.codeToHtml(codeContent, { lang }); const fragmentAst = fromHtml(highlightedCode, { fragment: true }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Reserve the class name `language-xxx` in the code tag fragmentAst.children[0].children[0].properties.className = - codeClassName; + 'language-' + lang; + + highlightLines.forEach((line) => { + if (line instanceof Array) { + for (let i = line[0]; i <= line[1]; i++) { + highlightSingleLine(i, fragmentAst); + } + } else { + highlightSingleLine(line, fragmentAst); + } + }); parent?.children.splice(index!, 1, ...fragmentAst.children); } });