]> OzVa Git service - gn-parser/commitdiff
Added elements
authorMax Value <greenwoodw50@gmail.com>
Sat, 3 May 2025 19:28:07 +0000 (20:28 +0100)
committerMax Value <greenwoodw50@gmail.com>
Sat, 3 May 2025 19:28:07 +0000 (20:28 +0100)
Added elements
- document
- metadata
- reference list
- list
- referencing elements

Started working on parsing module

16 files changed:
Cargo.toml
schema.gn
src/inline.rs [new file with mode: 0644]
src/lib.rs
src/main.rs
src/parser.rs
src/types.rs
src/types/document.rs [new file with mode: 0644]
src/types/heading.rs
src/types/list.rs [new file with mode: 0644]
src/types/metadata.rs
src/types/paragraph.rs
src/types/reference_list.rs [new file with mode: 0644]
src/types/reference_list/name_list.rs [new file with mode: 0644]
src/types/reference_list/reference.rs [new file with mode: 0644]
src/types/reference_list/work.rs [new file with mode: 0644]

index 91efa57cf67324db0b339bd3318e9ec0815959ac..710695a7b4e14650c800b5dc16a90ae19db006f9 100644 (file)
@@ -4,4 +4,5 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+chrono = "0.4.41"
 regex = "1.11.1"
index c6bb26c56e45559d91dc74cbd4540d93976b343a..c91201ab0b5913cdc570088f043fe7dba8615b67 100644 (file)
--- a/schema.gn
+++ b/schema.gn
@@ -1,10 +1,17 @@
-# The Goodnight Markdown Specification
+---
+title: The Goodnight Markdown Specification - Version 0.1.0
+author: Goodnight Publishing
+---
+
+[META]
 
-Version 0.1.0
+[TOC]
+
+[REFS]
 
 ## Features
 
-- Headings are unchanged from vanilla markdown.
+- Headings are unchanged from vanilla markdown (6 levels).
 - Paragraphs are unchanged from vanilla markdown.
 - Ordered lists are unchanged from vanilla markdown.
 - URLs and email addresses are unchanged from vanilla markdown.
@@ -20,13 +27,19 @@ Version 0.1.0
 - Italic styling will work only with a single underscore character.
 - Unordered lists will work only with the hyphen character.
 
+1. Ordered
+2. Lists
+3. Are
+4. As
+5. Written
+
 ### Macros
 
 To generate the title, table of contents and bibliography, the following macros can be used:
 
 - `[TITLE]` generates the title block
 - `[TOC]` generates the table of contents
-- `[REFS]` generates the bibliography
+- `[META]` generates the bibliography
 
 ### Metadata
 
diff --git a/src/inline.rs b/src/inline.rs
new file mode 100644 (file)
index 0000000..5d2873c
--- /dev/null
@@ -0,0 +1,47 @@
+fn parse_inline (string: &str, symbol: char, front: &str, back: &str) -> String {
+       let mut text = string.to_string();
+       // makes a cycling itterable of the front and back replacement
+       // so that we always get the right one moving left to right
+       let mut front_back = [back, front].into_iter().cycle();
+       for (index, _) in string.rmatch_indices(symbol) {
+               if index == 0 || !(&text[index - 1 .. index] == "\\") {
+                       text.replace_range(index .. index + 1, front_back.next().unwrap());
+               }
+       }
+       text
+}
+
+pub fn parse_inline_latex (string: &str) -> String {
+       let mut text = parse_inline(string, '*', "\\textbf{", "}");
+       text = parse_inline(&text, '_', "\\textit{", "}");
+       parse_inline(&text, '`', "\\texttt{", "}")
+}
+
+pub fn parse_inline_html (string: &str) -> String {
+       let mut text = parse_inline(string, '*', "<b>", "</b>");
+       text = parse_inline(&text, '_', "<em>", "</em>");
+       parse_inline(&text, '`', "<code>", "</code>")
+}
+
+pub fn parse_inline_gemtext (string: &str) -> String {
+       let mut text = parse_inline(string, '*', "", "");
+       text = parse_inline(&text, '_', "", "");
+       parse_inline(&text, '`', "", "")
+}
+
+pub fn parse_inline_remove (string: &str) -> String {
+       parse_inline_gemtext(string)
+}
+
+#[cfg(test)]
+mod tests {
+       use super::*;
+
+       #[test]
+       fn test_formatting_parser() {
+               let string = parse_inline("`G_ood_n*igh*t`", '*', "B", "B");
+               let string = parse_inline(&string, '_', "I", "I");
+               let string = parse_inline(&string, '`', "C", "C");
+               assert_eq!(string, "CGIoodInBighBtC");
+       }
+}
index 61aefec8753dd6d869e53b52f8c559aad17d9b37..e9cb1f0dfceec1135f507613613c8c889bbb9e3c 100644 (file)
@@ -1,6 +1,14 @@
 use std::fs;
 use std::error::Error;
 
+pub mod parser;
+pub mod inline;
+pub mod types;
+
+use crate::types::document::Document;
+use crate::types::Renderable;
+
+
 pub struct Config {
        file_path: String
 }
@@ -21,21 +29,8 @@ pub fn run(config: Config) -> Result<(), Box<dyn Error>>  {
        let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");
 
-       let new: Vec<u8>;
-
-       for line in contents.lines() {
-               println!("{}", line);
-       }
+       let document = Document::from(contents);
+       println!("{}", document.render_html());
 
        Ok(())
 }
-
-#[cfg(test)]
-mod tests {
-       use super::*;
-
-       #[test]
-       fn placeholder() {
-               assert!(true);
-       }
-}
index 49767face6ad8aed1f765ded6e9f82a148c0dab6..430d70754650011bae6e3631eb909276b8d5eb2b 100644 (file)
@@ -4,6 +4,7 @@ use gn_parser::Config;
 
 pub mod types;
 pub mod parser;
+pub mod inline;
 
 fn main() {
        let args: Vec<String> = env::args().collect();
index ec3c3ec7444c7d420ffa9bb26c07217bf37b61ea..349ec9cceefd73c7f448bc4688b8b4d47fc8a9d5 100644 (file)
@@ -1,7 +1,111 @@
-use crate::types::{Renderable, paragraph, metadata};
+use regex::Regex;
 
-pub fn parse (string: String) -> impl Renderable {
-       paragraph::Paragraph{ text: string.to_string() }
-}
+use crate::types::Renderable;
+use crate::types::heading::Heading;
+use crate::types::paragraph::Paragraph;
+use crate::types::metadata::Metadata;
+use crate::types::reference_list::ReferenceList;
+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)
+// and returns the list of all the elements of the document
+
+pub fn parse_body (string: String, metadata: Metadata, reference_list: Option<ReferenceList>) -> Vec<Box<dyn Renderable>> {
+       let mut document: Vec<Box<dyn Renderable>> = Vec::new();
+       let mut clean_string = string.to_string();
+       let mut metadata: Option<Metadata> = Some(metadata);
+       let mut 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::<Vec<_>>().iter().rev() {
+               clean_string.replace_range(mat.start()+1 .. mat.end()-1, "");
+       }
+
+       let mut lines = clean_string.lines().peekable();
+       while let Some(line) = lines.next() {
+               if !(line == "") {
+                       let first = &(String::from(line) + "       ")[0..7].chars().collect::<Vec<_>>();
+                       match (first[0], first[1], first[2], first[3], first[4], first[5]) {
+                               // match all heading patterns
+                               ('#', '#', '#', '#', '#', '#') => {
+                                       document.push(Box::new(Heading{text: line[6..].to_string(), level: 6}))
+                               },
+                               ('#', '#', '#', '#', '#', ..) => {
+                                       document.push(Box::new(Heading{text: line[5..].to_string(), level: 5}))
+                               },
+                               ('#', '#', '#', '#', ..) => {
+                                       document.push(Box::new(Heading{text: line[4..].to_string(), level: 4}))
+                               },
+                               ('#', '#', '#', ..) => {
+                                       document.push(Box::new(Heading{text: line[3..].to_string(), level: 3}))
+                               },
+                               ('#', '#', ..) => {
+                                       document.push(Box::new(Heading{text: line[2..].to_string(), level: 2}))
+                               },
+                               ('#', ..) => {
+                                       document.push(Box::new(Heading{text: line[1..].to_string(), level: 1}))
+                               },
 
+                               // match all unordered list patterns
+                               ('-', ..) => {
+                                       let mut items: Vec<Item> = vec![Item{text: line[1..].to_string()}];
+                                       while lines.peek() != None && lines.peek() != Some(&"") && &lines.peek().unwrap()[..1] == "-" {
+                                               items.push(Item{text: lines.next().unwrap()[1..].to_string()});
+                                       }
+                                       document.push(Box::new(List{items, ordered: false}))
+                               }
 
+                               // match all ordered list patterns (matches up to 999. ...)
+                               ('1'..='9', '.', ..) | ('1'..='9', '1'..='9', '.', ..) | ('1'..='9', '1'..='9', '1'..='9', '.', ..) => {
+                                       let mut items: Vec<Item> = vec![Item{text: line.split_once(".").unwrap().1.to_string()}];
+                                       while match lines.peek() {
+                                               Some(string) => {
+                                                       if string == &"" {false}
+                                                       else {
+                                                               let re = Regex::new(r"\d*.").unwrap();
+                                                               re.is_match_at(string, 0)
+                                                       }
+                                               },
+                                               None => false
+                                       } {
+                                               match lines.next().unwrap().split_once(".") {
+                                                       Some((_, text)) => {items.push(Item{text: text.to_string()});},
+                                                       None => ()
+                                               }
+                                       }
+                                       document.push(Box::new(List{items, ordered: true}))
+                               }
+
+                               // match all insert sequences like the title
+                               ('[', 'R', 'E', 'F', 'S', ']') => {
+                                       match reference_list {
+                                               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 => ()
+                                       }
+                               },
+                               ('[', 'T', 'O', 'C', ']', ..) => {
+                                       document.push(Box::new(Paragraph{text: String::from("This is where the table of contents will go")}))
+                               },
+
+                               // make everything else a paragraph
+                               _ => {
+                                       document.push(Box::new(Paragraph{text: line.to_string()}))
+                               }
+                       }
+               }
+       }
+
+       document
+}
index c66a6abcbfc30403ce8198089a3024bd82f62e23..870185390b8ac498c9525bf4b56e27958dfeac61 100644 (file)
@@ -1,6 +1,9 @@
 pub mod paragraph;
 pub mod heading;
 pub mod metadata;
+pub mod reference_list;
+pub mod document;
+pub mod list;
 
 pub trait Renderable {
        fn render_latex(&self) -> String;
diff --git a/src/types/document.rs b/src/types/document.rs
new file mode 100644 (file)
index 0000000..90ab2cb
--- /dev/null
@@ -0,0 +1,56 @@
+use crate::types::metadata::Metadata;
+use crate::types::reference_list::ReferenceList;
+use crate::types::Renderable;
+use crate::parser;
+
+pub struct Document {
+       body: Vec<Box<dyn Renderable>>,
+       style: Option<String>
+}
+
+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}
+       }
+}
+
+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}}}"));}
+                       None => ()
+               }
+               full_text.push_str("\\begin{document}");
+               for element in &self.body {
+                       full_text.push_str(&element.render_latex());
+               }
+               full_text + "\\end{document}"
+       }
+
+       fn render_html(&self) -> String {
+               let mut full_text = String::from("<html><head>");
+               match &self.style {
+                       Some(style) => {full_text.push_str(&format!("<link rel='stylesheet' href='{style}.css'>"));}
+                       None => ()
+               }
+               full_text.push_str("</head><body>");
+               for element in &self.body {
+                       full_text.push_str(&element.render_html());
+               }
+               full_text + "</body></html>"
+       }
+
+       fn render_gemtext(&self) -> String {
+               let mut full_text = String::new();
+               for element in &self.body {
+                       full_text.push_str(&element.render_gemtext());
+               }
+               full_text
+       }
+}
index dd184a051ae91e47f51ddb086b1673d946718e86..91a88ac35d1dd4470be3482002663369063d885f 100644 (file)
@@ -1,4 +1,5 @@
 use crate::types::Renderable;
+use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext};
 
 // submodule for handling headings
 
@@ -9,7 +10,7 @@ pub struct Heading {
 
 impl Renderable for Heading {
        fn render_latex (&self) -> String {
-               let text = self.text.to_string();
+               let text = parse_inline_latex(self.text.trim());
                let section = match self.level {
                        1 => "section",
                        2 => "subsection",
@@ -21,7 +22,7 @@ impl Renderable for Heading {
        }
 
        fn render_html (&self) -> String {
-               let text = self.text.to_string();
+               let text = parse_inline_html(self.text.trim());
                let section = match self.level {
                        1 => "h1",
                        2 => "h2",
@@ -30,11 +31,11 @@ impl Renderable for Heading {
                        5 => "h5",
                        _ => "h6"
                };
-               format!("<{section}>{text}</{section}>")
+               format!("<{section}>{text}</{section}>\n")
        }
 
        fn render_gemtext (&self) -> String {
-               let text = self.text.to_string();
+               let text = parse_inline_gemtext(self.text.trim());
                let section = match self.level {
                        1 => "#",
                        2 => "##",
diff --git a/src/types/list.rs b/src/types/list.rs
new file mode 100644 (file)
index 0000000..2cc74b6
--- /dev/null
@@ -0,0 +1,49 @@
+use crate::types::Renderable;
+use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext};
+
+pub struct List {
+       pub items: Vec<Item>,
+       pub ordered: bool
+}
+
+impl Renderable for List {
+       fn render_latex(&self) -> String {
+               let element = if self.ordered {"ennumerate"} 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"
+       }
+       fn render_html(&self) -> String {
+               let element = if self.ordered {"ol"} else {"ul"};
+               let mut full_text = format!("<{element}>\n");
+               for item in &self.items {
+                       full_text.push_str(&item.render_html());
+               }
+               full_text + &format!("</{element}>\n")
+       }
+       fn render_gemtext(&self) -> String {
+               let mut full_text = String::new();
+               for item in &self.items {
+                       full_text.push_str(&item.render_gemtext());
+               }
+               full_text
+       }
+}
+
+pub struct Item {
+       pub text: String
+}
+
+impl Renderable for Item {
+       fn render_latex(&self) -> String {
+               format!("\\item {}\n", parse_inline_latex(self.text.trim()))
+       }
+       fn render_html(&self) -> String {
+               format!("<li>{}</li>\n", parse_inline_html(self.text.trim()))
+       }
+       fn render_gemtext(&self) -> String {
+               format!("* {}\n", parse_inline_gemtext(self.text.trim()))
+       }
+}
index 8e091eb04e8fc6ccec915c07a55e65d066b6012a..4a7ad48c17969c4b0cd7a6286424b1cdf6d6128b 100644 (file)
@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 use regex::Regex;
+use chrono::prelude::*;
 
 use crate::types::Renderable;
 
@@ -7,60 +8,91 @@ pub struct Metadata {
        pub title: String,
        pub author: String,
        pub date: String,
+       pub style: Option<String>,
        pub keys: HashMap<String, String>
 }
 
 impl Metadata {
        pub fn new() -> Self {
+               let local: DateTime<Local> = Local::now();
+               let date = local.format("%e %b %Y").to_string();
+
                Metadata {
                        title: String::from("Title"),
                        author: String::from("Author"),
-                       date: String::from("Today"),
+                       date,
+                       style: None,
                        keys: HashMap::new()
                }
        }
        pub fn from(string: String) -> (Self, String) {
-               parse_metadata(string)
+               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
+               // 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(), "");
+               }
+
+               let mut data = Metadata::new();
+               for line in metadata.lines() {
+                       match line.split_once(':') {
+                               Some((key, value)) => {
+                                       match key.trim() {
+                                               "title" => {data.title = String::from(value.trim())},
+                                               "author" => {data.author = String::from(value.trim())},
+                                               "date" => {data.date = String::from(value.trim())},
+                                               "style" => {data.style = Some(String::from(value.trim()))},
+                                               _ => {data.keys.insert(String::from(key.trim()), String::from(value.trim()));}
+                                       }
+                               },
+                               None => ()
+                       }
+               }
+
+               (data, text)
        }
 }
 
 impl Renderable for Metadata {
        fn render_latex (&self) -> String {
+               format!(
+                       "\\begin{{centering}}
+                               {{\\Large{{}}{title}}}\\\\
+                               {{\\large{{}}{author}}}\\\\
+                               {{\\large{{}}{date}}}\\\\
+                       \\end{{centering}}",
+                       title = self.title.trim(),
+                       author = self.author.trim(),
+                       date = self.date.trim()
+               )
        }
 
        fn render_html (&self) -> String {
+               format!(
+"<hgroup>
+<h1>{title}</h1>
+<p>{author}</p>
+<p>{date}</p>
+</hgroup>\n",
+                 title = self.title.trim(),
+                 author = self.author.trim(),
+                 date = self.date.trim()
+               )
        }
 
        fn render_gemtext (&self) -> String {
+               format!(
+                       "# {title}
+                       {author} - {date}",
+                 title = self.title.trim(),
+                 author = self.author.trim(),
+                 date = self.date.trim()
+               )
        }
 }
-
-fn parse_metadata (string: String) -> (Metadata, String) {
-       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
-       let re = Regex::new(r"---[[\w\W]&&[^-]]*").unwrap();
-       for mat in re.find_iter(&string) {
-               metadata.push_str(mat.as_str());
-               text.replace_range(mat.range(), "");
-       }
-
-       let mut data = Metadata::new();
-       for line in metadata.lines() {
-               match line.split_once(':') {
-                       Some((key,value)) => {
-                               match key {
-                                       "title" => {data.title = String::from(value)},
-                                       "author" => {data.author = String::from(value)},
-                                       "date" => {data.date = String::from(value)},
-                                       _ => {data.keys.insert(String::from(key), String::from(value));}
-                               }
-                       },
-                       None => ()
-               }
-       }
-
-       (Metadata::new(), text)
-}
index 58298795a6c1115eff159fc2dc992f1c35291b30..09ac2f108a75db0ca3b51d7ceb8f7783bbbdf239 100644 (file)
@@ -1,4 +1,5 @@
 use crate::types::Renderable;
+use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext};
 
 // submodule for handling paragraphs
 
@@ -9,49 +10,22 @@ pub struct Paragraph {
 impl Renderable for Paragraph {
 
        fn render_latex (&self) -> String {
-               let text = parse_inline(&self.text, '*', "\\textbf{", "}");
-               let text = parse_inline(&text, '_', "\\textit{", "}");
-               parse_inline(&text, '`', "\\texttt{", "}")
+               parse_inline_latex(&self.text.trim())
        }
 
        fn render_html (&self) -> String {
-               let text = parse_inline(&self.text, '*', "<b>", "</b>");
-               let text = parse_inline(&text, '_', "<em>", "</em>");
-               parse_inline(&text, '`', "<code>", "</code>")
+               String::from("<p>") + &parse_inline_html(&self.text.trim()) + "</p>\n"
        }
 
        fn render_gemtext (&self) -> String {
-               let text = parse_inline(&self.text, '*', "", "");
-               let text = parse_inline(&text, '_', "", "");
-               parse_inline(&text, '`', "", "")
+               parse_inline_gemtext(&self.text.trim())
        }
 }
 
-fn parse_inline (string: &str, symbol: char, front: &str, back: &str) -> String {
-       let mut text = string.to_string();
-       // makes a cycling itterable of the front and back replacement
-       // so that we always get the right one moving left to right
-       let mut front_back = [back, front].into_iter().cycle();
-       for (index, _) in string.rmatch_indices(symbol) {
-               println!("{}", index);
-               if index == 0 || !(&text[index - 1 .. index] == "\\") {
-                       text.replace_range(index .. index + 1, front_back.next().unwrap());
-               }
-       }
-       text
-}
-
 #[cfg(test)]
 mod tests {
        use super::*;
 
-       #[test]
-       fn test_formatting_parser() {
-               let string = parse_inline("`G_ood_n*igh*t`", '*', "B", "B");
-               let string = parse_inline(&string, '_', "I", "I");
-               let string = parse_inline(&string, '`', "C", "C");
-               assert_eq!(string, "CGIoodInBighBtC");
-       }
        #[test]
        fn test_latex_parser() {
                let paragraph = Paragraph{ text: String::from("*Goodnight*")};
diff --git a/src/types/reference_list.rs b/src/types/reference_list.rs
new file mode 100644 (file)
index 0000000..74aad5d
--- /dev/null
@@ -0,0 +1,41 @@
+use crate::types::Renderable;
+
+pub mod name_list;
+pub mod reference;
+pub mod work;
+
+pub struct ReferenceList {
+       pub list: Vec<reference::Reference>
+}
+
+impl ReferenceList {
+       pub fn from (string: String) -> (Option<Self>, String) {
+               (None, string)
+       }
+}
+
+impl Renderable for ReferenceList {
+       fn render_latex(&self) -> String {
+               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>");
+               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");
+               for reference in &self.list {
+                       full_text.push_str(&(reference.render_gemtext() + "\n\n"));
+               }
+               full_text
+       }
+}
+
+
diff --git a/src/types/reference_list/name_list.rs b/src/types/reference_list/name_list.rs
new file mode 100644 (file)
index 0000000..b8ac22d
--- /dev/null
@@ -0,0 +1,68 @@
+// contains a list of names that are rendereded into a valid APA 7th name list
+// both for inline citations and full bibliography citations
+
+pub struct NameList {
+       pub string: String,
+}
+
+// the inline and full rendering use different ways of storing stings for some reason
+// look into this maybe
+
+impl NameList {
+       pub fn render_full(&self) -> String {
+               let mut names: Vec<String> = Vec::new();
+               for name in self.string.split(" and ") {
+                       if &name[0..1] == "{" {
+                               // the case for dealing with organisations
+                               names.push(String::from(&name[1..name.len()-1]));
+                       } else {
+                               // the case for dealing with people with an indeterminate
+                               // amount of names
+                               let last_name = name.split_whitespace().last().unwrap();
+                               let mut full_name = String::from(last_name) + ", ";
+                               name.split_whitespace()
+                               .filter(|n| n != &last_name)
+                               .for_each(|n| {
+                                       let initial = format!("{}. ", &n[0..1]);
+                                       full_name.push_str(&initial)
+                               });
+                               names.push(full_name);
+                       }
+               }
+               let mut all_names = String::new();
+               names.sort_by(|a, b| a.cmp(b));
+
+               // et al. if more than 3
+               if names.len() > 3 {
+                       names = names[0..4].to_vec();
+                       names.push(String::from("et al."));
+               }
+
+               for name in names {all_names.push_str(&name)};
+               all_names
+       }
+
+       pub fn render_inline(&self) -> String {
+               let mut names: Vec<&str> = Vec::new();
+               for name in self.string.split(" and ") {
+                       if &name[0..1] == "{" {
+                               // the case for dealing with organisations
+                               names.push(&name[1..name.len()-1]);
+                       } else {
+                               let last_name = name.split_whitespace().last().unwrap();
+                               names.push(last_name);
+                       }
+               }
+               let mut all_names = String::new();
+               names.sort_by(|a, b| a.cmp(b));
+
+               // et al. if more than 3
+               if names.len() > 3 {
+                       names = names[0..4].to_vec();
+                       names.push("et al.");
+               }
+
+               for name in names {all_names.push_str(&name)};
+               all_names
+       }
+}
diff --git a/src/types/reference_list/reference.rs b/src/types/reference_list/reference.rs
new file mode 100644 (file)
index 0000000..ea8e44e
--- /dev/null
@@ -0,0 +1,115 @@
+use crate::types::reference_list::name_list::NameList;
+use crate::types::reference_list::work::Work;
+use crate::types::Renderable;
+
+pub struct Reference {
+       pub names: NameList,
+       pub title: String,
+       pub year: Option<String>,
+       pub publisher: Option<String>,
+       pub url: Option<String>,
+       pub inside: Option<Work>
+}
+
+impl Renderable for Reference {
+       fn render_latex(&self) -> String {
+               let mut full_text = self.names.render_full();
+               match &self.year {
+                       Some(year) => {full_text.push_str(&format!(" ({year})."));},
+                       None => {full_text.push_str(" (n.d.).");}
+               };
+               match &self.inside {
+                       Some(inside) => {
+                               full_text.push_str(&format!(" {title}. {work}.", title = &self.title, work = inside.render_latex()));
+                       },
+                       None => {
+                               full_text.push_str(&format!(" \\textit{{{}}}.", &self.title));
+                       }
+               };
+               match &self.publisher {
+                       Some(publisher) => {
+                               full_text.push_str(&format!(" {}.", publisher));
+                       },
+                       None => ()
+               };
+               match &self.url {
+                       Some(url) => {
+                               full_text.push_str(&format!(" \\url{{{}}}.", url));
+                       },
+                       None => ()
+               };
+               full_text
+       }
+
+       fn render_html(&self) -> String {
+               let mut full_text = String::from("<cite>") + &self.names.render_full();
+               match &self.year {
+                       Some(year) => {full_text.push_str(&format!(" ({year})."));},
+                       None => {full_text.push_str(" (n.d.).");}
+               };
+               match &self.inside {
+                       Some(inside) => {
+                               full_text.push_str(&format!(" {title}. {work}.", title = &self.title, work = inside.render_html()));
+                       },
+                       None => {
+                               full_text.push_str(&format!(" <em>{}</em>.", &self.title));
+                       }
+               };
+               match &self.publisher {
+                       Some(publisher) => {
+                               full_text.push_str(&format!(" {}.", publisher));
+                       },
+                       None => ()
+               };
+               match &self.url {
+                       Some(url) => {
+                               full_text.push_str(&format!(" <a href='{url}'>{url}</a>."));
+                       },
+                       None => ()
+               };
+               full_text.push_str("</cite>");
+               full_text
+
+       }
+
+       fn render_gemtext(&self) -> String {
+               let mut full_text = self.names.render_full();
+               match &self.year {
+                       Some(year) => {full_text.push_str(&format!(" ({year})."));},
+                       None => {full_text.push_str(" (n.d.).");}
+               };
+               match &self.inside {
+                       Some(inside) => {
+                               full_text.push_str(&format!(" {title}. {work}.", title = &self.title, work = inside.render_gemtext()));
+                       },
+                       None => {
+                               full_text.push_str(&format!(" {}.", &self.title));
+                       }
+               };
+               match &self.publisher {
+                       Some(publisher) => {
+                               full_text.push_str(&format!(" {}.", publisher));
+                       },
+                       None => ()
+               };
+               match &self.url {
+                       Some(url) => {
+                               full_text.push_str(&format!("\n=> {}.", url));
+                       },
+                       None => ()
+               };
+               full_text
+
+       }
+}
+
+impl Reference {
+       pub fn render_inline(&self) -> String {
+               let mut full_text = String::from("(") + &self.names.render_inline();
+               match &self.year {
+                       Some(year) => {full_text.push_str(&format!(", {year})"))},
+                       None => {full_text.push_str(", n.d.)")}
+               }
+               full_text
+       }
+}
diff --git a/src/types/reference_list/work.rs b/src/types/reference_list/work.rs
new file mode 100644 (file)
index 0000000..cafea3f
--- /dev/null
@@ -0,0 +1,79 @@
+use crate::types::reference_list::name_list::NameList;
+use crate::types::Renderable;
+
+// submodule for rendering a work that contains a reference such as a journal or book
+// renders only part of a full reference, for example
+// ... In Greenwood, W. S. (Eds.), The Goodnight Markdown Specification (2nd ed.). ...
+
+pub struct Work {
+       pub names: Option<NameList>,
+       pub title: String,
+       pub issue: Option<String>,
+       pub volume: Option<String>,
+}
+
+impl Renderable for Work {
+       fn render_latex(&self) -> String {
+               let mut full_text = String::from("In ");
+               match &self.names {
+                       Some(name_list) => {
+                               let names = name_list.render_full() + "(Eds.),";
+                               full_text.push_str(&names);
+                       },
+                       None => ()
+               };
+               full_text.push_str(&format!(" \\textit{{{title}}}", title = self.title));
+               match &self.volume {
+                       Some(volume) => {full_text.push_str(&format!(" ({volume})"))},
+                       None => ()
+               };
+               match &self.issue {
+                       Some(issue) => {full_text.push_str(&format!(" {issue}."))},
+                       None => {full_text.push_str(".");}
+               };
+
+               full_text
+       }
+       fn render_html(&self) -> String {
+               let mut full_text = String::from("In ");
+               match &self.names {
+                       Some(name_list) => {
+                               let names = name_list.render_full() + "(Eds.),";
+                               full_text.push_str(&names);
+                       },
+                       None => ()
+               };
+               full_text.push_str(&format!(" <em>{title}</em>", title = self.title));
+               match &self.volume {
+                       Some(volume) => {full_text.push_str(&format!(" ({volume})"))},
+                       None => ()
+               };
+               match &self.issue {
+                       Some(issue) => {full_text.push_str(&format!(" {issue}."))},
+                       None => {full_text.push_str(".");}
+               };
+
+               full_text
+       }
+       fn render_gemtext(&self) -> String {
+               let mut full_text = String::from("In ");
+               match &self.names {
+                       Some(name_list) => {
+                               let names = name_list.render_full() + "(Eds.),";
+                               full_text.push_str(&names);
+                       },
+                       None => ()
+               };
+               full_text.push_str(&format!(" {title}", title = self.title));
+               match &self.volume {
+                       Some(volume) => {full_text.push_str(&format!(" ({volume})"))},
+                       None => ()
+               };
+               match &self.issue {
+                       Some(issue) => {full_text.push_str(&format!(" {issue}."))},
+                       None => {full_text.push_str(".");}
+               };
+
+               full_text
+       }
+}