From 853d265e1effe0bef910ec82af91c783927ba209 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 11 May 2023 12:53:47 -0400 Subject: [PATCH 01/18] deps: markdown-remark --- packages/integrations/markdoc/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 1838f149acd4..805e9e2fbae0 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -44,6 +44,7 @@ "astro": "workspace:^2.4.3" }, "devDependencies": { + "@astrojs/markdown-remark": "^2.2.0", "@types/chai": "^4.3.1", "@types/html-escaper": "^3.0.0", "@types/mocha": "^9.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94cb491ba0ca..01c3ac7f1c58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3878,6 +3878,9 @@ importers: specifier: ^3.17.3 version: 3.20.6 devDependencies: + '@astrojs/markdown-remark': + specifier: ^2.2.0 + version: link:../../markdown/remark '@types/chai': specifier: ^4.3.1 version: 4.3.3 From 1f673bd79a88a6271c24e94c71faf82696cfc9bf Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 11 May 2023 12:54:41 -0400 Subject: [PATCH 02/18] wip: heading-ids function --- .../markdoc/src/transforms/heading-ids.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/integrations/markdoc/src/transforms/heading-ids.ts diff --git a/packages/integrations/markdoc/src/transforms/heading-ids.ts b/packages/integrations/markdoc/src/transforms/heading-ids.ts new file mode 100644 index 000000000000..f43be1ce5349 --- /dev/null +++ b/packages/integrations/markdoc/src/transforms/heading-ids.ts @@ -0,0 +1,45 @@ +// ./schema/Heading.markdoc.js +import type { MarkdownHeading } from '@astrojs/markdown-remark'; +import { Tag, type Schema, type Node, type RenderableTreeNode } from '@markdoc/markdoc'; + +// Or replace this with your own function +function generateId(attributes: Record, textContent: string): string { + if (attributes.id && typeof attributes.id === 'string') { + return attributes.id; + } + return textContent.replace(/[?]/g, '').replace(/\s+/g, '-').toLowerCase(); +} + +function getTextContent(childNodes: Node[]) { + let text = ''; + for (const node of childNodes) { + if (node.inline && typeof node.attributes.content === 'string') { + text += node.attributes.content; + } else { + text += getTextContent(node.children); + } + } + return text; +} + +export function createHeadingWithIdSchema(): { headings: MarkdownHeading[]; schema: Schema } { + let headings: MarkdownHeading[] = []; + const schema: Schema = { + children: ['inline'], + attributes: { + id: { type: String }, + level: { type: Number, required: true, default: 1 }, + }, + transform(node, config) { + const textContent = node.attributes.content ?? getTextContent(node.children); + const attributes = node.transformAttributes(config); + const children = node.transformChildren(config); + + const slug = generateId(attributes, textContent); + headings.push({ slug, depth: attributes.level, text: textContent }); + + return new Tag(`h${node.attributes['level']}`, { ...attributes, id: slug }, children); + }, + }; + return { headings, schema }; +} From d1b98556813c18d1b458f44c8c86783e50c71886 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 12:17:28 -0400 Subject: [PATCH 03/18] chore: add `@astrojs/markdoc` to external --- packages/astro/src/core/config/vite-load.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/core/config/vite-load.ts b/packages/astro/src/core/config/vite-load.ts index a0d4ee913e1f..df9cfffe95f4 100644 --- a/packages/astro/src/core/config/vite-load.ts +++ b/packages/astro/src/core/config/vite-load.ts @@ -24,6 +24,7 @@ async function createViteLoader(root: string, fs: typeof fsType): Promise Date: Mon, 15 May 2023 12:41:00 -0400 Subject: [PATCH 04/18] feat: `headings` support --- .../markdoc/src/default-config.ts | 26 +++++--- packages/integrations/markdoc/src/index.ts | 66 +++++++++++-------- .../heading-ids.ts => nodes/heading.ts} | 20 +++--- .../integrations/markdoc/src/nodes/index.ts | 1 + 4 files changed, 65 insertions(+), 48 deletions(-) rename packages/integrations/markdoc/src/{transforms/heading-ids.ts => nodes/heading.ts} (61%) create mode 100644 packages/integrations/markdoc/src/nodes/index.ts diff --git a/packages/integrations/markdoc/src/default-config.ts b/packages/integrations/markdoc/src/default-config.ts index 16bd2c41f8cb..7a06a3b28cc1 100644 --- a/packages/integrations/markdoc/src/default-config.ts +++ b/packages/integrations/markdoc/src/default-config.ts @@ -1,18 +1,26 @@ import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc'; import type { ContentEntryModule } from 'astro'; +import { createHeadingNode } from './nodes/index.js'; +import type { MarkdownHeading } from '@astrojs/markdown-remark'; export function applyDefaultConfig( config: MarkdocConfig, - ctx: { - entry: ContentEntryModule; - } -): MarkdocConfig { + entry: ContentEntryModule +): { collectedHeadings: MarkdownHeading[]; config: MarkdocConfig } { + const headingNode = createHeadingNode(); return { - ...config, - variables: { - entry: ctx.entry, - ...config.variables, + collectedHeadings: headingNode.headings, + config: { + ...config, + variables: { + entry, + ...config.variables, + }, + nodes: { + heading: headingNode.schema, + ...config.nodes, + }, + // TODO: Syntax highlighting }, - // TODO: heading ID calculation, Shiki syntax highlighting }; } diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 5b3568992e9b..2048e2622e8c 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -52,7 +52,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration async getRenderModule({ entry, viteId }) { const ast = Markdoc.parse(entry.body); const pluginContext = this; - const markdocConfig = applyDefaultConfig(userMarkdocConfig, { entry }); + const { config: markdocConfig } = applyDefaultConfig(userMarkdocConfig, entry); const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => { return ( @@ -88,36 +88,44 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration }); } - return { - code: `import { jsx as h } from 'astro/jsx-runtime'; -import { applyDefaultConfig } from '@astrojs/markdoc/default-config'; -import { Renderer } from '@astrojs/markdoc/components'; + const res = `import { jsx as h } from 'astro/jsx-runtime'; + import Markdoc from '@markdoc/markdoc'; + import { applyDefaultConfig } from '@astrojs/markdoc/default-config'; + import { Renderer } from '@astrojs/markdoc/components'; import * as entry from ${JSON.stringify(viteId + '?astroContent')};${ - markdocConfigResult - ? `\nimport userConfig from ${JSON.stringify( - markdocConfigResult.fileUrl.pathname - )};` - : '' - }${ - astroConfig.experimental.assets - ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';` - : '' - } -const stringifiedAst = ${JSON.stringify( - /* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast) - )}; + markdocConfigResult + ? `\nimport userConfig from ${JSON.stringify( + markdocConfigResult.fileUrl.pathname + )};` + : '' + }${ + astroConfig.experimental.assets + ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config'; + userConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` + : '' + } +const stringifiedAst = ${JSON.stringify(JSON.stringify(ast))}; +export function getHeadings() { + ${ + /* Yes, we are transforming twice (once from `getHeadings()` and again from in case of variables). + TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself, + instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */ + '' + } + const { collectedHeadings, config } = applyDefaultConfig({ nodes: { headings: userConfig?.nodes?.headings } }, entry); + const ast = Markdoc.Ast.fromJSON(stringifiedAst); + Markdoc.transform(ast, config); + return collectedHeadings; +} export async function Content (props) { - const config = applyDefaultConfig(${ - markdocConfigResult - ? '{ ...userConfig, variables: { ...userConfig.variables, ...props } }' - : '{ variables: props }' - }, { entry });${ - astroConfig.experimental.assets - ? `\nconfig.nodes = { ...experimentalAssetsConfig.nodes, ...config.nodes };` - : '' - } - return h(Renderer, { stringifiedAst, config }); };`, - }; + const { config } = applyDefaultConfig({ + ...userConfig, + variables: { ...userConfig.variables, ...props }, + }, entry); + + return h(Renderer, { config, stringifiedAst }); +}`; + return { code: res }; }, contentModuleTypes: await fs.promises.readFile( new URL('../template/content-module-types.d.ts', import.meta.url), diff --git a/packages/integrations/markdoc/src/transforms/heading-ids.ts b/packages/integrations/markdoc/src/nodes/heading.ts similarity index 61% rename from packages/integrations/markdoc/src/transforms/heading-ids.ts rename to packages/integrations/markdoc/src/nodes/heading.ts index f43be1ce5349..81cf377ba297 100644 --- a/packages/integrations/markdoc/src/transforms/heading-ids.ts +++ b/packages/integrations/markdoc/src/nodes/heading.ts @@ -1,6 +1,6 @@ // ./schema/Heading.markdoc.js import type { MarkdownHeading } from '@astrojs/markdown-remark'; -import { Tag, type Schema, type Node, type RenderableTreeNode } from '@markdoc/markdoc'; +import Markdoc, { type Schema, type RenderableTreeNode } from '@markdoc/markdoc'; // Or replace this with your own function function generateId(attributes: Record, textContent: string): string { @@ -10,19 +10,19 @@ function generateId(attributes: Record, textContent: string): strin return textContent.replace(/[?]/g, '').replace(/\s+/g, '-').toLowerCase(); } -function getTextContent(childNodes: Node[]) { +function getTextContent(childNodes: RenderableTreeNode[]) { let text = ''; for (const node of childNodes) { - if (node.inline && typeof node.attributes.content === 'string') { - text += node.attributes.content; - } else { + if (typeof node === 'string' || typeof node === 'number') { + text += node; + } else if (typeof node === 'object' && Markdoc.Tag.isTag(node)) { text += getTextContent(node.children); } } return text; } -export function createHeadingWithIdSchema(): { headings: MarkdownHeading[]; schema: Schema } { +export function createHeadingNode(): { headings: MarkdownHeading[]; schema: Schema } { let headings: MarkdownHeading[] = []; const schema: Schema = { children: ['inline'], @@ -31,14 +31,14 @@ export function createHeadingWithIdSchema(): { headings: MarkdownHeading[]; sche level: { type: Number, required: true, default: 1 }, }, transform(node, config) { - const textContent = node.attributes.content ?? getTextContent(node.children); - const attributes = node.transformAttributes(config); + const { level, ...attributes } = node.transformAttributes(config); const children = node.transformChildren(config); + const textContent = node.attributes.content ?? getTextContent(children); const slug = generateId(attributes, textContent); - headings.push({ slug, depth: attributes.level, text: textContent }); + headings.push({ slug, depth: level, text: textContent }); - return new Tag(`h${node.attributes['level']}`, { ...attributes, id: slug }, children); + return new Markdoc.Tag(`h${level}`, { ...attributes, id: slug }, children); }, }; return { headings, schema }; diff --git a/packages/integrations/markdoc/src/nodes/index.ts b/packages/integrations/markdoc/src/nodes/index.ts new file mode 100644 index 000000000000..4f4e472797e2 --- /dev/null +++ b/packages/integrations/markdoc/src/nodes/index.ts @@ -0,0 +1 @@ +export { createHeadingNode } from './heading.js'; From b4cb277b8ab9e8755b3741bd7cbeb6da90b90003 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 15:12:21 -0400 Subject: [PATCH 05/18] fix: allow `render` config on headings --- packages/integrations/markdoc/package.json | 2 +- packages/integrations/markdoc/src/config.ts | 6 +- .../markdoc/src/default-config.ts | 26 ------- .../markdoc/src/experimental-assets-config.ts | 2 +- packages/integrations/markdoc/src/index.ts | 15 ++-- .../integrations/markdoc/src/nodes/heading.ts | 57 ++++++-------- .../integrations/markdoc/src/nodes/index.ts | 2 +- packages/integrations/markdoc/src/runtime.ts | 74 +++++++++++++++++++ 8 files changed, 114 insertions(+), 70 deletions(-) delete mode 100644 packages/integrations/markdoc/src/default-config.ts create mode 100644 packages/integrations/markdoc/src/runtime.ts diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 805e9e2fbae0..6024979ec206 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -21,7 +21,7 @@ "exports": { ".": "./dist/index.js", "./components": "./components/index.ts", - "./default-config": "./dist/default-config.js", + "./runtime": "./dist/runtime.js", "./config": "./dist/config.js", "./experimental-assets-config": "./dist/experimental-assets-config.js", "./package.json": "./package.json" diff --git a/packages/integrations/markdoc/src/config.ts b/packages/integrations/markdoc/src/config.ts index 09bbead120e8..7e235747e6d7 100644 --- a/packages/integrations/markdoc/src/config.ts +++ b/packages/integrations/markdoc/src/config.ts @@ -1,5 +1,9 @@ import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc'; -export { default as Markdoc } from '@markdoc/markdoc'; +import * as astroDefaultNodes from './nodes/index.js'; +import _Markdoc from '@markdoc/markdoc'; + +export const Markdoc = _Markdoc; +export const defaultNodes = { ...Markdoc.nodes, ...astroDefaultNodes }; export function defineMarkdocConfig(config: MarkdocConfig): MarkdocConfig { return config; diff --git a/packages/integrations/markdoc/src/default-config.ts b/packages/integrations/markdoc/src/default-config.ts deleted file mode 100644 index 7a06a3b28cc1..000000000000 --- a/packages/integrations/markdoc/src/default-config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc'; -import type { ContentEntryModule } from 'astro'; -import { createHeadingNode } from './nodes/index.js'; -import type { MarkdownHeading } from '@astrojs/markdown-remark'; - -export function applyDefaultConfig( - config: MarkdocConfig, - entry: ContentEntryModule -): { collectedHeadings: MarkdownHeading[]; config: MarkdocConfig } { - const headingNode = createHeadingNode(); - return { - collectedHeadings: headingNode.headings, - config: { - ...config, - variables: { - entry, - ...config.variables, - }, - nodes: { - heading: headingNode.schema, - ...config.nodes, - }, - // TODO: Syntax highlighting - }, - }; -} diff --git a/packages/integrations/markdoc/src/experimental-assets-config.ts b/packages/integrations/markdoc/src/experimental-assets-config.ts index 9627553551b1..2eb96ec99277 100644 --- a/packages/integrations/markdoc/src/experimental-assets-config.ts +++ b/packages/integrations/markdoc/src/experimental-assets-config.ts @@ -5,7 +5,7 @@ import { Image } from 'astro:assets'; // Separate module to only import `astro:assets` when // `experimental.assets` flag is set in a project. -// TODO: merge with `./default-config.ts` when `experimental.assets` is baselined. +// TODO: merge with `./runtime.ts` when `experimental.assets` is baselined. export const experimentalAssetsConfig: MarkdocConfig = { nodes: { image: { diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 2048e2622e8c..96c5bb844155 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -9,7 +9,7 @@ import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from import { emitESMImage } from 'astro/assets'; import { bold, red, yellow } from 'kleur/colors'; import type * as rollup from 'rollup'; -import { applyDefaultConfig } from './default-config.js'; +import { applyDefaultConfig } from './runtime.js'; import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js'; type SetupHookParams = HookParameters<'astro:config:setup'> & { @@ -52,7 +52,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration async getRenderModule({ entry, viteId }) { const ast = Markdoc.parse(entry.body); const pluginContext = this; - const { config: markdocConfig } = applyDefaultConfig(userMarkdocConfig, entry); + const markdocConfig = applyDefaultConfig(userMarkdocConfig, entry); const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => { return ( @@ -90,8 +90,8 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration const res = `import { jsx as h } from 'astro/jsx-runtime'; import Markdoc from '@markdoc/markdoc'; - import { applyDefaultConfig } from '@astrojs/markdoc/default-config'; import { Renderer } from '@astrojs/markdoc/components'; + import { collectHeadings, applyDefaultConfig } from '@astrojs/markdoc/runtime'; import * as entry from ${JSON.stringify(viteId + '?astroContent')};${ markdocConfigResult ? `\nimport userConfig from ${JSON.stringify( @@ -112,13 +112,14 @@ export function getHeadings() { instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */ '' } - const { collectedHeadings, config } = applyDefaultConfig({ nodes: { headings: userConfig?.nodes?.headings } }, entry); + const userHeadingConfig = userConfig?.nodes?.heading; + const config = applyDefaultConfig(userHeadingConfig ? { nodes: { heading: userHeadingConfig } } : {}, entry); const ast = Markdoc.Ast.fromJSON(stringifiedAst); - Markdoc.transform(ast, config); - return collectedHeadings; + const content = Markdoc.transform(ast, config); + return collectHeadings(Array.isArray(content) ? content : content.children); } export async function Content (props) { - const { config } = applyDefaultConfig({ + const config = applyDefaultConfig({ ...userConfig, variables: { ...userConfig.variables, ...props }, }, entry); diff --git a/packages/integrations/markdoc/src/nodes/heading.ts b/packages/integrations/markdoc/src/nodes/heading.ts index 81cf377ba297..97eec9a4e898 100644 --- a/packages/integrations/markdoc/src/nodes/heading.ts +++ b/packages/integrations/markdoc/src/nodes/heading.ts @@ -1,6 +1,5 @@ -// ./schema/Heading.markdoc.js -import type { MarkdownHeading } from '@astrojs/markdown-remark'; -import Markdoc, { type Schema, type RenderableTreeNode } from '@markdoc/markdoc'; +import Markdoc, { type Schema } from '@markdoc/markdoc'; +import { getTextContent } from '../runtime.js'; // Or replace this with your own function function generateId(attributes: Record, textContent: string): string { @@ -10,36 +9,28 @@ function generateId(attributes: Record, textContent: string): strin return textContent.replace(/[?]/g, '').replace(/\s+/g, '-').toLowerCase(); } -function getTextContent(childNodes: RenderableTreeNode[]) { - let text = ''; - for (const node of childNodes) { - if (typeof node === 'string' || typeof node === 'number') { - text += node; - } else if (typeof node === 'object' && Markdoc.Tag.isTag(node)) { - text += getTextContent(node.children); - } - } - return text; -} +export const heading: Schema = { + children: ['inline'], + attributes: { + id: { type: String }, + level: { type: Number, required: true, default: 1 }, + }, + transform(node, config) { + const { level, ...attributes } = node.transformAttributes(config); + const children = node.transformChildren(config); + const textContent = node.attributes.content ?? getTextContent(children); -export function createHeadingNode(): { headings: MarkdownHeading[]; schema: Schema } { - let headings: MarkdownHeading[] = []; - const schema: Schema = { - children: ['inline'], - attributes: { - id: { type: String }, - level: { type: Number, required: true, default: 1 }, - }, - transform(node, config) { - const { level, ...attributes } = node.transformAttributes(config); - const children = node.transformChildren(config); - const textContent = node.attributes.content ?? getTextContent(children); + const slug = generateId(attributes, textContent); - const slug = generateId(attributes, textContent); - headings.push({ slug, depth: level, text: textContent }); + const render = config.nodes?.heading?.render ?? `h${level}`; + const tagProps = + // For components, pass down `level` as a prop, + // alongside `__collectHeading` for our `headings` collector. + // Avoid accidentally rendering `level` as an HTML attribute otherwise! + typeof render === 'function' + ? { ...attributes, id: slug, __collectHeading: true, level } + : { ...attributes, id: slug }; - return new Markdoc.Tag(`h${level}`, { ...attributes, id: slug }, children); - }, - }; - return { headings, schema }; -} + return new Markdoc.Tag(render, tagProps, children); + }, +}; diff --git a/packages/integrations/markdoc/src/nodes/index.ts b/packages/integrations/markdoc/src/nodes/index.ts index 4f4e472797e2..3ca211db2eab 100644 --- a/packages/integrations/markdoc/src/nodes/index.ts +++ b/packages/integrations/markdoc/src/nodes/index.ts @@ -1 +1 @@ -export { createHeadingNode } from './heading.js'; +export { heading } from './heading.js'; diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts new file mode 100644 index 000000000000..c01175c970a8 --- /dev/null +++ b/packages/integrations/markdoc/src/runtime.ts @@ -0,0 +1,74 @@ +import type { MarkdownHeading } from '@astrojs/markdown-remark'; +import Markdoc, { + type RenderableTreeNode, + type ConfigType as MarkdocConfig, +} from '@markdoc/markdoc'; +import type { ContentEntryModule } from 'astro'; +import * as astroDefaultNodes from './nodes/index.js'; + +export function applyDefaultConfig( + config: MarkdocConfig, + entry: ContentEntryModule +): MarkdocConfig { + return { + ...config, + variables: { + entry, + ...config.variables, + }, + nodes: { + ...astroDefaultNodes, + ...config.nodes, + }, + // TODO: Syntax highlighting + }; +} + +/** + * Get text content as a string from a Markdoc transform AST + */ +export function getTextContent(childNodes: RenderableTreeNode[]): string { + let text = ''; + for (const node of childNodes) { + if (typeof node === 'string' || typeof node === 'number') { + text += node; + } else if (typeof node === 'object' && Markdoc.Tag.isTag(node)) { + text += getTextContent(node.children); + } + } + return text; +} + +const headingLevels = [1, 2, 3, 4, 5, 6] as const; + +/** + * Collect headings from Markdoc transform AST + * for `headings` result on `render()` return value + */ +export function collectHeadings(children: RenderableTreeNode[]): MarkdownHeading[] { + let collectedHeadings: MarkdownHeading[] = []; + for (const node of children) { + if (typeof node !== 'object' || !Markdoc.Tag.isTag(node)) continue; + + if (node.attributes.__collectHeading === true && typeof node.attributes.level === 'number') { + collectedHeadings.push({ + slug: node.attributes.id, + depth: node.attributes.level, + text: getTextContent(node.children), + }); + continue; + } + + for (const level of headingLevels) { + if (node.name === 'h' + level) { + collectedHeadings.push({ + slug: node.attributes.id, + depth: level, + text: getTextContent(node.children), + }); + } + } + collectedHeadings.concat(collectHeadings(node.children)); + } + return collectedHeadings; +} From d264d002ac099bccfab927a670bfeac75c49d427 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:03:50 -0400 Subject: [PATCH 06/18] fix: nonexistent `userConfig` --- packages/integrations/markdoc/src/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 96c5bb844155..dc0e6a6c84ae 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -92,16 +92,16 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration import Markdoc from '@markdoc/markdoc'; import { Renderer } from '@astrojs/markdoc/components'; import { collectHeadings, applyDefaultConfig } from '@astrojs/markdoc/runtime'; -import * as entry from ${JSON.stringify(viteId + '?astroContent')};${ - markdocConfigResult - ? `\nimport userConfig from ${JSON.stringify( - markdocConfigResult.fileUrl.pathname - )};` - : '' - }${ +import * as entry from ${JSON.stringify(viteId + '?astroContent')}; +${ + markdocConfigResult + ? `import _userConfig from ${JSON.stringify( + markdocConfigResult.fileUrl.pathname + )};\nconst userConfig = _userConfig ?? {};` + : 'const userConfig = {};' +}${ astroConfig.experimental.assets - ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config'; - userConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` + ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` : '' } const stringifiedAst = ${JSON.stringify(JSON.stringify(ast))}; @@ -112,8 +112,8 @@ export function getHeadings() { instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */ '' } - const userHeadingConfig = userConfig?.nodes?.heading; - const config = applyDefaultConfig(userHeadingConfig ? { nodes: { heading: userHeadingConfig } } : {}, entry); + const headingConfig = userConfig.nodes?.heading; + const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry); const ast = Markdoc.Ast.fromJSON(stringifiedAst); const content = Markdoc.transform(ast, config); return collectHeadings(Array.isArray(content) ? content : content.children); From 3612519a544ac723b4793030e39e22129f5456e4 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:05:09 -0400 Subject: [PATCH 07/18] test: headings, toc, astro component render --- .../fixtures/headings-custom/astro.config.mjs | 7 + .../headings-custom/markdoc.config.mjs | 11 + .../fixtures/headings-custom/package.json | 9 + .../src/components/Heading.astro | 14 ++ .../src/content/docs/headings.mdoc | 11 + .../headings-custom/src/pages/index.astro | 28 +++ .../test/fixtures/headings/astro.config.mjs | 7 + .../test/fixtures/headings/markdoc.config.mjs | 3 + .../test/fixtures/headings/package.json | 9 + .../headings/src/content/docs/headings.mdoc | 11 + .../fixtures/headings/src/pages/index.astro | 28 +++ .../markdoc/test/headings.test.js | 192 ++++++++++++++++++ pnpm-lock.yaml | 18 ++ 13 files changed, 348 insertions(+) create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/package.json create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc create mode 100644 packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro create mode 100644 packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/headings/package.json create mode 100644 packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc create mode 100644 packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro create mode 100644 packages/integrations/markdoc/test/headings.test.js diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs new file mode 100644 index 000000000000..29d846359bb2 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +import markdoc from '@astrojs/markdoc'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs new file mode 100644 index 000000000000..7f765c82040b --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs @@ -0,0 +1,11 @@ +import { defineMarkdocConfig, defaultNodes } from '@astrojs/markdoc/config'; +import Heading from './src/components/Heading.astro'; + +export default defineMarkdocConfig({ + nodes: { + heading: { + ...defaultNodes.heading, + render: Heading, + } + } +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/package.json b/packages/integrations/markdoc/test/fixtures/headings-custom/package.json new file mode 100644 index 000000000000..67a974912ec3 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/headings-custom", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro b/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro new file mode 100644 index 000000000000..ec6fa83050cd --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro @@ -0,0 +1,14 @@ +--- +type Props = { + level: number; + id: string; +}; + +const { level, id }: Props = Astro.props; + +const Tag = `h${level}`; +--- + + + + diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc new file mode 100644 index 000000000000..3eb66580a43a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc @@ -0,0 +1,11 @@ +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro new file mode 100644 index 000000000000..5880be0e3db7 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro @@ -0,0 +1,28 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('docs', 'headings'); +const { Content, headings } = await post.render(); +--- + + + + + + + + Content + + + + + + diff --git a/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs new file mode 100644 index 000000000000..29d846359bb2 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +import markdoc from '@astrojs/markdoc'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs new file mode 100644 index 000000000000..a5863ec1295f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs @@ -0,0 +1,3 @@ +import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({}); diff --git a/packages/integrations/markdoc/test/fixtures/headings/package.json b/packages/integrations/markdoc/test/fixtures/headings/package.json new file mode 100644 index 000000000000..1daaae400569 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/headings", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc new file mode 100644 index 000000000000..3eb66580a43a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc @@ -0,0 +1,11 @@ +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro new file mode 100644 index 000000000000..5880be0e3db7 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro @@ -0,0 +1,28 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('docs', 'headings'); +const { Content, headings } = await post.render(); +--- + + + + + + + + Content + + + + + + diff --git a/packages/integrations/markdoc/test/headings.test.js b/packages/integrations/markdoc/test/headings.test.js new file mode 100644 index 000000000000..5db50065cb62 --- /dev/null +++ b/packages/integrations/markdoc/test/headings.test.js @@ -0,0 +1,192 @@ +import { parseHTML } from 'linkedom'; +import { expect } from 'chai'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +async function getFixture(name) { + return await loadFixture({ + root: new URL(`./fixtures/${name}/`, import.meta.url), + }); +} + +describe('Markdoc - Headings', () => { + let fixture; + + before(async () => { + fixture = await getFixture('headings'); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('applies IDs to headings', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + + tocTest(document); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('applies IDs to headings', async () => { + const html = await fixture.readFile('/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const html = await fixture.readFile('/index.html'); + const { document } = parseHTML(html); + + tocTest(document); + }); + }); +}); + +describe('Markdoc - Headings with custom Astro renderer', () => { + let fixture; + + before(async () => { + fixture = await getFixture('headings-custom'); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('applies IDs to headings', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + + tocTest(document); + }); + + it('renders Astro component for each heading', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + + astroComponentTest(document); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('applies IDs to headings', async () => { + const html = await fixture.readFile('/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const html = await fixture.readFile('/index.html'); + const { document } = parseHTML(html); + + tocTest(document); + }); + + it('renders Astro component for each heading', async () => { + const html = await fixture.readFile('/index.html'); + const { document } = parseHTML(html); + + astroComponentTest(document); + }); + }); +}); + +const depthToHeadingMap = { + 1: { + slug: 'level-1-heading', + text: 'Level 1 heading', + }, + 2: { + slug: 'level-2-heading', + text: 'Level 2 heading', + }, + 3: { + slug: 'level-3-heading', + text: 'Level 3 heading', + }, + 4: { + slug: 'level-4-heading', + text: 'Level 4 heading', + }, + 5: { + slug: 'id-override', + text: 'Level 5 heading with override', + }, + 6: { + slug: 'level-6-heading', + text: 'Level 6 heading', + }, +}; + +/** @param {Document} document */ +function idTest(document) { + for (const [depth, info] of Object.entries(depthToHeadingMap)) { + expect(document.querySelector(`h${depth}`)?.getAttribute('id')).to.equal(info.slug); + } +} + +/** @param {Document} document */ +function tocTest(document) { + const toc = document.querySelector('[data-toc] > ul'); + expect(toc.children).to.have.lengthOf(Object.keys(depthToHeadingMap).length); + + for (const [depth, info] of Object.entries(depthToHeadingMap)) { + const linkEl = toc.querySelector(`a[href="#${info.slug}"]`); + expect(linkEl).to.exist; + expect(linkEl.getAttribute('data-depth')).to.equal(depth); + expect(linkEl.textContent.trim()).to.equal(info.text); + } +} + +/** @param {Document} document */ +function astroComponentTest(document) { + const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + for (const heading of headings) { + expect(heading.hasAttribute('data-custom-heading')).to.be.true; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01c3ac7f1c58..ac5596adb69e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3933,6 +3933,24 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/headings: + dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + + packages/integrations/markdoc/test/fixtures/headings-custom: + dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/image-assets: dependencies: '@astrojs/markdoc': From 5001a010b82fd23828355561f98964d17e59fed7 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:18:05 -0400 Subject: [PATCH 08/18] docs: README --- packages/integrations/markdoc/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/integrations/markdoc/README.md b/packages/integrations/markdoc/README.md index 9a8bda3bbc9a..4976bae34b17 100644 --- a/packages/integrations/markdoc/README.md +++ b/packages/integrations/markdoc/README.md @@ -143,30 +143,29 @@ Use tags like this fancy "aside" to add some *flair* to your docs. #### Render Markdoc nodes / HTML elements as Astro components -You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, and passes through [Markdoc's default attributes for headings](https://markdoc.dev/docs/nodes#built-in-nodes). +You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, and passes through Astro's default heading properties to define attributes and generate heading ids / slugs: ```js // markdoc.config.mjs -import { defineMarkdocConfig, Markdoc } from '@astrojs/markdoc/config'; +import { defineMarkdocConfig, defaultNodes } from '@astrojs/markdoc/config'; import Heading from './src/components/Heading.astro'; export default defineMarkdocConfig({ nodes: { heading: { render: Heading, - attributes: Markdoc.nodes.heading.attributes, + ...defaultNodes.heading, }, }, }) ``` -Now, all Markdown headings will render with the `Heading.astro` component, and pass these `attributes` as component props. For headings, Markdoc provides a `level` attribute containing the numeric heading level. +All Markdown headings will render the `Heading.astro` component and pass `attributes` as component props. For headings, Astro provides the following attributes by default: -This example uses a level 3 heading, automatically passing `level: 3` as the component prop: +- `level: number` The heading level 1 - 6 +- `id: string` An `id` generated from the heading's text contents. This corresponds to the `slug` generated by the [content `render()` function](https://docs.astro.build/en/guides/content-collections/#rendering-content-to-html). -```md -### I'm a level 3 heading! -``` +For example, the heading `### Level 3 heading!` will pass `level: 3` and `id: 'level-3-heading'` as component props. 📚 [Find all of Markdoc's built-in nodes and node attributes on their documentation.](https://markdoc.dev/docs/nodes#built-in-nodes) From d94d1b938112b89f1b14b2d3f8acb97e3214e630 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:24:52 -0400 Subject: [PATCH 09/18] chore: changeset --- .changeset/pretty-students-try.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pretty-students-try.md diff --git a/.changeset/pretty-students-try.md b/.changeset/pretty-students-try.md new file mode 100644 index 000000000000..d39e49d360ae --- /dev/null +++ b/.changeset/pretty-students-try.md @@ -0,0 +1,6 @@ +--- +'@astrojs/markdoc': patch +'astro': patch +--- + +Generate heading `id`s and populate the `headings` property for all Markdoc files From 19f56eaad6e9beec6d08ba851c98f959c9d0028b Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:41:52 -0400 Subject: [PATCH 10/18] refactor: expose Markdoc helpers from runtime --- packages/integrations/markdoc/src/index.ts | 7 +++---- packages/integrations/markdoc/src/runtime.ts | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index dc0e6a6c84ae..3d464b2b11f6 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -89,9 +89,8 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration } const res = `import { jsx as h } from 'astro/jsx-runtime'; - import Markdoc from '@markdoc/markdoc'; import { Renderer } from '@astrojs/markdoc/components'; - import { collectHeadings, applyDefaultConfig } from '@astrojs/markdoc/runtime'; + import { collectHeadings, applyDefaultConfig, transform, Ast } from '@astrojs/markdoc/runtime'; import * as entry from ${JSON.stringify(viteId + '?astroContent')}; ${ markdocConfigResult @@ -114,8 +113,8 @@ export function getHeadings() { } const headingConfig = userConfig.nodes?.heading; const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry); - const ast = Markdoc.Ast.fromJSON(stringifiedAst); - const content = Markdoc.transform(ast, config); + const ast = Ast.fromJSON(stringifiedAst); + const content = transform(ast, config); return collectHeadings(Array.isArray(content) ? content : content.children); } export async function Content (props) { diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index c01175c970a8..1d7fdd92ab0a 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -6,6 +6,8 @@ import Markdoc, { import type { ContentEntryModule } from 'astro'; import * as astroDefaultNodes from './nodes/index.js'; +export { transform, Ast } from '@markdoc/markdoc'; + export function applyDefaultConfig( config: MarkdocConfig, entry: ContentEntryModule From a5d411edb65aa315a6a84d4c7c246a8dff705b25 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 15 May 2023 16:50:11 -0400 Subject: [PATCH 11/18] fix: bad named exports (commonjsssss) --- packages/integrations/markdoc/src/index.ts | 6 +++--- packages/integrations/markdoc/src/runtime.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 3d464b2b11f6..d29f6136c12c 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -90,7 +90,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration const res = `import { jsx as h } from 'astro/jsx-runtime'; import { Renderer } from '@astrojs/markdoc/components'; - import { collectHeadings, applyDefaultConfig, transform, Ast } from '@astrojs/markdoc/runtime'; + import { collectHeadings, applyDefaultConfig, Markdoc } from '@astrojs/markdoc/runtime'; import * as entry from ${JSON.stringify(viteId + '?astroContent')}; ${ markdocConfigResult @@ -113,8 +113,8 @@ export function getHeadings() { } const headingConfig = userConfig.nodes?.heading; const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry); - const ast = Ast.fromJSON(stringifiedAst); - const content = transform(ast, config); + const ast = Markdoc.Ast.fromJSON(stringifiedAst); + const content = Markdoc.transform(ast, config); return collectHeadings(Array.isArray(content) ? content : content.children); } export async function Content (props) { diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index 1d7fdd92ab0a..1a0711d39cdc 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -6,7 +6,7 @@ import Markdoc, { import type { ContentEntryModule } from 'astro'; import * as astroDefaultNodes from './nodes/index.js'; -export { transform, Ast } from '@markdoc/markdoc'; +export { default as Markdoc } from '@markdoc/markdoc'; export function applyDefaultConfig( config: MarkdocConfig, From 9af7ebe7f6ac628368762e64f838d60ad817416f Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 16:35:35 -0400 Subject: [PATCH 12/18] refactor: defaultNodes -> nodes --- packages/integrations/markdoc/README.md | 4 ++-- packages/integrations/markdoc/src/config.ts | 4 ++-- packages/integrations/markdoc/src/runtime.ts | 4 ++-- .../markdoc/test/fixtures/headings-custom/markdoc.config.mjs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/integrations/markdoc/README.md b/packages/integrations/markdoc/README.md index 4976bae34b17..e3cec54999c3 100644 --- a/packages/integrations/markdoc/README.md +++ b/packages/integrations/markdoc/README.md @@ -147,14 +147,14 @@ You may also want to map standard HTML elements like headings and paragraphs to ```js // markdoc.config.mjs -import { defineMarkdocConfig, defaultNodes } from '@astrojs/markdoc/config'; +import { defineMarkdocConfig, nodes } from '@astrojs/markdoc/config'; import Heading from './src/components/Heading.astro'; export default defineMarkdocConfig({ nodes: { heading: { render: Heading, - ...defaultNodes.heading, + ...nodes.heading, }, }, }) diff --git a/packages/integrations/markdoc/src/config.ts b/packages/integrations/markdoc/src/config.ts index 7e235747e6d7..b894ce8ccee4 100644 --- a/packages/integrations/markdoc/src/config.ts +++ b/packages/integrations/markdoc/src/config.ts @@ -1,9 +1,9 @@ import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc'; -import * as astroDefaultNodes from './nodes/index.js'; +import * as astroNodes from './nodes/index.js'; import _Markdoc from '@markdoc/markdoc'; export const Markdoc = _Markdoc; -export const defaultNodes = { ...Markdoc.nodes, ...astroDefaultNodes }; +export const nodes = { ...Markdoc.nodes, ...astroNodes }; export function defineMarkdocConfig(config: MarkdocConfig): MarkdocConfig { return config; diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index 1a0711d39cdc..116987aa73a0 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -4,7 +4,7 @@ import Markdoc, { type ConfigType as MarkdocConfig, } from '@markdoc/markdoc'; import type { ContentEntryModule } from 'astro'; -import * as astroDefaultNodes from './nodes/index.js'; +import * as astroNodes from './nodes/index.js'; export { default as Markdoc } from '@markdoc/markdoc'; @@ -19,7 +19,7 @@ export function applyDefaultConfig( ...config.variables, }, nodes: { - ...astroDefaultNodes, + ...astroNodes, ...config.nodes, }, // TODO: Syntax highlighting diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs index 7f765c82040b..32fcf61e2056 100644 --- a/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs @@ -1,10 +1,10 @@ -import { defineMarkdocConfig, defaultNodes } from '@astrojs/markdoc/config'; +import { defineMarkdocConfig, nodes } from '@astrojs/markdoc/config'; import Heading from './src/components/Heading.astro'; export default defineMarkdocConfig({ nodes: { heading: { - ...defaultNodes.heading, + ...nodes.heading, render: Heading, } } From ded07aefd5157e5be430409372b4ed1a12390b73 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 16:45:50 -0400 Subject: [PATCH 13/18] deps: github-slugger --- packages/integrations/markdoc/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 6024979ec206..7ad8c79ce400 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -36,6 +36,7 @@ "dependencies": { "@markdoc/markdoc": "^0.2.2", "esbuild": "^0.17.12", + "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "kleur": "^4.1.5", "zod": "^3.17.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac5596adb69e..7166b894094d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3868,6 +3868,9 @@ importers: esbuild: specifier: ^0.17.12 version: 0.17.12 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 gray-matter: specifier: ^4.0.3 version: 4.0.3 From ffa109fe7ca67425373e1b95ddf1982a8bc9e036 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 18:14:53 -0400 Subject: [PATCH 14/18] fix: reset slugger cache on each render --- packages/integrations/markdoc/src/config.ts | 2 +- packages/integrations/markdoc/src/index.ts | 4 +++- .../integrations/markdoc/src/nodes/heading.ts | 18 ++++++++++++------ .../integrations/markdoc/src/nodes/index.ts | 5 ++++- packages/integrations/markdoc/src/runtime.ts | 3 ++- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/integrations/markdoc/src/config.ts b/packages/integrations/markdoc/src/config.ts index b894ce8ccee4..1a20b74316a4 100644 --- a/packages/integrations/markdoc/src/config.ts +++ b/packages/integrations/markdoc/src/config.ts @@ -1,5 +1,5 @@ import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc'; -import * as astroNodes from './nodes/index.js'; +import { nodes as astroNodes } from './nodes/index.js'; import _Markdoc from '@markdoc/markdoc'; export const Markdoc = _Markdoc; diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index d29f6136c12c..d6a4f72dbdfb 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -90,7 +90,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration const res = `import { jsx as h } from 'astro/jsx-runtime'; import { Renderer } from '@astrojs/markdoc/components'; - import { collectHeadings, applyDefaultConfig, Markdoc } from '@astrojs/markdoc/runtime'; + import { collectHeadings, applyDefaultConfig, Markdoc, headingSlugger } from '@astrojs/markdoc/runtime'; import * as entry from ${JSON.stringify(viteId + '?astroContent')}; ${ markdocConfigResult @@ -111,6 +111,7 @@ export function getHeadings() { instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */ '' } + headingSlugger.reset(); const headingConfig = userConfig.nodes?.heading; const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry); const ast = Markdoc.Ast.fromJSON(stringifiedAst); @@ -118,6 +119,7 @@ export function getHeadings() { return collectHeadings(Array.isArray(content) ? content : content.children); } export async function Content (props) { + headingSlugger.reset(); const config = applyDefaultConfig({ ...userConfig, variables: { ...userConfig.variables, ...props }, diff --git a/packages/integrations/markdoc/src/nodes/heading.ts b/packages/integrations/markdoc/src/nodes/heading.ts index 97eec9a4e898..81a9181c7a83 100644 --- a/packages/integrations/markdoc/src/nodes/heading.ts +++ b/packages/integrations/markdoc/src/nodes/heading.ts @@ -1,12 +1,18 @@ -import Markdoc, { type Schema } from '@markdoc/markdoc'; +import Markdoc, { type RenderableTreeNode, type Schema } from '@markdoc/markdoc'; import { getTextContent } from '../runtime.js'; +import Slugger from 'github-slugger'; -// Or replace this with your own function -function generateId(attributes: Record, textContent: string): string { +export const headingSlugger = new Slugger(); + +function getSlug(attributes: Record, children: RenderableTreeNode[]): string { if (attributes.id && typeof attributes.id === 'string') { return attributes.id; } - return textContent.replace(/[?]/g, '').replace(/\s+/g, '-').toLowerCase(); + const textContent = attributes.content ?? getTextContent(children); + let slug = headingSlugger.slug(textContent); + + if (slug.endsWith('-')) slug = slug.slice(0, -1); + return slug; } export const heading: Schema = { @@ -18,9 +24,9 @@ export const heading: Schema = { transform(node, config) { const { level, ...attributes } = node.transformAttributes(config); const children = node.transformChildren(config); - const textContent = node.attributes.content ?? getTextContent(children); - const slug = generateId(attributes, textContent); + + const slug = getSlug(attributes, children); const render = config.nodes?.heading?.render ?? `h${level}`; const tagProps = diff --git a/packages/integrations/markdoc/src/nodes/index.ts b/packages/integrations/markdoc/src/nodes/index.ts index 3ca211db2eab..c25b03f27563 100644 --- a/packages/integrations/markdoc/src/nodes/index.ts +++ b/packages/integrations/markdoc/src/nodes/index.ts @@ -1 +1,4 @@ -export { heading } from './heading.js'; +import { heading } from './heading.js'; +export { headingSlugger } from './heading.js'; + +export const nodes = { heading }; diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index 116987aa73a0..b1fecd02cff6 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -4,8 +4,9 @@ import Markdoc, { type ConfigType as MarkdocConfig, } from '@markdoc/markdoc'; import type { ContentEntryModule } from 'astro'; -import * as astroNodes from './nodes/index.js'; +import { nodes } from './nodes/index.js'; +export { headingSlugger } from './nodes/index.js'; export { default as Markdoc } from '@markdoc/markdoc'; export function applyDefaultConfig( From 47469cf61e17a95baaf2019b744f32c25b1b9927 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 18:17:08 -0400 Subject: [PATCH 15/18] fix: bad astroNodes import --- packages/integrations/markdoc/src/runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index b1fecd02cff6..b5001d7536df 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -4,7 +4,7 @@ import Markdoc, { type ConfigType as MarkdocConfig, } from '@markdoc/markdoc'; import type { ContentEntryModule } from 'astro'; -import { nodes } from './nodes/index.js'; +import { nodes as astroNodes } from './nodes/index.js'; export { headingSlugger } from './nodes/index.js'; export { default as Markdoc } from '@markdoc/markdoc'; From a02bafa058767b610dca4093c60e36706818a4f7 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 18:18:12 -0400 Subject: [PATCH 16/18] docs: explain headingSlugger export --- packages/integrations/markdoc/src/runtime.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index b5001d7536df..dadb73cd6601 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -6,6 +6,7 @@ import Markdoc, { import type { ContentEntryModule } from 'astro'; import { nodes as astroNodes } from './nodes/index.js'; +/** Used to reset Slugger cache on each build at runtime */ export { headingSlugger } from './nodes/index.js'; export { default as Markdoc } from '@markdoc/markdoc'; From d99ba9b5fafe2e3de35438db55590a4d8393480f Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 16 May 2023 18:20:10 -0400 Subject: [PATCH 17/18] docs: add back double stringify comment --- packages/integrations/markdoc/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index d6a4f72dbdfb..65f81644a09c 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -103,7 +103,7 @@ ${ ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` : '' } -const stringifiedAst = ${JSON.stringify(JSON.stringify(ast))}; +const stringifiedAst = ${JSON.stringify(/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast))}; export function getHeadings() { ${ /* Yes, we are transforming twice (once from `getHeadings()` and again from in case of variables). From 11c388f664346f605663ebd41d0f9d08d5cae0fb Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Tue, 16 May 2023 18:33:22 -0400 Subject: [PATCH 18/18] chore: bump to minor for internal exports change --- .changeset/pretty-students-try.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pretty-students-try.md b/.changeset/pretty-students-try.md index d39e49d360ae..657d6b6d8df6 100644 --- a/.changeset/pretty-students-try.md +++ b/.changeset/pretty-students-try.md @@ -1,5 +1,5 @@ --- -'@astrojs/markdoc': patch +'@astrojs/markdoc': minor 'astro': patch ---