From f721911eae7c66ae0b45c5f48cfff960dbabdc56 Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 8 Jan 2025 21:10:46 +0800 Subject: [PATCH] Fix markdoc render code block in if tag --- .changeset/twelve-cobras-shake.md | 5 + .../markdoc/components/Renderer.astro | 17 +- .../markdoc/components/TreeNode.ts | 146 ++++++++++-------- .../src/content/blog/with-components.mdoc | 9 ++ .../src/content/blog/with-components.mdoc | 9 ++ .../markdoc/test/render-components.test.js | 5 + .../test/render-extends-components.test.js | 5 + .../markdoc/test/syntax-highlighting.test.js | 21 +++ 8 files changed, 142 insertions(+), 75 deletions(-) create mode 100644 .changeset/twelve-cobras-shake.md diff --git a/.changeset/twelve-cobras-shake.md b/.changeset/twelve-cobras-shake.md new file mode 100644 index 000000000000..dc4ad6de7602 --- /dev/null +++ b/.changeset/twelve-cobras-shake.md @@ -0,0 +1,5 @@ +--- +'@astrojs/markdoc': patch +--- + +Fixes rendering code blocks within if tags diff --git a/packages/integrations/markdoc/components/Renderer.astro b/packages/integrations/markdoc/components/Renderer.astro index c26d92ad737a..270604091fd6 100644 --- a/packages/integrations/markdoc/components/Renderer.astro +++ b/packages/integrations/markdoc/components/Renderer.astro @@ -1,6 +1,6 @@ --- //! astro-head-inject -import type { Config } from '@markdoc/markdoc'; +import type { Config, RenderableTreeNodes } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; import { ComponentNode, createTreeNode } from './TreeNode.js'; @@ -12,13 +12,12 @@ type Props = { const { stringifiedAst, config } = Astro.props as Props; const ast = Markdoc.Ast.fromJSON(stringifiedAst); -const content = await Markdoc.transform(ast, config); +// The AST may be an array, and `transform` has overloads for arrays and non-array cases, +// However TypeScript seems to struggle to combine both overloads into a single signature. +// Also, `transform` returns a promise here but the types don't reflect that. +// @ts-expect-error +const content = (await Markdoc.transform(ast, config)) as RenderableTreeNodes; +const treeNode = await createTreeNode(content); --- -{ - Array.isArray(content) ? ( - content.map(async (c) => ) - ) : ( - - ) -} + diff --git a/packages/integrations/markdoc/components/TreeNode.ts b/packages/integrations/markdoc/components/TreeNode.ts index cbcf153292d7..1fb45bafcc9d 100644 --- a/packages/integrations/markdoc/components/TreeNode.ts +++ b/packages/integrations/markdoc/components/TreeNode.ts @@ -1,6 +1,6 @@ -import type { RenderableTreeNode } from '@markdoc/markdoc'; +import type { RenderableTreeNodes } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; -import type { AstroInstance } from 'astro'; +import type { AstroInstance, SSRResult } from 'astro'; import type { HTMLString } from 'astro/runtime/server/index.js'; import { createComponent, @@ -15,6 +15,9 @@ import { } from 'astro/runtime/server/index.js'; export type TreeNode = + // Markdoc `if` tag often returns an array of nodes in the AST, which gets translated + // here as an array of `TreeNode`s, which we'll render sequentially without a wrapper. + | TreeNode[] | { type: 'text'; content: string | HTMLString; @@ -35,75 +38,86 @@ export type TreeNode = children: TreeNode[]; }; -export const ComponentNode = createComponent({ - factory(result: any, { treeNode }: { treeNode: TreeNode }) { - if (treeNode.type === 'text') return render`${treeNode.content}`; - - const slots = { - default: () => - render`${treeNode.children.map((child) => - renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child }), - )}`, - }; - if (treeNode.type === 'component') { - let styles = '', - links = '', - scripts = ''; - if (Array.isArray(treeNode.collectedStyles)) { - styles = treeNode.collectedStyles - .map((style: any) => - renderUniqueStylesheet(result, { - type: 'inline', - content: style, - }), - ) - .join(''); - } - if (Array.isArray(treeNode.collectedLinks)) { - links = treeNode.collectedLinks - .map((link: any) => { - return renderUniqueStylesheet(result, { - type: 'external', - src: link[0] === '/' ? link : '/' + link, - }); - }) - .join(''); - } - if (Array.isArray(treeNode.collectedScripts)) { - scripts = treeNode.collectedScripts - .map((script: any) => renderScriptElement(script)) - .join(''); - } - - const head = unescapeHTML(styles + links + scripts); - - let headAndContent = createHeadAndContent( - head, - renderTemplate`${renderComponent( - result, - treeNode.component.name, - treeNode.component, - treeNode.props, - slots, - )}`, - ); - - // Let the runtime know that this component is being used. - result._metadata.propagators.add({ - init() { - return headAndContent; - }, - }); - - return headAndContent; +function renderTreeNodeToFactoryResult(result: SSRResult, treeNode: TreeNode) { + if (Array.isArray(treeNode)) { + return Promise.all(treeNode.map((node) => renderTreeNodeToFactoryResult(result, node))); + } + + if (treeNode.type === 'text') return render`${treeNode.content}`; + + const slots = { + default: () => + render`${treeNode.children.map((child) => + renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child }), + )}`, + }; + if (treeNode.type === 'component') { + let styles = '', + links = '', + scripts = ''; + if (Array.isArray(treeNode.collectedStyles)) { + styles = treeNode.collectedStyles + .map((style: any) => + renderUniqueStylesheet(result, { + type: 'inline', + content: style, + }), + ) + .join(''); + } + if (Array.isArray(treeNode.collectedLinks)) { + links = treeNode.collectedLinks + .map((link: any) => { + return renderUniqueStylesheet(result, { + type: 'external', + src: link[0] === '/' ? link : '/' + link, + }); + }) + .join(''); } - return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots); + if (Array.isArray(treeNode.collectedScripts)) { + scripts = treeNode.collectedScripts + .map((script: any) => renderScriptElement(script)) + .join(''); + } + + const head = unescapeHTML(styles + links + scripts); + + let headAndContent = createHeadAndContent( + head, + renderTemplate`${renderComponent( + result, + treeNode.component.name, + treeNode.component, + treeNode.props, + slots, + )}`, + ); + + // Let the runtime know that this component is being used. + // @ts-expect-error Astro only uses `init()` so specify it only (plus `_metadata` is internal) + result._metadata.propagators.add({ + init() { + return headAndContent; + }, + }); + + return headAndContent; + } + return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots); +} + +export const ComponentNode = createComponent({ + factory(result: SSRResult, { treeNode }: { treeNode: TreeNode | TreeNode[] }) { + return renderTreeNodeToFactoryResult(result, treeNode); }, propagation: 'self', }); -export async function createTreeNode(node: RenderableTreeNode): Promise { - if (isHTMLString(node)) { +export async function createTreeNode(node: RenderableTreeNodes): Promise { + if (Array.isArray(node)) { + return Promise.all(node.map((child) => createTreeNode(child))); + } else if (isHTMLString(node)) { return { type: 'text', content: node as HTMLString }; } else if (typeof node === 'string' || typeof node === 'number') { return { type: 'text', content: String(node) }; diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc index eb7d20426e59..0d1ec835c80a 100644 --- a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc @@ -17,3 +17,12 @@ And a code component for code blocks: ```js const isRenderedWithShiki = true; ``` + +{% if equals("true", "true") %} +Inside truthy + +```js +const isRenderedWithShikiInside = true; +``` + +{% /if %} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc index 61f404a9701a..8ff944b060d0 100644 --- a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc @@ -15,3 +15,12 @@ And a code component for code blocks: ```js const isRenderedWithShiki = true; ``` + +{% if equals("true", "true") %} +Inside truthy + +```js +const isRenderedWithShikiInside = true; +``` + +{% /if %} diff --git a/packages/integrations/markdoc/test/render-components.test.js b/packages/integrations/markdoc/test/render-components.test.js index b9ac8770f732..e8ddec90976a 100644 --- a/packages/integrations/markdoc/test/render-components.test.js +++ b/packages/integrations/markdoc/test/render-components.test.js @@ -74,6 +74,11 @@ function renderComponentsChecks(html) { const pre = document.querySelector('pre'); assert.notEqual(pre, null); assert.equal(pre.className, 'astro-code github-dark'); + + // Renders 2nd Astro Code component inside if tag + const pre2 = document.querySelectorAll('pre')[1]; + assert.notEqual(pre2, null); + assert.equal(pre2.className, 'astro-code github-dark'); } /** @param {string} html */ diff --git a/packages/integrations/markdoc/test/render-extends-components.test.js b/packages/integrations/markdoc/test/render-extends-components.test.js index 8d1778d3eeed..f5f1454c8e1b 100644 --- a/packages/integrations/markdoc/test/render-extends-components.test.js +++ b/packages/integrations/markdoc/test/render-extends-components.test.js @@ -61,4 +61,9 @@ function renderComponentsChecks(html) { const pre = document.querySelector('pre'); assert.notEqual(pre, null); assert.equal(pre.className, 'astro-code github-dark'); + + // Renders 2nd Astro Code component inside if tag + const pre2 = document.querySelectorAll('pre')[1]; + assert.notEqual(pre2, null); + assert.equal(pre2.className, 'astro-code github-dark'); } diff --git a/packages/integrations/markdoc/test/syntax-highlighting.test.js b/packages/integrations/markdoc/test/syntax-highlighting.test.js index f47b891f43bf..6ea841ae1249 100644 --- a/packages/integrations/markdoc/test/syntax-highlighting.test.js +++ b/packages/integrations/markdoc/test/syntax-highlighting.test.js @@ -68,6 +68,27 @@ describe('Markdoc - syntax highlighting', () => { assert.equal(pre.getAttribute('style').includes('word-wrap: break-word'), true); } }); + it('transform within if tags', async () => { + const ast = Markdoc.parse(` +{% if equals("true", "true") %} +Inside truthy + +\`\`\`js +const hello = "yes"; +\`\`\` + +{% /if %}`); + const content = await Markdoc.transform(ast, await getConfigExtendingShiki()); + assert.equal(content.children.length, 1); + assert.equal(content.children[0].length, 2); + const pTag = content.children[0][0]; + assert.equal(pTag.name, 'p'); + const codeBlock = content.children[0][1]; + assert.equal(isHTMLString(codeBlock), true); + const pre = parsePreTag(codeBlock); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('github-dark'), true); + }); }); describe('prism', () => {