diff options
| author | Fuwn <[email protected]> | 2022-06-14 08:45:09 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-06-14 08:45:09 +0000 |
| commit | b5703a59c59bfe95143cf8dc6cc54f20f67fb4fd (patch) | |
| tree | 44022e3944119c39df5b9082aaac3a77841c8c10 /src/ast.rs | |
| parent | fix(ast): list ast construction (diff) | |
| download | germ-b5703a59c59bfe95143cf8dc6cc54f20f67fb4fd.tar.xz germ-b5703a59c59bfe95143cf8dc6cc54f20f67fb4fd.zip | |
feat(macros): general utility macros
Diffstat (limited to 'src/ast.rs')
| -rw-r--r-- | src/ast.rs | 446 |
1 files changed, 0 insertions, 446 deletions
diff --git a/src/ast.rs b/src/ast.rs deleted file mode 100644 index eae0683..0000000 --- a/src/ast.rs +++ /dev/null @@ -1,446 +0,0 @@ -// This file is part of Germ <https://github.com/gemrest/germ>. -// Copyright (C) 2022-2022 Fuwn <[email protected]> -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 3. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. -// -// Copyright (C) 2022-2022 Fuwn <[email protected]> -// SPDX-License-Identifier: GPL-3.0-only - -//! Build AST trees from Gemtext - -/// A Gemtext AST node. -/// -/// Each Gemtext line is a `Node`, and some lines can even be grouped together, -/// such as the `Node::List` `Node`! -/// -/// # Gemtext Resources -/// -/// - [Gemtext Documentation](https://gemini.circumlunar.space/docs/gemtext.gmi) -/// - [Gemtext Cheatsheet](https://gemini.circumlunar.space/docs/cheatsheet.gmi). -/// - [Gemini Specification](https://gemini.circumlunar.space/docs/specification.gmi). -#[derive(Debug, PartialEq)] -pub enum Node { - /// A text line - /// - /// # Example - /// - /// ```gemini - /// This is a text line - /// ``` - Text(String), - /// A link line - /// - /// # Examples - /// - /// ```gemini - /// => /this-is-the-to This is the text - /// - /// => gemini://to.somewhere.link - /// ``` - Link { - /// The location that a link line is pointing to - /// - /// # Examples - /// - /// ```gemini - /// => /this-is-the-to This is the text - /// - /// => gemini://to.somewhere.link - /// ``` - to: String, - /// The text a link line *may* have - /// - /// # Examples - /// - /// ```gemini - /// => /this-is-the-to This line has text, unlike the next one. - /// - /// => gemini://to.somewhere.link - /// ``` - text: Option<String>, - }, - /// A heading line - /// - /// # Examples - /// - /// ```gemini - /// # This is a heading - /// - /// ## This is a sub-heading - /// - /// ### This is a sub-sub-heading - /// ``` - Heading { - /// The level of a heading - /// - /// # Examples - /// - /// ```gemini - /// # This is a level 1 heading - /// - /// ## This is a level 2 sub-heading - /// - /// ### This is a level 3 sub-sub-heading - /// ``` - level: usize, - /// The text of a heading - /// - /// # Examples - /// - /// ```gemini - /// # This is the headings text - /// - /// # This is also the headings text - /// ``` - text: String, - }, - /// A collection of sequential list item lines - /// - /// # Examples - /// - /// ```gemini - /// * These are - /// * sequential list - /// * items. - /// ``` - List(Vec<String>), - /// A blockquote line - /// - /// # Examples - /// - /// ```gemini - /// > This is a blockquote line - /// - /// > This is also a blockquote line - /// ``` - Blockquote(String), - /// A preformatted block - /// - /// # Examples - /// - /// Try to ignore the leading backslash in-front of the triple backticks, - /// they are there to not confuse the Markdown engine. - /// - /// ```gemini - /// \```This is the alt-text - /// This is the preformatted block - /// - /// This is the rest of the preformatted block - /// \``` - /// ``` - PreformattedText { - /// A preformatted blocks alt-text - /// - /// # Examples - /// - /// Try to ignore the leading backslash in-front of the triple backticks, - /// they are there to not confuse the Markdown engine. - /// - /// ```gemini - /// \```This is the alt-text - /// This is the preformatted block - /// - /// This is the rest of the preformatted block - /// \``` - /// ``` - alt_text: Option<String>, - /// A preformatted blocks content - /// - /// # Examples - /// - /// Try to ignore the leading backslash in-front of the triple backticks, - /// they are there to not confuse the Markdown engine. - /// - /// ```gemini - /// \```This is the alt-text - /// This is the preformatted blocks content - /// - /// This is the rest of the preformatted blocks content - /// \``` - /// ``` - text: String, - }, - /// A whitespace line, a line which contains nothing but whitespace. - Whitespace, -} - -/// An AST structure which contains an AST tree -/// -/// # Example -/// -/// ```rust -/// let _ = germ::ast::Ast::from_string(r#"=> gemini://gem.rest/ GemRest"#); -/// ``` -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 Self::evaluate( - line, - &mut lines, - &mut in_preformatted, - &mut in_list, - )); - } - - Self { - inner: ast - } - } - - #[must_use] - pub fn to_gemtext(&self) -> String { - let mut gemtext = "".to_string(); - - for node in &self.inner { - match node { - Node::Text(text) => gemtext.push_str(&format!("{}\n", text)), - Node::Link { - to, - text, - } => - gemtext.push_str(&format!( - "=> {}{}\n", - to, - text - .clone() - .map_or_else(|| "".to_string(), |text| format!(" {}", text)), - )), - Node::Heading { - level, - text, - } => - gemtext.push_str(&format!( - "{} {}\n", - match level { - 1 => "#", - 2 => "##", - 3 => "###", - _ => "", - }, - text - )), - Node::List(items) => - gemtext.push_str(&format!( - "{}\n", - items - .iter() - .map(|i| format!("* {}", i)) - .collect::<Vec<String>>() - .join("\n"), - )), - Node::Blockquote(text) => gemtext.push_str(&format!("> {}\n", text)), - Node::PreformattedText { - alt_text, - text, - } => - gemtext.push_str(&format!( - "```{}\n{}```\n", - alt_text.clone().unwrap_or_else(|| "".to_string()), - text - )), - Node::Whitespace => gemtext.push('\n'), - } - } - - gemtext - } - - /// 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 } - - #[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![]; - - // 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(); - - nodes.push(Node::Link { - to: split.next().expect("no location in link"), - text: { - let rest = split.collect::<Vec<String>>().join(" "); - - if rest.is_empty() { - None - } else { - Some(rest) - } - }, - }); - - 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, - }; - - 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; - } - - list_items.push(line.get(1..).unwrap_or("").trim_start().to_string()); - - if let Some(next_line) = lines.next() { - line = next_line; - } else { - break; - } - } - ">" => { - // 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 a backtick, it is a list item, so - // let's enter a preformatted text context. - *in_preformatted = !*in_preformatted; - - 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, - }); - - 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; - } - } - } - } - - if !list_items.is_empty() { - nodes.reverse(); - nodes.push(Node::List(list_items)); - nodes.reverse(); - } - - nodes - } -} |