diff --git a/src/generator.rs b/src/generator.rs index 6d6d8ac..dfd4017 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -42,20 +42,22 @@ pub fn rustdoc(input: String) -> Result { str } - GrammarItem::Text(v) => if group_started { - v.replacen("*", "", 1) - } else { - v - }, + GrammarItem::Text(v) => { + if group_started { + v.replacen("*", "", 1) + } else { + v + } + } // See GrammarItem::GroupStart => { group_started = true; String::from("# ") - }, + } GrammarItem::GroupEnd => { group_started = false; - continue - }, + continue; + } }; } @@ -171,6 +173,12 @@ fn generate_notation( "par" => String::from("# "), "details" | "pre" | "post" => String::from("\n\n"), "brief" | "short" => String::new(), + "code" => { + let lang = params.first().map(|p| p.as_str()).unwrap_or_default(); + let lang = lang.strip_prefix('.').unwrap_or(lang); + format!("```{lang}") + } + "endcode" => String::from("```"), _ => String::new(), }, (new_param, new_return, new_throw), @@ -359,6 +367,22 @@ mod test { ); } + #[test] + fn code() { + test_rustdoc!( + "@code\nfn main() {\n test( [1] ); // @code @throw\n@endcode", + "```\nfn main() {\n test( [1] ); // @code @throw\n```" + ); + } + + #[test] + fn code_with_lang() { + test_rustdoc!( + "@code{.rs}\nfn main() {\n test( [1] ); // @code @throw\n@endcode", + "```rs\nfn main() {\n test( [1] ); // @code @throw\n```" + ); + } + #[test] fn can_parse_example() { let example = include_str!("../tests/assets/example-bindgen.rs"); diff --git a/src/lexer.rs b/src/lexer.rs index 589753f..9c917ed 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -3,10 +3,22 @@ pub(crate) enum LexItem { At(String), Paren(char), Word(String), - Space, + Whitespace(char), NewLine, } +impl LexItem { + pub(crate) fn push_to(&self, acc: &mut String) { + match self { + LexItem::At(s) => acc.push_str(s), + LexItem::Paren(c) => acc.push(*c), + LexItem::Word(s) => acc.push_str(s), + LexItem::Whitespace(c) => acc.push(*c), + LexItem::NewLine => acc.push('\n'), + } + } +} + pub(crate) fn lex(input: String) -> Vec { let mut result = vec![]; @@ -34,12 +46,8 @@ pub(crate) fn lex(input: String) -> Vec { '{' | '}' => { result.push(LexItem::Paren(c)); } - ' ' => { - if let Some(v) = result.last_mut() { - if !matches!(v, LexItem::Space) { - result.push(LexItem::Space); - } - } + ' ' | '\t' => { + result.push(LexItem::Whitespace(c)); } '\n' => { result.push(LexItem::NewLine); @@ -72,9 +80,9 @@ mod test { vec![ LexItem::At("@".into()), LexItem::Word("name".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Memory".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Management".into()) ] ); @@ -85,9 +93,9 @@ mod test { vec![ LexItem::At("\\".into()), LexItem::Word("name".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Memory".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Management".into()) ] ); @@ -98,9 +106,9 @@ mod test { vec![ LexItem::At("\\\\".into()), LexItem::Word("name".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Memory".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Management".into()) ] ); @@ -116,12 +124,12 @@ mod test { LexItem::Paren('{'), LexItem::NewLine, LexItem::Word("*".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::At("@".into()), LexItem::Word("name".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Memory".into()), - LexItem::Space, + LexItem::Whitespace(' '), LexItem::Word("Management".into()), LexItem::NewLine, LexItem::At("@".into()), diff --git a/src/parser.rs b/src/parser.rs index 8d772fb..5345fca 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -24,11 +24,14 @@ pub(crate) enum GrammarItem { GroupEnd, } +enum ParamParser { + None, + Whitespace, + Paren, +} + pub(crate) fn parse(input: String) -> Result, ParseError> { - let mut lexed = lex(input); - lexed.push(LexItem::Space); - lexed.push(LexItem::Space); - lexed.push(LexItem::Space); + let lexed = lex(input); parse_items(lexed) } @@ -36,9 +39,36 @@ fn parse_items(input: Vec) -> Result, ParseError> { let mut grammar_items = vec![]; let mut param_iter_skip_count = 0; - for item in input.windows(4) { - let current = item.get(0).unwrap(); - let next = item.get(1); + for (index, current) in input.iter().enumerate() { + let rest = &input[index..]; + let next = rest.get(1); + + if param_iter_skip_count > 0 { + param_iter_skip_count -= 1; + continue; + } + + // Do not do any formatting inside of code blocks + let ends_code = matches!(current, LexItem::At(_)) + && matches!(next, Some(LexItem::Word(v)) if v == "endcode"); + if !ends_code { + match &mut grammar_items[..] { + [.., GrammarItem::Notation { tag, .. }] if tag == "code" => { + let mut text = String::new(); + current.push_to(&mut text); + + grammar_items.push(GrammarItem::Text(text)); + continue; + } + [.., GrammarItem::Notation { tag, .. }, GrammarItem::Text(text)] + if tag == "code" => + { + current.push_to(text); + continue; + } + _ => {} + } + } match current { LexItem::At(_) => { @@ -56,9 +86,10 @@ fn parse_items(input: Vec) -> Result, ParseError> { }, LexItem::Word(v) => { let mut meta = vec![]; - let params; let content; + let param_parser; + if v.starts_with("param") { let value = v.split('[').collect::>(); match value.get(1) { @@ -79,51 +110,63 @@ fn parse_items(input: Vec) -> Result, ParseError> { }, } - params = match item.get(3) { - None => vec![], - Some(LexItem::Word(v)) => vec![v.into()], - Some(_) => vec![], - }; - - content = "param" + content = "param"; + param_parser = ParamParser::Whitespace; } else { content = v; - params = match v.as_str() { + param_parser = match v.as_str() { "a" | "b" | "c" | "p" | "emoji" | "e" | "em" | "def" | "class" | "category" | "concept" | "enum" | "example" | "extends" | "file" | "sa" | "see" | "retval" - | "exception" | "throw" | "throws" => match item.get(3) { - None => vec![], - Some(LexItem::Word(v)) => vec![v.into()], - Some(_) => vec![], - }, - _ => vec![], + | "exception" | "throw" | "throws" => ParamParser::Whitespace, + "code" => ParamParser::Paren, + _ => ParamParser::None, }; } - if params.is_empty() { - param_iter_skip_count = 1; + let param = match param_parser { + ParamParser::None => None, + ParamParser::Whitespace => rest + .iter() + .enumerate() + .skip(2) + .find(|(_, next)| !matches!(next, LexItem::Whitespace(_))) + .and_then(|(skip, next)| match next { + LexItem::Word(word) => Some((skip, word)), + _ => None, + }), + ParamParser::Paren => match &rest { + [_, _, LexItem::Paren('{'), LexItem::Word(word), LexItem::Paren('}'), ..] => { + Some((4, word)) + } + _ => None, + }, + }; + + let params = if let Some((skip, word)) = param { + param_iter_skip_count = skip; + vec![word.into()] } else { - param_iter_skip_count = 2; - } + param_iter_skip_count = 1; + vec![] + }; grammar_items.push(GrammarItem::Notation { meta, params, tag: content.into(), }); + + if content == "endcode" { + grammar_items.push(GrammarItem::Text("".into())); + } } _ => {} } } } LexItem::Word(v) => { - if param_iter_skip_count > 0 { - param_iter_skip_count -= 1; - continue; - } - if let Some(prev) = grammar_items.last_mut() { match prev { GrammarItem::Text(text) => *text += v, @@ -133,14 +176,18 @@ fn parse_items(input: Vec) -> Result, ParseError> { grammar_items.push(GrammarItem::Text(v.into())); } } - LexItem::Space => { + LexItem::Whitespace(_) => { if let Some(prev) = grammar_items.last_mut() { match prev { + GrammarItem::Text(text) if text.ends_with(' ') => {} GrammarItem::Text(text) => *text += " ", + GrammarItem::Notation { params, .. } if !params.is_empty() => { + grammar_items.push(GrammarItem::Text(" ".into())) + } _ => grammar_items.push(GrammarItem::Text("".into())), } } else { - grammar_items.push(GrammarItem::Text(" ".into())) + grammar_items.push(GrammarItem::Text(" ".into())); } } LexItem::NewLine => { @@ -212,6 +259,24 @@ mod test { ); } + #[test] + pub fn param_tabs() { + let result = + parse("@param[in]\trandom\t\t\tThis is, without a doubt, a random argument.".into()) + .unwrap(); + assert_eq!( + result, + vec![ + GrammarItem::Notation { + meta: vec!["in".into()], + params: vec!["random".into()], + tag: "param".into(), + }, + GrammarItem::Text(" This is, without a doubt, a random argument.".into()) + ] + ); + } + #[test] pub fn groups() { let result = parse("@{\n* @name Memory Management\n@}".into()).unwrap(); @@ -233,7 +298,11 @@ mod test { #[test] pub fn trims_param_texts() { - let result = parse("@param[in] var Example description".into()).unwrap(); + let result = parse( + "@param[in] var Example description" + .into(), + ) + .unwrap(); assert_eq!( result, vec![ @@ -246,4 +315,79 @@ mod test { ] ) } + + #[test] + pub fn code() { + let result = parse("@code\nfn main() {}\n@endcode".into()).unwrap(); + + assert_eq!( + result, + vec![ + GrammarItem::Notation { + meta: vec![], + params: vec![], + tag: "code".into(), + }, + GrammarItem::Text("\nfn main() {}\n".into()), + GrammarItem::Notation { + meta: vec![], + params: vec![], + tag: "endcode".into(), + }, + GrammarItem::Text("".into()) + ] + ) + } + + #[test] + pub fn code_with_param() { + let result = parse("@code{.py}\nfn main() {}\n@endcode".into()).unwrap(); + + assert_eq!( + result, + vec![ + GrammarItem::Notation { + meta: vec![], + params: vec![".py".into()], + tag: "code".into(), + }, + GrammarItem::Text("\nfn main() {}\n".into()), + GrammarItem::Notation { + meta: vec![], + params: vec![], + tag: "endcode".into(), + }, + GrammarItem::Text("".into()) + ] + ) + } + + #[test] + pub fn code_with_args() { + let result = parse("@code\nfn main() {}\n@endcode\n\n@param[in] a - a".into()).unwrap(); + + assert_eq!( + result, + vec![ + GrammarItem::Notation { + meta: vec![], + params: vec![], + tag: "code".into(), + }, + GrammarItem::Text("\nfn main() {}\n".into()), + GrammarItem::Notation { + meta: vec![], + params: vec![], + tag: "endcode".into(), + }, + GrammarItem::Text("\n\n".into()), + GrammarItem::Notation { + meta: vec!["in".into()], + params: vec!["a".into()], + tag: "param".into() + }, + GrammarItem::Text(" - a".into()) + ] + ) + } }