├── fmt.sh ├── .gitignore ├── rustfmt.toml ├── src ├── template │ ├── mod.rs │ ├── tbl_builder.rs │ ├── owning_template_expander.rs │ ├── inline_template.rs │ └── text_template.rs ├── markdown │ ├── mod.rs │ ├── align.rs │ ├── header.rs │ ├── text.rs │ ├── tbl.rs │ ├── line.rs │ ├── compound.rs │ └── composite.rs ├── parser │ ├── mod.rs │ ├── text_parser.rs │ ├── options.rs │ └── line_parser.rs ├── clean.rs └── lib.rs ├── Cargo.toml ├── LICENSE └── README.md /fmt.sh: -------------------------------------------------------------------------------- 1 | cargo +nightly fmt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bacon-locations 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | style_edition = "2024" 3 | imports_granularity = "One" 4 | imports_layout = "Vertical" 5 | fn_params_layout = "Vertical" 6 | -------------------------------------------------------------------------------- /src/template/mod.rs: -------------------------------------------------------------------------------- 1 | mod inline_template; 2 | mod owning_template_expander; 3 | mod tbl_builder; 4 | mod text_template; 5 | 6 | pub use { 7 | inline_template::*, 8 | owning_template_expander::*, 9 | tbl_builder::*, 10 | text_template::{ 11 | SubTemplateExpander, 12 | TextTemplate, 13 | TextTemplateExpander, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimad" 3 | version = "0.14.0" 4 | authors = ["dystroy "] 5 | repository = "https://github.com/Canop/minimad" 6 | description = "light Markdown parser" 7 | edition = "2018" 8 | keywords = ["markdown", "parser", "template"] 9 | license = "MIT" 10 | categories = ["gui", "parser-implementations"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | once_cell = "1.21" 15 | 16 | [features] 17 | escaping = [] 18 | default = ["escaping"] 19 | -------------------------------------------------------------------------------- /src/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | mod align; 2 | mod composite; 3 | mod compound; 4 | mod header; 5 | mod line; 6 | mod tbl; 7 | mod text; 8 | 9 | pub use { 10 | align::Alignment, 11 | composite::{ 12 | Composite, 13 | CompositeStyle, 14 | }, 15 | compound::Compound, 16 | header::header_level, 17 | line::{ 18 | Line, 19 | MAX_HEADER_DEPTH, 20 | }, 21 | tbl::{ 22 | TableRow, 23 | TableRule, 24 | }, 25 | text::Text, 26 | }; 27 | -------------------------------------------------------------------------------- /src/markdown/align.rs: -------------------------------------------------------------------------------- 1 | /// Left, Center, Right or Unspecified 2 | #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] 3 | pub enum Alignment { 4 | #[default] 5 | Unspecified, 6 | Left, 7 | Center, 8 | Right, 9 | } 10 | 11 | impl Alignment { 12 | pub fn col_spec(self) -> &'static str { 13 | match self { 14 | Self::Left => "|:-", 15 | Self::Right => "|-:", 16 | Self::Center => "|:-:", 17 | Self::Unspecified => "|-", 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Canop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/markdown/header.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::cmp, 4 | }; 5 | 6 | /// count the number of '#' at start. Return 0 if they're 7 | /// not followed by a ' ' or if they're too many 8 | #[allow(clippy::needless_range_loop)] 9 | pub fn header_level(src: &str) -> usize { 10 | let src = src.as_bytes(); 11 | let mut l: usize = src.len(); 12 | if l > 2 { 13 | l = cmp::min(src.len() - 1, MAX_HEADER_DEPTH + 1); 14 | for i in 0..l { 15 | match src[i] { 16 | b'#' => {} 17 | b' ' => { 18 | return i; 19 | } 20 | _ => { 21 | return 0; 22 | } 23 | } 24 | } 25 | } 26 | 0 27 | } 28 | 29 | #[test] 30 | fn header_level_count() { 31 | assert_eq!(header_level(""), 0); 32 | assert_eq!(header_level("#"), 0); 33 | assert_eq!(header_level("# "), 0); // we don't allow empty headers 34 | assert_eq!(header_level("# A"), 1); 35 | assert_eq!(header_level(" "), 0); 36 | assert_eq!(header_level("test"), 0); 37 | assert_eq!(header_level("###b"), 0); 38 | assert_eq!(header_level("###"), 0); 39 | assert_eq!(header_level("### b"), 3); 40 | assert_eq!(header_level(" a b"), 0); 41 | assert_eq!(header_level("# titre"), 1); 42 | assert_eq!(header_level("#### *titre*"), 4); 43 | assert_eq!(header_level("######## a b"), 8); 44 | assert_eq!(header_level("######### a b"), 0); // too deep 45 | } 46 | -------------------------------------------------------------------------------- /src/markdown/text.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// a text, that is just a collection of lines 4 | #[derive(Debug, Default, PartialEq, Eq, Clone)] 5 | pub struct Text<'a> { 6 | pub lines: Vec>, 7 | } 8 | 9 | impl<'s> From<&'s str> for Text<'s> { 10 | /// Build a text from a multi-line string interpreted as markdown 11 | /// 12 | /// To build a text with parsing options, prefer the 13 | /// `termimad::parse_text` function 14 | fn from(md: &str) -> Text<'_> { 15 | parse_text(md, Options::default()) 16 | } 17 | } 18 | 19 | impl<'s> Text<'s> { 20 | pub fn from_str( 21 | s: &'s str, 22 | options: Options, 23 | ) -> Self { 24 | crate::parser::parse_lines(s.lines(), options) 25 | } 26 | /// Parse a text from markdown lines. 27 | pub fn from_md_lines(md_lines: I) -> Self 28 | where 29 | I: Iterator, 30 | { 31 | crate::parser::parse_lines(md_lines, Options::default()) 32 | } 33 | pub fn raw_str(s: &'s str) -> Self { 34 | let lines = s.lines().map(Line::raw_str).collect(); 35 | Self { lines } 36 | } 37 | } 38 | 39 | #[test] 40 | fn test_keep_code_fences() { 41 | let md = r"# Heading 42 | Simple text. 43 | ```rust 44 | let a = 10; 45 | let b = 20; 46 | ``` 47 | "; 48 | 49 | // not keeping code fences 50 | let text = Text::from_str(md, Options::default().keep_code_fences(false)); 51 | assert_eq!(text.lines.len(), 4); 52 | 53 | // keeping code fences 54 | let text = Text::from_str(md, Options::default().keep_code_fences(true)); 55 | dbg!(&text); 56 | assert_eq!(text.lines.len(), 6); 57 | let lang = text.lines[2].code_fence_lang().unwrap(); 58 | assert_eq!(lang, "rust"); 59 | } 60 | -------------------------------------------------------------------------------- /src/markdown/tbl.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone)] 4 | pub struct TableRow<'a> { 5 | pub cells: Vec>, 6 | } 7 | 8 | #[derive(Debug, PartialEq, Eq, Clone)] 9 | pub struct TableRule { 10 | pub cells: Vec, 11 | } 12 | 13 | impl TableRow<'_> { 14 | /// Try to read the cells as formatting cells 15 | /// 16 | /// (i.e. like `|:-:|-:|:-----|`) 17 | /// 18 | /// Implementation note: 19 | /// it could me more efficiently be tested during initial 20 | /// reading but I don't really want to duplicate the code 21 | /// of `line_parser::parse_compounds` until everything is 22 | /// stabilized. If it proves necessary I'll do a 23 | /// `line_parser::parse_cell` (and `parse_compound` won't take 24 | /// a `bool` parameter anymore). 25 | pub fn as_table_alignments(&self) -> Option { 26 | let mut formats = TableRule { cells: Vec::new() }; 27 | for cell in &self.cells { 28 | if cell.compounds.len() != 1 { 29 | return None; 30 | } 31 | let c = &cell.compounds[0].as_str(); 32 | let mut left_colon = false; 33 | let mut right_colon = false; 34 | for (idx, char) in c.char_indices() { 35 | match char { 36 | ':' if idx == 0 => left_colon = true, 37 | ':' => right_colon = true, 38 | '-' => {} 39 | _ => return None, 40 | } 41 | } 42 | formats.cells.push(match (left_colon, right_colon) { 43 | (false, false) => Alignment::Unspecified, 44 | (true, false) => Alignment::Left, 45 | (false, true) => Alignment::Right, 46 | (true, true) => Alignment::Center, 47 | }); 48 | } 49 | Some(formats) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] 4 | 5 | [s1]: https://img.shields.io/crates/v/minimad.svg 6 | [l1]: https://crates.io/crates/minimad 7 | 8 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 9 | [l2]: minimad/LICENSE 10 | 11 | [s3]: https://docs.rs/minimad/badge.svg 12 | [l3]: https://docs.rs/minimad/ 13 | 14 | [s4]: https://miaou.dystroy.org/static/shields/room.svg 15 | [l4]: https://miaou.dystroy.org/3 16 | 17 | A simple, non universal purpose, markdown parser. 18 | 19 | If you're looking for a Markdown parser, this one is probably *not* the one you want: 20 | 21 | 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. 22 | 23 | If you still think you might use Minimad directly (not through Temimad), you may contact me on Miaou for advice. 24 | 25 | ### Usage 26 | 27 | 28 | ```toml 29 | [dependencies] 30 | minimad = "0.7" 31 | ``` 32 | 33 | ```rust 34 | assert_eq!( 35 | Line::from("## a header with some **bold**!"), 36 | Line::new_header( 37 | 2, 38 | vec![ 39 | Compound::raw_str("a header with some "), 40 | Compound::raw_str("bold").bold(), 41 | Compound::raw_str("!"), 42 | ] 43 | ) 44 | ); 45 | 46 | assert_eq!( 47 | Line::from("Hello ~~wolrd~~ **World**. *Code*: `sqrt(π/2)`"), 48 | Line::new_paragraph(vec![ 49 | Compound::raw_str("Hello "), 50 | Compound::raw_str("wolrd").strikeout(), 51 | Compound::raw_str(" "), 52 | Compound::raw_str("World").bold(), 53 | Compound::raw_str(". "), 54 | Compound::raw_str("Code").italic(), 55 | Compound::raw_str(": "), 56 | Compound::raw_str("sqrt(π/2)").code(), 57 | ]) 58 | ); 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | mod line_parser; 2 | mod options; 3 | mod text_parser; 4 | 5 | pub use { 6 | line_parser::*, 7 | options::*, 8 | text_parser::*, 9 | }; 10 | 11 | #[test] 12 | fn indented_code_between_fences() { 13 | use crate::*; 14 | let md = r#" 15 | outside 16 | ```code 17 | a 18 | b 19 | ``` 20 | "#; 21 | assert_eq!( 22 | parse_text(md, Options::default().clean_indentations(true)), 23 | Text { 24 | lines: vec![ 25 | Line::new_paragraph(vec![Compound::raw_str("outside")]), 26 | Line::new_code(Compound::raw_str("a")), 27 | Line::new_code(Compound::raw_str(" b")), 28 | ] 29 | }, 30 | ); 31 | } 32 | 33 | #[test] 34 | fn test_clean() { 35 | use crate::*; 36 | let text = r#" 37 | bla bla bla 38 | * item 1 39 | * item 2 40 | "#; 41 | assert_eq!( 42 | parse_text( 43 | text, 44 | Options { 45 | clean_indentations: true, 46 | ..Default::default() 47 | } 48 | ), 49 | Text { 50 | lines: vec![ 51 | Line::from("bla bla bla"), 52 | Line::from("* item 1"), 53 | Line::from("* item 2"), 54 | ] 55 | }, 56 | ); 57 | } 58 | 59 | #[test] 60 | fn test_inline_code_continuation() { 61 | use crate::*; 62 | let md = r#" 63 | bla bla `code 64 | again` bla 65 | "#; 66 | // Without continuation 67 | let options = Options::default().clean_indentations(true); 68 | assert_eq!( 69 | parse_text(md, options), 70 | Text { 71 | lines: vec![Line::from("bla bla `code"), Line::from("again` bla"),] 72 | }, 73 | ); 74 | // With continuation 75 | let options = Options::default() 76 | .clean_indentations(true) 77 | .continue_inline_code(true); 78 | assert_eq!( 79 | parse_text(md, options), 80 | Text { 81 | lines: vec![Line::from("bla bla `code`"), Line::from("`again` bla"),] 82 | }, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/clean.rs: -------------------------------------------------------------------------------- 1 | pub fn is_blank(s: &str) -> bool { 2 | s.chars().all(char::is_whitespace) 3 | } 4 | 5 | /// Remove the superfluous lines and indentations you get when you insert 6 | /// in your code a multi-line raw literal. 7 | /// ``` 8 | /// let lines = minimad::clean::lines( 9 | /// r#" 10 | /// test 11 | /// hop 12 | /// hip 13 | /// "#, 14 | /// ); 15 | /// assert_eq!(lines.len(), 3); 16 | /// assert_eq!(lines[0], "test"); 17 | /// assert_eq!(lines[1], " hop"); 18 | /// assert_eq!(lines[2], "hip"); 19 | /// ``` 20 | #[must_use] 21 | pub fn lines(src: &str) -> Vec<&str> { 22 | let mut result_lines: Vec<&str> = Vec::new(); 23 | let mut src_lines = src.lines(); 24 | if let Some(mut first_line) = src_lines.next() { 25 | if first_line.is_empty() { 26 | if let Some(s) = src_lines.next() { 27 | first_line = s; 28 | } 29 | } 30 | result_lines.push(first_line); 31 | for line in src_lines { 32 | result_lines.push(line); 33 | } 34 | if is_blank(result_lines[result_lines.len() - 1]) { 35 | result_lines.truncate(result_lines.len() - 1); 36 | } 37 | if result_lines.len() > 1 { 38 | let mut white_prefix = String::new(); 39 | for char in first_line.chars() { 40 | if char.is_whitespace() { 41 | white_prefix.push(char); 42 | } else { 43 | break; 44 | } 45 | } 46 | if !white_prefix.is_empty() 47 | && result_lines 48 | .iter() 49 | .all(|line| line.starts_with(&white_prefix) || is_blank(line)) 50 | { 51 | result_lines = result_lines 52 | .iter() 53 | .map(|line| { 54 | if is_blank(line) { 55 | line 56 | } else { 57 | &line[white_prefix.len()..] 58 | } 59 | }) 60 | .collect(); 61 | } 62 | } 63 | } 64 | result_lines 65 | } 66 | -------------------------------------------------------------------------------- /src/parser/text_parser.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Parse a markdown string into a text 4 | pub fn parse( 5 | md: &str, 6 | options: Options, 7 | ) -> Text<'_> { 8 | if options.clean_indentations { 9 | parse_lines(clean::lines(md).into_iter(), options) 10 | } else { 11 | parse_lines(md.lines(), options) 12 | } 13 | } 14 | 15 | /// Parse lines 16 | pub(crate) fn parse_lines<'s, I>( 17 | md_lines: I, 18 | options: Options, 19 | ) -> Text<'s> 20 | where 21 | I: Iterator, 22 | { 23 | let mut lines = Vec::new(); 24 | let mut between_fences = false; 25 | let mut continue_code = false; 26 | let mut continue_italic = false; 27 | let mut continue_bold = false; 28 | let mut continue_strikeout = false; 29 | for md_line in md_lines { 30 | let mut line_parser = parser::LineParser::from(md_line); 31 | let line = if between_fences { 32 | continue_code = false; 33 | continue_italic = false; 34 | continue_bold = false; 35 | continue_strikeout = false; 36 | line_parser.as_code() 37 | } else { 38 | if continue_code { 39 | line_parser.code = true; 40 | } 41 | if continue_italic { 42 | line_parser.italic = true; 43 | } 44 | if continue_bold { 45 | line_parser.bold = true; 46 | } 47 | if continue_strikeout { 48 | line_parser.strikeout = true; 49 | } 50 | let line = line_parser.parse_line(); 51 | continue_code = options.continue_inline_code && line_parser.code; 52 | continue_italic = options.continue_italic && line_parser.italic; 53 | continue_bold = options.continue_bold && line_parser.bold; 54 | continue_strikeout = options.continue_strikeout && line_parser.strikeout; 55 | line 56 | }; 57 | match line { 58 | Line::CodeFence(..) => { 59 | between_fences = !between_fences; 60 | if options.keep_code_fences { 61 | lines.push(line); 62 | } 63 | } 64 | _ => { 65 | lines.push(line); 66 | } 67 | } 68 | } 69 | Text { lines } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This crate provides a *very* simple markdown parser. 3 | 4 | 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. 5 | 6 | It can be used on its own: 7 | 8 | ```rust 9 | use minimad::*; 10 | 11 | assert_eq!( 12 | parse_line("## a header with some **bold**!"), 13 | Line::new_header( 14 | 2, 15 | vec![ 16 | Compound::raw_str("a header with some "), 17 | Compound::raw_str("bold").bold(), 18 | Compound::raw_str("!"), 19 | ] 20 | ) 21 | ); 22 | 23 | assert_eq!( 24 | parse_inline("*Italic then **bold and italic `and some *code*`** and italic*"), 25 | Composite::from(vec![ 26 | Compound::raw_str("Italic then ").italic(), 27 | Compound::raw_str("bold and italic ").bold().italic(), 28 | Compound::raw_str("and some *code*").bold().italic().code(), 29 | Compound::raw_str(" and italic").italic(), 30 | ]) 31 | ); 32 | ``` 33 | 34 | The [`mad_inline`] macro is useful for semi-dynamic markdown building: it prevents characters like `'*'` from messing the style: 35 | 36 | ``` 37 | use minimad::*; 38 | 39 | let md1 = mad_inline!( 40 | "**$0 formula:** *$1*", // the markdown template, interpreted only once 41 | "Disk", // fills $0 42 | "2*π*r", // fills $1. Note that the stars don't mess the markdown 43 | ); 44 | let md2 = Composite::from(vec![ 45 | Compound::raw_str("Disk formula:").bold(), 46 | Compound::raw_str(" "), 47 | Compound::raw_str("2*π*r").italic(), 48 | ]); 49 | ``` 50 | 51 | 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. 52 | 53 | */ 54 | 55 | pub mod clean; 56 | mod markdown; 57 | pub mod parser; 58 | mod template; 59 | 60 | pub use { 61 | clean::*, 62 | markdown::*, 63 | parser::Options, 64 | template::*, 65 | }; 66 | 67 | /// reexport so that macros can be used without imports 68 | pub use once_cell; 69 | 70 | /// parse a markdown text 71 | pub fn parse_text( 72 | md: &str, 73 | options: Options, 74 | ) -> Text<'_> { 75 | parser::parse(md, options) 76 | } 77 | 78 | /// parse a line, which is meant to be part of a markdown text. 79 | /// This function shouldn't usually be used: if you don't want 80 | /// a text you probably need `parse_inline` 81 | pub fn parse_line(md: &str) -> Line<'_> { 82 | Line::from(md) 83 | } 84 | 85 | /// parse a monoline markdown snippet which isn't from a text. 86 | /// Don't produce some types of line: `TableRow`, `Code`, `ListItem` 87 | /// as they only make sense in a multi-line text. 88 | pub fn parse_inline(md: &str) -> Composite<'_> { 89 | Composite::from_inline(md) 90 | } 91 | -------------------------------------------------------------------------------- /src/parser/options.rs: -------------------------------------------------------------------------------- 1 | /// Markdown parsing options 2 | #[derive(Debug, Clone, Copy)] 3 | pub struct Options { 4 | /// Remove one or several superfluous levels of indentations 5 | /// 6 | /// This is useful when your text is too deeply intended, for 7 | /// example because it's defined in a raw literal: 8 | /// 9 | /// ``` 10 | /// use minimad::*; 11 | /// let text = r#" 12 | /// bla bla bla 13 | /// * item 1 14 | /// * item 2 15 | /// "#; 16 | /// assert_eq!( 17 | /// parse_text(text, Options { clean_indentations: true, ..Default::default() }), 18 | /// Text { lines: vec![ 19 | /// Line::from("bla bla bla"), 20 | /// Line::from("* item 1"), 21 | /// Line::from("* item 2"), 22 | /// ]}, 23 | /// ); 24 | /// ``` 25 | /// 26 | pub clean_indentations: bool, 27 | pub continue_inline_code: bool, 28 | pub continue_italic: bool, 29 | pub continue_bold: bool, 30 | pub continue_strikeout: bool, 31 | pub keep_code_fences: bool, 32 | } 33 | 34 | #[allow(clippy::derivable_impls)] 35 | impl Default for Options { 36 | fn default() -> Self { 37 | Self { 38 | clean_indentations: false, 39 | continue_inline_code: false, 40 | continue_italic: false, 41 | continue_bold: false, 42 | continue_strikeout: false, 43 | keep_code_fences: false, 44 | } 45 | } 46 | } 47 | 48 | impl Options { 49 | pub fn clean_indentations( 50 | mut self, 51 | value: bool, 52 | ) -> Self { 53 | self.clean_indentations = value; 54 | self 55 | } 56 | pub fn continue_inline_code( 57 | mut self, 58 | value: bool, 59 | ) -> Self { 60 | self.continue_inline_code = value; 61 | self 62 | } 63 | pub fn continue_italic( 64 | mut self, 65 | value: bool, 66 | ) -> Self { 67 | self.continue_italic = value; 68 | self 69 | } 70 | pub fn continue_bold( 71 | mut self, 72 | value: bool, 73 | ) -> Self { 74 | self.continue_bold = value; 75 | self 76 | } 77 | pub fn continue_strikeout( 78 | mut self, 79 | value: bool, 80 | ) -> Self { 81 | self.continue_strikeout = value; 82 | self 83 | } 84 | pub fn continue_spans( 85 | mut self, 86 | value: bool, 87 | ) -> Self { 88 | self.continue_inline_code = value; 89 | self.continue_italic = value; 90 | self.continue_bold = value; 91 | self.continue_strikeout = value; 92 | self 93 | } 94 | pub fn keep_code_fences( 95 | mut self, 96 | value: bool, 97 | ) -> Self { 98 | self.keep_code_fences = value; 99 | self 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/markdown/line.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const MAX_HEADER_DEPTH: usize = 8; 4 | 5 | /// a parsed line 6 | #[derive(Debug, PartialEq, Eq, Clone)] 7 | pub enum Line<'a> { 8 | Normal(Composite<'a>), // 9 | TableRow(TableRow<'a>), // a normal table row, with cells having content 10 | TableRule(TableRule), // a separator/border in a table, optionally defining alignments 11 | HorizontalRule, // an horizontal line dividing the screen 12 | CodeFence(Composite<'a>), 13 | } 14 | 15 | impl<'a> Line<'a> { 16 | pub fn from(md: &'a str) -> Self { 17 | parser::LineParser::from(md).line() 18 | } 19 | pub fn raw_str(s: &'a str) -> Self { 20 | Self::Normal(Composite::raw_str(s)) 21 | } 22 | pub fn char_length(&self) -> usize { 23 | match self { 24 | Line::Normal(composite) => composite.char_length(), 25 | Line::TableRow(row) => row.cells.iter().fold(0, |s, c| s + c.char_length()), 26 | _ => 0, // no known char length for table format lines 27 | } 28 | } 29 | pub fn new_paragraph(compounds: Vec>) -> Line<'_> { 30 | Line::Normal(Composite { 31 | style: CompositeStyle::Paragraph, 32 | compounds, 33 | }) 34 | } 35 | pub fn empty_code_fence() -> Line<'static> { 36 | Line::CodeFence(Composite { 37 | style: CompositeStyle::Paragraph, 38 | compounds: vec![], 39 | }) 40 | } 41 | pub fn new_code_fence(compounds: Vec>) -> Line<'_> { 42 | Line::CodeFence(Composite { 43 | style: CompositeStyle::Paragraph, 44 | compounds, 45 | }) 46 | } 47 | pub fn new_code(compound: Compound<'_>) -> Line<'_> { 48 | Line::Normal(Composite { 49 | style: CompositeStyle::Code, 50 | compounds: vec![compound], 51 | }) 52 | } 53 | pub fn new_quote(compounds: Vec>) -> Line<'_> { 54 | Line::Normal(Composite { 55 | style: CompositeStyle::Quote, 56 | compounds, 57 | }) 58 | } 59 | pub fn new_list_item( 60 | depth: u8, 61 | compounds: Vec>, 62 | ) -> Line<'_> { 63 | Line::Normal(Composite { 64 | style: CompositeStyle::ListItem(depth), 65 | compounds, 66 | }) 67 | } 68 | pub fn new_header( 69 | level: u8, 70 | compounds: Vec>, 71 | ) -> Line<'_> { 72 | Line::Normal(Composite { 73 | style: CompositeStyle::Header(level), 74 | compounds, 75 | }) 76 | } 77 | pub fn new_table_row(cells: Vec>) -> Line<'_> { 78 | Line::TableRow(TableRow { cells }) 79 | } 80 | pub fn new_table_alignments(cells: Vec) -> Line<'static> { 81 | Line::TableRule(TableRule { cells }) 82 | } 83 | pub fn is_table_row(&self) -> bool { 84 | matches!(self, Line::TableRow(_)) 85 | } 86 | #[allow(clippy::match_like_matches_macro)] 87 | pub fn is_table_part(&self) -> bool { 88 | match self { 89 | Line::Normal(_) => false, 90 | _ => true, 91 | } 92 | } 93 | #[inline(always)] 94 | pub fn is_code(&self) -> bool { 95 | match self { 96 | Line::Normal(composite) => composite.is_code(), 97 | _ => false, 98 | } 99 | } 100 | pub fn code_fence_lang(&self) -> Option<&str> { 101 | match self { 102 | Line::CodeFence(composite) => { 103 | let first = composite.compounds.first()?; 104 | Some(first.src) 105 | } 106 | _ => None, 107 | } 108 | } 109 | } 110 | 111 | #[test] 112 | pub fn count_chars() { 113 | assert_eq!(Line::from("τ").char_length(), 1); 114 | assert_eq!(Line::from("τ:`2π`").char_length(), 4); 115 | assert_eq!(Line::from("* item").char_length(), 4); 116 | } 117 | -------------------------------------------------------------------------------- /src/template/tbl_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct CellDef { 5 | /// how the cell will be filled, may be a template 6 | pub md: String, 7 | 8 | pub align: Alignment, 9 | } 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Col { 13 | pub header: CellDef, 14 | pub content: CellDef, 15 | } 16 | 17 | /// A facility to build templates for tables 18 | /// 19 | /// You can for example build a table this two ways: 20 | /// 21 | /// ### with an explicit string: 22 | /// 23 | /// ``` 24 | /// static MD: &str = r#" 25 | /// |-:|:-:|:-:|:-:|:-:|-:|:-:|:-:|:-|:- 26 | /// |id|dev|filesystem|disk|type|used|use%|free|size|mount point 27 | /// |-:|:-|:-|:-:|:-:|-:|-:|-:|:- 28 | /// ${rows 29 | /// |${id}|${dev-major}:${dev-minor}|${filesystem}|${disk}|${type}|~~${used}~~|~~${use-percents}~~ `${bar}`|*${free}*|**${size}**|${mount-point} 30 | /// } 31 | /// |-: 32 | /// "#; 33 | /// ``` 34 | /// ### with a `table_builder`: 35 | /// 36 | /// ``` 37 | /// use minimad::{*, Alignment::*}; 38 | /// 39 | /// let mut tbl = TableBuilder::default(); 40 | /// tbl 41 | /// .col(Col::simple("id").align(Right)) 42 | /// .col(Col::new("dev", "${dev-major}:${dev-minor}")) 43 | /// .col(Col::simple("filesystem")) 44 | /// .col(Col::simple("disk").align_content(Center)) 45 | /// .col(Col::simple("type")) 46 | /// .col(Col::new("used", "~~${used}~~")) 47 | /// .col(Col::new("use%", "~~${use-percents}~~ `${bar}`").align_content(Right)) 48 | /// .col(Col::new("free", "*${free}*").align(Right)) 49 | /// .col(Col::new("size", "**${size}**")) 50 | /// .col(Col::simple("mount point").align(Left)); 51 | /// ``` 52 | /// 53 | /// Both ways are mostly equivalent but a table builder makes it easier to dynamically 54 | /// define the columns. 55 | /// 56 | /// (example taken from [lfs](https://github.com/Canop/lfs)) 57 | #[derive(Debug, Clone, Default)] 58 | pub struct TableBuilder { 59 | pub cols: Vec, 60 | /// an optional name for the sub template for the rows. 61 | /// This is mostly useful when you want to concatenate 62 | /// table templates and you need to distinguish their 63 | /// subs 64 | pub rows_sub_name: Option, 65 | } 66 | 67 | impl CellDef { 68 | pub fn new>(md: S) -> Self { 69 | Self { 70 | md: md.into(), 71 | align: Alignment::Unspecified, 72 | } 73 | } 74 | pub fn align( 75 | mut self, 76 | align: Alignment, 77 | ) -> Self { 78 | self.align = align; 79 | self 80 | } 81 | } 82 | 83 | impl Col { 84 | pub fn simple>(var_name: S) -> Self { 85 | Self::new( 86 | var_name.as_ref().to_string(), 87 | format!("${{{}}}", var_name.as_ref().replace(' ', "-")), 88 | ) 89 | } 90 | pub fn new, C: Into>( 91 | header_md: H, 92 | content_md: C, 93 | ) -> Self { 94 | Self { 95 | header: CellDef::new(header_md).align(Alignment::Center), 96 | content: CellDef::new(content_md), 97 | } 98 | } 99 | pub fn align( 100 | mut self, 101 | align: Alignment, 102 | ) -> Self { 103 | self.header.align = align; 104 | self.content.align = align; 105 | self 106 | } 107 | pub fn align_header( 108 | mut self, 109 | align: Alignment, 110 | ) -> Self { 111 | self.header.align = align; 112 | self 113 | } 114 | pub fn align_content( 115 | mut self, 116 | align: Alignment, 117 | ) -> Self { 118 | self.content.align = align; 119 | self 120 | } 121 | } 122 | 123 | impl TableBuilder { 124 | pub fn col( 125 | &mut self, 126 | col: Col, 127 | ) -> &mut Self { 128 | self.cols.push(col); 129 | self 130 | } 131 | /// build the string of a template of the table 132 | pub fn template_md(&self) -> String { 133 | let mut md = String::new(); 134 | for col in &self.cols { 135 | md.push_str(col.header.align.col_spec()); 136 | } 137 | md.push('\n'); 138 | for col in &self.cols { 139 | md.push('|'); 140 | md.push_str(&col.header.md); 141 | } 142 | md.push('\n'); 143 | for col in &self.cols { 144 | md.push_str(col.content.align.col_spec()); 145 | } 146 | md.push_str("\n${"); 147 | if let Some(name) = self.rows_sub_name.as_ref() { 148 | md.push_str(name); 149 | } else { 150 | md.push_str("rows"); 151 | } 152 | md.push('\n'); 153 | for col in &self.cols { 154 | md.push('|'); 155 | md.push_str(&col.content.md); 156 | } 157 | md.push_str("\n}\n|-\n"); 158 | md 159 | } 160 | } 161 | 162 | impl From<&TableBuilder> for String { 163 | fn from(tblbl: &TableBuilder) -> String { 164 | tblbl.template_md() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/template/owning_template_expander.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// A template expander owning the value you set 4 | /// so that you don't have to keep them around until 5 | /// you produce the text to display. 6 | /// Additionnaly, the same expander can be used for several 7 | /// templates. 8 | #[derive(Default)] 9 | pub struct OwningTemplateExpander<'s> { 10 | ops: Vec>, 11 | default_value: Option, 12 | } 13 | #[derive(Default)] 14 | pub struct OwningSubTemplateExpander<'s> { 15 | ops: Vec>, 16 | } 17 | 18 | enum FillingOperation<'s> { 19 | Set { 20 | name: &'s str, 21 | value: String, 22 | }, 23 | SetMD { 24 | name: &'s str, 25 | value: String, 26 | }, 27 | SetLines { 28 | name: &'s str, 29 | value: String, 30 | }, 31 | SetLinesMD { 32 | name: &'s str, 33 | value: String, 34 | }, 35 | Sub { 36 | name: &'s str, 37 | sub_expander: OwningSubTemplateExpander<'s>, 38 | }, 39 | } 40 | enum SubFillingOperation<'s> { 41 | Set { name: &'s str, value: String }, 42 | SetMD { name: &'s str, value: String }, 43 | } 44 | 45 | impl<'s> OwningTemplateExpander<'s> { 46 | pub fn new() -> Self { 47 | Self::default() 48 | } 49 | 50 | /// set a default value to use when no replacement was defined. 51 | /// When you don't call this method, the expanded text contains the 52 | /// original names like `${my_arg_name}` (which is useful when developing 53 | /// your filling code) 54 | pub fn set_default>( 55 | &mut self, 56 | value: S, 57 | ) -> &mut Self { 58 | self.default_value = Some(value.into()); 59 | self 60 | } 61 | 62 | /// replace placeholders with name `name` with the given value, non interpreted 63 | /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) 64 | pub fn set( 65 | &mut self, 66 | name: &'s str, 67 | value: S, 68 | ) -> &mut Self { 69 | self.ops.push(FillingOperation::Set { 70 | name, 71 | value: value.to_string(), 72 | }); 73 | self 74 | } 75 | 76 | /// replace placeholders with name `name` with the given value, interpreted as markdown 77 | pub fn set_md>( 78 | &mut self, 79 | name: &'s str, 80 | value: S, 81 | ) -> &mut Self { 82 | self.ops.push(FillingOperation::SetMD { 83 | name, 84 | value: value.into(), 85 | }); 86 | self 87 | } 88 | 89 | /// return a sub template expander. You can do set and `set_md` 90 | /// on the returned sub to fill an instance of the repeation section. 91 | pub fn sub( 92 | &mut self, 93 | name: &'s str, 94 | ) -> &mut OwningSubTemplateExpander<'s> { 95 | let idx = self.ops.len(); 96 | self.ops.push(FillingOperation::Sub { 97 | name, 98 | sub_expander: OwningSubTemplateExpander::new(), 99 | }); 100 | match &mut self.ops[idx] { 101 | FillingOperation::Sub { 102 | name: _, 103 | sub_expander, 104 | } => sub_expander, 105 | _ => unreachable!(), 106 | } 107 | } 108 | 109 | /// replace a placeholder with several lines. 110 | /// This is mostly useful when the placeholder is a repeatable line (code, list item) 111 | pub fn set_lines>( 112 | &mut self, 113 | name: &'s str, 114 | raw_lines: S, 115 | ) -> &mut Self { 116 | self.ops.push(FillingOperation::SetLines { 117 | name, 118 | value: raw_lines.into(), 119 | }); 120 | self 121 | } 122 | 123 | /// replace a placeholder with several lines interpreted as markdown 124 | pub fn set_lines_md>( 125 | &mut self, 126 | name: &'s str, 127 | md: S, 128 | ) -> &mut Self { 129 | self.ops.push(FillingOperation::SetLinesMD { 130 | name, 131 | value: md.into(), 132 | }); 133 | self 134 | } 135 | 136 | /// build a text by applying the replacements to the initial template 137 | pub fn expand<'t>( 138 | &'s self, 139 | template: &'t TextTemplate<'s>, 140 | ) -> Text<'s> { 141 | let mut expander = template.expander(); 142 | if let Some(s) = &self.default_value { 143 | expander.set_all(s); 144 | } 145 | for op in &self.ops { 146 | match op { 147 | FillingOperation::Set { name, value } => { 148 | expander.set(name, value); 149 | } 150 | FillingOperation::SetMD { name, value } => { 151 | expander.set_md(name, value); 152 | } 153 | FillingOperation::SetLines { name, value } => { 154 | expander.set_lines(name, value); 155 | } 156 | FillingOperation::SetLinesMD { name, value } => { 157 | expander.set_lines_md(name, value); 158 | } 159 | FillingOperation::Sub { name, sub_expander } => { 160 | let sub = expander.sub(name); 161 | for op in &sub_expander.ops { 162 | match op { 163 | SubFillingOperation::Set { name, value } => { 164 | sub.set(name, value); 165 | } 166 | SubFillingOperation::SetMD { name, value } => { 167 | sub.set_md(name, value); 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | expander.expand() 175 | } 176 | } 177 | 178 | impl<'s> OwningSubTemplateExpander<'s> { 179 | pub fn new() -> Self { 180 | Self { ops: Vec::new() } 181 | } 182 | /// replace placeholders with name `name` with the given value, non interpreted 183 | /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) 184 | pub fn set( 185 | &mut self, 186 | name: &'s str, 187 | value: S, 188 | ) -> &mut Self { 189 | self.ops.push(SubFillingOperation::Set { 190 | name, 191 | value: value.to_string(), 192 | }); 193 | self 194 | } 195 | 196 | pub fn set_option( 197 | &mut self, 198 | name: &'s str, 199 | value: Option, 200 | ) -> &mut Self { 201 | if let Some(value) = value { 202 | self.ops.push(SubFillingOperation::Set { 203 | name, 204 | value: value.to_string(), 205 | }); 206 | } 207 | self 208 | } 209 | 210 | /// replace placeholders with name `name` with the given value, interpreted as markdown 211 | pub fn set_md>( 212 | &mut self, 213 | name: &'s str, 214 | value: S, 215 | ) -> &mut Self { 216 | self.ops.push(SubFillingOperation::SetMD { 217 | name, 218 | value: value.into(), 219 | }); 220 | self 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/markdown/compound.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{ 2 | self, 3 | Write, 4 | }; 5 | 6 | /// a Compound is a part of a line with a consistent styling. 7 | /// It can be part of word, several words, some inline code, or even the whole line. 8 | #[derive(Clone, PartialEq, Eq, Hash)] 9 | pub struct Compound<'s> { 10 | pub src: &'s str, 11 | pub bold: bool, 12 | pub italic: bool, 13 | pub code: bool, 14 | pub strikeout: bool, 15 | } 16 | 17 | impl<'s> Compound<'s> { 18 | /// make a raw unstyled compound 19 | /// Involves no parsing 20 | pub fn raw_str(src: &'s str) -> Compound<'s> { 21 | Compound { 22 | src, 23 | bold: false, 24 | italic: false, 25 | code: false, 26 | strikeout: false, 27 | } 28 | } 29 | /// change the content but keeps the style arguments 30 | pub fn set_str( 31 | &mut self, 32 | src: &'s str, 33 | ) { 34 | self.src = src; 35 | } 36 | /// change the attributes by taking the values from the other 37 | /// compound, keeping the str 38 | pub fn set_attributes_from( 39 | &mut self, 40 | other: &Compound, 41 | ) { 42 | self.bold = other.bold; 43 | self.italic = other.italic; 44 | self.code = other.code; 45 | self.strikeout = other.strikeout; 46 | } 47 | /// return a sub part of the compound, with the same styling 48 | /// `r_start` is relative, that is 0 is the index of the first 49 | /// byte of this compound. 50 | pub fn sub( 51 | &self, 52 | r_start: usize, 53 | r_end: usize, 54 | ) -> Compound<'s> { 55 | Compound { 56 | src: &self.src[r_start..r_end], 57 | bold: self.bold, 58 | italic: self.italic, 59 | code: self.code, 60 | strikeout: self.strikeout, 61 | } 62 | } 63 | /// return a sub part of the compound, with the same styling 64 | /// `r_start` is relative, that is 0 is the index of the first 65 | /// char of this compound. 66 | /// 67 | /// The difference with `sub` is that this method is unicode 68 | /// aware and counts the chars instead of asking for the bytes 69 | pub fn sub_chars( 70 | &self, 71 | r_start: usize, 72 | r_end: usize, 73 | ) -> Compound<'s> { 74 | let mut rb_start = 0; 75 | let mut rb_end = 0; 76 | for (char_idx, (byte_idx, _)) in self.as_str().char_indices().enumerate() { 77 | if char_idx == r_start { 78 | rb_start = byte_idx; 79 | } else if char_idx == r_end { 80 | rb_end = byte_idx; 81 | break; 82 | } 83 | } 84 | if rb_end == 0 && rb_start != 0 { 85 | self.tail(rb_start) 86 | } else { 87 | self.sub(rb_start, rb_end) 88 | } 89 | } 90 | /// return a sub part at end of the compound, with the same styling 91 | /// `r_start` is relative, that is if you give 0 you get a clone of 92 | /// this compound 93 | pub fn tail( 94 | &self, 95 | r_start: usize, 96 | ) -> Compound<'s> { 97 | Compound { 98 | src: &self.src[r_start..], 99 | bold: self.bold, 100 | italic: self.italic, 101 | code: self.code, 102 | strikeout: self.strikeout, 103 | } 104 | } 105 | /// return a sub part at end of the compound, with the same styling 106 | /// `r_start` is relative, that is if you give 0 you get a clone of 107 | /// this compound 108 | /// 109 | /// The difference with `tail` is that this method is unicode 110 | /// aware and counts the chars instead of asking for the bytes 111 | pub fn tail_chars( 112 | &self, 113 | r_start: usize, 114 | ) -> Compound<'s> { 115 | let mut rb_start = 0; 116 | for (char_idx, (byte_idx, _)) in self.as_str().char_indices().enumerate() { 117 | rb_start = byte_idx; 118 | if char_idx == r_start { 119 | break; 120 | } 121 | } 122 | self.tail(rb_start) 123 | } 124 | 125 | // shortens this compound by `tail_size` bytes and returns the tail 126 | // as another compound 127 | pub fn cut_tail( 128 | &mut self, 129 | tail_size: usize, 130 | ) -> Compound<'s> { 131 | let cut = self.src.len() - tail_size; 132 | let tail = Compound { 133 | src: &self.src[cut..], 134 | bold: self.bold, 135 | italic: self.italic, 136 | code: self.code, 137 | strikeout: self.strikeout, 138 | }; 139 | self.src = &self.src[0..cut]; 140 | tail 141 | } 142 | 143 | // make a raw unstyled compound from part of a string 144 | // Involves no parsing 145 | pub fn raw_part( 146 | src: &'s str, 147 | start: usize, 148 | end: usize, 149 | ) -> Compound<'s> { 150 | Compound { 151 | src: &src[start..end], 152 | bold: false, 153 | italic: false, 154 | code: false, 155 | strikeout: false, 156 | } 157 | } 158 | pub fn new( 159 | src: &'s str, // the source string from which the compound is a part 160 | start: usize, // start index in bytes 161 | end: usize, 162 | bold: bool, 163 | italic: bool, 164 | code: bool, 165 | strikeout: bool, 166 | ) -> Compound<'s> { 167 | Compound { 168 | src: &src[start..end], 169 | italic, 170 | bold, 171 | code, 172 | strikeout, 173 | } 174 | } 175 | pub fn bold(mut self) -> Compound<'s> { 176 | self.bold = true; 177 | self 178 | } 179 | pub fn italic(mut self) -> Compound<'s> { 180 | self.italic = true; 181 | self 182 | } 183 | pub fn code(mut self) -> Compound<'s> { 184 | self.code = true; 185 | self 186 | } 187 | pub fn strikeout(mut self) -> Compound<'s> { 188 | self.strikeout = true; 189 | self 190 | } 191 | pub fn set_bold( 192 | &mut self, 193 | bold: bool, 194 | ) { 195 | self.bold = bold; 196 | } 197 | pub fn set_italic( 198 | &mut self, 199 | italic: bool, 200 | ) { 201 | self.italic = italic; 202 | } 203 | pub fn set_code( 204 | &mut self, 205 | code: bool, 206 | ) { 207 | self.code = code; 208 | } 209 | pub fn set_strikeout( 210 | &mut self, 211 | strikeout: bool, 212 | ) { 213 | self.strikeout = strikeout; 214 | } 215 | pub fn as_str(&self) -> &'s str { 216 | self.src 217 | } 218 | pub fn char_length(&self) -> usize { 219 | self.as_str().chars().count() 220 | } 221 | pub fn is_empty(&self) -> bool { 222 | self.src.is_empty() 223 | } 224 | } 225 | 226 | impl fmt::Display for Compound<'_> { 227 | fn fmt( 228 | &self, 229 | f: &mut fmt::Formatter<'_>, 230 | ) -> fmt::Result { 231 | f.write_str(self.as_str())?; 232 | Ok(()) 233 | } 234 | } 235 | 236 | impl fmt::Debug for Compound<'_> { 237 | fn fmt( 238 | &self, 239 | f: &mut fmt::Formatter<'_>, 240 | ) -> fmt::Result { 241 | if self.bold { 242 | f.write_char('B')?; 243 | } 244 | if self.italic { 245 | f.write_char('I')?; 246 | } 247 | if self.code { 248 | f.write_char('C')?; 249 | } 250 | if self.strikeout { 251 | f.write_char('S')?; 252 | } 253 | f.write_char('"')?; 254 | f.write_str(self.as_str())?; 255 | f.write_char('"')?; 256 | Ok(()) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/template/inline_template.rs: -------------------------------------------------------------------------------- 1 | use crate::Composite; 2 | 3 | // There's an ergonomics limit here: https://stackoverflow.com/q/59306592/263525 4 | // It could probably be solved by defining 10 functions, each one with a different number of 5 | // arguments, and a global macro doing the routing. I won't try this until there's enough 6 | // users of termimad to justify it... 7 | // 8 | 9 | #[derive(Debug, Default, PartialEq, Eq)] 10 | struct Arg { 11 | compounds_idx: Vec, // indexes of the compounds the arg should fill 12 | } 13 | #[allow(dead_code)] // this isn't really dead code, it depends on the macro used 14 | impl Arg { 15 | fn add( 16 | &mut self, 17 | idx: usize, 18 | ) { 19 | self.compounds_idx.push(idx); 20 | } 21 | } 22 | 23 | /// a template built from a markdown string, with optional placeholder 24 | /// 25 | /// It can be used to build a composite and insert parts not interpreted 26 | /// as markdown. 27 | /// 28 | /// 29 | /// The [`mad_inline!`](macro.mad_inline.html) macro wraps the call to the `InlineTemplate` and 30 | /// is more convenient for most uses. 31 | #[derive(Debug, PartialEq, Eq)] 32 | pub struct InlineTemplate<'a> { 33 | composite: Composite<'a>, 34 | args: [Arg; 10], 35 | } 36 | 37 | impl<'a> InlineTemplate<'a> { 38 | /// build a template from a markdown string which may contain `$0` to `$9` 39 | pub fn from(md: &'a str) -> InlineTemplate<'a> { 40 | let mut composite = Composite::from_inline(md); 41 | let mut compounds = Vec::new(); 42 | let mut args: [Arg; 10] = Default::default(); 43 | for compound in composite.compounds { 44 | // we iterate over the compounds of the template strings 45 | // looking for the $i locus 46 | let mut after_dollar = false; 47 | let mut start = 0; 48 | for (idx, char) in compound.as_str().char_indices() { 49 | if after_dollar { 50 | if char.is_ascii_digit() { 51 | let num: u8 = (char as u8) - b'0'; 52 | if start + 1 < idx { 53 | compounds.push(compound.sub(start, idx - 1)); 54 | } 55 | start = idx + 1; 56 | args[usize::from(num)].compounds_idx.push(compounds.len()); 57 | compounds.push(compound.sub(idx - 1, start)); // placeholder 58 | } 59 | after_dollar = false; 60 | } else if char == '$' { 61 | after_dollar = true; 62 | } 63 | } 64 | let tail = compound.tail(start); 65 | if !tail.is_empty() { 66 | compounds.push(tail); 67 | } 68 | } 69 | composite.compounds = compounds; 70 | InlineTemplate { composite, args } 71 | } 72 | 73 | pub fn raw_composite(&self) -> Composite<'a> { 74 | self.composite.clone() 75 | } 76 | 77 | pub fn apply( 78 | &self, 79 | composite: &mut Composite<'a>, 80 | arg_idx: usize, 81 | value: &'a str, 82 | ) { 83 | if arg_idx > 9 { 84 | return; 85 | } 86 | for compound_idx in &self.args[arg_idx].compounds_idx { 87 | composite.compounds[*compound_idx].set_str(value.as_ref()); 88 | } 89 | } 90 | } 91 | 92 | /// build an inline from a string literal intepreted as markdown and 93 | /// optional arguments which may fill places designed as `$0`..`$9` 94 | /// 95 | /// Differences with parsing a string built with `format!`: 96 | /// * the arguments aren't interpreted as markdown, which is convenient to insert user supplied 97 | /// strings in a markdown template. 98 | /// * markdown parsing and template building are done only once (the template is stored in a lazy 99 | /// static) 100 | /// * arguments can be omited, repeated, or given in arbitrary order 101 | /// * no support for fmt parameters or arguments other than `&str` 102 | /// 103 | /// Example: 104 | /// ``` 105 | /// use minimad::*; 106 | /// 107 | /// let composite = mad_inline!( 108 | /// "**$0 formula:** *$1*", // the markdown template, interpreted only once 109 | /// "Disk", // fills $0 110 | /// "2*π*r", // fills $1. Note that the stars don't mess the markdown 111 | /// ); 112 | /// ``` 113 | /// 114 | #[macro_export] 115 | macro_rules! mad_inline { 116 | ( $md: literal $(, $value: expr )* $(,)? ) => {{ 117 | use minimad::once_cell::sync::Lazy; 118 | static TEMPLATE: Lazy> = Lazy::new(|| { 119 | minimad::InlineTemplate::from($md) 120 | }); 121 | #[allow(unused_mut)] 122 | #[allow(unused_variables)] 123 | let mut arg_idx = 0; 124 | #[allow(unused_mut)] 125 | let mut composite = TEMPLATE.raw_composite(); 126 | $( 127 | TEMPLATE.apply(&mut composite, arg_idx, $value); 128 | #[allow(unused_assignments)] // rustc bug 129 | { arg_idx += 1; } 130 | )* 131 | composite 132 | }}; 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use crate::{ 138 | self as minimad, // because the macro knows "minimad" 139 | template::inline_template::*, 140 | *, 141 | }; 142 | 143 | #[test] 144 | fn simple_template_parsing() { 145 | let mut args = <[Arg; 10]>::default(); 146 | args[0].add(3); 147 | args[1].add(1); 148 | assert_eq!( 149 | InlineTemplate::from("test $1 and $0"), 150 | InlineTemplate { 151 | composite: Composite::from(vec![ 152 | Compound::raw_str("test "), 153 | Compound::raw_str("$1"), 154 | Compound::raw_str(" and "), 155 | Compound::raw_str("$0"), 156 | ]), 157 | args, 158 | }, 159 | ); 160 | 161 | let mut args = <[Arg; 10]>::default(); 162 | args[0].add(1); 163 | args[2].add(0); 164 | args[2].add(2); 165 | assert_eq!( 166 | InlineTemplate::from("$2$0$2 "), // repetition and hole 167 | InlineTemplate { 168 | composite: Composite::from(vec![ 169 | Compound::raw_str("$2"), 170 | Compound::raw_str("$0"), 171 | Compound::raw_str("$2"), 172 | Compound::raw_str(" "), 173 | ]), 174 | args, 175 | }, 176 | ); 177 | } 178 | 179 | #[test] 180 | fn simple_composition() { 181 | let template = InlineTemplate::from("using $1 and **$0**"); 182 | let mut composite = template.raw_composite(); 183 | template.apply(&mut composite, 0, "First"); 184 | assert_eq!( 185 | composite, 186 | Composite::from(vec![ 187 | Compound::raw_str("using "), 188 | Compound::raw_str("$1"), 189 | Compound::raw_str(" and "), 190 | Compound::raw_str("First").bold(), 191 | ]), 192 | ); 193 | } 194 | 195 | #[test] 196 | fn macro_empty_composition() { 197 | let composite = mad_inline!("some `code`"); 198 | assert_eq!( 199 | composite, 200 | Composite::from(vec![ 201 | Compound::raw_str("some "), 202 | Compound::raw_str("code").code(), 203 | ]), 204 | ); 205 | } 206 | 207 | #[test] 208 | fn macro_complex_composition() { 209 | let composite = mad_inline!("**$1:** `$0`", "π*r²", "area"); 210 | assert_eq!( 211 | composite, 212 | Composite::from(vec![ 213 | Compound::raw_str("area").bold(), 214 | Compound::raw_str(":").bold(), 215 | Compound::raw_str(" "), 216 | Compound::raw_str("π*r²").code(), 217 | ]), 218 | ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/markdown/composite.rs: -------------------------------------------------------------------------------- 1 | /// a composite is a group of compounds. It can be a whole line, 2 | /// or a table cell 3 | use crate::*; 4 | 5 | /// The global style of a composite 6 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 7 | pub enum CompositeStyle { 8 | Paragraph, 9 | Header(u8), // never 0, and <= MAX_HEADER_DEPTH 10 | ListItem(u8), // can't be built > 3 by parsing 11 | Code, 12 | Quote, 13 | } 14 | 15 | /// a composite is a monoline sequence of compounds. 16 | /// It's defined by 17 | /// - the global style of the composite, if any 18 | /// - a vector of styled parts 19 | #[derive(Debug, PartialEq, Eq, Clone)] 20 | pub struct Composite<'a> { 21 | pub style: CompositeStyle, 22 | pub compounds: Vec>, 23 | } 24 | 25 | impl<'a> From>> for Composite<'a> { 26 | fn from(compounds: Vec>) -> Composite<'a> { 27 | Composite { 28 | style: CompositeStyle::Paragraph, 29 | compounds, 30 | } 31 | } 32 | } 33 | 34 | impl Default for Composite<'_> { 35 | fn default() -> Self { 36 | Self { 37 | style: CompositeStyle::Paragraph, 38 | compounds: Vec::new(), 39 | } 40 | } 41 | } 42 | 43 | impl<'a> Composite<'a> { 44 | pub fn new() -> Composite<'a> { 45 | Self::default() 46 | } 47 | /// parse a monoline markdown snippet which isn't from a text. 48 | pub fn from_inline(md: &'a str) -> Composite<'a> { 49 | parser::LineParser::from(md).inline() 50 | } 51 | pub fn raw_str(s: &'a str) -> Composite<'a> { 52 | Self { 53 | style: CompositeStyle::Paragraph, 54 | compounds: vec![Compound::raw_str(s)], 55 | } 56 | } 57 | pub fn is_code(&self) -> bool { 58 | matches!(self.style, CompositeStyle::Code) 59 | } 60 | pub fn is_list_item(&self) -> bool { 61 | matches!(self.style, CompositeStyle::ListItem { .. }) 62 | } 63 | pub fn is_quote(&self) -> bool { 64 | matches!(self.style, CompositeStyle::Quote) 65 | } 66 | /// return the total number of characters in the composite 67 | /// 68 | /// Example 69 | /// ```rust 70 | /// assert_eq!(minimad::Line::from("τ:`2π`").char_length(), 4); 71 | /// ``` 72 | /// 73 | /// This may not be the visible width: a renderer can 74 | /// add some things (maybe some caracters) to wrap inline code, 75 | /// or a bullet in front of a list item 76 | pub fn char_length(&self) -> usize { 77 | self.compounds 78 | .iter() 79 | .fold(0, |sum, compound| sum + compound.as_str().chars().count()) 80 | } 81 | /// remove all white spaces at left, unless in inline code 82 | /// Empty compounds are cleaned out 83 | pub fn trim_start_spaces(&mut self) { 84 | loop { 85 | if self.compounds.is_empty() { 86 | break; 87 | } 88 | if self.compounds[0].code { 89 | break; 90 | } 91 | self.compounds[0].src = self.compounds[0] 92 | .src 93 | .trim_start_matches(char::is_whitespace); 94 | if self.compounds[0].is_empty() { 95 | self.compounds.remove(0); 96 | } else { 97 | break; 98 | } 99 | } 100 | } 101 | /// remove all white spaces at right, unless in inline code 102 | /// Empty compounds are cleaned out 103 | pub fn trim_end_spaces(&mut self) { 104 | loop { 105 | if self.compounds.is_empty() { 106 | break; 107 | } 108 | let last = self.compounds.len() - 1; 109 | if self.compounds[last].code { 110 | break; 111 | } 112 | self.compounds[last].src = self.compounds[last] 113 | .src 114 | .trim_end_matches(char::is_whitespace); 115 | if self.compounds[last].is_empty() { 116 | self.compounds.remove(last); 117 | } else { 118 | break; 119 | } 120 | } 121 | } 122 | pub fn trim_spaces(&mut self) { 123 | self.trim_start_spaces(); 124 | self.trim_end_spaces(); 125 | } 126 | pub fn is_empty(&self) -> bool { 127 | self.compounds.len() == 0 128 | } 129 | /// remove characters, and whole compounds if necessary 130 | pub fn remove_chars_left( 131 | &mut self, 132 | mut to_remove: usize, 133 | ) { 134 | while to_remove > 0 { 135 | if self.compounds.is_empty() { 136 | return; 137 | } 138 | let compound_len = self.compounds[0].char_length(); 139 | if compound_len > to_remove { 140 | self.compounds[0] = self.compounds[0].tail_chars(to_remove); 141 | return; 142 | } 143 | self.compounds.remove(0); 144 | to_remove -= compound_len; 145 | } 146 | } 147 | /// remove characters, and whole compounds if necessary 148 | pub fn remove_chars_right( 149 | &mut self, 150 | mut to_remove: usize, 151 | ) { 152 | while to_remove > 0 { 153 | if self.compounds.is_empty() { 154 | return; 155 | } 156 | let compound_idx = self.compounds.len() - 1; 157 | let compound_len = self.compounds[compound_idx].char_length(); 158 | if compound_len > to_remove { 159 | self.compounds[compound_idx] = self.compounds[compound_idx].sub_chars(0, to_remove); 160 | return; 161 | } 162 | self.compounds.remove(compound_idx); 163 | to_remove -= compound_len; 164 | } 165 | } 166 | 167 | /// remove characters, and whole compounds if necessary. 168 | /// 169 | /// align is the alignment of the composite. If the composite is left 170 | /// aligned, we remove chars at the right. 171 | pub fn remove_chars( 172 | &mut self, 173 | to_remove: usize, 174 | align: Alignment, 175 | ) { 176 | match align { 177 | Alignment::Left => { 178 | self.remove_chars_right(to_remove); 179 | } 180 | Alignment::Right => { 181 | self.remove_chars_left(to_remove); 182 | } 183 | _ => { 184 | let to_remove_left = to_remove / 2; 185 | let to_remove_right = to_remove - to_remove_left; 186 | self.remove_chars_left(to_remove_left); 187 | self.remove_chars_right(to_remove_right); 188 | } 189 | } 190 | } 191 | } 192 | 193 | // Tests trimming composite 194 | #[cfg(test)] 195 | mod tests { 196 | use crate::*; 197 | 198 | #[test] 199 | fn composite_trim() { 200 | let mut left = Composite::from_inline(" *some* text "); 201 | left.trim_spaces(); 202 | assert_eq!( 203 | left, 204 | Composite { 205 | style: CompositeStyle::Paragraph, 206 | compounds: vec![ 207 | Compound::raw_str("some").italic(), 208 | Compound::raw_str(" text"), 209 | ] 210 | } 211 | ); 212 | } 213 | 214 | #[test] 215 | fn composite_trim_keep_code() { 216 | let mut left = Composite::from_inline(" ` ` "); 217 | left.trim_spaces(); 218 | assert_eq!( 219 | left, 220 | Composite { 221 | style: CompositeStyle::Paragraph, 222 | compounds: vec![Compound::raw_str(" ").code(),] 223 | } 224 | ); 225 | } 226 | 227 | #[test] 228 | fn empty_composite_trim() { 229 | let mut left = Composite::from_inline(" * * ** `` ** "); 230 | left.trim_start_spaces(); 231 | assert_eq!(left.compounds.len(), 0); 232 | } 233 | 234 | #[test] 235 | fn composite_remove_chars() { 236 | let mut composite = Composite::from_inline(" *A* *B* `Test` *7*"); 237 | composite.remove_chars_left(1); 238 | assert_eq!(composite.char_length(), 10); 239 | composite.remove_chars(5, Alignment::Right); // removes at left 240 | assert_eq!(composite.char_length(), 5); 241 | assert_eq!( 242 | composite.clone(), 243 | Composite { 244 | style: CompositeStyle::Paragraph, 245 | compounds: vec![ 246 | Compound::raw_str("est").code(), 247 | Compound::raw_str(" "), 248 | Compound::raw_str("7").italic(), 249 | ] 250 | }, 251 | ); 252 | composite.remove_chars_left(8); 253 | assert_eq!(composite.char_length(), 0); 254 | let mut composite = Composite::from_inline("`l'hélico` *est **rouge** vif!*"); 255 | composite.remove_chars(15, Alignment::Center); 256 | assert_eq!( 257 | composite, 258 | Composite { 259 | style: CompositeStyle::Paragraph, 260 | compounds: vec![ 261 | Compound::raw_str("o").code(), 262 | Compound::raw_str(" "), 263 | Compound::raw_str("est ").italic(), 264 | Compound::raw_str("rou").italic().bold(), 265 | ] 266 | }, 267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/parser/line_parser.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// The structure parsing a line or part of a line. 4 | /// A `LineParser` initialized from a markdown string exposes 2 main methods: 5 | /// * `line` parses a line which is supposed to be part of a markdown text. This 6 | /// method shouln't really be used externally: a text can be parsed in a whole 7 | /// using `Text::from` 8 | /// * `inline` parses a snippet which isn't supposed to be part of a markdown text. 9 | /// Some types of lines aren't produced this ways as they don't make sense out of 10 | /// a text: `ListItem`, `TableRow`, `Code`. 11 | /// 12 | /// Normally not used directly but though `line::from(str)` 13 | #[derive(Debug)] 14 | pub struct LineParser<'s> { 15 | src: &'s str, 16 | idx: usize, // current index in string, in bytes 17 | pub(crate) code: bool, 18 | pub(crate) italic: bool, 19 | pub(crate) bold: bool, 20 | pub(crate) strikeout: bool, 21 | } 22 | 23 | impl<'s> LineParser<'s> { 24 | pub fn from(src: &'s str) -> LineParser<'s> { 25 | LineParser { 26 | src, 27 | idx: 0, 28 | bold: false, 29 | italic: false, 30 | code: false, 31 | strikeout: false, 32 | } 33 | } 34 | fn close_compound( 35 | &mut self, 36 | end: usize, 37 | tag_length: usize, 38 | compounds: &mut Vec>, 39 | ) { 40 | if end > self.idx { 41 | compounds.push(Compound::new( 42 | self.src, 43 | self.idx, 44 | end, 45 | self.bold, 46 | self.italic, 47 | self.code, 48 | self.strikeout, 49 | )); 50 | } 51 | self.idx = end + tag_length; 52 | } 53 | fn code_block_compound_from_idx( 54 | &self, 55 | idx: usize, 56 | ) -> Compound<'s> { 57 | Compound::new(self.src, idx, self.src.len(), false, false, false, false) 58 | } 59 | fn parse_compounds( 60 | &mut self, 61 | stop_on_pipe: bool, 62 | ) -> Vec> { 63 | let mut compounds = Vec::new(); 64 | let mut after_first_star = false; 65 | let mut after_first_tilde = false; 66 | let mut after_antislash = false; 67 | 68 | // self.idx tracks byte indices, but str::char_indices returns an 69 | // iterator over chars, which may be wider than one byte. So we need 70 | // to skip not self.idx elements, but the number of chars that occur 71 | // before self.idx 72 | let chars_to_skip = self.src[..self.idx].chars().count(); 73 | for (idx, char) in self.src.char_indices().skip(chars_to_skip) { 74 | if self.code { 75 | // only one thing matters: whether we're closing the inline code 76 | if char == '`' { 77 | self.close_compound(idx, 1, &mut compounds); 78 | self.code = false; 79 | } 80 | after_antislash = false; 81 | after_first_star = false; 82 | continue; 83 | } 84 | 85 | #[cfg(feature = "escaping")] 86 | if after_antislash { 87 | after_antislash = false; 88 | match char { 89 | '*' | '~' | '|' | '`' => { 90 | self.close_compound(idx - 1, 1, &mut compounds); 91 | continue; 92 | } 93 | '\\' => { 94 | self.close_compound(idx, 1, &mut compounds); 95 | continue; 96 | } 97 | _ => {} // we don't escape at all normal chars 98 | } 99 | } else if char == '\\' { 100 | after_antislash = true; 101 | continue; 102 | } 103 | 104 | if after_first_star { 105 | match char { 106 | '*' => { 107 | // this is the second star 108 | self.close_compound(idx - 1, 2, &mut compounds); 109 | self.bold ^= true; 110 | } 111 | '~' => { 112 | after_first_tilde = true; 113 | self.close_compound(idx - 1, 2, &mut compounds); 114 | // we don't know yet if it's one or two tildes 115 | self.italic ^= true; 116 | } 117 | '|' if stop_on_pipe => { 118 | self.close_compound(idx - 1, 1, &mut compounds); 119 | return compounds; 120 | } 121 | '`' => { 122 | self.close_compound(idx - 1, 2, &mut compounds); 123 | self.italic ^= true; 124 | self.code = true; 125 | } 126 | _ => { 127 | // there was only one star 128 | // Note that we don't handle a tag just after a star (except in code) 129 | self.close_compound(idx - 1, 1, &mut compounds); 130 | self.italic ^= true; 131 | } 132 | } 133 | after_first_star = false; 134 | } else if after_first_tilde { 135 | match char { 136 | '*' => { 137 | after_first_star = true; 138 | // we don't know yet if it's one or two stars 139 | } 140 | '~' => { 141 | // this is the second tilde 142 | self.close_compound(idx - 1, 2, &mut compounds); 143 | self.strikeout ^= true; 144 | } 145 | '|' if stop_on_pipe => { 146 | self.close_compound(idx - 1, 1, &mut compounds); 147 | return compounds; 148 | } 149 | _ => { 150 | // there was only one tilde, which means nothing 151 | } 152 | } 153 | after_first_tilde = false; 154 | } else { 155 | match char { 156 | '*' => { 157 | after_first_star = true; 158 | // we don't know yet if it's one or two stars 159 | } 160 | '~' => { 161 | after_first_tilde = true; 162 | } 163 | '|' if stop_on_pipe => { 164 | self.close_compound(idx, 0, &mut compounds); 165 | return compounds; 166 | } 167 | '`' => { 168 | self.close_compound(idx, 1, &mut compounds); 169 | self.code = true; 170 | } 171 | _ => {} 172 | } 173 | } 174 | } 175 | let mut idx = self.src.len(); 176 | if after_first_star && self.italic { 177 | idx -= 1; 178 | } 179 | if after_first_tilde && self.strikeout { 180 | idx -= 1; 181 | } 182 | self.close_compound(idx, 0, &mut compounds); 183 | compounds 184 | } 185 | fn parse_cells(&mut self) -> Vec> { 186 | let mut cells = Vec::new(); 187 | while self.idx < self.src.len() { 188 | self.idx += 1; 189 | let style = if self.src[self.idx..].starts_with("* ") { 190 | self.idx += 2; 191 | CompositeStyle::ListItem(0) 192 | } else if self.src[self.idx..].starts_with(" * ") { 193 | self.idx += 3; 194 | CompositeStyle::ListItem(1) 195 | } else if self.src[self.idx..].starts_with(" * ") { 196 | self.idx += 4; 197 | CompositeStyle::ListItem(2) 198 | } else if self.src[self.idx..].starts_with(" * ") { 199 | self.idx += 5; 200 | CompositeStyle::ListItem(3) 201 | } else if self.src[self.idx..].starts_with("> ") { 202 | self.idx += 2; 203 | CompositeStyle::Quote 204 | } else { 205 | CompositeStyle::Paragraph 206 | }; 207 | self.bold = false; 208 | self.italic = false; 209 | self.code = false; 210 | self.strikeout = false; 211 | let compounds = self.parse_compounds(true); 212 | let mut composite = Composite { style, compounds }; 213 | composite.trim_spaces(); 214 | cells.push(composite); 215 | } 216 | if !cells.is_empty() && cells[cells.len() - 1].compounds.is_empty() { 217 | cells.pop(); 218 | } 219 | cells 220 | } 221 | pub fn inline(mut self) -> Composite<'s> { 222 | Composite { 223 | style: CompositeStyle::Paragraph, 224 | compounds: self.parse_compounds(false), 225 | } 226 | } 227 | /// should be called when the line must be interpreted as a code part, 228 | /// for example between code fences 229 | pub fn as_code(mut self) -> Line<'s> { 230 | if self.src.starts_with("```") { 231 | self.idx = 3; 232 | Line::new_code_fence(self.parse_compounds(false)) 233 | } else { 234 | Line::new_code(self.code_block_compound_from_idx(0)) 235 | } 236 | } 237 | pub fn line(mut self) -> Line<'s> { 238 | self.parse_line() 239 | } 240 | pub(crate) fn parse_line(&mut self) -> Line<'s> { 241 | if self.src.starts_with('|') { 242 | let tr = TableRow { 243 | cells: self.parse_cells(), 244 | }; 245 | return match tr.as_table_alignments() { 246 | Some(aligns) => Line::TableRule(aligns), 247 | None => Line::TableRow(tr), 248 | }; 249 | } 250 | if self.src.starts_with(" ") { 251 | return Line::new_code(self.code_block_compound_from_idx(4)); 252 | } 253 | if self.src.starts_with('\t') { 254 | return Line::new_code(self.code_block_compound_from_idx(1)); 255 | } 256 | if self.src.starts_with("* ") { 257 | self.idx = 2; 258 | return Line::new_list_item(0, self.parse_compounds(false)); 259 | } 260 | if self.src.starts_with(" * ") { 261 | self.idx = 3; 262 | return Line::new_list_item(1, self.parse_compounds(false)); 263 | } 264 | if self.src.starts_with(" * ") { 265 | self.idx = 4; 266 | return Line::new_list_item(2, self.parse_compounds(false)); 267 | } 268 | if self.src.starts_with(" * ") { 269 | self.idx = 5; 270 | return Line::new_list_item(3, self.parse_compounds(false)); 271 | } 272 | if self.src == ">" { 273 | return Line::new_quote(Vec::new()); 274 | } 275 | if self.src.starts_with("> ") { 276 | self.idx = 2; 277 | return Line::new_quote(self.parse_compounds(false)); 278 | } 279 | if self.src.starts_with("```") { 280 | self.idx = 3; 281 | return Line::new_code_fence(self.parse_compounds(false)); 282 | } 283 | let header_level = header_level(self.src); 284 | if header_level > 0 { 285 | self.idx = header_level + 1; 286 | return Line::new_header(header_level as u8, self.parse_compounds(false)); 287 | } 288 | let compounds = self.parse_compounds(false); 289 | if compounds_are_rule(&compounds) { 290 | Line::HorizontalRule 291 | } else { 292 | Line::new_paragraph(compounds) 293 | } 294 | } 295 | } 296 | 297 | const DASH: u8 = 45; 298 | 299 | fn compounds_are_rule(compounds: &[Compound<'_>]) -> bool { 300 | if compounds.len() != 1 { 301 | return false; 302 | } 303 | let s = compounds[0].as_str(); 304 | if s.len() < 3 { 305 | return false; 306 | } 307 | for c in s.as_bytes() { 308 | if *c != DASH { 309 | return false; 310 | } 311 | } 312 | true 313 | } 314 | 315 | /// Tests of line parsing 316 | #[cfg(test)] 317 | mod tests { 318 | use crate::*; 319 | 320 | #[test] 321 | fn simple_line_parsing() { 322 | assert_eq!( 323 | Line::from("Hello ~~wolrd~~ **World**. *Code*: `sqrt(π/2)`"), 324 | Line::new_paragraph(vec![ 325 | Compound::raw_str("Hello "), 326 | Compound::raw_str("wolrd").strikeout(), 327 | Compound::raw_str(" "), 328 | Compound::raw_str("World").bold(), 329 | Compound::raw_str(". "), 330 | Compound::raw_str("Code").italic(), 331 | Compound::raw_str(": "), 332 | Compound::raw_str("sqrt(π/2)").code(), 333 | ]) 334 | ); 335 | } 336 | 337 | #[test] 338 | fn nested_styles_parsing() { 339 | assert_eq!( 340 | Line::from("*Italic then **bold and italic `and some *code*`** and italic*"), 341 | Line::new_paragraph(vec![ 342 | Compound::raw_str("Italic then ").italic(), 343 | Compound::raw_str("bold and italic ").bold().italic(), 344 | Compound::raw_str("and some *code*").bold().italic().code(), 345 | Compound::raw_str(" and italic").italic(), 346 | ]) 347 | ); 348 | } 349 | 350 | #[test] 351 | fn quote() { 352 | assert_eq!( 353 | Line::from("> Veni, vidi, *vici*!"), 354 | Line::new_quote(vec![ 355 | Compound::raw_str("Veni, vidi, "), 356 | Compound::raw_str("vici").italic(), 357 | Compound::raw_str("!"), 358 | ]) 359 | ); 360 | } 361 | 362 | #[test] 363 | fn code_after_italic() { 364 | assert_eq!( 365 | Line::from("*name=*`code`"), 366 | Line::new_paragraph(vec![ 367 | Compound::raw_str("name=").italic(), 368 | Compound::raw_str("code").code(), 369 | ]) 370 | ); 371 | } 372 | 373 | #[test] 374 | /// this test is borderline. It wouldn't be very problematic to not support this case. 375 | /// A regression would thus be acceptable here (but I want it to be noticed) 376 | fn single_star() { 377 | assert_eq!( 378 | Line::from("*"), 379 | Line::new_paragraph(vec![Compound::raw_str("*"),]) 380 | ); 381 | } 382 | 383 | #[test] 384 | /// this test is borderline. It wouldn't be very problematic to not support it. 385 | /// A regression would thus be acceptable here (but I want it to be noticed) 386 | fn single_tilde() { 387 | assert_eq!( 388 | Line::from("~"), 389 | Line::new_paragraph(vec![Compound::raw_str("~"),]) 390 | ); 391 | } 392 | 393 | #[test] 394 | fn striked_after_italic() { 395 | assert_eq!( 396 | Line::from("*italic*~~striked~~"), 397 | Line::new_paragraph(vec![ 398 | Compound::raw_str("italic").italic(), 399 | Compound::raw_str("striked").strikeout(), 400 | ]) 401 | ); 402 | } 403 | 404 | #[test] 405 | fn tight_sequence() { 406 | assert_eq!( 407 | Line::from( 408 | "*italic*`code`**bold**`code`*italic**italic+bold***`code`*I*~~striked~~*I*" 409 | ), 410 | Line::new_paragraph(vec![ 411 | Compound::raw_str("italic").italic(), 412 | Compound::raw_str("code").code(), 413 | Compound::raw_str("bold").bold(), 414 | Compound::raw_str("code").code(), 415 | Compound::raw_str("italic").italic(), 416 | Compound::raw_str("italic+bold").italic().bold(), 417 | Compound::raw_str("code").code(), 418 | Compound::raw_str("I").italic(), 419 | Compound::raw_str("striked").strikeout(), 420 | Compound::raw_str("I").italic(), 421 | ]) 422 | ); 423 | } 424 | 425 | #[cfg(feature = "escaping")] 426 | #[test] 427 | fn escapes() { 428 | assert_eq!( 429 | Line::from("no \\*italic\\* here"), 430 | Line::new_paragraph(vec![ 431 | Compound::raw_str("no "), 432 | Compound::raw_str("*italic"), 433 | Compound::raw_str("* here"), 434 | ]) 435 | ); 436 | // check we're not removing chars with the escaping, and that 437 | // we're not losing the '\' when it's not escaping something 438 | // (only markdown modifiers can be escaped) 439 | assert_eq!( 440 | Line::from("a\\bc\\"), 441 | Line::new_paragraph(vec![Compound::raw_str("a\\bc\\"),]) 442 | ); 443 | assert_eq!( 444 | Line::from("*italic\\*and\\*still\\*italic*"), 445 | Line::new_paragraph(vec![ 446 | Compound::raw_str("italic").italic(), 447 | Compound::raw_str("*and").italic(), 448 | Compound::raw_str("*still").italic(), 449 | Compound::raw_str("*italic").italic(), 450 | ]) 451 | ); 452 | assert_eq!( 453 | Line::from( 454 | "\\**Italic then **bold\\\\ and \\`italic `and some *code*`** and italic*\\*" 455 | ), 456 | Line::new_paragraph(vec![ 457 | Compound::raw_str("*"), 458 | Compound::raw_str("Italic then ").italic(), 459 | Compound::raw_str("bold\\").bold().italic(), 460 | Compound::raw_str(" and ").bold().italic(), 461 | Compound::raw_str("`italic ").bold().italic(), 462 | Compound::raw_str("and some *code*").bold().italic().code(), 463 | Compound::raw_str(" and italic*").italic(), 464 | ]) 465 | ); 466 | } 467 | 468 | #[test] 469 | fn code_fence() { 470 | assert_eq!(Line::from("```"), Line::new_code_fence(vec![])); 471 | assert_eq!( 472 | Line::from("```rust"), 473 | Line::new_code_fence(vec![Compound::raw_str("rust"),]), 474 | ); 475 | } 476 | 477 | #[test] 478 | fn line_of_code() { 479 | assert_eq!( 480 | Line::from(" let r = Math.sin(π/2) * 7"), 481 | Line::new_code(Compound::raw_str("let r = Math.sin(π/2) * 7")) 482 | ); 483 | } 484 | 485 | #[test] 486 | fn standard_header() { 487 | assert_eq!( 488 | Line::from("### just a title"), 489 | Line::new_header(3, vec![Compound::raw_str("just a title"),]) 490 | ); 491 | } 492 | 493 | #[test] 494 | fn list_item() { 495 | assert_eq!( 496 | Line::from("* *list* item"), 497 | Line::new_list_item( 498 | 0, 499 | vec![ 500 | Compound::raw_str("list").italic(), 501 | Compound::raw_str(" item"), 502 | ] 503 | ) 504 | ); 505 | } 506 | 507 | #[test] 508 | fn deep_list_items() { 509 | assert_eq!( 510 | Line::from(" * *list* item"), 511 | Line::new_list_item( 512 | 1, 513 | vec![ 514 | Compound::raw_str("list").italic(), 515 | Compound::raw_str(" item"), 516 | ] 517 | ) 518 | ); 519 | assert_eq!( 520 | Line::from(" * deeper"), 521 | Line::new_list_item(2, vec![Compound::raw_str("deeper"),]) 522 | ); 523 | assert_eq!( 524 | Line::from(" * even **deeper**"), 525 | Line::new_list_item( 526 | 3, 527 | vec![ 528 | Compound::raw_str("even "), 529 | Compound::raw_str("deeper").bold(), 530 | ] 531 | ) 532 | ); 533 | assert_eq!( 534 | Line::from(" * but not this one..."), 535 | Line::new_code(Compound::raw_str("* but not this one...")), 536 | ); 537 | } 538 | 539 | #[test] 540 | fn horizontal_rule() { 541 | assert_eq!(Line::from("----------"), Line::HorizontalRule,); 542 | } 543 | 544 | #[test] 545 | fn styled_header() { 546 | assert_eq!( 547 | Line::from("## a header with some **bold**!"), 548 | Line::new_header( 549 | 2, 550 | vec![ 551 | Compound::raw_str("a header with some "), 552 | Compound::raw_str("bold").bold(), 553 | Compound::raw_str("!"), 554 | ] 555 | ) 556 | ); 557 | } 558 | 559 | #[test] 560 | fn table_row() { 561 | assert_eq!( 562 | Line::from("| bla |*italic*|hi!|> some quote"), 563 | Line::new_table_row(vec![ 564 | Composite { 565 | style: CompositeStyle::Paragraph, 566 | compounds: vec![Compound::raw_str("bla"),], 567 | }, 568 | Composite { 569 | style: CompositeStyle::Paragraph, 570 | compounds: vec![Compound::raw_str("italic").italic(),], 571 | }, 572 | Composite { 573 | style: CompositeStyle::Paragraph, 574 | compounds: vec![Compound::raw_str("hi!"),], 575 | }, 576 | Composite { 577 | style: CompositeStyle::Quote, 578 | compounds: vec![Compound::raw_str("some quote"),], 579 | } 580 | ]) 581 | ); 582 | } 583 | 584 | #[test] 585 | fn table_row_issue_4() { 586 | assert_eq!( 587 | Line::from("| 安 | 安 | 安 |"), 588 | Line::new_table_row(vec![ 589 | Composite { 590 | style: CompositeStyle::Paragraph, 591 | compounds: vec![Compound::raw_str("安"),], 592 | }, 593 | Composite { 594 | style: CompositeStyle::Paragraph, 595 | compounds: vec![Compound::raw_str("安"),], 596 | }, 597 | Composite { 598 | style: CompositeStyle::Paragraph, 599 | compounds: vec![Compound::raw_str("安"),], 600 | }, 601 | ]) 602 | ); 603 | } 604 | 605 | #[test] 606 | fn table_alignments() { 607 | assert_eq!( 608 | Line::from("|-----|:--|:-:|----:"), 609 | Line::new_table_alignments(vec![ 610 | Alignment::Unspecified, 611 | Alignment::Left, 612 | Alignment::Center, 613 | Alignment::Right, 614 | ]) 615 | ); 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/template/text_template.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Default)] 4 | struct SubTemplate<'s> { 5 | start_line_idx: usize, 6 | line_count: usize, 7 | name: &'s str, 8 | } 9 | 10 | #[derive(Debug, Default, PartialEq, Eq)] 11 | struct CompoundArg<'s> { 12 | name: &'s str, 13 | line_idx: usize, // index in the line in the template's text 14 | composite_idx: usize, // for when the line is multi-composite (ie it's a tablerow) 15 | compound_idx: usize, // index of the compound in the line's composite 16 | } 17 | 18 | /// a markdown template allowing you to replace some placeholders with 19 | /// given values, or to expand some sub-templates with repetitions 20 | /// (useful with lists, table rows, etc.) 21 | #[derive(Debug)] 22 | pub struct TextTemplate<'s> { 23 | pub text: Text<'s>, 24 | compound_args: Vec>, // replacements of compounds 25 | sub_templates: Vec>, 26 | } 27 | 28 | #[derive(Debug)] 29 | enum SubTemplateToken<'s> { 30 | None, 31 | Start(&'s str), 32 | End, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | struct Replacement<'s, 'b> { 37 | name: &'b str, 38 | value: &'s str, 39 | } 40 | 41 | /// an expander for a sub template. You get it using the `sub` method 42 | /// of the text expander 43 | #[derive(Debug)] 44 | pub struct SubTemplateExpander<'s, 'b> { 45 | name: &'b str, 46 | raw_replacements: Vec>, // replacements which are done as non interpreted compound content 47 | md_replacements: Vec>, 48 | } 49 | 50 | /// an expander you get from a template. You specify replacements 51 | /// on the expander then you ask it the text using `expand` 52 | pub struct TextTemplateExpander<'s, 'b> { 53 | template: &'b TextTemplate<'s>, 54 | text: Text<'s>, 55 | sub_expansions: Vec>, 56 | md_replacements: Vec>, 57 | lines_to_add: Vec>>, 58 | lines_to_exclude: Vec, // true when the line must not be copied into the final text 59 | } 60 | 61 | //------------------------------------------------------------------- 62 | // Template parsing 63 | //------------------------------------------------------------------- 64 | 65 | fn is_valid_name_char(c: char) -> bool { 66 | c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' 67 | } 68 | 69 | fn read_sub_template_token(md_line: &str) -> SubTemplateToken<'_> { 70 | let mut chars = md_line.chars(); 71 | match (chars.next(), chars.next()) { 72 | (Some('$'), Some('{')) => { 73 | // "${" : maybe a sub-template opening 74 | let name = &md_line[2..]; 75 | if !name.is_empty() && name.chars().all(is_valid_name_char) { 76 | SubTemplateToken::Start(name) 77 | } else { 78 | SubTemplateToken::None 79 | } 80 | } 81 | (Some('}'), None) => SubTemplateToken::End, 82 | _ => SubTemplateToken::None, 83 | } 84 | } 85 | 86 | /// find the `${some-name}` arguments in the composite, and add them 87 | /// to args. 88 | fn find_args<'s>( 89 | composite: &mut Composite<'s>, 90 | args: &mut Vec>, 91 | line_idx: usize, 92 | composite_idx: usize, 93 | ) { 94 | let mut compounds = Vec::new(); 95 | for compound in &composite.compounds { 96 | let mut start = 0; 97 | let mut iter = compound.as_str().char_indices(); 98 | while let Some((_, c)) = iter.next() { 99 | if c == '$' { 100 | if let Some((bridx, c)) = iter.next() { 101 | if c == '{' { 102 | for (idx, c) in &mut iter { 103 | if c == '}' { 104 | if idx - bridx > 1 { 105 | if start + 1 < bridx { 106 | compounds.push(compound.sub(start, bridx - 1)); 107 | } 108 | args.push(CompoundArg { 109 | name: &compound.as_str()[bridx + 1..idx], 110 | line_idx, 111 | composite_idx, 112 | compound_idx: compounds.len(), 113 | }); 114 | compounds.push(compound.sub(bridx - 1, idx + 1)); // placeholder 115 | start = idx + 1; 116 | } 117 | break; 118 | } else if !is_valid_name_char(c) { 119 | break; 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | let tail = compound.tail(start); 127 | if !tail.is_empty() { 128 | compounds.push(tail); 129 | } 130 | } 131 | composite.compounds = compounds; 132 | } 133 | 134 | impl<'s> From<&'s str> for TextTemplate<'s> { 135 | /// build a template from a markdown text with placeholders like ${some-name} 136 | /// and sub-templates 137 | fn from(md: &'s str) -> TextTemplate<'s> { 138 | let mut text = Text { lines: Vec::new() }; 139 | let mut compound_args = Vec::new(); 140 | let mut sub_templates = Vec::new(); 141 | let mut current_sub_template: Option> = None; 142 | let mut between_fences = false; 143 | for md_line in clean::lines(md) { 144 | match read_sub_template_token(md_line) { 145 | SubTemplateToken::Start(name) => { 146 | current_sub_template = Some(SubTemplate { 147 | start_line_idx: text.lines.len(), 148 | line_count: 0, 149 | name, 150 | }); 151 | continue; // so to not add the sub-tmpl opening to the text 152 | } 153 | SubTemplateToken::End => { 154 | if current_sub_template.is_some() { 155 | let mut sub_template = current_sub_template.take().unwrap(); 156 | sub_template.line_count = text.lines.len() - sub_template.start_line_idx; 157 | sub_templates.push(sub_template); 158 | continue; // so to not add the sub-tmpl closing to the text 159 | } else { 160 | // we'll assume this `}` isn't part of any templating 161 | } 162 | } 163 | SubTemplateToken::None => {} 164 | } 165 | let line_idx = text.lines.len(); 166 | let parser = parser::LineParser::from(md_line); 167 | let mut line = if between_fences { 168 | parser.as_code() 169 | } else { 170 | parser.line() 171 | }; 172 | match &mut line { 173 | Line::Normal(ref mut composite) => { 174 | find_args(composite, &mut compound_args, line_idx, 0); 175 | text.lines.push(line); 176 | } 177 | Line::TableRow(ref mut table_row) => { 178 | for (composite_idx, composite) in table_row.cells.iter_mut().enumerate() { 179 | find_args(composite, &mut compound_args, line_idx, composite_idx); 180 | } 181 | text.lines.push(line); 182 | } 183 | Line::CodeFence(..) => { 184 | between_fences = !between_fences; 185 | } 186 | _ => { 187 | text.lines.push(line); 188 | } 189 | } 190 | } 191 | TextTemplate { 192 | text, 193 | compound_args, 194 | sub_templates, 195 | } 196 | } 197 | } 198 | 199 | impl<'s> TextTemplate<'s> { 200 | /// return a new expander for the template 201 | pub fn expander<'b>(&'b self) -> TextTemplateExpander<'s, 'b> { 202 | TextTemplateExpander::from(self) 203 | } 204 | 205 | /// if the line `line_idx` is part of a sub template, return this 206 | /// template's index. Return None if it's not part of a template. 207 | /// 208 | /// This might be optimized by an internal vec in the future (or 209 | /// by just having another structure than a Text as internal md 210 | /// storage) 211 | fn get_sub_of_line( 212 | &self, 213 | line_idx: usize, 214 | ) -> Option { 215 | for (sub_idx, sub_template) in self.sub_templates.iter().enumerate() { 216 | if line_idx >= sub_template.start_line_idx 217 | && line_idx < sub_template.start_line_idx + sub_template.line_count 218 | { 219 | return Some(sub_idx); 220 | } 221 | } 222 | None 223 | } 224 | } 225 | 226 | //------------------------------------------------------------------- 227 | // Expansion 228 | //------------------------------------------------------------------- 229 | 230 | impl<'s, 'b> From<&'b TextTemplate<'s>> for TextTemplateExpander<'s, 'b> { 231 | /// Build a new expander for the template. The expander stores the additions 232 | /// done with `set`, `set_md`, `set_lines` or in the `sub` expanders. 233 | fn from(template: &'b TextTemplate<'s>) -> Self { 234 | // line insertion (from subtemplates and from set_lines) as well 235 | // as line removals are postponed until final text building so 236 | // that line indexes stay valid until that point). We just note 237 | // what lines to add to or exclude from the final text. 238 | let line_count = template.text.lines.len(); 239 | let lines_to_add = vec![Vec::new(); line_count]; 240 | let lines_to_exclude = vec![false; line_count]; 241 | Self { 242 | template, 243 | text: template.text.clone(), 244 | sub_expansions: Vec::new(), 245 | md_replacements: Vec::new(), 246 | lines_to_add, 247 | lines_to_exclude, 248 | } 249 | } 250 | } 251 | 252 | impl<'s, 'b> SubTemplateExpander<'s, 'b> { 253 | /// replace placeholders with name `name` with the given value, not interpreted as markdown 254 | pub fn set( 255 | &mut self, 256 | name: &'b str, 257 | value: &'s str, 258 | ) -> &mut SubTemplateExpander<'s, 'b> { 259 | self.raw_replacements.push(Replacement { name, value }); 260 | self 261 | } 262 | /// replace placeholder with name `name` with the given value, interpreted as markdown 263 | pub fn set_md( 264 | &mut self, 265 | name: &'b str, 266 | value: &'s str, 267 | ) -> &mut SubTemplateExpander<'s, 'b> { 268 | self.md_replacements.push(Replacement { name, value }); 269 | self 270 | } 271 | } 272 | 273 | fn set_in_line<'s>( 274 | line: &mut Line<'s>, 275 | compound_arg: &CompoundArg<'s>, 276 | value: &'s str, 277 | ) { 278 | match line { 279 | Line::Normal(composite) => { 280 | composite.compounds[compound_arg.compound_idx].set_str(value); 281 | } 282 | Line::TableRow(table_row) => { 283 | table_row.cells[compound_arg.composite_idx].compounds[compound_arg.compound_idx] 284 | .set_str(value); 285 | } 286 | _ => {} 287 | } 288 | } 289 | 290 | fn set_in_text<'s>( 291 | template: &TextTemplate<'s>, 292 | text: &mut Text<'s>, 293 | line_offset: usize, 294 | name: Option<&str>, 295 | value: &'s str, 296 | ) { 297 | for compound_arg in &template.compound_args { 298 | if name.is_none() || name == Some(compound_arg.name) { 299 | let idx = compound_arg.line_idx; 300 | if idx < line_offset || idx - line_offset >= text.lines.len() { 301 | continue; // can happen if a replacement name is present in the outside text 302 | } 303 | set_in_line(&mut text.lines[idx - line_offset], compound_arg, value); 304 | } 305 | } 306 | } 307 | 308 | #[allow(clippy::needless_lifetimes)] 309 | fn set_all_md_in_text<'s, 'b>( 310 | template: &TextTemplate<'s>, 311 | text: &mut Text<'s>, 312 | line_offset: usize, 313 | md_replacements: &[Replacement<'s, 'b>], 314 | ) { 315 | if md_replacements.is_empty() { 316 | return; // no need to iterate over all compound_args 317 | } 318 | for compound_arg in template.compound_args.iter().rev() { 319 | let idx = compound_arg.line_idx; 320 | if idx < line_offset || idx - line_offset >= text.lines.len() { 321 | continue; 322 | } 323 | for md_repl in md_replacements { 324 | if md_repl.name == compound_arg.name { 325 | let replacing_composite = Composite::from_inline(md_repl.value); 326 | // we replace the compound with the ones of the parsed value 327 | let patched_line = &mut text.lines[idx - line_offset]; 328 | match patched_line { 329 | Line::Normal(ref mut composite) => { 330 | replace_compound( 331 | composite, 332 | compound_arg.compound_idx, 333 | replacing_composite.compounds, 334 | ); 335 | } 336 | Line::TableRow(ref mut table_row) => { 337 | replace_compound( 338 | &mut table_row.cells[compound_arg.composite_idx], 339 | compound_arg.compound_idx, 340 | replacing_composite.compounds, 341 | ); 342 | } 343 | _ => {} 344 | } 345 | break; // it's not possible to apply two replacements to the compound 346 | } 347 | } 348 | } 349 | } 350 | 351 | /// replace a compound with several other ones. 352 | /// Do nothing if the passed compounds vec is empty. 353 | fn replace_compound<'s>( 354 | composite: &mut Composite<'s>, // composite in which to do the replacement 355 | mut compound_idx: usize, // index in the composite of the compound to remove 356 | mut replacing_compounds: Vec>, // the compounds taking the place of the removed one 357 | ) { 358 | let mut replacing_compounds = replacing_compounds.drain(..); 359 | if let Some(compound) = replacing_compounds.next() { 360 | composite.compounds[compound_idx] = compound; 361 | for compound in replacing_compounds { 362 | compound_idx += 1; 363 | composite.compounds.insert(compound_idx, compound); 364 | } 365 | } 366 | } 367 | 368 | impl<'s, 'b> TextTemplateExpander<'s, 'b> { 369 | /// replace placeholders with name `name` with the given value, non interpreted 370 | /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template) 371 | pub fn set( 372 | &mut self, 373 | name: &str, 374 | value: &'s str, 375 | ) -> &mut TextTemplateExpander<'s, 'b> { 376 | set_in_text(self.template, &mut self.text, 0, Some(name), value); 377 | self 378 | } 379 | 380 | /// replace all placeholders with the given value, non interpreted 381 | /// (i.e. stars, backquotes, etc. don't mess the styling defined by the template). 382 | /// This can be used at start to have a "default" value. 383 | pub fn set_all( 384 | &mut self, 385 | value: &'s str, 386 | ) -> &mut TextTemplateExpander<'s, 'b> { 387 | set_in_text(self.template, &mut self.text, 0, None, value); 388 | self 389 | } 390 | 391 | /// replace placeholders with name `name` with the given value, interpreted as markdown 392 | pub fn set_md( 393 | &mut self, 394 | name: &'b str, 395 | value: &'s str, 396 | ) -> &mut TextTemplateExpander<'s, 'b> { 397 | self.md_replacements.push(Replacement { name, value }); 398 | self 399 | } 400 | 401 | /// replace a placeholder with several lines. 402 | /// This is mostly useful when the placeholder is a repeatable line (code, list item) 403 | pub fn set_lines( 404 | &mut self, 405 | name: &'b str, 406 | raw_lines: &'s str, 407 | ) -> &mut TextTemplateExpander<'s, 'b> { 408 | for compound_arg in &self.template.compound_args { 409 | if compound_arg.name == name { 410 | // the line holding the compound is now considered a template, it's removed 411 | self.lines_to_exclude[compound_arg.line_idx] = true; 412 | for value in clean::lines(raw_lines) { 413 | let mut line = self.text.lines[compound_arg.line_idx].clone(); 414 | set_in_line(&mut line, compound_arg, value); 415 | self.lines_to_add[compound_arg.line_idx].push(line); 416 | } 417 | } 418 | } 419 | self 420 | } 421 | 422 | /// replace a placeholder with several lines interpreted as markdown 423 | pub fn set_lines_md( 424 | &mut self, 425 | name: &'b str, 426 | md: &'s str, 427 | ) -> &mut TextTemplateExpander<'s, 'b> { 428 | for compound_arg in &self.template.compound_args { 429 | if compound_arg.name == name { 430 | // the line holding the compound is now considered a template, it's removed 431 | self.lines_to_exclude[compound_arg.line_idx] = true; 432 | for line in md.lines().map(|md| parser::LineParser::from(md).line()) { 433 | self.lines_to_add[compound_arg.line_idx].push(line); 434 | } 435 | } 436 | } 437 | self 438 | } 439 | 440 | /// prepare expansion of a sub template and return a mutable reference to the 441 | /// object in which to set compound replacements 442 | pub fn sub( 443 | &mut self, 444 | name: &'b str, 445 | ) -> &mut SubTemplateExpander<'s, 'b> { 446 | let sub = SubTemplateExpander { 447 | name, 448 | raw_replacements: Vec::new(), 449 | md_replacements: Vec::new(), 450 | }; 451 | let idx = self.sub_expansions.len(); 452 | self.sub_expansions.push(sub); 453 | &mut self.sub_expansions[idx] 454 | } 455 | 456 | /// build a text by applying the replacements to the initial template 457 | pub fn expand(mut self) -> Text<'s> { 458 | // The simple replacements defined with expander.set(name, value) have 459 | // already be done at this point. 460 | 461 | // We triage the md_replacements: we can directly apply the ones which 462 | // are not applied to sub templates and we must defer the other ones 463 | // to the sub templates expansion phase 464 | let mut defered_repls: Vec>> = 465 | vec![Vec::new(); self.template.sub_templates.len()]; 466 | for compound_arg in self.template.compound_args.iter().rev() { 467 | let line_idx = compound_arg.line_idx; 468 | let sub_idx = self.template.get_sub_of_line(line_idx); 469 | for md_repl in &self.md_replacements { 470 | if md_repl.name == compound_arg.name { 471 | if let Some(sub_idx) = sub_idx { 472 | // we must clone because a repl can be applied several times 473 | defered_repls[sub_idx].push(md_repl.clone()); 474 | } else { 475 | let replacing_composite = Composite::from_inline(md_repl.value); 476 | // we replace the compound with the ones of the parsed value 477 | let patched_line = &mut self.text.lines[line_idx]; 478 | match patched_line { 479 | Line::Normal(ref mut composite) => { 480 | replace_compound( 481 | composite, 482 | compound_arg.compound_idx, 483 | replacing_composite.compounds, 484 | ); 485 | } 486 | Line::TableRow(ref mut table_row) => { 487 | replace_compound( 488 | &mut table_row.cells[compound_arg.composite_idx], 489 | compound_arg.compound_idx, 490 | replacing_composite.compounds, 491 | ); 492 | } 493 | _ => {} 494 | } 495 | break; // it's not possible to apply two replacements to the compound 496 | } 497 | } 498 | } 499 | } 500 | 501 | for (sub_idx, sub_template) in self.template.sub_templates.iter().enumerate() { 502 | let start = sub_template.start_line_idx; 503 | let end = start + sub_template.line_count; 504 | // we remove the lines of the subtemplate from the main text 505 | for idx in start..end { 506 | self.lines_to_exclude[idx] = true; 507 | } 508 | for sub_expansion in &self.sub_expansions { 509 | if sub_expansion.name != sub_template.name { 510 | continue; 511 | } 512 | let mut sub_text = Text::default(); 513 | for line in &self.text.lines[start..end] { 514 | sub_text.lines.push(line.clone()); 515 | } 516 | for repl in &sub_expansion.raw_replacements { 517 | set_in_text( 518 | self.template, 519 | &mut sub_text, 520 | sub_template.start_line_idx, 521 | Some(repl.name), 522 | repl.value, 523 | ); 524 | } 525 | let mut md_replacements = sub_expansion.md_replacements.clone(); 526 | md_replacements.extend(defered_repls[sub_idx].clone()); 527 | set_all_md_in_text( 528 | self.template, 529 | &mut sub_text, 530 | sub_template.start_line_idx, 531 | &md_replacements, 532 | ); 533 | for line in sub_text.lines.drain(..) { 534 | self.lines_to_add[sub_template.start_line_idx].push(line); 535 | } 536 | } 537 | } 538 | 539 | // we now do the removals and insertions we deffered until then. 540 | let mut lines = Vec::new(); 541 | for (idx, line) in self.text.lines.drain(..).enumerate() { 542 | if !self.lines_to_exclude[idx] { 543 | lines.push(line); 544 | } 545 | lines.append(&mut self.lines_to_add[idx]); 546 | } 547 | Text { lines } 548 | } 549 | } 550 | --------------------------------------------------------------------------------