summaryrefslogtreecommitdiff
path: root/packages/integrations/markdoc/src/runtime.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/markdoc/src/runtime.ts')
-rw-r--r--packages/integrations/markdoc/src/runtime.ts100
1 files changed, 85 insertions, 15 deletions
diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts
index b0e8f2554..d710f1bd8 100644
--- a/packages/integrations/markdoc/src/runtime.ts
+++ b/packages/integrations/markdoc/src/runtime.ts
@@ -1,19 +1,25 @@
import type { MarkdownHeading } from '@astrojs/markdown-remark';
-import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc';
+import type { AstroInstance } from 'astro';
+import {
+ createComponent,
+ renderComponent,
+ // @ts-expect-error Cannot find module 'astro/runtime/server/index.js' or its corresponding type declarations.
+} from 'astro/runtime/server/index.js';
+import Markdoc, {
+ type ConfigType,
+ type Node,
+ type NodeType,
+ type RenderableTreeNode,
+} from '@markdoc/markdoc';
import type { AstroMarkdocConfig } from './config.js';
import { setupHeadingConfig } from './heading-ids.js';
-/** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
-export { default as Markdoc } from '@markdoc/markdoc';
-
/**
* Merge user config with default config and set up context (ex. heading ID slugger)
* Called on each file's individual transform.
* TODO: virtual module to merge configs per-build instead of per-file?
*/
-export async function setupConfig(
- userConfig: AstroMarkdocConfig
-): Promise<Omit<AstroMarkdocConfig, 'extends'>> {
+export async function setupConfig(userConfig: AstroMarkdocConfig = {}): Promise<MergedConfig> {
let defaultConfig: AstroMarkdocConfig = setupHeadingConfig();
if (userConfig.extends) {
@@ -30,16 +36,19 @@ export async function setupConfig(
}
/** Used for synchronous `getHeadings()` function */
-export function setupConfigSync(
- userConfig: AstroMarkdocConfig
-): Omit<AstroMarkdocConfig, 'extends'> {
+export function setupConfigSync(userConfig: AstroMarkdocConfig = {}): MergedConfig {
const defaultConfig: AstroMarkdocConfig = setupHeadingConfig();
return mergeConfig(defaultConfig, userConfig);
}
+type MergedConfig = Required<Omit<AstroMarkdocConfig, 'extends'>>;
+
/** Merge function from `@markdoc/markdoc` internals */
-function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): AstroMarkdocConfig {
+export function mergeConfig(
+ configA: AstroMarkdocConfig,
+ configB: AstroMarkdocConfig
+): MergedConfig {
return {
...configA,
...configB,
@@ -63,9 +72,33 @@ function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig):
...configA.variables,
...configB.variables,
},
+ partials: {
+ ...configA.partials,
+ ...configB.partials,
+ },
+ validation: {
+ ...configA.validation,
+ ...configB.validation,
+ },
};
}
+export function resolveComponentImports(
+ markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>,
+ tagComponentMap: Record<string, AstroInstance['default']>,
+ nodeComponentMap: Record<NodeType, AstroInstance['default']>
+) {
+ for (const [tag, render] of Object.entries(tagComponentMap)) {
+ const config = markdocConfig.tags[tag];
+ if (config) config.render = render;
+ }
+ for (const [node, render] of Object.entries(nodeComponentMap)) {
+ const config = markdocConfig.nodes[node as NodeType];
+ if (config) config.render = render;
+ }
+ return markdocConfig;
+}
+
/**
* Get text content as a string from a Markdoc transform AST
*/
@@ -87,8 +120,10 @@ 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[] = [];
+export function collectHeadings(
+ children: RenderableTreeNode[],
+ collectedHeadings: MarkdownHeading[]
+) {
for (const node of children) {
if (typeof node !== 'object' || !Markdoc.Tag.isTag(node)) continue;
@@ -110,7 +145,42 @@ export function collectHeadings(children: RenderableTreeNode[]): MarkdownHeading
});
}
}
- collectedHeadings.concat(collectHeadings(node.children));
+ collectHeadings(node.children, collectedHeadings);
}
- return collectedHeadings;
+}
+
+export function createGetHeadings(stringifiedAst: string, userConfig: AstroMarkdocConfig) {
+ return function getHeadings() {
+ /* 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,
+ instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
+ const config = setupConfigSync(userConfig);
+ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
+ const content = Markdoc.transform(ast as Node, config as ConfigType);
+ let collectedHeadings: MarkdownHeading[] = [];
+ collectHeadings(Array.isArray(content) ? content : [content], collectedHeadings);
+ return collectedHeadings;
+ };
+}
+
+export function createContentComponent(
+ Renderer: AstroInstance['default'],
+ stringifiedAst: string,
+ userConfig: AstroMarkdocConfig,
+ tagComponentMap: Record<string, AstroInstance['default']>,
+ nodeComponentMap: Record<NodeType, AstroInstance['default']>
+) {
+ return createComponent({
+ async factory(result: any, props: Record<string, any>) {
+ const withVariables = mergeConfig(userConfig, { variables: props });
+ const config = resolveComponentImports(
+ await setupConfig(withVariables),
+ tagComponentMap,
+ nodeComponentMap
+ );
+
+ return renderComponent(result, Renderer.name, Renderer, { stringifiedAst, config }, {});
+ },
+ propagation: 'self',
+ });
}