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

Fix markdoc render code block in if tag #12930

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all 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/twelve-cobras-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/markdoc': patch
---

Fixes rendering code blocks within if tags
17 changes: 8 additions & 9 deletions packages/integrations/markdoc/components/Renderer.astro
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) => <ComponentNode treeNode={await createTreeNode(c)} />)
) : (
<ComponentNode treeNode={await createTreeNode(content)} />
)
}
<ComponentNode treeNode={treeNode} />
146 changes: 80 additions & 66 deletions packages/integrations/markdoc/components/TreeNode.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 all without a wrapper.
| TreeNode[]
| {
type: 'text';
content: string | HTMLString;
Expand All @@ -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 = {
Comment on lines -38 to -41
Copy link
Member Author

Choose a reason for hiding this comment

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

It might look like I'm doing a lot of change here, but I'm only extracting the factory function here out so I can call itself for arrays. Disabling whitespace in diff should make it easier to review.

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<TreeNode> {
if (isHTMLString(node)) {
export async function createTreeNode(node: RenderableTreeNodes): Promise<TreeNode> {
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) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
5 changes: 5 additions & 0 deletions packages/integrations/markdoc/test/render-components.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
21 changes: 21 additions & 0 deletions packages/integrations/markdoc/test/syntax-highlighting.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading