@astrojs/markdown-remark 0.0.0-cloudcannon-fix-20230306211609
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 +5 -0
- package/CHANGELOG.md +772 -0
- package/LICENSE +61 -0
- package/dist/frontmatter-injection.d.ts +8 -0
- package/dist/frontmatter-injection.js +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +118 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +10 -0
- package/dist/load-plugins.d.ts +2 -0
- package/dist/load-plugins.js +31 -0
- package/dist/rehype-collect-headings.d.ts +2 -0
- package/dist/rehype-collect-headings.js +99 -0
- package/dist/remark-content-rel-image-error.d.ts +8 -0
- package/dist/remark-content-rel-image-error.js +41 -0
- package/dist/remark-prism.d.ts +3 -0
- package/dist/remark-prism.js +28 -0
- package/dist/remark-scoped-styles.d.ts +2 -0
- package/dist/remark-scoped-styles.js +19 -0
- package/dist/remark-shiki.d.ts +3 -0
- package/dist/remark-shiki.js +75 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +0 -0
- package/package.json +58 -0
- package/src/frontmatter-injection.ts +41 -0
- package/src/index.ts +154 -0
- package/src/internal.ts +5 -0
- package/src/load-plugins.ts +42 -0
- package/src/rehype-collect-headings.ts +129 -0
- package/src/remark-content-rel-image-error.ts +53 -0
- package/src/remark-prism.ts +32 -0
- package/src/remark-scoped-styles.ts +18 -0
- package/src/remark-shiki.ts +106 -0
- package/src/types.ts +89 -0
- package/test/autolinking.test.js +43 -0
- package/test/entities.test.js +14 -0
- package/test/plugins.test.js +28 -0
- package/test/test-utils.js +3 -0
- package/tsconfig.json +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astrojs/markdown-remark",
|
|
3
|
+
"version": "0.0.0-cloudcannon-fix-20230306211609",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": "withastro",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/withastro/astro.git",
|
|
10
|
+
"directory": "packages/markdown/remark"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/withastro/astro/issues",
|
|
13
|
+
"homepage": "https://astro.build",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.js",
|
|
17
|
+
"./dist/internal.js": "./dist/internal.js"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"astro": "0.0.0-cloudcannon-fix-20230306211609"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@astrojs/prism": "0.0.0-cloudcannon-fix-20230306211609",
|
|
24
|
+
"github-slugger": "^1.4.0",
|
|
25
|
+
"import-meta-resolve": "^2.1.0",
|
|
26
|
+
"rehype-raw": "^6.1.1",
|
|
27
|
+
"rehype-stringify": "^9.0.3",
|
|
28
|
+
"remark-gfm": "^3.0.1",
|
|
29
|
+
"remark-parse": "^10.0.1",
|
|
30
|
+
"remark-rehype": "^10.1.0",
|
|
31
|
+
"remark-smartypants": "^2.0.0",
|
|
32
|
+
"shiki": "^0.11.1",
|
|
33
|
+
"unified": "^10.1.2",
|
|
34
|
+
"unist-util-visit": "^4.1.0",
|
|
35
|
+
"vfile": "^5.3.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/chai": "^4.3.1",
|
|
39
|
+
"@types/estree": "^1.0.0",
|
|
40
|
+
"@types/github-slugger": "^1.3.0",
|
|
41
|
+
"@types/hast": "^2.3.4",
|
|
42
|
+
"@types/mdast": "^3.0.10",
|
|
43
|
+
"@types/mocha": "^9.1.1",
|
|
44
|
+
"@types/unist": "^2.0.6",
|
|
45
|
+
"astro-scripts": "0.0.0-cloudcannon-fix-20230306211609",
|
|
46
|
+
"chai": "^4.3.6",
|
|
47
|
+
"mdast-util-mdx-expression": "^1.3.1",
|
|
48
|
+
"mocha": "^9.2.2"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"prepublish": "pnpm build",
|
|
52
|
+
"build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
|
|
53
|
+
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
|
54
|
+
"postbuild": "astro-scripts copy \"src/**/*.js\"",
|
|
55
|
+
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
|
56
|
+
"test": "mocha --exit --timeout 20000"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AstroMarkdownOptions,
|
|
3
|
+
MarkdownRenderingOptions,
|
|
4
|
+
MarkdownRenderingResult,
|
|
5
|
+
MarkdownVFile,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
|
|
9
|
+
import { loadPlugins } from './load-plugins.js';
|
|
10
|
+
import { rehypeHeadingIds } from './rehype-collect-headings.js';
|
|
11
|
+
import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
|
|
12
|
+
import remarkPrism from './remark-prism.js';
|
|
13
|
+
import scopedStyles from './remark-scoped-styles.js';
|
|
14
|
+
import remarkShiki from './remark-shiki.js';
|
|
15
|
+
|
|
16
|
+
import rehypeRaw from 'rehype-raw';
|
|
17
|
+
import rehypeStringify from 'rehype-stringify';
|
|
18
|
+
import remarkGfm from 'remark-gfm';
|
|
19
|
+
import markdown from 'remark-parse';
|
|
20
|
+
import markdownToHtml from 'remark-rehype';
|
|
21
|
+
import remarkSmartypants from 'remark-smartypants';
|
|
22
|
+
import { unified } from 'unified';
|
|
23
|
+
import { VFile } from 'vfile';
|
|
24
|
+
|
|
25
|
+
export { rehypeHeadingIds } from './rehype-collect-headings.js';
|
|
26
|
+
export * from './types.js';
|
|
27
|
+
|
|
28
|
+
export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
|
|
29
|
+
syntaxHighlight: 'shiki',
|
|
30
|
+
shikiConfig: {
|
|
31
|
+
langs: [],
|
|
32
|
+
theme: 'github-dark',
|
|
33
|
+
wrap: false,
|
|
34
|
+
},
|
|
35
|
+
remarkPlugins: [],
|
|
36
|
+
rehypePlugins: [],
|
|
37
|
+
remarkRehype: {},
|
|
38
|
+
gfm: true,
|
|
39
|
+
smartypants: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Shared utility for rendering markdown */
|
|
43
|
+
export async function renderMarkdown(
|
|
44
|
+
content: string,
|
|
45
|
+
opts: MarkdownRenderingOptions
|
|
46
|
+
): Promise<MarkdownRenderingResult> {
|
|
47
|
+
let {
|
|
48
|
+
fileURL,
|
|
49
|
+
syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
|
|
50
|
+
shikiConfig = markdownConfigDefaults.shikiConfig,
|
|
51
|
+
remarkPlugins = markdownConfigDefaults.remarkPlugins,
|
|
52
|
+
rehypePlugins = markdownConfigDefaults.rehypePlugins,
|
|
53
|
+
remarkRehype = markdownConfigDefaults.remarkRehype,
|
|
54
|
+
gfm = markdownConfigDefaults.gfm,
|
|
55
|
+
smartypants = markdownConfigDefaults.smartypants,
|
|
56
|
+
contentDir,
|
|
57
|
+
frontmatter: userFrontmatter = {},
|
|
58
|
+
} = opts;
|
|
59
|
+
const input = new VFile({ value: content, path: fileURL });
|
|
60
|
+
const scopedClassName = opts.$?.scopedClassName;
|
|
61
|
+
|
|
62
|
+
let parser = unified()
|
|
63
|
+
.use(markdown)
|
|
64
|
+
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
|
65
|
+
.use([]);
|
|
66
|
+
|
|
67
|
+
if (gfm) {
|
|
68
|
+
parser.use(remarkGfm);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (smartypants) {
|
|
72
|
+
parser.use(remarkSmartypants);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
|
76
|
+
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
|
77
|
+
|
|
78
|
+
loadedRemarkPlugins.forEach(([plugin, pluginOpts]) => {
|
|
79
|
+
parser.use([[plugin, pluginOpts]]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (scopedClassName) {
|
|
83
|
+
parser.use([scopedStyles(scopedClassName)]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (syntaxHighlight === 'shiki') {
|
|
87
|
+
parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
|
|
88
|
+
} else if (syntaxHighlight === 'prism') {
|
|
89
|
+
parser.use([remarkPrism(scopedClassName)]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply later in case user plugins resolve relative image paths
|
|
93
|
+
parser.use([toRemarkContentRelImageError({ contentDir })]);
|
|
94
|
+
|
|
95
|
+
parser.use([
|
|
96
|
+
[
|
|
97
|
+
markdownToHtml as any,
|
|
98
|
+
{
|
|
99
|
+
allowDangerousHtml: true,
|
|
100
|
+
passThrough: [],
|
|
101
|
+
...remarkRehype,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
loadedRehypePlugins.forEach(([plugin, pluginOpts]) => {
|
|
107
|
+
parser.use([[plugin, pluginOpts]]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
|
|
111
|
+
|
|
112
|
+
let vfile: MarkdownVFile;
|
|
113
|
+
try {
|
|
114
|
+
vfile = await parser.process(input);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Ensure that the error message contains the input filename
|
|
117
|
+
// to make it easier for the user to fix the issue
|
|
118
|
+
err = prefixError(err, `Failed to parse Markdown file "${input.path}"`);
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.error(err);
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headings = vfile?.data.__astroHeadings || [];
|
|
125
|
+
return {
|
|
126
|
+
metadata: { headings, source: content, html: String(vfile.value) },
|
|
127
|
+
code: String(vfile.value),
|
|
128
|
+
vfile,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function prefixError(err: any, prefix: string) {
|
|
133
|
+
// If the error is an object with a `message` property, attempt to prefix the message
|
|
134
|
+
if (err && err.message) {
|
|
135
|
+
try {
|
|
136
|
+
err.message = `${prefix}:\n${err.message}`;
|
|
137
|
+
return err;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Any errors here are ok, there's fallback code below
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If that failed, create a new error with the desired message and attempt to keep the stack
|
|
144
|
+
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
|
|
145
|
+
try {
|
|
146
|
+
wrappedError.stack = err.stack;
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
wrappedError.cause = err;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// It's ok if we could not set the stack or cause - the message is the most important part
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return wrappedError;
|
|
154
|
+
}
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolve as importMetaResolve } from 'import-meta-resolve';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as unified from 'unified';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
const cwdUrlStr = pathToFileURL(path.join(process.cwd(), 'package.json')).toString();
|
|
7
|
+
|
|
8
|
+
async function importPlugin(p: string | unified.Plugin): Promise<unified.Plugin> {
|
|
9
|
+
if (typeof p === 'string') {
|
|
10
|
+
// Try import from this package first
|
|
11
|
+
try {
|
|
12
|
+
const importResult = await import(p);
|
|
13
|
+
return importResult.default;
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
// Try import from user project
|
|
17
|
+
const resolved = await importMetaResolve(p, cwdUrlStr);
|
|
18
|
+
const importResult = await import(resolved);
|
|
19
|
+
return importResult.default;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadPlugins(
|
|
26
|
+
items: (string | [string, any] | unified.Plugin<any[], any> | [unified.Plugin<any[], any>, any])[]
|
|
27
|
+
): Promise<[unified.Plugin, any?]>[] {
|
|
28
|
+
return items.map((p) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
if (Array.isArray(p)) {
|
|
31
|
+
const [plugin, opts] = p;
|
|
32
|
+
return importPlugin(plugin)
|
|
33
|
+
.then((m) => resolve([m, opts]))
|
|
34
|
+
.catch((e) => reject(e));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return importPlugin(p)
|
|
38
|
+
.then((m) => resolve([m]))
|
|
39
|
+
.catch((e) => reject(e));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type Expression, type Super } from 'estree';
|
|
2
|
+
import Slugger from 'github-slugger';
|
|
3
|
+
import { type MdxTextExpression } from 'mdast-util-mdx-expression';
|
|
4
|
+
import { type Node } from 'unist';
|
|
5
|
+
import { visit } from 'unist-util-visit';
|
|
6
|
+
|
|
7
|
+
import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';
|
|
8
|
+
import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
|
|
9
|
+
|
|
10
|
+
const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
|
|
11
|
+
const codeTagNames = new Set(['code', 'pre']);
|
|
12
|
+
|
|
13
|
+
export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
|
|
14
|
+
return function (tree, file: MarkdownVFile) {
|
|
15
|
+
const headings: MarkdownHeading[] = [];
|
|
16
|
+
const slugger = new Slugger();
|
|
17
|
+
const isMDX = isMDXFile(file);
|
|
18
|
+
const astroData = safelyGetAstroData(file.data);
|
|
19
|
+
visit(tree, (node) => {
|
|
20
|
+
if (node.type !== 'element') return;
|
|
21
|
+
const { tagName } = node;
|
|
22
|
+
if (tagName[0] !== 'h') return;
|
|
23
|
+
const [_, level] = tagName.match(/h([0-6])/) ?? [];
|
|
24
|
+
if (!level) return;
|
|
25
|
+
const depth = Number.parseInt(level);
|
|
26
|
+
|
|
27
|
+
let text = '';
|
|
28
|
+
visit(node, (child, __, parent) => {
|
|
29
|
+
if (child.type === 'element' || parent == null) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (child.type === 'raw') {
|
|
33
|
+
if (child.value.match(/^\n?<.*>\n?$/)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (rawNodeTypes.has(child.type)) {
|
|
38
|
+
if (isMDX || codeTagNames.has(parent.tagName)) {
|
|
39
|
+
let value = child.value;
|
|
40
|
+
if (isMdxTextExpression(child) && !(astroData instanceof InvalidAstroDataError)) {
|
|
41
|
+
const frontmatterPath = getMdxFrontmatterVariablePath(child);
|
|
42
|
+
if (Array.isArray(frontmatterPath) && frontmatterPath.length > 0) {
|
|
43
|
+
const frontmatterValue = getMdxFrontmatterVariableValue(astroData, frontmatterPath);
|
|
44
|
+
if (typeof frontmatterValue === 'string') {
|
|
45
|
+
value = frontmatterValue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
text += value;
|
|
50
|
+
} else {
|
|
51
|
+
text += child.value.replace(/\{/g, '${');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
node.properties = node.properties || {};
|
|
57
|
+
if (typeof node.properties.id !== 'string') {
|
|
58
|
+
let slug = slugger.slug(text);
|
|
59
|
+
|
|
60
|
+
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
|
61
|
+
|
|
62
|
+
node.properties.id = slug;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
headings.push({ depth, slug: node.properties.id, text });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
file.data.__astroHeadings = headings;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isMDXFile(file: MarkdownVFile) {
|
|
73
|
+
return Boolean(file.history[0]?.endsWith('.mdx'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if an ESTree entry is `frontmatter.*.VARIABLE`.
|
|
78
|
+
* If it is, return the variable path (i.e. `["*", ..., "VARIABLE"]`) minus the `frontmatter` prefix.
|
|
79
|
+
*/
|
|
80
|
+
function getMdxFrontmatterVariablePath(node: MdxTextExpression): string[] | Error {
|
|
81
|
+
if (!node.data?.estree || node.data.estree.body.length !== 1) return new Error();
|
|
82
|
+
|
|
83
|
+
const statement = node.data.estree.body[0];
|
|
84
|
+
|
|
85
|
+
// Check for "[ANYTHING].[ANYTHING]".
|
|
86
|
+
if (statement?.type !== 'ExpressionStatement' || statement.expression.type !== 'MemberExpression')
|
|
87
|
+
return new Error();
|
|
88
|
+
|
|
89
|
+
let expression: Expression | Super = statement.expression;
|
|
90
|
+
const expressionPath: string[] = [];
|
|
91
|
+
|
|
92
|
+
// Traverse the expression, collecting the variable path.
|
|
93
|
+
while (
|
|
94
|
+
expression.type === 'MemberExpression' &&
|
|
95
|
+
expression.property.type === (expression.computed ? 'Literal' : 'Identifier')
|
|
96
|
+
) {
|
|
97
|
+
expressionPath.push(
|
|
98
|
+
expression.property.type === 'Literal'
|
|
99
|
+
? String(expression.property.value)
|
|
100
|
+
: expression.property.name
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expression = expression.object;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for "frontmatter.[ANYTHING]".
|
|
107
|
+
if (expression.type !== 'Identifier' || expression.name !== 'frontmatter') return new Error();
|
|
108
|
+
|
|
109
|
+
return expressionPath.reverse();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getMdxFrontmatterVariableValue(astroData: MarkdownAstroData, path: string[]) {
|
|
113
|
+
let value: MdxFrontmatterVariableValue = astroData.frontmatter;
|
|
114
|
+
|
|
115
|
+
for (const key of path) {
|
|
116
|
+
if (!value[key]) return undefined;
|
|
117
|
+
|
|
118
|
+
value = value[key];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isMdxTextExpression(node: Node): node is MdxTextExpression {
|
|
125
|
+
return node.type === 'mdxTextExpression';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type MdxFrontmatterVariableValue =
|
|
129
|
+
MarkdownAstroData['frontmatter'][keyof MarkdownAstroData['frontmatter']];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Image } from 'mdast';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import type { VFile } from 'vfile';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `src/content/` does not support relative image paths.
|
|
8
|
+
* This plugin throws an error if any are found
|
|
9
|
+
*/
|
|
10
|
+
export default function toRemarkContentRelImageError({ contentDir }: { contentDir: URL }) {
|
|
11
|
+
return function remarkContentRelImageError() {
|
|
12
|
+
return (tree: any, vfile: VFile) => {
|
|
13
|
+
if (typeof vfile?.path !== 'string') return;
|
|
14
|
+
|
|
15
|
+
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
|
|
16
|
+
if (!isContentFile) return;
|
|
17
|
+
|
|
18
|
+
const relImagePaths = new Set<string>();
|
|
19
|
+
visit(tree, 'image', function raiseError(node: Image) {
|
|
20
|
+
if (isRelativePath(node.url)) {
|
|
21
|
+
relImagePaths.add(node.url);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (relImagePaths.size === 0) return;
|
|
25
|
+
|
|
26
|
+
const errorMessage =
|
|
27
|
+
`Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files)\n` +
|
|
28
|
+
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
|
|
29
|
+
|
|
30
|
+
// Throw raw string to use `astro:markdown` default formatting
|
|
31
|
+
throw errorMessage;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Following utils taken from `packages/astro/src/core/path.ts`:
|
|
37
|
+
|
|
38
|
+
function isRelativePath(path: string) {
|
|
39
|
+
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function startsWithDotDotSlash(path: string) {
|
|
43
|
+
const c1 = path[0];
|
|
44
|
+
const c2 = path[1];
|
|
45
|
+
const c3 = path[2];
|
|
46
|
+
return c1 === '.' && c2 === '.' && c3 === '/';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startsWithDotSlash(path: string) {
|
|
50
|
+
const c1 = path[0];
|
|
51
|
+
const c2 = path[1];
|
|
52
|
+
return c1 === '.' && c2 === '/';
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
const noVisit = new Set(['root', 'html', 'text']);
|
|
4
|
+
|
|
5
|
+
type MaybeString = string | null | undefined;
|
|
6
|
+
|
|
7
|
+
/** */
|
|
8
|
+
function transformer(className: MaybeString) {
|
|
9
|
+
return function (tree: any) {
|
|
10
|
+
const visitor = (node: any) => {
|
|
11
|
+
let { lang, value } = node;
|
|
12
|
+
node.type = 'html';
|
|
13
|
+
|
|
14
|
+
let { html, classLanguage } = runHighlighterWithAstro(lang, value);
|
|
15
|
+
let classes = [classLanguage];
|
|
16
|
+
if (className) {
|
|
17
|
+
classes.push(className);
|
|
18
|
+
}
|
|
19
|
+
node.value = `<pre class="${classes.join(
|
|
20
|
+
' '
|
|
21
|
+
)}"><code is:raw class="${classLanguage}">${html}</code></pre>`;
|
|
22
|
+
return node;
|
|
23
|
+
};
|
|
24
|
+
return visit(tree, 'code', visitor);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function plugin(className: MaybeString) {
|
|
29
|
+
return transformer.bind(null, className);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
const noVisit = new Set(['root', 'html', 'text']);
|
|
3
|
+
|
|
4
|
+
/** */
|
|
5
|
+
export default function scopedStyles(className: string) {
|
|
6
|
+
const visitor = (node: any) => {
|
|
7
|
+
if (noVisit.has(node.type)) return;
|
|
8
|
+
|
|
9
|
+
const { data } = node;
|
|
10
|
+
let currentClassName = data?.hProperties?.class ?? '';
|
|
11
|
+
node.data = node.data || {};
|
|
12
|
+
node.data.hProperties = node.data.hProperties || {};
|
|
13
|
+
node.data.hProperties.class = `${className} ${currentClassName}`.trim();
|
|
14
|
+
|
|
15
|
+
return node;
|
|
16
|
+
};
|
|
17
|
+
return () => (tree: any) => visit(tree, visitor);
|
|
18
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type * as shiki from 'shiki';
|
|
2
|
+
import { getHighlighter } from 'shiki';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
import type { ShikiConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* getHighlighter() is the most expensive step of Shiki. Instead of calling it on every page,
|
|
8
|
+
* cache it here as much as possible. Make sure that your highlighters can be cached, state-free.
|
|
9
|
+
* We make this async, so that multiple calls to parse markdown still share the same highlighter.
|
|
10
|
+
*/
|
|
11
|
+
const highlighterCacheAsync = new Map<string, Promise<shiki.Highlighter>>();
|
|
12
|
+
|
|
13
|
+
const remarkShiki = async (
|
|
14
|
+
{ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig,
|
|
15
|
+
scopedClassName?: string | null
|
|
16
|
+
) => {
|
|
17
|
+
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
|
|
18
|
+
let highlighterAsync = highlighterCacheAsync.get(cacheID);
|
|
19
|
+
if (!highlighterAsync) {
|
|
20
|
+
highlighterAsync = getHighlighter({ theme }).then((hl) => {
|
|
21
|
+
hl.setColorReplacements({
|
|
22
|
+
'#000001': 'var(--astro-code-color-text)',
|
|
23
|
+
'#000002': 'var(--astro-code-color-background)',
|
|
24
|
+
'#000004': 'var(--astro-code-token-constant)',
|
|
25
|
+
'#000005': 'var(--astro-code-token-string)',
|
|
26
|
+
'#000006': 'var(--astro-code-token-comment)',
|
|
27
|
+
'#000007': 'var(--astro-code-token-keyword)',
|
|
28
|
+
'#000008': 'var(--astro-code-token-parameter)',
|
|
29
|
+
'#000009': 'var(--astro-code-token-function)',
|
|
30
|
+
'#000010': 'var(--astro-code-token-string-expression)',
|
|
31
|
+
'#000011': 'var(--astro-code-token-punctuation)',
|
|
32
|
+
'#000012': 'var(--astro-code-token-link)',
|
|
33
|
+
});
|
|
34
|
+
return hl;
|
|
35
|
+
});
|
|
36
|
+
highlighterCacheAsync.set(cacheID, highlighterAsync);
|
|
37
|
+
}
|
|
38
|
+
const highlighter = await highlighterAsync;
|
|
39
|
+
|
|
40
|
+
// NOTE: There may be a performance issue here for large sites that use `lang`.
|
|
41
|
+
// Since this will be called on every page load. Unclear how to fix this.
|
|
42
|
+
for (const lang of langs) {
|
|
43
|
+
await highlighter.loadLanguage(lang);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return () => (tree: any) => {
|
|
47
|
+
visit(tree, 'code', (node) => {
|
|
48
|
+
let lang: string;
|
|
49
|
+
|
|
50
|
+
if (typeof node.lang === 'string') {
|
|
51
|
+
const langExists = highlighter.getLoadedLanguages().includes(node.lang);
|
|
52
|
+
if (langExists) {
|
|
53
|
+
lang = node.lang;
|
|
54
|
+
} else {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.warn(`The language "${node.lang}" doesn't exist, falling back to plaintext.`);
|
|
57
|
+
lang = 'plaintext';
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
lang = 'plaintext';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let html = highlighter!.codeToHtml(node.value, { lang });
|
|
64
|
+
|
|
65
|
+
// Q: Couldn't these regexes match on a user's inputted code blocks?
|
|
66
|
+
// A: Nope! All rendered HTML is properly escaped.
|
|
67
|
+
// Ex. If a user typed `<span class="line"` into a code block,
|
|
68
|
+
// It would become this before hitting our regexes:
|
|
69
|
+
// <span class="line"
|
|
70
|
+
|
|
71
|
+
// Replace "shiki" class naming with "astro" and add "is:raw".
|
|
72
|
+
html = html.replace(
|
|
73
|
+
/<pre class="(.*?)shiki(.*?)"/,
|
|
74
|
+
`<pre is:raw class="$1astro-code$2${scopedClassName ? ' ' + scopedClassName : ''}"`
|
|
75
|
+
);
|
|
76
|
+
// Add "user-select: none;" for "+"/"-" diff symbols
|
|
77
|
+
if (node.lang === 'diff') {
|
|
78
|
+
html = html.replace(
|
|
79
|
+
/<span class="line"><span style="(.*?)">([\+|\-])/g,
|
|
80
|
+
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Handle code wrapping
|
|
84
|
+
// if wrap=null, do nothing.
|
|
85
|
+
if (wrap === false) {
|
|
86
|
+
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
87
|
+
} else if (wrap === true) {
|
|
88
|
+
html = html.replace(
|
|
89
|
+
/style="(.*?)"/,
|
|
90
|
+
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Apply scopedClassName to all nested lines
|
|
95
|
+
if (scopedClassName) {
|
|
96
|
+
html = html.replace(/\<span class="line"\>/g, `<span class="line ${scopedClassName}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
node.type = 'html';
|
|
100
|
+
node.value = html;
|
|
101
|
+
node.children = [];
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default remarkShiki;
|