diff --git a/.changeset/fix-markdoc-table-attributes.md b/.changeset/fix-markdoc-table-attributes.md new file mode 100644 index 000000000000..3bd4b2644d60 --- /dev/null +++ b/.changeset/fix-markdoc-table-attributes.md @@ -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. diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index ed241d96706d..3a7e9c63abc6 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -38,6 +38,7 @@ export async function setupConfig( merged = mergeConfig(merged, HTML_CONFIG); } + syncTagNodeAttributes(merged); return merged; } @@ -54,6 +55,7 @@ export function setupConfigSync( merged = mergeConfig(merged, HTML_CONFIG); } + syncTagNodeAttributes(merged); return merged; } @@ -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 = Markdoc.tags; + const builtinNodes: Record = 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` diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-table-attrs/astro.config.mjs new file mode 100644 index 000000000000..bd024e9ab87f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/astro.config.mjs @@ -0,0 +1,6 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-table-attrs/markdoc.config.ts new file mode 100644 index 000000000000..0a6941394d9b --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/markdoc.config.ts @@ -0,0 +1,14 @@ +import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + nodes: { + table: { + attributes: { + background: { + type: String, + matches: ['default', 'transparent'], + }, + }, + }, + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/package.json b/packages/integrations/markdoc/test/fixtures/render-table-attrs/package.json new file mode 100644 index 000000000000..5a429fdd2203 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-table-attrs", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content.config.ts b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content.config.ts new file mode 100644 index 000000000000..663df266cccb --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content.config.ts @@ -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, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content/blog/with-table-attrs.mdoc b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content/blog/with-table-attrs.mdoc new file mode 100644 index 000000000000..a500bb112c62 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content/blog/with-table-attrs.mdoc @@ -0,0 +1,13 @@ +--- +title: Post with table attributes +--- + +## Table with attributes + +{% table background="default" %} +* Feature +* Supported +--- +* Custom attributes +* Yes +{% /table %} diff --git a/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/pages/index.astro new file mode 100644 index 000000000000..f75cae4d52e3 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-table-attrs/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import { getEntry, render } from "astro:content"; + +const post = await getEntry('blog', 'with-table-attrs'); +const { Content } = await render(post); +--- + + + + + + Content + + + + + diff --git a/packages/integrations/markdoc/test/render-table-attrs.test.js b/packages/integrations/markdoc/test/render-table-attrs.test.js new file mode 100644 index 000000000000..b1dd7e3f83ed --- /dev/null +++ b/packages/integrations/markdoc/test/render-table-attrs.test.js @@ -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(); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d66757335799..b6857084e73f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5384,6 +5384,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/render-table-attrs: + dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/render-typographer: dependencies: '@astrojs/markdoc':