Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor createShikiHighlighter #11825

Merged
merged 4 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/breezy-colts-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/markdoc': patch
---

Uses latest version of `@astrojs/markdown-remark` with updated Shiki APIs
5 changes: 5 additions & 0 deletions .changeset/hungry-jokes-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': major
---

Updates return object of `createShikiHighlighter` as `codeToHast` and `codeToHtml` to allow generating either the hast or html string directly
9 changes: 9 additions & 0 deletions .changeset/large-zebras-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'astro': major
---

Updates internal Shiki rehype plugin to highlight code blocks as hast (using Shiki's `codeToHast()` API). This allows a more direct markdown and MDX processing, and improves the performance when building the project.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

However, a caveat with `codeToHast()` is that Shiki transformers' `postprocess` hook will now not run on code blocks in `.md` and `.mdx` files (also [documented in Shiki](https://shiki.style/guide/transformers#transformer-hooks)). Make sure the Shiki transformers passed to `markdown.shikiConfig.transformers` do not use the `postprocess` hook to avoid issues with the HTML output.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

Code blocks in `.mdoc` files and `<Code />` component will still work the same and shouldn't need any changes as they do not use the internal Shiki rehype plugin.
bluwy marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 4 additions & 4 deletions packages/astro/components/Code.astro
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ const highlighter = await getCachedHighlighter({
],
theme,
themes,
defaultColor,
wrap,
transformers,
});
const html = await highlighter.highlight(code, typeof lang === 'string' ? lang : lang.name, {
const html = await highlighter.codeToHtml(code, typeof lang === 'string' ? lang : lang.name, {
defaultColor,
wrap,
inline,
transformers,
meta,
attributes: rest as any,
});
Expand Down
12 changes: 10 additions & 2 deletions packages/integrations/markdoc/src/extensions/shiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { unescapeHTML } from 'astro/runtime/server/index.js';
import type { AstroMarkdocConfig } from '../config.js';

export default async function shiki(config?: ShikiConfig): Promise<AstroMarkdocConfig> {
const highlighter = await createShikiHighlighter(config);
const highlighter = await createShikiHighlighter({
langs: config?.langs,
theme: config?.theme,
themes: config?.themes,
});

return {
nodes: {
Expand All @@ -16,7 +20,11 @@ export default async function shiki(config?: ShikiConfig): Promise<AstroMarkdocC
// Only the `js` part is parsed as `attributes.language` and the rest is ignored. This means
// some Shiki transformers may not work correctly as it relies on the `meta`.
const lang = typeof attributes.language === 'string' ? attributes.language : 'plaintext';
const html = await highlighter.highlight(attributes.content, lang);
const html = await highlighter.codeToHtml(attributes.content, lang, {
wrap: config?.wrap,
defaultColor: config?.defaultColor,
transformers: config?.transformers,
});

// Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML
return unescapeHTML(html) as any;
Expand Down
23 changes: 16 additions & 7 deletions packages/markdown/remark/src/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { toText } from 'hast-util-to-text';
import { removePosition } from 'unist-util-remove-position';
import { visitParents } from 'unist-util-visit-parents';

type Highlighter = (code: string, language: string, options?: { meta?: string }) => Promise<string>;
type Highlighter = (
code: string,
language: string,
options?: { meta?: string },
) => Promise<Root | string>;

const languagePattern = /\blanguage-(\S+)\b/;

Expand Down Expand Up @@ -73,12 +77,17 @@ export async function highlightCodeBlocks(tree: Root, highlighter: Highlighter)
for (const { node, language, grandParent, parent } of nodes) {
const meta = (node.data as any)?.meta ?? node.properties.metastring ?? undefined;
const code = toText(node, { whitespace: 'pre' });
// TODO: In Astro 5, have `highlighter()` return hast directly to skip expensive HTML parsing and serialization.
const html = await highlighter(code, language, { meta });
// The replacement returns a root node with 1 child, the `<pr>` element replacement.
const replacement = fromHtml(html, { fragment: true }).children[0] as Element;
// We just generated this node, so any positional information is invalid.
removePosition(replacement);
const result = await highlighter(code, language, { meta });

let replacement: Element;
if (typeof result === 'string') {
// The replacement returns a root node with 1 child, the `<pre>` element replacement.
replacement = fromHtml(result, { fragment: true }).children[0] as Element;
// We just generated this node, so any positional information is invalid.
removePosition(replacement);
} else {
replacement = result.children[0] as Element;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this cast safe? we know there's at least 1 child and its an Element?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it should be. We currently control this function (it's mostly internal details for Shiki only), so if it changes we can also tweak this here.

}

// We replace the parent in its parent with the new `<pre>` element.
const index = grandParent.children.indexOf(parent);
Expand Down
7 changes: 6 additions & 1 deletion packages/markdown/remark/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export { rehypeHeadingIds } from './rehype-collect-headings.js';
export { remarkCollectImages } from './remark-collect-images.js';
export { rehypePrism } from './rehype-prism.js';
export { rehypeShiki } from './rehype-shiki.js';
export { createShikiHighlighter, type ShikiHighlighter } from './shiki.js';
export {
createShikiHighlighter,
type ShikiHighlighter,
type CreateShikiHighlighterOptions,
type ShikiHighlighterHighlightOptions,
} from './shiki.js';
export * from './types.js';

export const markdownConfigDefaults: Required<AstroMarkdownOptions> = {
Expand Down
15 changes: 13 additions & 2 deletions packages/markdown/remark/src/rehype-shiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => {
let highlighterAsync: Promise<ShikiHighlighter> | undefined;

return async (tree) => {
highlighterAsync ??= createShikiHighlighter(config);
highlighterAsync ??= createShikiHighlighter({
langs: config?.langs,
theme: config?.theme,
themes: config?.themes,
});
const highlighter = await highlighterAsync;

await highlightCodeBlocks(tree, highlighter.highlight);
await highlightCodeBlocks(tree, (code, language, options) => {
return highlighter.codeToHast(code, language, {
meta: options?.meta,
wrap: config?.wrap,
defaultColor: config?.defaultColor,
transformers: config?.transformers,
});
});
};
};
Loading
Loading