use satteri_pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
fn mdx_events(input: &str) -> Vec<Event<'_>> {
let opts = Options::ENABLE_MDX
| Options::ENABLE_TABLES
| Options::ENABLE_MATH
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS;
Parser::new_ext(input, opts).collect()
}
fn has(events: &[Event<'_>], pred: impl Fn(&Event<'_>) -> bool) -> bool {
events.iter().any(pred)
}
fn count(events: &[Event<'_>], pred: impl Fn(&Event<'_>) -> bool) -> usize {
events.iter().filter(|e| pred(e)).count()
}
#[test]
fn esm_import() {
let ev = mdx_events("import a from 'b'\n\nc");
assert!(has(
&ev,
|e| matches!(e, Event::MdxEsm(s) if s.contains("import"))
));
assert!(has(&ev, |e| matches!(e, Event::Start(Tag::Paragraph))));
}
#[test]
fn esm_export() {
let ev = mdx_events("export default a\n\nb");
assert!(has(
&ev,
|e| matches!(e, Event::MdxEsm(s) if s.contains("export"))
));
}
#[test]
fn esm_export_brace() {
let ev = mdx_events("export {a}\n");
assert!(has(&ev, |e| matches!(e, Event::MdxEsm(_))));
}
#[test]
fn esm_export_star() {
let ev = mdx_events("export * from 'a'\n");
assert!(has(&ev, |e| matches!(e, Event::MdxEsm(_))));
}
#[test]
fn esm_not_import_dot() {
assert!(!has(&mdx_events("import.\n"), |e| matches!(
e,
Event::MdxEsm(_)
)));
}
#[test]
fn esm_not_import_parens() {
assert!(!has(&mdx_events("import('a')\n"), |e| matches!(
e,
Event::MdxEsm(_)
)));
}
#[test]
fn esm_not_impossible() {
assert!(!has(&mdx_events("impossible\n"), |e| matches!(
e,
Event::MdxEsm(_)
)));
}
#[test]
fn esm_not_exporting() {
assert!(!has(&mdx_events("exporting\n"), |e| matches!(
e,
Event::MdxEsm(_)
)));
}
#[test]
fn esm_not_indented() {
assert!(!has(&mdx_events(" import a from 'b'\n"), |e| matches!(
e,
Event::MdxEsm(_)
)));
}
#[test]
fn esm_consecutive_imports() {
let ev = mdx_events("import a from 'b'\nexport default c\n\nd");
let esm_count = count(&ev, |e| matches!(e, Event::MdxEsm(_)));
assert!(esm_count >= 1, "consecutive import/export: {:?}", ev);
}
#[test]
fn esm_separated_by_blank() {
let ev = mdx_events("import a from 'b'\n\nexport default c\n\nd");
let esm_count = count(&ev, |e| matches!(e, Event::MdxEsm(_)));
assert!(esm_count >= 2, "separate ESM blocks: {:?}", ev);
}
#[test]
fn esm_between_paragraphs() {
let ev = mdx_events("a\n\nimport a from 'b'\n\nb");
assert!(has(&ev, |e| matches!(e, Event::MdxEsm(_))));
assert_eq!(count(&ev, |e| matches!(e, Event::Start(Tag::Paragraph))), 2);
}
#[test]
fn esm_import_styles() {
let cases = [
"import a from \"b\"\n",
"import * as a from \"b\"\n",
"import {a} from \"b\"\n",
"import {a as b} from \"c\"\n",
"import a, {b as c} from \"d\"\n",
"import a, * as b from \"c\"\n",
"import \"a\"\n",
];
for input in cases {
assert!(
has(&mdx_events(input), |e| matches!(e, Event::MdxEsm(_))),
"should be ESM: {}",
input
);
}
}
#[test]
fn esm_export_styles() {
let cases = [
"export var a = \"\"\n",
"export const a = \"\"\n",
"export let a = \"\"\n",
"export function a() {}\n",
"export class a {}\n",
"export default a = 1\n",
"export default function a() {}\n",
"export default class a {}\n",
"export * from \"a\"\n",
"export {a} from \"b\"\n",
];
for input in cases {
assert!(
has(&mdx_events(input), |e| matches!(e, Event::MdxEsm(_))),
"should be ESM: {}",
input
);
}
}
#[test]
fn esm_multiline_export_object() {
let input = "export const data = {\n users: [\n { name: 'Alice', age: 30 },\n { name: 'Bob', age: 25 }\n ]\n};\n\n# Hello\n";
let ev = mdx_events(input);
assert!(
has(
&ev,
|e| matches!(e, Event::MdxEsm(s) if s.contains("data") && s.contains("}"))
),
"multiline export object should be a single ESM block: {:?}",
ev
);
assert!(has(&ev, |e| matches!(e, Event::Start(Tag::Heading { .. }))));
}
#[test]
fn esm_multiline_export_array_ending() {
let input = "export const x = {\n items: [1, 2, 3]\n};\n";
let ev = mdx_events(input);
assert!(
has(&ev, |e| matches!(e, Event::MdxEsm(s) if s.contains("};"))),
"closing brace must be included: {:?}",
ev
);
}
#[test]
fn esm_export_spanning_blank_line() {
let input = "export const x = {\n a: 1,\n\n b: 2\n};\n\n# Hello\n";
let ev = mdx_events(input);
assert!(
has(
&ev,
|e| matches!(e, Event::MdxEsm(s) if s.contains("b: 2") && s.contains("};"))
),
"export spanning blank line should be a single ESM block: {:?}",
ev
);
assert!(has(&ev, |e| matches!(e, Event::Start(Tag::Heading { .. }))));
}
#[test]
fn esm_not_in_paragraph() {
let ev = mdx_events("a\nimport a from 'b'\n");
assert!(
!has(&ev, |e| matches!(e, Event::MdxEsm(_))),
"should not be ESM inside paragraph: {:?}",
ev
);
}
#[test]
fn esm_not_in_list() {
let ev = mdx_events("- import a from 'b'\n");
assert!(
!has(&ev, |e| matches!(e, Event::MdxEsm(_))),
"should not be ESM in list item: {:?}",
ev
);
}
#[test]
fn esm_not_in_blockquote() {
let ev = mdx_events("> export default c\n");
assert!(
!has(&ev, |e| matches!(e, Event::MdxEsm(_))),
"should not be ESM in blockquote: {:?}",
ev
);
}
#[test]
fn expr_flow_simple() {
let ev = mdx_events("{a}\n");
assert!(has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "a")
));
}
#[test]
fn expr_flow_empty() {
let ev = mdx_events("{}\n");
assert!(has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "")
));
}
#[test]
fn expr_flow_nested_braces() {
let ev = mdx_events("{a({b: 1})}\n");
assert!(has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "a({b: 1})")
));
}
#[test]
fn expr_flow_multiline() {
let ev = mdx_events("{\n}\n");
assert!(
has(&ev, |e| matches!(e, Event::MdxFlowExpression(_))),
"multiline expression: {:?}",
ev
);
}
#[test]
fn expr_flow_trailing_whitespace() {
let ev = mdx_events("{ a } \t\n");
assert!(
has(&ev, |e| matches!(e, Event::MdxFlowExpression(_))),
"trailing whitespace: {:?}",
ev
);
}
#[test]
fn expr_flow_leading_whitespace() {
let ev = mdx_events(" { a }\n");
let _ = ev;
}
#[test]
fn expr_flow_with_strings() {
let ev = mdx_events("{\"a { b }\"}\n");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "\"a { b }\"")
),
"strings in expression: {:?}",
ev
);
}
#[test]
fn expr_flow_with_template() {
let ev = mdx_events("{`a { b }`}\n");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "`a { b }`")
),
"template in expression: {:?}",
ev
);
}
#[test]
fn expr_text_simple() {
let ev = mdx_events("a {b} c");
assert!(has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "b")
));
}
#[test]
fn expr_text_empty() {
let ev = mdx_events("a {} b");
assert!(has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "")
));
}
#[test]
fn expr_text_nested() {
let ev = mdx_events("a {b({c: 1})} d");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "b({c: 1})")
),
"nested inline expression: {:?}",
ev
);
}
#[test]
fn expr_text_multiline() {
let ev = mdx_events("a {\n} b");
assert!(
has(&ev, |e| matches!(e, Event::MdxTextExpression(_))),
"multiline inline expression: {:?}",
ev
);
}
#[test]
fn expr_text_closing_brace_as_text() {
let ev = mdx_events("a } b");
assert!(
!has(&ev, |e| matches!(e, Event::MdxTextExpression(_))),
"lone }} should be text: {:?}",
ev
);
assert!(has(&ev, |e| matches!(e, Event::Text(_))));
}
#[test]
fn expr_text_at_start() {
let ev = mdx_events("{ a } b");
assert!(
has(&ev, |e| matches!(e, Event::MdxTextExpression(_))),
"expression at start: {:?}",
ev
);
}
#[test]
fn expr_text_with_parens() {
let ev = mdx_events("a{(b)}c");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "(b)")
),
"parens in expression: {:?}",
ev
);
}
#[test]
fn expr_text_with_string_braces() {
let ev = mdx_events("a {\"}\"} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "\"}\"")
),
"string with brace: {:?}",
ev
);
}
#[test]
fn expr_text_comment() {
let ev = mdx_events("a {/**/} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "/**/")
),
"comment in expression: {:?}",
ev
);
}
#[test]
fn expr_text_1_plus_1() {
let ev = mdx_events("a {1 + 1} b");
assert!(has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "1 + 1")
));
}
#[test]
fn expr_text_function() {
let ev = mdx_events("a {function () {}} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "function () {}")
),
"function in expression: {:?}",
ev
);
}
#[test]
fn expr_text_comment_with_braces() {
let ev = mdx_events("a {/* } */} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "/* } */")
),
"comment with braces: {:?}",
ev
);
}
#[test]
fn expr_text_string_with_brace() {
let ev = mdx_events("a {\"}\"} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "\"}\"")
),
"string with brace: {:?}",
ev
);
}
#[test]
fn expr_text_template_literal() {
let ev = mdx_events("a {`${x}`} b");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "`${x}`")
),
"template literal: {:?}",
ev
);
}
#[test]
fn jsx_flow_self_closing() {
let ev = mdx_events("<a />\n");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)));
}
#[test]
fn jsx_flow_closed() {
let ev = mdx_events("<a></a>\n");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)));
}
#[test]
fn jsx_flow_fragment() {
let ev = mdx_events("<>\n");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)));
}
#[test]
fn jsx_flow_attributes() {
let ev = mdx_events("<a b c:d e=\"\" f={/* g */} {...h} />\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"attributes: {:?}",
ev
);
}
#[test]
fn jsx_flow_with_expression_attr() {
let ev = mdx_events("<Chart values={[1,2,3]} />\n");
assert!(has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxFlowElement(s)) if s.contains("Chart"))
));
}
#[test]
fn jsx_flow_closing_fragment() {
let ev = mdx_events("</>\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"closing fragment: {:?}",
ev
);
}
#[test]
fn jsx_flow_closing_tag() {
let ev = mdx_events("</a>\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"closing tag: {:?}",
ev
);
}
#[test]
fn jsx_text_lowercase() {
let ev = mdx_events("a <b> c");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"lowercase <b>: {:?}",
ev
);
}
#[test]
fn jsx_text_self_closing() {
let ev = mdx_events("a <b/> c.");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)));
}
#[test]
fn jsx_text_closed() {
let ev = mdx_events("a <b></b> c.");
assert!(
count(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)) >= 1
);
}
#[test]
fn jsx_text_fragment() {
let ev = mdx_events("a <></> c.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"inline fragment: {:?}",
ev
);
}
#[test]
fn jsx_text_uppercase() {
let ev = mdx_events("a <Badge /> c.");
assert!(has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("Badge"))
));
}
#[test]
fn jsx_text_namespaced() {
let ev = mdx_events("<a:b />.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("a:b"))
),
"namespaced tag: {:?}",
ev
);
}
#[test]
fn jsx_text_member() {
let ev = mdx_events("<a.b.c />.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("a.b.c"))
),
"member tag: {:?}",
ev
);
}
#[test]
fn jsx_text_spread_attribute() {
let ev = mdx_events("<a {...b} />.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("...b"))
),
"spread attribute: {:?}",
ev
);
}
#[test]
fn jsx_text_boolean_attribute() {
let ev = mdx_events("<a b />.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"boolean attribute: {:?}",
ev
);
}
#[test]
fn jsx_text_string_attribute() {
let ev = mdx_events("<a b=\"c\" />.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("b=\"c\""))
),
"string attribute: {:?}",
ev
);
}
#[test]
fn jsx_text_expression_attribute() {
let ev = mdx_events("<a b={c} />.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("b={c}"))
),
"expression attribute: {:?}",
ev
);
}
#[test]
fn jsx_text_closing_tag() {
let ev = mdx_events("a </b> c");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"closing tag inline: {:?}",
ev
);
}
#[test]
fn jsx_text_not_lt_number() {
let ev = mdx_events("a < 3 b");
assert!(
!has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"< number is not JSX: {:?}",
ev
);
}
#[test]
fn jsx_text_complex_attrs() {
let ev = mdx_events("<a b c:d e=\"\" f={g} {...h} />.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"complex attrs: {:?}",
ev
);
}
#[test]
fn no_mdx_without_flag() {
let ev: Vec<_> = Parser::new("import foo from 'bar'\n").collect();
assert!(!has(&ev, |e| matches!(e, Event::MdxEsm(_))));
}
#[test]
fn no_mdx_expressions_without_flag() {
let ev: Vec<_> = Parser::new("a {b} c\n").collect();
assert!(!has(&ev, |e| matches!(e, Event::MdxTextExpression(_))));
}
#[test]
fn no_mdx_jsx_without_flag() {
let ev: Vec<_> = Parser::new("a <em>b</em> c\n").collect();
assert!(!has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)));
}
#[test]
fn expression_then_jsx_same_line_is_inline() {
let ev = mdx_events("{1}<a/>\n");
assert!(
has(&ev, |e| matches!(e, Event::MdxTextExpression(_)))
|| has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
))
|| has(&ev, |e| matches!(e, Event::MdxFlowExpression(_)))
|| has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"expression+jsx on same line: {:?}",
ev
);
}
#[test]
fn jsx_then_expression_same_line_is_inline() {
let ev = mdx_events("<x/>{1}\n");
let _ = ev;
}
#[test]
fn inline_expression_and_jsx() {
let ev = mdx_events("a {b} <C /> d");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "b")
),
"inline expr: {:?}",
ev
);
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("C"))
),
"inline jsx: {:?}",
ev
);
}
#[test]
fn esm_then_heading() {
let ev = mdx_events("import a from 'b'\n\n# c\n");
assert!(has(&ev, |e| matches!(e, Event::MdxEsm(_))));
assert!(has(&ev, |e| matches!(e, Event::Start(Tag::Heading { .. }))));
}
#[test]
fn jsx_flow_emits_end() {
let ev = mdx_events("<a />\n");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)));
assert!(has(&ev, |e| matches!(
e,
Event::End(TagEnd::MdxJsxFlowElement)
)));
}
#[test]
fn jsx_text_emits_end() {
let ev = mdx_events("x <b/> y");
assert!(has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)));
assert!(has(&ev, |e| matches!(
e,
Event::End(TagEnd::MdxJsxTextElement)
)));
}
#[test]
fn jsx_text_multiline_tag() {
let ev = mdx_events("a <b\nc\n d\n/> e.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline tag: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_string_attr() {
let ev = mdx_events("a <b c=\"d\ne\" /> f");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline string attr: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_expr_attr() {
let ev = mdx_events("a <b c={d\ne} /> f");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline expr attr: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_spread() {
let ev = mdx_events("a <b {c\nd} /> e");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline spread: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_complex() {
let ev = mdx_events("<a\nb \nc\n d\n/>.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("<a"))
),
"complex multiline: {:?}",
ev
);
}
#[test]
fn jsx_text_non_ascii_pi() {
let ev = mdx_events("a <π /> b.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"non-ascii π: {:?}",
ev
);
}
#[test]
fn jsx_text_dollar_tag() {
let ev = mdx_events("a <$Component /> b.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("$Component"))
),
"dollar tag: {:?}",
ev
);
}
#[test]
fn jsx_flow_non_ascii() {
let ev = mdx_events("<π />\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"flow non-ascii: {:?}",
ev
);
}
#[test]
fn jsx_flow_dollar() {
let ev = mdx_events("<$C />\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"flow dollar: {:?}",
ev
);
}
#[test]
fn jsx_text_dash_in_name() {
let ev = mdx_events("a <my-component /> b.");
assert!(
has(
&ev,
|e| matches!(e, Event::Start(Tag::MdxJsxTextElement(s)) if s.contains("my-component"))
),
"dash in name: {:?}",
ev
);
}
#[test]
fn jsx_text_dashes_in_name() {
let ev = mdx_events("a <a-->b</a-->.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"dashes in tag name: {:?}",
ev
);
}
#[test]
fn jsx_text_whitespace_after_slash() {
let ev = mdx_events("<a/ \t>.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"whitespace after slash: {:?}",
ev
);
}
#[test]
fn jsx_text_lt_space_not_jsx() {
let ev = mdx_events("a < \t>b c");
assert!(
!has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"< followed by space is not JSX: {:?}",
ev
);
}
#[test]
fn jsx_text_lt_newline_not_jsx() {
let ev = mdx_events("a < \nb\t>b c");
assert!(
!has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"< followed by newline is not JSX: {:?}",
ev
);
}
#[test]
fn less_than_number() {
let ev = mdx_events("1 < 3");
assert!(
!has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"1 < 3 is not JSX: {:?}",
ev
);
}
#[test]
fn html_comment_not_block_in_mdx() {
let ev = mdx_events("<!-- comment -->\n");
assert!(
!has(&ev, |e| matches!(e, Event::Start(Tag::HtmlBlock))),
"<!-- should not be HTML block in MDX: {:?}",
ev
);
}
#[test]
fn html_comment_not_inline_in_mdx() {
let ev = mdx_events("a <!-- b --> c");
assert!(
!has(&ev, |e| matches!(e, Event::InlineHtml(_))),
"<!-- should not be inline HTML in MDX: {:?}",
ev
);
}
#[test]
fn jsx_text_in_blockquote() {
let ev = mdx_events("> a <b>\n> c </b> d.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"jsx in blockquote: {:?}",
ev
);
assert!(
has(&ev, |e| matches!(e, Event::Start(Tag::BlockQuote(_)))),
"has blockquote: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_attr_in_blockquote() {
let ev = mdx_events("> a <b c=\"d\ne\" /> f");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline attr in blockquote: {:?}",
ev
);
}
#[test]
fn jsx_text_multiline_expr_attr_in_blockquote() {
let ev = mdx_events("> a <b c={d\ne} /> f");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"multiline expr attr in blockquote: {:?}",
ev
);
}
#[test]
fn expr_flow_in_blockquote() {
let ev = mdx_events("> {a}\n");
assert!(
has(
&ev,
|e| matches!(e, Event::MdxFlowExpression(s) if s.as_ref() == "a")
) || has(
&ev,
|e| matches!(e, Event::MdxTextExpression(s) if s.as_ref() == "a")
),
"expression in blockquote: {:?}",
ev
);
}
#[test]
fn jsx_text_markdown_children() {
let ev = mdx_events("a <b>*c*</b> d.");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"jsx with markdown children: {:?}",
ev
);
assert!(
has(&ev, |e| matches!(e, Event::Start(Tag::Emphasis))),
"emphasis inside jsx: {:?}",
ev
);
}
#[test]
fn jsx_text_nested_tags() {
let ev = mdx_events("a <>b <>c</> d</>.");
let jsx_count = count(&ev, |e| {
matches!(e, Event::Start(Tag::MdxJsxTextElement(_)))
});
assert!(jsx_count >= 2, "nested fragments: {:?}", ev);
}
#[test]
fn jsx_flow_with_content() {
let ev = mdx_events("<a>\nb\n</a>\n");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxFlowElement(_))
)),
"flow jsx with content: {:?}",
ev
);
}
#[test]
fn jsx_text_unclosed_does_not_crash() {
let ev = mdx_events("a <b> c");
assert!(
has(&ev, |e| matches!(
e,
Event::Start(Tag::MdxJsxTextElement(_))
)),
"unclosed tag should parse: {:?}",
ev
);
}
#[test]
fn jsx_text_mismatched_does_not_crash() {
let ev = mdx_events("a <></b> c");
let _ = ev;
}
#[test]
fn jsx_text_closing_without_open_does_not_crash() {
let ev = mdx_events("a </> c.");
let _ = ev;
}
#[test]
fn jsx_text_closing_self_closing_does_not_crash() {
let ev = mdx_events("a <a>b</a/> c");
let _ = ev;
}
#[test]
fn jsx_text_closing_with_attr_does_not_crash() {
let ev = mdx_events("a <a>b</a c> d");
let _ = ev;
}
#[test]
fn tab_indented_code_fence_in_list() {
let input = "5. item:\n\n\t\t```ts\n\tcontent {a: 1}\n\t\t```\n";
let events = mdx_events(input);
let has_code_block = has(&events, |e| matches!(e, Event::Start(Tag::CodeBlock(_))));
assert!(
has_code_block,
"Tab-indented code fence in list not detected: {:?}",
events
);
let has_expr = has(&events, |e| {
matches!(e, Event::MdxFlowExpression(_) | Event::MdxTextExpression(_))
});
assert!(
!has_expr,
"Content inside code fence incorrectly parsed as MDX expression: {:?}",
events
);
}
#[test]
fn tab_indented_code_fence_standalone() {
let input = "\t\t```ts\ncontent {a: 1}\n\t\t```\n";
let events = mdx_events(input);
let has_code_block = has(&events, |e| matches!(e, Event::Start(Tag::CodeBlock(_))));
assert!(
has_code_block,
"Tab-indented standalone code fence not detected: {:?}",
events
);
}
#[test]
fn expression_with_double_quote_string() {
let events = mdx_events(r#"{"hello"}"#);
let has_expr = has(&events, |e| matches!(e, Event::MdxFlowExpression(_)));
assert!(
has_expr,
"Double-quote string expression not detected: {:?}",
events
);
for e in &events {
if let Event::MdxFlowExpression(content) = e {
assert_eq!(content.as_ref(), r#""hello""#, "Wrong expression content");
}
}
}