├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── src ├── main.rs ├── tokenize.rs ├── gen.rs └── parse.rs ├── LICENCE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "mdtg" 5 | version = "1.0.1" 6 | 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mdtg" 3 | version = "1.0.1" 4 | authors = ["lukakerr "] 5 | description = "Quickly generate markdown tables in your terminal using a simplistic specification" 6 | homepage = "https://github.com/lukakerr/mdtg" 7 | repository = "https://github.com/lukakerr/mdtg" 8 | readme = "./README.md" 9 | keywords = ["markdown", "table", "terminal", "bnf"] 10 | categories = ["command-line-utilities", "parser-implementations", "text-processing"] 11 | license = "MIT" 12 | 13 | [dependencies] 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod gen; 2 | mod parse; 3 | mod tokenize; 4 | 5 | use gen::gen; 6 | use parse::parse; 7 | use std::env::args; 8 | use std::process::exit; 9 | use tokenize::tokenize; 10 | 11 | fn main() { 12 | let args: Vec = args().collect(); 13 | 14 | if args.len() <= 1 { 15 | return; 16 | } 17 | 18 | match tokenize(&args[1]) { 19 | Ok(tokens) => match parse(tokens) { 20 | Ok(ast) => { 21 | let output = gen(&ast); 22 | println!("{}", output); 23 | } 24 | Err(err) => report_err(err), 25 | }, 26 | Err(err) => report_err(err), 27 | } 28 | } 29 | 30 | fn report_err(err: String) { 31 | eprintln!("Error: {}", err); 32 | exit(1); 33 | } 34 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Luka Kerr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Table Generator `mdtg` 2 | 3 | [![Crates.io](https://img.shields.io/crates/d/mdtg.svg)](https://crates.io/crates/mdtg) 4 | 5 | Quickly generate markdown tables in your terminal using a simplistic specification. 6 | 7 | ### Installation 8 | 9 | #### `crates.io` 10 | 11 | ```bash 12 | $ cargo install mdtg 13 | ``` 14 | 15 | #### Manual 16 | 17 | ```bash 18 | $ git clone https://github.com/lukakerr/mdtg.git 19 | $ cargo run 20 | ``` 21 | 22 | ### Usage 23 | 24 | ```bash 25 | # Create a 4 by 4 table, without spaces in argument 26 | $ mdtg 4x4 27 | 28 | | | | | | 29 | | ------ | ------ | ------ | ------ | 30 | | | | | | 31 | | | | | | 32 | | | | | | 33 | | | | | | 34 | 35 | # Create a 3 by 5 table, with left, center and right aligned columns 36 | $ mdtg "3lcr x 5" 37 | 38 | | | | | 39 | | ------ |:------:| ------:| 40 | | | | | 41 | | | | | 42 | | | | | 43 | | | | | 44 | | | | | 45 | ``` 46 | 47 | ### BNF Grammar 48 | 49 | ``` 50 | Table -> Column Cross Row 51 | Column -> Num Positions 52 | Row -> Num 53 | Num -> Digit | Digit Num 54 | Digit -> [0-9] 55 | Cross -> "x" 56 | Positions -> Position | Position Positions 57 | Position -> "l" | "r" | "c" 58 | ``` 59 | -------------------------------------------------------------------------------- /src/tokenize.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Peekable; 2 | 3 | /// Represents an input token 4 | #[derive(Debug, PartialEq)] 5 | pub enum Token { 6 | Cross, 7 | Num(usize), 8 | Position(char), 9 | } 10 | 11 | /// Given a string, return a list of tokens representing that string, 12 | /// or an error if one has occurred 13 | pub fn tokenize(input: &str) -> Result, String> { 14 | let mut tokens = Vec::::new(); 15 | 16 | let mut it = input.chars().peekable(); 17 | 18 | while let Some(&c) = it.peek() { 19 | match c { 20 | 'x' => { 21 | tokens.push(Token::Cross); 22 | it.next(); 23 | } 24 | 'l' | 'c' | 'r' => { 25 | tokens.push(Token::Position(c)); 26 | it.next(); 27 | } 28 | '1'...'9' => { 29 | it.next(); 30 | let n = get_num(c, &mut it); 31 | tokens.push(Token::Num(n)); 32 | } 33 | ' ' => { 34 | it.next(); 35 | } 36 | '0' => return Err("Column/row number must be > 0".to_string()), 37 | _ => return Err(format!("Unexpected input '{}'", c)), 38 | } 39 | } 40 | 41 | Ok(tokens) 42 | } 43 | 44 | fn get_num>(c: char, iter: &mut Peekable) -> usize { 45 | let mut n = c.to_digit(10).unwrap() as usize; 46 | 47 | while let Some(Ok(digit)) = iter.peek().map(|c| c.to_string().parse::()) { 48 | n = n * 10 + digit; 49 | iter.next(); 50 | } 51 | 52 | n 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | 59 | #[test] 60 | fn test_simple_table() { 61 | let input = "3 x 5"; 62 | assert_eq!( 63 | tokenize(&input), 64 | Ok(vec![Token::Num(3), Token::Cross, Token::Num(5)]) 65 | ); 66 | } 67 | 68 | #[test] 69 | fn test_complex_table() { 70 | let input = "6lcr x 2"; 71 | assert_eq!( 72 | tokenize(&input), 73 | Ok(vec![ 74 | Token::Num(6), 75 | Token::Position('l'), 76 | Token::Position('c'), 77 | Token::Position('r'), 78 | Token::Cross, 79 | Token::Num(2) 80 | ]) 81 | ); 82 | } 83 | 84 | #[test] 85 | fn test_invalid_table() { 86 | let input = "3a x 5"; 87 | assert!(tokenize(&input).is_err()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/gen.rs: -------------------------------------------------------------------------------- 1 | use parse::{Node, AST}; 2 | 3 | static SPACE: &str = " "; 4 | static COLON: &str = ":"; 5 | static DASHES: &str = "------"; 6 | static INDENT: &str = " "; 7 | 8 | /// Generate a markdown table given an AST 9 | pub fn gen(ast: &Node) -> String { 10 | let mut output = String::new(); 11 | 12 | gen_table(ast, &mut output); 13 | 14 | output 15 | } 16 | 17 | fn gen_table(ast: &Node, output: &mut String) { 18 | gen_header(&ast, output); 19 | gen_rows(&ast, output); 20 | } 21 | 22 | fn gen_header(ast: &Node, output: &mut String) { 23 | let column_node = &ast.children[0]; 24 | 25 | if let AST::Column(c) = column_node.item { 26 | gen_row(c, true, output); 27 | gen_positions(&column_node, output); 28 | } 29 | } 30 | 31 | fn gen_positions(ast: &Node, output: &mut String) { 32 | if let AST::Column(c) = ast.item { 33 | let positional_row = |left, right| [left, DASHES, right].concat(); 34 | 35 | for i in 0..c { 36 | output.push_str("|"); 37 | 38 | if let Some(position_node) = ast.children.get(i) { 39 | if let AST::Position(p) = position_node.item { 40 | match p { 41 | 'l' => output.push_str(&positional_row(SPACE, SPACE)), 42 | 'c' => output.push_str(&positional_row(COLON, COLON)), 43 | 'r' => output.push_str(&positional_row(SPACE, COLON)), 44 | _ => {} 45 | } 46 | } 47 | } else { 48 | output.push_str(&positional_row(SPACE, SPACE)); 49 | } 50 | } 51 | 52 | output.push_str("|\n"); 53 | } 54 | } 55 | 56 | fn gen_rows(ast: &Node, output: &mut String) { 57 | let column_node = &ast.children[0]; 58 | let row_node = &ast.children[2]; 59 | 60 | if let AST::Column(c) = column_node.item { 61 | if let AST::Row(r) = row_node.item { 62 | for _ in 0..(r - 1) { 63 | gen_row(c, true, output); 64 | } 65 | 66 | gen_row(c, false, output); 67 | } 68 | } 69 | } 70 | 71 | fn gen_row(n: usize, newline: bool, output: &mut String) { 72 | for _ in 0..n { 73 | output.push_str("|"); 74 | output.push_str(INDENT); 75 | } 76 | 77 | let last = if newline { "|\n" } else { "|" }; 78 | output.push_str(last); 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn test_simple_table() { 87 | let mut table_node = Node::new(AST::Table); 88 | let column_node = Node::new(AST::Column(3)); 89 | let cross_node = Node::new(AST::Cross); 90 | let row_node = Node::new(AST::Row(5)); 91 | 92 | table_node.add_children(&[column_node, cross_node, row_node]); 93 | 94 | assert_eq!( 95 | gen(&table_node), 96 | "\ 97 | | | | |\n\ 98 | | ------ | ------ | ------ |\n\ 99 | | | | |\n\ 100 | | | | |\n\ 101 | | | | |\n\ 102 | | | | |\n\ 103 | | | | |" 104 | .to_string() 105 | ); 106 | } 107 | 108 | #[test] 109 | fn test_complex_table() { 110 | let mut table_node = Node::new(AST::Table); 111 | let mut column_node = Node::new(AST::Column(6)); 112 | let cross_node = Node::new(AST::Cross); 113 | let row_node = Node::new(AST::Row(2)); 114 | 115 | let left_position_node = Node::new(AST::Position('l')); 116 | let center_position_node = Node::new(AST::Position('c')); 117 | let right_position_node = Node::new(AST::Position('r')); 118 | 119 | column_node.add_children(&[ 120 | left_position_node, 121 | center_position_node, 122 | right_position_node, 123 | ]); 124 | table_node.add_children(&[column_node, cross_node, row_node]); 125 | 126 | assert_eq!( 127 | gen(&table_node), 128 | "\ 129 | | | | | | | |\n\ 130 | | ------ |:------:| ------:| ------ | ------ | ------ |\n\ 131 | | | | | | | |\n\ 132 | | | | | | | |" 133 | .to_string() 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use tokenize::Token; 2 | 3 | /// Represents an AST type 4 | #[derive(Debug, Clone, PartialEq)] 5 | pub enum AST { 6 | Column(usize), 7 | Row(usize), 8 | Position(char), 9 | Cross, 10 | Table, 11 | } 12 | 13 | /// Represents an AST node, containing an AST item and a list of children nodes 14 | #[derive(Debug, Clone, PartialEq)] 15 | pub struct Node { 16 | pub children: Vec, 17 | pub item: AST, 18 | } 19 | 20 | impl Node { 21 | /// Create a new `Node` using a given item 22 | pub fn new(item: AST) -> Node { 23 | Node { 24 | children: Vec::new(), 25 | item, 26 | } 27 | } 28 | 29 | /// Given a slice of nodes, add them all to the nodes children vec 30 | pub fn add_children(&mut self, children: &[Node]) { 31 | self.children.extend_from_slice(children); 32 | } 33 | } 34 | 35 | /// Parse a list of tokens into an AST. 36 | /// 37 | /// Returns a node representing the markdown table specification AST, 38 | /// or a string representing an error that has occurred during parsing. 39 | /// 40 | /// For example, an AST returned could look like 41 | /// 42 | /// ``` 43 | /// Node { 44 | /// children: [ 45 | /// Node { 46 | /// children: [ 47 | /// Node { children: [], item: Position('c') }, 48 | /// Node { children: [], item: Position('r') } 49 | /// ], 50 | /// item: Columns(3) 51 | /// }, 52 | /// Node { children: [], item: Cross }, 53 | /// Node { children: [], item: Rows(5) } 54 | /// ], 55 | /// item: Table 56 | /// } 57 | /// ``` 58 | pub fn parse(mut tokens: Vec) -> Result { 59 | tokens.reverse(); 60 | parse_table(&mut tokens) 61 | } 62 | 63 | fn parse_table(tokens: &mut Vec) -> Result { 64 | let columns_node = parse_columns(tokens)?; 65 | let cross_node = parse_cross(tokens)?; 66 | let rows_node = parse_rows(tokens)?; 67 | 68 | let mut table_node = Node::new(AST::Table); 69 | 70 | table_node.add_children(&[columns_node, cross_node, rows_node]); 71 | 72 | Ok(table_node) 73 | } 74 | 75 | fn parse_columns(tokens: &mut Vec) -> Result { 76 | let tok = tokens.pop(); 77 | 78 | fn is_position(tok: &Token) -> bool { 79 | if let Token::Position(_) = tok { 80 | true 81 | } else { 82 | false 83 | } 84 | } 85 | 86 | if let Some(Token::Num(n)) = tok { 87 | let mut columns_node = Node::new(AST::Column(n)); 88 | 89 | while tokens.last().map_or(false, is_position) { 90 | if let Some(Token::Position(p)) = tokens.pop() { 91 | let position_node = Node::new(AST::Position(p)); 92 | columns_node.add_children(&[position_node]); 93 | } 94 | } 95 | 96 | if columns_node.children.len() > n as usize { 97 | Err("Number of positions exceed number of columns".to_string()) 98 | } else { 99 | Ok(columns_node) 100 | } 101 | } else { 102 | Err("Expected a column number".to_string()) 103 | } 104 | } 105 | 106 | fn parse_cross(tokens: &mut Vec) -> Result { 107 | let tok = tokens.pop(); 108 | 109 | if let Some(Token::Cross) = tok { 110 | Ok(Node::new(AST::Cross)) 111 | } else { 112 | Err("Expected 'x'".to_string()) 113 | } 114 | } 115 | 116 | fn parse_rows(tokens: &mut Vec) -> Result { 117 | let tok = tokens.pop(); 118 | 119 | if let Some(Token::Num(n)) = tok { 120 | Ok(Node::new(AST::Row(n))) 121 | } else { 122 | Err("Expected a row number".to_string()) 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | 130 | #[test] 131 | fn test_simple_table() { 132 | let tokens = vec![Token::Num(3), Token::Cross, Token::Num(5)]; 133 | 134 | let mut table_node = Node::new(AST::Table); 135 | let column_node = Node::new(AST::Column(3)); 136 | let cross_node = Node::new(AST::Cross); 137 | let row_node = Node::new(AST::Row(5)); 138 | 139 | table_node.add_children(&[column_node, cross_node, row_node]); 140 | 141 | assert_eq!(parse(tokens), Ok(table_node)); 142 | } 143 | 144 | #[test] 145 | fn test_complex_table() { 146 | let tokens = vec![ 147 | Token::Num(6), 148 | Token::Position('l'), 149 | Token::Position('c'), 150 | Token::Position('r'), 151 | Token::Cross, 152 | Token::Num(2), 153 | ]; 154 | 155 | let mut table_node = Node::new(AST::Table); 156 | let mut column_node = Node::new(AST::Column(6)); 157 | let cross_node = Node::new(AST::Cross); 158 | let row_node = Node::new(AST::Row(2)); 159 | 160 | let left_position_node = Node::new(AST::Position('l')); 161 | let center_position_node = Node::new(AST::Position('c')); 162 | let right_position_node = Node::new(AST::Position('r')); 163 | 164 | column_node.add_children(&[ 165 | left_position_node, 166 | center_position_node, 167 | right_position_node, 168 | ]); 169 | table_node.add_children(&[column_node, cross_node, row_node]); 170 | 171 | assert_eq!(parse(tokens), Ok(table_node)); 172 | } 173 | 174 | #[test] 175 | fn test_missing_x_table() { 176 | let tokens = vec![Token::Num(3), Token::Num(5)]; 177 | assert_eq!(parse(tokens), Err("Expected 'x'".to_string())); 178 | } 179 | 180 | #[test] 181 | fn test_missing_column_table() { 182 | let tokens = vec![Token::Cross, Token::Num(5)]; 183 | assert_eq!(parse(tokens), Err("Expected a column number".to_string())); 184 | } 185 | 186 | #[test] 187 | fn test_missing_row_table() { 188 | let tokens = vec![Token::Num(3), Token::Cross]; 189 | assert_eq!(parse(tokens), Err("Expected a row number".to_string())); 190 | } 191 | 192 | #[test] 193 | fn test_excess_positions_table() { 194 | let tokens = vec![ 195 | Token::Num(1), 196 | Token::Position('c'), 197 | Token::Position('c'), 198 | Token::Cross, 199 | ]; 200 | assert_eq!( 201 | parse(tokens), 202 | Err("Number of positions exceed number of columns".to_string()) 203 | ); 204 | } 205 | } 206 | --------------------------------------------------------------------------------