@astrojs/markdown-remark 1.1.3 → 2.0.0-beta.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 724460aa5e72591a
1
+ @astrojs/markdown-remark:build: cache hit, replaying output 34663a00ad52a28a
2
2
  @astrojs/markdown-remark:build: 
3
- @astrojs/markdown-remark:build: > @astrojs/markdown-remark@1.1.3 build /home/runner/work/astro/astro/packages/markdown/remark
3
+ @astrojs/markdown-remark:build: > @astrojs/markdown-remark@2.0.0-beta.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,117 @@
1
1
  # @astrojs/markdown-remark
2
2
 
3
+ ## 2.0.0-beta.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#5687](https://github.com/withastro/astro/pull/5687) [`e2019be6f`](https://github.com/withastro/astro/commit/e2019be6ffa46fa33d92cfd346f9ecbe51bb7144) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Give remark and rehype plugins access to user frontmatter via frontmatter injection. This means `data.astro.frontmatter` is now the _complete_ Markdown or MDX document's frontmatter, rather than an empty object.
8
+
9
+ This allows plugin authors to modify existing frontmatter, or compute new properties based on other properties. For example, say you want to compute a full image URL based on an `imageSrc` slug in your document frontmatter:
10
+
11
+ ```ts
12
+ export function remarkInjectSocialImagePlugin() {
13
+ return function (tree, file) {
14
+ const { frontmatter } = file.data.astro;
15
+ frontmatter.socialImageSrc = new URL(frontmatter.imageSrc, 'https://my-blog.com/').pathname;
16
+ };
17
+ }
18
+ ```
19
+
20
+ #### Content Collections - new `remarkPluginFrontmatter` property
21
+
22
+ We have changed _inject_ frontmatter to _modify_ frontmatter in our docs to improve discoverability. This is based on support forum feedback, where "injection" is rarely the term used.
23
+
24
+ To reflect this, the `injectedFrontmatter` property has been renamed to `remarkPluginFrontmatter`. This should clarify this plugin is still separate from the `data` export Content Collections expose today.
25
+
26
+ #### Migration instructions
27
+
28
+ Plugin authors should now **check for user frontmatter when applying defaults.**
29
+
30
+ For example, say a remark plugin wants to apply a default `title` if none is present. Add a conditional to check if the property is present, and update if none exists:
31
+
32
+ ```diff
33
+ export function remarkInjectTitlePlugin() {
34
+ return function (tree, file) {
35
+ const { frontmatter } = file.data.astro;
36
+ + if (!frontmatter.title) {
37
+ frontmatter.title = 'Default title';
38
+ + }
39
+ }
40
+ }
41
+ ```
42
+
43
+ This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype.
44
+
45
+ - [#5684](https://github.com/withastro/astro/pull/5684) [`a9c292026`](https://github.com/withastro/astro/commit/a9c2920264e36cc5dc05f4adc1912187979edb0d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Refine Markdown and MDX configuration options for ease-of-use.
46
+
47
+ #### Markdown
48
+
49
+ - **Remove `remark-smartypants`** from Astro's default Markdown plugins.
50
+ - **Replace the `extendDefaultPlugins` option** with a simplified `gfm` boolean. This is enabled by default, and can be disabled to remove GitHub-Flavored Markdown.
51
+ - Ensure GitHub-Flavored Markdown is applied whether or not custom `remarkPlugins` or `rehypePlugins` are configured. If you want to apply custom plugins _and_ remove GFM, manually set `gfm: false` in your config.
52
+
53
+ #### MDX
54
+
55
+ - Support _all_ Markdown configuration options (except `drafts`) from your MDX integration config. This includes `syntaxHighlighting` and `shikiConfig` options to further customize the MDX renderer.
56
+ - Simplify `extendDefaults` 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.
57
+
58
+ #### Migration
59
+
60
+ To preserve your existing Markdown and MDX setup, you may need some configuration changes:
61
+
62
+ ##### Smartypants manual installation
63
+
64
+ [Smartypants](https://github.com/silvenon/remark-smartypants) has been removed from Astro's default setup. If you rely on this plugin, [install `remark-smartypants`](https://github.com/silvenon/remark-smartypants#installing) and apply to your `astro.config.*`:
65
+
66
+ ```diff
67
+ // astro.config.mjs
68
+ import { defineConfig } from 'astro/config';
69
+ + import smartypants from 'remark-smartypants';
70
+
71
+ export default defineConfig({
72
+ markdown: {
73
+ + remarkPlugins: [smartypants],
74
+ }
75
+ });
76
+ ```
77
+
78
+ ##### Migrate `extendDefaultPlugins` to `gfm`
79
+
80
+ You may have disabled Astro's built-in plugins (GitHub-Flavored Markdown and Smartypants) with the `extendDefaultPlugins` option. Since Smartypants has been removed, this has been renamed to `gfm`.
81
+
82
+ ```diff
83
+ // astro.config.mjs
84
+ import { defineConfig } from 'astro/config';
85
+
86
+ export default defineConfig({
87
+ markdown: {
88
+ - extendDefaultPlugins: false,
89
+ + gfm: false,
90
+ }
91
+ });
92
+ ```
93
+
94
+ Additionally, applying remark and rehype plugins **no longer disables** `gfm`. You will need to opt-out manually by setting `gfm` to `false`.
95
+
96
+ ##### Migrate MDX's `extendPlugins` to `extendMarkdownConfig`
97
+
98
+ You may have used the `extendPlugins` option to manage plugin defaults in MDX. This has been replaced by 2 flags:
99
+
100
+ - `extendMarkdownConfig` (`true` by default) to toggle Markdown config inheritance. This replaces the `extendPlugins: 'markdown'` option.
101
+ - `gfm` (`true` by default) to toggle GitHub-Flavored Markdown in MDX. This replaces the `extendPlugins: 'defaults'` option.
102
+
103
+ ## 1.2.0
104
+
105
+ ### Minor Changes
106
+
107
+ - [#5654](https://github.com/withastro/astro/pull/5654) [`2c65b433b`](https://github.com/withastro/astro/commit/2c65b433bf840a1bb93b0a1947df5949e33512ff) Thanks [@delucis](https://github.com/delucis)! - Refactor and export `rehypeHeadingIds` plugin
108
+
109
+ The `rehypeHeadingIds` plugin injects IDs for all headings in a Markdown document and can now also handle MDX inputs if needed. You can import and use this plugin if you need heading IDs to be injected _before_ other rehype plugins run.
110
+
111
+ ### Patch Changes
112
+
113
+ - [#5648](https://github.com/withastro/astro/pull/5648) [`853081d1c`](https://github.com/withastro/astro/commit/853081d1c857d8ad8a9634c37ed8fd123d32d241) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Prevent relative image paths in `src/content/`
114
+
3
115
  ## 1.1.3
4
116
 
5
117
  ### Patch Changes
@@ -242,7 +354,7 @@
242
354
 
243
355
  ### Minor Changes
244
356
 
245
- - [`e425f896`](https://github.com/withastro/astro/commit/e425f896b668d98033ad3b998b50c1f28bc7f6ee) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update config options to resepect [RFC0019](https://github.com/withastro/rfcs/blob/main/proposals/0019-config-finalization.md)
357
+ - [`e425f896`](https://github.com/withastro/astro/commit/e425f896b668d98033ad3b998b50c1f28bc7f6ee) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update config options to respect [RFC0019](https://github.com/withastro/rfcs/blob/main/proposals/0019-config-finalization.md)
246
358
 
247
359
  ## 0.7.0
248
360
 
@@ -0,0 +1,8 @@
1
+ import type { Data, VFile } from 'vfile';
2
+ import type { MarkdownAstroData } from './types.js';
3
+ export declare class InvalidAstroDataError extends TypeError {
4
+ }
5
+ export declare function safelyGetAstroData(vfileData: Data): MarkdownAstroData | InvalidAstroDataError;
6
+ export declare function toRemarkInitializeAstroData({ userFrontmatter, }: {
7
+ userFrontmatter: Record<string, any>;
8
+ }): () => (tree: any, vfile: VFile) => void;
@@ -0,0 +1,35 @@
1
+ function isValidAstroData(obj) {
2
+ if (typeof obj === "object" && obj !== null && obj.hasOwnProperty("frontmatter")) {
3
+ const { frontmatter } = obj;
4
+ try {
5
+ JSON.stringify(frontmatter);
6
+ } catch {
7
+ return false;
8
+ }
9
+ return typeof frontmatter === "object" && frontmatter !== null;
10
+ }
11
+ return false;
12
+ }
13
+ class InvalidAstroDataError extends TypeError {
14
+ }
15
+ function safelyGetAstroData(vfileData) {
16
+ const { astro } = vfileData;
17
+ if (!astro || !isValidAstroData(astro)) {
18
+ return new InvalidAstroDataError();
19
+ }
20
+ return astro;
21
+ }
22
+ function toRemarkInitializeAstroData({
23
+ userFrontmatter
24
+ }) {
25
+ return () => function(tree, vfile) {
26
+ if (!vfile.data.astro) {
27
+ vfile.data.astro = { frontmatter: userFrontmatter };
28
+ }
29
+ };
30
+ }
31
+ export {
32
+ InvalidAstroDataError,
33
+ safelyGetAstroData,
34
+ toRemarkInitializeAstroData
35
+ };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
1
+ import type { AstroMarkdownOptions, MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
2
+ export { rehypeHeadingIds } from './rehype-collect-headings.js';
2
3
  export * from './types.js';
3
- export declare const DEFAULT_REMARK_PLUGINS: string[];
4
- export declare const DEFAULT_REHYPE_PLUGINS: never[];
4
+ export declare const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'>;
5
5
  /** Shared utility for rendering markdown */
6
6
  export declare function renderMarkdown(content: string, opts: MarkdownRenderingOptions): Promise<MarkdownRenderingResult>;
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
+ import { toRemarkInitializeAstroData } from "./frontmatter-injection.js";
1
2
  import { loadPlugins } from "./load-plugins.js";
2
- import createCollectHeadings from "./rehype-collect-headings.js";
3
+ import { rehypeHeadingIds } from "./rehype-collect-headings.js";
3
4
  import rehypeEscape from "./rehype-escape.js";
4
5
  import rehypeExpressions from "./rehype-expressions.js";
5
6
  import rehypeIslands from "./rehype-islands.js";
6
7
  import rehypeJsx from "./rehype-jsx.js";
8
+ import toRemarkContentRelImageError from "./remark-content-rel-image-error.js";
7
9
  import remarkEscape from "./remark-escape.js";
8
- import { remarkInitializeAstroData } from "./remark-initialize-astro-data.js";
9
10
  import remarkMarkAndUnravel from "./remark-mark-and-unravel.js";
10
11
  import remarkMdxish from "./remark-mdxish.js";
11
12
  import remarkPrism from "./remark-prism.js";
@@ -14,32 +15,45 @@ import remarkShiki from "./remark-shiki.js";
14
15
  import remarkUnwrap from "./remark-unwrap.js";
15
16
  import rehypeRaw from "rehype-raw";
16
17
  import rehypeStringify from "rehype-stringify";
18
+ import remarkGfm from "remark-gfm";
17
19
  import markdown from "remark-parse";
18
20
  import markdownToHtml from "remark-rehype";
19
21
  import { unified } from "unified";
20
22
  import { VFile } from "vfile";
23
+ import { rehypeHeadingIds as rehypeHeadingIds2 } from "./rehype-collect-headings.js";
21
24
  export * from "./types.js";
22
- const DEFAULT_REMARK_PLUGINS = ["remark-gfm", "remark-smartypants"];
23
- const DEFAULT_REHYPE_PLUGINS = [];
25
+ const markdownConfigDefaults = {
26
+ syntaxHighlight: "shiki",
27
+ shikiConfig: {
28
+ langs: [],
29
+ theme: "github-dark",
30
+ wrap: false
31
+ },
32
+ remarkPlugins: [],
33
+ rehypePlugins: [],
34
+ remarkRehype: {},
35
+ gfm: true
36
+ };
24
37
  async function renderMarkdown(content, opts) {
25
38
  var _a;
26
39
  let {
27
40
  fileURL,
28
- syntaxHighlight = "shiki",
29
- shikiConfig = {},
30
- remarkPlugins = [],
31
- rehypePlugins = [],
32
- remarkRehype = {},
33
- extendDefaultPlugins = false,
34
- isAstroFlavoredMd = false
41
+ syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
42
+ shikiConfig = markdownConfigDefaults.shikiConfig,
43
+ remarkPlugins = markdownConfigDefaults.remarkPlugins,
44
+ rehypePlugins = markdownConfigDefaults.rehypePlugins,
45
+ remarkRehype = markdownConfigDefaults.remarkRehype,
46
+ gfm = markdownConfigDefaults.gfm,
47
+ isAstroFlavoredMd = false,
48
+ isExperimentalContentCollections = false,
49
+ contentDir,
50
+ frontmatter: userFrontmatter = {}
35
51
  } = opts;
36
52
  const input = new VFile({ value: content, path: fileURL });
37
53
  const scopedClassName = (_a = opts.$) == null ? void 0 : _a.scopedClassName;
38
- const { headings, rehypeCollectHeadings } = createCollectHeadings();
39
- let parser = unified().use(markdown).use(remarkInitializeAstroData).use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
40
- if (extendDefaultPlugins || remarkPlugins.length === 0 && rehypePlugins.length === 0) {
41
- remarkPlugins = [...DEFAULT_REMARK_PLUGINS, ...remarkPlugins];
42
- rehypePlugins = [...DEFAULT_REHYPE_PLUGINS, ...rehypePlugins];
54
+ let parser = unified().use(markdown).use(toRemarkInitializeAstroData({ userFrontmatter })).use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
55
+ if (gfm) {
56
+ parser.use(remarkGfm);
43
57
  }
44
58
  const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
45
59
  const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
@@ -54,6 +68,9 @@ async function renderMarkdown(content, opts) {
54
68
  } else if (syntaxHighlight === "prism") {
55
69
  parser.use([remarkPrism(scopedClassName)]);
56
70
  }
71
+ if (isExperimentalContentCollections) {
72
+ parser.use([toRemarkContentRelImageError({ contentDir })]);
73
+ }
57
74
  parser.use([
58
75
  [
59
76
  markdownToHtml,
@@ -74,7 +91,7 @@ async function renderMarkdown(content, opts) {
74
91
  parser.use([[plugin, pluginOpts]]);
75
92
  });
76
93
  parser.use(
77
- isAstroFlavoredMd ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings] : [rehypeCollectHeadings, rehypeRaw]
94
+ isAstroFlavoredMd ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds] : [rehypeHeadingIds, rehypeRaw]
78
95
  ).use(rehypeStringify, { allowDangerousHtml: true });
79
96
  let vfile;
80
97
  try {
@@ -84,6 +101,7 @@ async function renderMarkdown(content, opts) {
84
101
  console.error(err);
85
102
  throw err;
86
103
  }
104
+ const headings = (vfile == null ? void 0 : vfile.data.__astroHeadings) || [];
87
105
  return {
88
106
  metadata: { headings, source: content, html: String(vfile.value) },
89
107
  code: String(vfile.value),
@@ -108,7 +126,7 @@ ${err.message}`;
108
126
  return wrappedError;
109
127
  }
110
128
  export {
111
- DEFAULT_REHYPE_PLUGINS,
112
- DEFAULT_REMARK_PLUGINS,
129
+ markdownConfigDefaults,
130
+ rehypeHeadingIds2 as rehypeHeadingIds,
113
131
  renderMarkdown
114
132
  };
@@ -0,0 +1 @@
1
+ export { InvalidAstroDataError, safelyGetAstroData, toRemarkInitializeAstroData, } from './frontmatter-injection.js';
@@ -0,0 +1,10 @@
1
+ import {
2
+ InvalidAstroDataError,
3
+ safelyGetAstroData,
4
+ toRemarkInitializeAstroData
5
+ } from "./frontmatter-injection.js";
6
+ export {
7
+ InvalidAstroDataError,
8
+ safelyGetAstroData,
9
+ toRemarkInitializeAstroData
10
+ };
@@ -1,5 +1,2 @@
1
- import type { MarkdownHeading, RehypePlugin } from './types.js';
2
- export default function createCollectHeadings(): {
3
- headings: MarkdownHeading[];
4
- rehypeCollectHeadings: () => ReturnType<RehypePlugin>;
5
- };
1
+ import type { RehypePlugin } from './types.js';
2
+ export declare function rehypeHeadingIds(): ReturnType<RehypePlugin>;
@@ -1,64 +1,66 @@
1
1
  import Slugger from "github-slugger";
2
2
  import { toHtml } from "hast-util-to-html";
3
3
  import { visit } from "unist-util-visit";
4
- function createCollectHeadings() {
5
- const headings = [];
6
- const slugger = new Slugger();
7
- function rehypeCollectHeadings() {
8
- return function(tree) {
9
- visit(tree, (node) => {
10
- if (node.type !== "element")
4
+ const rawNodeTypes = /* @__PURE__ */ new Set(["text", "raw", "mdxTextExpression"]);
5
+ const codeTagNames = /* @__PURE__ */ new Set(["code", "pre"]);
6
+ function rehypeHeadingIds() {
7
+ return function(tree, file) {
8
+ const headings = [];
9
+ const slugger = new Slugger();
10
+ const isMDX = isMDXFile(file);
11
+ visit(tree, (node) => {
12
+ if (node.type !== "element")
13
+ return;
14
+ const { tagName } = node;
15
+ if (tagName[0] !== "h")
16
+ return;
17
+ const [_, level] = tagName.match(/h([0-6])/) ?? [];
18
+ if (!level)
19
+ return;
20
+ const depth = Number.parseInt(level);
21
+ let text = "";
22
+ let isJSX = false;
23
+ visit(node, (child, __, parent) => {
24
+ if (child.type === "element" || parent == null) {
11
25
  return;
12
- const { tagName } = node;
13
- if (tagName[0] !== "h")
14
- return;
15
- const [_, level] = tagName.match(/h([0-6])/) ?? [];
16
- if (!level)
17
- return;
18
- const depth = Number.parseInt(level);
19
- let text = "";
20
- let isJSX = false;
21
- visit(node, (child, __, parent) => {
22
- if (child.type === "element" || parent == null) {
26
+ }
27
+ if (child.type === "raw") {
28
+ if (child.value.match(/^\n?<.*>\n?$/)) {
23
29
  return;
24
30
  }
25
- if (child.type === "raw") {
26
- if (child.value.match(/^\n?<.*>\n?$/)) {
27
- return;
28
- }
29
- }
30
- if (child.type === "text" || child.type === "raw") {
31
- if ((/* @__PURE__ */ new Set(["code", "pre"])).has(parent.tagName)) {
32
- text += child.value;
33
- } else {
34
- text += child.value.replace(/\{/g, "${");
35
- isJSX = isJSX || child.value.includes("{");
36
- }
37
- }
38
- });
39
- node.properties = node.properties || {};
40
- if (typeof node.properties.id !== "string") {
41
- if (isJSX) {
42
- const raw = toHtml(node.children, { allowDangerousHtml: true }).replace(/\n(<)/g, "<").replace(/(>)\n/g, ">");
43
- node.properties.id = `$$slug(\`${text}\`)`;
44
- node.type = "raw";
45
- node.value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
31
+ }
32
+ if (rawNodeTypes.has(child.type)) {
33
+ if (isMDX || codeTagNames.has(parent.tagName)) {
34
+ text += child.value;
46
35
  } else {
47
- let slug = slugger.slug(text);
48
- if (slug.endsWith("-"))
49
- slug = slug.slice(0, -1);
50
- node.properties.id = slug;
36
+ text += child.value.replace(/\{/g, "${");
37
+ isJSX = isJSX || child.value.includes("{");
51
38
  }
52
39
  }
53
- headings.push({ depth, slug: node.properties.id, text });
54
40
  });
55
- };
56
- }
57
- return {
58
- headings,
59
- rehypeCollectHeadings
41
+ node.properties = node.properties || {};
42
+ if (typeof node.properties.id !== "string") {
43
+ if (isJSX) {
44
+ const raw = toHtml(node.children, { allowDangerousHtml: true }).replace(/\n(<)/g, "<").replace(/(>)\n/g, ">");
45
+ node.properties.id = `$$slug(\`${text}\`)`;
46
+ node.type = "raw";
47
+ node.value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
48
+ } else {
49
+ let slug = slugger.slug(text);
50
+ if (slug.endsWith("-"))
51
+ slug = slug.slice(0, -1);
52
+ node.properties.id = slug;
53
+ }
54
+ }
55
+ headings.push({ depth, slug: node.properties.id, text });
56
+ });
57
+ file.data.__astroHeadings = headings;
60
58
  };
61
59
  }
60
+ function isMDXFile(file) {
61
+ var _a;
62
+ return Boolean((_a = file.history[0]) == null ? void 0 : _a.endsWith(".mdx"));
63
+ }
62
64
  export {
63
- createCollectHeadings as default
65
+ rehypeHeadingIds
64
66
  };
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,39 @@
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
+ const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
7
+ if (!isContentFile)
8
+ return;
9
+ const relImagePaths = /* @__PURE__ */ new Set();
10
+ visit(tree, "image", function raiseError(node) {
11
+ if (isRelativePath(node.url)) {
12
+ relImagePaths.add(node.url);
13
+ }
14
+ });
15
+ if (relImagePaths.size === 0)
16
+ return;
17
+ 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)
18
+ ` + [...relImagePaths].map((path) => JSON.stringify(path)).join(",\n");
19
+ throw errorMessage;
20
+ };
21
+ };
22
+ }
23
+ function isRelativePath(path) {
24
+ return startsWithDotDotSlash(path) || startsWithDotSlash(path);
25
+ }
26
+ function startsWithDotDotSlash(path) {
27
+ const c1 = path[0];
28
+ const c2 = path[1];
29
+ const c3 = path[2];
30
+ return c1 === "." && c2 === "." && c3 === "/";
31
+ }
32
+ function startsWithDotSlash(path) {
33
+ const c1 = path[0];
34
+ const c2 = path[1];
35
+ return c1 === "." && c2 === "/";
36
+ }
37
+ export {
38
+ toRemarkContentRelImageError as default
39
+ };
package/dist/types.d.ts CHANGED
@@ -5,14 +5,16 @@ import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
5
5
  import type * as unified from 'unified';
6
6
  import type { VFile } from 'vfile';
7
7
  export type { Node } from 'unist';
8
+ export declare type MarkdownAstroData = {
9
+ frontmatter: Record<string, any>;
10
+ };
8
11
  export declare type RemarkPlugin<PluginParameters extends any[] = any[]> = unified.Plugin<PluginParameters, mdast.Root>;
9
12
  export declare type RemarkPlugins = (string | [string, any] | RemarkPlugin | [RemarkPlugin, any])[];
10
13
  export declare type RehypePlugin<PluginParameters extends any[] = any[]> = unified.Plugin<PluginParameters, hast.Root>;
11
14
  export declare type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[];
12
15
  export declare type RemarkRehype = Omit<RemarkRehypeOptions, 'handlers' | 'unknownHandler'> & {
13
- handlers: typeof Handlers;
14
- } & {
15
- handler: typeof Handler;
16
+ handlers?: typeof Handlers;
17
+ handler?: typeof Handler;
16
18
  };
17
19
  export interface ShikiConfig {
18
20
  langs?: ILanguageRegistration[];
@@ -20,14 +22,13 @@ export interface ShikiConfig {
20
22
  wrap?: boolean | null;
21
23
  }
22
24
  export interface AstroMarkdownOptions {
23
- mode?: 'md' | 'mdx';
24
25
  drafts?: boolean;
25
26
  syntaxHighlight?: 'shiki' | 'prism' | false;
26
27
  shikiConfig?: ShikiConfig;
27
28
  remarkPlugins?: RemarkPlugins;
28
29
  rehypePlugins?: RehypePlugins;
29
30
  remarkRehype?: RemarkRehype;
30
- extendDefaultPlugins?: boolean;
31
+ gfm?: boolean;
31
32
  }
32
33
  export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
33
34
  /** @internal */
@@ -37,6 +38,12 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
37
38
  scopedClassName: string | null;
38
39
  };
39
40
  isAstroFlavoredMd?: boolean;
41
+ /** Used to prevent relative image imports from `src/content/` */
42
+ isExperimentalContentCollections?: boolean;
43
+ /** Used to prevent relative image imports from `src/content/` */
44
+ contentDir: URL;
45
+ /** Used for frontmatter injection plugins */
46
+ frontmatter?: Record<string, any>;
40
47
  }
41
48
  export interface MarkdownHeading {
42
49
  depth: number;
@@ -48,6 +55,11 @@ export interface MarkdownMetadata {
48
55
  source: string;
49
56
  html: string;
50
57
  }
58
+ export interface MarkdownVFile extends VFile {
59
+ data: {
60
+ __astroHeadings?: MarkdownHeading[];
61
+ };
62
+ }
51
63
  export interface MarkdownRenderingResult {
52
64
  metadata: MarkdownMetadata;
53
65
  vfile: VFile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/markdown-remark",
3
- "version": "1.1.3",
3
+ "version": "2.0.0-beta.0",
4
4
  "type": "module",
5
5
  "author": "withastro",
6
6
  "license": "MIT",
@@ -13,7 +13,8 @@
13
13
  "homepage": "https://astro.build",
14
14
  "main": "./dist/index.js",
15
15
  "exports": {
16
- ".": "./dist/index.js"
16
+ ".": "./dist/index.js",
17
+ "./dist/internal.js": "./dist/internal.js"
17
18
  },
18
19
  "dependencies": {
19
20
  "@astrojs/micromark-extension-mdx-jsx": "^1.0.3",
@@ -34,7 +35,6 @@
34
35
  "remark-gfm": "^3.0.1",
35
36
  "remark-parse": "^10.0.1",
36
37
  "remark-rehype": "^10.1.0",
37
- "remark-smartypants": "^2.0.0",
38
38
  "shiki": "^0.11.1",
39
39
  "unified": "^10.1.2",
40
40
  "unist-util-map": "^3.1.1",
@@ -48,7 +48,7 @@
48
48
  "@types/mdast": "^3.0.10",
49
49
  "@types/mocha": "^9.1.1",
50
50
  "@types/unist": "^2.0.6",
51
- "astro-scripts": "0.0.8",
51
+ "astro-scripts": "0.0.9",
52
52
  "chai": "^4.3.6",
53
53
  "micromark-util-types": "^1.0.2",
54
54
  "mocha": "^9.2.2"
@@ -0,0 +1,41 @@
1
+ import type { Data, VFile } from 'vfile';
2
+ import type { MarkdownAstroData } from './types.js';
3
+
4
+ function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
5
+ if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) {
6
+ const { frontmatter } = obj as any;
7
+ try {
8
+ // ensure frontmatter is JSON-serializable
9
+ JSON.stringify(frontmatter);
10
+ } catch {
11
+ return false;
12
+ }
13
+ return typeof frontmatter === 'object' && frontmatter !== null;
14
+ }
15
+ return false;
16
+ }
17
+
18
+ export class InvalidAstroDataError extends TypeError {}
19
+
20
+ export function safelyGetAstroData(vfileData: Data): MarkdownAstroData | InvalidAstroDataError {
21
+ const { astro } = vfileData;
22
+
23
+ if (!astro || !isValidAstroData(astro)) {
24
+ return new InvalidAstroDataError();
25
+ }
26
+
27
+ return astro;
28
+ }
29
+
30
+ export function toRemarkInitializeAstroData({
31
+ userFrontmatter,
32
+ }: {
33
+ userFrontmatter: Record<string, any>;
34
+ }) {
35
+ return () =>
36
+ function (tree: any, vfile: VFile) {
37
+ if (!vfile.data.astro) {
38
+ vfile.data.astro = { frontmatter: userFrontmatter };
39
+ }
40
+ };
41
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,19 @@
1
- import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
2
-
1
+ import type {
2
+ AstroMarkdownOptions,
3
+ MarkdownRenderingOptions,
4
+ MarkdownRenderingResult,
5
+ MarkdownVFile,
6
+ } from './types';
7
+
8
+ import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
3
9
  import { loadPlugins } from './load-plugins.js';
4
- import createCollectHeadings from './rehype-collect-headings.js';
10
+ import { rehypeHeadingIds } from './rehype-collect-headings.js';
5
11
  import rehypeEscape from './rehype-escape.js';
6
12
  import rehypeExpressions from './rehype-expressions.js';
7
13
  import rehypeIslands from './rehype-islands.js';
8
14
  import rehypeJsx from './rehype-jsx.js';
15
+ import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
9
16
  import remarkEscape from './remark-escape.js';
10
- import { remarkInitializeAstroData } from './remark-initialize-astro-data.js';
11
17
  import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
12
18
  import remarkMdxish from './remark-mdxish.js';
13
19
  import remarkPrism from './remark-prism.js';
@@ -17,15 +23,27 @@ import remarkUnwrap from './remark-unwrap.js';
17
23
 
18
24
  import rehypeRaw from 'rehype-raw';
19
25
  import rehypeStringify from 'rehype-stringify';
26
+ import remarkGfm from 'remark-gfm';
20
27
  import markdown from 'remark-parse';
21
28
  import markdownToHtml from 'remark-rehype';
22
29
  import { unified } from 'unified';
23
30
  import { VFile } from 'vfile';
24
31
 
32
+ export { rehypeHeadingIds } from './rehype-collect-headings.js';
25
33
  export * from './types.js';
26
34
 
27
- export const DEFAULT_REMARK_PLUGINS = ['remark-gfm', 'remark-smartypants'];
28
- export const DEFAULT_REHYPE_PLUGINS = [];
35
+ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
36
+ syntaxHighlight: 'shiki',
37
+ shikiConfig: {
38
+ langs: [],
39
+ theme: 'github-dark',
40
+ wrap: false,
41
+ },
42
+ remarkPlugins: [],
43
+ rehypePlugins: [],
44
+ remarkRehype: {},
45
+ gfm: true,
46
+ };
29
47
 
30
48
  /** Shared utility for rendering markdown */
31
49
  export async function renderMarkdown(
@@ -34,26 +52,27 @@ export async function renderMarkdown(
34
52
  ): Promise<MarkdownRenderingResult> {
35
53
  let {
36
54
  fileURL,
37
- syntaxHighlight = 'shiki',
38
- shikiConfig = {},
39
- remarkPlugins = [],
40
- rehypePlugins = [],
41
- remarkRehype = {},
42
- extendDefaultPlugins = false,
55
+ syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
56
+ shikiConfig = markdownConfigDefaults.shikiConfig,
57
+ remarkPlugins = markdownConfigDefaults.remarkPlugins,
58
+ rehypePlugins = markdownConfigDefaults.rehypePlugins,
59
+ remarkRehype = markdownConfigDefaults.remarkRehype,
60
+ gfm = markdownConfigDefaults.gfm,
43
61
  isAstroFlavoredMd = false,
62
+ isExperimentalContentCollections = false,
63
+ contentDir,
64
+ frontmatter: userFrontmatter = {},
44
65
  } = opts;
45
66
  const input = new VFile({ value: content, path: fileURL });
46
67
  const scopedClassName = opts.$?.scopedClassName;
47
- const { headings, rehypeCollectHeadings } = createCollectHeadings();
48
68
 
49
69
  let parser = unified()
50
70
  .use(markdown)
51
- .use(remarkInitializeAstroData)
71
+ .use(toRemarkInitializeAstroData({ userFrontmatter }))
52
72
  .use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
53
73
 
54
- if (extendDefaultPlugins || (remarkPlugins.length === 0 && rehypePlugins.length === 0)) {
55
- remarkPlugins = [...DEFAULT_REMARK_PLUGINS, ...remarkPlugins];
56
- rehypePlugins = [...DEFAULT_REHYPE_PLUGINS, ...rehypePlugins];
74
+ if (gfm) {
75
+ parser.use(remarkGfm);
57
76
  }
58
77
 
59
78
  const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
@@ -73,6 +92,11 @@ export async function renderMarkdown(
73
92
  parser.use([remarkPrism(scopedClassName)]);
74
93
  }
75
94
 
95
+ // Apply later in case user plugins resolve relative image paths
96
+ if (isExperimentalContentCollections) {
97
+ parser.use([toRemarkContentRelImageError({ contentDir })]);
98
+ }
99
+
76
100
  parser.use([
77
101
  [
78
102
  markdownToHtml as any,
@@ -99,12 +123,12 @@ export async function renderMarkdown(
99
123
  parser
100
124
  .use(
101
125
  isAstroFlavoredMd
102
- ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings]
103
- : [rehypeCollectHeadings, rehypeRaw]
126
+ ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds]
127
+ : [rehypeHeadingIds, rehypeRaw]
104
128
  )
105
129
  .use(rehypeStringify, { allowDangerousHtml: true });
106
130
 
107
- let vfile: VFile;
131
+ let vfile: MarkdownVFile;
108
132
  try {
109
133
  vfile = await parser.process(input);
110
134
  } catch (err) {
@@ -116,6 +140,7 @@ export async function renderMarkdown(
116
140
  throw err;
117
141
  }
118
142
 
143
+ const headings = vfile?.data.__astroHeadings || [];
119
144
  return {
120
145
  metadata: { headings, source: content, html: String(vfile.value) },
121
146
  code: String(vfile.value),
@@ -0,0 +1,5 @@
1
+ export {
2
+ InvalidAstroDataError,
3
+ safelyGetAstroData,
4
+ toRemarkInitializeAstroData,
5
+ } from './frontmatter-injection.js';
@@ -2,72 +2,74 @@ import Slugger from 'github-slugger';
2
2
  import { toHtml } from 'hast-util-to-html';
3
3
  import { visit } from 'unist-util-visit';
4
4
 
5
- import type { MarkdownHeading, RehypePlugin } from './types.js';
5
+ import type { MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
6
6
 
7
- export default function createCollectHeadings() {
8
- const headings: MarkdownHeading[] = [];
9
- const slugger = new Slugger();
7
+ const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
8
+ const codeTagNames = new Set(['code', 'pre']);
10
9
 
11
- function rehypeCollectHeadings(): ReturnType<RehypePlugin> {
12
- return function (tree) {
13
- visit(tree, (node) => {
14
- if (node.type !== 'element') return;
15
- const { tagName } = node;
16
- if (tagName[0] !== 'h') return;
17
- const [_, level] = tagName.match(/h([0-6])/) ?? [];
18
- if (!level) return;
19
- const depth = Number.parseInt(level);
10
+ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
11
+ return function (tree, file: MarkdownVFile) {
12
+ const headings: MarkdownHeading[] = [];
13
+ const slugger = new Slugger();
14
+ const isMDX = isMDXFile(file);
15
+ visit(tree, (node) => {
16
+ if (node.type !== 'element') return;
17
+ const { tagName } = node;
18
+ if (tagName[0] !== 'h') return;
19
+ const [_, level] = tagName.match(/h([0-6])/) ?? [];
20
+ if (!level) return;
21
+ const depth = Number.parseInt(level);
20
22
 
21
- let text = '';
22
- let isJSX = false;
23
- visit(node, (child, __, parent) => {
24
- if (child.type === 'element' || parent == null) {
23
+ let text = '';
24
+ let isJSX = false;
25
+ visit(node, (child, __, parent) => {
26
+ if (child.type === 'element' || parent == null) {
27
+ return;
28
+ }
29
+ if (child.type === 'raw') {
30
+ if (child.value.match(/^\n?<.*>\n?$/)) {
25
31
  return;
26
32
  }
27
- if (child.type === 'raw') {
28
- if (child.value.match(/^\n?<.*>\n?$/)) {
29
- return;
30
- }
31
- }
32
- if (child.type === 'text' || child.type === 'raw') {
33
- if (new Set(['code', 'pre']).has(parent.tagName)) {
34
- text += child.value;
35
- } else {
36
- text += child.value.replace(/\{/g, '${');
37
- isJSX = isJSX || child.value.includes('{');
38
- }
33
+ }
34
+ if (rawNodeTypes.has(child.type)) {
35
+ if (isMDX || codeTagNames.has(parent.tagName)) {
36
+ text += child.value;
37
+ } else {
38
+ text += child.value.replace(/\{/g, '${');
39
+ isJSX = isJSX || child.value.includes('{');
39
40
  }
40
- });
41
+ }
42
+ });
41
43
 
42
- node.properties = node.properties || {};
43
- if (typeof node.properties.id !== 'string') {
44
- if (isJSX) {
45
- // HACK: serialized JSX from internal plugins, ignore these for slug
46
- const raw = toHtml(node.children, { allowDangerousHtml: true })
47
- .replace(/\n(<)/g, '<')
48
- .replace(/(>)\n/g, '>');
49
- // HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime
50
- node.properties.id = `$$slug(\`${text}\`)`;
51
- (node as any).type = 'raw';
52
- (
53
- node as any
54
- ).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
55
- } else {
56
- let slug = slugger.slug(text);
44
+ node.properties = node.properties || {};
45
+ if (typeof node.properties.id !== 'string') {
46
+ if (isJSX) {
47
+ // HACK: serialized JSX from internal plugins, ignore these for slug
48
+ const raw = toHtml(node.children, { allowDangerousHtml: true })
49
+ .replace(/\n(<)/g, '<')
50
+ .replace(/(>)\n/g, '>');
51
+ // HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime
52
+ node.properties.id = `$$slug(\`${text}\`)`;
53
+ (node as any).type = 'raw';
54
+ (
55
+ node as any
56
+ ).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
57
+ } else {
58
+ let slug = slugger.slug(text);
57
59
 
58
- if (slug.endsWith('-')) slug = slug.slice(0, -1);
60
+ if (slug.endsWith('-')) slug = slug.slice(0, -1);
59
61
 
60
- node.properties.id = slug;
61
- }
62
+ node.properties.id = slug;
62
63
  }
64
+ }
63
65
 
64
- headings.push({ depth, slug: node.properties.id, text });
65
- });
66
- };
67
- }
66
+ headings.push({ depth, slug: node.properties.id, text });
67
+ });
68
68
 
69
- return {
70
- headings,
71
- rehypeCollectHeadings,
69
+ file.data.__astroHeadings = headings;
72
70
  };
73
71
  }
72
+
73
+ function isMDXFile(file: MarkdownVFile) {
74
+ return Boolean(file.history[0]?.endsWith('.mdx'));
75
+ }
@@ -0,0 +1,51 @@
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
+ const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
14
+ if (!isContentFile) return;
15
+
16
+ const relImagePaths = new Set<string>();
17
+ visit(tree, 'image', function raiseError(node: Image) {
18
+ if (isRelativePath(node.url)) {
19
+ relImagePaths.add(node.url);
20
+ }
21
+ });
22
+ if (relImagePaths.size === 0) return;
23
+
24
+ const errorMessage =
25
+ `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` +
26
+ [...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
27
+
28
+ // Throw raw string to use `astro:markdown` default formatting
29
+ throw errorMessage;
30
+ };
31
+ };
32
+ }
33
+
34
+ // Following utils taken from `packages/astro/src/core/path.ts`:
35
+
36
+ function isRelativePath(path: string) {
37
+ return startsWithDotDotSlash(path) || startsWithDotSlash(path);
38
+ }
39
+
40
+ function startsWithDotDotSlash(path: string) {
41
+ const c1 = path[0];
42
+ const c2 = path[1];
43
+ const c3 = path[2];
44
+ return c1 === '.' && c2 === '.' && c3 === '/';
45
+ }
46
+
47
+ function startsWithDotSlash(path: string) {
48
+ const c1 = path[0];
49
+ const c2 = path[1];
50
+ return c1 === '.' && c2 === '/';
51
+ }
package/src/types.ts CHANGED
@@ -11,6 +11,10 @@ import type { VFile } from 'vfile';
11
11
 
12
12
  export type { Node } from 'unist';
13
13
 
14
+ export type MarkdownAstroData = {
15
+ frontmatter: Record<string, any>;
16
+ };
17
+
14
18
  export type RemarkPlugin<PluginParameters extends any[] = any[]> = unified.Plugin<
15
19
  PluginParameters,
16
20
  mdast.Root
@@ -26,8 +30,9 @@ export type RehypePlugin<PluginParameters extends any[] = any[]> = unified.Plugi
26
30
  export type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[];
27
31
 
28
32
  export type RemarkRehype = Omit<RemarkRehypeOptions, 'handlers' | 'unknownHandler'> & {
29
- handlers: typeof Handlers;
30
- } & { handler: typeof Handler };
33
+ handlers?: typeof Handlers;
34
+ handler?: typeof Handler;
35
+ };
31
36
 
32
37
  export interface ShikiConfig {
33
38
  langs?: ILanguageRegistration[];
@@ -36,14 +41,13 @@ export interface ShikiConfig {
36
41
  }
37
42
 
38
43
  export interface AstroMarkdownOptions {
39
- mode?: 'md' | 'mdx';
40
44
  drafts?: boolean;
41
45
  syntaxHighlight?: 'shiki' | 'prism' | false;
42
46
  shikiConfig?: ShikiConfig;
43
47
  remarkPlugins?: RemarkPlugins;
44
48
  rehypePlugins?: RehypePlugins;
45
49
  remarkRehype?: RemarkRehype;
46
- extendDefaultPlugins?: boolean;
50
+ gfm?: boolean;
47
51
  }
48
52
 
49
53
  export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
@@ -54,6 +58,12 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
54
58
  scopedClassName: string | null;
55
59
  };
56
60
  isAstroFlavoredMd?: boolean;
61
+ /** Used to prevent relative image imports from `src/content/` */
62
+ isExperimentalContentCollections?: boolean;
63
+ /** Used to prevent relative image imports from `src/content/` */
64
+ contentDir: URL;
65
+ /** Used for frontmatter injection plugins */
66
+ frontmatter?: Record<string, any>;
57
67
  }
58
68
 
59
69
  export interface MarkdownHeading {
@@ -68,6 +78,12 @@ export interface MarkdownMetadata {
68
78
  html: string;
69
79
  }
70
80
 
81
+ export interface MarkdownVFile extends VFile {
82
+ data: {
83
+ __astroHeadings?: MarkdownHeading[];
84
+ };
85
+ }
86
+
71
87
  export interface MarkdownRenderingResult {
72
88
  metadata: MarkdownMetadata;
73
89
  vfile: VFile;
@@ -1,2 +0,0 @@
1
- import type { VFile } from 'vfile';
2
- export declare function remarkInitializeAstroData(): (tree: any, vfile: VFile) => void;
@@ -1,10 +0,0 @@
1
- function remarkInitializeAstroData() {
2
- return function(tree, vfile) {
3
- if (!vfile.data.astro) {
4
- vfile.data.astro = { frontmatter: {} };
5
- }
6
- };
7
- }
8
- export {
9
- remarkInitializeAstroData
10
- };
@@ -1,9 +0,0 @@
1
- import type { VFile } from 'vfile';
2
-
3
- export function remarkInitializeAstroData() {
4
- return function (tree: any, vfile: VFile) {
5
- if (!vfile.data.astro) {
6
- vfile.data.astro = { frontmatter: {} };
7
- }
8
- };
9
- }