-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(markdown): don’t generate mdast html nodes (#10104)
* fix(markdown): don’t generate mdast html nodes `html` nodes from mdast are converted to `raw` hast nodes. These nodes are then not processed by proper rehype plugins. Typically if a remark plugin generates `html` nodes, this indicates it should have actually been a rehype plugin. This changes the remark plugins that generate `html` nodes into rehype nodes. These were `remarkPrism` and `remarkShiki`. Closes #9909 * Apply suggestions from code review * refactor(mdx): move user defined rehype plugins after syntax highlighting * fix(mdx): fix issue in mdx rehype plugin ordering * docs: explain why html/raw nodes are avoided in changeset This also includes some hints on what users could do to upgrade of they rely on these nodes. * Fix MDX rehype plugin ordering * refactor(remark): restore remarkPrism and remarkShiki They aren’t used anymore, but removing would be a breaking change. * chore: mark deprecated * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <[email protected]> * Update .changeset/thirty-beds-smoke.md Co-authored-by: Sarah Rainsberger <[email protected]> --------- Co-authored-by: Emanuele Stoppa <[email protected]> Co-authored-by: Bjorn Lu <[email protected]> Co-authored-by: Sarah Rainsberger <[email protected]>
- Loading branch information
1 parent
5a95287
commit a31bbd7
Showing
10 changed files
with
166 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
"@astrojs/mdx": minor | ||
"@astrojs/markdown-remark": minor | ||
--- | ||
|
||
Changes Astro's internal syntax highlighting to use rehype plugins instead of remark plugins. This provides better interoperability with other [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) that deal with code blocks, in particular with third party syntax highlighting plugins and [`rehype-mermaid`](https://github.com/remcohaszing/rehype-mermaid). | ||
|
||
This may be a breaking change if you are currently using: | ||
- a remark plugin that relies on nodes of type `html` | ||
- a rehype plugin that depends on nodes of type `raw`. | ||
|
||
Please review your rendered code samples carefully, and if necessary, consider using a rehype plugin that deals with the generated `element` nodes instead. You can transform the AST of raw HTML strings, or alternatively use [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) to get a string from a `raw` node. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { Element, Root } from 'hast'; | ||
import { fromHtml } from 'hast-util-from-html'; | ||
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) => string; | ||
|
||
const languagePattern = /\blanguage-(\S+)\b/; | ||
|
||
/** | ||
* A hast utility to syntax highlight code blocks with a given syntax highlighter. | ||
* | ||
* @param tree | ||
* The hast tree in which to syntax highlight code blocks. | ||
* @param highlighter | ||
* A fnction which receives the code and language, and returns the HTML of a syntax | ||
* highlighted `<pre>` element. | ||
*/ | ||
export function highlightCodeBlocks(tree: Root, highlighter: Highlighter) { | ||
// We’re looking for `<code>` elements | ||
visitParents(tree, { type: 'element', tagName: 'code' }, (node, ancestors) => { | ||
const parent = ancestors.at(-1); | ||
|
||
// Whose parent is a `<pre>`. | ||
if (parent?.type !== 'element' || parent.tagName !== 'pre') { | ||
return; | ||
} | ||
|
||
// Where the `<code>` is the only child. | ||
if (parent.children.length !== 1) { | ||
return; | ||
} | ||
|
||
// And the `<code>` has a class name that starts with `language-`. | ||
let languageMatch: RegExpMatchArray | null | undefined; | ||
let { className } = node.properties; | ||
if (typeof className === 'string') { | ||
languageMatch = className.match(languagePattern); | ||
} else if (Array.isArray(className)) { | ||
for (const cls of className) { | ||
if (typeof cls !== 'string') { | ||
continue; | ||
} | ||
|
||
languageMatch = cls.match(languagePattern); | ||
if (languageMatch) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
// Don’t mighlight math code blocks. | ||
if (languageMatch?.[1] === 'math') { | ||
return; | ||
} | ||
|
||
const code = toText(node, { whitespace: 'pre' }); | ||
const html = highlighter(code, languageMatch?.[1] || 'plaintext'); | ||
// 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); | ||
|
||
// We replace the parent in its parent with the new `<pre>` element. | ||
const grandParent = ancestors.at(-2)!; | ||
const index = grandParent.children.indexOf(parent); | ||
grandParent.children[index] = replacement; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter'; | ||
import type { Root } from 'hast'; | ||
import type { Plugin } from 'unified'; | ||
import { highlightCodeBlocks } from './highlight.js'; | ||
|
||
export const rehypePrism: Plugin<[], Root> = () => (tree) => { | ||
highlightCodeBlocks(tree, (code, language) => { | ||
let { html, classLanguage } = runHighlighterWithAstro(language, code); | ||
|
||
return `<pre class="${classLanguage}"><code is:raw class="${classLanguage}">${html}</code></pre>`; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { Root } from 'hast'; | ||
import type { Plugin } from 'unified'; | ||
import { createShikiHighlighter, type ShikiHighlighter } from './shiki.js'; | ||
import type { ShikiConfig } from './types.js'; | ||
import { highlightCodeBlocks } from './highlight.js'; | ||
|
||
export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => { | ||
let highlighterAsync: Promise<ShikiHighlighter> | undefined; | ||
|
||
return async (tree) => { | ||
highlighterAsync ??= createShikiHighlighter(config); | ||
const highlighter = await highlighterAsync; | ||
|
||
highlightCodeBlocks(tree, highlighter.highlight); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.