Markdown parser fork with extended syntax for personal use.
1use markdown::{mdast::Node, message::Message, Constructs, ParseOptions};
2use test_utils::swc::{parse_esm, parse_expression};
3mod test_utils;
4
5#[allow(unused)]
6#[derive(Debug)]
7enum Error {
8 Mdast(Message),
9 Serde(serde_json::Error),
10}
11
12#[test]
13#[cfg(feature = "serde")]
14fn serde_constructs() -> Result<(), Error> {
15 use pretty_assertions::assert_eq;
16
17 assert_eq!(
18 serde_json::to_string(&Constructs::default()).unwrap(),
19 r#"{"attention":true,"autolink":true,"blockQuote":true,"characterEscape":true,"characterReference":true,"codeIndented":true,"codeFenced":true,"codeText":true,"definition":true,"frontmatter":false,"gfmAutolinkLiteral":false,"gfmFootnoteDefinition":false,"gfmLabelStartFootnote":false,"gfmStrikethrough":false,"gfmTable":false,"gfmTaskListItem":false,"hardBreakEscape":true,"hardBreakTrailing":true,"headingAtx":true,"headingSetext":true,"htmlFlow":true,"htmlText":true,"labelStartImage":true,"labelStartLink":true,"labelEnd":true,"listItem":true,"mathFlow":false,"mathText":false,"mdxEsm":false,"mdxExpressionFlow":false,"mdxExpressionText":false,"mdxJsxFlow":false,"mdxJsxText":false,"thematicBreak":true}"#
20 );
21
22 Ok(())
23}
24
25#[test]
26#[cfg(feature = "serde")]
27fn serde_compile_options() -> Result<(), Error> {
28 use pretty_assertions::assert_eq;
29
30 assert_eq!(
31 serde_json::to_string(&markdown::CompileOptions::gfm()).unwrap(),
32 r#"{"allowAnyImgSrc":false,"allowDangerousHtml":false,"allowDangerousProtocol":false,"defaultLineEnding":"\n","gfmFootnoteBackLabel":null,"gfmFootnoteClobberPrefix":null,"gfmFootnoteLabelAttributes":null,"gfmFootnoteLabelTagName":null,"gfmFootnoteLabel":null,"gfmTaskListItemCheckable":false,"gfmTagfilter":true}"#
33 );
34
35 Ok(())
36}
37
38#[test]
39#[cfg(feature = "serde")]
40fn serde_parse_options() -> Result<(), Error> {
41 use pretty_assertions::assert_eq;
42
43 assert_eq!(
44 serde_json::to_string(&ParseOptions::gfm()).unwrap(),
45 r#"{"constructs":{"attention":true,"autolink":true,"blockQuote":true,"characterEscape":true,"characterReference":true,"codeIndented":true,"codeFenced":true,"codeText":true,"definition":true,"frontmatter":false,"gfmAutolinkLiteral":true,"gfmFootnoteDefinition":true,"gfmLabelStartFootnote":true,"gfmStrikethrough":true,"gfmTable":true,"gfmTaskListItem":true,"hardBreakEscape":true,"hardBreakTrailing":true,"headingAtx":true,"headingSetext":true,"htmlFlow":true,"htmlText":true,"labelStartImage":true,"labelStartLink":true,"labelEnd":true,"listItem":true,"mathFlow":false,"mathText":false,"mdxEsm":false,"mdxExpressionFlow":false,"mdxExpressionText":false,"mdxJsxFlow":false,"mdxJsxText":false,"thematicBreak":true},"gfmStrikethroughSingleTilde":true,"mathTextSingleDollar":true}"#
46 );
47
48 Ok(())
49}
50
51#[test]
52fn serde_blockquote() -> Result<(), Error> {
53 assert_serde(
54 "> a",
55 r#"{
56 "type": "root",
57 "children": [
58 {
59 "type": "blockquote",
60 "children": [
61 {
62 "type": "paragraph",
63 "children": [{"type": "text", "value": "a"}]
64 }
65 ]
66 }
67 ]
68}
69"#,
70 ParseOptions::default(),
71 )
72}
73
74#[test]
75fn serde_footnote_definition() -> Result<(), Error> {
76 assert_serde(
77 "[^a]: b",
78 r#"{
79 "type": "root",
80 "children": [
81 {
82 "type": "footnoteDefinition",
83 "identifier": "a",
84 "label": "a",
85 "children": [
86 {
87 "type": "paragraph",
88 "children": [{"type": "text", "value": "b"}]
89 }
90 ]
91 }
92 ]
93}"#,
94 ParseOptions::gfm(),
95 )
96}
97
98#[test]
99fn serde_mdx_jsx_flow_element() -> Result<(), Error> {
100 let options = ParseOptions {
101 mdx_esm_parse: Some(Box::new(parse_esm)),
102 mdx_expression_parse: Some(Box::new(parse_expression)),
103 ..ParseOptions::mdx()
104 };
105
106 assert_serde(
107 "<a b c={d} e=\"f\" {...g} />",
108 r#"{
109 "type": "root",
110 "children": [
111 {
112 "type": "mdxJsxFlowElement",
113 "name": "a",
114 "attributes": [
115 {"type": "mdxJsxAttribute", "name": "b"},
116 {
117 "type": "mdxJsxAttribute",
118 "name": "c",
119 "value": {
120 "_markdownRsStops": [[0, 8]],
121 "type": "mdxJsxAttributeValueExpression",
122 "value": "d"
123 }
124 },
125 {"type": "mdxJsxAttribute", "name": "e", "value": "f"},
126 {
127 "_markdownRsStops": [[0, 18]],
128 "type": "mdxJsxExpressionAttribute",
129 "value": "...g"
130 }
131 ],
132 "children": []
133 }
134 ]
135}"#,
136 options,
137 )
138}
139
140#[test]
141fn serde_list() -> Result<(), Error> {
142 assert_serde(
143 "* a",
144 r#"{
145 "type": "root",
146 "children": [
147 {
148 "type": "list",
149 "ordered": false,
150 "spread": false,
151 "children": [
152 {
153 "type": "listItem",
154 "spread": false,
155 "children": [
156 {
157 "type": "paragraph",
158 "children": [{"type": "text", "value": "a"}]
159 }
160 ]
161 }
162 ]
163 }
164 ]
165}"#,
166 ParseOptions::default(),
167 )
168}
169
170#[test]
171fn serde_mdxjs_esm() -> Result<(), Error> {
172 let options = ParseOptions {
173 mdx_esm_parse: Some(Box::new(parse_esm)),
174 mdx_expression_parse: Some(Box::new(parse_expression)),
175 ..ParseOptions::mdx()
176 };
177
178 assert_serde(
179 "import a, {b} from 'c'",
180 r#"{
181 "type": "root",
182 "children": [
183 {
184 "_markdownRsStops": [[0, 0]],
185 "type": "mdxjsEsm",
186 "value": "import a, {b} from 'c'"
187 }
188 ]
189}"#,
190 options,
191 )
192}
193
194#[test]
195fn serde_toml() -> Result<(), Error> {
196 assert_serde(
197 "+++\na: b\n+++",
198 r#"{
199 "type": "root",
200 "children": [{"type": "toml", "value": "a: b"}]
201}"#,
202 ParseOptions {
203 constructs: Constructs {
204 frontmatter: true,
205 ..Constructs::default()
206 },
207 ..ParseOptions::default()
208 },
209 )
210}
211
212#[test]
213fn serde_yaml() -> Result<(), Error> {
214 assert_serde(
215 "---\na: b\n---",
216 r#"{
217 "type": "root",
218 "children": [{"type": "yaml", "value": "a: b"}]
219}"#,
220 ParseOptions {
221 constructs: Constructs {
222 frontmatter: true,
223 ..Constructs::default()
224 },
225 ..ParseOptions::default()
226 },
227 )
228}
229
230#[test]
231fn serde_break() -> Result<(), Error> {
232 assert_serde(
233 "a\\\nb",
234 r#"{
235 "type": "root",
236 "children": [
237 {
238 "type": "paragraph",
239 "children": [
240 {"type": "text", "value": "a"},
241 {"type": "break"},
242 {"type": "text", "value": "b"}
243 ]
244 }
245 ]
246}"#,
247 ParseOptions::default(),
248 )
249}
250
251#[test]
252fn serde_inline_code() -> Result<(), Error> {
253 assert_serde(
254 "`a`",
255 r#"{
256 "type": "root",
257 "children": [
258 {
259 "type": "paragraph",
260 "children": [{"type": "inlineCode", "value": "a"}]
261 }
262 ]
263}"#,
264 ParseOptions::default(),
265 )
266}
267
268#[test]
269fn serde_inline_math() -> Result<(), Error> {
270 assert_serde(
271 "$a$",
272 r#"{
273 "type": "root",
274 "children": [
275 {
276 "type": "paragraph",
277 "children": [{"type": "inlineMath","value": "a"}]
278 }
279 ]
280}"#,
281 ParseOptions {
282 constructs: Constructs {
283 math_text: true,
284 ..Constructs::default()
285 },
286 ..ParseOptions::default()
287 },
288 )
289}
290
291#[test]
292fn serde_delete() -> Result<(), Error> {
293 assert_serde(
294 "~~a~~",
295 r#"{
296 "type": "root",
297 "children": [
298 {
299 "type": "paragraph",
300 "children": [
301 {
302 "type": "delete",
303 "children": [{"type": "text","value": "a"}]
304 }
305 ]
306 }
307 ]
308}"#,
309 ParseOptions::gfm(),
310 )
311}
312
313#[test]
314fn serde_emphasis() -> Result<(), Error> {
315 assert_serde(
316 "*a*",
317 r#"{
318 "type": "root",
319 "children": [
320 {
321 "type": "paragraph",
322 "children": [
323 {
324 "type": "emphasis",
325 "children": [{"type": "text","value": "a"}]
326 }
327 ]
328 }
329 ]
330}"#,
331 ParseOptions::default(),
332 )
333}
334
335#[test]
336fn serde_mdx_text_expression() -> Result<(), Error> {
337 let options = ParseOptions {
338 mdx_esm_parse: Some(Box::new(parse_esm)),
339 mdx_expression_parse: Some(Box::new(parse_expression)),
340 ..ParseOptions::mdx()
341 };
342
343 assert_serde(
344 "a {b}",
345 r#"{
346 "type": "root",
347 "children": [
348 {
349 "type": "paragraph",
350 "children": [
351 {"type": "text","value": "a "},
352 {
353 "_markdownRsStops": [[0,3]],
354 "type": "mdxTextExpression",
355 "value": "b"
356 }
357 ]
358 }
359 ]
360}"#,
361 options,
362 )
363}
364
365#[test]
366fn serde_footnote_reference() -> Result<(), Error> {
367 assert_serde(
368 "[^a]\n\n[^a]: b",
369 r#"{
370 "type": "root",
371 "children": [
372 {
373 "type": "paragraph",
374 "children": [
375 {"type": "footnoteReference", "identifier": "a", "label": "a"}
376 ]
377 },
378 {
379 "type": "footnoteDefinition",
380 "identifier": "a",
381 "label": "a",
382 "children": [
383 {
384 "type": "paragraph",
385 "children": [{"type": "text", "value": "b"}]
386 }
387 ]
388 }
389 ]
390}"#,
391 ParseOptions::gfm(),
392 )
393}
394
395#[test]
396fn serde_html() -> Result<(), Error> {
397 assert_serde(
398 "<a>",
399 r#"{
400 "type": "root",
401 "children": [{"type": "html", "value": "<a>"}]
402}"#,
403 ParseOptions::gfm(),
404 )
405}
406
407#[test]
408fn serde_image() -> Result<(), Error> {
409 assert_serde(
410 "",
411 r#"{
412 "type": "root",
413 "children": [
414 {
415 "type": "paragraph",
416 "children": [{"type": "image", "url": "b", "alt": "a"}]
417 }
418 ]
419}"#,
420 ParseOptions::default(),
421 )
422}
423
424#[test]
425fn serde_image_reference() -> Result<(), Error> {
426 assert_serde(
427 "![a]\n\n[a]: b",
428 r#"{
429 "type": "root",
430 "children": [
431 {
432 "type": "paragraph",
433 "children": [
434 {
435 "type": "imageReference",
436 "alt": "a",
437 "label": "a",
438 "identifier": "a",
439 "referenceType": "shortcut"
440 }
441 ]
442 },
443 {"type": "definition", "url": "b", "identifier": "a", "label": "a"}
444 ]
445}"#,
446 ParseOptions::default(),
447 )
448}
449
450#[test]
451fn serde_mdx_jsx_text_element() -> Result<(), Error> {
452 assert_serde(
453 "a <b c />",
454 r#"{
455 "type": "root",
456 "children": [
457 {
458 "type": "paragraph",
459 "children": [
460 {"type": "text", "value": "a "},
461 {
462 "type": "mdxJsxTextElement",
463 "name": "b",
464 "attributes": [{"type": "mdxJsxAttribute", "name": "c"}],
465 "children": []
466 }
467 ]
468 }
469 ]
470}"#,
471 ParseOptions::mdx(),
472 )
473}
474
475#[test]
476fn serde_link() -> Result<(), Error> {
477 assert_serde(
478 "[a](b)",
479 r#"{
480 "type": "root",
481 "children": [
482 {
483 "type": "paragraph",
484 "children": [
485 {
486 "type": "link",
487 "url": "b",
488 "children": [{"type": "text", "value": "a"}]
489 }
490 ]
491 }
492 ]
493}"#,
494 ParseOptions::default(),
495 )
496}
497
498#[test]
499fn serde_link_reference() -> Result<(), Error> {
500 assert_serde(
501 "[a]\n\n[a]: b",
502 r#"{
503 "type": "root",
504 "children": [
505 {
506 "type": "paragraph",
507 "children": [
508 {
509 "type": "linkReference",
510 "identifier": "a",
511 "label": "a",
512 "referenceType": "shortcut",
513 "children": [{"type": "text", "value": "a"}]
514 }
515 ]
516 },
517 {
518 "type": "definition",
519 "url": "b",
520 "identifier": "a",
521 "label": "a"
522 }
523 ]
524}"#,
525 ParseOptions::default(),
526 )
527}
528
529#[test]
530fn serde_strong() -> Result<(), Error> {
531 assert_serde(
532 "**a**",
533 r#"{
534 "type": "root",
535 "children": [
536 {
537 "type": "paragraph",
538 "children": [
539 {
540 "type": "strong",
541 "children": [{"type": "text", "value": "a"}]
542 }
543 ]
544 }
545 ]
546}"#,
547 ParseOptions::default(),
548 )
549}
550
551#[test]
552fn serde_text() -> Result<(), Error> {
553 assert_serde(
554 "a",
555 r#"{
556 "type": "root",
557 "children": [
558 {"type": "paragraph", "children": [{"type": "text", "value": "a"}]}
559 ]
560}"#,
561 ParseOptions::default(),
562 )
563}
564
565#[test]
566fn serde_code() -> Result<(), Error> {
567 assert_serde(
568 "~~~js eval\nconsole.log(1)\n~~~",
569 r#"{
570 "type": "root",
571 "children": [
572 {"type": "code", "lang": "js", "meta": "eval", "value": "console.log(1)"}
573 ]
574}"#,
575 ParseOptions::default(),
576 )?;
577
578 assert_serde(
579 "```\nconsole.log(1)\n```",
580 r#"{
581 "type": "root",
582 "children": [{"type": "code", "value": "console.log(1)"}]
583}"#,
584 ParseOptions::default(),
585 )
586}
587
588#[test]
589fn serde_math() -> Result<(), Error> {
590 assert_serde(
591 "$$\n1 + 1 = 2\n$$",
592 r#"{
593 "type": "root",
594 "children": [{"type": "math", "value": "1 + 1 = 2"}]
595}"#,
596 ParseOptions {
597 constructs: Constructs {
598 math_flow: true,
599 ..Constructs::default()
600 },
601 ..ParseOptions::default()
602 },
603 )
604}
605
606#[test]
607fn serde_mdx_flow_expression() -> Result<(), Error> {
608 let options = ParseOptions {
609 mdx_esm_parse: Some(Box::new(parse_esm)),
610 mdx_expression_parse: Some(Box::new(parse_expression)),
611 ..ParseOptions::mdx()
612 };
613
614 assert_serde(
615 "{a}",
616 r#"{
617 "type": "root",
618 "children": [
619 {"_markdownRsStops": [[0, 1]], "type": "mdxFlowExpression", "value": "a"}
620 ]
621}"#,
622 options,
623 )
624}
625
626#[test]
627fn serde_heading() -> Result<(), Error> {
628 assert_serde(
629 "# a",
630 r#"{
631 "type": "root",
632 "children": [
633 {
634 "type": "heading",
635 "depth": 1,
636 "children": [{"type": "text", "value": "a"}]
637 }
638 ]
639}"#,
640 ParseOptions::default(),
641 )
642}
643
644#[test]
645fn serde_table() -> Result<(), Error> {
646 // To do: `"none"` should serialize in serde as `null`.
647 assert_serde(
648 "| a | b | c | d |\n| - | :- | -: | :-: |\n| 1 | 2 | 3 | 4 |",
649 r#"{
650 "type": "root",
651 "children": [
652 {
653 "type": "table",
654 "align": [null, "left", "right", "center"],
655 "children": [
656 {
657 "type": "tableRow",
658 "children": [
659 {"type": "tableCell", "children": [{"type": "text", "value": "a"}]},
660 {"type": "tableCell", "children": [{"type": "text", "value": "b"}]},
661 {"type": "tableCell", "children": [{"type": "text", "value": "c"}]},
662 {"type": "tableCell", "children": [{"type": "text", "value": "d"}]}
663 ]
664 },
665 {
666 "type": "tableRow",
667 "children": [
668 {"type": "tableCell","children": [{"type": "text", "value": "1"}]},
669 {"type": "tableCell","children": [{"type": "text", "value": "2"}]},
670 {"type": "tableCell","children": [{"type": "text", "value": "3"}]},
671 {"type": "tableCell","children": [{"type": "text", "value": "4"}]}
672 ]
673 }
674 ]
675 }
676 ]
677}"#,
678 ParseOptions::gfm(),
679 )
680}
681
682#[test]
683fn serde_thematic_break() -> Result<(), Error> {
684 assert_serde(
685 "***",
686 r#"{"type": "root", "children": [{"type": "thematicBreak"}]}"#,
687 ParseOptions::default(),
688 )
689}
690
691#[test]
692fn serde_definition() -> Result<(), Error> {
693 assert_serde(
694 "[a]: b",
695 r###"{
696 "type": "root",
697 "children": [
698 {"type": "definition", "url": "b", "identifier": "a", "label": "a"}
699 ]
700}"###,
701 ParseOptions::default(),
702 )
703}
704
705#[test]
706fn serde_paragraph() -> Result<(), Error> {
707 assert_serde(
708 "a",
709 r#"{
710 "type": "root",
711 "children": [
712 {
713 "type": "paragraph",
714 "children": [{"type": "text", "value": "a"}]
715 }
716 ]
717}"#,
718 ParseOptions::default(),
719 )
720}
721
722/// Assert serde of mdast constructs.
723///
724/// Refer below links for the mdast JSON construct types.
725/// * <https://github.com/syntax-tree/mdast#nodes>
726/// * <https://github.com/syntax-tree/mdast-util-mdx#syntax-tree>
727/// * <https://github.com/syntax-tree/mdast-util-frontmatter#syntax-tree>
728#[cfg(feature = "serde")]
729fn assert_serde(input: &str, expected: &str, options: ParseOptions) -> Result<(), Error> {
730 use pretty_assertions::assert_eq;
731
732 let mut source = markdown::to_mdast(input, &options).map_err(Error::Mdast)?;
733
734 remove_position(&mut source);
735 // Serialize to JSON
736 let actual_value: serde_json::Value = serde_json::to_value(&source).map_err(Error::Serde)?;
737 let expected_value: serde_json::Value = serde_json::from_str(expected).map_err(Error::Serde)?;
738
739 // Assert serialization.
740 assert_eq!(actual_value, expected_value);
741
742 // Assert deserialization.
743 assert_eq!(
744 source,
745 serde_json::from_value(actual_value).map_err(Error::Serde)?
746 );
747
748 Ok(())
749}
750
751#[cfg(not(feature = "serde"))]
752#[allow(unused_variables)]
753fn assert_serde(input: &str, expected: &str, options: ParseOptions) -> Result<(), Error> {
754 Ok(())
755}
756
757#[allow(dead_code)]
758fn remove_position(node: &mut Node) {
759 if let Some(children) = node.children_mut() {
760 for child in children {
761 remove_position(child);
762 }
763 }
764
765 node.position_set(None);
766}