summaryrefslogtreecommitdiff
path: root/packages/integrations/markdoc/src/runtime.ts
diff options
context:
space:
mode:
authorGravatar Ben Holmes <hey@bholmes.dev> 2023-06-27 15:05:17 -0400
committerGravatar GitHub <noreply@github.com> 2023-06-27 15:05:17 -0400
commitfb7af551148f5ca6c4f98a4e556c8948c5690919 (patch)
tree7c65c758ead74ee129bc958279a6cf02e931cb4d /packages/integrations/markdoc/src/runtime.ts
parent8821f0504845b351247ce6c1e2ae581a71806209 (diff)
downloadastro-fb7af551148f5ca6c4f98a4e556c8948c5690919.tar.gz
astro-fb7af551148f5ca6c4f98a4e556c8948c5690919.tar.zst
astro-fb7af551148f5ca6c4f98a4e556c8948c5690919.zip
feat: New Markdoc `render` API (#7468)
* feat: URL support for markdoc tags * refactor: move to separate file * feat: support URL for markdoc nodes * feat: support `extends` with URL * chore: changeset * fix: bad AstroMarkdocConfig type * fix: experimentalAssetsConfig missing * fix: correctly merge runtime config * chore: formatting * deps: astro internal helpers * feat: component() util, new astro bundling * chore: remove now unused code * todo: missing hint * fix: import.meta.url type error * wip: test nested collection calls * feat: resolve paths from project root * refactor: move getHeadings() to runtime module * fix: broken collectHeadings * test: update fixture configs * chore: remove suggestions. Out of scope! * fix: throw outside esbuild * refactor: shuffle imports around * Revert "wip: test nested collection calls" This reverts commit 9354b3cf9222fd65b974b0cddf4e7a95ab3cd2b2. * chore: revert back to mjs config * chore: add jsdocs to stringified helpers * fix: restore updated changeset --------- Co-authored-by: bholmesdev <bholmesdev@gmail.com>
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',
+ });
}