@astrojs/markdown-remark 2.0.0 → 2.1.0

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,5 +1,5 @@
1
- @astrojs/markdown-remark:build: cache hit, replaying output 6cabe659bf0ade09
1
+ @astrojs/markdown-remark:build: cache hit, replaying output a9a85fd17740b5e6
2
2
  @astrojs/markdown-remark:build: 
3
- @astrojs/markdown-remark:build: > @astrojs/markdown-remark@2.0.0 build /home/runner/work/astro/astro/packages/markdown/remark
3
+ @astrojs/markdown-remark:build: > @astrojs/markdown-remark@2.1.0 build /home/runner/work/astro/astro/packages/markdown/remark
4
4
  @astrojs/markdown-remark:build: > astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json
5
5
  @astrojs/markdown-remark:build: 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @astrojs/markdown-remark
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#6344](https://github.com/withastro/astro/pull/6344) [`694918a56`](https://github.com/withastro/astro/commit/694918a56b01104831296be0c25456135a63c784) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add a new experimental flag (`experimental.assets`) to enable our new core Assets story.
8
+
9
+ This unlocks a few features:
10
+
11
+ - A new built-in image component and JavaScript API to transform and optimize images.
12
+ - Relative images with automatic optimization in Markdown.
13
+ - Support for validating assets using content collections.
14
+ - and more!
15
+
16
+ See [Assets (Experimental)](https://docs.astro.build/en/guides/assets/) on our docs site for more information on how to use this feature!
17
+
18
+ - [#6213](https://github.com/withastro/astro/pull/6213) [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated compilation settings to disable downlevelling for Node 14
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [[`fec583909`](https://github.com/withastro/astro/commit/fec583909ab62829dc0c1600e2387979365f2b94), [`b087b83fe`](https://github.com/withastro/astro/commit/b087b83fe266c431fe34a07d5c2293cc4ab011c6), [`694918a56`](https://github.com/withastro/astro/commit/694918a56b01104831296be0c25456135a63c784), [`a20610609`](https://github.com/withastro/astro/commit/a20610609863ae3b48afe96819b8f11ae4f414d5), [`a4a74ab70`](https://github.com/withastro/astro/commit/a4a74ab70cd2aa0d812a1f6b202c4e240a8913bf), [`75921b3cd`](https://github.com/withastro/astro/commit/75921b3cd916d439f6392c487c21532fde35ed13), [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808)]:
23
+ - astro@2.1.0
24
+ - @astrojs/prism@2.1.0
25
+
26
+ ## 2.0.1
27
+
28
+ ### Patch Changes
29
+
30
+ - [#5978](https://github.com/withastro/astro/pull/5978) [`7abb1e905`](https://github.com/withastro/astro/commit/7abb1e9056c4b4fd0abfced347df32a41cdfbf28) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fix MDX heading IDs generation when using a frontmatter reference
31
+
32
+ - Updated dependencies [[`b53e0717b`](https://github.com/withastro/astro/commit/b53e0717b7f6b042baaeec7f87999e99c76c031c), [`60b32d585`](https://github.com/withastro/astro/commit/60b32d58565d87e87573eb268408293fc28ec657), [`883e0cc29`](https://github.com/withastro/astro/commit/883e0cc29968d51ed6c7515be035a40b28bafdad), [`dabce6b8c`](https://github.com/withastro/astro/commit/dabce6b8c684f851c3535f8acead06cbef6dce2a), [`aedf23f85`](https://github.com/withastro/astro/commit/aedf23f8582e32a6b94b81ddba9b323831f2b22a)]:
33
+ - astro@2.0.2
34
+
3
35
  ## 2.0.0
4
36
 
5
37
  ### Major Changes
@@ -41,47 +73,47 @@
41
73
  - [#5785](https://github.com/withastro/astro/pull/5785) [`16107b6a1`](https://github.com/withastro/astro/commit/16107b6a10514ef1b563e585ec9add4b14f42b94) Thanks [@delucis](https://github.com/delucis)! - Drop support for legacy Astro-flavored Markdown
42
74
 
43
75
  - [#5684](https://github.com/withastro/astro/pull/5684) [`a9c292026`](https://github.com/withastro/astro/commit/a9c2920264e36cc5dc05f4adc1912187979edb0d) & [#5769](https://github.com/withastro/astro/pull/5769) [`93e633922`](https://github.com/withastro/astro/commit/93e633922c2e449df3bb2357b3683af1d3c0e07b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Refine Markdown and MDX configuration options for ease-of-use.
44
-
76
+
45
77
  - **Markdown**
46
78
 
47
- - **Replace the `extendDefaultPlugins` option** with a `gfm` boolean and a `smartypants` boolean. These are enabled by default, and can be disabled to remove GitHub-Flavored Markdown and SmartyPants.
48
-
49
- - Ensure GitHub-Flavored Markdown and SmartyPants are applied whether or not custom `remarkPlugins` or `rehypePlugins` are configured. If you want to apply custom plugins _and_ remove Astro's default plugins, manually set `gfm: false` and `smartypants: false` in your config.
79
+ - **Replace the `extendDefaultPlugins` option** with a `gfm` boolean and a `smartypants` boolean. These are enabled by default, and can be disabled to remove GitHub-Flavored Markdown and SmartyPants.
80
+
81
+ - Ensure GitHub-Flavored Markdown and SmartyPants are applied whether or not custom `remarkPlugins` or `rehypePlugins` are configured. If you want to apply custom plugins _and_ remove Astro's default plugins, manually set `gfm: false` and `smartypants: false` in your config.
50
82
 
51
83
  - **Migrate `extendDefaultPlugins` to `gfm` and `smartypants`**
52
84
 
53
- You may have disabled Astro's built-in plugins (GitHub-Flavored Markdown and Smartypants) with the `extendDefaultPlugins` option. This has now been split into 2 flags to disable each plugin individually:
85
+ You may have disabled Astro's built-in plugins (GitHub-Flavored Markdown and Smartypants) with the `extendDefaultPlugins` option. This has now been split into 2 flags to disable each plugin individually:
54
86
 
55
- - `markdown.gfm` to disable GitHub-Flavored Markdown
56
- - `markdown.smartypants` to disable SmartyPants
87
+ - `markdown.gfm` to disable GitHub-Flavored Markdown
88
+ - `markdown.smartypants` to disable SmartyPants
57
89
 
58
- ```diff
59
- // astro.config.mjs
60
- import { defineConfig } from 'astro/config';
90
+ ```diff
91
+ // astro.config.mjs
92
+ import { defineConfig } from 'astro/config';
61
93
 
62
- export default defineConfig({
63
- markdown: {
64
- - extendDefaultPlugins: false,
65
- + smartypants: false,
66
- + gfm: false,
67
- }
68
- });
69
- ```
94
+ export default defineConfig({
95
+ markdown: {
96
+ - extendDefaultPlugins: false,
97
+ + smartypants: false,
98
+ + gfm: false,
99
+ }
100
+ });
101
+ ```
70
102
 
71
- Additionally, applying remark and rehype plugins **no longer disables** `gfm` and `smartypants`. You will need to opt-out manually by setting `gfm` and `smartypants` to `false`.
103
+ Additionally, applying remark and rehype plugins **no longer disables** `gfm` and `smartypants`. You will need to opt-out manually by setting `gfm` and `smartypants` to `false`.
72
104
 
73
105
  - **MDX**
74
106
 
75
- - Support _all_ Markdown configuration options (except `drafts`) from your MDX integration config. This includes `syntaxHighlighting` and `shikiConfig` options to further customize the MDX renderer.
107
+ - Support _all_ Markdown configuration options (except `drafts`) from your MDX integration config. This includes `syntaxHighlighting` and `shikiConfig` options to further customize the MDX renderer.
76
108
 
77
- - Simplify `extendPlugins` to an `extendMarkdownConfig` option. MDX options will default to their equivalent in your Markdown config. By setting `extendMarkdownConfig` to false, you can "eject" to set your own syntax highlighting, plugins, and more.
109
+ - Simplify `extendPlugins` to an `extendMarkdownConfig` option. MDX options will default to their equivalent in your Markdown config. By setting `extendMarkdownConfig` to false, you can "eject" to set your own syntax highlighting, plugins, and more.
78
110
 
79
111
  - **Migrate MDX's `extendPlugins` to `extendMarkdownConfig`**
80
112
 
81
- You may have used the `extendPlugins` option to manage plugin defaults in MDX. This has been replaced by 3 flags:
113
+ You may have used the `extendPlugins` option to manage plugin defaults in MDX. This has been replaced by 3 flags:
82
114
 
83
- - `extendMarkdownConfig` (`true` by default) to toggle Markdown config inheritance. This replaces the `extendPlugins: 'markdown'` option.
84
- - `gfm` (`true` by default) and `smartypants` (`true` by default) to toggle GitHub-Flavored Markdown and SmartyPants in MDX. This replaces the `extendPlugins: 'defaults'` option.
115
+ - `extendMarkdownConfig` (`true` by default) to toggle Markdown config inheritance. This replaces the `extendPlugins: 'markdown'` option.
116
+ - `gfm` (`true` by default) and `smartypants` (`true` by default) to toggle GitHub-Flavored Markdown and SmartyPants in MDX. This replaces the `extendPlugins: 'defaults'` option.
85
117
 
86
118
  - [#5825](https://github.com/withastro/astro/pull/5825) [`52209ca2a`](https://github.com/withastro/astro/commit/52209ca2ad72a30854947dcb3a90ab4db0ac0a6f) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Baseline the experimental `contentCollections` flag. You're free to remove this from your astro config!
87
119
 
@@ -98,7 +130,6 @@
98
130
 
99
131
  This marks `astro` as a `peerDependency` of several packages that are already getting `major` version bumps. This is so we can more properly track the dependency between them and what version of Astro they are being used with.
100
132
 
101
-
102
133
  **Patch Changes**
103
134
 
104
135
  - [#5837](https://github.com/withastro/astro/pull/5837) [`12f65a4d5`](https://github.com/withastro/astro/commit/12f65a4d55e3fd2993c2f67b18794dd536280c69) Thanks [@giuseppelt](https://github.com/giuseppelt)! - fix shiki css class replace logic
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { toRemarkInitializeAstroData } from "./frontmatter-injection.js";
2
2
  import { loadPlugins } from "./load-plugins.js";
3
3
  import { rehypeHeadingIds } from "./rehype-collect-headings.js";
4
- import toRemarkContentRelImageError from "./remark-content-rel-image-error.js";
4
+ import toRemarkCollectImages from "./remark-collect-images.js";
5
5
  import remarkPrism from "./remark-prism.js";
6
6
  import scopedStyles from "./remark-scoped-styles.js";
7
7
  import remarkShiki from "./remark-shiki.js";
@@ -13,6 +13,7 @@ import markdownToHtml from "remark-rehype";
13
13
  import remarkSmartypants from "remark-smartypants";
14
14
  import { unified } from "unified";
15
15
  import { VFile } from "vfile";
16
+ import { rehypeImages } from "./rehype-images.js";
16
17
  import { rehypeHeadingIds as rehypeHeadingIds2 } from "./rehype-collect-headings.js";
17
18
  export * from "./types.js";
18
19
  const markdownConfigDefaults = {
@@ -28,6 +29,7 @@ const markdownConfigDefaults = {
28
29
  gfm: true,
29
30
  smartypants: true
30
31
  };
32
+ const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
31
33
  async function renderMarkdown(content, opts) {
32
34
  var _a;
33
35
  let {
@@ -39,32 +41,37 @@ async function renderMarkdown(content, opts) {
39
41
  remarkRehype = markdownConfigDefaults.remarkRehype,
40
42
  gfm = markdownConfigDefaults.gfm,
41
43
  smartypants = markdownConfigDefaults.smartypants,
42
- contentDir,
43
44
  frontmatter: userFrontmatter = {}
44
45
  } = opts;
45
46
  const input = new VFile({ value: content, path: fileURL });
46
47
  const scopedClassName = (_a = opts.$) == null ? void 0 : _a.scopedClassName;
47
48
  let parser = unified().use(markdown).use(toRemarkInitializeAstroData({ userFrontmatter })).use([]);
48
- if (gfm) {
49
- parser.use(remarkGfm);
50
- }
51
- if (smartypants) {
52
- parser.use(remarkSmartypants);
49
+ if (!isPerformanceBenchmark && gfm) {
50
+ if (gfm) {
51
+ parser.use(remarkGfm);
52
+ }
53
+ if (smartypants) {
54
+ parser.use(remarkSmartypants);
55
+ }
53
56
  }
54
57
  const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
55
58
  const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
56
59
  loadedRemarkPlugins.forEach(([plugin, pluginOpts]) => {
57
60
  parser.use([[plugin, pluginOpts]]);
58
61
  });
59
- if (scopedClassName) {
60
- parser.use([scopedStyles(scopedClassName)]);
61
- }
62
- if (syntaxHighlight === "shiki") {
63
- parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
64
- } else if (syntaxHighlight === "prism") {
65
- parser.use([remarkPrism(scopedClassName)]);
62
+ if (!isPerformanceBenchmark) {
63
+ if (scopedClassName) {
64
+ parser.use([scopedStyles(scopedClassName)]);
65
+ }
66
+ if (syntaxHighlight === "shiki") {
67
+ parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
68
+ } else if (syntaxHighlight === "prism") {
69
+ parser.use([remarkPrism(scopedClassName)]);
70
+ }
71
+ if (opts.experimentalAssets) {
72
+ parser.use([toRemarkCollectImages(opts.resolveImage)]);
73
+ }
66
74
  }
67
- parser.use([toRemarkContentRelImageError({ contentDir })]);
68
75
  parser.use([
69
76
  [
70
77
  markdownToHtml,
@@ -78,7 +85,13 @@ async function renderMarkdown(content, opts) {
78
85
  loadedRehypePlugins.forEach(([plugin, pluginOpts]) => {
79
86
  parser.use([[plugin, pluginOpts]]);
80
87
  });
81
- parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
88
+ if (opts.experimentalAssets) {
89
+ parser.use(rehypeImages(await opts.imageService, opts.assetsDir));
90
+ }
91
+ if (!isPerformanceBenchmark) {
92
+ parser.use([rehypeHeadingIds]);
93
+ }
94
+ parser.use([rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
82
95
  let vfile;
83
96
  try {
84
97
  vfile = await parser.process(input);
@@ -1,5 +1,6 @@
1
1
  import Slugger from "github-slugger";
2
2
  import { visit } from "unist-util-visit";
3
+ import { InvalidAstroDataError, safelyGetAstroData } from "./frontmatter-injection.js";
3
4
  const rawNodeTypes = /* @__PURE__ */ new Set(["text", "raw", "mdxTextExpression"]);
4
5
  const codeTagNames = /* @__PURE__ */ new Set(["code", "pre"]);
5
6
  function rehypeHeadingIds() {
@@ -7,6 +8,7 @@ function rehypeHeadingIds() {
7
8
  const headings = [];
8
9
  const slugger = new Slugger();
9
10
  const isMDX = isMDXFile(file);
11
+ const astroData = safelyGetAstroData(file.data);
10
12
  visit(tree, (node) => {
11
13
  if (node.type !== "element")
12
14
  return;
@@ -29,7 +31,17 @@ function rehypeHeadingIds() {
29
31
  }
30
32
  if (rawNodeTypes.has(child.type)) {
31
33
  if (isMDX || codeTagNames.has(parent.tagName)) {
32
- text += child.value;
34
+ let value = child.value;
35
+ if (isMdxTextExpression(child) && !(astroData instanceof InvalidAstroDataError)) {
36
+ const frontmatterPath = getMdxFrontmatterVariablePath(child);
37
+ if (Array.isArray(frontmatterPath) && frontmatterPath.length > 0) {
38
+ const frontmatterValue = getMdxFrontmatterVariableValue(astroData, frontmatterPath);
39
+ if (typeof frontmatterValue === "string") {
40
+ value = frontmatterValue;
41
+ }
42
+ }
43
+ }
44
+ text += value;
33
45
  } else {
34
46
  text += child.value.replace(/\{/g, "${");
35
47
  }
@@ -51,6 +63,37 @@ function isMDXFile(file) {
51
63
  var _a;
52
64
  return Boolean((_a = file.history[0]) == null ? void 0 : _a.endsWith(".mdx"));
53
65
  }
66
+ function getMdxFrontmatterVariablePath(node) {
67
+ var _a;
68
+ if (!((_a = node.data) == null ? void 0 : _a.estree) || node.data.estree.body.length !== 1)
69
+ return new Error();
70
+ const statement = node.data.estree.body[0];
71
+ if ((statement == null ? void 0 : statement.type) !== "ExpressionStatement" || statement.expression.type !== "MemberExpression")
72
+ return new Error();
73
+ let expression = statement.expression;
74
+ const expressionPath = [];
75
+ while (expression.type === "MemberExpression" && expression.property.type === (expression.computed ? "Literal" : "Identifier")) {
76
+ expressionPath.push(
77
+ expression.property.type === "Literal" ? String(expression.property.value) : expression.property.name
78
+ );
79
+ expression = expression.object;
80
+ }
81
+ if (expression.type !== "Identifier" || expression.name !== "frontmatter")
82
+ return new Error();
83
+ return expressionPath.reverse();
84
+ }
85
+ function getMdxFrontmatterVariableValue(astroData, path) {
86
+ let value = astroData.frontmatter;
87
+ for (const key of path) {
88
+ if (!value[key])
89
+ return void 0;
90
+ value = value[key];
91
+ }
92
+ return value;
93
+ }
94
+ function isMdxTextExpression(node) {
95
+ return node.type === "mdxTextExpression";
96
+ }
54
97
  export {
55
98
  rehypeHeadingIds
56
99
  };
@@ -0,0 +1,2 @@
1
+ import type { MarkdownVFile } from './types.js';
2
+ export declare function rehypeImages(imageService: any, assetsDir: URL | undefined): () => (tree: any, file: MarkdownVFile) => void;
@@ -0,0 +1,71 @@
1
+ import sizeOf from "image-size";
2
+ import { join as pathJoin } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { visit } from "unist-util-visit";
5
+ import { pathToFileURL } from "url";
6
+ function rehypeImages(imageService, assetsDir) {
7
+ return () => function(tree, file) {
8
+ visit(tree, (node) => {
9
+ var _a;
10
+ if (!assetsDir)
11
+ return;
12
+ if (node.type !== "element")
13
+ return;
14
+ if (node.tagName !== "img")
15
+ return;
16
+ if ((_a = node.properties) == null ? void 0 : _a.src) {
17
+ if (file.dirname) {
18
+ if (!isRelativePath(node.properties.src) && !isAliasedPath(node.properties.src))
19
+ return;
20
+ let fileURL;
21
+ if (isAliasedPath(node.properties.src)) {
22
+ fileURL = new URL(stripAliasPath(node.properties.src), assetsDir);
23
+ } else {
24
+ fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src));
25
+ }
26
+ const fileData = sizeOf(fileURLToPath(fileURL));
27
+ fileURL.searchParams.append("origWidth", fileData.width.toString());
28
+ fileURL.searchParams.append("origHeight", fileData.height.toString());
29
+ fileURL.searchParams.append("origFormat", fileData.type.toString());
30
+ let options = {
31
+ src: {
32
+ src: fileURL,
33
+ width: fileData.width,
34
+ height: fileData.height,
35
+ format: fileData.type
36
+ },
37
+ alt: node.properties.alt
38
+ };
39
+ const imageURL = imageService.getURL(options);
40
+ node.properties = Object.assign(node.properties, {
41
+ src: imageURL,
42
+ ...imageService.getHTMLAttributes !== void 0 ? imageService.getHTMLAttributes(options) : {}
43
+ });
44
+ }
45
+ }
46
+ });
47
+ };
48
+ }
49
+ function isAliasedPath(path) {
50
+ return path.startsWith("~/assets");
51
+ }
52
+ function stripAliasPath(path) {
53
+ return path.replace("~/assets/", "");
54
+ }
55
+ function isRelativePath(path) {
56
+ return startsWithDotDotSlash(path) || startsWithDotSlash(path);
57
+ }
58
+ function startsWithDotDotSlash(path) {
59
+ const c1 = path[0];
60
+ const c2 = path[1];
61
+ const c3 = path[2];
62
+ return c1 === "." && c2 === "." && c3 === "/";
63
+ }
64
+ function startsWithDotSlash(path) {
65
+ const c1 = path[0];
66
+ const c2 = path[1];
67
+ return c1 === "." && c2 === "/";
68
+ }
69
+ export {
70
+ rehypeImages
71
+ };
@@ -0,0 +1,4 @@
1
+ import type { VFile } from 'vfile';
2
+ declare type OptionalResolveImage = ((path: string) => Promise<string>) | undefined;
3
+ export default function toRemarkCollectImages(resolveImage: OptionalResolveImage): () => (tree: any, vfile: VFile) => Promise<void>;
4
+ export {};
@@ -0,0 +1,28 @@
1
+ import { visit } from "unist-util-visit";
2
+ function toRemarkCollectImages(resolveImage) {
3
+ return () => async function(tree, vfile) {
4
+ if (typeof (vfile == null ? void 0 : vfile.path) !== "string")
5
+ return;
6
+ const imagePaths = /* @__PURE__ */ new Set();
7
+ visit(tree, "image", function raiseError(node) {
8
+ imagePaths.add(node.url);
9
+ });
10
+ if (imagePaths.size === 0) {
11
+ vfile.data.imagePaths = [];
12
+ return;
13
+ } else if (resolveImage) {
14
+ const mapping = /* @__PURE__ */ new Map();
15
+ for (const path of Array.from(imagePaths)) {
16
+ const id = await resolveImage(path);
17
+ mapping.set(path, id);
18
+ }
19
+ visit(tree, "image", function raiseError(node) {
20
+ node.url = mapping.get(node.url);
21
+ });
22
+ }
23
+ vfile.data.imagePaths = Array.from(imagePaths);
24
+ };
25
+ }
26
+ export {
27
+ toRemarkCollectImages as default
28
+ };
package/dist/types.d.ts CHANGED
@@ -38,10 +38,12 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
38
38
  $?: {
39
39
  scopedClassName: string | null;
40
40
  };
41
- /** Used to prevent relative image imports from `src/content/` */
42
- contentDir: URL;
43
41
  /** Used for frontmatter injection plugins */
44
42
  frontmatter?: Record<string, any>;
43
+ experimentalAssets?: boolean;
44
+ imageService?: any;
45
+ assetsDir?: URL;
46
+ resolveImage?: (path: string) => Promise<string>;
45
47
  }
46
48
  export interface MarkdownHeading {
47
49
  depth: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/markdown-remark",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "author": "withastro",
6
6
  "license": "MIT",
@@ -17,11 +17,12 @@
17
17
  "./dist/internal.js": "./dist/internal.js"
18
18
  },
19
19
  "peerDependencies": {
20
- "astro": "^2.0.0"
20
+ "astro": "^2.1.0"
21
21
  },
22
22
  "dependencies": {
23
- "@astrojs/prism": "^2.0.0",
23
+ "@astrojs/prism": "^2.1.0",
24
24
  "github-slugger": "^1.4.0",
25
+ "image-size": "^1.0.2",
25
26
  "import-meta-resolve": "^2.1.0",
26
27
  "rehype-raw": "^6.1.1",
27
28
  "rehype-stringify": "^9.0.3",
@@ -36,13 +37,15 @@
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/chai": "^4.3.1",
40
+ "@types/estree": "^1.0.0",
39
41
  "@types/github-slugger": "^1.3.0",
40
42
  "@types/hast": "^2.3.4",
41
43
  "@types/mdast": "^3.0.10",
42
44
  "@types/mocha": "^9.1.1",
43
45
  "@types/unist": "^2.0.6",
44
- "astro-scripts": "0.0.10",
46
+ "astro-scripts": "0.0.14",
45
47
  "chai": "^4.3.6",
48
+ "mdast-util-mdx-expression": "^1.3.1",
46
49
  "mocha": "^9.2.2"
47
50
  },
48
51
  "scripts": {
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import type {
8
8
  import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
9
9
  import { loadPlugins } from './load-plugins.js';
10
10
  import { rehypeHeadingIds } from './rehype-collect-headings.js';
11
- import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
11
+ import toRemarkCollectImages from './remark-collect-images.js';
12
12
  import remarkPrism from './remark-prism.js';
13
13
  import scopedStyles from './remark-scoped-styles.js';
14
14
  import remarkShiki from './remark-shiki.js';
@@ -21,6 +21,7 @@ import markdownToHtml from 'remark-rehype';
21
21
  import remarkSmartypants from 'remark-smartypants';
22
22
  import { unified } from 'unified';
23
23
  import { VFile } from 'vfile';
24
+ import { rehypeImages } from './rehype-images.js';
24
25
 
25
26
  export { rehypeHeadingIds } from './rehype-collect-headings.js';
26
27
  export * from './types.js';
@@ -39,6 +40,9 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
39
40
  smartypants: true,
40
41
  };
41
42
 
43
+ // Skip nonessential plugins during performance benchmark runs
44
+ const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
45
+
42
46
  /** Shared utility for rendering markdown */
43
47
  export async function renderMarkdown(
44
48
  content: string,
@@ -53,7 +57,6 @@ export async function renderMarkdown(
53
57
  remarkRehype = markdownConfigDefaults.remarkRehype,
54
58
  gfm = markdownConfigDefaults.gfm,
55
59
  smartypants = markdownConfigDefaults.smartypants,
56
- contentDir,
57
60
  frontmatter: userFrontmatter = {},
58
61
  } = opts;
59
62
  const input = new VFile({ value: content, path: fileURL });
@@ -64,12 +67,13 @@ export async function renderMarkdown(
64
67
  .use(toRemarkInitializeAstroData({ userFrontmatter }))
65
68
  .use([]);
66
69
 
67
- if (gfm) {
68
- parser.use(remarkGfm);
69
- }
70
-
71
- if (smartypants) {
72
- parser.use(remarkSmartypants);
70
+ if (!isPerformanceBenchmark && gfm) {
71
+ if (gfm) {
72
+ parser.use(remarkGfm);
73
+ }
74
+ if (smartypants) {
75
+ parser.use(remarkSmartypants);
76
+ }
73
77
  }
74
78
 
75
79
  const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
@@ -79,18 +83,22 @@ export async function renderMarkdown(
79
83
  parser.use([[plugin, pluginOpts]]);
80
84
  });
81
85
 
82
- if (scopedClassName) {
83
- parser.use([scopedStyles(scopedClassName)]);
84
- }
86
+ if (!isPerformanceBenchmark) {
87
+ if (scopedClassName) {
88
+ parser.use([scopedStyles(scopedClassName)]);
89
+ }
85
90
 
86
- if (syntaxHighlight === 'shiki') {
87
- parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
88
- } else if (syntaxHighlight === 'prism') {
89
- parser.use([remarkPrism(scopedClassName)]);
90
- }
91
+ if (syntaxHighlight === 'shiki') {
92
+ parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
93
+ } else if (syntaxHighlight === 'prism') {
94
+ parser.use([remarkPrism(scopedClassName)]);
95
+ }
91
96
 
92
- // Apply later in case user plugins resolve relative image paths
93
- parser.use([toRemarkContentRelImageError({ contentDir })]);
97
+ if (opts.experimentalAssets) {
98
+ // Apply later in case user plugins resolve relative image paths
99
+ parser.use([toRemarkCollectImages(opts.resolveImage)]);
100
+ }
101
+ }
94
102
 
95
103
  parser.use([
96
104
  [
@@ -107,7 +115,14 @@ export async function renderMarkdown(
107
115
  parser.use([[plugin, pluginOpts]]);
108
116
  });
109
117
 
110
- parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
118
+ if (opts.experimentalAssets) {
119
+ parser.use(rehypeImages(await opts.imageService, opts.assetsDir));
120
+ }
121
+ if (!isPerformanceBenchmark) {
122
+ parser.use([rehypeHeadingIds]);
123
+ }
124
+
125
+ parser.use([rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
111
126
 
112
127
  let vfile: MarkdownVFile;
113
128
  try {
@@ -144,7 +159,7 @@ function prefixError(err: any, prefix: string) {
144
159
  const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
145
160
  try {
146
161
  wrappedError.stack = err.stack;
147
- // @ts-ignore
162
+ // @ts-expect-error
148
163
  wrappedError.cause = err;
149
164
  } catch (error) {
150
165
  // It's ok if we could not set the stack or cause - the message is the most important part
@@ -1,7 +1,11 @@
1
+ import { type Expression, type Super } from 'estree';
1
2
  import Slugger from 'github-slugger';
3
+ import { type MdxTextExpression } from 'mdast-util-mdx-expression';
4
+ import { type Node } from 'unist';
2
5
  import { visit } from 'unist-util-visit';
3
6
 
4
- import type { MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
7
+ import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';
8
+ import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
5
9
 
6
10
  const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
7
11
  const codeTagNames = new Set(['code', 'pre']);
@@ -11,6 +15,7 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
11
15
  const headings: MarkdownHeading[] = [];
12
16
  const slugger = new Slugger();
13
17
  const isMDX = isMDXFile(file);
18
+ const astroData = safelyGetAstroData(file.data);
14
19
  visit(tree, (node) => {
15
20
  if (node.type !== 'element') return;
16
21
  const { tagName } = node;
@@ -31,7 +36,17 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
31
36
  }
32
37
  if (rawNodeTypes.has(child.type)) {
33
38
  if (isMDX || codeTagNames.has(parent.tagName)) {
34
- text += child.value;
39
+ let value = child.value;
40
+ if (isMdxTextExpression(child) && !(astroData instanceof InvalidAstroDataError)) {
41
+ const frontmatterPath = getMdxFrontmatterVariablePath(child);
42
+ if (Array.isArray(frontmatterPath) && frontmatterPath.length > 0) {
43
+ const frontmatterValue = getMdxFrontmatterVariableValue(astroData, frontmatterPath);
44
+ if (typeof frontmatterValue === 'string') {
45
+ value = frontmatterValue;
46
+ }
47
+ }
48
+ }
49
+ text += value;
35
50
  } else {
36
51
  text += child.value.replace(/\{/g, '${');
37
52
  }
@@ -57,3 +72,58 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
57
72
  function isMDXFile(file: MarkdownVFile) {
58
73
  return Boolean(file.history[0]?.endsWith('.mdx'));
59
74
  }
75
+
76
+ /**
77
+ * Check if an ESTree entry is `frontmatter.*.VARIABLE`.
78
+ * If it is, return the variable path (i.e. `["*", ..., "VARIABLE"]`) minus the `frontmatter` prefix.
79
+ */
80
+ function getMdxFrontmatterVariablePath(node: MdxTextExpression): string[] | Error {
81
+ if (!node.data?.estree || node.data.estree.body.length !== 1) return new Error();
82
+
83
+ const statement = node.data.estree.body[0];
84
+
85
+ // Check for "[ANYTHING].[ANYTHING]".
86
+ if (statement?.type !== 'ExpressionStatement' || statement.expression.type !== 'MemberExpression')
87
+ return new Error();
88
+
89
+ let expression: Expression | Super = statement.expression;
90
+ const expressionPath: string[] = [];
91
+
92
+ // Traverse the expression, collecting the variable path.
93
+ while (
94
+ expression.type === 'MemberExpression' &&
95
+ expression.property.type === (expression.computed ? 'Literal' : 'Identifier')
96
+ ) {
97
+ expressionPath.push(
98
+ expression.property.type === 'Literal'
99
+ ? String(expression.property.value)
100
+ : expression.property.name
101
+ );
102
+
103
+ expression = expression.object;
104
+ }
105
+
106
+ // Check for "frontmatter.[ANYTHING]".
107
+ if (expression.type !== 'Identifier' || expression.name !== 'frontmatter') return new Error();
108
+
109
+ return expressionPath.reverse();
110
+ }
111
+
112
+ function getMdxFrontmatterVariableValue(astroData: MarkdownAstroData, path: string[]) {
113
+ let value: MdxFrontmatterVariableValue = astroData.frontmatter;
114
+
115
+ for (const key of path) {
116
+ if (!value[key]) return undefined;
117
+
118
+ value = value[key];
119
+ }
120
+
121
+ return value;
122
+ }
123
+
124
+ function isMdxTextExpression(node: Node): node is MdxTextExpression {
125
+ return node.type === 'mdxTextExpression';
126
+ }
127
+
128
+ type MdxFrontmatterVariableValue =
129
+ MarkdownAstroData['frontmatter'][keyof MarkdownAstroData['frontmatter']];
@@ -0,0 +1,78 @@
1
+ import sizeOf from 'image-size';
2
+ import { join as pathJoin } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { visit } from 'unist-util-visit';
5
+ import { pathToFileURL } from 'url';
6
+ import type { MarkdownVFile } from './types.js';
7
+
8
+ export function rehypeImages(imageService: any, assetsDir: URL | undefined) {
9
+ return () =>
10
+ function (tree: any, file: MarkdownVFile) {
11
+ visit(tree, (node) => {
12
+ if (!assetsDir) return;
13
+ if (node.type !== 'element') return;
14
+ if (node.tagName !== 'img') return;
15
+
16
+ if (node.properties?.src) {
17
+ if (file.dirname) {
18
+ if (!isRelativePath(node.properties.src) && !isAliasedPath(node.properties.src)) return;
19
+
20
+ let fileURL: URL;
21
+ if (isAliasedPath(node.properties.src)) {
22
+ fileURL = new URL(stripAliasPath(node.properties.src), assetsDir);
23
+ } else {
24
+ fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src));
25
+ }
26
+
27
+ const fileData = sizeOf(fileURLToPath(fileURL));
28
+ fileURL.searchParams.append('origWidth', fileData.width!.toString());
29
+ fileURL.searchParams.append('origHeight', fileData.height!.toString());
30
+ fileURL.searchParams.append('origFormat', fileData.type!.toString());
31
+
32
+ let options = {
33
+ src: {
34
+ src: fileURL,
35
+ width: fileData.width,
36
+ height: fileData.height,
37
+ format: fileData.type,
38
+ },
39
+ alt: node.properties.alt,
40
+ };
41
+
42
+ const imageURL = imageService.getURL(options);
43
+ node.properties = Object.assign(node.properties, {
44
+ src: imageURL,
45
+ ...(imageService.getHTMLAttributes !== undefined
46
+ ? imageService.getHTMLAttributes(options)
47
+ : {}),
48
+ });
49
+ }
50
+ }
51
+ });
52
+ };
53
+ }
54
+
55
+ function isAliasedPath(path: string) {
56
+ return path.startsWith('~/assets');
57
+ }
58
+
59
+ function stripAliasPath(path: string) {
60
+ return path.replace('~/assets/', '');
61
+ }
62
+
63
+ function isRelativePath(path: string) {
64
+ return startsWithDotDotSlash(path) || startsWithDotSlash(path);
65
+ }
66
+
67
+ function startsWithDotDotSlash(path: string) {
68
+ const c1 = path[0];
69
+ const c2 = path[1];
70
+ const c3 = path[2];
71
+ return c1 === '.' && c2 === '.' && c3 === '/';
72
+ }
73
+
74
+ function startsWithDotSlash(path: string) {
75
+ const c1 = path[0];
76
+ const c2 = path[1];
77
+ return c1 === '.' && c2 === '/';
78
+ }
@@ -0,0 +1,32 @@
1
+ import type { Image } from 'mdast';
2
+ import { visit } from 'unist-util-visit';
3
+ import type { VFile } from 'vfile';
4
+
5
+ type OptionalResolveImage = ((path: string) => Promise<string>) | undefined;
6
+
7
+ export default function toRemarkCollectImages(resolveImage: OptionalResolveImage) {
8
+ return () =>
9
+ async function (tree: any, vfile: VFile) {
10
+ if (typeof vfile?.path !== 'string') return;
11
+
12
+ const imagePaths = new Set<string>();
13
+ visit(tree, 'image', function raiseError(node: Image) {
14
+ imagePaths.add(node.url);
15
+ });
16
+ if (imagePaths.size === 0) {
17
+ vfile.data.imagePaths = [];
18
+ return;
19
+ } else if (resolveImage) {
20
+ const mapping = new Map<string, string>();
21
+ for (const path of Array.from(imagePaths)) {
22
+ const id = await resolveImage(path);
23
+ mapping.set(path, id);
24
+ }
25
+ visit(tree, 'image', function raiseError(node: Image) {
26
+ node.url = mapping.get(node.url)!;
27
+ });
28
+ }
29
+
30
+ vfile.data.imagePaths = Array.from(imagePaths);
31
+ };
32
+ }
package/src/types.ts CHANGED
@@ -58,10 +58,12 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
58
58
  $?: {
59
59
  scopedClassName: string | null;
60
60
  };
61
- /** Used to prevent relative image imports from `src/content/` */
62
- contentDir: URL;
63
61
  /** Used for frontmatter injection plugins */
64
62
  frontmatter?: Record<string, any>;
63
+ experimentalAssets?: boolean;
64
+ imageService?: any;
65
+ assetsDir?: URL;
66
+ resolveImage?: (path: string) => Promise<string>;
65
67
  }
66
68
 
67
69
  export interface MarkdownHeading {
package/tsconfig.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "include": ["src"],
4
4
  "compilerOptions": {
5
5
  "allowJs": true,
6
- "target": "ES2020",
7
- "module": "ES2020",
6
+ "target": "ES2021",
7
+ "module": "ES2022",
8
8
  "outDir": "./dist"
9
9
  }
10
10
  }
@@ -1,8 +0,0 @@
1
- import type { VFile } from 'vfile';
2
- /**
3
- * `src/content/` does not support relative image paths.
4
- * This plugin throws an error if any are found
5
- */
6
- export default function toRemarkContentRelImageError({ contentDir }: {
7
- contentDir: URL;
8
- }): () => (tree: any, vfile: VFile) => void;
@@ -1,41 +0,0 @@
1
- import { visit } from "unist-util-visit";
2
- import { pathToFileURL } from "url";
3
- function toRemarkContentRelImageError({ contentDir }) {
4
- return function remarkContentRelImageError() {
5
- return (tree, vfile) => {
6
- if (typeof (vfile == null ? void 0 : vfile.path) !== "string")
7
- return;
8
- const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
9
- if (!isContentFile)
10
- return;
11
- const relImagePaths = /* @__PURE__ */ new Set();
12
- visit(tree, "image", function raiseError(node) {
13
- if (isRelativePath(node.url)) {
14
- relImagePaths.add(node.url);
15
- }
16
- });
17
- if (relImagePaths.size === 0)
18
- return;
19
- const errorMessage = `Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files)
20
- ` + [...relImagePaths].map((path) => JSON.stringify(path)).join(",\n");
21
- throw errorMessage;
22
- };
23
- };
24
- }
25
- function isRelativePath(path) {
26
- return startsWithDotDotSlash(path) || startsWithDotSlash(path);
27
- }
28
- function startsWithDotDotSlash(path) {
29
- const c1 = path[0];
30
- const c2 = path[1];
31
- const c3 = path[2];
32
- return c1 === "." && c2 === "." && c3 === "/";
33
- }
34
- function startsWithDotSlash(path) {
35
- const c1 = path[0];
36
- const c2 = path[1];
37
- return c1 === "." && c2 === "/";
38
- }
39
- export {
40
- toRemarkContentRelImageError as default
41
- };
@@ -1,53 +0,0 @@
1
- import type { Image } from 'mdast';
2
- import { visit } from 'unist-util-visit';
3
- import { pathToFileURL } from 'url';
4
- import type { VFile } from 'vfile';
5
-
6
- /**
7
- * `src/content/` does not support relative image paths.
8
- * This plugin throws an error if any are found
9
- */
10
- export default function toRemarkContentRelImageError({ contentDir }: { contentDir: URL }) {
11
- return function remarkContentRelImageError() {
12
- return (tree: any, vfile: VFile) => {
13
- if (typeof vfile?.path !== 'string') return;
14
-
15
- const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
16
- if (!isContentFile) return;
17
-
18
- const relImagePaths = new Set<string>();
19
- visit(tree, 'image', function raiseError(node: Image) {
20
- if (isRelativePath(node.url)) {
21
- relImagePaths.add(node.url);
22
- }
23
- });
24
- if (relImagePaths.size === 0) return;
25
-
26
- const errorMessage =
27
- `Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files)\n` +
28
- [...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
29
-
30
- // Throw raw string to use `astro:markdown` default formatting
31
- throw errorMessage;
32
- };
33
- };
34
- }
35
-
36
- // Following utils taken from `packages/astro/src/core/path.ts`:
37
-
38
- function isRelativePath(path: string) {
39
- return startsWithDotDotSlash(path) || startsWithDotSlash(path);
40
- }
41
-
42
- function startsWithDotDotSlash(path: string) {
43
- const c1 = path[0];
44
- const c2 = path[1];
45
- const c3 = path[2];
46
- return c1 === '.' && c2 === '.' && c3 === '/';
47
- }
48
-
49
- function startsWithDotSlash(path: string) {
50
- const c1 = path[0];
51
- const c2 = path[1];
52
- return c1 === '.' && c2 === '/';
53
- }