@astrojs/markdown-remark 0.10.2 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 0.10.2
4
16
 
5
17
  ### Patch 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
- raw += child.value;
33
- text += child.value;
34
- isJSX = isJSX || child.value.includes("{");
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
- node.properties.id = `$$slug(\`${text.replace(/\{/g, "${")}\`)`;
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 {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/markdown-remark",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "author": "withastro",
6
6
  "license": "MIT",
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
- raw += child.value;
36
- text += child.value;
37
- isJSX = isJSX || child.value.includes('{');
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.replace(/\{/g, '${')}\`)`;
50
+ node.properties.id = `$$slug(\`${text}\`)`;
46
51
  (node as any).type = 'raw';
47
52
  (
48
53
  node as any
@@ -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
+ }
@@ -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 expession', async () => {
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}`);
@@ -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&lt;never, never&gt; (at TypeScript v\${frontmatter.version})\`)}><code is:raw>{}</code> is equivalent to <code is:raw>Record&lt;never, never&gt;</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
+ });