@astrojs/markdoc 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,23 @@
1
- import type { AstroInstance } from 'astro';
2
- import type { RenderableTreeNode } from '@markdoc/markdoc';
1
+ import type { RenderableTreeNodes } from '@markdoc/markdoc';
3
2
  import Markdoc from '@markdoc/markdoc';
3
+ import type { AstroInstance, SSRResult } from 'astro';
4
+ import type { HTMLString } from 'astro/runtime/server/index.js';
4
5
  import {
5
6
  createComponent,
6
- renderComponent,
7
+ createHeadAndContent,
8
+ isHTMLString,
7
9
  render,
10
+ renderComponent,
8
11
  renderScriptElement,
12
+ renderTemplate,
9
13
  renderUniqueStylesheet,
10
- createHeadAndContent,
11
14
  unescapeHTML,
12
- renderTemplate,
13
- HTMLString,
14
- isHTMLString,
15
15
  } from 'astro/runtime/server/index.js';
16
16
 
17
- export type TreeNode =
17
+ type TreeNode =
18
+ // Markdoc `if` tag often returns an array of nodes in the AST, which gets translated
19
+ // here as an array of `TreeNode`s, which we'll render all without a wrapper.
20
+ | TreeNode[]
18
21
  | {
19
22
  type: 'text';
20
23
  content: string | HTMLString;
@@ -35,81 +38,90 @@ export type TreeNode =
35
38
  children: TreeNode[];
36
39
  };
37
40
 
38
- export const ComponentNode = createComponent({
39
- factory(result: any, { treeNode }: { treeNode: TreeNode }) {
40
- if (treeNode.type === 'text') return render`${treeNode.content}`;
41
-
42
- const slots = {
43
- default: () =>
44
- render`${treeNode.children.map((child) =>
45
- renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child })
46
- )}`,
47
- };
48
- if (treeNode.type === 'component') {
49
- let styles = '',
50
- links = '',
51
- scripts = '';
52
- if (Array.isArray(treeNode.collectedStyles)) {
53
- styles = treeNode.collectedStyles
54
- .map((style: any) =>
55
- renderUniqueStylesheet(result, {
56
- type: 'inline',
57
- content: style,
58
- })
59
- )
60
- .join('');
61
- }
62
- if (Array.isArray(treeNode.collectedLinks)) {
63
- links = treeNode.collectedLinks
64
- .map((link: any) => {
65
- return renderUniqueStylesheet(result, {
66
- type: 'external',
67
- src: link[0] === '/' ? link : '/' + link,
68
- });
69
- })
70
- .join('');
71
- }
72
- if (Array.isArray(treeNode.collectedScripts)) {
73
- scripts = treeNode.collectedScripts
74
- .map((script: any) => renderScriptElement(script))
75
- .join('');
76
- }
77
-
78
- const head = unescapeHTML(styles + links + scripts);
79
-
80
- let headAndContent = createHeadAndContent(
81
- head,
82
- renderTemplate`${renderComponent(
83
- result,
84
- treeNode.component.name,
85
- treeNode.component,
86
- treeNode.props,
87
- slots
88
- )}`
89
- );
90
-
91
- // Let the runtime know that this component is being used.
92
- // `result.propagators` has been moved to `result._metadata.propagators`
93
- // TODO: remove this fallback in the next markdoc integration major
94
- const propagators = result._metadata.propagators || result.propagators;
95
- propagators.set(
96
- {},
97
- {
98
- init() {
99
- return headAndContent;
100
- },
101
- }
102
- );
103
-
104
- return headAndContent;
41
+ function renderTreeNodeToFactoryResult(result: SSRResult, treeNode: TreeNode) {
42
+ if (Array.isArray(treeNode)) {
43
+ return Promise.all(
44
+ treeNode.map((node) =>
45
+ renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: node }),
46
+ ),
47
+ );
48
+ }
49
+
50
+ if (treeNode.type === 'text') return render`${treeNode.content}`;
51
+
52
+ const slots = {
53
+ default: () =>
54
+ render`${treeNode.children.map((child) =>
55
+ renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child }),
56
+ )}`,
57
+ };
58
+ if (treeNode.type === 'component') {
59
+ let styles = '',
60
+ links = '',
61
+ scripts = '';
62
+ if (Array.isArray(treeNode.collectedStyles)) {
63
+ styles = treeNode.collectedStyles
64
+ .map((style: any) =>
65
+ renderUniqueStylesheet(result, {
66
+ type: 'inline',
67
+ content: style,
68
+ }),
69
+ )
70
+ .join('');
71
+ }
72
+ if (Array.isArray(treeNode.collectedLinks)) {
73
+ links = treeNode.collectedLinks
74
+ .map((link: any) => {
75
+ return renderUniqueStylesheet(result, {
76
+ type: 'external',
77
+ src: link[0] === '/' ? link : '/' + link,
78
+ });
79
+ })
80
+ .join('');
81
+ }
82
+ if (Array.isArray(treeNode.collectedScripts)) {
83
+ scripts = treeNode.collectedScripts
84
+ .map((script: any) => renderScriptElement(script))
85
+ .join('');
105
86
  }
106
- return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
87
+
88
+ const head = unescapeHTML(styles + links + scripts);
89
+
90
+ let headAndContent = createHeadAndContent(
91
+ head,
92
+ renderTemplate`${renderComponent(
93
+ result,
94
+ treeNode.component.name,
95
+ treeNode.component,
96
+ treeNode.props,
97
+ slots,
98
+ )}`,
99
+ );
100
+
101
+ // Let the runtime know that this component is being used.
102
+ // @ts-expect-error Astro only uses `init()` so specify it only (plus `_metadata` is internal)
103
+ result._metadata.propagators.add({
104
+ init() {
105
+ return headAndContent;
106
+ },
107
+ });
108
+
109
+ return headAndContent;
110
+ }
111
+ return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
112
+ }
113
+
114
+ export const ComponentNode = createComponent({
115
+ factory(result: SSRResult, { treeNode }: { treeNode: TreeNode | TreeNode[] }) {
116
+ return renderTreeNodeToFactoryResult(result, treeNode);
107
117
  },
108
118
  propagation: 'self',
109
119
  });
110
120
 
111
- export async function createTreeNode(node: RenderableTreeNode): Promise<TreeNode> {
112
- if (isHTMLString(node)) {
121
+ export async function createTreeNode(node: RenderableTreeNodes): Promise<TreeNode> {
122
+ if (Array.isArray(node)) {
123
+ return Promise.all(node.map((child) => createTreeNode(child)));
124
+ } else if (isHTMLString(node)) {
113
125
  return { type: 'text', content: node as HTMLString };
114
126
  } else if (typeof node === 'string' || typeof node === 'number') {
115
127
  return { type: 'text', content: String(node) };
@@ -1,13 +1,13 @@
1
- import Markdoc from "@markdoc/markdoc";
2
- import matter from "gray-matter";
3
1
  import fs from "node:fs";
4
2
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { MarkdocError, isComponentConfig, isValidUrl, prependForwardSlash } from "./utils.js";
7
- import { emitESMImage } from "astro/assets/utils";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { parseFrontmatter } from "@astrojs/markdown-remark";
5
+ import Markdoc from "@markdoc/markdoc";
6
+ import { emitImageMetadata } from "astro/assets/utils/node";
8
7
  import { htmlTokenTransform } from "./html/transform/html-token-transform.js";
9
8
  import { setupConfig } from "./runtime.js";
10
9
  import { getMarkdocTokenizer } from "./tokenizer.js";
10
+ import { isComponentConfig, isValidUrl, MarkdocError, prependForwardSlash } from "./utils.js";
11
11
  async function getContentEntryType({
12
12
  markdocConfigResult,
13
13
  astroConfig,
@@ -15,76 +15,82 @@ async function getContentEntryType({
15
15
  }) {
16
16
  return {
17
17
  extensions: [".mdoc"],
18
- getEntryInfo,
18
+ getEntryInfo({ fileUrl, contents }) {
19
+ const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl));
20
+ return {
21
+ data: parsed.frontmatter,
22
+ body: parsed.content.trim(),
23
+ slug: parsed.frontmatter.slug,
24
+ rawData: parsed.rawFrontmatter
25
+ };
26
+ },
19
27
  handlePropagation: true,
20
28
  async getRenderModule({ contents, fileUrl, viteId }) {
21
- const entry = getEntryInfo({ contents, fileUrl });
29
+ const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl));
22
30
  const tokenizer = getMarkdocTokenizer(options);
23
- let tokens = tokenizer.tokenize(entry.body);
31
+ let tokens = tokenizer.tokenize(parsed.content);
24
32
  if (options?.allowHTML) {
25
33
  tokens = htmlTokenTransform(tokenizer, tokens);
26
34
  }
27
35
  const ast = Markdoc.parse(tokens);
28
- const usedTags = getUsedTags(ast);
29
36
  const userMarkdocConfig = markdocConfigResult?.config ?? {};
30
37
  const markdocConfigUrl = markdocConfigResult?.fileUrl;
38
+ const pluginContext = this;
39
+ const markdocConfig = await setupConfig(userMarkdocConfig, options);
40
+ const filePath = fileURLToPath(fileUrl);
41
+ raiseValidationErrors({
42
+ ast,
43
+ /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */
44
+ markdocConfig,
45
+ viteId,
46
+ astroConfig,
47
+ filePath
48
+ });
49
+ await resolvePartials({
50
+ ast,
51
+ markdocConfig,
52
+ fileUrl,
53
+ allowHTML: options?.allowHTML,
54
+ tokenizer,
55
+ pluginContext,
56
+ root: astroConfig.root,
57
+ raisePartialValidationErrors: (partialAst, partialPath) => {
58
+ raiseValidationErrors({
59
+ ast: partialAst,
60
+ markdocConfig,
61
+ viteId,
62
+ astroConfig,
63
+ filePath: partialPath
64
+ });
65
+ }
66
+ });
67
+ const usedTags = getUsedTags(ast);
31
68
  let componentConfigByTagMap = {};
32
69
  for (const tag of usedTags) {
33
- const render = userMarkdocConfig.tags?.[tag]?.render;
70
+ const render = markdocConfig.tags?.[tag]?.render;
34
71
  if (isComponentConfig(render)) {
35
72
  componentConfigByTagMap[tag] = render;
36
73
  }
37
74
  }
38
75
  let componentConfigByNodeMap = {};
39
- for (const [nodeType, schema] of Object.entries(userMarkdocConfig.nodes ?? {})) {
76
+ for (const [nodeType, schema] of Object.entries(markdocConfig.nodes ?? {})) {
40
77
  const render = schema?.render;
41
78
  if (isComponentConfig(render)) {
42
79
  componentConfigByNodeMap[nodeType] = render;
43
80
  }
44
81
  }
45
- const pluginContext = this;
46
- const markdocConfig = await setupConfig(userMarkdocConfig, options);
47
- const filePath = fileURLToPath(fileUrl);
48
- const validationErrors = Markdoc.validate(
49
- ast,
50
- /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */
51
- markdocConfig
52
- ).filter((e) => {
53
- return (
54
- // Ignore `variable-undefined` errors.
55
- // Variables can be configured at runtime,
56
- // so we cannot validate them at build time.
57
- e.error.id !== "variable-undefined" && (e.error.level === "error" || e.error.level === "critical")
58
- );
82
+ await emitOptimizedImages(ast.children, {
83
+ hasDefaultImage: Boolean(markdocConfig.nodes.image),
84
+ astroConfig,
85
+ pluginContext,
86
+ filePath
59
87
  });
60
- if (validationErrors.length) {
61
- const frontmatterBlockOffset = entry.rawData.split("\n").length + 2;
62
- const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
63
- throw new MarkdocError({
64
- message: [
65
- `**${String(rootRelativePath)}** contains invalid content:`,
66
- ...validationErrors.map((e) => `- ${e.error.message}`)
67
- ].join("\n"),
68
- location: {
69
- // Error overlay does not support multi-line or ranges.
70
- // Just point to the first line.
71
- line: frontmatterBlockOffset + validationErrors[0].lines[0],
72
- file: viteId
73
- }
74
- });
75
- }
76
- if (astroConfig.experimental.assets) {
77
- await emitOptimizedImages(ast.children, {
78
- astroConfig,
79
- pluginContext,
80
- filePath
81
- });
82
- }
83
88
  const res = `import { Renderer } from '@astrojs/markdoc/components';
84
89
  import { createGetHeadings, createContentComponent } from '@astrojs/markdoc/runtime';
85
- ${markdocConfigUrl ? `import markdocConfig from ${JSON.stringify(markdocConfigUrl.pathname)};` : "const markdocConfig = {};"}${astroConfig.experimental.assets ? `
86
- import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
87
- markdocConfig.nodes = { ...experimentalAssetsConfig.nodes, ...markdocConfig.nodes };` : ""}
90
+ ${markdocConfigUrl ? `import markdocConfig from ${JSON.stringify(fileURLToPath(markdocConfigUrl))};` : "const markdocConfig = {};"}
91
+
92
+ import { assetsConfig } from '@astrojs/markdoc/runtime-assets-config';
93
+ markdocConfig.nodes = { ...assetsConfig.nodes, ...markdocConfig.nodes };
88
94
 
89
95
  ${getStringifiedImports(componentConfigByTagMap, "Tag", astroConfig.root)}
90
96
  ${getStringifiedImports(componentConfigByNodeMap, "Node", astroConfig.root)}
@@ -104,7 +110,7 @@ export const Content = createContentComponent(
104
110
  Renderer,
105
111
  stringifiedAst,
106
112
  markdocConfig,
107
- options,
113
+ options,
108
114
  tagComponentMap,
109
115
  nodeComponentMap,
110
116
  )`;
@@ -116,43 +122,139 @@ export const Content = createContentComponent(
116
122
  )
117
123
  };
118
124
  }
125
+ async function resolvePartials({
126
+ ast,
127
+ fileUrl,
128
+ root,
129
+ tokenizer,
130
+ allowHTML,
131
+ markdocConfig,
132
+ pluginContext,
133
+ raisePartialValidationErrors
134
+ }) {
135
+ const relativePartialPath = path.relative(fileURLToPath(root), fileURLToPath(fileUrl));
136
+ for (const node of ast.walk()) {
137
+ if (node.type === "tag" && node.tag === "partial") {
138
+ const { file } = node.attributes;
139
+ if (!file) {
140
+ throw new MarkdocError({
141
+ // Should be caught by Markdoc validation step.
142
+ message: `(Uncaught error) Partial tag requires a 'file' attribute`
143
+ });
144
+ }
145
+ if (markdocConfig.partials?.[file]) continue;
146
+ let partialPath;
147
+ let partialContents;
148
+ try {
149
+ const resolved = await pluginContext.resolve(file, fileURLToPath(fileUrl));
150
+ let partialId = resolved?.id;
151
+ if (!partialId) {
152
+ const attemptResolveAsRelative = await pluginContext.resolve(
153
+ "./" + file,
154
+ fileURLToPath(fileUrl)
155
+ );
156
+ if (!attemptResolveAsRelative?.id) throw new Error();
157
+ partialId = attemptResolveAsRelative.id;
158
+ }
159
+ partialPath = fileURLToPath(new URL(prependForwardSlash(partialId), "file://"));
160
+ partialContents = await fs.promises.readFile(partialPath, "utf-8");
161
+ } catch {
162
+ throw new MarkdocError({
163
+ message: [
164
+ `**${String(relativePartialPath)}** contains invalid content:`,
165
+ `Could not read partial file \`${file}\`. Does the file exist?`
166
+ ].join("\n")
167
+ });
168
+ }
169
+ if (pluginContext.meta.watchMode) pluginContext.addWatchFile(partialPath);
170
+ let partialTokens = tokenizer.tokenize(partialContents);
171
+ if (allowHTML) {
172
+ partialTokens = htmlTokenTransform(tokenizer, partialTokens);
173
+ }
174
+ const partialAst = Markdoc.parse(partialTokens);
175
+ raisePartialValidationErrors(partialAst, partialPath);
176
+ await resolvePartials({
177
+ ast: partialAst,
178
+ root,
179
+ fileUrl: pathToFileURL(partialPath),
180
+ tokenizer,
181
+ allowHTML,
182
+ markdocConfig,
183
+ pluginContext,
184
+ raisePartialValidationErrors
185
+ });
186
+ Object.assign(node, partialAst);
187
+ }
188
+ }
189
+ }
190
+ function raiseValidationErrors({
191
+ ast,
192
+ markdocConfig,
193
+ viteId,
194
+ astroConfig,
195
+ filePath
196
+ }) {
197
+ const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
198
+ return (e.error.level === "error" || e.error.level === "critical") && // Ignore `variable-undefined` errors.
199
+ // Variables can be configured at runtime,
200
+ // so we cannot validate them at build time.
201
+ e.error.id !== "variable-undefined" && // Ignore missing partial errors.
202
+ // We will resolve these in `resolvePartials`.
203
+ !(e.error.id === "attribute-value-invalid" && /^Partial .+ not found/.test(e.error.message));
204
+ });
205
+ if (validationErrors.length) {
206
+ const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
207
+ throw new MarkdocError({
208
+ message: [
209
+ `**${String(rootRelativePath)}** contains invalid content:`,
210
+ ...validationErrors.map((e) => `- ${e.error.message}`)
211
+ ].join("\n"),
212
+ location: {
213
+ // Error overlay does not support multi-line or ranges.
214
+ // Just point to the first line.
215
+ line: validationErrors[0].lines[0],
216
+ file: viteId
217
+ }
218
+ });
219
+ }
220
+ }
119
221
  function getUsedTags(markdocAst) {
120
222
  const tags = /* @__PURE__ */ new Set();
121
223
  const validationErrors = Markdoc.validate(markdocAst);
122
224
  for (const { error } of validationErrors) {
123
225
  if (error.id === "tag-undefined") {
124
- const [, tagName] = error.message.match(/Undefined tag: '(.*)'/) ?? [];
226
+ const [, tagName] = /Undefined tag: '(.*)'/.exec(error.message) ?? [];
125
227
  tags.add(tagName);
126
228
  }
127
229
  }
128
230
  return tags;
129
231
  }
130
- function getEntryInfo({ fileUrl, contents }) {
131
- const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
132
- return {
133
- data: parsed.data,
134
- body: parsed.content,
135
- slug: parsed.data.slug,
136
- rawData: parsed.matter
137
- };
138
- }
139
232
  async function emitOptimizedImages(nodeChildren, ctx) {
140
233
  for (const node of nodeChildren) {
141
- if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
142
- const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
143
- if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
144
- const src = await emitESMImage(
145
- resolved.id,
146
- ctx.pluginContext.meta.watchMode,
147
- ctx.pluginContext.emitFile
148
- );
149
- node.attributes.__optimizedSrc = src;
150
- } else {
151
- throw new MarkdocError({
152
- message: `Could not resolve image ${JSON.stringify(
153
- node.attributes.src
154
- )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
155
- });
234
+ let isComponent = node.type === "tag" && node.tag === "image" || node.type === "image" && ctx.hasDefaultImage;
235
+ if ((node.type === "image" || isComponent) && typeof node.attributes.src === "string") {
236
+ let attributeName = isComponent ? "src" : "__optimizedSrc";
237
+ if (shouldOptimizeImage(node.attributes.src)) {
238
+ const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
239
+ if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
240
+ const src = await emitImageMetadata(resolved.id, ctx.pluginContext.emitFile);
241
+ const fsPath = resolved.id;
242
+ if (src) {
243
+ if (ctx.astroConfig.output === "static") {
244
+ if (globalThis.astroAsset.referencedImages)
245
+ globalThis.astroAsset.referencedImages.add(fsPath);
246
+ }
247
+ node.attributes[attributeName] = { ...src, fsPath };
248
+ }
249
+ } else {
250
+ throw new MarkdocError({
251
+ message: `Could not resolve image ${JSON.stringify(
252
+ node.attributes.src
253
+ )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
254
+ });
255
+ }
256
+ } else if (isComponent) {
257
+ node.attributes[attributeName] = node.attributes.src;
156
258
  }
157
259
  }
158
260
  await emitOptimizedImages(node.children, ctx);
@@ -165,7 +267,7 @@ function getStringifiedImports(componentConfigMap, componentNamePrefix, root) {
165
267
  let stringifiedComponentImports = "";
166
268
  for (const [key, config] of Object.entries(componentConfigMap)) {
167
269
  const importName = config.namedExport ? `{ ${config.namedExport} as ${componentNamePrefix + toImportName(key)} }` : componentNamePrefix + toImportName(key);
168
- const resolvedPath = config.type === "local" ? new URL(config.path, root).pathname : config.path;
270
+ const resolvedPath = config.type === "local" ? fileURLToPath(new URL(config.path, root)) : config.path;
169
271
  stringifiedComponentImports += `import ${importName} from ${JSON.stringify(resolvedPath)};
170
272
  `;
171
273
  }
@@ -183,10 +285,9 @@ function getStringifiedMap(componentConfigMap, componentNamePrefix) {
183
285
  stringifiedComponentMap += "}";
184
286
  return stringifiedComponentMap;
185
287
  }
186
- function parseFrontmatter(fileContents, filePath) {
288
+ function safeParseFrontmatter(fileContents, filePath) {
187
289
  try {
188
- matter.clearCache();
189
- return matter(fileContents);
290
+ return parseFrontmatter(fileContents, { frontmatter: "empty-with-lines" });
190
291
  } catch (e) {
191
292
  if (e.name === "YAMLException") {
192
293
  const err = e;
@@ -1,3 +1,3 @@
1
1
  import type { ShikiConfig } from 'astro';
2
2
  import type { AstroMarkdocConfig } from '../config.js';
3
- export default function shiki({ langs, theme, wrap, }?: ShikiConfig): Promise<AstroMarkdocConfig>;
3
+ export default function shiki(config?: ShikiConfig): Promise<AstroMarkdocConfig>;
@@ -1,94 +1,23 @@
1
+ import { createShikiHighlighter } from "@astrojs/markdown-remark";
1
2
  import Markdoc from "@markdoc/markdoc";
2
3
  import { unescapeHTML } from "astro/runtime/server/index.js";
3
- import { getHighlighter } from "shiki";
4
- const compatThemes = {
5
- "material-darker": "material-theme-darker",
6
- "material-default": "material-theme",
7
- "material-lighter": "material-theme-lighter",
8
- "material-ocean": "material-theme-ocean",
9
- "material-palenight": "material-theme-palenight"
10
- };
11
- const normalizeTheme = (theme) => {
12
- if (typeof theme === "string") {
13
- return compatThemes[theme] || theme;
14
- } else if (compatThemes[theme.name]) {
15
- return { ...theme, name: compatThemes[theme.name] };
16
- } else {
17
- return theme;
18
- }
19
- };
20
- const ASTRO_COLOR_REPLACEMENTS = {
21
- "#000001": "var(--astro-code-color-text)",
22
- "#000002": "var(--astro-code-color-background)",
23
- "#000004": "var(--astro-code-token-constant)",
24
- "#000005": "var(--astro-code-token-string)",
25
- "#000006": "var(--astro-code-token-comment)",
26
- "#000007": "var(--astro-code-token-keyword)",
27
- "#000008": "var(--astro-code-token-parameter)",
28
- "#000009": "var(--astro-code-token-function)",
29
- "#000010": "var(--astro-code-token-string-expression)",
30
- "#000011": "var(--astro-code-token-punctuation)",
31
- "#000012": "var(--astro-code-token-link)"
32
- };
33
- const PRE_SELECTOR = /<pre class="(.*?)shiki(.*?)"/;
34
- const LINE_SELECTOR = /<span class="line"><span style="(.*?)">([\+|\-])/g;
35
- const INLINE_STYLE_SELECTOR = /style="(.*?)"/;
36
- const highlighterCache = /* @__PURE__ */ new Map();
37
- async function shiki({
38
- langs = [],
39
- theme = "github-dark",
40
- wrap = false
41
- } = {}) {
42
- theme = normalizeTheme(theme);
43
- const cacheID = typeof theme === "string" ? theme : theme.name;
44
- if (!highlighterCache.has(cacheID)) {
45
- highlighterCache.set(
46
- cacheID,
47
- await getHighlighter({ theme }).then((hl) => {
48
- hl.setColorReplacements(ASTRO_COLOR_REPLACEMENTS);
49
- return hl;
50
- })
51
- );
52
- }
53
- const highlighter = highlighterCache.get(cacheID);
54
- for (const lang of langs) {
55
- await highlighter.loadLanguage(lang);
56
- }
4
+ async function shiki(config) {
5
+ const highlighter = await createShikiHighlighter({
6
+ langs: config?.langs,
7
+ theme: config?.theme,
8
+ themes: config?.themes
9
+ });
57
10
  return {
58
11
  nodes: {
59
12
  fence: {
60
13
  attributes: Markdoc.nodes.fence.attributes,
61
- transform({ attributes }) {
62
- let lang;
63
- if (typeof attributes.language === "string") {
64
- const langExists = highlighter.getLoadedLanguages().includes(attributes.language);
65
- if (langExists) {
66
- lang = attributes.language;
67
- } else {
68
- console.warn(
69
- `[Shiki highlighter] The language "${attributes.language}" doesn't exist, falling back to plaintext.`
70
- );
71
- lang = "plaintext";
72
- }
73
- } else {
74
- lang = "plaintext";
75
- }
76
- let html = highlighter.codeToHtml(attributes.content, { lang });
77
- html = html.replace(PRE_SELECTOR, `<pre class="$1astro-code$2"`);
78
- if (attributes.language === "diff") {
79
- html = html.replace(
80
- LINE_SELECTOR,
81
- '<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
82
- );
83
- }
84
- if (wrap === false) {
85
- html = html.replace(INLINE_STYLE_SELECTOR, 'style="$1; overflow-x: auto;"');
86
- } else if (wrap === true) {
87
- html = html.replace(
88
- INLINE_STYLE_SELECTOR,
89
- 'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
90
- );
91
- }
14
+ async transform({ attributes }) {
15
+ const lang = typeof attributes.language === "string" ? attributes.language : "plaintext";
16
+ const html = await highlighter.codeToHtml(attributes.content, lang, {
17
+ wrap: config?.wrap,
18
+ defaultColor: config?.defaultColor,
19
+ transformers: config?.transformers
20
+ });
92
21
  return unescapeHTML(html);
93
22
  }
94
23
  }
@@ -1,10 +1,10 @@
1
1
  import { type Config as MarkdocConfig, type Schema } from '@markdoc/markdoc';
2
2
  import Slugger from 'github-slugger';
3
- type HeadingIdConfig = MarkdocConfig & {
3
+ interface HeadingIdConfig extends MarkdocConfig {
4
4
  ctx: {
5
5
  headingSlugger: Slugger;
6
6
  };
7
- };
7
+ }
8
8
  export declare const heading: Schema;
9
9
  export declare function setupHeadingConfig(): HeadingIdConfig;
10
10
  export {};
@@ -8,10 +8,7 @@ function getSlug(attributes, children, headingSlugger) {
8
8
  return attributes.id;
9
9
  }
10
10
  const textContent = attributes.content ?? getTextContent(children);
11
- let slug = headingSlugger.slug(textContent);
12
- if (slug.endsWith("-"))
13
- slug = slug.slice(0, -1);
14
- return slug;
11
+ return headingSlugger.slug(textContent);
15
12
  }
16
13
  const heading = {
17
14
  children: ["inline"],