Skip to content
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
9 changes: 9 additions & 0 deletions .changeset/fix-markdoc-table-attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/markdoc': patch
---

Fixes custom attributes on Markdoc's built-in `{% table %}` tag causing "Invalid attribute" validation errors.

In Markdoc, `table` exists as both a tag (`{% table %}`) and a node (the inner table structure). When users defined custom attributes on either `nodes.table` or `tags.table`, the attributes weren't synced to the counterpart, causing validation to fail on whichever side was missing the declaration.

The fix automatically syncs custom attribute declarations between tags and nodes that share the same name, so users can define attributes on either side and have them work correctly.
42 changes: 42 additions & 0 deletions packages/integrations/markdoc/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export async function setupConfig(
merged = mergeConfig(merged, HTML_CONFIG);
}

syncTagNodeAttributes(merged);
return merged;
}

Expand All @@ -54,6 +55,7 @@ export function setupConfigSync(
merged = mergeConfig(merged, HTML_CONFIG);
}

syncTagNodeAttributes(merged);
return merged;
}

Expand Down Expand Up @@ -98,6 +100,46 @@ export function mergeConfig(
};
}

/**
* Sync custom attributes between tags and nodes that share the same name.
* In Markdoc, `table` exists as both a tag (`{% table %}`) and a node (the inner
* table structure). Attributes on the tag propagate to the child node in the AST,
* so both schemas must declare the same attributes for validation to pass.
* When users configure attributes on only one side, this copies them to the other.
*/
function syncTagNodeAttributes(config: MergedConfig): void {
// Markdoc's types don't have a string index signature, so we need the explicit
// type to index with a dynamic key in the loop below
const builtinTags: Record<string, any> = Markdoc.tags;
const builtinNodes: Record<string, any> = Markdoc.nodes;

for (const name of Object.keys(builtinTags)) {
if (!(name in builtinNodes)) continue;

const tagSchema = config.tags[name];
const nodeSchema = config.nodes[name as NodeType];
const tagAttrs = tagSchema?.attributes;
const nodeAttrs = nodeSchema?.attributes;

// Nothing to sync if neither side has custom attributes
if (!tagAttrs && !nodeAttrs) continue;

const mergedAttrs = { ...tagAttrs, ...nodeAttrs };

if (tagSchema) {
config.tags[name] = { ...tagSchema, attributes: mergedAttrs };
} else {
config.tags[name] = { ...builtinTags[name], attributes: mergedAttrs };
}

if (nodeSchema) {
config.nodes[name as NodeType] = { ...nodeSchema, attributes: mergedAttrs };
} else {
config.nodes[name as NodeType] = { ...builtinNodes[name], attributes: mergedAttrs };
}
}
}

/**
* Check if a transform function respects the `render` property.
* Astro's built-in transforms (like for headings) check `config.nodes?.X?.render`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import markdoc from '@astrojs/markdoc';
import { defineConfig } from 'astro/config';

export default defineConfig({
integrations: [markdoc()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineMarkdocConfig } from '@astrojs/markdoc/config';

export default defineMarkdocConfig({
nodes: {
table: {
attributes: {
background: {
type: String,
matches: ['default', 'transparent'],
},
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/markdoc-render-table-attrs",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/markdoc": "workspace:*",
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
loader: glob({ pattern: '**/*.mdoc', base: './src/content/blog' }),
});

export const collections = {
blog,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Post with table attributes
---

## Table with attributes

{% table background="default" %}
* Feature
* Supported
---
* Custom attributes
* Yes
{% /table %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { getEntry, render } from "astro:content";

const post = await getEntry('blog', 'with-table-attrs');
const { Content } = await render(post);
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Content</title>
</head>
<body>
<Content />
</body>
</html>
49 changes: 49 additions & 0 deletions packages/integrations/markdoc/test/render-table-attrs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { parseHTML } from 'linkedom';
import { loadFixture } from '../../../astro/test/test-utils.js';

async function getFixture() {
return await loadFixture({
root: new URL('./fixtures/render-table-attrs/', import.meta.url),
});
}

describe('Markdoc - table attributes', () => {
describe('build', () => {
it('renders table with custom attributes without validation errors', async () => {
const fixture = await getFixture();
await fixture.build();

const html = await fixture.readFile('/index.html');
const { document } = parseHTML(html);

const th = document.querySelector('th');
assert.ok(th, 'table header should exist');
assert.equal(th.textContent, 'Feature');

const td = document.querySelector('td');
assert.equal(td.textContent, 'Custom attributes');
});
});

describe('dev', () => {
it('renders table with custom attributes without validation errors', async () => {
const fixture = await getFixture();
const server = await fixture.startDevServer();

const res = await fixture.fetch('/');
const html = await res.text();
const { document } = parseHTML(html);

const th = document.querySelector('th');
assert.ok(th, 'table header should exist');
assert.equal(th.textContent, 'Feature');

const td = document.querySelector('td');
assert.equal(td.textContent, 'Custom attributes');

await server.stop();
});
});
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading