mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
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
This commit is contained in:
parent
8ca8943ce2
commit
90cfade88c
27 changed files with 521 additions and 118 deletions
19
.changeset/moody-spies-learn.md
Normal file
19
.changeset/moody-spies-learn.md
Normal file
|
@ -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.`
|
||||
|
||||

|
||||
```
|
|
@ -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:*",
|
||||
|
|
|
@ -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);
|
||||
|
|
5
packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc
vendored
Normal file
5
packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
## HTML in a partial
|
||||
|
||||
<ul>
|
||||
<li id="partial">List item</li>
|
||||
</ul>
|
5
packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc
vendored
Normal file
5
packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: With Partial
|
||||
---
|
||||
|
||||
{% partial file="./_partial.mdoc" /%}
|
7
packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import markdoc from '@astrojs/markdoc';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [markdoc()],
|
||||
});
|
7
packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Markdoc, defineMarkdocConfig } from '@astrojs/markdoc/config';
|
||||
|
||||
export default defineMarkdocConfig({
|
||||
partials: {
|
||||
configured: Markdoc.parse('# Configured partial {% #configured %}'),
|
||||
},
|
||||
});
|
9
packages/integrations/markdoc/test/fixtures/render-partials/package.json
vendored
Normal file
9
packages/integrations/markdoc/test/fixtures/render-partials/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/markdoc-render-partials",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
3
packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc
vendored
Normal file
3
packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
## Partial {% #top %}
|
||||
|
||||
{% partial file="../nested/_partial.mdoc" /%}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Post with partials
|
||||
---
|
||||
|
||||
{% partial file="_partial.mdoc" /%}
|
||||
|
||||
{% partial file="configured" /%}
|
1
packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc
vendored
Normal file
1
packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
## Nested partial {% #nested %}
|
19
packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro
vendored
Normal file
19
packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro
vendored
Normal file
|
@ -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>
|
|
@ -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()],
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/markdoc": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
"@astrojs/preact": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"preact": "^10.20.1"
|
||||
}
|
||||
}
|
||||
|
|
10
packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx
vendored
Normal file
10
packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx
vendored
Normal file
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import Counter from './Counter';
|
||||
---
|
||||
|
||||
<Counter client:load />
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
<p id="deeply-nested">Deeply nested partial</p>
|
3
packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc
vendored
Normal file
3
packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
Render components from a deeply nested partial:
|
||||
|
||||
{% deeply-nested /%}
|
|
@ -0,0 +1,7 @@
|
|||
# Hello from a partial!
|
||||
|
||||
Render a component from a partial:
|
||||
|
||||
{% counter /%}
|
||||
|
||||
{% partial file="../_nested.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
|
||||
|
|
7
packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
}
|
89
packages/integrations/markdoc/test/render-components.test.js
Normal file
89
packages/integrations/markdoc/test/render-components.test.js
Normal file
|
@ -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');
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -186,6 +199,16 @@ function renderRandomlyCasedHTMLAttributesChecks(html) {
|
|||
assert.equal(td4.getAttribute('rowspan'), '2');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
|
|
|
@ -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');
|
||||
}
|
|
@ -23,6 +23,18 @@ describe('Markdoc - render', () => {
|
|||
await server.stop();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
renderPartialsChecks(html);
|
||||
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('renders content - with config', async () => {
|
||||
const fixture = await getFixture('render-with-config');
|
||||
const server = await fixture.startDevServer();
|
||||
|
@ -35,30 +47,6 @@ describe('Markdoc - render', () => {
|
|||
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');
|
||||
const server = await fixture.startDevServer();
|
||||
|
||||
const res = await fixture.fetch('/');
|
||||
const html = await res.text();
|
||||
|
||||
renderIndentedComponentsChecks(html);
|
||||
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('renders content - with `render: null` in document', async () => {
|
||||
const fixture = await getFixture('render-null');
|
||||
const server = await fixture.startDevServer();
|
||||
|
@ -94,6 +82,15 @@ describe('Markdoc - render', () => {
|
|||
renderSimpleChecks(html);
|
||||
});
|
||||
|
||||
it('renders content - with partials', async () => {
|
||||
const fixture = await getFixture('render-partials');
|
||||
await fixture.build();
|
||||
|
||||
const html = await fixture.readFile('/index.html');
|
||||
|
||||
renderPartialsChecks(html);
|
||||
});
|
||||
|
||||
it('renders content - with config', async () => {
|
||||
const fixture = await getFixture('render-with-config');
|
||||
await fixture.build();
|
||||
|
@ -103,24 +100,6 @@ describe('Markdoc - render', () => {
|
|||
renderConfigChecks(html);
|
||||
});
|
||||
|
||||
it('renders content - with components', async () => {
|
||||
const fixture = await getFixture('render-with-components');
|
||||
await fixture.build();
|
||||
|
||||
const html = await fixture.readFile('/index.html');
|
||||
|
||||
renderComponentsChecks(html);
|
||||
});
|
||||
|
||||
it('renders content - with indented components', async () => {
|
||||
const fixture = await getFixture('render-with-indented-components');
|
||||
await fixture.build();
|
||||
|
||||
const html = await fixture.readFile('/index.html');
|
||||
|
||||
renderIndentedComponentsChecks(html);
|
||||
});
|
||||
|
||||
it('renders content - with `render: null` in document', async () => {
|
||||
const fixture = await getFixture('render-null');
|
||||
await fixture.build();
|
||||
|
@ -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 */
|
||||
|
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue