feat: URL support for markdoc tags

This commit is contained in:
bholmesdev 2023-06-07 15:25:24 -04:00 committed by bholmesdev
parent 32bde967f4
commit d87544c241
3 changed files with 99 additions and 39 deletions

View file

@ -9,7 +9,7 @@ import _Markdoc from '@markdoc/markdoc';
import type { AstroInstance } from 'astro'; import type { AstroInstance } from 'astro';
import { heading } from './heading-ids.js'; import { heading } from './heading-ids.js';
type Render = AstroInstance['default'] | string; type Render = AstroInstance['default'] | URL | string;
export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit< export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit<
MarkdocConfig, MarkdocConfig,

View file

@ -61,6 +61,9 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
} }
const userMarkdocConfig = markdocConfigResult?.config ?? {}; const userMarkdocConfig = markdocConfigResult?.config ?? {};
const markdocConfigId = 'astro:markdoc-config';
const resolvedMarkdocConfigId = '\x00' + markdocConfigId;
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) { function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl)); const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
return { return {
@ -130,51 +133,45 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
} from 'astro/runtime/server/index.js'; } from 'astro/runtime/server/index.js';
import { Renderer } from '@astrojs/markdoc/components'; import { Renderer } from '@astrojs/markdoc/components';
import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime'; import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
${ import userConfig from ${JSON.stringify(markdocConfigId)};${
markdocConfigResult
? `import _userConfig from ${JSON.stringify(
markdocConfigResultId
)};\nconst userConfig = _userConfig ?? {};`
: 'const userConfig = {};'
}${
astroConfig.experimental.assets astroConfig.experimental.assets
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` ? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };`
: '' : ''
} }
const stringifiedAst = ${JSON.stringify( const stringifiedAst = ${JSON.stringify(
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast) /* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
)}; )};
export function getHeadings() { export function getHeadings() {
${ ${
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables). /* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself, 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. */ instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
'' ''
} }
const headingConfig = userConfig.nodes?.heading; const headingConfig = userConfig.nodes?.heading;
const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {}); const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
const ast = Markdoc.Ast.fromJSON(stringifiedAst); const ast = Markdoc.Ast.fromJSON(stringifiedAst);
const content = Markdoc.transform(ast, config); const content = Markdoc.transform(ast, config);
return collectHeadings(Array.isArray(content) ? content : content.children); return collectHeadings(Array.isArray(content) ? content : content.children);
} }
export const Content = createComponent({ export const Content = createComponent({
async factory(result, props) { async factory(result, props) {
const config = await setupConfig({ const config = await setupConfig({
...userConfig, ...userConfig,
variables: { ...userConfig.variables, ...props }, variables: { ...userConfig.variables, ...props },
}); });
return renderComponent( return renderComponent(
result, result,
Renderer.name, Renderer.name,
Renderer, Renderer,
{ stringifiedAst, config }, { stringifiedAst, config },
{} {}
); );
}, },
propagation: 'self', propagation: 'self',
});`; });`;
return { code: res }; return { code: res };
}, },
contentModuleTypes: await fs.promises.readFile( contentModuleTypes: await fs.promises.readFile(
@ -227,6 +224,48 @@ export const Content = createComponent({
} }
}, },
}, },
{
name: '@astrojs/markdoc:config',
resolveId(this: rollup.PluginContext, id: string) {
if (id === markdocConfigId) {
return resolvedMarkdocConfigId;
}
},
load(id: string) {
if (id !== resolvedMarkdocConfigId) return;
// TODO: migrate config loader, invalidate on change
if (!markdocConfigResult) {
return `export default {}`;
}
const { config, fileUrl } = markdocConfigResult;
let componentPathnameByTag: Record<string, string> = {};
const { tags = {}, nodes = {} /* TODO: nodes */ } = config;
for (const [name, value] of Object.entries(tags)) {
if (value.render instanceof URL) {
componentPathnameByTag[name] = value.render.pathname;
}
}
let stringifiedComponentImports = '';
let stringifiedComponentMap = '{';
for (const [tag, componentPathname] of Object.entries(componentPathnameByTag)) {
stringifiedComponentImports += `import ${tag} from ${JSON.stringify(
componentPathname + '?astroPropagatedAssets'
)};\n`;
stringifiedComponentMap += `${tag},\n`;
}
stringifiedComponentMap += '}';
const code = `import { resolveComponentImports } from '@astrojs/markdoc/runtime';
import markdocConfig from ${JSON.stringify(fileUrl.pathname)};
${stringifiedComponentImports};
const tagComponentMap = ${stringifiedComponentMap};
export default resolveComponentImports(markdocConfig, tagComponentMap);`;
console.log('$$$markdoc-config', code);
return code;
},
},
], ],
}, },
}); });

View file

@ -1,4 +1,5 @@
import type { MarkdownHeading } from '@astrojs/markdown-remark'; import type { MarkdownHeading } from '@astrojs/markdown-remark';
import type { AstroInstance } from 'astro';
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'; import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc';
import type { AstroMarkdocConfig } from './config.js'; import type { AstroMarkdocConfig } from './config.js';
import { setupHeadingConfig } from './heading-ids.js'; import { setupHeadingConfig } from './heading-ids.js';
@ -66,6 +67,26 @@ function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig):
}; };
} }
export function resolveComponentImports(
markdocConfig: AstroMarkdocConfig,
tagComponentMap: Record<string, AstroInstance['default']>
) {
const resolvedTags = { ...markdocConfig.tags };
for (const [tagName, tagConfig] of Object.entries(resolvedTags)) {
if (tagName in tagComponentMap) {
resolvedTags[tagName] = {
...tagConfig,
render: tagComponentMap[tagName],
};
}
}
console.log('resolvedTags', resolvedTags);
return {
...markdocConfig,
tags: resolvedTags,
};
}
/** /**
* Get text content as a string from a Markdoc transform AST * Get text content as a string from a Markdoc transform AST
*/ */