From 3a026a7f9271bddebca896d13396c7db5511824f Mon Sep 17 00:00:00 2001 From: Max Value Date: Wed, 7 May 2025 18:52:13 +0100 Subject: [PATCH] See last commit --- Cargo.toml | 2 + schema.gn | 100 +++++++++++++++++----- src/inline.rs | 2 +- src/lib.rs | 17 +++- src/parser.rs | 116 +++++++++++++++++++------- src/types.rs | 4 +- src/types/blockquote.rs | 31 +++++++ src/types/contents.rs | 25 ++++++ src/types/document.rs | 49 +++++++---- src/types/heading.rs | 22 +++-- src/types/list.rs | 10 ++- src/types/metadata.rs | 45 ++++++---- src/types/paragraph.rs | 6 +- src/types/preformatted.rs | 21 +++++ src/types/reference_list.rs | 100 +++++++++++++++++++++- src/types/reference_list/name_list.rs | 6 ++ src/types/reference_list/reference.rs | 18 +++- src/types/reference_list/work.rs | 11 +++ 18 files changed, 472 insertions(+), 113 deletions(-) create mode 100644 src/types/blockquote.rs create mode 100644 src/types/contents.rs create mode 100644 src/types/preformatted.rs diff --git a/Cargo.toml b/Cargo.toml index 710695a..8ea5d8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "gn-parser" version = "0.1.0" +authors = ["William Greenwood "] +description = "Goodnight Markdown parser. A markdown format for refereced works." edition = "2021" [dependencies] diff --git a/schema.gn b/schema.gn index 6128703..8937f37 100644 --- a/schema.gn +++ b/schema.gn @@ -1,38 +1,71 @@ --- title: The Goodnight Markdown Specification - Version 0.1.0 author: Goodnight Publishing +style: default --- [META] [TOC] -[REFS] +# Features + +Along with the features below, some features where removed: -## Features - -- Headings are unchanged from vanilla markdown (6 levels). -- Paragraphs are unchanged from vanilla markdown. -- URLs and email addresses are unchanged from vanilla markdown. -- Images are unchanged from vanilla markdown. -- Inline code with the backtick character is unchanged from vanilla markdown. -- Escaping via the backslash character is unchanged from vanilla markdown. -- Links are unchanged from vanilla markdown but are all reference style. -- Block quotes are indented. -- Code blocks are only used with 3 backticks before and after. - Line breaks have been removed. - Horizontal rules have been removed. + +I don't belive in line breaks and horizontal rules are the responsibility of the stylesheet. + +## Headings + +Headings are prefixed with the `#` hastag character as with standard markdown. There must not be a space before the `#` character. A space may or may not be put after the `#`. Inline formatting will be properly parsed. Inline references will be properly parsed. + +## Links + +Still to be implemented + +## Email addresses + +Still to be implemented + +## Paragraphs + +`text about paragraphs` + +## Images + +Still to be implemented + +## Inline code + +Inline code blocks can be used with the `\`` backtick character. + +## Code blocks + +Code blocks can be used with 3 backtick characters: `\`\`\``. This sequence must start on the first character on a new line. + +## Block quotes + +Block quotes are started with the tab character `\t`. They can contain any other elements. + + Here is an example of a blockquote. + + - Here is a list element in a blockquote + + Tabbed lines with a space between them are joined into one single blockquote. If two different blockquotes are needed next to each other, a backslash at the first character of a newline can split the two up. + +## Inline formatting + - Bold styling will work only with a single asterisk character. - Italic styling will work only with a single underscore character. + +## Lists + - Unordered lists will work only with the hyphen character. - Ordered lists will work only with the period character. -. Test -. Of -. Ordered -. List - -### Macros +## Macros To generate the title, table of contents and bibliography, the following macros can be used: @@ -40,7 +73,7 @@ To generate the title, table of contents and bibliography, the following macros - `[TOC]` generates the table of contents - `[META]` generates the bibliography -### Metadata +## Metadata The metadata format used is the YAML format. To generate the title, the standard LaTeX keys of "title", "author" and "date", should be used. If "style" is used, the parser will introduce the stylesheet with the relevant extension. For example, `style: stylesheet` will introduce `stylesheet.css` for html and `stylesheet.sty` for LaTeX. @@ -51,16 +84,39 @@ author: William Greenwood --- ``` -### Referencing +## Referencing -All referencing in Goodnight is APA 7th. It can be done similar to referenced links with a syntax similar to YAML metadata: +All referencing in Goodnight is APA 7th [lindsey2015]. It can be done similar to referenced links with a syntax similar to YAML metadata: ``` "Placeholder quote" [1] -[1] +\[1] title: Placeholder title author: Placeholder name ``` If one of the keys is placed in square brackets after the in-text reference, it will be printed. If only the reference id is used, the full in-text reference will be used. If something that is not one of the keys is placed in square brackets within the full refrence, for example a page number. + +The keys are as follows: + +. `author`. The authors or entities behind the work seperated by the word "and". Wrap entity names in curly brackets +. `title` +. `year` (optional) +. `publisher` (optional) +. `url` (optional) +. `journaltitle`: Title of the book or journal the work is in (optional) +. `issuetitle`: Issue title of the book or journal the work is in (optional, must be used with `journaltitle`) +. `editor`: Author of the book or journal the work is in (optional, must be used with `journaltitle`) +. `volume`: Volume of the book or journal the work is in (optional, must be used with `journaltitle`) + +[REFS] + +[lindsey2015] +author: Marley-Vincent Lindsey +editor: William Value +title: The Politics of Pokémon. Socialized Gaming, Religious Themes and the Construction of Communal Narratives +journaltitle: Heidelberg Journal of Religions on the Internet +issuetitle: Religion in Digital Games Reloaded. Immersion into the Field +year: 2015 +volume: 7th diff --git a/src/inline.rs b/src/inline.rs index 5d2873c..d1ec55d 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -14,7 +14,7 @@ fn parse_inline (string: &str, symbol: char, front: &str, back: &str) -> String pub fn parse_inline_latex (string: &str) -> String { let mut text = parse_inline(string, '*', "\\textbf{", "}"); text = parse_inline(&text, '_', "\\textit{", "}"); - parse_inline(&text, '`', "\\texttt{", "}") + parse_inline(&text, '`', "\\texttt{", "}").replace("#", "\\#") } pub fn parse_inline_html (string: &str) -> String { diff --git a/src/lib.rs b/src/lib.rs index e9cb1f0..e1b4ed5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,18 +10,20 @@ use crate::types::Renderable; pub struct Config { - file_path: String + file_path: String, + format: String } impl Config { pub fn build(args: &[String]) -> Result { - if args.len() < 2 { + if args.len() < 3 { return Err("not enough arguments"); } let file_path = args[1].clone(); + let format = args[2].clone(); - Ok(Config { file_path }) + Ok(Config { file_path, format }) } } @@ -30,7 +32,14 @@ pub fn run(config: Config) -> Result<(), Box> { .expect("Should have been able to read the file"); let document = Document::from(contents); - println!("{}", document.render_html()); + let format = config.format; + println!("{}", match format.as_str() { + "html" => {document.render_html()}, + "latex" => {document.render_latex()}, + "gemtext" => {document.render_gemtext()}, + "metadata" => {document.render_metadata()}, + _ => {String::from("?")} + }); Ok(()) } diff --git a/src/parser.rs b/src/parser.rs index 2944eb5..f253e79 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5,6 +5,9 @@ use crate::types::heading::Heading; use crate::types::paragraph::Paragraph; use crate::types::metadata::Metadata; use crate::types::reference_list::ReferenceList; +use crate::types::preformatted::Preformatted; +use crate::types::blockquote::BlockQuote; +use crate::types::contents::Contents; use crate::types::list::{Item, List}; // this gets the full text of the file from the document class (with the reference list and metadata removed) @@ -12,45 +15,58 @@ use crate::types::list::{Item, List}; pub fn parse_body (string: String, metadata: Metadata, reference_list: Option) -> Vec> { let mut document: Vec> = Vec::new(); - let mut clean_string = string.to_string(); - let mut metadata: Option = Some(metadata); - let mut reference_list = reference_list; + let metadata = metadata; + let reference_list = reference_list; - // bellow matches and cleans up all line breaks in the middle of paragraphs - let re = Regex::new(r"[\w\d][ \t]*\n[\w&&\D]").unwrap(); - for mat in re.find_iter(&string).collect::>().iter().rev() { - clean_string.replace_range(mat.start()+1 .. mat.end()-1, ""); - } - - let mut lines = clean_string.lines().peekable(); + let mut lines = string.lines().peekable(); while let Some(line) = lines.next() { if !(line == "") { let first = &(String::from(line) + " ")[0..7].chars().collect::>(); match (first[0], first[1], first[2], first[3], first[4], first[5]) { + + // match all code blocks + ('`', '`', '`', ..) => { + let mut text = String::from(&line[3..]); + while match lines.peek() { + Some(string) => { + if string.starts_with("```") {false} + else { + true + } + }, + None => false + } { + text += lines.next().unwrap(); + text += "\n"; + } + document.push(Box::new(Preformatted{text})); + lines.next(); // consume the final ``` + } + // match all heading patterns ('#', '#', '#', '#', '#', '#') => { - document.push(Box::new(Heading{text: line[6..].to_string(), level: 6})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[6..].to_string()), level: 6})) }, ('#', '#', '#', '#', '#', ..) => { - document.push(Box::new(Heading{text: line[5..].to_string(), level: 5})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[5..].to_string()), level: 5})) }, ('#', '#', '#', '#', ..) => { - document.push(Box::new(Heading{text: line[4..].to_string(), level: 4})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[4..].to_string()), level: 4})) }, ('#', '#', '#', ..) => { - document.push(Box::new(Heading{text: line[3..].to_string(), level: 3})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[3..].to_string()), level: 3})) }, ('#', '#', ..) => { - document.push(Box::new(Heading{text: line[2..].to_string(), level: 2})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[2..].to_string()), level: 2})) }, ('#', ..) => { - document.push(Box::new(Heading{text: line[1..].to_string(), level: 1})) + document.push(Box::new(Heading{text: parse_reference(&reference_list, line[1..].to_string()), level: 1})) }, // match all list patterns ('-', ..) | ('.', ..) => { let matching = &line[..1]; - let mut items: Vec = vec![Item{text: line[1..].to_string()}]; + let mut items: Vec = vec![Item{text: parse_reference(&reference_list, line[1..].to_string())}]; while match lines.peek() { Some(string) => { if string == &"" {false} @@ -60,36 +76,66 @@ pub fn parse_body (string: String, metadata: Metadata, reference_list: Option false } { - items.push(Item{text: lines.next().unwrap()[1..].to_string()}); + items.push(Item{text: parse_reference(&reference_list, lines.next().unwrap()[1..].to_string())}); } document.push(Box::new(List{items, ordered: matching == "."})) } // match all insert sequences like the title ('[', 'R', 'E', 'F', 'S', ']') => { - match reference_list { + match reference_list.clone() { Some(bibliography) => { document.push(Box::new(bibliography)); - reference_list = None; }, None => () }}, - ('[', 'M', 'E', 'T', 'A', ']') => { - match metadata { - Some(title) => { - document.push(Box::new(title)); - metadata = None; - }, - None => () - } - }, + ('[', 'M', 'E', 'T', 'A', ']') => {document.push(Box::new(metadata.clone()))}, ('[', 'T', 'O', 'C', ']', ..) => { - document.push(Box::new(Paragraph{text: String::from("This is where the table of contents will go")})) + document.push(Box::new(Contents{elements: None})) }, + // match all block quotes + ('\t', ..) => { + let mut text = String::from(&line[1..]); + while match lines.peek() { + Some(string) => { + if string.is_empty() { + text += "\n"; + text += &lines.next().unwrap(); + true + } + else if &string[0..1] == "\t" { + text += "\n"; + text += &lines.next().unwrap()[1..]; + true + } + else {false} + }, + None => false + } {} + document.push( + Box::new(BlockQuote{elements: parse_body(text, metadata.clone(), reference_list.clone())})) + } + // make everything else a paragraph _ => { - document.push(Box::new(Paragraph{text: line.to_string()})) + let mut text = String::from(line); + while match lines.peek() { + Some(string) => { + // regex could use some work + // will fail if there is a single character on the next line down + let re = Regex::new(r"[[\w\W]&&[^-`\.#\n]][^\.]+").unwrap(); + re.is_match_at(&string, 0) + }, + None => false + } { + text += " "; + text += lines.next().unwrap(); + } + document.push( + Box::new(Paragraph{ + text: parse_reference(&reference_list, text.to_string()) + })) } } } @@ -97,3 +143,11 @@ pub fn parse_body (string: String, metadata: Metadata, reference_list: Option, text: String) -> String { + match reference_list.clone() { + Some(bibliography) => { + bibliography.parse_inline(text)}, + None => text + } +} diff --git a/src/types.rs b/src/types.rs index 8701853..4c2bcca 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,6 +4,9 @@ pub mod metadata; pub mod reference_list; pub mod document; pub mod list; +pub mod preformatted; +pub mod blockquote; +pub mod contents; pub trait Renderable { fn render_latex(&self) -> String; @@ -11,4 +14,3 @@ pub trait Renderable { fn render_gemtext(&self) -> String; } - diff --git a/src/types/blockquote.rs b/src/types/blockquote.rs new file mode 100644 index 0000000..1d94fdd --- /dev/null +++ b/src/types/blockquote.rs @@ -0,0 +1,31 @@ +use crate::types::Renderable; + +pub struct BlockQuote { + pub elements: Vec> +} + +impl Renderable for BlockQuote { + fn render_latex(&self) -> String { + let mut full_text = String::from("\\begin{quote}\n"); + for element in &self.elements { + full_text.push_str(&element.render_latex()); + } + full_text + "\\end{quote}\n\n" + } + + fn render_html(&self) -> String { + let mut full_text = String::from("
"); + for element in &self.elements { + full_text.push_str(&element.render_html()); + } + full_text + "
" + } + + fn render_gemtext(&self) -> String { + let mut full_text = String::new(); + for element in &self.elements { + full_text.push_str(&(String::from("\t") + &element.render_gemtext())); + } + full_text + } +} diff --git a/src/types/contents.rs b/src/types/contents.rs new file mode 100644 index 0000000..2b3d6b1 --- /dev/null +++ b/src/types/contents.rs @@ -0,0 +1,25 @@ +use crate::types::{heading::Heading, Renderable}; + +pub struct Contents { + pub elements: Option> +} + +impl Renderable for Contents { + fn render_latex(&self) -> String { + String::from("\\tableofcontents\n\n") + } + + fn render_html(&self) -> String { + String::from("\ +

Table of Contents

+

This is where the table of contents will go

+ ") + } + + fn render_gemtext(&self) -> String { + String::from("\ +## Table of Contents + +This is where the Table of Contents will go.\n\n") + } +} diff --git a/src/types/document.rs b/src/types/document.rs index 90ab2cb..25b7e65 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -1,32 +1,37 @@ use crate::types::metadata::Metadata; use crate::types::reference_list::ReferenceList; use crate::types::Renderable; -use crate::parser; +use crate::parser::parse_body; + pub struct Document { body: Vec>, - style: Option + metadata: Metadata } impl Document { pub fn from(string: String) -> Self { let (metadata, string) = Metadata::from(string); let (reference_list, string) = ReferenceList::from(string); - Document { body: parser::parse_body(string, metadata, reference_list), style: None} + + let body = parse_body(string, metadata.clone(), reference_list); + let document = Document { body, metadata}; + + document } } impl Renderable for Document { fn render_latex(&self) -> String { - let mut full_text = String::from( - "\\documentclass{article} - \\usepackage{hyperref}" - ); - match &self.style { - Some(style) => {full_text.push_str(&format!("\\usepackage{{{style}}}"));} + let mut full_text = String::from("\ +\\documentclass{article} +\\usepackage{hyperref} + "); + match &self.metadata.style { + Some(style) => {full_text.push_str(&format!("\\usepackage{{{style}}}\n"));} None => () } - full_text.push_str("\\begin{document}"); + full_text.push_str("\\begin{document}\n\n"); for element in &self.body { full_text.push_str(&element.render_latex()); } @@ -34,16 +39,16 @@ impl Renderable for Document { } fn render_html(&self) -> String { - let mut full_text = String::from(""); - match &self.style { - Some(style) => {full_text.push_str(&format!(""));} + let mut full_text = String::from("\n\n"); + match &self.metadata.style { + Some(style) => {full_text.push_str(&format!("\n"));} None => () } - full_text.push_str(""); + full_text.push_str("\n\n
\n"); for element in &self.body { full_text.push_str(&element.render_html()); } - full_text + "" + full_text + "
\n\n\n" } fn render_gemtext(&self) -> String { @@ -54,3 +59,17 @@ impl Renderable for Document { full_text } } + +impl Document{ + pub fn render_metadata(&self) -> String { + let mut data = self.metadata.keys.clone(); + + data.insert(String::from("title"), self.metadata.title.to_string()); + data.insert(String::from("author"), self.metadata.author.to_string()); + data.insert(String::from("date"), self.metadata.date.to_string()); + + data.insert(String::from("elements"), format!("{}", self.body.len())); + + format!("{data:?}") + } +} diff --git a/src/types/heading.rs b/src/types/heading.rs index 91a88ac..a98afb7 100644 --- a/src/types/heading.rs +++ b/src/types/heading.rs @@ -5,7 +5,7 @@ use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext} pub struct Heading { pub text: String, - pub level: u8 + pub level: usize } impl Renderable for Heading { @@ -18,17 +18,16 @@ impl Renderable for Heading { 4 => "paragraph", _ => "subparagraph" }; - format!("\\{section}{{{text}}}") + format!("\\{section}{{{text}}}\n\n") } fn render_html (&self) -> String { let text = parse_inline_html(self.text.trim()); let section = match self.level { - 1 => "h1", - 2 => "h2", - 3 => "h3", - 4 => "h4", - 5 => "h5", + 1 => "h2", + 2 => "h3", + 3 => "h4", + 4 => "h5", _ => "h6" }; format!("<{section}>{text}\n") @@ -37,11 +36,10 @@ impl Renderable for Heading { fn render_gemtext (&self) -> String { let text = parse_inline_gemtext(self.text.trim()); let section = match self.level { - 1 => "#", - 2 => "##", - _ => "###" + 1 => "##", + _ => "###", }; - format!("{section} {text}") + format!("{section} {text}\n\n") } } @@ -57,7 +55,7 @@ mod tests { #[test] fn test_html_parser() { let heading = Heading{ text: String::from("Goodnight"), level: 5}; - assert_eq!(&heading.render_html(), "
Goodnight
"); + assert_eq!(&heading.render_html(), "
Goodnight
\n"); } #[test] fn test_gemtext_parser() { diff --git a/src/types/list.rs b/src/types/list.rs index 2cc74b6..e06dbfb 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -8,12 +8,12 @@ pub struct List { impl Renderable for List { fn render_latex(&self) -> String { - let element = if self.ordered {"ennumerate"} else {"itemize"}; + let element = if self.ordered {"enumerate"} else {"itemize"}; let mut full_text = format!("\\begin{{{element}}}\n"); for item in &self.items { full_text.push_str(&item.render_latex()); } - full_text + "\\end{" + element + "}\n" + full_text + "\\end{" + element + "}\n\n" } fn render_html(&self) -> String { let element = if self.ordered {"ol"} else {"ul"}; @@ -28,7 +28,7 @@ impl Renderable for List { for item in &self.items { full_text.push_str(&item.render_gemtext()); } - full_text + full_text + "\n" } } @@ -38,11 +38,13 @@ pub struct Item { impl Renderable for Item { fn render_latex(&self) -> String { - format!("\\item {}\n", parse_inline_latex(self.text.trim())) + format!("\t\\item {}\n", parse_inline_latex(self.text.trim())) } + fn render_html(&self) -> String { format!("
  • {}
  • \n", parse_inline_html(self.text.trim())) } + fn render_gemtext(&self) -> String { format!("* {}\n", parse_inline_gemtext(self.text.trim())) } diff --git a/src/types/metadata.rs b/src/types/metadata.rs index 4a7ad48..27f2976 100644 --- a/src/types/metadata.rs +++ b/src/types/metadata.rs @@ -29,14 +29,16 @@ impl Metadata { let mut text = string.to_string(); let mut metadata = String::from(""); - // there can only be one metadata block at the moment due to how the block is removed - // from the wider string. This is not essecial but would be nice + // there can only be one metadata block at the moment // regex is a little tricky, means that you cant have more than one hypen next to another at once // but does allow hypens in metadata let re = Regex::new(r"-{3}(([\w\W&&[^-]]+-?[\w\W&&[^-]]+)*)-{3}").unwrap(); - for mat in re.find_iter(&string) { - metadata.push_str(mat.as_str()); - text.replace_range(mat.range(), ""); + match re.find(&string) { + Some(mat) => { + metadata.push_str(mat.as_str()); + text.replace_range(mat.range(), ""); + }, + None => () } let mut data = Metadata::new(); @@ -61,12 +63,15 @@ impl Metadata { impl Renderable for Metadata { fn render_latex (&self) -> String { - format!( - "\\begin{{centering}} - {{\\Large{{}}{title}}}\\\\ - {{\\large{{}}{author}}}\\\\ - {{\\large{{}}{date}}}\\\\ - \\end{{centering}}", + format!("\ +\\begin{{centering}} + {{\\Large{{}}\\textbf{{{title}}}}}\\\\ + \\vspace{{2mm}} + {{\\large{{}}{author}}}\\\\ + \\vspace{{2mm}} + {{\\large{{}}{date}}}\\\\ +\\end{{centering}}\n + ", title = self.title.trim(), author = self.author.trim(), date = self.date.trim() @@ -87,12 +92,24 @@ impl Renderable for Metadata { } fn render_gemtext (&self) -> String { - format!( - "# {title} - {author} - {date}", + format!("\ +# {title}\n +{author} - {date}\n\n", title = self.title.trim(), author = self.author.trim(), date = self.date.trim() ) } } + +impl Clone for Metadata { + fn clone(&self) -> Metadata { + Metadata { + title: self.title.clone(), + author: self.author.clone(), + date: self.date.clone(), + style: self.style.clone(), + keys: self.keys.clone() + } + } +} diff --git a/src/types/paragraph.rs b/src/types/paragraph.rs index 09ac2f1..9030448 100644 --- a/src/types/paragraph.rs +++ b/src/types/paragraph.rs @@ -10,7 +10,7 @@ pub struct Paragraph { impl Renderable for Paragraph { fn render_latex (&self) -> String { - parse_inline_latex(&self.text.trim()) + parse_inline_latex(&self.text.trim()) + "\n\n" } fn render_html (&self) -> String { @@ -18,7 +18,7 @@ impl Renderable for Paragraph { } fn render_gemtext (&self) -> String { - parse_inline_gemtext(&self.text.trim()) + parse_inline_gemtext(&self.text.trim()) + "\n\n" } } @@ -34,7 +34,7 @@ mod tests { #[test] fn test_html_parser() { let paragraph = Paragraph{ text: String::from("_Goodnight_")}; - assert_eq!(¶graph.render_html(), "Goodnight"); + assert_eq!(¶graph.render_html(), "

    Goodnight

    \n"); } #[test] fn test_gemtext_parser() { diff --git a/src/types/preformatted.rs b/src/types/preformatted.rs new file mode 100644 index 0000000..036ad2f --- /dev/null +++ b/src/types/preformatted.rs @@ -0,0 +1,21 @@ +use crate::types::Renderable; + +// submodule for prerendered text and code + +pub struct Preformatted { + pub text: String +} + +impl Renderable for Preformatted { + fn render_latex(&self) -> String { + String::from("\\begin{verbatim}\n") + &self.text.trim() + "\n\\end{verbatim}\n\n" + } + + fn render_html(&self) -> String { + String::from("
    ") + &self.text.trim() + "
    \n" + } + + fn render_gemtext(&self) -> String { + String::from("```\n") + &self.text.trim() + "\n```\n\n" + } +} diff --git a/src/types/reference_list.rs b/src/types/reference_list.rs index 74aad5d..9fa6ef1 100644 --- a/src/types/reference_list.rs +++ b/src/types/reference_list.rs @@ -1,4 +1,9 @@ +use regex::{Match, Regex}; + use crate::types::Renderable; +use crate::types::reference_list::reference::Reference; +use crate::types::reference_list::name_list::NameList; +use crate::types::reference_list::work::Work; pub mod name_list; pub mod reference; @@ -10,27 +15,106 @@ pub struct ReferenceList { impl ReferenceList { pub fn from (string: String) -> (Option, String) { - (None, string) + let mut list: Vec = Vec::new(); + + // this might be too big honestly, its a pretty simple regex tho + let re = Regex::new(r"\[(?[\d\w]+)\]\n(author:((?[^\n]+\n?))|(title:(?[^\n]+\n?))|(year:(?<year>[^\n]+\n?))|(publisher:(?<publisher>[^\n]+\n?))|(url:(?<url>[^\n]+\n?))|(journaltitle:(?<journaltitle>[^\n]+\n?))|(issuetitle:(?<issuetitle>[^\n]+\n?))|(editor:(?<editor>[^\n]+\n?))|(volume:(?<volume>[^\n]+\n?)))+").unwrap(); + + for capture in re.captures_iter(&string) { + if match capture.name("id") { + Some(mat) => { + if &string[mat.start()-2..mat.start()-1] == "\\" {false} + else {true} + }, + None => false + } { + let new_reference = Reference { + id: capture.name("id").map_or( + String::from("none"), + |m| m.as_str().trim().to_string() + ), + names: NameList { + string: capture.name("author").map_or( + String::from("None"), + |m| m.as_str().trim().to_string() + ) + }, + title: capture.name("title").map_or( + String::from("None"), + |m| m.as_str().trim().to_string() + ), + year: match_string(capture.name("year")), + publisher: match_string(capture.name("publisher")), + url: match_string(capture.name("url")), + inside: match capture.name("journaltitle") { + Some(mat) => { + Some( + Work { + title: mat.as_str().trim().to_string(), + names: capture.name("editor").map_or(None, |m| Some(NameList { string: m.as_str().trim().to_string()})), + issue: match_string(capture.name("issuetitle")), + volume: match_string(capture.name("volume")) + } + ) + } + None => None + } + }; + list.push(new_reference); + } + } + + // order the list into alphabetical order + list.sort_by(|a, b| a.names.render_full().cmp(&b.names.render_full())); + + // remove references from full string + let mut new_string = string.to_string(); + for mat in re.find_iter(&string).collect::<Vec<_>>().iter().rev() { + if &string[mat.start()-1..mat.start()] != "\\" { + new_string.replace_range(mat.range(), ""); + } + } + + (match list.len() { + 0 => None, + _ => Some(ReferenceList{ list }) + }, new_string) + } + + pub fn parse_inline(&self, string: String) -> String { + let mut new_string = string.to_string(); + let re = Regex::new(r"\[[\d\w]+\]").unwrap(); + for mat in re.find_iter(&string).collect::<Vec<_>>().iter().rev() { + if !(&string[mat.start()-1..mat.start()] == "\\") { + let inline_id = &string[mat.start()+1..mat.end()-1].trim(); + for reference in &self.list { + if &reference.id == inline_id { + new_string.replace_range(mat.range(), &reference.render_inline()); + } + } + } + } + new_string } } impl Renderable for ReferenceList { fn render_latex(&self) -> String { - let mut full_text = String::from("\\section{Bibliography}\n\n"); + let mut full_text = String::from("\\section*{Bibliography}\n\n"); for reference in &self.list { full_text.push_str(&(reference.render_latex() + "\n\n")); } full_text } fn render_html(&self) -> String { - let mut full_text = String::from("<section><h1>Bibliography</h1>"); + let mut full_text = String::from("<section><h2>Bibliography</h2>"); for reference in &self.list { full_text.push_str(&(reference.render_html() + "\n\n")); } full_text + "</section>" } fn render_gemtext(&self) -> String { - let mut full_text = String::from("# Bibliography\n\n"); + let mut full_text = String::from("## Bibliography\n\n"); for reference in &self.list { full_text.push_str(&(reference.render_gemtext() + "\n\n")); } @@ -38,4 +122,12 @@ impl Renderable for ReferenceList { } } +impl Clone for ReferenceList { + fn clone(&self) -> Self { + ReferenceList { list: self.list.clone() } + } +} +fn match_string(option_match: Option<Match>) -> Option<String> { + option_match.map_or(None, |m| Some(m.as_str().trim().to_string())) +} diff --git a/src/types/reference_list/name_list.rs b/src/types/reference_list/name_list.rs index b8ac22d..119c4d8 100644 --- a/src/types/reference_list/name_list.rs +++ b/src/types/reference_list/name_list.rs @@ -8,6 +8,12 @@ pub struct NameList { // the inline and full rendering use different ways of storing stings for some reason // look into this maybe +impl Clone for NameList { + fn clone(&self) -> Self { + NameList { string: self.string.clone() } + } +} + impl NameList { pub fn render_full(&self) -> String { let mut names: Vec<String> = Vec::new(); diff --git a/src/types/reference_list/reference.rs b/src/types/reference_list/reference.rs index ea8e44e..7b4e59d 100644 --- a/src/types/reference_list/reference.rs +++ b/src/types/reference_list/reference.rs @@ -3,6 +3,7 @@ use crate::types::reference_list::work::Work; use crate::types::Renderable; pub struct Reference { + pub id: String, pub names: NameList, pub title: String, pub year: Option<String>, @@ -42,7 +43,7 @@ impl Renderable for Reference { } fn render_html(&self) -> String { - let mut full_text = String::from("<cite>") + &self.names.render_full(); + let mut full_text = String::from("<p>") + &self.names.render_full(); match &self.year { Some(year) => {full_text.push_str(&format!(" ({year})."));}, None => {full_text.push_str(" (n.d.).");} @@ -67,7 +68,7 @@ impl Renderable for Reference { }, None => () }; - full_text.push_str("</cite>"); + full_text.push_str("</p>"); full_text } @@ -103,6 +104,19 @@ impl Renderable for Reference { } } +impl Clone for Reference { + fn clone(&self) -> Self { + Reference { + id: self.id.clone(), + names: self.names.clone(), + title: self.title.clone(), + year: self.year.clone(), + publisher: self.publisher.clone(), + url: self.url.clone(), + inside: self.inside.clone()} + } +} + impl Reference { pub fn render_inline(&self) -> String { let mut full_text = String::from("(") + &self.names.render_inline(); diff --git a/src/types/reference_list/work.rs b/src/types/reference_list/work.rs index cafea3f..fea92fa 100644 --- a/src/types/reference_list/work.rs +++ b/src/types/reference_list/work.rs @@ -77,3 +77,14 @@ impl Renderable for Work { full_text } } + +impl Clone for Work { + fn clone(&self) -> Self { + Work { + names: self.names.clone(), + title: self.title.clone(), + issue: self.issue.clone(), + volume: self.volume.clone() + } + } +} -- 2.39.2