@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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +13 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -4
- package/dist/rehype-collect-headings.d.ts +2 -5
- package/dist/rehype-collect-headings.js +52 -50
- package/dist/remark-content-rel-image-error.d.ts +8 -0
- package/dist/remark-content-rel-image-error.js +39 -0
- package/dist/types.d.ts +9 -0
- package/package.json +2 -2
- package/src/index.ts +15 -6
- package/src/rehype-collect-headings.ts +57 -55
- package/src/remark-content-rel-image-error.ts +51 -0
- package/src/types.ts +10 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[35m@astrojs/markdown-remark:build: [0mcache hit, replaying output [
|
|
1
|
+
[35m@astrojs/markdown-remark:build: [0mcache hit, replaying output [2m968e841d80fa22fa[0m
|
|
2
2
|
[35m@astrojs/markdown-remark:build: [0m
|
|
3
|
-
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@1.
|
|
3
|
+
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@1.2.0 build /home/runner/work/astro/astro/packages/markdown/remark
|
|
4
4
|
[35m@astrojs/markdown-remark:build: [0m> astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json
|
|
5
5
|
[35m@astrojs/markdown-remark:build: [0m
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
2
|
-
export
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
103
|
-
: [
|
|
110
|
+
? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds]
|
|
111
|
+
: [rehypeHeadingIds, rehypeRaw]
|
|
104
112
|
)
|
|
105
113
|
.use(rehypeStringify, { allowDangerousHtml: true });
|
|
106
114
|
|
|
107
|
-
let 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
|
-
|
|
8
|
-
|
|
9
|
-
const slugger = new Slugger();
|
|
7
|
+
const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
|
|
8
|
+
const codeTagNames = new Set(['code', 'pre']);
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
+
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
+
node.properties.id = slug;
|
|
62
63
|
}
|
|
64
|
+
}
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
}
|
|
66
|
+
headings.push({ depth, slug: node.properties.id, text });
|
|
67
|
+
});
|
|
68
68
|
|
|
69
|
-
|
|
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;
|