@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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +113 -1
- package/dist/frontmatter-injection.d.ts +8 -0
- package/dist/frontmatter-injection.js +35 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +37 -19
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +10 -0
- 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 +17 -5
- package/package.json +4 -4
- package/src/frontmatter-injection.ts +41 -0
- package/src/index.ts +45 -20
- package/src/internal.ts +5 -0
- package/src/rehype-collect-headings.ts +57 -55
- package/src/remark-content-rel-image-error.ts +51 -0
- package/src/types.ts +20 -4
- package/dist/remark-initialize-astro-data.d.ts +0 -2
- package/dist/remark-initialize-astro-data.js +0 -10
- package/src/remark-initialize-astro-data.ts +0 -9
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 [2m34663a00ad52a28a[0m
|
|
2
2
|
[35m@astrojs/markdown-remark:build: [0m
|
|
3
|
-
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@
|
|
3
|
+
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@2.0.0-beta.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,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
|
|
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
|
|
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
|
|
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
|
|
23
|
-
|
|
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 =
|
|
29
|
-
shikiConfig =
|
|
30
|
-
remarkPlugins =
|
|
31
|
-
rehypePlugins =
|
|
32
|
-
remarkRehype =
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
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
|
-
|
|
112
|
-
|
|
129
|
+
markdownConfigDefaults,
|
|
130
|
+
rehypeHeadingIds2 as rehypeHeadingIds,
|
|
113
131
|
renderMarkdown
|
|
114
132
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InvalidAstroDataError, safelyGetAstroData, toRemarkInitializeAstroData, } from './frontmatter-injection.js';
|
package/dist/internal.js
ADDED
|
@@ -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
|
@@ -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
|
|
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
|
-
|
|
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": "
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
28
|
-
|
|
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 =
|
|
38
|
-
shikiConfig =
|
|
39
|
-
remarkPlugins =
|
|
40
|
-
rehypePlugins =
|
|
41
|
-
remarkRehype =
|
|
42
|
-
|
|
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(
|
|
71
|
+
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
|
52
72
|
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
|
53
73
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
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,
|
|
103
|
-
: [
|
|
126
|
+
? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds]
|
|
127
|
+
: [rehypeHeadingIds, rehypeRaw]
|
|
104
128
|
)
|
|
105
129
|
.use(rehypeStringify, { allowDangerousHtml: true });
|
|
106
130
|
|
|
107
|
-
let 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),
|
package/src/internal.ts
ADDED
|
@@ -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
|
@@ -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
|
|
30
|
-
|
|
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
|
-
|
|
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;
|