diff --git a/Cargo.lock b/Cargo.lock index e2d0909..354c7de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" dependencies = [ "byteorder", - "serde", + "serde 1.0.118", ] [[package]] @@ -185,6 +185,12 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "bytemuck" version = "1.4.1" @@ -235,7 +241,7 @@ checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", - "num-traits", + "num-traits 0.2.14", "time", "winapi 0.3.9", ] @@ -292,6 +298,22 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom", + "rust-ini", + "serde 1.0.118", + "serde-hjson", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "console" version = "0.13.0" @@ -820,7 +842,7 @@ dependencies = [ "log 0.4.11", "rand 0.7.3", "regex", - "serde", + "serde 1.0.118", "serde_derive", "serde_json", "tempfile", @@ -992,7 +1014,7 @@ dependencies = [ "jpeg-decoder", "num-iter", "num-rational", - "num-traits", + "num-traits 0.2.14", "png", "scoped_threadpool", "tiff", @@ -1117,6 +1139,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.81" @@ -1132,6 +1167,16 @@ dependencies = [ "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +dependencies = [ + "serde 0.8.23", + "serde_test", +] + [[package]] name = "linked-hash-map" version = "0.5.3" @@ -1314,6 +1359,28 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check 0.9.2", +] + +[[package]] +name = "nom_locate" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67484adf5711f94f2f28b653bf231bff8e438be33bf5b0f35935a0db4f618a2" +dependencies = [ + "bytecount", + "memchr", + "nom", +] + [[package]] name = "notify" version = "4.0.15" @@ -1332,6 +1399,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits 0.2.14", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1339,7 +1417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg 1.0.1", - "num-traits", + "num-traits 0.2.14", ] [[package]] @@ -1350,7 +1428,7 @@ checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ "autocfg 1.0.1", "num-integer", - "num-traits", + "num-traits 0.2.14", ] [[package]] @@ -1361,7 +1439,16 @@ checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg 1.0.1", "num-integer", - "num-traits", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", ] [[package]] @@ -1600,7 +1687,7 @@ dependencies = [ "chrono", "indexmap", "line-wrap", - "serde", + "serde 1.0.118", "xml-rs", ] @@ -1937,7 +2024,7 @@ dependencies = [ "native-tls", "percent-encoding 2.1.0", "pin-project-lite 0.2.0", - "serde", + "serde 1.0.118", "serde_urlencoded", "tokio", "tokio-tls", @@ -1949,6 +2036,22 @@ dependencies = [ "winreg 0.7.0", ] +[[package]] +name = "rsass" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1144f0d9fd5df1f8fbac387e91b73a2d315e013ee6beb48353a71d8ec2c5ab" +dependencies = [ + "lazy_static", + "nom", + "nom_locate", + "num-bigint", + "num-integer", + "num-rational", + "num-traits 0.2.14", + "rand 0.7.3", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -1961,6 +2064,12 @@ dependencies = [ "crossbeam-utils 0.8.1", ] +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + [[package]] name = "rustc-demangle" version = "0.1.18" @@ -2045,6 +2154,12 @@ dependencies = [ "libc", ] +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + [[package]] name = "serde" version = "1.0.118" @@ -2054,6 +2169,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "linked-hash-map 0.3.0", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + [[package]] name = "serde_derive" version = "1.0.118" @@ -2073,7 +2201,16 @@ checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", - "serde", + "serde 1.0.118", +] + +[[package]] +name = "serde_test" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +dependencies = [ + "serde 0.8.23", ] [[package]] @@ -2085,7 +2222,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde", + "serde 1.0.118", ] [[package]] @@ -2135,6 +2272,7 @@ dependencies = [ "charred", "chrono", "colored", + "config", "crossbeam-utils 0.7.2", "env_logger 0.7.1", "failure", @@ -2155,8 +2293,8 @@ dependencies = [ "rayon", "regex", "reqwest", - "serde", - "serde_derive", + "rsass", + "serde 1.0.118", "sha2", "structopt", "syntect", @@ -2175,6 +2313,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.7.0" @@ -2260,7 +2404,7 @@ dependencies = [ "onig", "plist", "regex-syntax", - "serde", + "serde 1.0.118", "serde_derive", "serde_json", "walkdir", @@ -2403,7 +2547,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" dependencies = [ - "serde", + "serde 1.0.118", ] [[package]] @@ -2614,7 +2758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" dependencies = [ "cfg-if 1.0.0", - "serde", + "serde 1.0.118", "serde_json", "wasm-bindgen-macro", ] @@ -2824,5 +2968,5 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" dependencies = [ - "linked-hash-map", + "linked-hash-map 0.5.3", ] diff --git a/Cargo.toml b/Cargo.toml index 52283ca..e4f34aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,9 +36,8 @@ colored = "1.9.3" gh-emoji = "1.0.3" notify = "4.0.12" toml = "0.5.6" -serde ="1.0.111" -serde_derive = "1.0.111" -reqwest = {version = "0.10", features=["blocking"]} +serde = { version = "1.0.111", features = ["serde_derive"] } +reqwest = { version = "0.10", features = ["blocking"] } mime_guess = "2.0.3" mime = "0.3.16" base64 = "0.12.3" @@ -51,6 +50,8 @@ platform-dirs = "0.2.0" image = "0.23.12" parking_lot = "0.11.1" sha2 = "0.9.2" +config = "0.10.1" +rsass = "0.16.0" -headless_chrome = {version = "0.9.0", optional = true} -failure = {version = "0.1.8", optional = true} +headless_chrome = { version = "0.9.0", optional = true } +failure = { version = "0.1.8", optional = true } diff --git a/README.md b/README.md index 6206761..c1bd6db 100644 --- a/README.md +++ b/README.md @@ -197,90 +197,111 @@ Placeholder ``` Metadata can also be defined in a separate toml file with simple key-value pairs. +The file `Manifest.toml` will always be included by default. Example: ```toml -# bibliography.bib.toml -author = "Snek" -published = "2020" -test-key = ["test value", "test value 2"] - -# those files won't get imported -ignored-imports = ["style.css"] - -# stylesheets that should be included -included-stylesheets = ["style2.css"] +# document metadata +[metadata] +# language setting of the document +language = 'en' -# other metadata files that should be included -included-configs = [] +# author of the document +author = 'author' -# bibliography that should be included -included-bibliography = ["mybib.toml"] -# glossary that sould be included -included-glossary = ["myglossary.toml"] +# features used in the document +[features] # if external sources (images, stylesheets, MathJax) # should be embedded into the document (default: true) -embed-external = true +embed_external = true # If SmartArrows should be used (default: true) -smart-arrows = true - -# Includes a MathJax script tag in the document to render MathML in chromium. -# (default: true) -include-math-jax = true +smart_arrows = true +include_mathjax = true -### Image processing options ### +[imports] +# those files won't get imported +ignored_imports = [] -# Force convert images to the specified format. -# Supported formats are png, jpeg, gif, bmp, (ico needs size <= 256), avif, pnm -# (default: keep original) -image-format = "jpg" +# stylesheets that should be included +included_stylesheets = ['style.css'] -# the max width for the images. -# if an image is larger than that it get's resized. -# (default: none) -image-max-width = 700 +# bibliography that should be included +included_bibliography = ['Bibliography.toml'] -# the max width for the images. -# if an image is larger than that it get's resized. -# (default: none) -image-max-height = 800 +# glossary that sould be included +included_glossaries = ['Glossary.toml'] -### PDF Options - needs the pdf feature enabled ### +# settings related to pdf rendering +[pdf] # If the header and footer of the pdf should be displayed (default: true) -pdf-display-header-footer = true +display_header_footer = true -# PDF header template of each page (default: empty) -pdf-header-template = "
" +# PDF header template of each page (default: '
') +header_template = '
' # PDF footer template of each page (default: see chromium_pdf assets) -pdf-footer-template = "
" +footer_template = ''' +
+ / +
''' -# Top margin of the pdf. Should be between 0 and 1. (default: 1.0) -pdf-margin-top = 1 +# The scale at which the website is rendered into pdf. +page_scale = 1.0 + +# margin of the pdf document +[pdf.margin] + +# Top margin of the pdf. Should be between 0 and 1. (default: 0.5) +top = 0.5 -# Bottom margin of the pdf. Should be between 0 and 1. (default: 1.0) -pdf-margin-bottom = 1 +# Bottom margin of the pdf. Should be between 0 and 1. (default: 0.5) +bottom = 0.5 # Left margin of the pdf. Should be between 0 and 1. -pdf-margin-left = 0 +left = 0 # Right margin of the pdf. Should be between 0 and 1. -pdf-margin-right = 0 +right = 0 -# Page height of the pdf -pdf-page-height = 100 -# Page width of the pdf -pdf-page-width = 80 +# image settings +[images] -# The scale at which the website is rendered into pdf. -pdf-page-scale = 1.0 +# Force convert images to the specified format. +# Supported formats are png, jpeg, gif, bmp, (ico needs size <= 256), avif, pnm +# (default: keep original) +format = "png" + +# the max width for the images. +# if an image is larger than that it get's resized. +# (default: none) +max_width = 700 + +# the max width for the images. +# if an image is larger than that it get's resized. +# (default: none) +max_height = 500 + + +# Visual adjustments +[style] +# how bibliography references should be displayed +bib_ref_display = '{{number}}' + +# the chosen theme for the document +# one of: GithubLight, SolarizedLight, OceanLight, SolarizedDark, OceanDark, MagicDark +theme = 'GithubLight' + +# custom metadata +# String -> String Mappings +[custom_attributes] +custom_key1 = "Custom Value" ``` The `[Section]` keys are not relevant as the structure gets flattened before the values are read. diff --git a/src/elements/mod.rs b/src/elements/mod.rs index da3363a..631de81 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -1,13 +1,10 @@ pub mod tokens; use crate::format::PlaceholderTemplate; -use crate::references::configuration::keys::{ - EMBED_EXTERNAL, IMAGE_FORMAT, IMAGE_MAX_HEIGHT, IMAGE_MAX_WIDTH, -}; -use crate::references::configuration::{ConfigRefEntry, Configuration, Value}; use crate::references::glossary::{GlossaryManager, GlossaryReference}; use crate::references::placeholders::ProcessPlaceholders; use crate::references::templates::{Template, TemplateVariable}; +use crate::settings::Settings; use crate::utils::downloads::{DownloadManager, PendingDownload}; use crate::utils::image_converting::{ImageConverter, PendingImage}; use asciimath_rs::elements::special::Expression; @@ -18,7 +15,6 @@ use image::ImageFormat; use mime::Mime; use parking_lot::Mutex; use std::collections::HashMap; -use std::iter::FromIterator; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; @@ -77,7 +73,7 @@ pub struct Document { pub(crate) is_root: bool, pub(crate) path: Option, pub(crate) placeholders: Vec>>, - pub config: Configuration, + pub config: Arc>, pub bibliography: BibManager, pub downloads: Arc>, pub images: Arc>, @@ -256,7 +252,7 @@ pub struct Placeholder { #[derive(Clone, Debug)] pub struct RefLink { - pub(crate) description: Box, + pub(crate) description: TextLine, pub(crate) reference: String, } @@ -318,7 +314,7 @@ impl Document { is_root: true, path: None, placeholders: Vec::new(), - config: Configuration::default(), + config: Arc::new(Mutex::new(Settings::default())), bibliography: BibManager::new(), stylesheets: Vec::new(), downloads: Arc::new(Mutex::new(DownloadManager::new())), @@ -356,7 +352,7 @@ impl Document { list.ordered = ordered; self.elements.iter().for_each(|e| match e { Block::Section(sec) => { - if !sec.get_hide_in_toc() { + if !sec.is_hidden_in_toc() { let mut item = ListItem::new(Line::RefLink(sec.header.get_anchor()), 1, ordered); item.children.append(&mut sec.get_toc_list(ordered).items); @@ -445,41 +441,24 @@ impl Document { fn process_media(&self) { let downloads = Arc::clone(&self.downloads); - if let Some(Value::Bool(embed)) = self - .config - .get_entry(EMBED_EXTERNAL) - .map(|e| e.get().clone()) - { - if embed { - downloads.lock().download_all(); - } - } else { + if self.config.lock().features.embed_external { downloads.lock().download_all(); } - if let Some(Value::String(s)) = self.config.get_entry(IMAGE_FORMAT).map(|e| e.get().clone()) - { + if let Some(s) = &self.config.lock().images.format { if let Some(format) = ImageFormat::from_extension(s) { self.images.lock().set_target_format(format); } } - let mut image_width = -1; - let mut image_height = -1; + let mut image_width = 0; + let mut image_height = 0; - if let Some(Value::Integer(i)) = self - .config - .get_entry(IMAGE_MAX_WIDTH) - .map(|v| v.get().clone()) - { + if let Some(i) = self.config.lock().images.max_width { image_width = i; image_height = i; } - if let Some(Value::Integer(i)) = self - .config - .get_entry(IMAGE_MAX_HEIGHT) - .map(|v| v.get().clone()) - { + if let Some(i) = self.config.lock().images.max_height { image_height = i; - if image_width < 0 { + if image_width <= 0 { image_width = i; } } @@ -507,9 +486,10 @@ impl Section { pub fn get_toc_list(&self, ordered: bool) -> List { let mut list = List::new(); + self.elements.iter().for_each(|e| { if let Block::Section(sec) = e { - if !sec.get_hide_in_toc() { + if !sec.is_hidden_in_toc() { let mut item = ListItem::new(Line::RefLink(sec.header.get_anchor()), 1, ordered); item.children.append(&mut sec.get_toc_list(ordered).items); @@ -521,7 +501,7 @@ impl Section { list } - pub(crate) fn get_hide_in_toc(&self) -> bool { + pub(crate) fn is_hidden_in_toc(&self) -> bool { if let Some(meta) = &self.metadata { meta.get_bool("toc-hidden") } else { @@ -577,7 +557,7 @@ impl Header { pub fn get_anchor(&self) -> RefLink { RefLink { - description: Box::new(self.line.clone()), + description: self.line.as_raw_text().as_plain_line(), reference: self.anchor.clone(), } } @@ -633,6 +613,16 @@ impl TextLine { pub fn add_subtext(&mut self, subtext: Inline) { self.subtext.push(subtext) } + + pub fn as_plain_line(&self) -> TextLine { + TextLine { + subtext: self + .subtext + .iter() + .map(|s| Inline::Plain(s.as_plain_text())) + .collect(), + } + } } impl Table { @@ -766,19 +756,6 @@ impl Metadata for InlineMetadata { } } -impl Into> for InlineMetadata { - fn into(self) -> HashMap { - HashMap::from_iter(self.data.iter().filter_map(|(k, v)| match v { - MetadataValue::String(s) => Some((k.clone(), Value::String(s.clone()))), - MetadataValue::Bool(b) => Some((k.clone(), Value::Bool(*b))), - MetadataValue::Integer(i) => Some((k.clone(), Value::Integer(*i))), - MetadataValue::Float(f) => Some((k.clone(), Value::Float(*f))), - MetadataValue::Template(t) => Some((k.clone(), Value::Template(t.clone()))), - _ => None, - })) - } -} - impl Image { pub fn get_content(&self) -> Option> { let mut data = None; @@ -802,15 +779,11 @@ pub struct BibEntry { pub struct BibReference { pub(crate) key: String, pub(crate) entry_anchor: Arc>, - pub(crate) display: Option, + pub(crate) display: Option, } impl BibReference { - pub fn new( - key: String, - display: Option, - anchor: Arc>, - ) -> Self { + pub fn new(key: String, display: Option, anchor: Arc>) -> Self { Self { key: key.to_string(), display, @@ -823,8 +796,7 @@ impl BibReference { let entry = entry.lock(); if let Some(display) = &self.display { - let display = display.read().unwrap(); - let mut template = PlaceholderTemplate::new(display.get().as_string()); + let mut template = PlaceholderTemplate::new(display.clone()); let mut value_map = HashMap::new(); value_map.insert("key".to_string(), entry.key()); @@ -853,3 +825,71 @@ impl MetadataValue { } } } + +impl Line { + pub fn as_raw_text(&self) -> TextLine { + match self { + Line::Text(t) => t.clone(), + Line::Ruler(_) => TextLine::new(), + Line::RefLink(r) => r.description.clone(), + Line::Anchor(a) => a.inner.as_raw_text().as_plain_line(), + Line::Centered(c) => c.line.clone(), + Line::BibEntry(_) => TextLine::new(), + } + } +} + +impl Inline { + pub fn as_plain_text(&self) -> PlainText { + match self { + Inline::Plain(p) => p.clone(), + Inline::Bold(b) => b.value.iter().fold( + PlainText { + value: String::new(), + }, + |a, b| PlainText { + value: format!("{} {}", a.value, b.as_plain_text().value), + }, + ), + Inline::Italic(i) => i.value.iter().fold( + PlainText { + value: String::new(), + }, + |a, b| PlainText { + value: format!("{} {}", a.value, b.as_plain_text().value), + }, + ), + Inline::Underlined(u) => u.value.iter().fold( + PlainText { + value: String::new(), + }, + |a, b| PlainText { + value: format!("{} {}", a.value, b.as_plain_text().value), + }, + ), + Inline::Striked(s) => s.value.iter().fold( + PlainText { + value: String::new(), + }, + |a, b| PlainText { + value: format!("{} {}", a.value, b.as_plain_text().value), + }, + ), + Inline::Monospace(m) => PlainText { + value: m.value.clone(), + }, + Inline::Superscript(s) => s.value.iter().fold( + PlainText { + value: String::new(), + }, + |a, b| PlainText { + value: format!("{} {}", a.value, b.as_plain_text().value), + }, + ), + Inline::Colored(c) => c.value.as_plain_text(), + _ => PlainText { + value: String::new(), + }, + } + } +} diff --git a/src/format/assets/base.scss b/src/format/assets/base.scss new file mode 100644 index 0000000..adf221c --- /dev/null +++ b/src/format/assets/base.scss @@ -0,0 +1,159 @@ +body { + background-color: $body-background; + overflow-x: hidden; + color: $primary-color; + word-break: break-word; +} + +.content { + font-family: "Fira Sans", "Noto Sans", SansSerif, sans-serif; + width: 100vh; + max-width: calc(100% - 4rem); + padding: 2rem; + margin: auto; + background-color: $background-color; +} + +h1 { + font-size: 2.2rem; +} + +h2 { + font-size: 1.8rem; +} + +h3 { + font-size: 1.4rem; +} + +h4 { + font-size: 1rem; +} + +h5 { + font-size: 0.8rem; +} + +h6 { + font-size: 0.4rem; +} + +img { + max-width: 100%; + max-height: 100vh; + height: auto; +} + +code { + color: $primary-color; + + pre { + font-family: "Fira Code", "Mono", monospace; + padding: 0.8em 0.2em; + background-color: $code-background !important; + border-radius: 0.25em; + overflow: auto; + } + + &.inlineCode { + font-family: "Fira Code", monospace; + border-radius: 0.1em; + background-color: $code-background; + padding: 0 0.1em; + } +} + +.tableWrapper { + overflow-x: auto; + width: 100%; + + & > table { + margin: auto; + } +} + +table { + border-collapse: collapse; + + tr { + &:nth-child(odd) { + background-color: $table-background-alt; + } + + &:nth-child(1) { + background-color: $table-background-alt; + font-weight: bold; + border-bottom: 1px solid invert($background-color) + } + } +} + +table td, table th { + border-left: 1px solid invert($background-color); + padding: 0.2em 0.5em; +} + +table tr td:first-child, table tr th:first-child { + border-left: none; +} + +blockquote { + margin-left: 0; + background-color: rgba(0, 0, 0, 0); +} + +a { + color: $secondary-color; +} + +.quote { + border-left: 0.3em solid $quote-background-alt; + border-radius: 0.2em; + padding-left: 1em; + margin-left: 0; + background-color: $quote-background; + + .metadata { + font-style: italic; + padding-left: 0.5em; + color: $primary-variant-1; + } +} + + +.figure { + width: 100%; + display: block; + text-align: center; + + .imageDescription { + display: block; + color: $primary-variant-1; + font-style: italic; + } +} + +.centered { + text-align: center; +} + +.glossaryReference { + text-decoration: none; + color: inherit; + border-bottom: 1px dotted $primary-color; +} + +.arrow { + font-family: "Fira Code", "Mono", monospace; +} + +@media print { + + .content > section > section, .content > section > section { + page-break-inside: avoid; + } + + body { + background-color: $background-color !important; + } +} \ No newline at end of file diff --git a/src/format/assets/dark-magic.scss b/src/format/assets/dark-magic.scss new file mode 100644 index 0000000..f1cac35 --- /dev/null +++ b/src/format/assets/dark-magic.scss @@ -0,0 +1,14 @@ +$background-color: #1e1d2c; +$background-color-variant-1: lighten($background-color, 7%); +$background-color-variant-2: lighten($background-color, 14%); +$background-color-variant-3: lighten($background-color, 21%); + +$primary-color: #EEE; +$primary-variant-1: darken($primary-color, 14%); +$secondary-color: #3aa7df; + +$body-background: darken($background-color, 5%); +$code-background: lighten($background-color, 5%); +$table-background-alt: $background-color-variant-2; +$quote-background: lighten($background-color-variant-1, 3%); +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/assets/dark-ocean.scss b/src/format/assets/dark-ocean.scss new file mode 100644 index 0000000..510f274 --- /dev/null +++ b/src/format/assets/dark-ocean.scss @@ -0,0 +1,14 @@ +$background-color: darken(#2b303b, 8%); +$background-color-variant-1: lighten($background-color, 7%); +$background-color-variant-2: lighten($background-color, 14%); +$background-color-variant-3: lighten($background-color, 21%); + +$primary-color: #EEE; +$primary-variant-1: darken($primary-color, 14%); +$secondary-color: #3aa7df; + +$body-background: darken($background-color, 5%); +$code-background: $background-color-variant-1; +$table-background-alt: $background-color-variant-2; +$quote-background: lighten($background-color-variant-1, 3%); +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/assets/dark-solarized.scss b/src/format/assets/dark-solarized.scss new file mode 100644 index 0000000..a067a27 --- /dev/null +++ b/src/format/assets/dark-solarized.scss @@ -0,0 +1,13 @@ +$background-color: darken(#002b36, 5%); +$background-color-variant-1: lighten($background-color, 7%); +$background-color-variant-2: lighten($background-color, 14%); +$background-color-variant-3: lighten($background-color, 21%); +$primary-color: #EEE; +$primary-variant-1: darken($primary-color, 14%); +$secondary-color: #0096c9; + +$body-background: darken($background-color, 3%); +$code-background: $background-color-variant-1; +$table-background-alt: lighten($background-color, 10%); +$quote-background: lighten($background-color-variant-1, 3%); +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/assets/light-github.scss b/src/format/assets/light-github.scss new file mode 100644 index 0000000..05d74cb --- /dev/null +++ b/src/format/assets/light-github.scss @@ -0,0 +1,13 @@ +$background-color: #FFF; +$background-color-variant-1: darken($background-color, 7%); +$background-color-variant-2: darken($background-color, 14%); +$background-color-variant-3: darken($background-color, 21%); +$primary-color: #000; +$primary-variant-1: lighten($primary-color, 14%); +$secondary-color: #00286a; + +$body-background: $background-color-variant-1; +$code-background: $background-color-variant-1; +$table-background-alt: $background-color-variant-2; +$quote-background: $background-color-variant-2; +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/assets/light-ocean.scss b/src/format/assets/light-ocean.scss new file mode 100644 index 0000000..af7aef2 --- /dev/null +++ b/src/format/assets/light-ocean.scss @@ -0,0 +1,13 @@ +$background-color: #FFF; +$background-color-variant-1: darken($background-color, 7%); +$background-color-variant-2: darken($background-color, 14%); +$background-color-variant-3: darken($background-color, 21%); +$primary-color: #112; +$primary-variant-1: lighten($primary-color, 14%); +$secondary-color: #00348e; + +$body-background: $background-color-variant-1; +$code-background: $background-color-variant-1; +$table-background-alt: $background-color-variant-2; +$quote-background: $background-color-variant-2; +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/assets/light-solarized.scss b/src/format/assets/light-solarized.scss new file mode 100644 index 0000000..dcfbb87 --- /dev/null +++ b/src/format/assets/light-solarized.scss @@ -0,0 +1,13 @@ +$background-color: #fff8f0; +$background-color-variant-1: darken($background-color, 4%); +$background-color-variant-2: darken($background-color, 8%); +$background-color-variant-3: darken($background-color, 12%); +$primary-color: #112; +$primary-variant-1: lighten($primary-color, 14%); +$secondary-color: #2b61be; + +$body-background: $background-color-variant-1; +$code-background: $background-color-variant-1; +$table-background-alt: $background-color-variant-2; +$quote-background: $background-color-variant-2; +$quote-background-alt: $background-color-variant-3; \ No newline at end of file diff --git a/src/format/chromium_pdf/mod.rs b/src/format/chromium_pdf/mod.rs index 748d8a1..440311b 100644 --- a/src/format/chromium_pdf/mod.rs +++ b/src/format/chromium_pdf/mod.rs @@ -2,19 +2,16 @@ use crate::elements::Document; use crate::format::chromium_pdf::result::{PdfRenderingError, PdfRenderingResult}; use crate::format::html::html_writer::HTMLWriter; use crate::format::html::to_html::ToHtml; -use crate::references::configuration::keys::{ - INCLUDE_MATHJAX, PDF_DISPLAY_HEADER_FOOTER, PDF_FOOTER_TEMPLATE, PDF_HEADER_TEMPLATE, - PDF_MARGIN_BOTTOM, PDF_MARGIN_LEFT, PDF_MARGIN_RIGHT, PDF_MARGIN_TOP, PDF_PAGE_HEIGHT, - PDF_PAGE_SCALE, PDF_PAGE_WIDTH, -}; -use crate::references::configuration::Configuration; +use crate::settings::Settings; use crate::utils::caching::CacheStorage; +use bibliographix::Mutex; use headless_chrome::protocol::page::PrintToPdfOptions; use headless_chrome::{Browser, LaunchOptionsBuilder, Tab}; use std::fs; use std::fs::OpenOptions; use std::io::BufWriter; use std::path::PathBuf; +use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; @@ -25,14 +22,8 @@ pub fn render_to_pdf(document: Document) -> PdfRenderingResult> { let cache = CacheStorage::new(); let mut file_path = PathBuf::from(format!("tmp-document.html")); file_path = cache.get_file_path(&file_path); - let mut mathjax = false; - - if let Some(entry) = document.config.get_entry(INCLUDE_MATHJAX) { - if entry.get().as_bool() == Some(true) { - mathjax = true; - } - } let config = document.config.clone(); + let mathjax = config.lock().features.include_mathjax; let handle = thread::spawn({ let file_path = file_path.clone(); @@ -107,49 +98,23 @@ fn wait_for_mathjax(tab: &Tab, timeout: Duration) -> PdfRenderingResult<()> { Ok(()) } -fn get_pdf_options(config: Configuration) -> PrintToPdfOptions { +fn get_pdf_options(config: Arc>) -> PrintToPdfOptions { + let config = config.lock().pdf.clone(); PrintToPdfOptions { landscape: None, - display_header_footer: config - .get_entry(PDF_DISPLAY_HEADER_FOOTER) - .and_then(|value| value.get().as_bool()), + display_header_footer: Some(config.display_header_footer), print_background: Some(true), - scale: config - .get_entry(PDF_PAGE_SCALE) - .and_then(|value| value.get().as_float()) - .map(|value| value as f32), - paper_width: config - .get_entry(PDF_PAGE_WIDTH) - .and_then(|value| value.get().as_float()) - .map(|value| value as f32), - paper_height: config - .get_entry(PDF_PAGE_HEIGHT) - .and_then(|value| value.get().as_float()) - .map(|value| value as f32), - margin_top: config - .get_entry(PDF_MARGIN_TOP) - .and_then(|value| value.get().as_float()) - .map(|f| f as f32), - margin_bottom: config - .get_entry(PDF_MARGIN_BOTTOM) - .and_then(|value| value.get().as_float()) - .map(|f| f as f32), - margin_left: config - .get_entry(PDF_MARGIN_LEFT) - .and_then(|value| value.get().as_float()) - .map(|f| f as f32), - margin_right: config - .get_entry(PDF_MARGIN_RIGHT) - .and_then(|value| value.get().as_float()) - .map(|f| f as f32), + scale: Some(config.page_scale), + paper_width: config.page_width, + paper_height: config.page_height, + margin_top: config.margin.top, + margin_bottom: config.margin.bottom, + margin_left: config.margin.left, + margin_right: config.margin.right, page_ranges: None, ignore_invalid_page_ranges: None, - header_template: config - .get_entry(PDF_HEADER_TEMPLATE) - .map(|value| value.get().as_string()), - footer_template: config - .get_entry(PDF_FOOTER_TEMPLATE) - .map(|value| value.get().as_string()), + header_template: config.header_template, + footer_template: config.footer_template, prefer_css_page_size: None, } } diff --git a/src/format/html/assets/style.css b/src/format/html/assets/style.css deleted file mode 100644 index 5df7317..0000000 --- a/src/format/html/assets/style.css +++ /dev/null @@ -1,149 +0,0 @@ -body { - background-color: #DDD; - overflow-x: hidden; - color: #000; - word-break: break-word; -} - -.content { - font-family: "Fira Sans", "Noto Sans", SansSerif, sans-serif; - width: 100vh; - max-width: calc(100% - 4rem); - padding: 2rem; - margin: auto; - background-color: #FFF; -} - -h1 { - font-size: 2.2rem; -} - -h2 { - font-size: 1.8rem; -} - -h3 { - font-size: 1.4rem; -} - -h4 { - font-size: 1rem; -} - -h5 { - font-size: 0.8rem; -} - -h6 { - font-size: 0.4rem; -} - -img { - max-width: 100%; - max-height: 100vh; - height: auto; -} - -code { - color: #000; -} - -code pre { - font-family: "Fira Code", "Mono", monospace; - padding: 0.8em 0.2em; - background-color: #EEE !important; - border-radius: 0.25em; -} - -code.inlineCode { - font-family: "Fira Code", monospace; - border-radius: 0.1em; - background-color: #EEE; - padding: 0 0.1em -} - -.tableWrapper { - overflow-x: auto; - width: 100%; -} - -.tableWrapper > table { - margin: auto; -} - -table { - border-collapse: collapse; -} - -table tr:nth-child(odd) { - background-color: #DDD; -} - -table tr:nth-child(1) { - background-color: white; - font-weight: bold; - border-bottom: 1px solid black; -} - -table td, table th { - border-left: 1px solid black; - padding: 0.2em 0.5em -} - -table tr td:first-child, table tr th:first-child { - border-left: none; -} - -blockquote { - margin-left: 0; - background-color: rgba(0, 0, 0, 0); -} - -.quote { - border-left: 0.3em solid gray; - border-radius: 0.2em; - padding-left: 1em; - margin-left: 0; - background-color: #EEE; -} - -.quote .metadata { - font-style: italic; - padding-left: 0.5em; - color: #444 -} - -.figure { - width: 100%; - display: block; - text-align: center; -} - -.figure .imageDescription { - display: block; - color: #444; - font-style: italic; -} - -.centered { - text-align: center; -} - -.glossaryReference { - text-decoration: none; - color: inherit; - border-bottom: 1px dotted #000; -} - -.arrow { - font-family: "Fira Code", "Mono", monospace; -} - -@media print { - .content > section > section, .content > section > section { - page-break-inside: avoid; - } - body { - background-color: white !important; - } -} \ No newline at end of file diff --git a/src/format/html/html_writer.rs b/src/format/html/html_writer.rs index e5ba6e6..1a518b3 100644 --- a/src/format/html/html_writer.rs +++ b/src/format/html/html_writer.rs @@ -1,14 +1,16 @@ +use crate::settings::style_settings::Theme; use std::io; use std::io::Write; pub struct HTMLWriter { inner: Box, + theme: Theme, } impl HTMLWriter { /// Creates a new writer - pub fn new(inner: Box) -> Self { - Self { inner } + pub fn new(inner: Box, theme: Theme) -> Self { + Self { inner, theme } } /// Writes a raw string @@ -30,4 +32,9 @@ impl HTMLWriter { pub fn flush(&mut self) -> io::Result<()> { self.inner.flush() } + + /// Return the theme of the html writer + pub fn get_theme(&mut self) -> Theme { + self.theme.clone() + } } diff --git a/src/format/html/to_html.rs b/src/format/html/to_html.rs index 28a6f95..464a590 100644 --- a/src/format/html/to_html.rs +++ b/src/format/html/to_html.rs @@ -1,16 +1,14 @@ use crate::elements::*; use crate::format::html::html_writer::HTMLWriter; +use crate::format::style::{get_code_theme_for_theme, get_css_for_theme}; use crate::format::PlaceholderTemplate; -use crate::references::configuration::keys::{INCLUDE_MATHJAX, META_LANG}; use crate::references::glossary::{GlossaryDisplay, GlossaryReference}; use crate::references::templates::{Template, TemplateVariable}; use asciimath_rs::format::mathml::ToMathML; use htmlescape::encode_attribute; use minify::html::minify; use std::io; -use syntect::highlighting::ThemeSet; use syntect::html::highlighted_html_for_string; -use syntect::parsing::SyntaxSet; const MATHJAX_URL: &str = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"; @@ -107,12 +105,9 @@ impl ToHtml for Document { }; if self.is_root { - let language = self - .config - .get_entry(META_LANG) - .map(|e| e.get().as_string()) - .unwrap_or("en".to_string()); - let style = minify(std::include_str!("assets/style.css")); + let language = self.config.lock().metadata.language.clone(); + + let style = minify(get_css_for_theme(writer.get_theme()).as_str()); writer.write("".to_string())?; writer.write("", MATHJAX_URL @@ -321,19 +311,19 @@ impl ToHtml for Cell { impl ToHtml for CodeBlock { fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { writer.write("
0 { writer.write(" lang=\"".to_string())?; writer.write_attribute(self.language.clone())?; writer.write("\">".to_string())?; - lazy_static::lazy_static! { static ref PS: SyntaxSet = SyntaxSet::load_defaults_nonewlines(); } - lazy_static::lazy_static! { static ref TS: ThemeSet = ThemeSet::load_defaults(); } + let (theme, syntax_set) = get_code_theme_for_theme(writer.get_theme()); - if let Some(syntax) = PS.find_syntax_by_token(self.language.as_str()) { + if let Some(syntax) = syntax_set.find_syntax_by_token(self.language.as_str()) { writer.write(highlighted_html_for_string( self.code.as_str(), - &PS, + &syntax_set, syntax, - &TS.themes["InspiredGitHub"], + &theme, ))?; } else { writer.write("
".to_string())?;
diff --git a/src/format/mod.rs b/src/format/mod.rs
index 168963b..6fc2726 100644
--- a/src/format/mod.rs
+++ b/src/format/mod.rs
@@ -4,6 +4,7 @@ use std::collections::HashMap;
 #[cfg(feature = "pdf")]
 pub mod chromium_pdf;
 pub mod html;
+pub mod style;
 
 pub struct PlaceholderTemplate {
     value: String,
diff --git a/src/format/style.rs b/src/format/style.rs
new file mode 100644
index 0000000..c73eb9c
--- /dev/null
+++ b/src/format/style.rs
@@ -0,0 +1,55 @@
+use crate::settings::style_settings::Theme;
+use std::time::Instant;
+use syntect::highlighting::ThemeSet;
+use syntect::parsing::SyntaxSet;
+
+/// Returns the css of a theme compiled from sass
+pub fn get_css_for_theme(theme: Theme) -> String {
+    let start = Instant::now();
+    let vars = match theme {
+        Theme::GitHub => include_str!("assets/light-github.scss"),
+        Theme::SolarizedDark => include_str!("assets/dark-solarized.scss"),
+        Theme::SolarizedLight => include_str!("assets/light-solarized.scss"),
+        Theme::OceanDark => include_str!("assets/dark-ocean.scss"),
+        Theme::OceanLight => include_str!("assets/light-ocean.scss"),
+        Theme::MagicDark => include_str!("assets/dark-magic.scss"),
+    };
+    let style = format!("{}\n{}", vars, include_str!("assets/base.scss"));
+
+    let css = compile_sass(&*style);
+
+    log::debug!("Compiled style in {} ms", start.elapsed().as_millis());
+
+    css
+}
+
+/// Returns the syntax theme for a given theme
+pub fn get_code_theme_for_theme(theme: Theme) -> (syntect::highlighting::Theme, SyntaxSet) {
+    lazy_static::lazy_static! { static ref PS: SyntaxSet = SyntaxSet::load_defaults_nonewlines(); }
+    lazy_static::lazy_static! { static ref TS: ThemeSet = ThemeSet::load_defaults(); }
+
+    let theme = match theme {
+        Theme::GitHub => "InspiredGitHub",
+        Theme::SolarizedDark => "Solarized (dark)",
+        Theme::SolarizedLight => "Solarized (light)",
+        Theme::OceanDark => "base16-ocean.dark",
+        Theme::OceanLight => "base16-ocean.light",
+        Theme::MagicDark => "base16-ocean.dark",
+    };
+
+    return (TS.themes[theme].clone(), PS.clone());
+}
+
+fn compile_sass(sass: &str) -> String {
+    String::from_utf8(
+        rsass::compile_scss(
+            sass.as_bytes(),
+            rsass::output::Format {
+                style: rsass::output::Style::Compressed,
+                precision: 5,
+            },
+        )
+        .unwrap(),
+    )
+    .unwrap()
+}
diff --git a/src/lib.rs b/src/lib.rs
index 81e8702..5f5aa8b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ pub mod elements;
 pub mod format;
 pub mod parser;
 pub mod references;
+pub mod settings;
 pub mod utils;
 
 pub use parser::Parser;
diff --git a/src/main.rs b/src/main.rs
index 288e5a9..ddb7d5c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ use snekdown::elements::Document;
 use snekdown::format::html::html_writer::HTMLWriter;
 use snekdown::format::html::to_html::ToHtml;
 use snekdown::parser::ParserOptions;
+use snekdown::settings::Settings;
 use snekdown::utils::caching::CacheStorage;
 use snekdown::Parser;
 use std::fs::{File, OpenOptions};
@@ -31,6 +32,9 @@ enum SubCommand {
     /// Parse and render the document.
     Render(RenderOptions),
 
+    /// Initializes the project with default settings
+    Init,
+
     /// Clears the cache directory
     ClearCache,
 }
@@ -94,6 +98,7 @@ fn main() {
             let cache = CacheStorage::new();
             cache.clear().expect("Failed to clear cache");
         }
+        SubCommand::Init => init(),
     };
 }
 
@@ -107,6 +112,31 @@ fn get_level_style(level: Level) -> colored::Color {
     }
 }
 
+fn init() {
+    let settings = Settings::default();
+    let settings_string = toml::to_string_pretty(&settings).unwrap();
+    let manifest_path = PathBuf::from("Manifest.toml");
+    let bibliography_path = PathBuf::from("Bibliography.toml");
+    let glossary_path = PathBuf::from("Glossary.toml");
+
+    if !manifest_path.exists() {
+        let mut file = OpenOptions::new()
+            .create(true)
+            .write(true)
+            .truncate(true)
+            .open("Manifest.toml")
+            .unwrap();
+        file.write_all(settings_string.as_bytes()).unwrap();
+        file.flush().unwrap();
+    }
+    if !bibliography_path.exists() {
+        File::create("Bibliography.toml".to_string()).unwrap();
+    }
+    if !glossary_path.exists() {
+        File::create("Glossary.toml".to_string()).unwrap();
+    }
+}
+
 /// Watches a file with all of its imports and renders on change
 fn watch(opt: &WatchOptions) {
     let parser = render(&opt.render_options);
@@ -177,7 +207,7 @@ fn render_format(opt: &RenderOptions, document: Document, writer: BufWriter) {
-    let mut writer = HTMLWriter::new(Box::new(writer));
+    let mut writer = HTMLWriter::new(Box::new(writer), document.config.lock().style.theme.clone());
     document.to_html(&mut writer).unwrap();
     writer.flush().unwrap();
 }
diff --git a/src/parser/block.rs b/src/parser/block.rs
index bcb0c1a..c2c5a40 100644
--- a/src/parser/block.rs
+++ b/src/parser/block.rs
@@ -1,7 +1,7 @@
 use super::ParseResult;
 use crate::elements::tokens::*;
 use crate::elements::{
-    Block, CodeBlock, Import, List, ListItem, MathBlock, Paragraph, Quote, Section, Table,
+    Block, CodeBlock, Import, List, ListItem, MathBlock, Metadata, Paragraph, Quote, Section, Table,
 };
 use crate::parser::inline::ParseInline;
 use crate::parser::line::ParseLine;
@@ -26,7 +26,7 @@ impl ParseBlock for Parser {
     fn parse_block(&mut self) -> ParseResult {
         if let Some(section) = self.section_return {
             if section <= self.section_nesting && (self.section_nesting > 0) {
-                return Err(self.ctm.assert_error(None));
+                return Err(self.ctm.assert_error(None).into());
             } else {
                 self.section_return = None;
             }
@@ -35,7 +35,7 @@ impl ParseBlock for Parser {
             log::trace!("Block::Section");
             Block::Section(section)
         } else if let Some(_) = self.section_return {
-            return Err(self.ctm.err());
+            return Err(self.ctm.err().into());
         } else if let Ok(list) = self.parse_list() {
             log::trace!("Block::List");
             Block::List(list)
@@ -60,7 +60,7 @@ impl ParseBlock for Parser {
                 Block::Null
             }
         } else if let Some(_) = self.section_return {
-            return Err(self.ctm.err());
+            return Err(self.ctm.err().into());
         } else if let Ok(pholder) = self.parse_placeholder() {
             log::trace!("Block::Placeholder");
             Block::Placeholder(pholder)
@@ -68,7 +68,7 @@ impl ParseBlock for Parser {
             log::trace!("Block::Paragraph");
             Block::Paragraph(paragraph)
         } else {
-            return Err(self.ctm.err());
+            return Err(self.ctm.err().into());
         };
 
         Ok(token)
@@ -78,6 +78,7 @@ impl ParseBlock for Parser {
     fn parse_section(&mut self) -> ParseResult
{ let start_index = self.ctm.get_index(); self.ctm.seek_whitespace(); + if self.ctm.check_char(&HASH) { let mut size = 1; while let Some(_) = self.ctm.next_char() { @@ -94,13 +95,15 @@ impl ParseBlock for Parser { if size <= self.section_nesting { self.section_return = Some(size); } - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } self.ctm.seek_any(&INLINE_WHITESPACE)?; let mut header = self.parse_header()?; header.size = size; self.section_nesting = size; self.sections.push(size); + self.section_anchors.push(header.anchor.clone()); + let mut section = Section::new(header); section.metadata = metadata; self.ctm.seek_whitespace(); @@ -110,6 +113,7 @@ impl ParseBlock for Parser { } self.sections.pop(); + self.section_anchors.pop(); if let Some(sec) = self.sections.last() { self.section_nesting = *sec } else { @@ -117,7 +121,7 @@ impl ParseBlock for Parser { } Ok(section) } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } @@ -167,7 +171,7 @@ impl ParseBlock for Parser { }; if self.ctm.check_char(&META_CLOSE) { if self.ctm.next_char() == None { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } let mut quote = Quote::new(metadata); @@ -186,7 +190,7 @@ impl ParseBlock for Parser { } } if quote.text.len() == 0 { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } Ok(quote) @@ -213,7 +217,7 @@ impl ParseBlock for Parser { if paragraph.elements.len() > 0 { Ok(paragraph) } else { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } } @@ -273,7 +277,7 @@ impl ParseBlock for Parser { if list.items.len() > 0 { Ok(list) } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } @@ -319,7 +323,7 @@ impl ParseBlock for Parser { path.push(character); } if self.ctm.check_char(&LB) || path.is_empty() { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } if self.ctm.check_char(&IMPORT_CLOSE) { self.ctm.seek_one()?; @@ -328,22 +332,20 @@ impl ParseBlock for Parser { if self.section_nesting > 0 { self.section_return = Some(0); - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } let metadata = self .parse_inline_metadata() .ok() - .map(|m| m.into()) + .map(|m| m.get_string_map()) .unwrap_or(HashMap::new()); - self.ctm.seek_whitespace(); - match self.import(path.clone(), &metadata) { ImportType::Document(Ok(anchor)) => Ok(Some(Import { path, anchor })), ImportType::Stylesheet(_) => Ok(None), ImportType::Bibliography(_) => Ok(None), ImportType::Manifest(_) => Ok(None), - _ => Err(self.ctm.err()), + _ => Err(self.ctm.err().into()), } } } diff --git a/src/parser/inline.rs b/src/parser/inline.rs index 39bd2d6..2665307 100644 --- a/src/parser/inline.rs +++ b/src/parser/inline.rs @@ -3,7 +3,6 @@ use crate::elements::tokens::*; use crate::elements::BibReference; use crate::elements::*; use crate::parser::block::ParseBlock; -use crate::references::configuration::keys::{BIB_REF_DISPLAY, SMART_ARROWS}; use crate::references::glossary::GlossaryDisplay; use crate::references::glossary::GlossaryReference; use crate::references::templates::{GetTemplateVariables, Template, TemplateVariable}; @@ -48,11 +47,12 @@ impl ParseInline for Parser { self.ctm.assert_char(surrounding, Some(start_index))?; self.ctm.seek_one()?; let mut inline = vec![self.parse_inline()?]; + while !self.ctm.check_char(surrounding) { if let Ok(result) = self.parse_inline() { inline.push(result) } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } if !self.ctm.check_eof() { @@ -71,10 +71,10 @@ impl ParseInline for Parser { } } if self.ctm.check_char(&PIPE) || self.ctm.check_char(&LB) { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } else if self.ctm.check_eof() { log::trace!("EOF"); - Err(self.ctm.err()) + Err(self.ctm.err().into()) } else if let Ok(image) = self.parse_image() { log::trace!("Inline::Image {:?}", image); Ok(Inline::Image(image)) @@ -160,7 +160,7 @@ impl ParseInline for Parser { image_data: pending_image, }) } else { - Err(self.ctm.rewind_with_error(start_index)) + Err(self.ctm.rewind_with_error(start_index).into()) } } @@ -185,7 +185,7 @@ impl ParseInline for Parser { self.inline_break_at.pop(); self.ctm.seek_one()?; } else if !short_syntax { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } self.ctm.assert_char(&URL_OPEN, Some(start_index))?; self.ctm.seek_one()?; @@ -218,7 +218,7 @@ impl ParseInline for Parser { } else if self.ctm.check_char(&SPACE) { false } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); }; self.ctm.seek_one()?; self.ctm.assert_char(&CHECK_CLOSE, Some(start_index))?; @@ -233,11 +233,12 @@ impl ParseInline for Parser { self.ctm.assert_sequence(&BOLD, Some(start_index))?; self.ctm.seek_one()?; let mut inline = vec![self.parse_inline()?]; + while !self.ctm.check_sequence(&BOLD) { if let Ok(result) = self.parse_inline() { inline.push(result); } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } self.ctm.seek_one()?; @@ -261,12 +262,12 @@ impl ParseInline for Parser { if let Ok(result) = self.parse_inline() { inline.push(result); } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } self.ctm.rewind(self.ctm.get_index() - STRIKED.len()); if self.ctm.check_any(WHITESPACE) { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } for _ in 0..(STRIKED.len() + 1) { self.ctm.seek_one()?; @@ -330,7 +331,7 @@ impl ParseInline for Parser { name, }) } else { - Err(self.ctm.rewind_with_error(start_index)) + Err(self.ctm.rewind_with_error(start_index).into()) } } @@ -347,7 +348,7 @@ impl ParseInline for Parser { )?; self.ctm.seek_one()?; if color.is_empty() { - return Err(self.ctm.err()); + return Err(self.ctm.err().into()); } Ok(Colored { value: Box::new(self.parse_inline()?), @@ -367,7 +368,15 @@ impl ParseInline for Parser { let bib_ref = BibRef::new(key.clone()); let ref_entry = Arc::new(RwLock::new(BibReference::new( key, - self.options.document.config.get_ref_entry(BIB_REF_DISPLAY), + Some( + self.options + .document + .config + .lock() + .style + .bib_ref_display + .clone(), + ), bib_ref.anchor(), ))); self.options @@ -422,7 +431,7 @@ impl ParseInline for Parser { self.ctm .get_string_until_any_or_rewind(&WHITESPACE, &[TILDE], start_index)?; if key.is_empty() { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } while !key.is_empty() && !key.chars().last().unwrap().is_alphabetic() { self.ctm.rewind(self.ctm.get_index() - 1); @@ -441,7 +450,7 @@ impl ParseInline for Parser { /// parses plain text as a string until it encounters an unescaped special inline char fn parse_plain(&mut self) -> ParseResult { if self.ctm.check_char(&LB) { - return Err(self.ctm.err()); + return Err(self.ctm.err().into()); } let mut characters = String::new(); if !self.ctm.check_char(&SPECIAL_ESCAPE) { @@ -466,7 +475,7 @@ impl ParseInline for Parser { if characters.len() > 0 { Ok(PlainText { value: characters }) } else { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } } @@ -490,7 +499,7 @@ impl ParseInline for Parser { if values.len() == 0 { // if there was a linebreak (the metadata wasn't closed) or there is no inner data // return an error - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } Ok(InlineMetadata { data: values }) @@ -566,7 +575,7 @@ impl ParseInline for Parser { { name_str } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); }; if !self.ctm.check_eof() { self.ctm.seek_one()?; @@ -590,7 +599,7 @@ impl ParseInline for Parser { self.ctm.seek_one()?; if self.ctm.check_char(&TEMPLATE) { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } let mut elements = Vec::new(); @@ -645,15 +654,8 @@ impl ParseInline for Parser { /// Parses an arrow fn parse_arrow(&mut self) -> ParseResult<Arrow> { - if !self - .options - .document - .config - .get_entry(SMART_ARROWS) - .and_then(|e| e.get().as_bool()) - .unwrap_or(true) - { - Err(self.ctm.err()) + if !self.options.document.config.lock().features.smart_arrows { + Err(self.ctm.err().into()) } else if self.ctm.check_sequence(A_LEFT_RIGHT_ARROW) { self.ctm.seek_one()?; Ok(Arrow::LeftRightArrow) @@ -673,7 +675,7 @@ impl ParseInline for Parser { self.ctm.seek_one()?; Ok(Arrow::BigLeftArrow) } else { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } } } diff --git a/src/parser/line.rs b/src/parser/line.rs index 0a50eb2..13cdeef 100644 --- a/src/parser/line.rs +++ b/src/parser/line.rs @@ -25,7 +25,7 @@ impl ParseLine for Parser { fn parse_line(&mut self) -> ParseResult<Line> { if self.ctm.check_eof() { log::trace!("EOF"); - Err(self.ctm.err()) + Err(self.ctm.err().into()) } else { if let Ok(ruler) = self.parse_ruler() { log::trace!("Line::Ruler"); @@ -40,7 +40,7 @@ impl ParseLine for Parser { log::trace!("Line::Text"); Ok(Line::Text(text)) } else { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } } } @@ -53,6 +53,9 @@ impl ParseLine for Parser { self.ctm.get_text()[start_index..self.ctm.get_index()] .iter() .for_each(|e| anchor.push(*e)); + if let Some(last) = self.section_anchors.last() { + anchor = format!("{}-{}", last, anchor); + } anchor.retain(|c| !c.is_whitespace()); log::trace!("Line::Header"); Ok(Header::new(line, anchor)) @@ -76,11 +79,11 @@ impl ParseLine for Parser { } if !self.ctm.check_any(&INLINE_WHITESPACE) { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } self.ctm.seek_any(&INLINE_WHITESPACE)?; if self.ctm.check_char(&MINUS) { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } let item = ListItem::new(self.parse_line()?, level as u16, ordered); @@ -96,7 +99,7 @@ impl ParseLine for Parser { self.ctm.assert_char(&PIPE, Some(start_index))?; self.ctm.seek_one()?; if self.ctm.check_char(&PIPE) { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } self.inline_break_at.push(PIPE); @@ -134,7 +137,7 @@ impl ParseLine for Parser { log::trace!("Line::TableRow"); Ok(row) } else { - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } @@ -181,7 +184,7 @@ impl ParseLine for Parser { if text.subtext.len() > 0 { Ok(text) } else { - Err(self.ctm.err()) + Err(self.ctm.err().into()) } } @@ -211,7 +214,7 @@ impl ParseLine for Parser { msg, self.get_position_string() ); - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } } else { @@ -232,7 +235,7 @@ impl ParseLine for Parser { msg, self.get_position_string() ); - return Err(self.ctm.rewind_with_error(start_index)); + return Err(self.ctm.rewind_with_error(start_index).into()); } } }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7085440..a05d390 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,31 +5,54 @@ pub(crate) mod line; use self::block::ParseBlock; use crate::elements::tokens::LB; use crate::elements::{Document, ImportAnchor}; -use crate::references::configuration::keys::{ - IMP_BIBLIOGRAPHY, IMP_CONFIGS, IMP_GLOSSARY, IMP_IGNORE, IMP_STYLESHEETS, -}; -use crate::references::configuration::Value; -use charred::tapemachine::{CharTapeMachine, TapeError, TapeResult}; +use crate::settings::SettingsError; +use charred::tapemachine::{CharTapeMachine, TapeError}; use crossbeam_utils::sync::WaitGroup; use regex::Regex; use std::collections::HashMap; +use std::fmt; use std::fs::{read_to_string, File}; -use std::io::BufReader; +use std::io::{self, BufReader}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::thread; -pub type ParseResult<T> = TapeResult<T>; -pub type ParseError = TapeError; +pub type ParseResult<T> = Result<T, ParseError>; -const DEFAULT_IMPORTS: &'static [(&str, &str)] = &[ - ("snekdown.toml", "manifest"), - ("manifest.toml", "manifest"), - ("bibliography.toml", "bibliography"), - ("bibliography2.bib.toml", "bibliography"), - ("glossary.toml", "glossary"), - ("style.css", "stylesheet"), -]; +#[derive(Debug)] +pub enum ParseError { + TapeError(TapeError), + SettingsError(SettingsError), + IoError(io::Error), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::TapeError(e) => write!(f, "{}", e), + ParseError::SettingsError(e) => write!(f, "{}", e), + ParseError::IoError(e) => write!(f, "IO Error: {}", e), + } + } +} + +impl From<TapeError> for ParseError { + fn from(e: TapeError) -> Self { + Self::TapeError(e) + } +} + +impl From<SettingsError> for ParseError { + fn from(e: SettingsError) -> Self { + Self::SettingsError(e) + } +} + +impl From<io::Error> for ParseError { + fn from(e: io::Error) -> Self { + Self::IoError(e) + } +} #[derive(Clone, Debug)] pub struct ParserOptions { @@ -65,6 +88,7 @@ pub struct Parser { pub(crate) ctm: CharTapeMachine, section_nesting: u8, sections: Vec<u8>, + section_anchors: Vec<String>, section_return: Option<u8>, wg: WaitGroup, pub(crate) block_break_at: Vec<char>, @@ -88,6 +112,7 @@ impl Parser { Self { options, sections: Vec::new(), + section_anchors: Vec::new(), section_nesting: 0, section_return: None, wg: WaitGroup::new(), @@ -117,7 +142,7 @@ impl Parser { text_unil.reverse(); let mut inline_pos = 0; - while text_unil[inline_pos] != LB { + while inline_pos < text_unil.len() && text_unil[inline_pos] != LB { inline_pos += 1; } if let Some(path) = &self.options.path { @@ -150,7 +175,7 @@ impl Parser { path.to_str().unwrap(), self.get_position_string(), ); - return Err(self.ctm.assert_error(None)); + return Err(self.ctm.assert_error(None).into()); } let anchor = Arc::new(RwLock::new(ImportAnchor::new())); let anchor_clone = Arc::clone(&anchor); @@ -181,7 +206,7 @@ impl Parser { /// Returns the text of an imported text file fn import_text_file(&self, path: PathBuf) -> ParseResult<String> { - read_to_string(path).map_err(|_| self.ctm.err()) + read_to_string(path).map_err(ParseError::from) } fn import_stylesheet(&mut self, path: PathBuf) -> ParseResult<()> { @@ -197,13 +222,12 @@ impl Parser { } fn import_manifest(&mut self, path: PathBuf) -> ParseResult<()> { - let contents = self.import_text_file(path)?; - let value = contents - .parse::<toml::Value>() - .map_err(|_| self.ctm.err())?; - self.options.document.config.set_from_toml(&value); - - Ok(()) + self.options + .document + .config + .lock() + .merge(path) + .map_err(ParseError::from) } /// Imports a glossary @@ -223,7 +247,7 @@ impl Parser { } /// Imports a path - fn import(&mut self, path: String, args: &HashMap<String, Value>) -> ImportType { + fn import(&mut self, path: String, args: &HashMap<String, String>) -> ImportType { log::debug!( "Importing file {}\n\t--> {}\n", path, @@ -242,20 +266,9 @@ impl Parser { .file_name() .and_then(|f| Some(f.to_str().unwrap().to_string())) { - if let Some(Value::Array(ignore)) = self - .options - .document - .config - .get_entry(IMP_IGNORE) - .and_then(|e| Some(e.get().clone())) - { - let ignore = ignore - .iter() - .map(|v| v.as_string()) - .collect::<Vec<String>>(); - if ignore.contains(&fname) { - return ImportType::None; - } + let ignore = &self.options.document.config.lock().imports.ignored_imports; + if ignore.contains(&fname) { + return ImportType::None; } } { @@ -270,7 +283,7 @@ impl Parser { } paths.push(path.clone()); } - match args.get("type").map(|e| e.as_string().to_lowercase()) { + match args.get("type").cloned() { Some(s) if s == "stylesheet".to_string() => { ImportType::Stylesheet(self.import_stylesheet(path)) } @@ -328,14 +341,10 @@ impl Parser { let wg = self.wg.clone(); self.wg = WaitGroup::new(); if !self.options.is_child { - for (path, file_type) in DEFAULT_IMPORTS { - if self.transform_path(path.to_string()).exists() { - self.import( - path.to_string(), - &maplit::hashmap! {"type".to_string() => Value::String(file_type.to_string())}, - ); - } - } + self.import( + "Manifest.toml".to_string(), + &maplit::hashmap! {"type".to_string() => "manifest".to_string()}, + ); } wg.wait(); if !self.options.is_child { @@ -353,57 +362,25 @@ impl Parser { /// Imports files from the configs import values fn import_from_config(&mut self) { - if let Some(Value::Array(mut imp)) = self - .options - .document - .config - .get_entry(IMP_STYLESHEETS) - .and_then(|e| Some(e.get().clone())) - { - let args = - maplit::hashmap! {"type".to_string() => Value::String("stylesheet".to_string())}; - while let Some(Value::String(s)) = imp.pop() { - self.import(s, &args); - } - } - if let Some(Value::Array(mut imp)) = self - .options - .document - .config - .get_entry(IMP_CONFIGS) - .and_then(|e| Some(e.get().clone())) - { - let args = maplit::hashmap! {"type".to_string() => Value::String("config".to_string())}; - while let Some(Value::String(s)) = imp.pop() { - self.import(s, &args); - } + let config = Arc::clone(&self.options.document.config); + + let mut stylesheets = config.lock().imports.included_stylesheets.clone(); + let args = maplit::hashmap! {"type".to_string() => "stylesheet".to_string()}; + while let Some(s) = stylesheets.pop() { + self.import(s, &args); } - if let Some(Value::Array(mut imp)) = self - .options - .document - .config - .get_entry(IMP_BIBLIOGRAPHY) - .and_then(|e| Some(e.get().clone())) - { - let args = - maplit::hashmap! {"type".to_string() => Value::String("bibliography".to_string())}; - while let Some(Value::String(s)) = imp.pop() { - self.import(s, &args); - } + + let mut bibliography = config.lock().imports.included_bibliography.clone(); + let args = maplit::hashmap! {"type".to_string() => "bibliography".to_string()}; + while let Some(s) = bibliography.pop() { + self.import(s, &args); } - if let Some(Value::Array(mut imp)) = self - .options - .document - .config - .get_entry(IMP_GLOSSARY) - .and_then(|e| Some(e.get().clone())) - { - let args = - maplit::hashmap! {"type".to_string() => Value::String("glossary".to_string())}; - while let Some(Value::String(s)) = imp.pop() { - self.import(s, &args); - } + let mut glossaries = config.lock().imports.included_glossaries.clone(); + + let args = maplit::hashmap! {"type".to_string() =>"glossary".to_string()}; + while let Some(s) = glossaries.pop() { + self.import(s, &args); } } } diff --git a/src/references/configuration/keys.rs b/src/references/configuration/keys.rs deleted file mode 100644 index cce5760..0000000 --- a/src/references/configuration/keys.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![allow(unused)] - -pub const BIB_REF_DISPLAY: &str = "bib-ref-display"; -pub const META_LANG: &str = "language"; - -// import and include options -pub const IMP_IGNORE: &str = "ignored-imports"; -pub const IMP_STYLESHEETS: &str = "included-stylesheets"; -pub const IMP_CONFIGS: &str = "included-configs"; -pub const IMP_BIBLIOGRAPHY: &str = "included-bibliography"; -pub const IMP_GLOSSARY: &str = "included-glossary"; -pub const EMBED_EXTERNAL: &str = "embed-external"; -pub const SMART_ARROWS: &str = "smart-arrows"; -pub const INCLUDE_MATHJAX: &str = "include-math-jax"; - -// PDF options -pub const PDF_DISPLAY_HEADER_FOOTER: &str = "pfd-display-header-footer"; -pub const PDF_HEADER_TEMPLATE: &str = "pdf-header-template"; -pub const PDF_FOOTER_TEMPLATE: &str = "pdf-footer-template"; -pub const PDF_MARGIN_TOP: &str = "pdf-margin-top"; -pub const PDF_MARGIN_BOTTOM: &str = "pdf-margin-bottom"; -pub const PDF_MARGIN_LEFT: &str = "pdf-margin-left"; -pub const PDF_MARGIN_RIGHT: &str = "pdf-margin-right"; -pub const PDF_PAGE_HEIGHT: &str = "pdf-page-height"; -pub const PDF_PAGE_WIDTH: &str = "pdf-page-width"; -pub const PDF_PAGE_SCALE: &str = "pdf-page-scale"; - -// Image Options -pub const IMAGE_FORMAT: &str = "image-format"; -pub const IMAGE_MAX_WIDTH: &str = "image-max-width"; -pub const IMAGE_MAX_HEIGHT: &str = "image-max-height"; diff --git a/src/references/configuration/mod.rs b/src/references/configuration/mod.rs deleted file mode 100644 index 335f5cc..0000000 --- a/src/references/configuration/mod.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::elements::MetadataValue; -use crate::references::configuration::keys::{ - BIB_REF_DISPLAY, META_LANG, PDF_DISPLAY_HEADER_FOOTER, PDF_FOOTER_TEMPLATE, - PDF_HEADER_TEMPLATE, PDF_MARGIN_BOTTOM, PDF_MARGIN_TOP, -}; -use crate::references::templates::Template; -use serde::export::TryFrom; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - -pub(crate) mod keys; - -#[derive(Clone, Debug)] -pub enum Value { - String(String), - Bool(bool), - Float(f64), - Integer(i64), - Template(Template), - Array(Vec<Value>), -} - -#[derive(Clone, Debug)] -pub struct ConfigEntry { - inner: Value, -} - -pub type ConfigRefEntry = Arc<RwLock<ConfigEntry>>; - -#[derive(Clone, Debug)] -pub struct Configuration { - config: Arc<RwLock<HashMap<String, ConfigRefEntry>>>, -} - -impl Value { - pub fn as_string(&self) -> String { - match self { - Value::String(string) => string.clone(), - Value::Integer(int) => format!("{}", int), - Value::Float(f) => format!("{:02}", f), - Value::Bool(b) => format!("{}", b), - Value::Array(a) => a.iter().fold("".to_string(), |a, b| { - format!("{} \"{}\"", a, b.as_string()) - }), - _ => "".to_string(), - } - } - - /// Returns the bool value if the value is a boolean - pub fn as_bool(&self) -> Option<bool> { - match self { - Value::Bool(b) => Some(*b), - _ => None, - } - } - - pub fn as_float(&self) -> Option<f64> { - match self { - Value::Float(v) => Some(*v), - _ => None, - } - } -} - -impl ConfigEntry { - pub fn new(value: Value) -> Self { - Self { inner: value } - } - - pub fn set(&mut self, value: Value) { - self.inner = value; - } - - pub fn get(&self) -> &Value { - &self.inner - } -} - -impl Default for Configuration { - fn default() -> Self { - let mut self_config = Self::new(); - self_config.set(BIB_REF_DISPLAY, Value::String("{{number}}".to_string())); - self_config.set(META_LANG, Value::String("en".to_string())); - self_config.set(PDF_MARGIN_BOTTOM, Value::Float(0.5)); - self_config.set(PDF_MARGIN_TOP, Value::Float(0.5)); - self_config.set(PDF_DISPLAY_HEADER_FOOTER, Value::Bool(true)); - self_config.set( - PDF_HEADER_TEMPLATE, - Value::String("<div></div>".to_string()), - ); - self_config.set( - PDF_FOOTER_TEMPLATE, - Value::String( - include_str!("../../format/chromium_pdf/assets/default-footer-template.html") - .to_string(), - ), - ); - - self_config - } -} - -impl Configuration { - pub fn new() -> Self { - Self { - config: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// returns the value of a config entry - pub fn get_entry(&self, key: &str) -> Option<ConfigEntry> { - let config = self.config.read().unwrap(); - if let Some(entry) = config.get(key) { - let value = entry.read().unwrap(); - Some(value.clone()) - } else { - None - } - } - - /// returns a config entry that is a reference to a value - pub fn get_ref_entry(&self, key: &str) -> Option<ConfigRefEntry> { - let config = self.config.read().unwrap(); - if let Some(entry) = config.get(&key.to_string()) { - Some(Arc::clone(entry)) - } else { - None - } - } - - /// Sets a config parameter - pub fn set(&mut self, key: &str, value: Value) { - let mut config = self.config.write().unwrap(); - if let Some(entry) = config.get(&key.to_string()) { - entry.write().unwrap().set(value) - } else { - config.insert( - key.to_string(), - Arc::new(RwLock::new(ConfigEntry::new(value))), - ); - } - } - - /// Sets a config value based on a metadata value - pub fn set_from_meta(&mut self, key: &str, value: MetadataValue) { - match value { - MetadataValue::String(string) => self.set(key, Value::String(string)), - MetadataValue::Bool(bool) => self.set(key, Value::Bool(bool)), - MetadataValue::Float(f) => self.set(key, Value::Float(f)), - MetadataValue::Integer(i) => self.set(key, Value::Integer(i)), - MetadataValue::Template(t) => self.set(key, Value::Template(t)), - _ => {} - } - } - - pub fn set_from_toml(&mut self, value: &toml::Value) -> Option<()> { - let table = value.as_table().cloned()?; - table.iter().for_each(|(k, v)| { - match v { - toml::Value::Table(_) => self.set_from_toml(v).unwrap_or(()), - _ => self.set(k, Value::try_from(v.clone()).unwrap()), - }; - }); - - Some(()) - } -} - -impl TryFrom<toml::Value> for Value { - type Error = (); - - fn try_from(value: toml::Value) -> Result<Self, Self::Error> { - match value { - toml::Value::Table(_) => Err(()), - toml::Value::Float(f) => Ok(Value::Float(f)), - toml::Value::Integer(i) => Ok(Value::Integer(i)), - toml::Value::String(s) => Ok(Value::String(s)), - toml::Value::Boolean(b) => Ok(Value::Bool(b)), - toml::Value::Datetime(dt) => Ok(Value::String(dt.to_string())), - toml::Value::Array(a) => Ok(Value::Array( - a.iter() - .cloned() - .filter_map(|e| Value::try_from(e).ok()) - .collect::<Vec<Value>>(), - )), - } - } -} diff --git a/src/references/mod.rs b/src/references/mod.rs index 2ebfb66..33b22fe 100644 --- a/src/references/mod.rs +++ b/src/references/mod.rs @@ -1,5 +1,4 @@ pub mod bibliography; -pub mod configuration; pub mod glossary; pub mod placeholders; pub mod templates; diff --git a/src/references/placeholders.rs b/src/references/placeholders.rs index f9c0be6..14f8950 100644 --- a/src/references/placeholders.rs +++ b/src/references/placeholders.rs @@ -35,6 +35,8 @@ const P_GLS: &str = "gls"; const P_DATE: &str = "date"; const P_TIME: &str = "time"; const P_DATETIME: &str = "datetime"; +const P_AUTHOR: &str = "author"; +const P_TITLE: &str = "title"; impl ProcessPlaceholders for Document { /// parses all placeholders and assigns values to them @@ -65,10 +67,24 @@ impl ProcessPlaceholders for Document { P_DATETIME => pholder.set_value(inline!(Inline::Plain(PlainText { value: format!("{} {}", get_date_string(), get_time_string()) }))), + P_AUTHOR => { + if let Some(value) = self.config.lock().metadata.author.clone() { + pholder.set_value(inline!(Inline::Plain(PlainText { value }))) + } + } + P_TITLE => { + if let Some(value) = self.config.lock().metadata.title.clone() { + pholder.set_value(inline!(Inline::Plain(PlainText { value }))) + } + } _ => { - if let Some(entry) = self.config.get_entry(pholder.name.to_lowercase().as_str()) + if let Some(value) = self + .config + .lock() + .custom_attributes + .get(pholder.name.to_lowercase().as_str()) + .cloned() { - let value = entry.get().as_string(); pholder.set_value(inline!(Inline::Plain(PlainText { value }))) } } @@ -94,7 +110,7 @@ impl ProcessPlaceholders for Document { }))); if let Some(meta) = &pholder.metadata { if let Some(value) = meta.data.get(S_VALUE) { - self.config.set_from_meta(key, value.clone()) + self.config.lock().set_from_meta(key, value.clone()) } } } diff --git a/src/settings/feature_settings.rs b/src/settings/feature_settings.rs new file mode 100644 index 0000000..aa38da7 --- /dev/null +++ b/src/settings/feature_settings.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FeatureSettings { + pub embed_external: bool, + pub smart_arrows: bool, + pub include_mathjax: bool, +} + +impl Default for FeatureSettings { + fn default() -> Self { + Self { + embed_external: true, + smart_arrows: true, + include_mathjax: true, + } + } +} diff --git a/src/settings/image_settings.rs b/src/settings/image_settings.rs new file mode 100644 index 0000000..4e836d8 --- /dev/null +++ b/src/settings/image_settings.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ImageSettings { + pub format: Option<String>, + pub max_width: Option<u32>, + pub max_height: Option<u32>, +} + +impl Default for ImageSettings { + fn default() -> Self { + Self { + format: None, + max_height: None, + max_width: None, + } + } +} diff --git a/src/settings/import_settings.rs b/src/settings/import_settings.rs new file mode 100644 index 0000000..32e24c8 --- /dev/null +++ b/src/settings/import_settings.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ImportSettings { + pub ignored_imports: Vec<String>, + pub included_stylesheets: Vec<String>, + pub included_bibliography: Vec<String>, + pub included_glossaries: Vec<String>, +} + +impl Default for ImportSettings { + fn default() -> Self { + Self { + ignored_imports: Vec::with_capacity(0), + included_stylesheets: vec!["style.css".to_string()], + included_bibliography: vec!["Bibliography.toml".to_string()], + included_glossaries: vec!["Glossary.toml".to_string()], + } + } +} diff --git a/src/settings/metadata_settings.rs b/src/settings/metadata_settings.rs new file mode 100644 index 0000000..386abae --- /dev/null +++ b/src/settings/metadata_settings.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MetadataSettings { + pub title: Option<String>, + pub author: Option<String>, + pub language: String, +} + +impl Default for MetadataSettings { + fn default() -> Self { + Self { + title: None, + author: None, + language: "en".to_string(), + } + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..a76cec0 --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,125 @@ +use crate::elements::{Metadata, MetadataValue}; +use crate::settings::feature_settings::FeatureSettings; +use crate::settings::image_settings::ImageSettings; +use crate::settings::import_settings::ImportSettings; +use crate::settings::metadata_settings::MetadataSettings; +use crate::settings::pdf_settings::PDFSettings; +use crate::settings::style_settings::StyleSettings; +use config::{ConfigError, Source}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{self, Display}; +use std::io; +use std::mem; +use std::path::PathBuf; + +pub mod feature_settings; +pub mod image_settings; +pub mod import_settings; +pub mod metadata_settings; +pub mod pdf_settings; +pub mod style_settings; + +pub type SettingsResult<T> = Result<T, SettingsError>; + +#[derive(Debug)] +pub enum SettingsError { + IoError(io::Error), + ConfigError(ConfigError), + TomlError(toml::ser::Error), +} + +impl Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::IoError(e) => write!(f, "IO Error: {}", e), + Self::ConfigError(e) => write!(f, "Config Error: {}", e), + Self::TomlError(e) => write!(f, "Toml Error: {}", e), + } + } +} + +impl Error for SettingsError {} + +impl From<io::Error> for SettingsError { + fn from(e: io::Error) -> Self { + Self::IoError(e) + } +} + +impl From<ConfigError> for SettingsError { + fn from(e: ConfigError) -> Self { + Self::ConfigError(e) + } +} + +impl From<toml::ser::Error> for SettingsError { + fn from(e: toml::ser::Error) -> Self { + Self::TomlError(e) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct Settings { + pub metadata: MetadataSettings, + pub features: FeatureSettings, + pub imports: ImportSettings, + pub pdf: PDFSettings, + pub images: ImageSettings, + pub style: StyleSettings, + pub custom_attributes: HashMap<String, String>, +} + +impl Source for Settings { + fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> { + Box::new(self.clone()) + } + + fn collect(&self) -> Result<HashMap<String, config::Value>, config::ConfigError> { + let source_str = + toml::to_string(&self).map_err(|e| config::ConfigError::Foreign(Box::new(e)))?; + let result = toml::de::from_str(&source_str) + .map_err(|e| config::ConfigError::Foreign(Box::new(e)))?; + + Ok(result) + } +} + +impl Settings { + /// Loads the settings from the specified path + pub fn load(path: PathBuf) -> SettingsResult<Self> { + let mut settings = config::Config::default(); + settings + .merge(Self::default())? + .merge(config::File::from(path))?; + let settings: Self = settings.try_into()?; + + Ok(settings) + } + + /// Merges the current settings with the settings from the given path + /// returning updated settings + pub fn merge(&mut self, path: PathBuf) -> SettingsResult<()> { + let mut settings = config::Config::default(); + settings + .merge(self.clone())? + .merge(config::File::from(path))?; + let mut settings: Self = settings.try_into()?; + mem::swap(self, &mut settings); // replace the old settings with the new ones + + Ok(()) + } + + pub fn append_metadata<M: Metadata>(&mut self, metadata: M) { + let entries = metadata.get_string_map(); + for (key, value) in entries { + self.custom_attributes.insert(key, value); + } + } + + pub fn set_from_meta(&mut self, key: &str, value: MetadataValue) { + self.custom_attributes + .insert(key.to_string(), value.to_string()); + } +} diff --git a/src/settings/pdf_settings.rs b/src/settings/pdf_settings.rs new file mode 100644 index 0000000..de7fe9b --- /dev/null +++ b/src/settings/pdf_settings.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PDFSettings { + pub display_header_footer: bool, + pub header_template: Option<String>, + pub footer_template: Option<String>, + pub page_height: Option<f32>, + pub page_width: Option<f32>, + pub page_scale: f32, + pub margin: PDFMarginSettings, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PDFMarginSettings { + pub top: Option<f32>, + pub bottom: Option<f32>, + pub left: Option<f32>, + pub right: Option<f32>, +} + +impl Default for PDFMarginSettings { + fn default() -> Self { + Self { + top: Some(0.5), + bottom: Some(0.5), + left: None, + right: None, + } + } +} + +impl Default for PDFSettings { + fn default() -> Self { + Self { + display_header_footer: true, + header_template: Some("<div></div>".to_string()), + footer_template: Some( + include_str!("../format/chromium_pdf/assets/default-footer-template.html") + .to_string(), + ), + page_height: None, + page_width: None, + page_scale: 1.0, + margin: Default::default(), + } + } +} diff --git a/src/settings/style_settings.rs b/src/settings/style_settings.rs new file mode 100644 index 0000000..c5b43d9 --- /dev/null +++ b/src/settings/style_settings.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct StyleSettings { + pub bib_ref_display: String, + pub theme: Theme, +} + +impl Default for StyleSettings { + fn default() -> Self { + Self { + bib_ref_display: "{{number}}".to_string(), + theme: Theme::GitHub, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum Theme { + GitHub, + SolarizedDark, + SolarizedLight, + OceanDark, + OceanLight, + MagicDark, +}