minimad-0.13.0/.cargo_vcs_info.json0000644000000001360000000000100125330ustar { "git": { "sha1": "54b77371d63a6eda9eaee3e21b9634a08b86d440" }, "path_in_vcs": "" }minimad-0.13.0/.gitignore000064400000000000000000000000571046102023000133150ustar 00000000000000.bacon-locations /target **/*.rs.bk Cargo.lock minimad-0.13.0/Cargo.toml0000644000000016200000000000100105300ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "minimad" version = "0.13.0" authors = ["dystroy "] description = "light Markdown parser" readme = "README.md" keywords = [ "markdown", "parser", "template", ] categories = [ "gui", "parser-implementations", ] license = "MIT" repository = "https://github.com/Canop/minimad" [dependencies.once_cell] version = "1.17" [features] default = ["escaping"] escaping = [] minimad-0.13.0/Cargo.toml.orig000064400000000000000000000006331046102023000142140ustar 00000000000000[package] name = "minimad" version = "0.13.0" authors = ["dystroy "] repository = "https://github.com/Canop/minimad" description = "light Markdown parser" edition = "2018" keywords = ["markdown", "parser", "template"] license = "MIT" categories = ["gui", "parser-implementations"] readme = "README.md" [dependencies] once_cell = "1.17" [features] escaping = [] default = ["escaping"] minimad-0.13.0/LICENSE000064400000000000000000000020461046102023000123320ustar 00000000000000MIT License Copyright (c) 2019 Canop Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. minimad-0.13.0/README.md000064400000000000000000000033441046102023000126060ustar 00000000000000 [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/minimad.svg [l1]: https://crates.io/crates/minimad [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: minimad/LICENSE [s3]: https://docs.rs/minimad/badge.svg [l3]: https://docs.rs/minimad/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3 A simple, non universal purpose, markdown parser. If you're looking for a Markdown parser, this one is probably *not* the one you want: Minimad can be used on its own but is first designed for the [termimad](https://github.com/Canop/termimad) lib, which displays static and dynamic markdown snippets on a terminal without mixing the skin with the code. Minimad sports a line-oriented flat structure (i.e. not a tree) which might not suit your needs. If you still think you might use Minimad directly (not through Temimad), you may contact me on Miaou for advice. ### Usage ```toml [dependencies] minimad = "0.7" ``` ```rust assert_eq!( Line::from("## a header with some **bold**!"), Line::new_header( 2, vec![ Compound::raw_str("a header with some "), Compound::raw_str("bold").bold(), Compound::raw_str("!"), ] ) ); assert_eq!( Line::from("Hello ~~wolrd~~ **World**. *Code*: `sqrt(π/2)`"), Line::new_paragraph(vec![ Compound::raw_str("Hello "), Compound::raw_str("wolrd").strikeout(), Compound::raw_str(" "), Compound::raw_str("World").bold(), Compound::raw_str(". "), Compound::raw_str("Code").italic(), Compound::raw_str(": "), Compound::raw_str("sqrt(π/2)").code(), ]) ); ``` minimad-0.13.0/fmt.sh000075500000000000000000000000671046102023000124530ustar 00000000000000rustup default nightly cargo fmt rustup default stable minimad-0.13.0/rustfmt.toml000064400000000000000000000001311046102023000137170ustar 00000000000000edition = "2021" version = "Two" imports_layout = "Vertical" fn_args_layout = "Vertical" minimad-0.13.0/src/clean.rs000064400000000000000000000036001046102023000135410ustar 00000000000000pub fn is_blank(s: &str) -> bool { s.chars().all(char::is_whitespace) } /// Remove the superfluous lines and indentations you get when you insert /// in your code a multi-line raw literal. /// ``` /// let lines = minimad::clean::lines( /// r#" /// test /// hop /// hip /// "#, /// ); /// assert_eq!(lines.len(), 3); /// assert_eq!(lines[0], "test"); /// assert_eq!(lines[1], " hop"); /// assert_eq!(lines[2], "hip"); /// ``` pub fn lines(src: &str) -> Vec<&str> { let mut result_lines: Vec<&str> = Vec::new(); let mut src_lines = src.lines(); if let Some(mut first_line) = src_lines.next() { if first_line.is_empty() { if let Some(s) = src_lines.next() { first_line = s; } } result_lines.push(first_line); for line in src_lines { result_lines.push(line); } if is_blank(result_lines[result_lines.len() - 1]) { result_lines.truncate(result_lines.len() - 1); } if result_lines.len() > 1 { let mut white_prefix = String::new(); for char in first_line.chars() { if char.is_whitespace() { white_prefix.push(char); } else { break; } } if !white_prefix.is_empty() && result_lines .iter() .all(|line| line.starts_with(&white_prefix) || is_blank(line)) { result_lines = result_lines .iter() .map(|line| { if is_blank(line) { line } else { &line[white_prefix.len()..] } }) .collect(); } } } result_lines } minimad-0.13.0/src/lib.rs000064400000000000000000000046331046102023000132340ustar 00000000000000/*! This crate provides a *very* simple markdown parser. It's the basis of the [termimad](https://github.com/Canop/termimad) lib, which displays static and dynamic markdown snippets on a terminal without mixing the skin with the code and wrapping the text and tables as needed. It can be used on its own: ```rust use minimad::*; assert_eq!( parse_line("## a header with some **bold**!"), Line::new_header( 2, vec![ Compound::raw_str("a header with some "), Compound::raw_str("bold").bold(), Compound::raw_str("!"), ] ) ); assert_eq!( parse_inline("*Italic then **bold and italic `and some *code*`** and italic*"), Composite::from(vec![ Compound::raw_str("Italic then ").italic(), Compound::raw_str("bold and italic ").bold().italic(), Compound::raw_str("and some *code*").bold().italic().code(), Compound::raw_str(" and italic").italic(), ]) ); ``` The [mad_inline] macro is useful for semi-dynamic markdown building: it prevents characters like `'*'` from messing the style: ``` use minimad::*; let md1 = mad_inline!( "**$0 formula:** *$1*", // the markdown template, interpreted only once "Disk", // fills $0 "2*π*r", // fills $1. Note that the stars don't mess the markdown ); let md2 = Composite::from(vec![ Compound::raw_str("Disk formula:").bold(), Compound::raw_str(" "), Compound::raw_str("2*π*r").italic(), ]); ``` Note that Termimad contains macros and tools to deal with templates. If your goal is to print in the console you should use Termimad's functions. */ pub mod clean; mod markdown; pub mod parser; mod template; pub use { clean::*, markdown::*, parser::Options, template::*, }; /// reexport so that macros can be used without imports pub use once_cell; /// parse a markdown text pub fn parse_text( md: &str, options: Options, ) -> Text { parser::parse(md, options) } /// parse a line, which is meant to be part of a markdown text. /// This function shouldn't usually be used: if you don't want /// a text you probably need `parse_inline` pub fn parse_line(md: &str) -> Line { Line::from(md) } /// parse a monoline markdown snippet which isn't from a text. /// Don't produce some types of line: TableRow, Code, ListItem /// as they only make sense in a multi-line text. pub fn parse_inline(md: &str) -> Composite { Composite::from_inline(md) } minimad-0.13.0/src/markdown/align.rs000064400000000000000000000006501046102023000153750ustar 00000000000000/// Left, Center, Right or Unspecified #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] pub enum Alignment { #[default] Unspecified, Left, Center, Right, } impl Alignment { pub fn col_spec(self) -> &'static str { match self { Self::Left => "|:-", Self::Right => "|-:", Self::Center => "|:-:", Self::Unspecified => "|-", } } } minimad-0.13.0/src/markdown/composite.rs000064400000000000000000000202441046102023000163060ustar 00000000000000/// a composite is a group of compounds. It can be a whole line, /// or a table cell use crate::*; /// The global style of a composite #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CompositeStyle { Paragraph, Header(u8), // never 0, and <= MAX_HEADER_DEPTH ListItem(u8), // can't be built > 3 by parsing Code, Quote, } /// a composite is a monoline sequence of compounds. /// It's defined by /// - the global style of the composite, if any /// - a vector of styled parts #[derive(Debug, PartialEq, Eq, Clone)] pub struct Composite<'a> { pub style: CompositeStyle, pub compounds: Vec>, } impl<'a> From>> for Composite<'a> { fn from(compounds: Vec>) -> Composite<'a> { Composite { style: CompositeStyle::Paragraph, compounds, } } } impl Default for Composite<'_> { fn default() -> Self { Self { style: CompositeStyle::Paragraph, compounds: Vec::new(), } } } impl<'a> Composite<'a> { pub fn new() -> Composite<'a> { Self::default() } /// parse a monoline markdown snippet which isn't from a text. pub fn from_inline(md: &'a str) -> Composite<'a> { parser::LineParser::from(md).inline() } pub fn raw_str(s: &'a str) -> Composite<'a> { Self { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str(s)], } } #[inline(always)] pub fn is_code(&self) -> bool { matches!(self.style, CompositeStyle::Code { .. }) } #[inline(always)] pub fn is_list_item(&self) -> bool { matches!(self.style, CompositeStyle::ListItem { .. }) } #[inline(always)] pub fn is_quote(&self) -> bool { matches!(self.style, CompositeStyle::Quote { .. }) } /// return the total number of characters in the composite /// /// Example /// ```rust /// assert_eq!(minimad::Line::from("τ:`2π`").char_length(), 4); /// ``` /// /// This may not be the visible width: a renderer can /// add some things (maybe some caracters) to wrap inline code, /// or a bullet in front of a list item #[inline(always)] pub fn char_length(&self) -> usize { self.compounds .iter() .fold(0, |sum, compound| sum + compound.as_str().chars().count()) } /// remove all white spaces at left, unless in inline code /// Empty compounds are cleaned out pub fn trim_start_spaces(&mut self) { loop { if self.compounds.is_empty() { break; } if self.compounds[0].code { break; } self.compounds[0].src = self.compounds[0] .src .trim_start_matches(char::is_whitespace); if self.compounds[0].is_empty() { self.compounds.remove(0); } else { break; } } } /// remove all white spaces at right, unless in inline code /// Empty compounds are cleaned out pub fn trim_end_spaces(&mut self) { loop { if self.compounds.is_empty() { break; } let last = self.compounds.len() - 1; if self.compounds[last].code { break; } self.compounds[last].src = self.compounds[last] .src .trim_end_matches(char::is_whitespace); if self.compounds[last].is_empty() { self.compounds.remove(last); } else { break; } } } pub fn trim_spaces(&mut self) { self.trim_start_spaces(); self.trim_end_spaces(); } pub fn is_empty(&self) -> bool { self.compounds.len() == 0 } /// remove characters, and whole compounds if necessary pub fn remove_chars_left( &mut self, mut to_remove: usize, ) { while to_remove > 0 { if self.compounds.is_empty() { return; } let compound_len = self.compounds[0].char_length(); if compound_len > to_remove { self.compounds[0] = self.compounds[0].tail_chars(to_remove); return; } else { self.compounds.remove(0); to_remove -= compound_len; } } } /// remove characters, and whole compounds if necessary pub fn remove_chars_right( &mut self, mut to_remove: usize, ) { while to_remove > 0 { if self.compounds.is_empty() { return; } let compound_idx = self.compounds.len() - 1; let compound_len = self.compounds[compound_idx].char_length(); if compound_len > to_remove { self.compounds[compound_idx] = self.compounds[compound_idx].sub_chars(0, to_remove); return; } else { self.compounds.remove(compound_idx); to_remove -= compound_len; } } } /// remove characters, and whole compounds if necessary. /// /// align is the alignment of the composite. If the composite is left /// aligned, we remove chars at the right. pub fn remove_chars( &mut self, to_remove: usize, align: Alignment, ) { match align { Alignment::Left => { self.remove_chars_right(to_remove); } Alignment::Right => { self.remove_chars_left(to_remove); } _ => { let to_remove_left = to_remove / 2; let to_remove_right = to_remove - to_remove_left; self.remove_chars_left(to_remove_left); self.remove_chars_right(to_remove_right); } } } } // Tests trimming composite #[cfg(test)] mod tests { use crate::*; #[test] fn composite_trim() { let mut left = Composite::from_inline(" *some* text "); left.trim_spaces(); assert_eq!( left, Composite { style: CompositeStyle::Paragraph, compounds: vec![ Compound::raw_str("some").italic(), Compound::raw_str(" text"), ] } ); } #[test] fn composite_trim_keep_code() { let mut left = Composite::from_inline(" ` ` "); left.trim_spaces(); assert_eq!( left, Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str(" ").code(),] } ); } #[test] fn empty_composite_trim() { let mut left = Composite::from_inline(" * * ** `` ** "); left.trim_start_spaces(); assert_eq!(left.compounds.len(), 0); } #[test] fn composite_remove_chars() { let mut composite = Composite::from_inline(" *A* *B* `Test` *7*"); composite.remove_chars_left(1); assert_eq!(composite.char_length(), 10); composite.remove_chars(5, Alignment::Right); // removes at left assert_eq!(composite.char_length(), 5); assert_eq!( composite.clone(), Composite { style: CompositeStyle::Paragraph, compounds: vec![ Compound::raw_str("est").code(), Compound::raw_str(" "), Compound::raw_str("7").italic(), ] }, ); composite.remove_chars_left(8); assert_eq!(composite.char_length(), 0); let mut composite = Composite::from_inline("`l'hélico` *est **rouge** vif!*"); composite.remove_chars(15, Alignment::Center); assert_eq!( composite, Composite { style: CompositeStyle::Paragraph, compounds: vec![ Compound::raw_str("o").code(), Compound::raw_str(" "), Compound::raw_str("est ").italic(), Compound::raw_str("rou").italic().bold(), ] }, ); } } minimad-0.13.0/src/markdown/compound.rs000064400000000000000000000155451046102023000161400ustar 00000000000000use std::fmt::{ self, Write, }; /// a Compound is a part of a line with a consistent styling. /// It can be part of word, several words, some inline code, or even the whole line. #[derive(Clone, PartialEq, Eq, Hash)] pub struct Compound<'s> { pub src: &'s str, pub bold: bool, pub italic: bool, pub code: bool, pub strikeout: bool, } impl<'s> Compound<'s> { /// make a raw unstyled compound /// Involves no parsing #[inline(always)] pub fn raw_str(src: &'s str) -> Compound<'s> { Compound { src, bold: false, italic: false, code: false, strikeout: false, } } /// change the content but keeps the style arguments pub fn set_str( &mut self, src: &'s str, ) { self.src = src; } /// change the attributes by taking the values from the other /// compound, keeping the str pub fn set_attributes_from( &mut self, other: &Compound, ) { self.bold = other.bold; self.italic = other.italic; self.code = other.code; self.strikeout = other.strikeout; } /// return a sub part of the compound, with the same styling /// r_start is relative, that is 0 is the index of the first /// byte of this compound. #[inline(always)] pub fn sub( &self, r_start: usize, r_end: usize, ) -> Compound<'s> { Compound { src: &self.src[r_start..r_end], bold: self.bold, italic: self.italic, code: self.code, strikeout: self.strikeout, } } /// return a sub part of the compound, with the same styling /// r_start is relative, that is 0 is the index of the first /// char of this compound. /// /// The difference with `sub` is that this method is unicode /// aware and counts the chars instead of asking for the bytes #[inline(always)] pub fn sub_chars( &self, r_start: usize, r_end: usize, ) -> Compound<'s> { let mut rb_start = 0; let mut rb_end = 0; for (char_idx, (byte_idx, _)) in self.as_str().char_indices().enumerate() { if char_idx == r_start { rb_start = byte_idx; } else if char_idx == r_end { rb_end = byte_idx; break; } } if rb_end == 0 && rb_start != 0 { self.tail(rb_start) } else { self.sub(rb_start, rb_end) } } /// return a sub part at end of the compound, with the same styling /// r_start is relative, that is if you give 0 you get a clone of /// this compound #[inline(always)] pub fn tail( &self, r_start: usize, ) -> Compound<'s> { Compound { src: &self.src[r_start..], bold: self.bold, italic: self.italic, code: self.code, strikeout: self.strikeout, } } /// return a sub part at end of the compound, with the same styling /// r_start is relative, that is if you give 0 you get a clone of /// this compound /// /// The difference with `tail` is that this method is unicode /// aware and counts the chars instead of asking for the bytes #[inline(always)] pub fn tail_chars( &self, r_start: usize, ) -> Compound<'s> { let mut rb_start = 0; for (char_idx, (byte_idx, _)) in self.as_str().char_indices().enumerate() { rb_start = byte_idx; if char_idx == r_start { break; } } self.tail(rb_start) } // shortens this compound by `tail_size` bytes and returns the tail // as another compound pub fn cut_tail( &mut self, tail_size: usize, ) -> Compound<'s> { let cut = self.src.len() - tail_size; let tail = Compound { src: &self.src[cut..], bold: self.bold, italic: self.italic, code: self.code, strikeout: self.strikeout, }; self.src = &self.src[0..cut]; tail } // make a raw unstyled compound from part of a string // Involves no parsing #[inline(always)] pub fn raw_part( src: &'s str, start: usize, end: usize, ) -> Compound<'s> { Compound { src: &src[start..end], bold: false, italic: false, code: false, strikeout: false, } } #[inline(always)] pub fn new( src: &'s str, // the source string from which the compound is a part start: usize, // start index in bytes end: usize, bold: bool, italic: bool, code: bool, strikeout: bool, ) -> Compound<'s> { Compound { src: &src[start..end], italic, bold, code, strikeout, } } #[inline(always)] pub fn bold(mut self) -> Compound<'s> { self.bold = true; self } #[inline(always)] pub fn italic(mut self) -> Compound<'s> { self.italic = true; self } #[inline(always)] pub fn code(mut self) -> Compound<'s> { self.code = true; self } #[inline(always)] pub fn strikeout(mut self) -> Compound<'s> { self.strikeout = true; self } #[inline(always)] pub fn set_bold( &mut self, bold: bool, ) { self.bold = bold; } #[inline(always)] pub fn set_italic( &mut self, italic: bool, ) { self.italic = italic; } #[inline(always)] pub fn set_code( &mut self, code: bool, ) { self.code = code; } #[inline(always)] pub fn set_strikeout( &mut self, strikeout: bool, ) { self.strikeout = strikeout; } #[inline(always)] pub fn as_str(&self) -> &'s str { self.src } #[inline(always)] pub fn char_length(&self) -> usize { self.as_str().chars().count() } #[inline(always)] pub fn is_empty(&self) -> bool { self.src.is_empty() } } impl fmt::Display for Compound<'_> { fn fmt( &self, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { f.write_str(self.as_str())?; Ok(()) } } impl fmt::Debug for Compound<'_> { fn fmt( &self, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { if self.bold { f.write_char('B')?; } if self.italic { f.write_char('I')?; } if self.code { f.write_char('C')?; } if self.strikeout { f.write_char('S')?; } f.write_char('"')?; f.write_str(self.as_str())?; f.write_char('"')?; Ok(()) } } minimad-0.13.0/src/markdown/header.rs000064400000000000000000000024161046102023000155350ustar 00000000000000use { crate::*, std::cmp, }; /// count the number of '#' at start. Return 0 if they're /// not followed by a ' ' or if they're too many #[allow(clippy::needless_range_loop)] pub fn header_level(src: &str) -> usize { let src = src.as_bytes(); let mut l: usize = src.len(); if l > 2 { l = cmp::min(src.len() - 1, MAX_HEADER_DEPTH + 1); for i in 0..l { match src[i] { b'#' => {} b' ' => { return i; } _ => { return 0; } } } } 0 } #[test] fn header_level_count() { assert_eq!(header_level(""), 0); assert_eq!(header_level("#"), 0); assert_eq!(header_level("# "), 0); // we don't allow empty headers assert_eq!(header_level("# A"), 1); assert_eq!(header_level(" "), 0); assert_eq!(header_level("test"), 0); assert_eq!(header_level("###b"), 0); assert_eq!(header_level("###"), 0); assert_eq!(header_level("### b"), 3); assert_eq!(header_level(" a b"), 0); assert_eq!(header_level("# titre"), 1); assert_eq!(header_level("#### *titre*"), 4); assert_eq!(header_level("######## a b"), 8); assert_eq!(header_level("######### a b"), 0); // too deep } minimad-0.13.0/src/markdown/line.rs000064400000000000000000000063101046102023000152310ustar 00000000000000use crate::*; pub const MAX_HEADER_DEPTH: usize = 8; /// a parsed line #[derive(Debug, PartialEq, Eq, Clone)] pub enum Line<'a> { Normal(Composite<'a>), // TableRow(TableRow<'a>), // a normal table row, with cells having content TableRule(TableRule), // a separator/border in a table, optionally defining alignments HorizontalRule, // an horizontal line dividing the screen CodeFence(Composite<'a>), } impl<'a> Line<'a> { pub fn from(md: &'a str) -> Self { parser::LineParser::from(md).line() } pub fn raw_str(s: &'a str) -> Self { Self::Normal(Composite::raw_str(s)) } #[inline(always)] pub fn char_length(&self) -> usize { match self { Line::Normal(composite) => composite.char_length(), Line::TableRow(row) => row.cells.iter().fold(0, |s, c| s + c.char_length()), _ => 0, // no known char length for table format lines } } pub fn new_paragraph(compounds: Vec>) -> Line<'_> { Line::Normal(Composite { style: CompositeStyle::Paragraph, compounds, }) } pub fn empty_code_fence() -> Line<'static> { Line::CodeFence(Composite { style: CompositeStyle::Paragraph, compounds: vec![], }) } pub fn new_code_fence(compounds: Vec>) -> Line<'_> { Line::CodeFence(Composite { style: CompositeStyle::Paragraph, compounds, }) } pub fn new_code(compound: Compound<'_>) -> Line<'_> { Line::Normal(Composite { style: CompositeStyle::Code, compounds: vec![compound], }) } pub fn new_quote(compounds: Vec>) -> Line<'_> { Line::Normal(Composite { style: CompositeStyle::Quote, compounds, }) } pub fn new_list_item( depth: u8, compounds: Vec>, ) -> Line<'_> { Line::Normal(Composite { style: CompositeStyle::ListItem(depth), compounds, }) } pub fn new_header( level: u8, compounds: Vec>, ) -> Line<'_> { Line::Normal(Composite { style: CompositeStyle::Header(level), compounds, }) } pub fn new_table_row(cells: Vec>) -> Line<'_> { Line::TableRow(TableRow { cells }) } pub fn new_table_alignments(cells: Vec) -> Line<'static> { Line::TableRule(TableRule { cells }) } #[inline(always)] pub fn is_table_row(&self) -> bool { matches!(self, Line::TableRow(_)) } #[inline(always)] #[allow(clippy::match_like_matches_macro)] pub fn is_table_part(&self) -> bool { match self { Line::Normal(_) => false, _ => true, } } #[inline(always)] pub fn is_code(&self) -> bool { match self { Line::Normal(composite) => composite.is_code(), _ => false, } } } #[test] pub fn count_chars() { assert_eq!(Line::from("τ").char_length(), 1); assert_eq!(Line::from("τ:`2π`").char_length(), 4); assert_eq!(Line::from("* item").char_length(), 4); } minimad-0.13.0/src/markdown/mod.rs000064400000000000000000000005541046102023000150650ustar 00000000000000mod align; mod composite; mod compound; mod header; mod line; mod tbl; mod text; pub use { align::Alignment, composite::{ Composite, CompositeStyle, }, compound::Compound, header::header_level, line::{ Line, MAX_HEADER_DEPTH, }, tbl::{ TableRow, TableRule, }, text::Text, }; minimad-0.13.0/src/markdown/tbl.rs000064400000000000000000000032201046102023000150600ustar 00000000000000use crate::*; #[derive(Debug, PartialEq, Eq, Clone)] pub struct TableRow<'a> { pub cells: Vec>, } #[derive(Debug, PartialEq, Eq, Clone)] pub struct TableRule { pub cells: Vec, } impl<'a> TableRow<'a> { /// Try to read the cells as formatting cells /// (i.e. like `|:-:|-:|:-----|` /// Implementation note: /// it could me more efficiently be tested during initial /// reading but I don't really want to duplicate the code /// of line_parser::parse_compounds until everything is /// stabilized. If it proves necessary I'll do a /// line_parser::parse_cell (and parse_compound won't take /// a bool paramater anymore). pub fn as_table_alignments(&self) -> Option { let mut formats = TableRule { cells: Vec::new() }; for cell in &self.cells { if cell.compounds.len() != 1 { return None; } let c = &cell.compounds[0].as_str(); let mut left_colon = false; let mut right_colon = false; for (idx, char) in c.char_indices() { match char { ':' if idx == 0 => left_colon = true, ':' => right_colon = true, '-' => {} _ => return None, } } formats.cells.push(match (left_colon, right_colon) { (false, false) => Alignment::Unspecified, (true, false) => Alignment::Left, (false, true) => Alignment::Right, (true, true) => Alignment::Center, }); } Some(formats) } } minimad-0.13.0/src/markdown/text.rs000064400000000000000000000016521046102023000152720ustar 00000000000000use crate::*; /// a text, that is just a collection of lines #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct Text<'a> { pub lines: Vec>, } impl<'s> From<&'s str> for Text<'s> { /// Build a text from a multi-line string interpreted as markdown /// /// To build a text with parsing options, prefer the /// `termimad::parse_text` function fn from(md: &str) -> Text<'_> { parse_text(md, Options::default()) } } impl<'s> Text<'s> { /// Parse a text from markdown lines. /// /// To build a text with parsing options, prefer the /// `termimad::parse_text` function pub fn from_md_lines(md_lines: I) -> Self where I: Iterator, { crate::parser::parse_lines(md_lines, Options::default()) } pub fn raw_str(s: &'s str) -> Self { let lines = s.lines().map(Line::raw_str).collect(); Self { lines } } } minimad-0.13.0/src/parser/line_parser.rs000064400000000000000000000477231046102023000162740ustar 00000000000000use crate::*; /// The structure parsing a line or part of a line. /// A LineParser initialized from a markdown string exposes 2 main methods: /// * `line` parses a line which is supposed to be part of a markdown text. This /// method shouln't really be used externally: a text can be parsed in a whole /// using `Text::from` /// * `inline` parses a snippet which isn't supposed to be part of a markdown text. /// Some types of lines aren't produced this ways as they don't make sense out of /// a text: ListItem, TableRow, Code. /// /// Normally not used directly but though `line::from(str)` #[derive(Debug)] pub struct LineParser<'s> { src: &'s str, idx: usize, // current index in string, in bytes pub(crate) code: bool, pub(crate) italic: bool, pub(crate) bold: bool, pub(crate) strikeout: bool, } impl<'s> LineParser<'s> { pub fn from(src: &'s str) -> LineParser<'_> { LineParser { src, idx: 0, bold: false, italic: false, code: false, strikeout: false, } } fn close_compound( &mut self, end: usize, tag_length: usize, compounds: &mut Vec>, ) { if end > self.idx { compounds.push(Compound::new( self.src, self.idx, end, self.bold, self.italic, self.code, self.strikeout, )); } self.idx = end + tag_length; } fn code_block_compound_from_idx( &self, idx: usize, ) -> Compound<'s> { Compound::new(self.src, idx, self.src.len(), false, false, false, false) } fn parse_compounds( &mut self, stop_on_pipe: bool, ) -> Vec> { let mut compounds = Vec::new(); let mut after_first_star = false; let mut after_first_tilde = false; let mut after_antislash = false; // self.idx tracks byte indices, but str::char_indices returns an // iterator over chars, which may be wider than one byte. So we need // to skip not self.idx elements, but the number of chars that occur // before self.idx let chars_to_skip = self.src[..self.idx].chars().count(); for (idx, char) in self.src.char_indices().skip(chars_to_skip) { if self.code { // only one thing matters: whether we're closing the inline code if char == '`' { self.close_compound(idx, 1, &mut compounds); self.code = false; } after_antislash = false; after_first_star = false; continue; } #[cfg(feature = "escaping")] if after_antislash { after_antislash = false; match char { '*' | '~' | '|' | '`' => { self.close_compound(idx - 1, 1, &mut compounds); continue; } '\\' => { self.close_compound(idx, 1, &mut compounds); continue; } _ => {} // we don't escape at all normal chars } } else if char == '\\' { after_antislash = true; continue; } if after_first_star { match char { '*' => { // this is the second star self.close_compound(idx - 1, 2, &mut compounds); self.bold ^= true; } '~' => { after_first_tilde = true; self.close_compound(idx - 1, 2, &mut compounds); // we don't know yet if it's one or two tildes self.italic ^= true; } '|' if stop_on_pipe => { self.close_compound(idx - 1, 1, &mut compounds); return compounds; } '`' => { self.close_compound(idx - 1, 2, &mut compounds); self.italic ^= true; self.code = true; } _ => { // there was only one star // Note that we don't handle a tag just after a star (except in code) self.close_compound(idx - 1, 1, &mut compounds); self.italic ^= true; } } after_first_star = false; } else if after_first_tilde { match char { '*' => { after_first_star = true; // we don't know yet if it's one or two stars } '~' => { // this is the second tilde self.close_compound(idx - 1, 2, &mut compounds); self.strikeout ^= true; } '|' if stop_on_pipe => { self.close_compound(idx - 1, 1, &mut compounds); return compounds; } _ => { // there was only one tilde, which means nothing } } after_first_tilde = false; } else { match char { '*' => { after_first_star = true; // we don't know yet if it's one or two stars } '~' => { after_first_tilde = true; } '|' if stop_on_pipe => { self.close_compound(idx, 0, &mut compounds); return compounds; } '`' => { self.close_compound(idx, 1, &mut compounds); self.code = true; } _ => {} } } } let mut idx = self.src.len(); if after_first_star && self.italic { idx -= 1; } if after_first_tilde && self.strikeout { idx -= 1; } self.close_compound(idx, 0, &mut compounds); compounds } fn parse_cells(&mut self) -> Vec> { let mut cells = Vec::new(); while self.idx < self.src.len() { self.idx += 1; let style = if self.src[self.idx..].starts_with("* ") { self.idx += 2; CompositeStyle::ListItem(0) } else if self.src[self.idx..].starts_with(" * ") { self.idx += 3; CompositeStyle::ListItem(1) } else if self.src[self.idx..].starts_with(" * ") { self.idx += 4; CompositeStyle::ListItem(2) } else if self.src[self.idx..].starts_with(" * ") { self.idx += 5; CompositeStyle::ListItem(3) } else if self.src[self.idx..].starts_with("> ") { self.idx += 2; CompositeStyle::Quote } else { CompositeStyle::Paragraph }; self.bold = false; self.italic = false; self.code = false; self.strikeout = false; let compounds = self.parse_compounds(true); let mut composite = Composite { style, compounds }; composite.trim_spaces(); cells.push(composite); } if !cells.is_empty() && cells[cells.len() - 1].compounds.is_empty() { cells.pop(); } cells } pub fn inline(mut self) -> Composite<'s> { Composite { style: CompositeStyle::Paragraph, compounds: self.parse_compounds(false), } } /// should be called when the line must be interpreted as a code part, /// for example between code fences pub fn as_code(mut self) -> Line<'s> { if self.src.starts_with("```") { self.idx = 3; Line::new_code_fence(self.parse_compounds(false)) } else { Line::new_code(self.code_block_compound_from_idx(0)) } } pub fn line(mut self) -> Line<'s> { self.parse_line() } pub(crate) fn parse_line(&mut self) -> Line<'s> { if self.src.starts_with('|') { let tr = TableRow { cells: self.parse_cells(), }; return match tr.as_table_alignments() { Some(aligns) => Line::TableRule(aligns), None => Line::TableRow(tr), }; } if self.src.starts_with(" ") { return Line::new_code(self.code_block_compound_from_idx(4)); } if self.src.starts_with('\t') { return Line::new_code(self.code_block_compound_from_idx(1)); } if self.src.starts_with("* ") { self.idx = 2; return Line::new_list_item(0, self.parse_compounds(false)); } if self.src.starts_with(" * ") { self.idx = 3; return Line::new_list_item(1, self.parse_compounds(false)); } if self.src.starts_with(" * ") { self.idx = 4; return Line::new_list_item(2, self.parse_compounds(false)); } if self.src.starts_with(" * ") { self.idx = 5; return Line::new_list_item(3, self.parse_compounds(false)); } if self.src == ">" { return Line::new_quote(Vec::new()); } if self.src.starts_with("> ") { self.idx = 2; return Line::new_quote(self.parse_compounds(false)); } if self.src.starts_with("```") { self.idx = 3; return Line::new_code_fence(self.parse_compounds(false)); } let header_level = header_level(self.src); if header_level > 0 { self.idx = header_level + 1; return Line::new_header(header_level as u8, self.parse_compounds(false)); } let compounds = self.parse_compounds(false); if compounds_are_rule(&compounds) { Line::HorizontalRule } else { Line::new_paragraph(compounds) } } } const DASH: u8 = 45; fn compounds_are_rule(compounds: &[Compound<'_>]) -> bool { if compounds.len() != 1 { return false; } let s = compounds[0].as_str(); if s.len() < 3 { return false; } for c in s.as_bytes() { if *c != DASH { return false; } } true } /// Tests of line parsing #[cfg(test)] mod tests { use crate::*; #[test] fn simple_line_parsing() { assert_eq!( Line::from("Hello ~~wolrd~~ **World**. *Code*: `sqrt(π/2)`"), Line::new_paragraph(vec![ Compound::raw_str("Hello "), Compound::raw_str("wolrd").strikeout(), Compound::raw_str(" "), Compound::raw_str("World").bold(), Compound::raw_str(". "), Compound::raw_str("Code").italic(), Compound::raw_str(": "), Compound::raw_str("sqrt(π/2)").code(), ]) ); } #[test] fn nested_styles_parsing() { assert_eq!( Line::from("*Italic then **bold and italic `and some *code*`** and italic*"), Line::new_paragraph(vec![ Compound::raw_str("Italic then ").italic(), Compound::raw_str("bold and italic ").bold().italic(), Compound::raw_str("and some *code*").bold().italic().code(), Compound::raw_str(" and italic").italic(), ]) ); } #[test] fn quote() { assert_eq!( Line::from("> Veni, vidi, *vici*!"), Line::new_quote(vec![ Compound::raw_str("Veni, vidi, "), Compound::raw_str("vici").italic(), Compound::raw_str("!"), ]) ); } #[test] fn code_after_italic() { assert_eq!( Line::from("*name=*`code`"), Line::new_paragraph(vec![ Compound::raw_str("name=").italic(), Compound::raw_str("code").code(), ]) ); } #[test] /// this test is borderline. It wouldn't be very problematic to not support this case. /// A regression would thus be acceptable here (but I want it to be noticed) fn single_star() { assert_eq!( Line::from("*"), Line::new_paragraph(vec![Compound::raw_str("*"),]) ); } #[test] /// this test is borderline. It wouldn't be very problematic to not support it. /// A regression would thus be acceptable here (but I want it to be noticed) fn single_tilde() { assert_eq!( Line::from("~"), Line::new_paragraph(vec![Compound::raw_str("~"),]) ); } #[test] fn striked_after_italic() { assert_eq!( Line::from("*italic*~~striked~~"), Line::new_paragraph(vec![ Compound::raw_str("italic").italic(), Compound::raw_str("striked").strikeout(), ]) ); } #[test] fn tight_sequence() { assert_eq!( Line::from( "*italic*`code`**bold**`code`*italic**italic+bold***`code`*I*~~striked~~*I*" ), Line::new_paragraph(vec![ Compound::raw_str("italic").italic(), Compound::raw_str("code").code(), Compound::raw_str("bold").bold(), Compound::raw_str("code").code(), Compound::raw_str("italic").italic(), Compound::raw_str("italic+bold").italic().bold(), Compound::raw_str("code").code(), Compound::raw_str("I").italic(), Compound::raw_str("striked").strikeout(), Compound::raw_str("I").italic(), ]) ); } #[cfg(feature = "escaping")] #[test] fn escapes() { assert_eq!( Line::from("no \\*italic\\* here"), Line::new_paragraph(vec![ Compound::raw_str("no "), Compound::raw_str("*italic"), Compound::raw_str("* here"), ]) ); // check we're not removing chars with the escaping, and that // we're not losing the '\' when it's not escaping something // (only markdown modifiers can be escaped) assert_eq!( Line::from("a\\bc\\"), Line::new_paragraph(vec![Compound::raw_str("a\\bc\\"),]) ); assert_eq!( Line::from("*italic\\*and\\*still\\*italic*"), Line::new_paragraph(vec![ Compound::raw_str("italic").italic(), Compound::raw_str("*and").italic(), Compound::raw_str("*still").italic(), Compound::raw_str("*italic").italic(), ]) ); assert_eq!( Line::from( "\\**Italic then **bold\\\\ and \\`italic `and some *code*`** and italic*\\*" ), Line::new_paragraph(vec![ Compound::raw_str("*"), Compound::raw_str("Italic then ").italic(), Compound::raw_str("bold\\").bold().italic(), Compound::raw_str(" and ").bold().italic(), Compound::raw_str("`italic ").bold().italic(), Compound::raw_str("and some *code*").bold().italic().code(), Compound::raw_str(" and italic*").italic(), ]) ); } #[test] fn code_fence() { assert_eq!(Line::from("```"), Line::new_code_fence(vec![])); assert_eq!( Line::from("```rust"), Line::new_code_fence(vec![Compound::raw_str("rust"),]), ); } #[test] fn line_of_code() { assert_eq!( Line::from(" let r = Math.sin(π/2) * 7"), Line::new_code(Compound::raw_str("let r = Math.sin(π/2) * 7")) ); } #[test] fn standard_header() { assert_eq!( Line::from("### just a title"), Line::new_header(3, vec![Compound::raw_str("just a title"),]) ); } #[test] fn list_item() { assert_eq!( Line::from("* *list* item"), Line::new_list_item( 0, vec![ Compound::raw_str("list").italic(), Compound::raw_str(" item"), ] ) ); } #[test] fn deep_list_items() { assert_eq!( Line::from(" * *list* item"), Line::new_list_item( 1, vec![ Compound::raw_str("list").italic(), Compound::raw_str(" item"), ] ) ); assert_eq!( Line::from(" * deeper"), Line::new_list_item(2, vec![Compound::raw_str("deeper"),]) ); assert_eq!( Line::from(" * even **deeper**"), Line::new_list_item( 3, vec![ Compound::raw_str("even "), Compound::raw_str("deeper").bold(), ] ) ); assert_eq!( Line::from(" * but not this one..."), Line::new_code(Compound::raw_str("* but not this one...")), ); } #[test] fn horizontal_rule() { assert_eq!(Line::from("----------"), Line::HorizontalRule,); } #[test] fn styled_header() { assert_eq!( Line::from("## a header with some **bold**!"), Line::new_header( 2, vec![ Compound::raw_str("a header with some "), Compound::raw_str("bold").bold(), Compound::raw_str("!"), ] ) ); } #[test] fn table_row() { assert_eq!( Line::from("| bla |*italic*|hi!|> some quote"), Line::new_table_row(vec![ Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("bla"),], }, Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("italic").italic(),], }, Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("hi!"),], }, Composite { style: CompositeStyle::Quote, compounds: vec![Compound::raw_str("some quote"),], } ]) ); } #[test] fn table_row_issue_4() { assert_eq!( Line::from("| 安 | 安 | 安 |"), Line::new_table_row(vec![ Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("安"),], }, Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("安"),], }, Composite { style: CompositeStyle::Paragraph, compounds: vec![Compound::raw_str("安"),], }, ]) ); } #[test] fn table_alignments() { assert_eq!( Line::from("|-----|:--|:-:|----:"), Line::new_table_alignments(vec![ Alignment::Unspecified, Alignment::Left, Alignment::Center, Alignment::Right, ]) ); } } minimad-0.13.0/src/parser/mod.rs000064400000000000000000000034161046102023000145370ustar 00000000000000mod line_parser; mod options; mod text_parser; pub use { line_parser::*, options::*, text_parser::*, }; #[test] fn indented_code_between_fences() { use crate::*; let md = r#" outside ```code a b ``` "#; assert_eq!( parse_text(md, Options::default().clean_indentations(true)), Text { lines: vec![ Line::new_paragraph(vec![Compound::raw_str("outside")]), Line::new_code(Compound::raw_str("a")), Line::new_code(Compound::raw_str(" b")), ] }, ); } #[test] fn test_clean() { use crate::*; let text = r#" bla bla bla * item 1 * item 2 "#; assert_eq!( parse_text( text, Options { clean_indentations: true, ..Default::default() } ), Text { lines: vec![ Line::from("bla bla bla"), Line::from("* item 1"), Line::from("* item 2"), ] }, ); } #[test] fn test_inline_code_continuation() { use crate::*; let md = r#" bla bla `code again` bla "#; // Without continuation let options = Options::default().clean_indentations(true); assert_eq!( parse_text(md, options), Text { lines: vec![Line::from("bla bla `code"), Line::from("again` bla"),] }, ); // With continuation let options = Options::default() .clean_indentations(true) .continue_inline_code(true); assert_eq!( parse_text(md, options), Text { lines: vec![Line::from("bla bla `code`"), Line::from("`again` bla"),] }, ); } minimad-0.13.0/src/parser/options.rs000064400000000000000000000042331046102023000154510ustar 00000000000000/// Markdown parsing options #[derive(Debug, Clone, Copy)] pub struct Options { /// Remove one or several superfluous levels of indentations /// /// This is useful when your text is too deeply intended, for /// example because it's defined in a raw literal: /// /// ``` /// use minimad::*; /// let text = r#" /// bla bla bla /// * item 1 /// * item 2 /// "#; /// assert_eq!( /// parse_text(text, Options { clean_indentations: true, ..Default::default() }), /// Text { lines: vec![ /// Line::from("bla bla bla"), /// Line::from("* item 1"), /// Line::from("* item 2"), /// ]}, /// ); /// ``` /// pub clean_indentations: bool, pub continue_inline_code: bool, pub continue_italic: bool, pub continue_bold: bool, pub continue_strikeout: bool, } #[allow(clippy::derivable_impls)] impl Default for Options { fn default() -> Self { Self { clean_indentations: false, continue_inline_code: false, continue_italic: false, continue_bold: false, continue_strikeout: false, } } } impl Options { pub fn clean_indentations( mut self, value: bool, ) -> Self { self.clean_indentations = value; self } pub fn continue_inline_code( mut self, value: bool, ) -> Self { self.continue_inline_code = value; self } pub fn continue_italic( mut self, value: bool, ) -> Self { self.continue_italic = value; self } pub fn continue_bold( mut self, value: bool, ) -> Self { self.continue_bold = value; self } pub fn continue_strikeout( mut self, value: bool, ) -> Self { self.continue_strikeout = value; self } pub fn continue_spans( mut self, value: bool, ) -> Self { self.continue_inline_code = value; self.continue_italic = value; self.continue_bold = value; self.continue_strikeout = value; self } } minimad-0.13.0/src/parser/text_parser.rs000064400000000000000000000036271046102023000163240ustar 00000000000000use crate::*; /// Parse a markdown string into a text pub fn parse( md: &str, options: Options, ) -> Text<'_> { if options.clean_indentations { parse_lines(clean::lines(md).into_iter(), options) } else { parse_lines(md.lines(), options) } } /// Parse lines pub(crate) fn parse_lines<'s, I>( md_lines: I, options: Options, ) -> Text<'s> where I: Iterator, { let mut lines = Vec::new(); let mut between_fences = false; let mut continue_code = false; let mut continue_italic = false; let mut continue_bold = false; let mut continue_strikeout = false; for md_line in md_lines { let mut line_parser = parser::LineParser::from(md_line); let line = if between_fences { continue_code = false; continue_italic = false; continue_bold = false; continue_strikeout = false; line_parser.as_code() } else { if continue_code { line_parser.code = true; } if continue_italic { line_parser.italic = true; } if continue_bold { line_parser.bold = true; } if continue_strikeout { line_parser.strikeout = true; } let line = line_parser.parse_line(); continue_code = options.continue_inline_code && line_parser.code; continue_italic = options.continue_italic && line_parser.italic; continue_bold = options.continue_bold && line_parser.bold; continue_strikeout = options.continue_strikeout && line_parser.strikeout; line }; match line { Line::CodeFence(..) => { between_fences = !between_fences; } _ => { lines.push(line); } } } Text { lines } } minimad-0.13.0/src/template/inline_template.rs000064400000000000000000000155531046102023000174550ustar 00000000000000use crate::Composite; // There's an ergonomics limit here: https://stackoverflow.com/q/59306592/263525 // It could probably be solved by defining 10 functions, each one with a different number of // arguments, and a global macro doing the routing. I won't try this until there's enough // users of termimad to justify it... // #[derive(Debug, Default, PartialEq, Eq)] struct Arg { compounds_idx: Vec, // indexes of the compounds the arg should fill } #[allow(dead_code)] // this isn't really dead code, it depends on the macro used impl Arg { fn add( &mut self, idx: usize, ) { self.compounds_idx.push(idx); } } /// a template built from a markdown string, with optional placeholder /// /// It can be used to build a composite and insert parts not interpreted /// as markdown. /// /// /// The [`mad_inline!`](macro.mad_inline.html) macro wraps the call to the `InlineTemplate` and /// is more convenient for most uses. #[derive(Debug, PartialEq, Eq)] pub struct InlineTemplate<'a> { composite: Composite<'a>, args: [Arg; 10], } impl<'a> InlineTemplate<'a> { /// build a template from a markdown string which may contain `$0` to `$9` pub fn from(md: &'a str) -> InlineTemplate<'a> { let mut composite = Composite::from_inline(md); let mut compounds = Vec::new(); let mut args: [Arg; 10] = Default::default(); for compound in composite.compounds { // we iterate over the compounds of the template strings // looking for the $i locus let mut after_dollar = false; let mut start = 0; for (idx, char) in compound.as_str().char_indices() { if after_dollar { if char.is_ascii_digit() { let num: u8 = (char as u8) - b'0'; if start + 1 < idx { compounds.push(compound.sub(start, idx - 1)); } start = idx + 1; args[usize::from(num)].compounds_idx.push(compounds.len()); compounds.push(compound.sub(idx - 1, start)); // placeholder } after_dollar = false; } else if char == '$' { after_dollar = true; } } let tail = compound.tail(start); if !tail.is_empty() { compounds.push(tail); } } composite.compounds = compounds; InlineTemplate { composite, args } } pub fn raw_composite(&self) -> Composite<'a> { self.composite.clone() } pub fn apply( &self, composite: &mut Composite<'a>, arg_idx: usize, value: &'a str, ) { if arg_idx > 9 { return; } for compound_idx in &self.args[arg_idx].compounds_idx { composite.compounds[*compound_idx].set_str(value.as_ref()); } } } /// build an inline from a string literal intepreted as markdown and /// optional arguments which may fill places designed as `$0`..`$9` /// /// Differences with parsing a string built with `format!`: /// * the arguments aren't interpreted as markdown, which is convenient to insert user supplied /// strings in a markdown template. /// * markdown parsing and template building are done only once (the template is stored in a lazy /// static) /// * arguments can be omited, repeated, or given in arbitrary order /// * no support for fmt parameters or arguments other than `&str` /// /// Example: /// ``` /// use minimad::*; /// /// let composite = mad_inline!( /// "**$0 formula:** *$1*", // the markdown template, interpreted only once /// "Disk", // fills $0 /// "2*π*r", // fills $1. Note that the stars don't mess the markdown /// ); /// ``` /// #[macro_export] macro_rules! mad_inline { ( $md: literal $(, $value: expr )* $(,)? ) => {{ use minimad::once_cell::sync::Lazy; static TEMPLATE: Lazy> = Lazy::new(|| { minimad::InlineTemplate::from($md) }); #[allow(unused_mut)] #[allow(unused_variables)] let mut arg_idx = 0; #[allow(unused_mut)] let mut composite = TEMPLATE.raw_composite(); $( TEMPLATE.apply(&mut composite, arg_idx, $value); #[allow(unused_assignments)] // rustc bug { arg_idx += 1; } )* composite }}; } #[cfg(test)] mod tests { use crate::{ self as minimad, // because the macro knows "minimad" template::inline_template::*, *, }; #[test] fn simple_template_parsing() { let mut args = <[Arg; 10]>::default(); args[0].add(3); args[1].add(1); assert_eq!( InlineTemplate::from("test $1 and $0"), InlineTemplate { composite: Composite::from(vec![ Compound::raw_str("test "), Compound::raw_str("$1"), Compound::raw_str(" and "), Compound::raw_str("$0"), ]), args, }, ); let mut args = <[Arg; 10]>::default(); args[0].add(1); args[2].add(0); args[2].add(2); assert_eq!( InlineTemplate::from("$2$0$2 "), // repetition and hole InlineTemplate { composite: Composite::from(vec![ Compound::raw_str("$2"), Compound::raw_str("$0"), Compound::raw_str("$2"), Compound::raw_str(" "), ]), args, }, ); } #[test] fn simple_composition() { let template = InlineTemplate::from("using $1 and **$0**"); let mut composite = template.raw_composite(); template.apply(&mut composite, 0, "First"); assert_eq!( composite, Composite::from(vec![ Compound::raw_str("using "), Compound::raw_str("$1"), Compound::raw_str(" and "), Compound::raw_str("First").bold(), ]), ); } #[test] fn macro_empty_composition() { let composite = mad_inline!("some `code`"); assert_eq!( composite, Composite::from(vec![ Compound::raw_str("some "), Compound::raw_str("code").code(), ]), ); } #[test] fn macro_complex_composition() { let composite = mad_inline!("**$1:** `$0`", "π*r²", "area"); assert_eq!( composite, Composite::from(vec![ Compound::raw_str("area").bold(), Compound::raw_str(":").bold(), Compound::raw_str(" "), Compound::raw_str("π*r²").code(), ]), ); } } minimad-0.13.0/src/template/mod.rs000064400000000000000000000005011046102023000150460ustar 00000000000000mod inline_template; mod owning_template_expander; mod tbl_builder; mod text_template; pub use { inline_template::InlineTemplate, owning_template_expander::OwningTemplateExpander, tbl_builder::*, text_template::{ SubTemplateExpander, TextTemplate, TextTemplateExpander, }, }; minimad-0.13.0/src/template/owning_template_expander.rs000064400000000000000000000143131046102023000213570ustar 00000000000000use crate::*; /// A template expander owning the value you set /// so that you don't have to keep them around until /// you produce the text to display. /// Additionnaly, the same expander can be used for several /// templates. #[derive(Default)] pub struct OwningTemplateExpander<'s> { ops: Vec>, default_value: Option, } #[derive(Default)] pub struct OwningSubTemplateExpander<'s> { ops: Vec>, } enum FillingOperation<'s> { Set { name: &'s str, value: String, }, SetMD { name: &'s str, value: String, }, SetLines { name: &'s str, value: String, }, SetLinesMD { name: &'s str, value: String, }, Sub { name: &'s str, sub_expander: OwningSubTemplateExpander<'s>, }, } enum SubFillingOperation<'s> { Set { name: &'s str, value: String }, SetMD { name: &'s str, value: String }, } impl<'s> OwningTemplateExpander<'s> { pub fn new() -> Self { Self::default() } /// set a default value to use when no replacement was defined. /// When you don't call this method, the expanded text contains the /// original names like `${my_arg_name}` (which is useful when developing /// your filling code) pub fn set_default>( &mut self, value: S, ) -> &mut Self { self.default_value = Some(value.into()); self } /// replace placeholders with name `name` with the given value, non interpreted /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) pub fn set( &mut self, name: &'s str, value: S, ) -> &mut Self { self.ops.push(FillingOperation::Set { name, value: value.to_string(), }); self } /// replace placeholders with name `name` with the given value, interpreted as markdown pub fn set_md>( &mut self, name: &'s str, value: S, ) -> &mut Self { self.ops.push(FillingOperation::SetMD { name, value: value.into(), }); self } /// return a sub template expander. You can do set and set_md /// on the returned sub to fill an instance of the repeation section. pub fn sub( &mut self, name: &'s str, ) -> &mut OwningSubTemplateExpander<'s> { let idx = self.ops.len(); self.ops.push(FillingOperation::Sub { name, sub_expander: OwningSubTemplateExpander::new(), }); match &mut self.ops[idx] { FillingOperation::Sub { name: _, sub_expander, } => sub_expander, _ => unreachable!(), } } /// replace a placeholder with several lines. /// This is mostly useful when the placeholder is a repeatable line (code, list item) pub fn set_lines>( &mut self, name: &'s str, raw_lines: S, ) -> &mut Self { self.ops.push(FillingOperation::SetLines { name, value: raw_lines.into(), }); self } /// replace a placeholder with several lines interpreted as markdown pub fn set_lines_md>( &mut self, name: &'s str, md: S, ) -> &mut Self { self.ops.push(FillingOperation::SetLinesMD { name, value: md.into(), }); self } /// build a text by applying the replacements to the initial template pub fn expand<'t>( &'s self, template: &'t TextTemplate<'s>, ) -> Text<'s> { let mut expander = template.expander(); if let Some(s) = &self.default_value { expander.set_all(s); } for op in &self.ops { match op { FillingOperation::Set { name, value } => { expander.set(name, value); } FillingOperation::SetMD { name, value } => { expander.set_md(name, value); } FillingOperation::SetLines { name, value } => { expander.set_lines(name, value); } FillingOperation::SetLinesMD { name, value } => { expander.set_lines_md(name, value); } FillingOperation::Sub { name, sub_expander } => { let sub = expander.sub(name); for op in &sub_expander.ops { match op { SubFillingOperation::Set { name, value } => { sub.set(name, value); } SubFillingOperation::SetMD { name, value } => { sub.set_md(name, value); } } } } } } expander.expand() } } impl<'s> OwningSubTemplateExpander<'s> { pub fn new() -> Self { Self { ops: Vec::new() } } /// replace placeholders with name `name` with the given value, non interpreted /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) pub fn set( &mut self, name: &'s str, value: S, ) -> &mut Self { self.ops.push(SubFillingOperation::Set { name, value: value.to_string(), }); self } pub fn set_option( &mut self, name: &'s str, value: Option, ) -> &mut Self { if let Some(value) = value { self.ops.push(SubFillingOperation::Set { name, value: value.to_string(), }); } self } /// replace placeholders with name `name` with the given value, interpreted as markdown pub fn set_md>( &mut self, name: &'s str, value: S, ) -> &mut Self { self.ops.push(SubFillingOperation::SetMD { name, value: value.into(), }); self } } minimad-0.13.0/src/template/tbl_builder.rs000064400000000000000000000102371046102023000165650ustar 00000000000000use crate::*; #[derive(Debug, Clone)] pub struct CellDef { /// how the cell will be filled, may be a template pub md: String, pub align: Alignment, } #[derive(Debug, Clone)] pub struct Col { pub header: CellDef, pub content: CellDef, } /// A facility to build templates for tables /// /// You can for example build a table this two ways: /// /// ### with an explicit string: /// /// ``` /// static MD: &str = r#" /// |-:|:-:|:-:|:-:|:-:|-:|:-:|:-:|:-|:- /// |id|dev|filesystem|disk|type|used|use%|free|size|mount point /// |-:|:-|:-|:-:|:-:|-:|-:|-:|:- /// ${rows /// |${id}|${dev-major}:${dev-minor}|${filesystem}|${disk}|${type}|~~${used}~~|~~${use-percents}~~ `${bar}`|*${free}*|**${size}**|${mount-point} /// } /// |-: /// "#; /// ``` /// ### with a table_builder: /// /// ``` /// use minimad::{*, Alignment::*}; /// /// let mut tbl = TableBuilder::default(); /// tbl /// .col(Col::simple("id").align(Right)) /// .col(Col::new("dev", "${dev-major}:${dev-minor}")) /// .col(Col::simple("filesystem")) /// .col(Col::simple("disk").align_content(Center)) /// .col(Col::simple("type")) /// .col(Col::new("used", "~~${used}~~")) /// .col(Col::new("use%", "~~${use-percents}~~ `${bar}`").align_content(Right)) /// .col(Col::new("free", "*${free}*").align(Right)) /// .col(Col::new("size", "**${size}**")) /// .col(Col::simple("mount point").align(Left)); /// ``` /// /// Both ways are mostly equivalent but a table builder makes it easier to dynamically /// define the columns. /// /// (example taken from [lfs](https://github.com/Canop/lfs)) #[derive(Debug, Clone, Default)] pub struct TableBuilder { pub cols: Vec, /// an optional name for the sub template for the rows. /// This is mostly useful when you want to concatenate /// table templates and you need to distinguish their /// subs pub rows_sub_name: Option, } impl CellDef { pub fn new>(md: S) -> Self { Self { md: md.into(), align: Alignment::Unspecified, } } pub fn align( mut self, align: Alignment, ) -> Self { self.align = align; self } } impl Col { pub fn simple>(var_name: S) -> Self { Self::new( var_name.as_ref().to_string(), format!("${{{}}}", var_name.as_ref().replace(' ', "-")), ) } pub fn new, C: Into>( header_md: H, content_md: C, ) -> Self { Self { header: CellDef::new(header_md).align(Alignment::Center), content: CellDef::new(content_md), } } pub fn align( mut self, align: Alignment, ) -> Self { self.header.align = align; self.content.align = align; self } pub fn align_header( mut self, align: Alignment, ) -> Self { self.header.align = align; self } pub fn align_content( mut self, align: Alignment, ) -> Self { self.content.align = align; self } } impl TableBuilder { pub fn col( &mut self, col: Col, ) -> &mut Self { self.cols.push(col); self } /// build the string of a template of the table pub fn template_md(&self) -> String { let mut md = String::new(); for col in &self.cols { md.push_str(col.header.align.col_spec()); } md.push('\n'); for col in &self.cols { md.push('|'); md.push_str(&col.header.md); } md.push('\n'); for col in &self.cols { md.push_str(col.content.align.col_spec()); } md.push_str("\n${"); if let Some(name) = self.rows_sub_name.as_ref() { md.push_str(name); } else { md.push_str("rows"); } md.push('\n'); for col in &self.cols { md.push('|'); md.push_str(&col.content.md); } md.push_str("\n}\n|-\n"); md } } impl From<&TableBuilder> for String { fn from(tblbl: &TableBuilder) -> String { tblbl.template_md() } } minimad-0.13.0/src/template/text_template.rs000064400000000000000000000506541046102023000171640ustar 00000000000000use crate::*; #[derive(Debug, Default)] struct SubTemplate<'s> { start_line_idx: usize, line_count: usize, name: &'s str, } #[derive(Debug, Default, PartialEq, Eq)] struct CompoundArg<'s> { name: &'s str, line_idx: usize, // index in the line in the template's text composite_idx: usize, // for when the line is multi-composite (ie it's a tablerow) compound_idx: usize, // index of the compound in the line's composite } /// a markdown template allowing you to replace some placeholders with /// given values, or to expand some sub-templates with repetitions /// (useful with lists, table rows, etc.) #[derive(Debug)] pub struct TextTemplate<'s> { pub text: Text<'s>, compound_args: Vec>, // replacements of compounds sub_templates: Vec>, } #[derive(Debug)] enum SubTemplateToken<'s> { None, Start(&'s str), End, } #[derive(Debug, Clone)] struct Replacement<'s, 'b> { name: &'b str, value: &'s str, } /// an expander for a sub template. You get it using the `sub` method /// of the text expander #[derive(Debug)] pub struct SubTemplateExpander<'s, 'b> { name: &'b str, raw_replacements: Vec>, // replacements which are done as non interpreted compound content md_replacements: Vec>, } /// an expander you get from a template. You specify replacements /// on the expander then you ask it the text using `expand` pub struct TextTemplateExpander<'s, 'b> { template: &'b TextTemplate<'s>, text: Text<'s>, sub_expansions: Vec>, md_replacements: Vec>, lines_to_add: Vec>>, lines_to_exclude: Vec, // true when the line must not be copied into the final text } //------------------------------------------------------------------- // Template parsing //------------------------------------------------------------------- fn is_valid_name_char(c: char) -> bool { c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' } fn read_sub_template_token(md_line: &str) -> SubTemplateToken<'_> { let mut chars = md_line.chars(); match (chars.next(), chars.next()) { (Some('$'), Some('{')) => { // "${" : maybe a sub-template opening let name = &md_line[2..]; if !name.is_empty() && name.chars().all(is_valid_name_char) { SubTemplateToken::Start(name) } else { SubTemplateToken::None } } (Some('}'), None) => SubTemplateToken::End, _ => SubTemplateToken::None, } } /// find the `${some-name}` arguments in the composite, and add them /// to args. fn find_args<'s>( composite: &mut Composite<'s>, args: &mut Vec>, line_idx: usize, composite_idx: usize, ) { let mut compounds = Vec::new(); for compound in &composite.compounds { let mut start = 0; let mut iter = compound.as_str().char_indices(); while let Some((_, c)) = iter.next() { if c == '$' { if let Some((bridx, c)) = iter.next() { if c == '{' { for (idx, c) in &mut iter { if c == '}' { if idx - bridx > 1 { if start + 1 < bridx { compounds.push(compound.sub(start, bridx - 1)); } args.push(CompoundArg { name: &compound.as_str()[bridx + 1..idx], line_idx, composite_idx, compound_idx: compounds.len(), }); compounds.push(compound.sub(bridx - 1, idx + 1)); // placeholder start = idx + 1; } break; } else if !is_valid_name_char(c) { break; } } } } } } let tail = compound.tail(start); if !tail.is_empty() { compounds.push(tail); } } composite.compounds = compounds; } impl<'s> From<&'s str> for TextTemplate<'s> { /// build a template from a markdown text with placeholders like ${some-name} /// and sub-templates fn from(md: &'s str) -> TextTemplate<'s> { let mut text = Text { lines: Vec::new() }; let mut compound_args = Vec::new(); let mut sub_templates = Vec::new(); let mut current_sub_template: Option> = None; let mut between_fences = false; for md_line in clean::lines(md) { match read_sub_template_token(md_line) { SubTemplateToken::Start(name) => { current_sub_template = Some(SubTemplate { start_line_idx: text.lines.len(), line_count: 0, name, }); continue; // so to not add the sub-tmpl opening to the text } SubTemplateToken::End => { if current_sub_template.is_some() { let mut sub_template = current_sub_template.take().unwrap(); sub_template.line_count = text.lines.len() - sub_template.start_line_idx; sub_templates.push(sub_template); continue; // so to not add the sub-tmpl closing to the text } else { // we'll assume this `}` isn't part of any templating } } SubTemplateToken::None => {} } let line_idx = text.lines.len(); let parser = parser::LineParser::from(md_line); let mut line = if between_fences { parser.as_code() } else { parser.line() }; match &mut line { Line::Normal(ref mut composite) => { find_args(composite, &mut compound_args, line_idx, 0); text.lines.push(line); } Line::TableRow(ref mut table_row) => { for (composite_idx, composite) in table_row.cells.iter_mut().enumerate() { find_args(composite, &mut compound_args, line_idx, composite_idx); } text.lines.push(line); } Line::CodeFence(..) => { between_fences = !between_fences; } _ => { text.lines.push(line); } }; } TextTemplate { text, compound_args, sub_templates, } } } impl<'s> TextTemplate<'s> { /// return a new expander for the template pub fn expander<'b>(&'b self) -> TextTemplateExpander<'s, 'b> { TextTemplateExpander::from(self) } /// if the line `line_idx` is part of a template, return this /// template's index. Return None if it's not part of a template. /// /// This might be optimized by an internal vec in the future (or /// by just having another structure than a Text as internal md /// storage) fn get_sub_of_line( &self, line_idx: usize, ) -> Option { for (sub_idx, sub_template) in self.sub_templates.iter().enumerate() { if line_idx >= sub_template.start_line_idx && line_idx < sub_template.start_line_idx + sub_template.line_count { return Some(sub_idx); } } None } } //------------------------------------------------------------------- // Expansion //------------------------------------------------------------------- impl<'s, 'b> From<&'b TextTemplate<'s>> for TextTemplateExpander<'s, 'b> { /// Build a new expander for the template. The expander stores the additions /// done with `set`, `set_md`, `set_lines` or in the `sub` expanders. fn from(template: &'b TextTemplate<'s>) -> Self { // line insertion (from subtemplates and from set_lines) as well // as line removals are postponed until final text building so // that line indexes stay valid until that point). We just note // what lines to add to or exclude from the final text. let line_count = template.text.lines.len(); let lines_to_add = vec![Vec::new(); line_count]; let lines_to_exclude = vec![false; line_count]; Self { template, text: template.text.clone(), sub_expansions: Vec::new(), md_replacements: Vec::new(), lines_to_add, lines_to_exclude, } } } impl<'s, 'b> SubTemplateExpander<'s, 'b> { /// replace placeholders with name `name` with the given value, not interpreted as markdown pub fn set( &mut self, name: &'b str, value: &'s str, ) -> &mut SubTemplateExpander<'s, 'b> { self.raw_replacements.push(Replacement { name, value }); self } /// replace placeholder with name `name` with the given value, interpreted as markdown pub fn set_md( &mut self, name: &'b str, value: &'s str, ) -> &mut SubTemplateExpander<'s, 'b> { self.md_replacements.push(Replacement { name, value }); self } } fn set_in_line<'s>( line: &mut Line<'s>, compound_arg: &CompoundArg<'s>, value: &'s str, ) { match line { Line::Normal(composite) => { composite.compounds[compound_arg.compound_idx].set_str(value); } Line::TableRow(table_row) => { table_row.cells[compound_arg.composite_idx].compounds[compound_arg.compound_idx] .set_str(value); } _ => {} } } fn set_in_text<'s>( template: &TextTemplate<'s>, text: &mut Text<'s>, line_offset: usize, name: Option<&str>, value: &'s str, ) { for compound_arg in &template.compound_args { if name.is_none() || name == Some(compound_arg.name) { let idx = compound_arg.line_idx; if idx < line_offset || idx - line_offset >= text.lines.len() { continue; // can happen if a replacement name is present in the outside text } set_in_line(&mut text.lines[idx - line_offset], compound_arg, value); } } } #[allow(clippy::needless_lifetimes)] fn set_all_md_in_text<'s, 'b>( template: &TextTemplate<'s>, text: &mut Text<'s>, line_offset: usize, md_replacements: &[Replacement<'s, 'b>], ) { if md_replacements.is_empty() { return; // no need to iterate over all compound_args } for compound_arg in template.compound_args.iter().rev() { let idx = compound_arg.line_idx; if idx < line_offset || idx - line_offset >= text.lines.len() { continue; } for md_repl in md_replacements { if md_repl.name == compound_arg.name { let replacing_composite = Composite::from_inline(md_repl.value); // we replace the compound with the ones of the parsed value let patched_line = &mut text.lines[idx - line_offset]; match patched_line { Line::Normal(ref mut composite) => { replace_compound( composite, compound_arg.compound_idx, replacing_composite.compounds, ); } Line::TableRow(ref mut table_row) => { replace_compound( &mut table_row.cells[compound_arg.composite_idx], compound_arg.compound_idx, replacing_composite.compounds, ); } _ => {} } break; // it's not possible to apply two replacements to the compound } } } } /// replace a compound with several other ones. /// Do nothing if the passed compounds vec is empty. fn replace_compound<'s>( composite: &mut Composite<'s>, // composite in which to do the replacement mut compound_idx: usize, // index in the composite of the compound to remove mut replacing_compounds: Vec>, // the compounds taking the place of the removed one ) { let mut replacing_compounds = replacing_compounds.drain(..); if let Some(compound) = replacing_compounds.next() { composite.compounds[compound_idx] = compound; for compound in replacing_compounds { compound_idx += 1; composite.compounds.insert(compound_idx, compound); } } } impl<'s, 'b> TextTemplateExpander<'s, 'b> { /// replace placeholders with name `name` with the given value, non interpreted /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) pub fn set( &mut self, name: &str, value: &'s str, ) -> &mut TextTemplateExpander<'s, 'b> { set_in_text(self.template, &mut self.text, 0, Some(name), value); self } /// replace all placeholders with the given value, non interpreted /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template). /// This can be used at start to have a "default" value. pub fn set_all( &mut self, value: &'s str, ) -> &mut TextTemplateExpander<'s, 'b> { set_in_text(self.template, &mut self.text, 0, None, value); self } /// replace placeholders with name `name` with the given value, interpreted as markdown pub fn set_md( &mut self, name: &'b str, value: &'s str, ) -> &mut TextTemplateExpander<'s, 'b> { self.md_replacements.push(Replacement { name, value }); self } /// replace a placeholder with several lines. /// This is mostly useful when the placeholder is a repeatable line (code, list item) pub fn set_lines( &mut self, name: &'b str, raw_lines: &'s str, ) -> &mut TextTemplateExpander<'s, 'b> { for compound_arg in &self.template.compound_args { if compound_arg.name == name { // the line holding the compound is now considered a template, it's removed self.lines_to_exclude[compound_arg.line_idx] = true; for value in clean::lines(raw_lines) { let mut line = self.text.lines[compound_arg.line_idx].clone(); set_in_line(&mut line, compound_arg, value); self.lines_to_add[compound_arg.line_idx].push(line); } } } self } /// replace a placeholder with several lines interpreted as markdown pub fn set_lines_md( &mut self, name: &'b str, md: &'s str, ) -> &mut TextTemplateExpander<'s, 'b> { for compound_arg in &self.template.compound_args { if compound_arg.name == name { // the line holding the compound is now considered a template, it's removed self.lines_to_exclude[compound_arg.line_idx] = true; for line in md.lines().map(|md| parser::LineParser::from(md).line()) { self.lines_to_add[compound_arg.line_idx].push(line); } } } self } /// prepare expansion of a sub template and return a mutable reference to the /// object in which to set compound replacements pub fn sub( &mut self, name: &'b str, ) -> &mut SubTemplateExpander<'s, 'b> { let sub = SubTemplateExpander { name, raw_replacements: Vec::new(), md_replacements: Vec::new(), }; let idx = self.sub_expansions.len(); self.sub_expansions.push(sub); &mut self.sub_expansions[idx] } /// build a text by applying the replacements to the initial template pub fn expand(mut self) -> Text<'s> { // The simple replacements defined with expander.set(name, value) have // already be done at this point. // We triage the md_replacements: we can directly apply the ones which // are not applied to sub templates and we must defer the other ones // to the sub templates expansion phase let mut defered_repls: Vec>> = vec![Vec::new(); self.template.sub_templates.len()]; for compound_arg in self.template.compound_args.iter().rev() { let line_idx = compound_arg.line_idx; let sub_idx = self.template.get_sub_of_line(line_idx); for md_repl in &self.md_replacements { if md_repl.name == compound_arg.name { if let Some(sub_idx) = sub_idx { // we must clone because a repl can be applied several times defered_repls[sub_idx].push(md_repl.clone()); } else { let replacing_composite = Composite::from_inline(md_repl.value); // we replace the compound with the ones of the parsed value let patched_line = &mut self.text.lines[line_idx]; match patched_line { Line::Normal(ref mut composite) => { replace_compound( composite, compound_arg.compound_idx, replacing_composite.compounds, ); } Line::TableRow(ref mut table_row) => { replace_compound( &mut table_row.cells[compound_arg.composite_idx], compound_arg.compound_idx, replacing_composite.compounds, ); } _ => {} } break; // it's not possible to apply two replacements to the compound } } } } for (sub_idx, sub_template) in self.template.sub_templates.iter().enumerate() { let start = sub_template.start_line_idx; let end = start + sub_template.line_count; // we remove the lines of the subtemplate from the main text for idx in start..end { self.lines_to_exclude[idx] = true; } for sub_expansion in self.sub_expansions.iter() { if sub_expansion.name != sub_template.name { continue; } let mut sub_text = Text::default(); for line in &self.text.lines[start..end] { sub_text.lines.push(line.clone()); } for repl in &sub_expansion.raw_replacements { set_in_text( self.template, &mut sub_text, sub_template.start_line_idx, Some(repl.name), repl.value, ); } let mut md_replacements = sub_expansion.md_replacements.clone(); md_replacements.extend(defered_repls[sub_idx].clone()); set_all_md_in_text( self.template, &mut sub_text, sub_template.start_line_idx, &md_replacements, ); for line in sub_text.lines.drain(..) { self.lines_to_add[sub_template.start_line_idx].push(line); } } } // we now do the removals and insertions we deffered until then. let mut lines = Vec::new(); for (idx, line) in self.text.lines.drain(..).enumerate() { if !self.lines_to_exclude[idx] { lines.push(line); } lines.append(&mut self.lines_to_add[idx]); } Text { lines } } }