@astrojs/markdown-remark 2.2.0 → 2.2.1
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/package.json +6 -3
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -832
- package/src/frontmatter-injection.ts +0 -41
- package/src/index.ts +0 -170
- package/src/internal.ts +0 -5
- package/src/load-plugins.ts +0 -42
- package/src/rehype-collect-headings.ts +0 -129
- package/src/rehype-images.ts +0 -19
- package/src/remark-collect-images.ts +0 -30
- package/src/remark-prism.ts +0 -32
- package/src/remark-scoped-styles.ts +0 -18
- package/src/remark-shiki.ts +0 -126
- package/src/types.ts +0 -96
- package/test/autolinking.test.js +0 -43
- package/test/entities.test.js +0 -14
- package/test/plugins.test.js +0 -28
- package/test/test-utils.js +0 -3
- package/tsconfig.json +0 -10
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { VFile, VFileData as Data } 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
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
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 { remarkCollectImages } from './remark-collect-images.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
|
-
import { rehypeImages } from './rehype-images.js';
|
|
25
|
-
|
|
26
|
-
export { rehypeHeadingIds } from './rehype-collect-headings.js';
|
|
27
|
-
export { remarkCollectImages } from './remark-collect-images.js';
|
|
28
|
-
export * from './types.js';
|
|
29
|
-
|
|
30
|
-
export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
|
|
31
|
-
syntaxHighlight: 'shiki',
|
|
32
|
-
shikiConfig: {
|
|
33
|
-
langs: [],
|
|
34
|
-
theme: 'github-dark',
|
|
35
|
-
wrap: false,
|
|
36
|
-
},
|
|
37
|
-
remarkPlugins: [],
|
|
38
|
-
rehypePlugins: [],
|
|
39
|
-
remarkRehype: {},
|
|
40
|
-
gfm: true,
|
|
41
|
-
smartypants: true,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Skip nonessential plugins during performance benchmark runs
|
|
45
|
-
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
|
|
46
|
-
|
|
47
|
-
/** Shared utility for rendering markdown */
|
|
48
|
-
export async function renderMarkdown(
|
|
49
|
-
content: string,
|
|
50
|
-
opts: MarkdownRenderingOptions
|
|
51
|
-
): Promise<MarkdownRenderingResult> {
|
|
52
|
-
let {
|
|
53
|
-
fileURL,
|
|
54
|
-
syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
|
|
55
|
-
shikiConfig = markdownConfigDefaults.shikiConfig,
|
|
56
|
-
remarkPlugins = markdownConfigDefaults.remarkPlugins,
|
|
57
|
-
rehypePlugins = markdownConfigDefaults.rehypePlugins,
|
|
58
|
-
remarkRehype = markdownConfigDefaults.remarkRehype,
|
|
59
|
-
gfm = markdownConfigDefaults.gfm,
|
|
60
|
-
smartypants = markdownConfigDefaults.smartypants,
|
|
61
|
-
frontmatter: userFrontmatter = {},
|
|
62
|
-
} = opts;
|
|
63
|
-
const input = new VFile({ value: content, path: fileURL });
|
|
64
|
-
const scopedClassName = opts.$?.scopedClassName;
|
|
65
|
-
|
|
66
|
-
let parser = unified()
|
|
67
|
-
.use(markdown)
|
|
68
|
-
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
|
69
|
-
.use([]);
|
|
70
|
-
|
|
71
|
-
if (!isPerformanceBenchmark && gfm) {
|
|
72
|
-
if (gfm) {
|
|
73
|
-
parser.use(remarkGfm);
|
|
74
|
-
}
|
|
75
|
-
if (smartypants) {
|
|
76
|
-
parser.use(remarkSmartypants);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
|
81
|
-
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
|
82
|
-
|
|
83
|
-
loadedRemarkPlugins.forEach(([plugin, pluginOpts]) => {
|
|
84
|
-
parser.use([[plugin, pluginOpts]]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (!isPerformanceBenchmark) {
|
|
88
|
-
if (scopedClassName) {
|
|
89
|
-
parser.use([scopedStyles(scopedClassName)]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (syntaxHighlight === 'shiki') {
|
|
93
|
-
parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
|
|
94
|
-
} else if (syntaxHighlight === 'prism') {
|
|
95
|
-
parser.use([remarkPrism(scopedClassName)]);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (opts.experimentalAssets) {
|
|
99
|
-
// Apply later in case user plugins resolve relative image paths
|
|
100
|
-
parser.use([remarkCollectImages]);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
parser.use([
|
|
105
|
-
[
|
|
106
|
-
markdownToHtml as any,
|
|
107
|
-
{
|
|
108
|
-
allowDangerousHtml: true,
|
|
109
|
-
passThrough: [],
|
|
110
|
-
...remarkRehype,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
]);
|
|
114
|
-
|
|
115
|
-
loadedRehypePlugins.forEach(([plugin, pluginOpts]) => {
|
|
116
|
-
parser.use([[plugin, pluginOpts]]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (opts.experimentalAssets) {
|
|
120
|
-
parser.use(rehypeImages());
|
|
121
|
-
}
|
|
122
|
-
if (!isPerformanceBenchmark) {
|
|
123
|
-
parser.use([rehypeHeadingIds]);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
parser.use([rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
|
|
127
|
-
|
|
128
|
-
let vfile: MarkdownVFile;
|
|
129
|
-
try {
|
|
130
|
-
vfile = await parser.process(input);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
// Ensure that the error message contains the input filename
|
|
133
|
-
// to make it easier for the user to fix the issue
|
|
134
|
-
err = prefixError(err, `Failed to parse Markdown file "${input.path}"`);
|
|
135
|
-
// eslint-disable-next-line no-console
|
|
136
|
-
console.error(err);
|
|
137
|
-
throw err;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const headings = vfile?.data.__astroHeadings || [];
|
|
141
|
-
return {
|
|
142
|
-
metadata: { headings, source: content, html: String(vfile.value) },
|
|
143
|
-
code: String(vfile.value),
|
|
144
|
-
vfile,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function prefixError(err: any, prefix: string) {
|
|
149
|
-
// If the error is an object with a `message` property, attempt to prefix the message
|
|
150
|
-
if (err && err.message) {
|
|
151
|
-
try {
|
|
152
|
-
err.message = `${prefix}:\n${err.message}`;
|
|
153
|
-
return err;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
// Any errors here are ok, there's fallback code below
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// If that failed, create a new error with the desired message and attempt to keep the stack
|
|
160
|
-
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
|
|
161
|
-
try {
|
|
162
|
-
wrappedError.stack = err.stack;
|
|
163
|
-
// @ts-expect-error
|
|
164
|
-
wrappedError.cause = err;
|
|
165
|
-
} catch (error) {
|
|
166
|
-
// It's ok if we could not set the stack or cause - the message is the most important part
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return wrappedError;
|
|
170
|
-
}
|
package/src/internal.ts
DELETED
package/src/load-plugins.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { resolve as importMetaResolve } from 'import-meta-resolve';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import type * 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
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { Expression, 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']];
|
package/src/rehype-images.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { visit } from 'unist-util-visit';
|
|
2
|
-
import type { MarkdownVFile } from './types.js';
|
|
3
|
-
|
|
4
|
-
export function rehypeImages() {
|
|
5
|
-
return () =>
|
|
6
|
-
function (tree: any, file: MarkdownVFile) {
|
|
7
|
-
visit(tree, (node) => {
|
|
8
|
-
if (node.type !== 'element') return;
|
|
9
|
-
if (node.tagName !== 'img') return;
|
|
10
|
-
|
|
11
|
-
if (node.properties?.src) {
|
|
12
|
-
if (file.data.imagePaths?.has(node.properties.src)) {
|
|
13
|
-
node.properties['__ASTRO_IMAGE_'] = node.properties.src;
|
|
14
|
-
delete node.properties.src;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Image } from 'mdast';
|
|
2
|
-
import { visit } from 'unist-util-visit';
|
|
3
|
-
import type { MarkdownVFile } from './types';
|
|
4
|
-
|
|
5
|
-
export function remarkCollectImages() {
|
|
6
|
-
return function (tree: any, vfile: MarkdownVFile) {
|
|
7
|
-
if (typeof vfile?.path !== 'string') return;
|
|
8
|
-
|
|
9
|
-
const imagePaths = new Set<string>();
|
|
10
|
-
visit(tree, 'image', (node: Image) => {
|
|
11
|
-
if (shouldOptimizeImage(node.url)) imagePaths.add(node.url);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
vfile.data.imagePaths = imagePaths;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function shouldOptimizeImage(src: string) {
|
|
19
|
-
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
20
|
-
return !isValidUrl(src) && !src.startsWith('/');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isValidUrl(str: string): boolean {
|
|
24
|
-
try {
|
|
25
|
-
new URL(str);
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/remark-prism.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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;
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
package/src/remark-shiki.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
// Map of old theme names to new names to preserve compatibility when we upgrade shiki
|
|
14
|
-
const compatThemes: Record<string, string> = {
|
|
15
|
-
'material-darker': 'material-theme-darker',
|
|
16
|
-
'material-default': 'material-theme',
|
|
17
|
-
'material-lighter': 'material-theme-lighter',
|
|
18
|
-
'material-ocean': 'material-theme-ocean',
|
|
19
|
-
'material-palenight': 'material-theme-palenight',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const normalizeTheme = (theme: string | shiki.IShikiTheme) => {
|
|
23
|
-
if (typeof theme === 'string') {
|
|
24
|
-
return compatThemes[theme] || theme;
|
|
25
|
-
} else if (compatThemes[theme.name]) {
|
|
26
|
-
return { ...theme, name: compatThemes[theme.name] };
|
|
27
|
-
} else {
|
|
28
|
-
return theme;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const remarkShiki = async (
|
|
33
|
-
{ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig,
|
|
34
|
-
scopedClassName?: string | null
|
|
35
|
-
) => {
|
|
36
|
-
theme = normalizeTheme(theme);
|
|
37
|
-
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
|
|
38
|
-
let highlighterAsync = highlighterCacheAsync.get(cacheID);
|
|
39
|
-
if (!highlighterAsync) {
|
|
40
|
-
highlighterAsync = getHighlighter({ theme }).then((hl) => {
|
|
41
|
-
hl.setColorReplacements({
|
|
42
|
-
'#000001': 'var(--astro-code-color-text)',
|
|
43
|
-
'#000002': 'var(--astro-code-color-background)',
|
|
44
|
-
'#000004': 'var(--astro-code-token-constant)',
|
|
45
|
-
'#000005': 'var(--astro-code-token-string)',
|
|
46
|
-
'#000006': 'var(--astro-code-token-comment)',
|
|
47
|
-
'#000007': 'var(--astro-code-token-keyword)',
|
|
48
|
-
'#000008': 'var(--astro-code-token-parameter)',
|
|
49
|
-
'#000009': 'var(--astro-code-token-function)',
|
|
50
|
-
'#000010': 'var(--astro-code-token-string-expression)',
|
|
51
|
-
'#000011': 'var(--astro-code-token-punctuation)',
|
|
52
|
-
'#000012': 'var(--astro-code-token-link)',
|
|
53
|
-
});
|
|
54
|
-
return hl;
|
|
55
|
-
});
|
|
56
|
-
highlighterCacheAsync.set(cacheID, highlighterAsync);
|
|
57
|
-
}
|
|
58
|
-
const highlighter = await highlighterAsync;
|
|
59
|
-
|
|
60
|
-
// NOTE: There may be a performance issue here for large sites that use `lang`.
|
|
61
|
-
// Since this will be called on every page load. Unclear how to fix this.
|
|
62
|
-
for (const lang of langs) {
|
|
63
|
-
await highlighter.loadLanguage(lang);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return () => (tree: any) => {
|
|
67
|
-
visit(tree, 'code', (node) => {
|
|
68
|
-
let lang: string;
|
|
69
|
-
|
|
70
|
-
if (typeof node.lang === 'string') {
|
|
71
|
-
const langExists = highlighter.getLoadedLanguages().includes(node.lang);
|
|
72
|
-
if (langExists) {
|
|
73
|
-
lang = node.lang;
|
|
74
|
-
} else {
|
|
75
|
-
// eslint-disable-next-line no-console
|
|
76
|
-
console.warn(`The language "${node.lang}" doesn't exist, falling back to plaintext.`);
|
|
77
|
-
lang = 'plaintext';
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
lang = 'plaintext';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let html = highlighter!.codeToHtml(node.value, { lang });
|
|
84
|
-
|
|
85
|
-
// Q: Couldn't these regexes match on a user's inputted code blocks?
|
|
86
|
-
// A: Nope! All rendered HTML is properly escaped.
|
|
87
|
-
// Ex. If a user typed `<span class="line"` into a code block,
|
|
88
|
-
// It would become this before hitting our regexes:
|
|
89
|
-
// <span class="line"
|
|
90
|
-
|
|
91
|
-
// Replace "shiki" class naming with "astro" and add "is:raw".
|
|
92
|
-
html = html.replace(
|
|
93
|
-
/<pre class="(.*?)shiki(.*?)"/,
|
|
94
|
-
`<pre is:raw class="$1astro-code$2${scopedClassName ? ' ' + scopedClassName : ''}"`
|
|
95
|
-
);
|
|
96
|
-
// Add "user-select: none;" for "+"/"-" diff symbols
|
|
97
|
-
if (node.lang === 'diff') {
|
|
98
|
-
html = html.replace(
|
|
99
|
-
/<span class="line"><span style="(.*?)">([\+|\-])/g,
|
|
100
|
-
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
// Handle code wrapping
|
|
104
|
-
// if wrap=null, do nothing.
|
|
105
|
-
if (wrap === false) {
|
|
106
|
-
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
107
|
-
} else if (wrap === true) {
|
|
108
|
-
html = html.replace(
|
|
109
|
-
/style="(.*?)"/,
|
|
110
|
-
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Apply scopedClassName to all nested lines
|
|
115
|
-
if (scopedClassName) {
|
|
116
|
-
html = html.replace(/\<span class="line"\>/g, `<span class="line ${scopedClassName}"`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
node.type = 'html';
|
|
120
|
-
node.value = html;
|
|
121
|
-
node.children = [];
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export default remarkShiki;
|