diff options
Diffstat (limited to 'src/ast.rs')
| -rw-r--r-- | src/ast.rs | 321 |
1 files changed, 170 insertions, 151 deletions
@@ -174,185 +174,204 @@ pub enum Node { Whitespace, } -/// Build an AST tree from Gemtext. -/// -/// # Example -/// -/// ```rust -/// germ::ast::build(r#"=> gemini://gem.rest/ GemRest"#); -/// ``` -#[must_use] -pub fn build(source: &str) -> Vec<Node> { - let mut ast = vec![]; - let mut in_preformatted = false; - let mut in_list = false; - let mut lines = source.lines(); +pub struct Ast { + inner: Vec<Node>, +} +impl Ast { + /// Build an AST tree from Gemtext. + /// + /// # Example + /// + /// ```rust + /// let _ = germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#); + /// ``` + #[must_use] + pub fn from_string(source: &str) -> Self { + let mut ast = vec![]; + let mut in_preformatted = false; + let mut in_list = false; + let mut lines = source.lines(); - // Iterate over all lines in the Gemtext `source` - while let Some(line) = lines.next() { - // Evaluate the Gemtext line and append its AST node to the `ast` tree - ast.append(&mut evaluate( - line, - &mut lines, - &mut in_preformatted, - &mut in_list, - )); - } + // Iterate over all lines in the Gemtext `source` + while let Some(line) = lines.next() { + // Evaluate the Gemtext line and append its AST node to the `ast` tree + ast.append(&mut Self::evaluate( + line, + &mut lines, + &mut in_preformatted, + &mut in_list, + )); + } - ast -} + Self { + inner: ast + } + } -#[allow(clippy::too_many_lines)] -fn evaluate( - line: &str, - lines: &mut std::str::Lines<'_>, - in_preformatted: &mut bool, - in_list: &mut bool, -) -> Vec<Node> { - let mut preformatted = String::new(); - let mut alt_text = String::new(); - let mut nodes = vec![]; - let mut line = line; - let mut list_items = vec![]; + /// The actual AST of `Ast` + /// + /// # Example + /// + /// ```rust + /// + /// let _ = germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#).inner(); + /// ``` + #[must_use] + pub const fn inner(&self) -> &Vec<Node> { &self.inner } - // Enter a not-so-infinite loop as sometimes, we may need to stay in an - // evaluation loop, e.g., multiline contexts: preformatted text, lists, etc. - loop { - // Match the first character of the Gemtext line to understand the line type - match line.get(0..1).unwrap_or("") { - "=" => { - // If the Gemtext line starts with an "=" ("=>"), it is a link line, so - // splitting it up should be easy enough. - let line = line.get(2..).unwrap(); - let mut split = line - .split_whitespace() - .map(String::from) - .collect::<Vec<String>>() - .into_iter(); + #[allow(clippy::too_many_lines)] + fn evaluate( + line: &str, + lines: &mut std::str::Lines<'_>, + in_preformatted: &mut bool, + in_list: &mut bool, + ) -> Vec<Node> { + let mut preformatted = String::new(); + let mut alt_text = String::new(); + let mut nodes = vec![]; + let mut line = line; + let mut list_items = vec![]; - nodes.push(Node::Link { - to: split.next().expect("no location in link"), - text: { - let rest = split.collect::<Vec<String>>().join(" "); + // Enter a not-so-infinite loop as sometimes, we may need to stay in an + // evaluation loop, e.g., multiline contexts: preformatted text, lists, etc. + loop { + // Match the first character of the Gemtext line to understand the line + // type + match line.get(0..1).unwrap_or("") { + "=" => { + // If the Gemtext line starts with an "=" ("=>"), it is a link line, + // so splitting it up should be easy enough. + let line = line.get(2..).unwrap(); + let mut split = line + .split_whitespace() + .map(String::from) + .collect::<Vec<String>>() + .into_iter(); - if rest.is_empty() { - None - } else { - Some(rest) - } - }, - }); + nodes.push(Node::Link { + to: split.next().expect("no location in link"), + text: { + let rest = split.collect::<Vec<String>>().join(" "); - break; - } - "#" => { - // If the Gemtext line starts with an "#", it is a heading, so let's - // find out how deep it goes. - let level = match line.get(0..3) { - Some(root) => - if root.contains("###") { - 3 - } else if root.contains("##") { - 2 - } else if root.contains('#') { - 1 - } else { - 0 + if rest.is_empty() { + None + } else { + Some(rest) + } }, - None => 0, - }; - - nodes.push(Node::Heading { - level, - // Here, we are `get`ing the `&str` starting at the `level`-th index, - // then trimming the start. These operations effectively off the line - // identifier. - text: line.get(level..).unwrap_or("").trim_start().to_string(), - }); + }); - break; - } - "*" => { - // If the Gemtext line starts with an asterisk, it is a list item, so - // let's enter a list context. - if !*in_list { - *in_list = true; + break; } + "#" => { + // If the Gemtext line starts with an "#", it is a heading, so let's + // find out how deep it goes. + let level = match line.get(0..3) { + Some(root) => + if root.contains("###") { + 3 + } else if root.contains("##") { + 2 + } else if root.contains('#') { + 1 + } else { + 0 + }, + None => 0, + }; - list_items.push(line.get(1..).unwrap_or("").trim_start().to_string()); + nodes.push(Node::Heading { + level, + // Here, we are `get`ing the `&str` starting at the `level`-th + // index, then trimming the start. These operations + // effectively off the line identifier. + text: line.get(level..).unwrap_or("").trim_start().to_string(), + }); - line = lines.next().unwrap(); - } - ">" => { - // If the Gemtext line starts with an ">", it is a blockquote, so let's - // just clip off the line identifier. - nodes.push(Node::Blockquote( - line.get(1..).unwrap_or("").trim_start().to_string(), - )); + break; + } + "*" => { + // If the Gemtext line starts with an asterisk, it is a list item, so + // let's enter a list context. + if !*in_list { + *in_list = true; + } - break; - } - "`" => { - // If the Gemtext line starts with a backtick, it is a list item, so - // let's enter a preformatted text context. - *in_preformatted = !*in_preformatted; + list_items.push(line.get(1..).unwrap_or("").trim_start().to_string()); - if *in_preformatted { - alt_text = line.get(3..).unwrap_or("").to_string(); line = lines.next().unwrap(); - } else { - nodes.push(Node::PreformattedText { - alt_text: if alt_text.is_empty() { - None - } else { - Some(alt_text) - }, - text: preformatted, - }); + } + ">" => { + // If the Gemtext line starts with an ">", it is a blockquote, so + // let's just clip off the line identifier. + nodes.push(Node::Blockquote( + line.get(1..).unwrap_or("").trim_start().to_string(), + )); break; } - } - "" if !*in_preformatted => { - // If the line has nothing on it, it is a whitespace line, as long as we - // aren't in a preformatted line context. - nodes.push(Node::Whitespace); + "`" => { + // If the Gemtext line starts with a backtick, it is a list item, so + // let's enter a preformatted text context. + *in_preformatted = !*in_preformatted; - break; - } - // This as a catchall, it does a number of things. - _ => { - if *in_preformatted { - // If we are in a preformatted line context, add the line to the - // preformatted blocks content and increment the line. - preformatted.push_str(&format!("{}\n", line)); + if *in_preformatted { + alt_text = line.get(3..).unwrap_or("").to_string(); + line = lines.next().unwrap(); + } else { + nodes.push(Node::PreformattedText { + alt_text: if alt_text.is_empty() { + None + } else { + Some(alt_text) + }, + text: preformatted, + }); - line = lines.next().unwrap(); - } else { - // If we are in a list item and hit a catchall, that must mean that we - // encountered a line which is not a list line, so let's stop adding - // items to the list context. - if *in_list { - *in_list = false; + break; + } + } + "" if !*in_preformatted => { + // If the line has nothing on it, it is a whitespace line, as long as + // we aren't in a preformatted line context. + nodes.push(Node::Whitespace); + + break; + } + // This as a catchall, it does a number of things. + _ => { + if *in_preformatted { + // If we are in a preformatted line context, add the line to the + // preformatted blocks content and increment the line. + preformatted.push_str(&format!("{}\n", line)); + + line = lines.next().unwrap(); + } else { + // If we are in a list item and hit a catchall, that must mean that + // we encountered a line which is not a list line, so + // let's stop adding items to the list context. + if *in_list { + *in_list = false; + + nodes.push(Node::Text(line.to_string())); + + break; + } nodes.push(Node::Text(line.to_string())); break; } - - nodes.push(Node::Text(line.to_string())); - - break; } } } - } - if !list_items.is_empty() { - nodes.reverse(); - nodes.push(Node::List(list_items)); - nodes.reverse(); - } + if !list_items.is_empty() { + nodes.reverse(); + nodes.push(Node::List(list_items)); + nodes.reverse(); + } - nodes + nodes + } } |