@astrojs/markdown-remark 0.12.0 → 0.14.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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +48 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -15
- package/dist/mdxjs.js +8 -5
- package/dist/rehype-collect-headings.d.ts +5 -0
- package/dist/{rehype-collect-headers.js → rehype-collect-headings.js} +11 -8
- package/dist/rehype-escape.d.ts +1 -0
- package/dist/rehype-escape.js +8 -4
- package/dist/remark-initialize-astro-data.d.ts +2 -0
- package/dist/remark-initialize-astro-data.js +10 -0
- package/dist/remark-prism.js +5 -35
- package/dist/remark-shiki.js +16 -4
- package/dist/types.d.ts +5 -2
- package/package.json +2 -6
- package/src/index.ts +26 -23
- package/src/{rehype-collect-headers.ts → rehype-collect-headings.ts} +12 -8
- package/src/rehype-escape.ts +8 -3
- package/src/remark-escape.ts +1 -1
- package/src/remark-initialize-astro-data.ts +9 -0
- package/src/remark-prism.ts +2 -43
- package/src/types.ts +5 -2
- package/test/autolinking.test.js +88 -72
- package/test/components.test.js +12 -10
- package/test/entities.test.js +23 -0
- package/test/expressions.test.js +17 -15
- package/test/strictness.test.js +15 -16
- package/dist/rehype-collect-headers.d.ts +0 -5
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 [2mf2df71f430c1c421[0m
|
|
2
2
|
[35m@astrojs/markdown-remark:build: [0m
|
|
3
|
-
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@0.
|
|
3
|
+
[35m@astrojs/markdown-remark:build: [0m> @astrojs/markdown-remark@0.14.1 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,53 @@
|
|
|
1
1
|
# @astrojs/markdown-remark
|
|
2
2
|
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4176](https://github.com/withastro/astro/pull/4176) [`2675b8633`](https://github.com/withastro/astro/commit/2675b8633c5d5c45b237ec87940d5eaf1bfb1b4b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Support frontmatter injection for MD and MDX using remark and rehype plugins
|
|
8
|
+
|
|
9
|
+
* [#4137](https://github.com/withastro/astro/pull/4137) [`471c6f784`](https://github.com/withastro/astro/commit/471c6f784e21399676c8b2002665ffdf83a1c59e) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Speed up internal markdown builds with new vite-plugin markdown
|
|
10
|
+
|
|
11
|
+
- [#4169](https://github.com/withastro/astro/pull/4169) [`16034f0dd`](https://github.com/withastro/astro/commit/16034f0dd5b3683e9e022dbd413e85bd18d2b031) Thanks [@hippotastic](https://github.com/hippotastic)! - Fix double-escaping of non-highlighted code blocks in Astro-flavored markdown
|
|
12
|
+
|
|
13
|
+
## 0.14.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [#4114](https://github.com/withastro/astro/pull/4114) [`64432bcb8`](https://github.com/withastro/astro/commit/64432bcb873efd0e4297c00fc9583a1fe516dfe7) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Refactor `@astrojs/mdx` and `@astrojs/markdown-remark` to use `@astrojs/prism` instead of duplicating the code
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [[`64432bcb8`](https://github.com/withastro/astro/commit/64432bcb873efd0e4297c00fc9583a1fe516dfe7)]:
|
|
22
|
+
- @astrojs/prism@0.7.0
|
|
23
|
+
|
|
24
|
+
## 0.13.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- [`ba11b3399`](https://github.com/withastro/astro/commit/ba11b33996d79c32da947986edb0f32dbcc04aaf) Thanks [@RafidMuhymin](https://github.com/RafidMuhymin)! - fixed generated slugs in markdown that ends with a dash
|
|
29
|
+
|
|
30
|
+
* [#4016](https://github.com/withastro/astro/pull/4016) [`00fab4ce1`](https://github.com/withastro/astro/commit/00fab4ce135eb799cac69140403d7724686733d6) Thanks [@bholmesdev](https://github.com/bholmesdev)! - The use of components and JSX expressions in Markdown are no longer supported by default.
|
|
31
|
+
|
|
32
|
+
For long term support, migrate to the `@astrojs/mdx` integration for MDX support (including `.mdx` pages!).
|
|
33
|
+
|
|
34
|
+
Not ready to migrate to MDX? Add the legacy flag to your Astro config to re-enable the previous Markdown support.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// https://astro.build/config
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
legacy: {
|
|
40
|
+
astroFlavoredMarkdown: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- [#4031](https://github.com/withastro/astro/pull/4031) [`6e27a5fdc`](https://github.com/withastro/astro/commit/6e27a5fdc21276cad26cd50e16a2709a40a7cbac) Thanks [@natemoo-re](https://github.com/natemoo-re)! - **BREAKING** Renamed Markdown utility function `getHeaders()` to `getHeadings()`.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- [#4008](https://github.com/withastro/astro/pull/4008) [`399d7e269`](https://github.com/withastro/astro/commit/399d7e269834d11c046b390705a9a53d3738f3cf) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Avoid parsing JSX, components, and Astro islands when using "plain" md mode. This brings `markdown.mode: 'md'` in-line with our docs description.
|
|
50
|
+
|
|
3
51
|
## 0.12.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export * from './types.js';
|
|
|
3
3
|
export declare const DEFAULT_REMARK_PLUGINS: string[];
|
|
4
4
|
export declare const DEFAULT_REHYPE_PLUGINS: never[];
|
|
5
5
|
/** Shared utility for rendering markdown */
|
|
6
|
-
export declare function renderMarkdown(content: string, opts
|
|
6
|
+
export declare function renderMarkdown(content: string, opts: MarkdownRenderingOptions): Promise<MarkdownRenderingResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { loadPlugins } from "./load-plugins.js";
|
|
2
|
-
import
|
|
2
|
+
import createCollectHeadings from "./rehype-collect-headings.js";
|
|
3
3
|
import rehypeEscape from "./rehype-escape.js";
|
|
4
4
|
import rehypeExpressions from "./rehype-expressions.js";
|
|
5
5
|
import rehypeIslands from "./rehype-islands.js";
|
|
6
6
|
import rehypeJsx from "./rehype-jsx.js";
|
|
7
7
|
import remarkEscape from "./remark-escape.js";
|
|
8
|
+
import { remarkInitializeAstroData } from "./remark-initialize-astro-data.js";
|
|
8
9
|
import remarkMarkAndUnravel from "./remark-mark-and-unravel.js";
|
|
9
10
|
import remarkMdxish from "./remark-mdxish.js";
|
|
10
11
|
import remarkPrism from "./remark-prism.js";
|
|
@@ -20,21 +21,20 @@ import { VFile } from "vfile";
|
|
|
20
21
|
export * from "./types.js";
|
|
21
22
|
const DEFAULT_REMARK_PLUGINS = ["remark-gfm", "remark-smartypants"];
|
|
22
23
|
const DEFAULT_REHYPE_PLUGINS = [];
|
|
23
|
-
async function renderMarkdown(content, opts
|
|
24
|
+
async function renderMarkdown(content, opts) {
|
|
24
25
|
var _a;
|
|
25
26
|
let {
|
|
26
27
|
fileURL,
|
|
27
|
-
mode = "mdx",
|
|
28
28
|
syntaxHighlight = "shiki",
|
|
29
29
|
shikiConfig = {},
|
|
30
30
|
remarkPlugins = [],
|
|
31
|
-
rehypePlugins = []
|
|
31
|
+
rehypePlugins = [],
|
|
32
|
+
isAstroFlavoredMd = false
|
|
32
33
|
} = opts;
|
|
33
34
|
const input = new VFile({ value: content, path: fileURL });
|
|
34
35
|
const scopedClassName = (_a = opts.$) == null ? void 0 : _a.scopedClassName;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
let parser = unified().use(markdown).use(isMDX ? [remarkMdxish, remarkMarkAndUnravel] : []).use([remarkUnwrap, remarkEscape]);
|
|
36
|
+
const { headings, rehypeCollectHeadings } = createCollectHeadings();
|
|
37
|
+
let parser = unified().use(markdown).use(remarkInitializeAstroData).use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
|
38
38
|
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
|
|
39
39
|
remarkPlugins = [...DEFAULT_REMARK_PLUGINS];
|
|
40
40
|
rehypePlugins = [...DEFAULT_REHYPE_PLUGINS];
|
|
@@ -57,32 +57,34 @@ async function renderMarkdown(content, opts = {}) {
|
|
|
57
57
|
markdownToHtml,
|
|
58
58
|
{
|
|
59
59
|
allowDangerousHtml: true,
|
|
60
|
-
passThrough: [
|
|
60
|
+
passThrough: isAstroFlavoredMd ? [
|
|
61
61
|
"raw",
|
|
62
62
|
"mdxFlowExpression",
|
|
63
63
|
"mdxJsxFlowElement",
|
|
64
64
|
"mdxJsxTextElement",
|
|
65
65
|
"mdxTextExpression"
|
|
66
|
-
]
|
|
66
|
+
] : []
|
|
67
67
|
}
|
|
68
68
|
]
|
|
69
69
|
]);
|
|
70
70
|
loadedRehypePlugins.forEach(([plugin, pluginOpts]) => {
|
|
71
71
|
parser.use([[plugin, pluginOpts]]);
|
|
72
72
|
});
|
|
73
|
-
parser.use(
|
|
74
|
-
|
|
73
|
+
parser.use(
|
|
74
|
+
isAstroFlavoredMd ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings] : [rehypeCollectHeadings, rehypeRaw]
|
|
75
|
+
).use(rehypeStringify, { allowDangerousHtml: true });
|
|
76
|
+
let vfile;
|
|
75
77
|
try {
|
|
76
|
-
|
|
77
|
-
result = vfile.toString();
|
|
78
|
+
vfile = await parser.process(input);
|
|
78
79
|
} catch (err) {
|
|
79
80
|
err = prefixError(err, `Failed to parse Markdown file "${input.path}"`);
|
|
80
81
|
console.error(err);
|
|
81
82
|
throw err;
|
|
82
83
|
}
|
|
83
84
|
return {
|
|
84
|
-
metadata: {
|
|
85
|
-
code:
|
|
85
|
+
metadata: { headings, source: content, html: String(vfile.value) },
|
|
86
|
+
code: String(vfile.value),
|
|
87
|
+
vfile
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
function prefixError(err, prefix) {
|
package/dist/mdxjs.js
CHANGED
|
@@ -5,11 +5,14 @@ import { mdxExpression } from "micromark-extension-mdx-expression";
|
|
|
5
5
|
import { mdxMd } from "micromark-extension-mdx-md";
|
|
6
6
|
import { combineExtensions } from "micromark-util-combine-extensions";
|
|
7
7
|
function mdxjs(options) {
|
|
8
|
-
const settings = Object.assign(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const settings = Object.assign(
|
|
9
|
+
{
|
|
10
|
+
acorn: Parser.extend(acornJsx()),
|
|
11
|
+
acornOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
12
|
+
addResult: true
|
|
13
|
+
},
|
|
14
|
+
options
|
|
15
|
+
);
|
|
13
16
|
return combineExtensions([mdxExpression(settings), mdxJsx(settings), mdxMd]);
|
|
14
17
|
}
|
|
15
18
|
export {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import Slugger from "github-slugger";
|
|
2
2
|
import { toHtml } from "hast-util-to-html";
|
|
3
3
|
import { visit } from "unist-util-visit";
|
|
4
|
-
function
|
|
5
|
-
const
|
|
4
|
+
function createCollectHeadings() {
|
|
5
|
+
const headings = [];
|
|
6
6
|
const slugger = new Slugger();
|
|
7
|
-
function
|
|
7
|
+
function rehypeCollectHeadings() {
|
|
8
8
|
return function(tree) {
|
|
9
9
|
visit(tree, (node) => {
|
|
10
10
|
if (node.type !== "element")
|
|
@@ -44,18 +44,21 @@ function createCollectHeaders() {
|
|
|
44
44
|
node.type = "raw";
|
|
45
45
|
node.value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
|
|
46
46
|
} else {
|
|
47
|
-
|
|
47
|
+
let slug = slugger.slug(text);
|
|
48
|
+
if (slug.endsWith("-"))
|
|
49
|
+
slug = slug.slice(0, -1);
|
|
50
|
+
node.properties.id = slug;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
headings.push({ depth, slug: node.properties.id, text });
|
|
51
54
|
});
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
headings,
|
|
59
|
+
rehypeCollectHeadings
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
export {
|
|
60
|
-
|
|
63
|
+
createCollectHeadings as default
|
|
61
64
|
};
|
package/dist/rehype-escape.d.ts
CHANGED
package/dist/rehype-escape.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { visit } from "unist-util-visit";
|
|
1
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
2
|
+
function escapeEntities(value) {
|
|
3
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
4
|
+
}
|
|
2
5
|
function rehypeEscape() {
|
|
3
6
|
return function(node) {
|
|
4
7
|
return visit(node, "element", (el) => {
|
|
5
8
|
if (el.tagName === "code" || el.tagName === "pre") {
|
|
6
9
|
el.properties["is:raw"] = true;
|
|
7
10
|
visit(el, "raw", (raw) => {
|
|
8
|
-
raw.value = raw.value
|
|
11
|
+
raw.value = escapeEntities(raw.value);
|
|
9
12
|
});
|
|
13
|
+
return SKIP;
|
|
10
14
|
}
|
|
11
|
-
return el;
|
|
12
15
|
});
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
export {
|
|
16
|
-
rehypeEscape as default
|
|
19
|
+
rehypeEscape as default,
|
|
20
|
+
escapeEntities
|
|
17
21
|
};
|
package/dist/remark-prism.js
CHANGED
|
@@ -1,49 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Prism from "prismjs";
|
|
3
|
-
import loadLanguages from "prismjs/components/index.js";
|
|
1
|
+
import { runHighlighterWithAstro } from "@astrojs/prism/dist/highlighter";
|
|
4
2
|
import { visit } from "unist-util-visit";
|
|
5
3
|
const noVisit = /* @__PURE__ */ new Set(["root", "html", "text"]);
|
|
6
|
-
const languageMap = /* @__PURE__ */ new Map([["ts", "typescript"]]);
|
|
7
|
-
function runHighlighter(lang, code) {
|
|
8
|
-
let classLanguage = `language-${lang}`;
|
|
9
|
-
if (lang == null) {
|
|
10
|
-
lang = "plaintext";
|
|
11
|
-
}
|
|
12
|
-
const ensureLoaded = (language) => {
|
|
13
|
-
if (language && !Prism.languages[language]) {
|
|
14
|
-
loadLanguages([language]);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
if (languageMap.has(lang)) {
|
|
18
|
-
ensureLoaded(languageMap.get(lang));
|
|
19
|
-
} else if (lang === "astro") {
|
|
20
|
-
ensureLoaded("typescript");
|
|
21
|
-
addAstro(Prism);
|
|
22
|
-
} else {
|
|
23
|
-
ensureLoaded("markup-templating");
|
|
24
|
-
ensureLoaded(lang);
|
|
25
|
-
}
|
|
26
|
-
if (lang && !Prism.languages[lang]) {
|
|
27
|
-
console.warn(`Unable to load the language: ${lang}`);
|
|
28
|
-
}
|
|
29
|
-
const grammar = Prism.languages[lang];
|
|
30
|
-
let html = code;
|
|
31
|
-
if (grammar) {
|
|
32
|
-
html = Prism.highlight(code, grammar, lang);
|
|
33
|
-
}
|
|
34
|
-
return { classLanguage, html };
|
|
35
|
-
}
|
|
36
4
|
function transformer(className) {
|
|
37
5
|
return function(tree) {
|
|
38
6
|
const visitor = (node) => {
|
|
39
7
|
let { lang, value } = node;
|
|
40
8
|
node.type = "html";
|
|
41
|
-
let { html, classLanguage } =
|
|
9
|
+
let { html, classLanguage } = runHighlighterWithAstro(lang, value);
|
|
42
10
|
let classes = [classLanguage];
|
|
43
11
|
if (className) {
|
|
44
12
|
classes.push(className);
|
|
45
13
|
}
|
|
46
|
-
node.value = `<pre class="${classes.join(
|
|
14
|
+
node.value = `<pre class="${classes.join(
|
|
15
|
+
" "
|
|
16
|
+
)}"><code is:raw class="${classLanguage}">${html}</code></pre>`;
|
|
47
17
|
return node;
|
|
48
18
|
};
|
|
49
19
|
return visit(tree, "code", visitor);
|
package/dist/remark-shiki.js
CHANGED
|
@@ -27,15 +27,27 @@ const remarkShiki = async ({ langs = [], theme = "github-dark", wrap = false },
|
|
|
27
27
|
lang = "plaintext";
|
|
28
28
|
}
|
|
29
29
|
let html = highlighter.codeToHtml(node.value, { lang });
|
|
30
|
-
html = html.replace(
|
|
31
|
-
|
|
30
|
+
html = html.replace(
|
|
31
|
+
'<pre class="shiki"',
|
|
32
|
+
`<pre is:raw class="astro-code${scopedClassName ? " " + scopedClassName : ""}"`
|
|
33
|
+
);
|
|
34
|
+
html = html.replace(
|
|
35
|
+
/style="(background-)?color: var\(--shiki-/g,
|
|
36
|
+
'style="$1color: var(--astro-code-'
|
|
37
|
+
);
|
|
32
38
|
if (node.lang === "diff") {
|
|
33
|
-
html = html.replace(
|
|
39
|
+
html = html.replace(
|
|
40
|
+
/<span class="line"><span style="(.*?)">([\+|\-])/g,
|
|
41
|
+
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
42
|
+
);
|
|
34
43
|
}
|
|
35
44
|
if (wrap === false) {
|
|
36
45
|
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
37
46
|
} else if (wrap === true) {
|
|
38
|
-
html = html.replace(
|
|
47
|
+
html = html.replace(
|
|
48
|
+
/style="(.*?)"/,
|
|
49
|
+
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
50
|
+
);
|
|
39
51
|
}
|
|
40
52
|
if (scopedClassName) {
|
|
41
53
|
html = html.replace(/\<span class="line"\>/g, `<span class="line ${scopedClassName}"`);
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type * as hast from 'hast';
|
|
|
2
2
|
import type * as mdast from 'mdast';
|
|
3
3
|
import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
|
|
4
4
|
import type * as unified from 'unified';
|
|
5
|
+
import type { VFile } from 'vfile';
|
|
5
6
|
export type { Node } from 'unist';
|
|
6
7
|
export declare type RemarkPlugin<PluginParameters extends any[] = any[]> = unified.Plugin<PluginParameters, mdast.Root>;
|
|
7
8
|
export declare type RemarkPlugins = (string | [string, any] | RemarkPlugin | [RemarkPlugin, any])[];
|
|
@@ -27,18 +28,20 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
|
|
27
28
|
$?: {
|
|
28
29
|
scopedClassName: string | null;
|
|
29
30
|
};
|
|
31
|
+
isAstroFlavoredMd?: boolean;
|
|
30
32
|
}
|
|
31
|
-
export interface
|
|
33
|
+
export interface MarkdownHeading {
|
|
32
34
|
depth: number;
|
|
33
35
|
slug: string;
|
|
34
36
|
text: string;
|
|
35
37
|
}
|
|
36
38
|
export interface MarkdownMetadata {
|
|
37
|
-
|
|
39
|
+
headings: MarkdownHeading[];
|
|
38
40
|
source: string;
|
|
39
41
|
html: string;
|
|
40
42
|
}
|
|
41
43
|
export interface MarkdownRenderingResult {
|
|
42
44
|
metadata: MarkdownMetadata;
|
|
45
|
+
vfile: VFile;
|
|
43
46
|
code: string;
|
|
44
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/markdown-remark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "withastro",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,18 +17,15 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@astrojs/micromark-extension-mdx-jsx": "^1.0.3",
|
|
20
|
-
"@astrojs/prism": "^0.
|
|
20
|
+
"@astrojs/prism": "^0.7.0",
|
|
21
21
|
"acorn": "^8.7.1",
|
|
22
22
|
"acorn-jsx": "^5.3.2",
|
|
23
|
-
"assert": "^2.0.0",
|
|
24
23
|
"github-slugger": "^1.4.0",
|
|
25
24
|
"mdast-util-mdx-expression": "^1.2.1",
|
|
26
25
|
"mdast-util-mdx-jsx": "^1.2.0",
|
|
27
|
-
"mdast-util-to-string": "^3.1.0",
|
|
28
26
|
"micromark-extension-mdx-expression": "^1.0.3",
|
|
29
27
|
"micromark-extension-mdx-md": "^1.0.0",
|
|
30
28
|
"micromark-util-combine-extensions": "^1.0.0",
|
|
31
|
-
"prismjs": "^1.28.0",
|
|
32
29
|
"rehype-raw": "^6.1.1",
|
|
33
30
|
"rehype-stringify": "^9.0.3",
|
|
34
31
|
"remark-gfm": "^3.0.1",
|
|
@@ -47,7 +44,6 @@
|
|
|
47
44
|
"@types/hast": "^2.3.4",
|
|
48
45
|
"@types/mdast": "^3.0.10",
|
|
49
46
|
"@types/mocha": "^9.1.1",
|
|
50
|
-
"@types/prismjs": "^1.26.0",
|
|
51
47
|
"@types/unist": "^2.0.6",
|
|
52
48
|
"astro-scripts": "0.0.6",
|
|
53
49
|
"chai": "^4.3.6",
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types';
|
|
2
2
|
|
|
3
3
|
import { loadPlugins } from './load-plugins.js';
|
|
4
|
-
import
|
|
4
|
+
import createCollectHeadings from './rehype-collect-headings.js';
|
|
5
5
|
import rehypeEscape from './rehype-escape.js';
|
|
6
6
|
import rehypeExpressions from './rehype-expressions.js';
|
|
7
7
|
import rehypeIslands from './rehype-islands.js';
|
|
8
8
|
import rehypeJsx from './rehype-jsx.js';
|
|
9
9
|
import remarkEscape from './remark-escape.js';
|
|
10
|
+
import { remarkInitializeAstroData } from './remark-initialize-astro-data.js';
|
|
10
11
|
import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
|
|
11
12
|
import remarkMdxish from './remark-mdxish.js';
|
|
12
13
|
import remarkPrism from './remark-prism.js';
|
|
@@ -29,25 +30,24 @@ export const DEFAULT_REHYPE_PLUGINS = [];
|
|
|
29
30
|
/** Shared utility for rendering markdown */
|
|
30
31
|
export async function renderMarkdown(
|
|
31
32
|
content: string,
|
|
32
|
-
opts: MarkdownRenderingOptions
|
|
33
|
+
opts: MarkdownRenderingOptions
|
|
33
34
|
): Promise<MarkdownRenderingResult> {
|
|
34
35
|
let {
|
|
35
36
|
fileURL,
|
|
36
|
-
mode = 'mdx',
|
|
37
37
|
syntaxHighlight = 'shiki',
|
|
38
38
|
shikiConfig = {},
|
|
39
39
|
remarkPlugins = [],
|
|
40
40
|
rehypePlugins = [],
|
|
41
|
+
isAstroFlavoredMd = false,
|
|
41
42
|
} = opts;
|
|
42
43
|
const input = new VFile({ value: content, path: fileURL });
|
|
43
44
|
const scopedClassName = opts.$?.scopedClassName;
|
|
44
|
-
const
|
|
45
|
-
const { headers, rehypeCollectHeaders } = createCollectHeaders();
|
|
45
|
+
const { headings, rehypeCollectHeadings } = createCollectHeadings();
|
|
46
46
|
|
|
47
47
|
let parser = unified()
|
|
48
48
|
.use(markdown)
|
|
49
|
-
.use(
|
|
50
|
-
.use([remarkUnwrap, remarkEscape]);
|
|
49
|
+
.use(remarkInitializeAstroData)
|
|
50
|
+
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
|
51
51
|
|
|
52
52
|
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
|
|
53
53
|
remarkPlugins = [...DEFAULT_REMARK_PLUGINS];
|
|
@@ -76,13 +76,15 @@ export async function renderMarkdown(
|
|
|
76
76
|
markdownToHtml as any,
|
|
77
77
|
{
|
|
78
78
|
allowDangerousHtml: true,
|
|
79
|
-
passThrough:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
passThrough: isAstroFlavoredMd
|
|
80
|
+
? [
|
|
81
|
+
'raw',
|
|
82
|
+
'mdxFlowExpression',
|
|
83
|
+
'mdxJsxFlowElement',
|
|
84
|
+
'mdxJsxTextElement',
|
|
85
|
+
'mdxTextExpression',
|
|
86
|
+
]
|
|
87
|
+
: [],
|
|
86
88
|
},
|
|
87
89
|
],
|
|
88
90
|
]);
|
|
@@ -92,16 +94,16 @@ export async function renderMarkdown(
|
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
parser
|
|
95
|
-
.use(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
.use(
|
|
98
|
+
isAstroFlavoredMd
|
|
99
|
+
? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings]
|
|
100
|
+
: [rehypeCollectHeadings, rehypeRaw]
|
|
101
|
+
)
|
|
99
102
|
.use(rehypeStringify, { allowDangerousHtml: true });
|
|
100
103
|
|
|
101
|
-
let
|
|
104
|
+
let vfile: VFile;
|
|
102
105
|
try {
|
|
103
|
-
|
|
104
|
-
result = vfile.toString();
|
|
106
|
+
vfile = await parser.process(input);
|
|
105
107
|
} catch (err) {
|
|
106
108
|
// Ensure that the error message contains the input filename
|
|
107
109
|
// to make it easier for the user to fix the issue
|
|
@@ -112,8 +114,9 @@ export async function renderMarkdown(
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
return {
|
|
115
|
-
metadata: {
|
|
116
|
-
code:
|
|
117
|
+
metadata: { headings, source: content, html: String(vfile.value) },
|
|
118
|
+
code: String(vfile.value),
|
|
119
|
+
vfile,
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -2,13 +2,13 @@ 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 {
|
|
5
|
+
import type { MarkdownHeading, RehypePlugin } from './types.js';
|
|
6
6
|
|
|
7
|
-
export default function
|
|
8
|
-
const
|
|
7
|
+
export default function createCollectHeadings() {
|
|
8
|
+
const headings: MarkdownHeading[] = [];
|
|
9
9
|
const slugger = new Slugger();
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function rehypeCollectHeadings(): ReturnType<RehypePlugin> {
|
|
12
12
|
return function (tree) {
|
|
13
13
|
visit(tree, (node) => {
|
|
14
14
|
if (node.type !== 'element') return;
|
|
@@ -53,17 +53,21 @@ export default function createCollectHeaders() {
|
|
|
53
53
|
node as any
|
|
54
54
|
).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
|
|
55
55
|
} else {
|
|
56
|
-
|
|
56
|
+
let slug = slugger.slug(text);
|
|
57
|
+
|
|
58
|
+
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
|
59
|
+
|
|
60
|
+
node.properties.id = slug;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
headings.push({ depth, slug: node.properties.id, text });
|
|
61
65
|
});
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
headings,
|
|
71
|
+
rehypeCollectHeadings,
|
|
68
72
|
};
|
|
69
73
|
}
|
package/src/rehype-escape.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { visit } from 'unist-util-visit';
|
|
1
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
export function escapeEntities(value: string): string {
|
|
4
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export default function rehypeEscape(): any {
|
|
4
8
|
return function (node: any): any {
|
|
@@ -8,10 +12,11 @@ export default function rehypeEscape(): any {
|
|
|
8
12
|
// Visit all raw children and escape HTML tags to prevent Markdown code
|
|
9
13
|
// like "This is a `<script>` tag" from actually opening a script tag
|
|
10
14
|
visit(el, 'raw', (raw) => {
|
|
11
|
-
raw.value = raw.value
|
|
15
|
+
raw.value = escapeEntities(raw.value);
|
|
12
16
|
});
|
|
17
|
+
// Do not visit children to prevent double escaping
|
|
18
|
+
return SKIP;
|
|
13
19
|
}
|
|
14
|
-
return el;
|
|
15
20
|
});
|
|
16
21
|
};
|
|
17
22
|
}
|
package/src/remark-escape.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Literal } from 'unist';
|
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
3
|
|
|
4
4
|
// In code blocks, this removes the JS comment wrapper added to
|
|
5
|
-
// HTML comments by vite-plugin-markdown.
|
|
5
|
+
// HTML comments by vite-plugin-markdown-legacy.
|
|
6
6
|
export default function remarkEscape() {
|
|
7
7
|
return (tree: any) => {
|
|
8
8
|
visit(tree, 'code', removeCommentWrapper);
|
package/src/remark-prism.ts
CHANGED
|
@@ -1,48 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Prism from 'prismjs';
|
|
3
|
-
import loadLanguages from 'prismjs/components/index.js';
|
|
1
|
+
import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter';
|
|
4
2
|
import { visit } from 'unist-util-visit';
|
|
5
3
|
const noVisit = new Set(['root', 'html', 'text']);
|
|
6
4
|
|
|
7
|
-
const languageMap = new Map([['ts', 'typescript']]);
|
|
8
|
-
|
|
9
|
-
function runHighlighter(lang: string, code: string) {
|
|
10
|
-
let classLanguage = `language-${lang}`;
|
|
11
|
-
|
|
12
|
-
if (lang == null) {
|
|
13
|
-
lang = 'plaintext';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const ensureLoaded = (language: string) => {
|
|
17
|
-
if (language && !Prism.languages[language]) {
|
|
18
|
-
loadLanguages([language]);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
if (languageMap.has(lang)) {
|
|
23
|
-
ensureLoaded(languageMap.get(lang)!);
|
|
24
|
-
} else if (lang === 'astro') {
|
|
25
|
-
ensureLoaded('typescript');
|
|
26
|
-
addAstro(Prism);
|
|
27
|
-
} else {
|
|
28
|
-
ensureLoaded('markup-templating'); // Prism expects this to exist for a number of other langs
|
|
29
|
-
ensureLoaded(lang);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (lang && !Prism.languages[lang]) {
|
|
33
|
-
// eslint-disable-next-line no-console
|
|
34
|
-
console.warn(`Unable to load the language: ${lang}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const grammar = Prism.languages[lang];
|
|
38
|
-
let html = code;
|
|
39
|
-
if (grammar) {
|
|
40
|
-
html = Prism.highlight(code, grammar, lang);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { classLanguage, html };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
5
|
type MaybeString = string | null | undefined;
|
|
47
6
|
|
|
48
7
|
/** */
|
|
@@ -52,7 +11,7 @@ function transformer(className: MaybeString) {
|
|
|
52
11
|
let { lang, value } = node;
|
|
53
12
|
node.type = 'html';
|
|
54
13
|
|
|
55
|
-
let { html, classLanguage } =
|
|
14
|
+
let { html, classLanguage } = runHighlighterWithAstro(lang, value);
|
|
56
15
|
let classes = [classLanguage];
|
|
57
16
|
if (className) {
|
|
58
17
|
classes.push(className);
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type * as hast from 'hast';
|
|
|
2
2
|
import type * as mdast from 'mdast';
|
|
3
3
|
import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
|
|
4
4
|
import type * as unified from 'unified';
|
|
5
|
+
import type { VFile } from 'vfile';
|
|
5
6
|
|
|
6
7
|
export type { Node } from 'unist';
|
|
7
8
|
|
|
@@ -41,21 +42,23 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
|
|
41
42
|
$?: {
|
|
42
43
|
scopedClassName: string | null;
|
|
43
44
|
};
|
|
45
|
+
isAstroFlavoredMd?: boolean;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
export interface
|
|
48
|
+
export interface MarkdownHeading {
|
|
47
49
|
depth: number;
|
|
48
50
|
slug: string;
|
|
49
51
|
text: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface MarkdownMetadata {
|
|
53
|
-
|
|
55
|
+
headings: MarkdownHeading[];
|
|
54
56
|
source: string;
|
|
55
57
|
html: string;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface MarkdownRenderingResult {
|
|
59
61
|
metadata: MarkdownMetadata;
|
|
62
|
+
vfile: VFile;
|
|
60
63
|
code: string;
|
|
61
64
|
}
|
package/test/autolinking.test.js
CHANGED
|
@@ -2,91 +2,107 @@ import { renderMarkdown } from '../dist/index.js';
|
|
|
2
2
|
import chai from 'chai';
|
|
3
3
|
|
|
4
4
|
describe('autolinking', () => {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
describe('plain md', () => {
|
|
6
|
+
it('autolinks URLs starting with a protocol in plain text', async () => {
|
|
7
|
+
const { code } = await renderMarkdown(`See https://example.com for more.`, {});
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
chai
|
|
10
|
+
.expect(code.replace(/\n/g, ''))
|
|
11
|
+
.to.equal(`<p>See <a href="https://example.com">https://example.com</a> for more.</p>`);
|
|
12
|
+
});
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
it('autolinks URLs starting with "www." in plain text', async () => {
|
|
15
|
+
const { code } = await renderMarkdown(`See www.example.com for more.`, {});
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
chai
|
|
18
|
+
.expect(code.trim())
|
|
19
|
+
.to.equal(`<p>See <a href="http://www.example.com">www.example.com</a> for more.</p>`);
|
|
20
|
+
});
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
chai
|
|
28
|
-
.expect(code.trim())
|
|
29
|
-
.to.equal(
|
|
30
|
-
`<p>See <code is:raw>https://example.com</code> or ` +
|
|
31
|
-
`<code is:raw>www.example.com</code> for more.</p>`
|
|
22
|
+
it('does not autolink URLs in code blocks', async () => {
|
|
23
|
+
const { code } = await renderMarkdown(
|
|
24
|
+
'See `https://example.com` or `www.example.com` for more.',
|
|
25
|
+
{}
|
|
32
26
|
);
|
|
27
|
+
|
|
28
|
+
chai
|
|
29
|
+
.expect(code.trim())
|
|
30
|
+
.to.equal(
|
|
31
|
+
`<p>See <code>https://example.com</code> or ` +
|
|
32
|
+
`<code>www.example.com</code> for more.</p>`
|
|
33
|
+
);
|
|
34
|
+
});
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
'Example:\n```\nGo to https://example.com or www.example.com now.\n```',
|
|
38
|
-
{}
|
|
39
|
-
);
|
|
37
|
+
describe('astro-flavored md', () => {
|
|
38
|
+
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
it('does not autolink URLs in code blocks', async () => {
|
|
41
|
+
const { code } = await renderAstroMd(
|
|
42
|
+
'See `https://example.com` or `www.example.com` for more.',
|
|
43
|
+
{}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
chai
|
|
47
|
+
.expect(code.trim())
|
|
48
|
+
.to.equal(
|
|
49
|
+
`<p>See <code is:raw>https://example.com</code> or ` +
|
|
50
|
+
`<code is:raw>www.example.com</code> for more.</p>`
|
|
51
|
+
);
|
|
52
|
+
});
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`<a test href="https://example.com">https://example.com</a>`,
|
|
51
|
-
{}
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
chai
|
|
55
|
-
.expect(code.replace(/\n/g, ''))
|
|
56
|
-
.to.equal(
|
|
57
|
-
`<p>See <a href="http://example.com">http://example.com</a> or ` +
|
|
58
|
-
`<a test href="https://example.com">https://example.com</a></p>`
|
|
54
|
+
it('does not autolink URLs in fenced code blocks', async () => {
|
|
55
|
+
const { code } = await renderAstroMd(
|
|
56
|
+
'Example:\n```\nGo to https://example.com or www.example.com now.\n```'
|
|
59
57
|
);
|
|
60
|
-
});
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`<p>See <a href="https://www.example.com">www.example.com</a> or ` +
|
|
73
|
-
`<a test href="https://www.example.com">www.example.com</a></p>`
|
|
59
|
+
chai
|
|
60
|
+
.expect(code)
|
|
61
|
+
.to.contain(`<pre is:raw`)
|
|
62
|
+
.to.contain(`Go to https://example.com or www.example.com now.`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not autolink URLs starting with a protocol when nested inside links', async () => {
|
|
66
|
+
const { code } = await renderAstroMd(
|
|
67
|
+
`See [http://example.com](http://example.com) or ` +
|
|
68
|
+
`<a test href="https://example.com">https://example.com</a>`
|
|
74
69
|
);
|
|
75
|
-
});
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`</strong></a>`
|
|
71
|
+
chai
|
|
72
|
+
.expect(code.replace(/\n/g, ''))
|
|
73
|
+
.to.equal(
|
|
74
|
+
`<p>See <a href="http://example.com">http://example.com</a> or ` +
|
|
75
|
+
`<a test href="https://example.com">https://example.com</a></p>`
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not autolink URLs starting with "www." when nested inside links', async () => {
|
|
80
|
+
const { code } = await renderAstroMd(
|
|
81
|
+
`See [www.example.com](https://www.example.com) or ` +
|
|
82
|
+
`<a test href="https://www.example.com">www.example.com</a>`
|
|
90
83
|
);
|
|
84
|
+
|
|
85
|
+
chai
|
|
86
|
+
.expect(code.replace(/\n/g, ''))
|
|
87
|
+
.to.equal(
|
|
88
|
+
`<p>See <a href="https://www.example.com">www.example.com</a> or ` +
|
|
89
|
+
`<a test href="https://www.example.com">www.example.com</a></p>`
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not autolink URLs when nested several layers deep inside links', async () => {
|
|
94
|
+
const { code } = await renderAstroMd(
|
|
95
|
+
`<a href="https://www.example.com">**Visit _our www.example.com or ` +
|
|
96
|
+
`http://localhost pages_ for more!**</a>`
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
chai
|
|
100
|
+
.expect(code.replace(/\n/g, ''))
|
|
101
|
+
.to.equal(
|
|
102
|
+
`<a href="https://www.example.com"><strong>` +
|
|
103
|
+
`Visit <em>our www.example.com or http://localhost pages</em> for more!` +
|
|
104
|
+
`</strong></a>`
|
|
105
|
+
);
|
|
106
|
+
});
|
|
91
107
|
});
|
|
92
108
|
});
|
package/test/components.test.js
CHANGED
|
@@ -2,32 +2,34 @@ import { renderMarkdown } from '../dist/index.js';
|
|
|
2
2
|
import chai from 'chai';
|
|
3
3
|
|
|
4
4
|
describe('components', () => {
|
|
5
|
+
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
|
|
6
|
+
|
|
5
7
|
it('should be able to serialize string', async () => {
|
|
6
|
-
const { code } = await
|
|
8
|
+
const { code } = await renderAstroMd(`<Component str="cool!" />`);
|
|
7
9
|
|
|
8
10
|
chai.expect(code).to.equal(`<Component str="cool!" />`);
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
it('should be able to serialize boolean attribute', async () => {
|
|
12
|
-
const { code } = await
|
|
14
|
+
const { code } = await renderAstroMd(`<Component bool={true} />`);
|
|
13
15
|
|
|
14
16
|
chai.expect(code).to.equal(`<Component bool={true} />`);
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
it('should be able to serialize array', async () => {
|
|
18
|
-
const { code } = await
|
|
20
|
+
const { code } = await renderAstroMd(`<Component prop={["a", "b", "c"]} />`);
|
|
19
21
|
|
|
20
22
|
chai.expect(code).to.equal(`<Component prop={["a", "b", "c"]} />`);
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
it('should be able to serialize object', async () => {
|
|
24
|
-
const { code } = await
|
|
26
|
+
const { code } = await renderAstroMd(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
|
|
25
27
|
|
|
26
28
|
chai.expect(code).to.equal(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
it('should be able to serialize empty attribute', async () => {
|
|
30
|
-
const { code } = await
|
|
32
|
+
const { code } = await renderAstroMd(`<Component empty />`);
|
|
31
33
|
|
|
32
34
|
chai.expect(code).to.equal(`<Component empty />`);
|
|
33
35
|
});
|
|
@@ -35,25 +37,25 @@ describe('components', () => {
|
|
|
35
37
|
// Notable omission: shorthand attribute
|
|
36
38
|
|
|
37
39
|
it('should be able to serialize spread attribute', async () => {
|
|
38
|
-
const { code } = await
|
|
40
|
+
const { code } = await renderAstroMd(`<Component {...spread} />`);
|
|
39
41
|
|
|
40
42
|
chai.expect(code).to.equal(`<Component {...spread} />`);
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it('should allow client:* directives', async () => {
|
|
44
|
-
const { code } = await
|
|
46
|
+
const { code } = await renderAstroMd(`<Component client:load />`);
|
|
45
47
|
|
|
46
48
|
chai.expect(code).to.equal(`<Component client:load />`);
|
|
47
49
|
});
|
|
48
50
|
|
|
49
51
|
it('should normalize children', async () => {
|
|
50
|
-
const { code } = await
|
|
52
|
+
const { code } = await renderAstroMd(`<Component bool={true}>Hello world!</Component>`);
|
|
51
53
|
|
|
52
54
|
chai.expect(code).to.equal(`<Component bool={true}>Hello world!</Component>`);
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
it('should be able to nest components', async () => {
|
|
56
|
-
const { code } = await
|
|
58
|
+
const { code } = await renderAstroMd(
|
|
57
59
|
`<Component bool={true}><Component>Hello world!</Component></Component>`,
|
|
58
60
|
{}
|
|
59
61
|
);
|
|
@@ -64,7 +66,7 @@ describe('components', () => {
|
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
it('should allow markdown without many spaces', async () => {
|
|
67
|
-
const { code } = await
|
|
69
|
+
const { code } = await renderAstroMd(
|
|
68
70
|
`<Component>
|
|
69
71
|
# Hello world!
|
|
70
72
|
</Component>`,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderMarkdown } from '../dist/index.js';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
|
|
4
|
+
describe('entities', () => {
|
|
5
|
+
it('should not unescape entities in regular Markdown', async () => {
|
|
6
|
+
const { code } = await renderMarkdown(`<i>This should NOT be italic</i>`, {
|
|
7
|
+
isAstroFlavoredMd: false,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
expect(code).to.equal(`<p><i>This should NOT be italic</i></p>`);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should not escape entities in code blocks twice in Astro-flavored markdown', async () => {
|
|
14
|
+
const { code } = await renderMarkdown(`\`\`\`astro\n<h1>{x && x.name || ''}!</h1>\n\`\`\``, {
|
|
15
|
+
isAstroFlavoredMd: true,
|
|
16
|
+
syntaxHighlight: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(code).to.equal(
|
|
20
|
+
`<pre is:raw><code class="language-astro"><h1>{x && x.name || ''}!</h1>\n</code></pre>`
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/test/expressions.test.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { renderMarkdown } from '../dist/index.js';
|
|
2
|
-
import chai
|
|
2
|
+
import chai from 'chai';
|
|
3
3
|
|
|
4
4
|
describe('expressions', () => {
|
|
5
|
+
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
|
|
6
|
+
|
|
5
7
|
it('should be able to serialize bare expression', async () => {
|
|
6
|
-
const { code } = await
|
|
8
|
+
const { code } = await renderAstroMd(`{a}`, {});
|
|
7
9
|
|
|
8
10
|
chai.expect(code).to.equal(`{a}`);
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
it('should be able to serialize expression inside component', async () => {
|
|
12
|
-
const { code } = await
|
|
14
|
+
const { code } = await renderAstroMd(`<Component>{a}</Component>`, {});
|
|
13
15
|
|
|
14
16
|
chai.expect(code).to.equal(`<Component>{a}</Component>`);
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
it('should be able to serialize expression inside markdown', async () => {
|
|
18
|
-
const { code } = await
|
|
20
|
+
const { code } = await renderAstroMd(`# {frontmatter.title}`, {});
|
|
19
21
|
|
|
20
22
|
chai
|
|
21
23
|
.expect(code)
|
|
@@ -23,7 +25,7 @@ describe('expressions', () => {
|
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it('should be able to serialize complex expression inside markdown', async () => {
|
|
26
|
-
const { code } = await
|
|
28
|
+
const { code } = await renderAstroMd(`# Hello {frontmatter.name}`, {});
|
|
27
29
|
|
|
28
30
|
chai
|
|
29
31
|
.expect(code)
|
|
@@ -31,7 +33,7 @@ describe('expressions', () => {
|
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
it('should be able to serialize complex expression with markup inside markdown', async () => {
|
|
34
|
-
const { code } = await
|
|
36
|
+
const { code } = await renderAstroMd(`# Hello <span>{frontmatter.name}</span>`, {});
|
|
35
37
|
|
|
36
38
|
chai
|
|
37
39
|
.expect(code)
|
|
@@ -41,7 +43,7 @@ describe('expressions', () => {
|
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it('should be able to avoid evaluating JSX-like expressions in an inline code & generate a slug for id', async () => {
|
|
44
|
-
const { code } = await
|
|
46
|
+
const { code } = await renderAstroMd(`# \`{frontmatter.title}\``, {});
|
|
45
47
|
|
|
46
48
|
chai
|
|
47
49
|
.expect(code)
|
|
@@ -49,17 +51,17 @@ describe('expressions', () => {
|
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => {
|
|
52
|
-
const { code } = await
|
|
54
|
+
const { code } = await renderAstroMd(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {});
|
|
53
55
|
|
|
54
56
|
chai
|
|
55
57
|
.expect(code)
|
|
56
58
|
.to.equal(
|
|
57
|
-
'<h1 id="-foo--is-a-shorthand-for--foo-foo
|
|
59
|
+
'<h1 id="-foo--is-a-shorthand-for--foo-foo"><code is:raw>{ foo }</code> is a shorthand for <code is:raw>{ foo: foo }</code></h1>'
|
|
58
60
|
);
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
it('should be able to avoid evaluating JSX-like expressions & escape HTML tag characters in inline codes', async () => {
|
|
62
|
-
const { code } = await
|
|
64
|
+
const { code } = await renderAstroMd(
|
|
63
65
|
`###### \`{}\` is equivalent to \`Record<never, never>\` <small>(at TypeScript v{frontmatter.version})</small>`,
|
|
64
66
|
{}
|
|
65
67
|
);
|
|
@@ -72,7 +74,7 @@ describe('expressions', () => {
|
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
it('should be able to encode ampersand characters in code blocks', async () => {
|
|
75
|
-
const { code } = await
|
|
77
|
+
const { code } = await renderAstroMd(
|
|
76
78
|
'The ampersand in ` ` must be encoded in code blocks.',
|
|
77
79
|
{}
|
|
78
80
|
);
|
|
@@ -85,7 +87,7 @@ describe('expressions', () => {
|
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
it('should be able to encode ampersand characters in fenced code blocks', async () => {
|
|
88
|
-
const { code } = await
|
|
90
|
+
const { code } = await renderAstroMd(`
|
|
89
91
|
\`\`\`md
|
|
90
92
|
The ampersand in \` \` must be encoded in code blocks.
|
|
91
93
|
\`\`\`
|
|
@@ -95,7 +97,7 @@ describe('expressions', () => {
|
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
it('should be able to serialize function expression', async () => {
|
|
98
|
-
const { code } = await
|
|
100
|
+
const { code } = await renderAstroMd(
|
|
99
101
|
`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`,
|
|
100
102
|
{}
|
|
101
103
|
);
|
|
@@ -104,13 +106,13 @@ describe('expressions', () => {
|
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
it('should unwrap HTML comments in inline code blocks', async () => {
|
|
107
|
-
const { code } = await
|
|
109
|
+
const { code } = await renderAstroMd(`\`{/*<!-- HTML comment -->*/}\``);
|
|
108
110
|
|
|
109
111
|
chai.expect(code).to.equal('<p><code is:raw><!-- HTML comment --></code></p>');
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
it('should unwrap HTML comments in code fences', async () => {
|
|
113
|
-
const { code } = await
|
|
115
|
+
const { code } = await renderAstroMd(
|
|
114
116
|
`
|
|
115
117
|
\`\`\`
|
|
116
118
|
<!-- HTML comment -->
|
package/test/strictness.test.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { renderMarkdown } from '../dist/index.js';
|
|
2
2
|
import chai from 'chai';
|
|
3
3
|
|
|
4
|
-
describe('strictness', () => {
|
|
4
|
+
describe('strictness in Astro-flavored markdown', () => {
|
|
5
|
+
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
|
|
6
|
+
|
|
5
7
|
it('should allow self-closing HTML tags (void elements)', async () => {
|
|
6
|
-
const { code } = await
|
|
8
|
+
const { code } = await renderAstroMd(
|
|
7
9
|
`Use self-closing void elements<br>like word<wbr>break and images: <img src="hi.jpg">`,
|
|
8
10
|
{}
|
|
9
11
|
);
|
|
@@ -17,25 +19,25 @@ describe('strictness', () => {
|
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
it('should allow attribute names starting with ":" after element names', async () => {
|
|
20
|
-
const { code } = await
|
|
22
|
+
const { code } = await renderAstroMd(`<div :class="open ? '' : 'hidden'">Test</div>`, {});
|
|
21
23
|
|
|
22
24
|
chai.expect(code.trim()).to.equal(`<div :class="open ? '' : 'hidden'">Test</div>`);
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it('should allow attribute names starting with ":" after local element names', async () => {
|
|
26
|
-
const { code } = await
|
|
28
|
+
const { code } = await renderAstroMd(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`, {});
|
|
27
29
|
|
|
28
30
|
chai.expect(code.trim()).to.equal(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`);
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
it('should allow attribute names starting with ":" after attribute names', async () => {
|
|
32
|
-
const { code } = await
|
|
34
|
+
const { code } = await renderAstroMd(`<input type="text" disabled :placeholder="hi">`, {});
|
|
33
35
|
|
|
34
36
|
chai.expect(code.trim()).to.equal(`<input type="text" disabled :placeholder="hi" />`);
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
it('should allow attribute names starting with ":" after local attribute names', async () => {
|
|
38
|
-
const { code } = await
|
|
40
|
+
const { code } = await renderAstroMd(
|
|
39
41
|
`<input type="text" x-test:disabled :placeholder="hi">`,
|
|
40
42
|
{}
|
|
41
43
|
);
|
|
@@ -44,19 +46,19 @@ describe('strictness', () => {
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
it('should allow attribute names starting with ":" after attribute values', async () => {
|
|
47
|
-
const { code } = await
|
|
49
|
+
const { code } = await renderAstroMd(`<input type="text" :placeholder="placeholder">`, {});
|
|
48
50
|
|
|
49
51
|
chai.expect(code.trim()).to.equal(`<input type="text" :placeholder="placeholder" />`);
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
it('should allow attribute names starting with "@" after element names', async () => {
|
|
53
|
-
const { code } = await
|
|
55
|
+
const { code } = await renderAstroMd(`<button @click="handleClick">Test</button>`, {});
|
|
54
56
|
|
|
55
57
|
chai.expect(code.trim()).to.equal(`<button @click="handleClick">Test</button>`);
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
it('should allow attribute names starting with "@" after local element names', async () => {
|
|
59
|
-
const { code } = await
|
|
61
|
+
const { code } = await renderAstroMd(
|
|
60
62
|
`<button.local @click="handleClick">Test</button.local>`,
|
|
61
63
|
{}
|
|
62
64
|
);
|
|
@@ -65,16 +67,13 @@ describe('strictness', () => {
|
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
it('should allow attribute names starting with "@" after attribute names', async () => {
|
|
68
|
-
const { code } = await
|
|
69
|
-
`<button disabled @click="handleClick">Test</button>`,
|
|
70
|
-
{}
|
|
71
|
-
);
|
|
70
|
+
const { code } = await renderAstroMd(`<button disabled @click="handleClick">Test</button>`, {});
|
|
72
71
|
|
|
73
72
|
chai.expect(code.trim()).to.equal(`<button disabled @click="handleClick">Test</button>`);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
it('should allow attribute names starting with "@" after local attribute names', async () => {
|
|
77
|
-
const { code } = await
|
|
76
|
+
const { code } = await renderAstroMd(
|
|
78
77
|
`<button x-test:disabled @click="handleClick">Test</button>`,
|
|
79
78
|
{}
|
|
80
79
|
);
|
|
@@ -83,7 +82,7 @@ describe('strictness', () => {
|
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
it('should allow attribute names starting with "@" after attribute values', async () => {
|
|
86
|
-
const { code } = await
|
|
85
|
+
const { code } = await renderAstroMd(
|
|
87
86
|
`<button type="submit" @click="handleClick">Test</button>`,
|
|
88
87
|
{}
|
|
89
88
|
);
|
|
@@ -92,7 +91,7 @@ describe('strictness', () => {
|
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
it('should allow attribute names containing dots', async () => {
|
|
95
|
-
const { code } = await
|
|
94
|
+
const { code } = await renderAstroMd(`<input x-on:input.debounce.500ms="fetchResults">`, {});
|
|
96
95
|
|
|
97
96
|
chai.expect(code.trim()).to.equal(`<input x-on:input.debounce.500ms="fetchResults" />`);
|
|
98
97
|
});
|