@astrojs/markdown-remark 1.1.3 → 1.2.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 968e841d80fa22fa
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@1.2.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,17 @@
1
1
  # @astrojs/markdown-remark
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ 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.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#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/`
14
+
3
15
  ## 1.1.3
4
16
 
5
17
  ### Patch Changes
@@ -242,7 +254,7 @@
242
254
 
243
255
  ### Minor Changes
244
256
 
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)
257
+ - [`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
258
 
247
259
  ## 0.7.0
248
260
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
2
+ export { rehypeHeadingIds } from './rehype-collect-headings.js';
2
3
  export * from './types.js';
3
4
  export declare const DEFAULT_REMARK_PLUGINS: string[];
4
5
  export declare const DEFAULT_REHYPE_PLUGINS: never[];
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { loadPlugins } from "./load-plugins.js";
2
- import createCollectHeadings from "./rehype-collect-headings.js";
2
+ import { rehypeHeadingIds } from "./rehype-collect-headings.js";
3
3
  import rehypeEscape from "./rehype-escape.js";
4
4
  import rehypeExpressions from "./rehype-expressions.js";
5
5
  import rehypeIslands from "./rehype-islands.js";
6
6
  import rehypeJsx from "./rehype-jsx.js";
7
+ import toRemarkContentRelImageError from "./remark-content-rel-image-error.js";
7
8
  import remarkEscape from "./remark-escape.js";
8
9
  import { remarkInitializeAstroData } from "./remark-initialize-astro-data.js";
9
10
  import remarkMarkAndUnravel from "./remark-mark-and-unravel.js";
@@ -18,6 +19,7 @@ import markdown from "remark-parse";
18
19
  import markdownToHtml from "remark-rehype";
19
20
  import { unified } from "unified";
20
21
  import { VFile } from "vfile";
22
+ import { rehypeHeadingIds as rehypeHeadingIds2 } from "./rehype-collect-headings.js";
21
23
  export * from "./types.js";
22
24
  const DEFAULT_REMARK_PLUGINS = ["remark-gfm", "remark-smartypants"];
23
25
  const DEFAULT_REHYPE_PLUGINS = [];
@@ -31,11 +33,12 @@ async function renderMarkdown(content, opts) {
31
33
  rehypePlugins = [],
32
34
  remarkRehype = {},
33
35
  extendDefaultPlugins = false,
34
- isAstroFlavoredMd = false
36
+ isAstroFlavoredMd = false,
37
+ isExperimentalContentCollections = false,
38
+ contentDir
35
39
  } = opts;
36
40
  const input = new VFile({ value: content, path: fileURL });
37
41
  const scopedClassName = (_a = opts.$) == null ? void 0 : _a.scopedClassName;
38
- const { headings, rehypeCollectHeadings } = createCollectHeadings();
39
42
  let parser = unified().use(markdown).use(remarkInitializeAstroData).use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
40
43
  if (extendDefaultPlugins || remarkPlugins.length === 0 && rehypePlugins.length === 0) {
41
44
  remarkPlugins = [...DEFAULT_REMARK_PLUGINS, ...remarkPlugins];
@@ -54,6 +57,9 @@ async function renderMarkdown(content, opts) {
54
57
  } else if (syntaxHighlight === "prism") {
55
58
  parser.use([remarkPrism(scopedClassName)]);
56
59
  }
60
+ if (isExperimentalContentCollections) {
61
+ parser.use([toRemarkContentRelImageError({ contentDir })]);
62
+ }
57
63
  parser.use([
58
64
  [
59
65
  markdownToHtml,
@@ -74,7 +80,7 @@ async function renderMarkdown(content, opts) {
74
80
  parser.use([[plugin, pluginOpts]]);
75
81
  });
76
82
  parser.use(
77
- isAstroFlavoredMd ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings] : [rehypeCollectHeadings, rehypeRaw]
83
+ isAstroFlavoredMd ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds] : [rehypeHeadingIds, rehypeRaw]
78
84
  ).use(rehypeStringify, { allowDangerousHtml: true });
79
85
  let vfile;
80
86
  try {
@@ -84,6 +90,7 @@ async function renderMarkdown(content, opts) {
84
90
  console.error(err);
85
91
  throw err;
86
92
  }
93
+ const headings = (vfile == null ? void 0 : vfile.data.__astroHeadings) || [];
87
94
  return {
88
95
  metadata: { headings, source: content, html: String(vfile.value) },
89
96
  code: String(vfile.value),
@@ -110,5 +117,6 @@ ${err.message}`;
110
117
  export {
111
118
  DEFAULT_REHYPE_PLUGINS,
112
119
  DEFAULT_REMARK_PLUGINS,
120
+ rehypeHeadingIds2 as rehypeHeadingIds,
113
121
  renderMarkdown
114
122
  };
@@ -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
@@ -37,6 +37,10 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
37
37
  scopedClassName: string | null;
38
38
  };
39
39
  isAstroFlavoredMd?: boolean;
40
+ /** Used to prevent relative image imports from `src/content/` */
41
+ isExperimentalContentCollections?: boolean;
42
+ /** Used to prevent relative image imports from `src/content/` */
43
+ contentDir: URL;
40
44
  }
41
45
  export interface MarkdownHeading {
42
46
  depth: number;
@@ -48,6 +52,11 @@ export interface MarkdownMetadata {
48
52
  source: string;
49
53
  html: string;
50
54
  }
55
+ export interface MarkdownVFile extends VFile {
56
+ data: {
57
+ __astroHeadings?: MarkdownHeading[];
58
+ };
59
+ }
51
60
  export interface MarkdownRenderingResult {
52
61
  metadata: MarkdownMetadata;
53
62
  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": "1.2.0",
4
4
  "type": "module",
5
5
  "author": "withastro",
6
6
  "license": "MIT",
@@ -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"
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
- import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
1
+ import type { MarkdownRenderingOptions, MarkdownRenderingResult, MarkdownVFile } from './types';
2
2
 
3
3
  import { loadPlugins } from './load-plugins.js';
4
- import createCollectHeadings from './rehype-collect-headings.js';
4
+ import { rehypeHeadingIds } from './rehype-collect-headings.js';
5
5
  import rehypeEscape from './rehype-escape.js';
6
6
  import rehypeExpressions from './rehype-expressions.js';
7
7
  import rehypeIslands from './rehype-islands.js';
8
8
  import rehypeJsx from './rehype-jsx.js';
9
+ import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
9
10
  import remarkEscape from './remark-escape.js';
10
11
  import { remarkInitializeAstroData } from './remark-initialize-astro-data.js';
11
12
  import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
@@ -22,6 +23,7 @@ import markdownToHtml from 'remark-rehype';
22
23
  import { unified } from 'unified';
23
24
  import { VFile } from 'vfile';
24
25
 
26
+ export { rehypeHeadingIds } from './rehype-collect-headings.js';
25
27
  export * from './types.js';
26
28
 
27
29
  export const DEFAULT_REMARK_PLUGINS = ['remark-gfm', 'remark-smartypants'];
@@ -41,10 +43,11 @@ export async function renderMarkdown(
41
43
  remarkRehype = {},
42
44
  extendDefaultPlugins = false,
43
45
  isAstroFlavoredMd = false,
46
+ isExperimentalContentCollections = false,
47
+ contentDir,
44
48
  } = opts;
45
49
  const input = new VFile({ value: content, path: fileURL });
46
50
  const scopedClassName = opts.$?.scopedClassName;
47
- const { headings, rehypeCollectHeadings } = createCollectHeadings();
48
51
 
49
52
  let parser = unified()
50
53
  .use(markdown)
@@ -73,6 +76,11 @@ export async function renderMarkdown(
73
76
  parser.use([remarkPrism(scopedClassName)]);
74
77
  }
75
78
 
79
+ // Apply later in case user plugins resolve relative image paths
80
+ if (isExperimentalContentCollections) {
81
+ parser.use([toRemarkContentRelImageError({ contentDir })]);
82
+ }
83
+
76
84
  parser.use([
77
85
  [
78
86
  markdownToHtml as any,
@@ -99,12 +107,12 @@ export async function renderMarkdown(
99
107
  parser
100
108
  .use(
101
109
  isAstroFlavoredMd
102
- ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings]
103
- : [rehypeCollectHeadings, rehypeRaw]
110
+ ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds]
111
+ : [rehypeHeadingIds, rehypeRaw]
104
112
  )
105
113
  .use(rehypeStringify, { allowDangerousHtml: true });
106
114
 
107
- let vfile: VFile;
115
+ let vfile: MarkdownVFile;
108
116
  try {
109
117
  vfile = await parser.process(input);
110
118
  } catch (err) {
@@ -116,6 +124,7 @@ export async function renderMarkdown(
116
124
  throw err;
117
125
  }
118
126
 
127
+ const headings = vfile?.data.__astroHeadings || [];
119
128
  return {
120
129
  metadata: { headings, source: content, html: String(vfile.value) },
121
130
  code: String(vfile.value),
@@ -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
@@ -54,6 +54,10 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
54
54
  scopedClassName: string | null;
55
55
  };
56
56
  isAstroFlavoredMd?: boolean;
57
+ /** Used to prevent relative image imports from `src/content/` */
58
+ isExperimentalContentCollections?: boolean;
59
+ /** Used to prevent relative image imports from `src/content/` */
60
+ contentDir: URL;
57
61
  }
58
62
 
59
63
  export interface MarkdownHeading {
@@ -68,6 +72,12 @@ export interface MarkdownMetadata {
68
72
  html: string;
69
73
  }
70
74
 
75
+ export interface MarkdownVFile extends VFile {
76
+ data: {
77
+ __astroHeadings?: MarkdownHeading[];
78
+ };
79
+ }
80
+
71
81
  export interface MarkdownRenderingResult {
72
82
  metadata: MarkdownMetadata;
73
83
  vfile: VFile;