From 6e8da44efbbe5094f540c6e70a20908d65492b09 Mon Sep 17 00:00:00 2001 From: Ahmad Yasser Date: Sat, 14 Feb 2026 13:00:23 -0500 Subject: [PATCH] fix(markdoc): resolve custom attributes on built-in table tag (#15457) * fix(markdoc): sync custom attributes between tags and nodes with shared names In Markdoc, `table` exists as both a tag (`{% table %}`) and a node (the inner table structure). When users configure custom attributes on `nodes.table` or `tags.table`, the AST propagates those attributes to both the tag and node, but validation only checks the schema for each type independently. This caused "Invalid attribute" errors when attributes were declared on only one side. Add `syncTagNodeAttributes()` to automatically merge attribute declarations between tags and nodes that share the same name after config setup, so users can define attributes on either side. Fixes #14220 * chore: clarify why explicit types are needed on builtinTags/builtinNodes --- .changeset/fix-markdoc-table-attributes.md | 9 ++++ packages/integrations/markdoc/src/runtime.ts | 42 ++++++++++++++++ .../render-table-attrs/astro.config.mjs | 6 +++ .../render-table-attrs/markdoc.config.ts | 14 ++++++ .../fixtures/render-table-attrs/package.json | 9 ++++ .../render-table-attrs/src/content.config.ts | 10 ++++ .../src/content/blog/with-table-attrs.mdoc | 13 +++++ .../render-table-attrs/src/pages/index.astro | 17 +++++++ .../markdoc/test/render-table-attrs.test.js | 49 +++++++++++++++++++ pnpm-lock.yaml | 9 ++++ 10 files changed, 178 insertions(+) create mode 100644 .changeset/fix-markdoc-table-attributes.md create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/astro.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/markdoc.config.ts create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/package.json create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content.config.ts create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/src/content/blog/with-table-attrs.mdoc create mode 100644 packages/integrations/markdoc/test/fixtures/render-table-attrs/src/pages/index.astro create mode 100644 packages/integrations/markdoc/test/render-table-attrs.test.js 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':