diff options
| author | Fuwn <[email protected]> | 2022-06-02 04:04:27 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-06-02 04:04:27 +0000 |
| commit | 96c139a26a94cf0fd4d531df3c07156b3b531568 (patch) | |
| tree | ed2f288d32a8a69947f3a67bfa437021015e5f05 | |
| parent | docs(cargo): bump version to 0.2.3 (diff) | |
| download | germ-96c139a26a94cf0fd4d531df3c07156b3b531568.tar.xz germ-96c139a26a94cf0fd4d531df3c07156b3b531568.zip | |
feat(germ): ast struct
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | examples/ast.rs | 2 | ||||
| -rw-r--r-- | src/ast.rs | 321 | ||||
| -rw-r--r-- | src/convert.rs | 16 | ||||
| -rw-r--r-- | src/convert/html.rs | 6 | ||||
| -rw-r--r-- | src/convert/markdown.rs | 8 | ||||
| -rw-r--r-- | src/request/status.rs | 3 | ||||
| -rw-r--r-- | tests/ast.rs | 12 |
8 files changed, 197 insertions, 175 deletions
@@ -21,6 +21,8 @@ request = ["rustls", "url", "anyhow"] meta = [] [dependencies] -rustls = { version = "0.20.5", features = ["dangerous_configuration"], optional = true } # TLS +rustls = { version = "0.20.5", features = [ + "dangerous_configuration" +], optional = true } # TLS url = { version = "2.2.2", optional = true } # URL Validation anyhow = { version = "1.0.57", optional = true } # `Result` diff --git a/examples/ast.rs b/examples/ast.rs index cbc97d8..884bd91 100644 --- a/examples/ast.rs +++ b/examples/ast.rs @@ -46,7 +46,7 @@ This is more text after a blank line. That was a link without text."#; fn main() { - for node in germ::ast::build(EXAMPLE_GEMTEXT) { + for node in germ::ast::Ast::from_string(EXAMPLE_GEMTEXT).inner() { println!("{:?}", node); } } @@ -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 + } } diff --git a/src/convert.rs b/src/convert.rs index d46b854..c68696e 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -18,6 +18,8 @@ //! Convert Gemtext into many types of markup. +use crate::ast::Ast; + mod html; mod markdown; @@ -36,16 +38,16 @@ pub enum Target { /// ```rust /// use germ::convert; /// -/// convert::from_ast( -/// germ::ast::build(r#"=> gemini://gem.rest/ GemRest"#), +/// let _ = convert::from_ast( +/// &germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#), /// &convert::Target::HTML, /// ); /// ``` #[must_use] -pub fn from_ast(source: Vec<crate::ast::Node>, target: &Target) -> String { +pub fn from_ast(source: &Ast, target: &Target) -> String { match target { - Target::Markdown => markdown::convert(source), - Target::HTML => html::convert(source), + Target::Markdown => markdown::convert(source.inner()), + Target::HTML => html::convert(source.inner()), } } @@ -56,12 +58,12 @@ pub fn from_ast(source: Vec<crate::ast::Node>, target: &Target) -> String { /// ```rust /// use germ::convert; /// -/// convert::from_string( +/// let _ = convert::from_string( /// r#"=> gemini://gem.rest/ GemRest"#, /// &convert::Target::HTML, /// ); /// ``` #[must_use] pub fn from_string(source: &str, target: &Target) -> String { - from_ast(crate::ast::build(source), target) + from_ast(&Ast::from_string(source), target) } diff --git a/src/convert/html.rs b/src/convert/html.rs index 2f031b0..7b1cafe 100644 --- a/src/convert/html.rs +++ b/src/convert/html.rs @@ -18,7 +18,7 @@ use crate::ast::Node; -pub fn convert(source: Vec<Node>) -> String { +pub fn convert(source: &[Node]) -> String { let mut html = String::new(); // Since we have an AST tree of the Gemtext, it is very easy to convert from @@ -33,7 +33,7 @@ pub fn convert(source: Vec<Node>) -> String { html.push_str(&format!( "<a href=\"{}\">{}</a><br>", to, - text.unwrap_or_else(|| to.clone()) + text.clone().unwrap_or_else(|| to.clone()) )); } Node::Heading { @@ -55,7 +55,7 @@ pub fn convert(source: Vec<Node>) -> String { html.push_str(&format!( "<ul>{}</ul>", items - .into_iter() + .iter() .map(|i| format!("<li>{}</li>", i)) .collect::<Vec<String>>() .join("\n") diff --git a/src/convert/markdown.rs b/src/convert/markdown.rs index b400002..c368973 100644 --- a/src/convert/markdown.rs +++ b/src/convert/markdown.rs @@ -18,7 +18,7 @@ use crate::ast::Node; -pub fn convert(source: Vec<Node>) -> String { +pub fn convert(source: &[Node]) -> String { let mut markdown = String::new(); // Since we have an AST tree of the Gemtext, it is very easy to convert from @@ -30,7 +30,7 @@ pub fn convert(source: Vec<Node>) -> String { to, text, } => - markdown.push_str(&*text.map_or_else( + markdown.push_str(&*text.clone().map_or_else( || format!("<{}>\n", to), |text| format!("[{}]({})\n", text, to), )), @@ -53,7 +53,7 @@ pub fn convert(source: Vec<Node>) -> String { markdown.push_str(&format!( "{}\n", items - .into_iter() + .iter() .map(|i| format!("- {}", i)) .collect::<Vec<String>>() .join("\n"), @@ -65,7 +65,7 @@ pub fn convert(source: Vec<Node>) -> String { } => { markdown.push_str(&format!( "```{}\n{}```\n", - alt_text.unwrap_or_else(|| "".to_string()), + alt_text.clone().unwrap_or_else(|| "".to_string()), text )); } diff --git a/src/request/status.rs b/src/request/status.rs index c8c3ae4..f46059a 100644 --- a/src/request/status.rs +++ b/src/request/status.rs @@ -16,8 +16,7 @@ // Copyright (C) 2022-2022 Fuwn <[email protected]> // SPDX-License-Identifier: GPL-3.0-only -use std::fmt; -use std::fmt::Formatter; +use std::{fmt, fmt::Formatter}; /// Simple Gemini status reporting /// diff --git a/tests/ast.rs b/tests/ast.rs index 2fbc1d3..8b3882d 100644 --- a/tests/ast.rs +++ b/tests/ast.rs @@ -18,12 +18,12 @@ #[cfg(test)] mod test { - use germ::ast::{build, Node}; + use germ::ast::{Ast, Node}; #[test] fn build_multi_line_list_with_text() { assert_eq!( - build("* item1\n* 2\nhi text"), + *Ast::from_string("* item1\n* 2\nhi text").inner(), vec![ Node::List(vec!["item1".to_string(), "2".to_string()]), Node::Text("hi text".to_string()), @@ -34,7 +34,7 @@ mod test { #[test] fn build_multi_line_vec() { assert_eq!( - build("=> /test hi\nhi there\n> hi"), + *Ast::from_string("=> /test hi\nhi there\n> hi").inner(), vec![ Node::Link { to: "/test".to_string(), @@ -49,8 +49,8 @@ mod test { #[test] fn build_single_0th_from_vec() { assert_eq!( - build("=> /test hi"), - vec![Node::Link { + Ast::from_string("=> /test hi").inner(), + &vec![Node::Link { to: "/test".to_string(), text: Some("hi".to_string()), }], @@ -60,7 +60,7 @@ mod test { #[test] fn build_single_element() { assert_eq!( - build("=> /test hi").get(0).unwrap(), + Ast::from_string("=> /test hi").inner().get(0).unwrap(), &Node::Link { to: "/test".to_string(), text: Some("hi".to_string()), |