diff options
author | 2024-04-03 16:48:53 -0400 | |
---|---|---|
committer | 2024-04-03 16:48:53 -0400 | |
commit | 90cfade88c2b9a34d8a5fe711ce329732d690409 (patch) | |
tree | b76d0de544c8aebf0a8051aed6cb615af964295f | |
parent | 8ca8943ce2c10f06c90398f10c583002cd9a6bee (diff) | |
download | astro-90cfade88c2b9a34d8a5fe711ce329732d690409.tar.gz astro-90cfade88c2b9a34d8a5fe711ce329732d690409.tar.zst astro-90cfade88c2b9a34d8a5fe711ce329732d690409.zip |
feat: automatic Markdoc partial resolution (#10649)
* wip: react counter example
* feat: resolve markdoc partials by file path
* test: components within partials
* test: html within partial
* chore: changeset
* fix: respect user configured partials
* test: basic partials
* chore: lock
* chore: fix lock
* chore: minor -> patch
* fix: use --parallel for dev server timeout error
* refactor: move component tests to separate file
* fix: build indent fixture
* fix: check before addWatchFile
* refactor: rootRelative -> relativePartial
* deps: use workspace react integration
* refactor: split test files by fixture
* refactor: switch to preact to avoid react prod build error
* feat: use vite pluginContext
* fix: handle missing ./
* chore: bump timeout
27 files changed, 512 insertions, 109 deletions
diff --git a/.changeset/moody-spies-learn.md b/.changeset/moody-spies-learn.md new file mode 100644 index 000000000..dbd0be539 --- /dev/null +++ b/.changeset/moody-spies-learn.md @@ -0,0 +1,19 @@ +--- +"@astrojs/markdoc": patch +--- + +Add automatic resolution for Markdoc partials. This allows you to render other Markdoc files inside of a given entry. Reference files using the `partial` tag with a `file` attribute for the relative file path: + +```md +<!--src/content/blog/post.mdoc--> + +{% partial file="my-partials/_diagram.mdoc" /%} + +<!--src/content/blog/my-partials/_diagram.mdoc--> + +## Diagram + +This partial will render inside of `post.mdoc.` + + +``` diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index f1ba10559..ba6c7b360 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -59,7 +59,7 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test --timeout 40000 \"test/**/*.test.js\"" + "test": "astro-scripts test --timeout 60000 \"test/**/*.test.js\"" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/markdoc/src/content-entry-type.ts b/packages/integrations/markdoc/src/content-entry-type.ts index 89f9f9e86..5168c49c1 100644 --- a/packages/integrations/markdoc/src/content-entry-type.ts +++ b/packages/integrations/markdoc/src/content-entry-type.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Config as MarkdocConfig, Node } from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; import type { AstroConfig, ContentEntryType } from 'astro'; @@ -38,9 +38,41 @@ export async function getContentEntryType({ } const ast = Markdoc.parse(tokens); - const usedTags = getUsedTags(ast); const userMarkdocConfig = markdocConfigResult?.config ?? {}; const markdocConfigUrl = markdocConfigResult?.fileUrl; + const pluginContext = this; + const markdocConfig = await setupConfig(userMarkdocConfig, options); + const filePath = fileURLToPath(fileUrl); + raiseValidationErrors({ + ast, + /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */ + markdocConfig: markdocConfig as MarkdocConfig, + entry, + viteId, + astroConfig, + filePath, + }); + await resolvePartials({ + ast, + markdocConfig: markdocConfig as MarkdocConfig, + fileUrl, + allowHTML: options?.allowHTML, + tokenizer, + pluginContext, + root: astroConfig.root, + raisePartialValidationErrors: (partialAst, partialPath) => { + raiseValidationErrors({ + ast: partialAst, + markdocConfig: markdocConfig as MarkdocConfig, + entry, + viteId, + astroConfig, + filePath: partialPath, + }); + }, + }); + + const usedTags = getUsedTags(ast); let componentConfigByTagMap: Record<string, ComponentConfig> = {}; // Only include component imports for tags used in the document. @@ -59,42 +91,6 @@ export async function getContentEntryType({ } } - const pluginContext = this; - const markdocConfig = await setupConfig(userMarkdocConfig, options); - - const filePath = fileURLToPath(fileUrl); - - const validationErrors = Markdoc.validate( - ast, - /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */ - markdocConfig as MarkdocConfig - ).filter((e) => { - return ( - // Ignore `variable-undefined` errors. - // Variables can be configured at runtime, - // so we cannot validate them at build time. - e.error.id !== 'variable-undefined' && - (e.error.level === 'error' || e.error.level === 'critical') - ); - }); - if (validationErrors.length) { - // Heuristic: take number of newlines for `rawData` and add 2 for the `---` fences - const frontmatterBlockOffset = entry.rawData.split('\n').length + 2; - const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath); - throw new MarkdocError({ - message: [ - `**${String(rootRelativePath)}** contains invalid content:`, - ...validationErrors.map((e) => `- ${e.error.message}`), - ].join('\n'), - location: { - // Error overlay does not support multi-line or ranges. - // Just point to the first line. - line: frontmatterBlockOffset + validationErrors[0].lines[0], - file: viteId, - }, - }); - } - await emitOptimizedImages(ast.children, { astroConfig, pluginContext, @@ -142,6 +138,136 @@ export const Content = createContentComponent( }; } +/** + * Recursively resolve partial tags to their content. + * Note: Mutates the `ast` object directly. + */ +async function resolvePartials({ + ast, + fileUrl, + root, + tokenizer, + allowHTML, + markdocConfig, + pluginContext, + raisePartialValidationErrors, +}: { + ast: Node; + fileUrl: URL; + root: URL; + tokenizer: any; + allowHTML?: boolean; + markdocConfig: MarkdocConfig; + pluginContext: Rollup.PluginContext; + raisePartialValidationErrors: (ast: Node, filePath: string) => void; +}) { + const relativePartialPath = path.relative(fileURLToPath(root), fileURLToPath(fileUrl)); + for (const node of ast.walk()) { + if (node.type === 'tag' && node.tag === 'partial') { + const { file } = node.attributes; + if (!file) { + throw new MarkdocError({ + // Should be caught by Markdoc validation step. + message: `(Uncaught error) Partial tag requires a 'file' attribute`, + }); + } + + if (markdocConfig.partials?.[file]) continue; + + let partialPath: string; + let partialContents: string; + try { + const resolved = await pluginContext.resolve(file, fileURLToPath(fileUrl)); + let partialId = resolved?.id; + if (!partialId) { + const attemptResolveAsRelative = await pluginContext.resolve( + './' + file, + fileURLToPath(fileUrl) + ); + if (!attemptResolveAsRelative?.id) throw new Error(); + partialId = attemptResolveAsRelative.id; + } + + partialPath = fileURLToPath(new URL(prependForwardSlash(partialId), 'file://')); + partialContents = await fs.promises.readFile(partialPath, 'utf-8'); + } catch { + throw new MarkdocError({ + message: [ + `**${String(relativePartialPath)}** contains invalid content:`, + `Could not read partial file \`${file}\`. Does the file exist?`, + ].join('\n'), + }); + } + if (pluginContext.meta.watchMode) pluginContext.addWatchFile(partialPath); + let partialTokens = tokenizer.tokenize(partialContents); + if (allowHTML) { + partialTokens = htmlTokenTransform(tokenizer, partialTokens); + } + const partialAst = Markdoc.parse(partialTokens); + raisePartialValidationErrors(partialAst, partialPath); + await resolvePartials({ + ast: partialAst, + root, + fileUrl: pathToFileURL(partialPath), + tokenizer, + allowHTML, + markdocConfig, + pluginContext, + raisePartialValidationErrors, + }); + + Object.assign(node, partialAst); + } + } +} + +function raiseValidationErrors({ + ast, + markdocConfig, + entry, + viteId, + astroConfig, + filePath, +}: { + ast: Node; + markdocConfig: MarkdocConfig; + entry: ReturnType<typeof getEntryInfo>; + viteId: string; + astroConfig: AstroConfig; + filePath: string; +}) { + const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => { + return ( + (e.error.level === 'error' || e.error.level === 'critical') && + // Ignore `variable-undefined` errors. + // Variables can be configured at runtime, + // so we cannot validate them at build time. + e.error.id !== 'variable-undefined' && + // Ignore missing partial errors. + // We will resolve these in `resolvePartials`. + !(e.error.id === 'attribute-value-invalid' && e.error.message.match(/^Partial .+ not found/)) + ); + }); + + if (validationErrors.length) { + // Heuristic: take number of newlines for `rawData` and add 2 for the `---` fences + const frontmatterBlockOffset = entry.rawData.split('\n').length + 2; + const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath); + throw new MarkdocError({ + message: [ + `**${String(rootRelativePath)}** contains invalid content:`, + ...validationErrors.map((e) => `- ${e.error.message}`), + ].join('\n'), + location: { + // Error overlay does not support multi-line or ranges. + // Just point to the first line. + line: frontmatterBlockOffset + validationErrors[0].lines[0], + file: viteId, + }, + }); + } +} + function getUsedTags(markdocAst: Node) { const tags = new Set<string>(); const validationErrors = Markdoc.validate(markdocAst); diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc new file mode 100644 index 000000000..f8774e911 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc @@ -0,0 +1,5 @@ +## HTML in a partial + +<ul> + <li id="partial">List item</li> +</ul> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc new file mode 100644 index 000000000..c42d3cd70 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc @@ -0,0 +1,5 @@ +--- +title: With Partial +--- + +{% partial file="./_partial.mdoc" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts new file mode 100644 index 000000000..c9762aed5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts @@ -0,0 +1,7 @@ +import { Markdoc, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + partials: { + configured: Markdoc.parse('# Configured partial {% #configured %}'), + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/package.json b/packages/integrations/markdoc/test/fixtures/render-partials/package.json new file mode 100644 index 000000000..021e1c2d9 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-partials", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc new file mode 100644 index 000000000..4ace9a9d3 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc @@ -0,0 +1,3 @@ +## Partial {% #top %} + +{% partial file="../nested/_partial.mdoc" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc new file mode 100644 index 000000000..2d9a87110 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc @@ -0,0 +1,7 @@ +--- +title: Post with partials +--- + +{% partial file="_partial.mdoc" /%} + +{% partial file="configured" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc new file mode 100644 index 000000000..4193609bf --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc @@ -0,0 +1 @@ +## Nested partial {% #nested %} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro new file mode 100644 index 000000000..e9549f314 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from 'astro:content'; + +const post = await getEntryBySlug('blog', 'with-partials'); +const { Content } = await post.render(); +--- + +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Content</title> + </head> + <body> + <Content /> + </body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs index 1bd8ba93f..a5d98b012 100644 --- a/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs @@ -1,7 +1,8 @@ import markdoc from '@astrojs/markdoc'; import { defineConfig } from 'astro/config'; +import preact from '@astrojs/preact'; // https://astro.build/config export default defineConfig({ - integrations: [markdoc()], + integrations: [markdoc(), preact()], }); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts index 2016327a8..6093ec593 100644 --- a/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts @@ -1,4 +1,4 @@ -import { component, defineMarkdocConfig } from '@astrojs/markdoc/config'; +import { Markdoc, component, defineMarkdocConfig } from '@astrojs/markdoc/config'; export default defineMarkdocConfig({ nodes: { @@ -22,5 +22,11 @@ export default defineMarkdocConfig({ }, }, }, + counter: { + render: component('./src/components/CounterWrapper.astro'), + }, + 'deeply-nested': { + render: component('./src/components/DeeplyNested.astro'), + }, }, -}) +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/package.json b/packages/integrations/markdoc/test/fixtures/render-with-components/package.json index b81033473..f70e2b6c1 100644 --- a/packages/integrations/markdoc/test/fixtures/render-with-components/package.json +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/package.json @@ -4,6 +4,8 @@ "private": true, "dependencies": { "@astrojs/markdoc": "workspace:*", - "astro": "workspace:*" + "@astrojs/preact": "workspace:*", + "astro": "workspace:*", + "preact": "^10.20.1" } } diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx new file mode 100644 index 000000000..f1e239718 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx @@ -0,0 +1,10 @@ +import { useState } from 'preact/hooks'; + +export default function Counter() { + const [count, setCount] = useState(1); + return ( + <button id="counter" onClick={() => setCount(count + 1)}> + {count} + </button> + ); +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro new file mode 100644 index 000000000..e45ac6438 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro @@ -0,0 +1,5 @@ +--- +import Counter from './Counter'; +--- + +<Counter client:load /> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro new file mode 100644 index 000000000..eb23f675a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro @@ -0,0 +1,5 @@ +--- + +--- + +<p id="deeply-nested">Deeply nested partial</p> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc new file mode 100644 index 000000000..68f529280 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc @@ -0,0 +1,3 @@ +Render components from a deeply nested partial: + +{% deeply-nested /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc new file mode 100644 index 000000000..4a015695c --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc @@ -0,0 +1,7 @@ +# Hello from a partial! + +Render a component from a partial: + +{% counter /%} + +{% partial file="../_nested.mdoc" /%} 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 61f404a97..eb7d20426 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 @@ -10,6 +10,8 @@ This uses a custom marquee component with a shortcode: I'm a marquee too! {% /marquee-element %} +{% partial file="_counter.mdoc" /%} + And a code component for code blocks: ```js diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json b/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json new file mode 100644 index 000000000..99df2e61a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } +}
\ No newline at end of file diff --git a/packages/integrations/markdoc/test/render-components.test.js b/packages/integrations/markdoc/test/render-components.test.js new file mode 100644 index 000000000..5639770ab --- /dev/null +++ b/packages/integrations/markdoc/test/render-components.test.js @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import { describe, it, before, after } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/render-with-components/', import.meta.url); + +describe('Markdoc - render components', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - with components', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderComponentsChecks(html); + }); + + it('renders content - with components inside partials', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderComponentsInsidePartialsChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - with components', async () => { + const html = await fixture.readFile('/index.html'); + + renderComponentsChecks(html); + }); + + it('renders content - with components inside partials', async () => { + const html = await fixture.readFile('/index.html'); + + renderComponentsInsidePartialsChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderComponentsChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with components'); + + // Renders custom shortcode component + const marquee = document.querySelector('marquee'); + assert.notEqual(marquee, null); + assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + + // Renders Astro Code component + const pre = document.querySelector('pre'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); +} + +/** @param {string} html */ +function renderComponentsInsidePartialsChecks(html) { + const { document } = parseHTML(html); + // renders Counter.tsx + const button = document.querySelector('#counter'); + assert.equal(button.textContent, '1'); + + // renders DeeplyNested.astro + const deeplyNested = document.querySelector('#deeply-nested'); + assert.equal(deeplyNested.textContent, 'Deeply nested partial'); +} diff --git a/packages/integrations/markdoc/test/render-html.test.js b/packages/integrations/markdoc/test/render-html.test.js index 785599ae5..4780444bf 100644 --- a/packages/integrations/markdoc/test/render-html.test.js +++ b/packages/integrations/markdoc/test/render-html.test.js @@ -54,6 +54,13 @@ describe('Markdoc - render html', () => { renderRandomlyCasedHTMLAttributesChecks(html); }); + + it('renders content - html within partials', async () => { + const res = await fixture.fetch('/with-partial'); + const html = await res.text(); + + renderHTMLWithinPartialChecks(html); + }); }); describe('build', () => { @@ -84,6 +91,12 @@ describe('Markdoc - render html', () => { renderRandomlyCasedHTMLAttributesChecks(html); }); + + it('renders content - html within partials', async () => { + const html = await fixture.readFile('/with-partial/index.html'); + + renderHTMLWithinPartialChecks(html); + }); }); }); @@ -187,6 +200,16 @@ function renderRandomlyCasedHTMLAttributesChecks(html) { } /** + * @param {string} html + */ +function renderHTMLWithinPartialChecks(html) { + const { document } = parseHTML(html); + + const li = document.querySelector('ul > li#partial'); + assert.equal(li.textContent, 'List item'); +} + +/** * Asserts that the rendered HTML tags with interleaved Markdoc tags (both block and inline) rendered in the expected nested graph of elemements * * @param {string} html */ diff --git a/packages/integrations/markdoc/test/render-indented-components.test.js b/packages/integrations/markdoc/test/render-indented-components.test.js new file mode 100644 index 000000000..60b2a4ab5 --- /dev/null +++ b/packages/integrations/markdoc/test/render-indented-components.test.js @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { describe, it, before, after } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/render-with-indented-components/', import.meta.url); + +describe('Markdoc - render indented components', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - with indented components', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderIndentedComponentsChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - with indented components', async () => { + const html = await fixture.readFile('/index.html'); + + renderIndentedComponentsChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderIndentedComponentsChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with indented components'); + + // Renders custom shortcode components + const marquees = document.querySelectorAll('marquee'); + assert.equal(marquees.length, 2); + + // Renders h3 + const h3 = document.querySelector('h3'); + assert.equal(h3.textContent, 'I am an h3!'); + + // Renders Astro Code component + const pre = document.querySelector('pre'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); +} diff --git a/packages/integrations/markdoc/test/render.test.js b/packages/integrations/markdoc/test/render.test.js index 0960a980c..d439adcd2 100644 --- a/packages/integrations/markdoc/test/render.test.js +++ b/packages/integrations/markdoc/test/render.test.js @@ -23,38 +23,26 @@ describe('Markdoc - render', () => { await server.stop(); }); - it('renders content - with config', async () => { - const fixture = await getFixture('render-with-config'); + it('renders content - with partials', async () => { + const fixture = await getFixture('render-partials'); const server = await fixture.startDevServer(); const res = await fixture.fetch('/'); const html = await res.text(); - renderConfigChecks(html); + renderPartialsChecks(html); await server.stop(); }); - it('renders content - with components', async () => { - const fixture = await getFixture('render-with-components'); - const server = await fixture.startDevServer(); - - const res = await fixture.fetch('/'); - const html = await res.text(); - - renderComponentsChecks(html); - - await server.stop(); - }); - - it('renders content - with indented components', async () => { - const fixture = await getFixture('render-with-indented-components'); + it('renders content - with config', async () => { + const fixture = await getFixture('render-with-config'); const server = await fixture.startDevServer(); const res = await fixture.fetch('/'); const html = await res.text(); - renderIndentedComponentsChecks(html); + renderConfigChecks(html); await server.stop(); }); @@ -94,31 +82,22 @@ describe('Markdoc - render', () => { renderSimpleChecks(html); }); - it('renders content - with config', async () => { - const fixture = await getFixture('render-with-config'); - await fixture.build(); - - const html = await fixture.readFile('/index.html'); - - renderConfigChecks(html); - }); - - it('renders content - with components', async () => { - const fixture = await getFixture('render-with-components'); + it('renders content - with partials', async () => { + const fixture = await getFixture('render-partials'); await fixture.build(); const html = await fixture.readFile('/index.html'); - renderComponentsChecks(html); + renderPartialsChecks(html); }); - it('renders content - with indented components', async () => { - const fixture = await getFixture('render-with-indented-components'); + it('renders content - with config', async () => { + const fixture = await getFixture('render-with-config'); await fixture.build(); const html = await fixture.readFile('/index.html'); - renderIndentedComponentsChecks(html); + renderConfigChecks(html); }); it('renders content - with `render: null` in document', async () => { @@ -152,40 +131,14 @@ function renderNullChecks(html) { } /** @param {string} html */ -function renderComponentsChecks(html) { +function renderPartialsChecks(html) { const { document } = parseHTML(html); - const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with components'); - - // Renders custom shortcode component - const marquee = document.querySelector('marquee'); - assert.notEqual(marquee, null); - assert.equal(marquee.hasAttribute('data-custom-marquee'), true); - - // Renders Astro Code component - const pre = document.querySelector('pre'); - assert.notEqual(pre, null); - assert.equal(pre.className, 'astro-code github-dark'); -} - -/** @param {string} html */ -function renderIndentedComponentsChecks(html) { - const { document } = parseHTML(html); - const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with indented components'); - - // Renders custom shortcode components - const marquees = document.querySelectorAll('marquee'); - assert.equal(marquees.length, 2); - - // Renders h3 - const h3 = document.querySelector('h3'); - assert.equal(h3.textContent, 'I am an h3!'); - - // Renders Astro Code component - const pre = document.querySelector('pre'); - assert.notEqual(pre, null); - assert.equal(pre.className, 'astro-code github-dark'); + const top = document.querySelector('#top'); + assert.ok(top); + const nested = document.querySelector('#nested'); + assert.ok(nested); + const configured = document.querySelector('#configured'); + assert.ok(configured); } /** @param {string} html */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6987c6c12..212870d05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4267,6 +4267,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/render-partials: + dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/render-simple: dependencies: '@astrojs/markdoc': @@ -4281,9 +4290,15 @@ importers: '@astrojs/markdoc': specifier: workspace:* version: link:../../.. + '@astrojs/preact': + specifier: workspace:* + version: link:../../../../preact astro: specifier: workspace:* version: link:../../../../../astro + preact: + specifier: ^10.20.1 + version: 10.20.1 packages/integrations/markdoc/test/fixtures/render-with-config: dependencies: |