@astrojs/markdown-remark 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/index.js +18 -0
- package/dist/rehype-collect-headers.js +11 -8
- package/dist/rehype-escape.js +3 -0
- package/dist/rehype-jsx.js +37 -61
- package/dist/remark-mdxish.js +36 -1
- package/dist/remark-shiki.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +27 -0
- package/src/rehype-collect-headers.ts +14 -9
- package/src/rehype-escape.ts +5 -0
- package/src/rehype-jsx.ts +39 -42
- package/src/remark-mdxish.ts +42 -1
- package/test/components.test.js +11 -6
- package/test/expressions.test.js +33 -2
- package/test/strictness.test.js +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @astrojs/markdown-remark
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#3502](https://github.com/withastro/astro/pull/3502) [`939fe159`](https://github.com/withastro/astro/commit/939fe159255cecf1cab5c1b3da2670d30ac8e4a7) Thanks [@nokazn](https://github.com/nokazn)! - Fix cases for JSX-like expressions in code blocks of headings
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#3514](https://github.com/withastro/astro/pull/3514) [`6c955ca6`](https://github.com/withastro/astro/commit/6c955ca643a7a071609ce8a5258cc7faf5a636b2) Thanks [@hippotastic](https://github.com/hippotastic)! - Fix Markdown errors missing source filename
|
|
12
|
+
|
|
13
|
+
* [#3516](https://github.com/withastro/astro/pull/3516) [`30578015`](https://github.com/withastro/astro/commit/30578015919e019cd8dd354288a45c1fc63bd01f) Thanks [@hippotastic](https://github.com/hippotastic)! - Fix: Allow self-closing tags in Markdown
|
|
14
|
+
|
|
15
|
+
## 0.10.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#3486](https://github.com/withastro/astro/pull/3486) [`119ecf8d`](https://github.com/withastro/astro/commit/119ecf8d469f034eaf1b1217523954d29f492cb6) Thanks [@hippotastic](https://github.com/hippotastic)! - Fix components in markdown regressions
|
|
20
|
+
|
|
21
|
+
## 0.10.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [#3444](https://github.com/withastro/astro/pull/3444) [`51db2b9b`](https://github.com/withastro/astro/commit/51db2b9b4efd899bdd7efc481a5f226b3b040614) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: markdown imports failing due to internal dependency issue
|
|
26
|
+
|
|
3
27
|
## 0.10.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,7 @@ async function renderMarkdown(content, opts = {}) {
|
|
|
80
80
|
const vfile = await parser.use([rehypeCollectHeaders]).use(rehypeStringify, { allowDangerousHtml: true }).process(input);
|
|
81
81
|
result = vfile.toString();
|
|
82
82
|
} catch (err) {
|
|
83
|
+
err = prefixError(err, `Failed to parse Markdown file "${input.path}"`);
|
|
83
84
|
console.error(err);
|
|
84
85
|
throw err;
|
|
85
86
|
}
|
|
@@ -88,6 +89,23 @@ async function renderMarkdown(content, opts = {}) {
|
|
|
88
89
|
code: result.toString()
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
function prefixError(err, prefix) {
|
|
93
|
+
if (err && err.message) {
|
|
94
|
+
try {
|
|
95
|
+
err.message = `${prefix}:
|
|
96
|
+
${err.message}`;
|
|
97
|
+
return err;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ""}`);
|
|
102
|
+
try {
|
|
103
|
+
wrappedError.stack = err.stack;
|
|
104
|
+
wrappedError.cause = err;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
}
|
|
107
|
+
return wrappedError;
|
|
108
|
+
}
|
|
91
109
|
export {
|
|
92
110
|
DEFAULT_REHYPE_PLUGINS,
|
|
93
111
|
DEFAULT_REMARK_PLUGINS,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
+
import { toHtml } from "hast-util-to-html";
|
|
2
3
|
import Slugger from "github-slugger";
|
|
3
4
|
function createCollectHeaders() {
|
|
4
5
|
const headers = [];
|
|
@@ -15,29 +16,31 @@ function createCollectHeaders() {
|
|
|
15
16
|
if (!level)
|
|
16
17
|
return;
|
|
17
18
|
const depth = Number.parseInt(level);
|
|
18
|
-
let raw = "";
|
|
19
19
|
let text = "";
|
|
20
20
|
let isJSX = false;
|
|
21
|
-
visit(node, (child) => {
|
|
22
|
-
if (child.type === "element") {
|
|
21
|
+
visit(node, (child, _2, parent) => {
|
|
22
|
+
if (child.type === "element" || parent == null) {
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (child.type === "raw") {
|
|
26
26
|
if (child.value.startsWith("\n<") || child.value.endsWith(">\n")) {
|
|
27
|
-
raw += child.value.replace(/^\n|\n$/g, "");
|
|
28
27
|
return;
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
if (child.type === "text" || child.type === "raw") {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if ((/* @__PURE__ */ new Set(["code", "pre"])).has(parent.tagName)) {
|
|
32
|
+
text += child.value;
|
|
33
|
+
} else {
|
|
34
|
+
text += child.value.replace(/\{/g, "${");
|
|
35
|
+
isJSX = isJSX || child.value.includes("{");
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
});
|
|
37
39
|
node.properties = node.properties || {};
|
|
38
40
|
if (typeof node.properties.id !== "string") {
|
|
39
41
|
if (isJSX) {
|
|
40
|
-
|
|
42
|
+
const raw = toHtml(node.children, { allowDangerousHtml: true }).replace(/\n(<)/g, "<").replace(/(>)\n/g, ">");
|
|
43
|
+
node.properties.id = `$$slug(\`${text}\`)`;
|
|
41
44
|
node.type = "raw";
|
|
42
45
|
node.value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
|
|
43
46
|
} else {
|
package/dist/rehype-escape.js
CHANGED
|
@@ -4,6 +4,9 @@ function rehypeEscape() {
|
|
|
4
4
|
return visit(node, "element", (el) => {
|
|
5
5
|
if (el.tagName === "code" || el.tagName === "pre") {
|
|
6
6
|
el.properties["is:raw"] = true;
|
|
7
|
+
visit(el, "raw", (raw) => {
|
|
8
|
+
raw.value = raw.value.replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
});
|
|
7
10
|
}
|
|
8
11
|
return el;
|
|
9
12
|
});
|
package/dist/rehype-jsx.js
CHANGED
|
@@ -1,69 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
-
var __spreadValues = (a, b) => {
|
|
9
|
-
for (var prop in b || (b = {}))
|
|
10
|
-
if (__hasOwnProp.call(b, prop))
|
|
11
|
-
__defNormalProp(a, prop, b[prop]);
|
|
12
|
-
if (__getOwnPropSymbols)
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
16
|
-
}
|
|
17
|
-
return a;
|
|
18
|
-
};
|
|
19
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
-
import { map } from "unist-util-map";
|
|
21
|
-
const MDX_ELEMENTS = /* @__PURE__ */ new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
const MDX_ELEMENTS = ["mdxJsxFlowElement", "mdxJsxTextElement"];
|
|
22
3
|
function rehypeJsx() {
|
|
23
4
|
return function(node) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
if (!entry.name) {
|
|
41
|
-
return acc + ` ${attr}`;
|
|
42
|
-
}
|
|
43
|
-
return acc + ` ${entry.name}${attr ? "=" : ""}${attr}`;
|
|
44
|
-
}, "");
|
|
45
|
-
if (child.children.length === 0) {
|
|
46
|
-
return {
|
|
47
|
-
type: "raw",
|
|
48
|
-
value: `<${child.name}${attrs} />`
|
|
49
|
-
};
|
|
5
|
+
visit(node, "element", (child) => {
|
|
6
|
+
child.tagName = `${child.tagName}`;
|
|
7
|
+
});
|
|
8
|
+
visit(node, MDX_ELEMENTS, (child, index, parent) => {
|
|
9
|
+
if (index === null || !Boolean(parent))
|
|
10
|
+
return;
|
|
11
|
+
const attrs = child.attributes.reduce((acc, entry) => {
|
|
12
|
+
let attr = entry.value;
|
|
13
|
+
if (attr && typeof attr === "object") {
|
|
14
|
+
attr = `{${attr.value}}`;
|
|
15
|
+
} else if (attr && entry.type === "mdxJsxExpressionAttribute") {
|
|
16
|
+
attr = `{${attr}}`;
|
|
17
|
+
} else if (attr === null) {
|
|
18
|
+
attr = "";
|
|
19
|
+
} else if (typeof attr === "string") {
|
|
20
|
+
attr = `"${attr}"`;
|
|
50
21
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
if (!entry.name) {
|
|
23
|
+
return acc + ` ${attr}`;
|
|
24
|
+
}
|
|
25
|
+
return acc + ` ${entry.name}${attr ? "=" : ""}${attr}`;
|
|
26
|
+
}, "");
|
|
27
|
+
if (child.children.length === 0) {
|
|
28
|
+
child.type = "raw";
|
|
29
|
+
child.value = `<${child.name}${attrs} />`;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const openingTag = {
|
|
33
|
+
type: "raw",
|
|
34
|
+
value: `
|
|
54
35
|
<${child.name}${attrs}>`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
};
|
|
37
|
+
const closingTag = {
|
|
38
|
+
type: "raw",
|
|
39
|
+
value: `</${child.name}>
|
|
59
40
|
`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
type: "element",
|
|
63
|
-
tagName: `Fragment`
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
return child;
|
|
41
|
+
};
|
|
42
|
+
parent.children.splice(index, 1, openingTag, ...child.children, closingTag);
|
|
67
43
|
});
|
|
68
44
|
};
|
|
69
45
|
}
|
package/dist/remark-mdxish.js
CHANGED
|
@@ -3,13 +3,48 @@ import { mdxFromMarkdown, mdxToMarkdown } from "./mdast-util-mdxish.js";
|
|
|
3
3
|
function remarkMdxish(options = {}) {
|
|
4
4
|
const data = this.data();
|
|
5
5
|
add("micromarkExtensions", mdxjs(options));
|
|
6
|
-
add("fromMarkdownExtensions", mdxFromMarkdown());
|
|
6
|
+
add("fromMarkdownExtensions", makeFromMarkdownLessStrict(mdxFromMarkdown()));
|
|
7
7
|
add("toMarkdownExtensions", mdxToMarkdown());
|
|
8
8
|
function add(field, value) {
|
|
9
9
|
const list = data[field] ? data[field] : data[field] = [];
|
|
10
10
|
list.push(value);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function makeFromMarkdownLessStrict(extensions) {
|
|
14
|
+
extensions.forEach((extension) => {
|
|
15
|
+
["mdxJsxFlowTag", "mdxJsxTextTag"].forEach((exitHandler) => {
|
|
16
|
+
if (!extension.exit || !extension.exit[exitHandler])
|
|
17
|
+
return;
|
|
18
|
+
extension.exit[exitHandler] = chainHandlers(fixSelfClosing, extension.exit[exitHandler]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return extensions;
|
|
22
|
+
}
|
|
23
|
+
const selfClosingTags = /* @__PURE__ */ new Set([
|
|
24
|
+
"area",
|
|
25
|
+
"base",
|
|
26
|
+
"br",
|
|
27
|
+
"col",
|
|
28
|
+
"embed",
|
|
29
|
+
"hr",
|
|
30
|
+
"img",
|
|
31
|
+
"input",
|
|
32
|
+
"link",
|
|
33
|
+
"meta",
|
|
34
|
+
"source",
|
|
35
|
+
"track",
|
|
36
|
+
"wbr"
|
|
37
|
+
]);
|
|
38
|
+
function fixSelfClosing() {
|
|
39
|
+
const tag = this.getData("mdxJsxTag");
|
|
40
|
+
if (tag.name && selfClosingTags.has(tag.name))
|
|
41
|
+
tag.selfClosing = true;
|
|
42
|
+
}
|
|
43
|
+
function chainHandlers(...handlers) {
|
|
44
|
+
return function handlerChain(token) {
|
|
45
|
+
handlers.forEach((handler) => handler.call(this, token));
|
|
46
|
+
};
|
|
47
|
+
}
|
|
13
48
|
export {
|
|
14
49
|
remarkMdxish as default
|
|
15
50
|
};
|
package/dist/remark-shiki.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ShikiConfig } from './types.js';
|
|
2
|
-
declare const remarkShiki: ({ langs, theme, wrap }: ShikiConfig, scopedClassName?: string | null
|
|
2
|
+
declare const remarkShiki: ({ langs, theme, wrap }: ShikiConfig, scopedClassName?: string | null) => Promise<() => (tree: any) => void>;
|
|
3
3
|
export default remarkShiki;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -109,6 +109,9 @@ export async function renderMarkdown(
|
|
|
109
109
|
.process(input);
|
|
110
110
|
result = vfile.toString();
|
|
111
111
|
} catch (err) {
|
|
112
|
+
// Ensure that the error message contains the input filename
|
|
113
|
+
// to make it easier for the user to fix the issue
|
|
114
|
+
err = prefixError(err, `Failed to parse Markdown file "${input.path}"`);
|
|
112
115
|
console.error(err);
|
|
113
116
|
throw err;
|
|
114
117
|
}
|
|
@@ -118,3 +121,27 @@ export async function renderMarkdown(
|
|
|
118
121
|
code: result.toString(),
|
|
119
122
|
};
|
|
120
123
|
}
|
|
124
|
+
|
|
125
|
+
function prefixError(err: any, prefix: string) {
|
|
126
|
+
// If the error is an object with a `message` property, attempt to prefix the message
|
|
127
|
+
if (err && err.message) {
|
|
128
|
+
try {
|
|
129
|
+
err.message = `${prefix}:\n${err.message}`;
|
|
130
|
+
return err;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Any errors here are ok, there's fallback code below
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If that failed, create a new error with the desired message and attempt to keep the stack
|
|
137
|
+
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
|
|
138
|
+
try {
|
|
139
|
+
wrappedError.stack = err.stack;
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
wrappedError.cause = err;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// It's ok if we could not set the stack or cause - the message is the most important part
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return wrappedError;
|
|
147
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { visit } from 'unist-util-visit';
|
|
2
|
+
import { toHtml } from 'hast-util-to-html';
|
|
2
3
|
import Slugger from 'github-slugger';
|
|
3
4
|
|
|
4
5
|
import type { MarkdownHeader, RehypePlugin } from './types.js';
|
|
@@ -17,32 +18,36 @@ export default function createCollectHeaders() {
|
|
|
17
18
|
if (!level) return;
|
|
18
19
|
const depth = Number.parseInt(level);
|
|
19
20
|
|
|
20
|
-
let raw = '';
|
|
21
21
|
let text = '';
|
|
22
22
|
let isJSX = false;
|
|
23
|
-
visit(node, (child) => {
|
|
24
|
-
if (child.type === 'element') {
|
|
23
|
+
visit(node, (child, _, parent) => {
|
|
24
|
+
if (child.type === 'element' || parent == null) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (child.type === 'raw') {
|
|
28
|
-
// HACK: serialized JSX from internal plugins, ignore these for slug
|
|
29
28
|
if (child.value.startsWith('\n<') || child.value.endsWith('>\n')) {
|
|
30
|
-
raw += child.value.replace(/^\n|\n$/g, '');
|
|
31
29
|
return;
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
if (child.type === 'text' || child.type === 'raw') {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
if (new Set(['code', 'pre']).has(parent.tagName)) {
|
|
34
|
+
text += child.value;
|
|
35
|
+
} else {
|
|
36
|
+
text += child.value.replace(/\{/g, '${');
|
|
37
|
+
isJSX = isJSX || child.value.includes('{');
|
|
38
|
+
}
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
node.properties = node.properties || {};
|
|
42
43
|
if (typeof node.properties.id !== 'string') {
|
|
43
44
|
if (isJSX) {
|
|
45
|
+
// HACK: serialized JSX from internal plugins, ignore these for slug
|
|
46
|
+
const raw = toHtml(node.children, { allowDangerousHtml: true })
|
|
47
|
+
.replace(/\n(<)/g, '<')
|
|
48
|
+
.replace(/(>)\n/g, '>');
|
|
44
49
|
// HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime
|
|
45
|
-
node.properties.id = `$$slug(\`${text
|
|
50
|
+
node.properties.id = `$$slug(\`${text}\`)`;
|
|
46
51
|
(node as any).type = 'raw';
|
|
47
52
|
(
|
|
48
53
|
node as any
|
package/src/rehype-escape.ts
CHANGED
|
@@ -5,6 +5,11 @@ export default function rehypeEscape(): any {
|
|
|
5
5
|
return visit(node, 'element', (el) => {
|
|
6
6
|
if (el.tagName === 'code' || el.tagName === 'pre') {
|
|
7
7
|
el.properties['is:raw'] = true;
|
|
8
|
+
// Visit all raw children and escape HTML tags to prevent Markdown code
|
|
9
|
+
// like "This is a `<script>` tag" from actually opening a script tag
|
|
10
|
+
visit(el, 'raw', (raw) => {
|
|
11
|
+
raw.value = raw.value.replace(/</g, '<').replace(/>/g, '>');
|
|
12
|
+
});
|
|
8
13
|
}
|
|
9
14
|
return el;
|
|
10
15
|
});
|
package/src/rehype-jsx.ts
CHANGED
|
@@ -1,51 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
2
|
|
|
3
|
-
const MDX_ELEMENTS =
|
|
3
|
+
const MDX_ELEMENTS = ['mdxJsxFlowElement', 'mdxJsxTextElement'];
|
|
4
4
|
export default function rehypeJsx(): any {
|
|
5
5
|
return function (node: any): any {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
const attrs = child.attributes.reduce((acc: any[], entry: any) => {
|
|
12
|
-
let attr = entry.value;
|
|
13
|
-
if (attr && typeof attr === 'object') {
|
|
14
|
-
attr = `{${attr.value}}`;
|
|
15
|
-
} else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
|
|
16
|
-
attr = `{${attr}}`;
|
|
17
|
-
} else if (attr === null) {
|
|
18
|
-
attr = '';
|
|
19
|
-
} else if (typeof attr === 'string') {
|
|
20
|
-
attr = `"${attr}"`;
|
|
21
|
-
}
|
|
22
|
-
if (!entry.name) {
|
|
23
|
-
return acc + ` ${attr}`;
|
|
24
|
-
}
|
|
25
|
-
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
|
|
26
|
-
}, '');
|
|
6
|
+
visit(node, 'element', (child: any) => {
|
|
7
|
+
child.tagName = `${child.tagName}`;
|
|
8
|
+
});
|
|
9
|
+
visit(node, MDX_ELEMENTS, (child: any, index: number | null, parent: any) => {
|
|
10
|
+
if (index === null || !Boolean(parent)) return;
|
|
27
11
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
const attrs = child.attributes.reduce((acc: any[], entry: any) => {
|
|
13
|
+
let attr = entry.value;
|
|
14
|
+
if (attr && typeof attr === 'object') {
|
|
15
|
+
attr = `{${attr.value}}`;
|
|
16
|
+
} else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
|
|
17
|
+
attr = `{${attr}}`;
|
|
18
|
+
} else if (attr === null) {
|
|
19
|
+
attr = '';
|
|
20
|
+
} else if (typeof attr === 'string') {
|
|
21
|
+
attr = `"${attr}"`;
|
|
22
|
+
}
|
|
23
|
+
if (!entry.name) {
|
|
24
|
+
return acc + ` ${attr}`;
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
child.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
...child,
|
|
44
|
-
type: 'element',
|
|
45
|
-
tagName: `Fragment`,
|
|
46
|
-
};
|
|
26
|
+
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
|
|
27
|
+
}, '');
|
|
28
|
+
|
|
29
|
+
if (child.children.length === 0) {
|
|
30
|
+
child.type = 'raw';
|
|
31
|
+
child.value = `<${child.name}${attrs} />`;
|
|
32
|
+
return;
|
|
47
33
|
}
|
|
48
|
-
|
|
34
|
+
|
|
35
|
+
// Replace the current child node with its children
|
|
36
|
+
// wrapped by raw opening and closing tags
|
|
37
|
+
const openingTag = {
|
|
38
|
+
type: 'raw',
|
|
39
|
+
value: `\n<${child.name}${attrs}>`,
|
|
40
|
+
};
|
|
41
|
+
const closingTag = {
|
|
42
|
+
type: 'raw',
|
|
43
|
+
value: `</${child.name}>\n`,
|
|
44
|
+
};
|
|
45
|
+
parent.children.splice(index, 1, openingTag, ...child.children, closingTag);
|
|
49
46
|
});
|
|
50
47
|
};
|
|
51
48
|
}
|
package/src/remark-mdxish.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { mdxjs } from 'micromark-extension-mdxjs';
|
|
2
2
|
import { mdxFromMarkdown, mdxToMarkdown } from './mdast-util-mdxish.js';
|
|
3
|
+
import type * as fromMarkdown from 'mdast-util-from-markdown';
|
|
4
|
+
import type { Tag } from 'mdast-util-mdx-jsx';
|
|
3
5
|
|
|
4
6
|
export default function remarkMdxish(this: any, options = {}) {
|
|
5
7
|
const data = this.data();
|
|
6
8
|
|
|
7
9
|
add('micromarkExtensions', mdxjs(options));
|
|
8
|
-
add('fromMarkdownExtensions', mdxFromMarkdown());
|
|
10
|
+
add('fromMarkdownExtensions', makeFromMarkdownLessStrict(mdxFromMarkdown()));
|
|
9
11
|
add('toMarkdownExtensions', mdxToMarkdown());
|
|
10
12
|
|
|
11
13
|
function add(field: string, value: unknown) {
|
|
@@ -13,3 +15,42 @@ export default function remarkMdxish(this: any, options = {}) {
|
|
|
13
15
|
list.push(value);
|
|
14
16
|
}
|
|
15
17
|
}
|
|
18
|
+
|
|
19
|
+
function makeFromMarkdownLessStrict(extensions: fromMarkdown.Extension[]) {
|
|
20
|
+
extensions.forEach((extension) => {
|
|
21
|
+
// Fix exit handlers that are too strict
|
|
22
|
+
['mdxJsxFlowTag', 'mdxJsxTextTag'].forEach((exitHandler) => {
|
|
23
|
+
if (!extension.exit || !extension.exit[exitHandler]) return;
|
|
24
|
+
extension.exit[exitHandler] = chainHandlers(fixSelfClosing, extension.exit[exitHandler]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return extensions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selfClosingTags = new Set([
|
|
32
|
+
'area',
|
|
33
|
+
'base',
|
|
34
|
+
'br',
|
|
35
|
+
'col',
|
|
36
|
+
'embed',
|
|
37
|
+
'hr',
|
|
38
|
+
'img',
|
|
39
|
+
'input',
|
|
40
|
+
'link',
|
|
41
|
+
'meta',
|
|
42
|
+
'source',
|
|
43
|
+
'track',
|
|
44
|
+
'wbr',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function fixSelfClosing(this: fromMarkdown.CompileContext) {
|
|
48
|
+
const tag = this.getData('mdxJsxTag') as Tag;
|
|
49
|
+
if (tag.name && selfClosingTags.has(tag.name)) tag.selfClosing = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function chainHandlers(...handlers: fromMarkdown.Handle[]) {
|
|
53
|
+
return function handlerChain(this: fromMarkdown.CompileContext, token: fromMarkdown.Token) {
|
|
54
|
+
handlers.forEach((handler) => handler.call(this, token));
|
|
55
|
+
};
|
|
56
|
+
}
|
package/test/components.test.js
CHANGED
|
@@ -49,9 +49,18 @@ describe('components', () => {
|
|
|
49
49
|
it('should normalize children', async () => {
|
|
50
50
|
const { code } = await renderMarkdown(`<Component bool={true}>Hello world!</Component>`, {});
|
|
51
51
|
|
|
52
|
+
chai.expect(code).to.equal(`\n<Component bool={true}>Hello world!</Component>\n`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should be able to nest components', async () => {
|
|
56
|
+
const { code } = await renderMarkdown(
|
|
57
|
+
`<Component bool={true}><Component>Hello world!</Component></Component>`,
|
|
58
|
+
{}
|
|
59
|
+
);
|
|
60
|
+
|
|
52
61
|
chai
|
|
53
62
|
.expect(code)
|
|
54
|
-
.to.equal(
|
|
63
|
+
.to.equal(`\n<Component bool={true}>\n<Component>Hello world!</Component>\n</Component>\n`);
|
|
55
64
|
});
|
|
56
65
|
|
|
57
66
|
it('should allow markdown without many spaces', async () => {
|
|
@@ -62,10 +71,6 @@ describe('components', () => {
|
|
|
62
71
|
{}
|
|
63
72
|
);
|
|
64
73
|
|
|
65
|
-
chai
|
|
66
|
-
.expect(code)
|
|
67
|
-
.to.equal(
|
|
68
|
-
`<Fragment>\n<Component><h1 id="hello-world">Hello world!</h1></Component>\n</Fragment>`
|
|
69
|
-
);
|
|
74
|
+
chai.expect(code).to.equal(`\n<Component><h1 id="hello-world">Hello world!</h1></Component>\n`);
|
|
70
75
|
});
|
|
71
76
|
});
|
package/test/expressions.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { renderMarkdown } from '../dist/index.js';
|
|
|
2
2
|
import chai from 'chai';
|
|
3
3
|
|
|
4
4
|
describe('expressions', () => {
|
|
5
|
-
it('should be able to serialize bare
|
|
5
|
+
it('should be able to serialize bare expression', async () => {
|
|
6
6
|
const { code } = await renderMarkdown(`{a}`, {});
|
|
7
7
|
|
|
8
8
|
chai.expect(code).to.equal(`{a}`);
|
|
@@ -11,7 +11,7 @@ describe('expressions', () => {
|
|
|
11
11
|
it('should be able to serialize expression inside component', async () => {
|
|
12
12
|
const { code } = await renderMarkdown(`<Component>{a}</Component>`, {});
|
|
13
13
|
|
|
14
|
-
chai.expect(code).to.equal(
|
|
14
|
+
chai.expect(code).to.equal(`\n<Component>{a}</Component>\n`);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('should be able to serialize expression inside markdown', async () => {
|
|
@@ -40,6 +40,37 @@ describe('expressions', () => {
|
|
|
40
40
|
);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('should be able to avoid evaluating JSX-like expressions in an inline code & generate a slug for id', async () => {
|
|
44
|
+
const { code } = await renderMarkdown(`# \`{frontmatter.title}\``, {});
|
|
45
|
+
|
|
46
|
+
chai
|
|
47
|
+
.expect(code)
|
|
48
|
+
.to.equal('<h1 id="frontmattertitle"><code is:raw>{frontmatter.title}</code></h1>');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => {
|
|
52
|
+
const { code } = await renderMarkdown(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {});
|
|
53
|
+
|
|
54
|
+
chai
|
|
55
|
+
.expect(code)
|
|
56
|
+
.to.equal(
|
|
57
|
+
'<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
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should be able to avoid evaluating JSX-like expressions & escape HTML tag characters in inline codes', async () => {
|
|
62
|
+
const { code } = await renderMarkdown(
|
|
63
|
+
`###### \`{}\` is equivalent to \`Record<never, never>\` <small>(at TypeScript v{frontmatter.version})</small>`,
|
|
64
|
+
{}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
chai
|
|
68
|
+
.expect(code)
|
|
69
|
+
.to.equal(
|
|
70
|
+
`<h6 id={$$slug(\`{} is equivalent to Record<never, never> (at TypeScript v\${frontmatter.version})\`)}><code is:raw>{}</code> is equivalent to <code is:raw>Record<never, never></code> <small>(at TypeScript v{frontmatter.version})</small></h6>`
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
43
74
|
it('should be able to serialize function expression', async () => {
|
|
44
75
|
const { code } = await renderMarkdown(
|
|
45
76
|
`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { renderMarkdown } from '../dist/index.js';
|
|
2
|
+
import chai from 'chai';
|
|
3
|
+
|
|
4
|
+
describe('strictness', () => {
|
|
5
|
+
it('should allow self-closing HTML tags (void elements)', async () => {
|
|
6
|
+
const { code } = await renderMarkdown(
|
|
7
|
+
`Use self-closing void elements<br>like word<wbr>break and images: <img src="hi.jpg">`,
|
|
8
|
+
{}
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
chai
|
|
12
|
+
.expect(code)
|
|
13
|
+
.to.equal(
|
|
14
|
+
`<p>Use self-closing void elements<br />like word<wbr />break and images: ` +
|
|
15
|
+
`<img src="hi.jpg" /></p>`
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|