edition = "2021"
[dependencies]
+chrono = "0.4.41"
regex = "1.11.1"
-# 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.
- 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
--- /dev/null
+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");
+ }
+}
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
}
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);
- }
-}
pub mod types;
pub mod parser;
+pub mod inline;
fn main() {
let args: Vec<String> = env::args().collect();
-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
+}
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;
--- /dev/null
+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
+ }
+}
use crate::types::Renderable;
+use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext};
// submodule for handling headings
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",
}
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",
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 => "##",
--- /dev/null
+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()))
+ }
+}
use std::collections::HashMap;
use regex::Regex;
+use chrono::prelude::*;
use crate::types::Renderable;
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)
-}
use crate::types::Renderable;
+use crate::inline::{parse_inline_latex, parse_inline_html, parse_inline_gemtext};
// submodule for handling paragraphs
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*")};
--- /dev/null
+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
+ }
+}
+
+
--- /dev/null
+// 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
+ }
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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
+ }
+}