diff --git a/.gitignore b/.gitignore index d6fdae5..e8f8b67 100644 --- a/.gitignore +++ b/.gitignore @@ -193,4 +193,5 @@ $RECYCLE.BIN/ !.yarn/versions *.node -.vscode \ No newline at end of file +.vscode +.idea \ No newline at end of file diff --git a/__test__/__snapshots__/index.spec.ts.snap b/__test__/__snapshots__/index.spec.ts.snap index 7aed824..3d18846 100644 --- a/__test__/__snapshots__/index.spec.ts.snap +++ b/__test__/__snapshots__/index.spec.ts.snap @@ -438,6 +438,12 @@ exports[`compile > should render container type correctly 1`] = `

This is a block of type danger

+
+ DETAILS +
+

This is a block of type details

+
+
" `; @@ -449,6 +455,8 @@ function _createMdxContent(props) { div: "div", p: "p", code: "code", + details: "details", + summary: "summary", }, _provideComponents(), props.components @@ -544,6 +552,21 @@ function _createMdxContent(props) { + {"\\n"} + <_components.details className="rspress-directive details"> + <_components.summary className="rspress-directive-title"> + {"DETAILS"} + + <_components.div className="rspress-directive-content"> + <_components.p> + {"This is a "} + <_components.code>{"block"} + {" of type "} + <_components.code>{"details"} + {"\\n"} + + + ); } @@ -749,3 +772,157 @@ function MDXContent(props = {}) { export default MDXContent; " `; + +exports[`compile > should render github alerts correctly 1`] = ` +"
+
TIP
+
+

+ Helpful advice for doing things better or more easily. +

+
+
+
+
NOTE
+
+

Please read this note!

+
+
+
+
WARNING
+
+

Use dummy instead of demo

+
+
+
+
CAUTION
+
+

Use this code:-

+
console.log(69);
+
+
+
+
+ DETAILS +
+

This is a block of type details

+
+
+" +`; + +exports[`compile > should render github alerts correctly 2`] = ` +"import { useMDXComponents as _provideComponents } from "@mdx-js/react"; +function _createMdxContent(props) { + const _components = Object.assign( + { + div: "div", + p: "p", + strong: "strong", + h1: "h1", + code: "code", + pre: "pre", + details: "details", + summary: "summary", + }, + _provideComponents(), + props.components + ); + return ( + <> + <_components.div className="rspress-directive TIP"> + <_components.div className="rspress-directive-title"> + {"TIP"} + + <_components.div className="rspress-directive-content"> + {"\\n"} + <_components.p> + <_components.strong> + {"Helpful advice for doing things better or more easily."} + + + {"\\n"} + + + {"\\n"} + <_components.div className="rspress-directive NOTE"> + <_components.div className="rspress-directive-title"> + {"NOTE"} + + <_components.div className="rspress-directive-content"> + {"\\n"} + {"\\n"} + <_components.h1>{"Please read this note!"} + {"\\n"} + + + {"\\n"} + <_components.div className="rspress-directive warning"> + <_components.div className="rspress-directive-title"> + {"WARNING"} + + <_components.div className="rspress-directive-content"> + {"\\n"} + <_components.p> + {"Use "} + <_components.code>{"dummy"} + {" instead of "} + <_components.code>{"demo"} + + {"\\n"} + + + {"\\n"} + <_components.div className="rspress-directive CAUTION"> + <_components.div className="rspress-directive-title"> + {"CAUTION"} + + <_components.div className="rspress-directive-content"> + {"\\n"} + {"\\n"} + <_components.p>{"Use this code:-"} + {"\\n"} + <_components.pre> + <_components.code className="language-javascript"> + {"console.log(69);\\n"} + + + {"\\n"} + + + {"\\n"} + <_components.details className="rspress-directive details"> + <_components.summary className="rspress-directive-title"> + {"DETAILS"} + + <_components.div className="rspress-directive-content"> + {"\\n"} + <_components.p> + {"This is a "} + <_components.code>{"block"} + {" of type "} + <_components.code>{"details"} + + {"\\n"} + + + + ); +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = Object.assign( + {}, + _provideComponents(), + props.components + ); + return MDXLayout ? ( + + <_createMdxContent {...props} /> + + ) : ( + _createMdxContent(props) + ); +} +export default MDXContent; +" +`; diff --git a/__test__/container-type.md b/__test__/container-type.md index d501f1d..83b489f 100644 --- a/__test__/container-type.md +++ b/__test__/container-type.md @@ -21,3 +21,7 @@ This is a `block` of type `danger` :::caution This is a `block` of type `danger` ::: + +:::details +This is a `block` of type `details` +::: \ No newline at end of file diff --git a/__test__/github-alert-syntax.md b/__test__/github-alert-syntax.md new file mode 100644 index 0000000..81fbdbf --- /dev/null +++ b/__test__/github-alert-syntax.md @@ -0,0 +1,19 @@ +> [!TIP] +> **Helpful advice for doing things better or more easily.** + +> [!NOTE] +> +> # Please read this note! + +> [!warning] +> Use `dummy` instead of `demo` + +> [!CAUTION] +> +> Use this code:- +> ```javascript +> console.log(69); +> ``` + +> [!details] +> This is a `block` of type `details` diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 37e886c..9192540 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -42,9 +42,9 @@ describe("compile", () => { test("should render container type correctly", async () => { const { code: result, html } = await testCompile({ value: readFileSync(path.join(__dirname, "./container-type.md"), "utf8"), - filepath: "xxx.mdx", + filepath: "container-type.md", development: true, - root: "xxx", + root: "", }); expect(html).toMatchSnapshot(); @@ -57,9 +57,9 @@ describe("compile", () => { path.join(__dirname, "./container-type-with-space.md"), "utf8", ), - filepath: "xxx.mdx", + filepath: "container-type-with-space.md", development: true, - root: "xxx", + root: "", }); expect(html).toMatchSnapshot(); @@ -72,9 +72,9 @@ describe("compile", () => { path.join(__dirname, "./container-content.md"), "utf8", ), - filepath: "xxx.mdx", + filepath: "container-content.md", development: true, - root: "xxx", + root: "", }); expect(html).toMatchSnapshot(); @@ -87,9 +87,9 @@ describe("compile", () => { path.join(__dirname, "./container-title.mdx"), "utf8", ), - filepath: "xxx.mdx", + filepath: "container-title.mdx", development: true, - root: "xxx", + root: "", }); expect(html).toMatchSnapshot(); @@ -99,9 +99,24 @@ describe("compile", () => { test("should render container title in md correctly", async () => { const { code: result, html } = await testCompile({ value: readFileSync(path.join(__dirname, "./container-title.md"), "utf8"), - filepath: "xxx.md", + filepath: "container-title.md", development: true, - root: "xxx", + root: "", + }); + + expect(html).toMatchSnapshot(); + expect(result).toMatchSnapshot(); + }); + + test("should render github alerts correctly", async () => { + const { code: result, html } = await testCompile({ + value: readFileSync( + path.join(__dirname, "./github-alert-syntax.md"), + "utf8", + ), + filepath: "github-alert-syntax.md", + development: true, + root: "", }); expect(html).toMatchSnapshot(); diff --git a/crates/plugin_container/src/lib.rs b/crates/plugin_container/src/lib.rs index 16576cb..be38ead 100644 --- a/crates/plugin_container/src/lib.rs +++ b/crates/plugin_container/src/lib.rs @@ -59,8 +59,12 @@ fn create_new_container_node( container_title.to_string() }; + let is_details = container_type == "details"; + let title_tag_name = if is_details { "summary" } else { "div" }; + let root_tag_name = if is_details { "details" } else { "div" }; + let container_title_node = hast::Element { - tag_name: "div".into(), + tag_name: title_tag_name.into(), properties: vec![( "className".into(), hast::PropertyValue::SpaceSeparated(vec!["rspress-directive-title".into()]), @@ -81,7 +85,7 @@ fn create_new_container_node( position: None, }; hast::Node::Element(hast::Element { - tag_name: "div".into(), + tag_name: root_tag_name.into(), properties: vec![( "className".into(), hast::PropertyValue::SpaceSeparated(vec!["rspress-directive".into(), container_type.into()]), @@ -110,6 +114,61 @@ fn wrap_node_with_paragraph( hast::Node::Element(paragraph) } +fn is_valid_container_type(container_type: &String) -> bool { + let mut container_type = container_type.clone(); + container_type.make_ascii_lowercase(); + let valid_types = [ + "tip", "note", "warning", "caution", "danger", "info", "details", + ]; + valid_types.contains(&container_type.as_str()) +} + +fn parse_github_alerts_container_meta(meta: &str) -> (String, String) { + // GitHub Alert's verification is very strict. + // space and breaks are not allowed, they must be a whole. + // but can remove spaces or breaks at the beginning and end. + let lines = meta.lines(); + + let mut container_type = String::new(); + let mut remaining_data = String::new(); + + let mut is_first_line = true; + + for line in lines { + // clear breaks if no container_type + if container_type.is_empty() && line.is_empty() { + continue; + } + + if container_type.is_empty() && is_first_line { + is_first_line = false; + + let split_line = line.trim().split_once(']'); + + container_type = split_line + .unwrap_or(("", "")) + .0 + .to_owned() + .replace("[!", ""); + remaining_data = split_line.unwrap_or(("", "")).1.to_owned(); + + if container_type.is_empty() { + break; + } + + continue; + } + + if remaining_data.is_empty() { + remaining_data = line.to_owned(); + } else { + remaining_data = format!("{}\n{}", remaining_data, line); + } + } + + (container_type, remaining_data) +} + fn traverse_children(root: &mut hast::Root) { let mut container_type = String::new(); let mut container_title = String::new(); @@ -123,32 +182,93 @@ fn traverse_children(root: &mut hast::Root) { let child = &root.children[index]; if let hast::Node::Element(element) = child { // Meet the start of the container - if element.tag_name == "p" && !container_content_start { - if let Some(hast::Node::Text(text)) = element.children.first() { - if text.value.starts_with(":::") { - (container_type, container_title) = parse_container_meta(&text.value); - // If the second element is MdxExpression, we parse the value and reassign the container_title - if let Some(hast::Node::MdxExpression(expression)) = element.children.get(1) { - container_title = parse_title_from_meta(&expression.value); + if !container_content_start { + // e.g. :::tip + if element.tag_name == "p" { + if let Some(hast::Node::Text(text)) = element.children.first() { + if text.value.starts_with(":::") { + (container_type, container_title) = parse_container_meta(&text.value); + if !is_valid_container_type(&container_type) { + index += 1; + continue; + } + // If the second element is MdxExpression, we parse the value and reassign the container_title + if let Some(hast::Node::MdxExpression(expression)) = element.children.get(1) { + container_title = parse_title_from_meta(&expression.value); + } + container_content_start = true; + container_content_start_index = index; + // :::tip\nThis is a tip + // We should record the `This is a tip` + for line in text.value.lines().skip(1) { + if line.ends_with(":::") { + container_content_end = true; + container_content_end_index = index; + break; + }; + + container_content.push(wrap_node_with_paragraph( + &element.properties.clone(), + &[hast::Node::Text(hast::Text { + value: line.into(), + position: None, + })], + )); + } } - container_content_start = true; - container_content_start_index = index; - // :::tip\nThis is a tip - // We should record the `This is a tip` - for line in text.value.lines().skip(1) { - if line.ends_with(":::") { - container_content_end = true; - container_content_end_index = index; - break; - }; - - container_content.push(wrap_node_with_paragraph( - &element.properties.clone(), - &[hast::Node::Text(hast::Text { - value: line.into(), - position: None, - })], - )); + } + } + // e.g. > [!tip] + if element.tag_name == "blockquote" { + // why use element.children.get(1)? + // in crates/mdx_rs/mdast_util_to_hast.rs, method `transform_block_quote` + // always insert Text { value: "\n".into(), position: None } in blockquote's children + if let Some(hast::Node::Element(first_element)) = element.children.get(1) { + if first_element.tag_name == "p" { + if let Some(hast::Node::Text(text)) = first_element.children.first() { + if text.value.trim().starts_with("[!") { + // split data if previous step parse in one line + // e.g

[!TIP] this is a tip

+ let (self_container_type, remaining_data) = + parse_github_alerts_container_meta(&text.value); + if !is_valid_container_type(&self_container_type) { + index += 1; + continue; + } + // in this case, container_type as container_title + container_type = self_container_type.clone(); + container_title = self_container_type.clone(); + container_title.make_ascii_uppercase(); + + container_content_start = true; + container_content_start_index = index; + + // reform paragraph tag + let mut paragraph_children = first_element.children.clone(); + if !remaining_data.is_empty() { + paragraph_children[0] = hast::Node::Text(hast::Text { + value: remaining_data, + position: None, + }) + } else { + paragraph_children.remove(0); + } + // reform blockquote tag + let mut children = element.children.clone(); + + if paragraph_children.is_empty() { + children.remove(1); + } else { + children[1] = + wrap_node_with_paragraph(&element.properties.clone(), ¶graph_children) + } + + container_content = children; + + container_content_end = true; + container_content_end_index = index; + } + } } } } @@ -247,17 +367,22 @@ fn traverse_children(root: &mut hast::Root) { } pub fn mdx_plugin_container(root: &mut hast::Node) { - // Traverse children, get all p tags, check if they start with ::: + // 1. Traverse children, get all p tags, check if they start with ::: // If it is, it is regarded as container syntax, and the content from the beginning of ::: to the end of a certain ::: is regarded as a container - // The element of this container is a div element, className is "rspress-container" + // 2. Traverse children, get all blockquote tags, check if they next child's first element is p tags and if start with [! and end of ] + // If it is, it is regarded as container syntax, and the content from the beginning of blockquote to the end of a certain blockquote is regarded as a container + // The element of this container is a div element, className is "rspress-directive" // for example: // :::tip // this is a tip // ::: + // or + // > [!tip] + // > this is a tip // Will be transformed to: - //
- //
tip
- //
+ //
+ //
tip
+ //
//

This is a tip

//
//
@@ -612,6 +737,22 @@ mod tests { ); } + #[test] + fn test_parse_github_alerts_container_meta() { + assert_eq!( + parse_github_alerts_container_meta("[!TIP]"), + ("TIP".into(), "".into()) + ); + assert_eq!( + parse_github_alerts_container_meta("[!TIP this is tip block"), + ("".into(), "".into()) + ); + assert_eq!( + parse_github_alerts_container_meta("[!TIP] this is tip block"), + ("TIP".into(), " this is tip block".into()) + ); + } + #[test] fn test_container_plugin_with_mdx_flow_in_content() { let mut root = hast::Node::Root(hast::Root { @@ -689,4 +830,158 @@ mod tests { }) ); } + + #[test] + fn test_container_plugin_width_details_title() { + let mut root = hast::Node::Root(hast::Root { + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: ":::details Note".into(), + position: None, + })], + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "This is a tip".into(), + position: None, + })], + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: ":::".into(), + position: None, + })], + position: None, + }), + ], + position: None, + }); + + mdx_plugin_container(&mut root); + + assert_eq!( + root, + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "details".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive".into(), "details".into()]) + ),], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "summary".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive-title".into()]) + )], + children: vec![hast::Node::Text(hast::Text { + value: "Note".into(), + position: None, + })], + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "div".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive-content".into()]) + )], + children: vec![hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "This is a tip".into(), + position: None, + })], + position: None, + })], + position: None, + }) + ], + position: None, + })], + position: None, + }) + ); + } + + #[test] + fn test_container_plugin_with_github_alerts_title() { + let mut root = hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "blockquote".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "[!TIP]".into(), + position: None, + })], + position: None, + }), + ], + position: None, + })], + position: None, + }); + + mdx_plugin_container(&mut root); + + assert_eq!( + root, + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "div".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive".into(), "TIP".into()]) + ),], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "div".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive-title".into()]) + )], + children: vec![hast::Node::Text(hast::Text { + value: "TIP".into(), + position: None, + })], + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "div".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["rspress-directive-content".into()]) + )], + children: vec![hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }),], + position: None, + }) + ], + position: None, + })], + position: None, + }) + ); + } }