diff options
author | Kevin Gimbel <hallo@kevingimbel.com> | 2019-12-04 17:53:38 +0100 |
---|---|---|
committer | Kevin Gimbel <hallo@kevingimbel.com> | 2019-12-04 17:53:38 +0100 |
commit | a54cfc37863bdd8a4322770589d78cf9bd479b9e (patch) | |
tree | 51e6306d08ea347dc925067c994fe07d334747f0 | |
parent | 5fafefc4325cd6f65fc92e9092e2e02be38351bd (diff) |
Fix #1, add depth arguments
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | README.md | 35 | ||||
-rw-r--r-- | mktoc.iml | 15 | ||||
-rw-r--r-- | src/bin.rs | 8 | ||||
-rw-r--r-- | src/lib.rs | 87 | ||||
-rw-r--r-- | tests/files/README_02.md | 42 | ||||
-rw-r--r-- | tests/files/README_04_code_block_issues.md | 24 |
8 files changed, 154 insertions, 61 deletions
@@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "mktoc" -version = "1.0.0" +version = "1.1.0" dependencies = [ "criterion 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2,7 +2,7 @@ name = "mktoc" description = "Generate Table of Contents from Markdown files" license = "MIT" -version = "1.0.0" +version = "1.1.0" authors = ["Kevin Gimbel <hallo@kevingimbel.com>"] edition = "2018" @@ -1,5 +1,5 @@ # `mktoc` -> Blazingly fast Table of Content generator +> Blazingly fast Markdown Table of Content generator ![](https://github.com/kevingimbel/mktoc/workflows/Clippy%20check/badge.svg) @@ -16,11 +16,11 @@ ## About -`mktoc` parses markdown files and generates a Table Of Content linking all headlines up to heading level 6 deep. +`mktoc` parses markdown files and generates a Table Of Content linking all headlines up to heading level 6 deep, or as specified by command line arguments. A start depth and maximum depth can be specified. ## Installation -`mktoc` can be installed using Cargo, the Rust package manager, or by downloading a binary from GitHub. +`mktoc` can be installed using Cargo, the Rust package manager. ### Cargo @@ -30,20 +30,43 @@ $ cargo install mktoc ### Binary -Download latest release from [https://github.com/kevingimbel/mktoc/releases](https://github.com/kevingimbel/mktoc/releases) and place it somewhere in your `PATH`, e.g. `/usr/local/bin`. +Binaries are actually not available yet. If you know how releasing binaries with Rust can be implemented, please let me know! ## Usage Specify `--write` to overwrite the given file, otherwise the modified content is written to stdout. ``` -# mktoc [--write] <FILE> +# mktoc [--write] [--max-depth|-M] [--min-depth|-m] <FILE> $ mktoc --write README.md +$ mktoc --write -m 2 -M 4 README.md ``` See `mktoc --help` for list of all arguments and flags. +``` +mktoc 1.1.0 + +USAGE: + mktoc [FLAGS] [OPTIONS] <file> + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + -w, --write + +OPTIONS: + -M, --max-depth <max-depth> [default: 6] + -m, --min-depth <min-depth> [default: 1] + +ARGS: + <file> +``` ## Performance -`mktoc` is blazingly fast. Large files such as the README examples in `tests/files/` render in 0.009s (9ms) on average. +`mktoc` is fast but can probably be even faster! Pull Requests and bug reports are appreciated! + +## License + +MIT, see LICENSE file.
\ No newline at end of file diff --git a/mktoc.iml b/mktoc.iml new file mode 100644 index 0000000..7fe828a --- /dev/null +++ b/mktoc.iml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="RUST_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file @@ -8,6 +8,12 @@ struct Cli { #[structopt(long, short)] write: bool, + + #[structopt(long, short="m", default_value="1")] + min_depth: i32, + + #[structopt(long, short="M", default_value="6")] + max_depth: i32, } fn handle_write(new_toc: String) { @@ -30,7 +36,7 @@ fn handle_write(new_toc: String) { fn main() { let opts = Cli::from_args(); - let res = mktoc::make_toc(opts.file); + let res = mktoc::make_toc(opts.file, opts.min_depth, opts.max_depth); match res { Ok(new_toc) => { @@ -16,6 +16,7 @@ struct Cli { const COMMENT_BEGIN: &str = "<!-- BEGIN mktoc -->"; const COMMENT_END: &str = "<!-- END mktoc -->"; +/// reads a file into a mutable string fn read_file(file_path: String) -> Result<String, ::std::io::Error> { let mut file = File::open(file_path)?; let mut contents = String::new(); @@ -23,33 +24,62 @@ fn read_file(file_path: String) -> Result<String, ::std::io::Error> { Ok(contents) } - -fn generate_toc(original_content: String) -> String { - // @TODO: This RegEx creates weird outputs if the headline contains additional markdown, like images - let re = regex::Regex::new(r"((#{1,6}\s))((.*))").unwrap(); +/// parses a string and extracts all headlines to build a table of contents +/// +/// Uses basic regex "((#{1,6}\s))((.*))" to parse headings. Right now this produces errors because it also matches +/// comments in code blocks and if the headline contains images those will be matched, too. +pub fn generate_toc(original_content: String, min_depth: i32, max_depth: i32) -> String { + let mut already_found_code_open = false; + let mut code_block_found = false; let mut new_toc = String::from(COMMENT_BEGIN); - // let caps = re.captures(content.as_str()); - for caps in re.captures_iter(original_content.as_str()) { - let level: usize = caps.get(2).unwrap().as_str().chars().count() - 1; - let text = caps.get(3).unwrap().as_str(); - // @TODO: Use real URL encoding - let link = text.replace(" ", "-").to_ascii_lowercase(); - // let spaces = " ".repeat(level -1); - let spaces = match level { - 3 => String::from(" "), - 4 => String::from(" "), - 5 => String::from(" "), - 6 => String::from(" "), - _ => String::from(""), - }; - - new_toc = format!( - "{old}\n{spaces}- [{text}](#{link})", - old = new_toc, - spaces = spaces, - text = text, - link = link - ); + let re = regex::Regex::new(r"((#{1,6}\s))((.*))").unwrap(); + for line in original_content.lines() { + + if line.starts_with("```") { + code_block_found = true; + } + + if !code_block_found && !already_found_code_open { + if line.starts_with("#") { + let caps = re.captures(line).unwrap(); + let level: i32 = (caps.get(2).unwrap().as_str().chars().count() - 1) as i32; + if level < min_depth { + continue; + } + + if level > max_depth { + continue; + } + + + let text = caps.get(3).unwrap().as_str(); + let link = text.replace(" ", "-").to_ascii_lowercase(); + let spaces = match level { + 3 => String::from(" "), + 4 => String::from(" "), + 5 => String::from(" "), + 6 => String::from(" "), + _ => String::from(""), + }; + new_toc = format!( + "{old}\n{spaces}- [{text}](#{link})", + old = new_toc.as_str(), + spaces = spaces, + text = text, + link = link + ); + } + } + + if code_block_found && already_found_code_open { + code_block_found = false; + already_found_code_open = false; + } + + if line.starts_with("```") { + already_found_code_open = true; + } + } new_toc = format!("{}\n{}", new_toc, COMMENT_END); @@ -57,9 +87,10 @@ fn generate_toc(original_content: String) -> String { new_toc } -pub fn make_toc(file_path_in: String) -> Result<String, ::std::io::Error> { +/// takes a file path as `String` and returns a table of contents for the file +pub fn make_toc(file_path_in: String, min_depth: i32, max_depth: i32) -> Result<String, ::std::io::Error> { let content = read_file(file_path_in)?; - let new_toc = generate_toc(content.to_owned()); + let new_toc = generate_toc(content.to_owned(), min_depth, max_depth); let re_toc = regex::Regex::new(r"(?ms)^(<!-- BEGIN mktoc).*(END mktoc -->)").unwrap(); let res: String = re_toc .replace_all(content.as_str(), new_toc.as_str()) diff --git a/tests/files/README_02.md b/tests/files/README_02.md index 1fc07b2..be4a991 100644 --- a/tests/files/README_02.md +++ b/tests/files/README_02.md @@ -2,31 +2,25 @@ This README.md is taken from [https://github.com/terraform-aws-modules/terraform --- <!-- BEGIN mktoc --> -- [AWS VPC Terraform module](#AWS-VPC-Terraform-module) -- [Terraform versions](#Terraform-versions) -- [Usage](#Usage) -- [External NAT Gateway IPs](#External-NAT-Gateway-IPs) -- [The rest of arguments are omitted for brevity](#The-rest-of-arguments-are-omitted-for-brevity) -- [<= Skip creation of EIPs for the NAT Gateways](#<=-Skip-creation-of-EIPs-for-the-NAT-Gateways) -- [<= IPs specified here as input to the module](#<=-IPs-specified-here-as-input-to-the-module) -- [NAT Gateway Scenarios](#NAT-Gateway-Scenarios) - - [One NAT Gateway per subnet (default)](#One-NAT-Gateway-per-subnet-(default)) - - [Single NAT Gateway](#Single-NAT-Gateway) - - [One NAT Gateway per availability zone](#One-NAT-Gateway-per-availability-zone) +- [AWS VPC Terraform module](#aws-vpc-terraform-module) +- [Terraform versions](#terraform-versions) +- [Usage](#usage) +- [External NAT Gateway IPs](#external-nat-gateway-ips) +- [NAT Gateway Scenarios](#nat-gateway-scenarios) + - [One NAT Gateway per subnet (default)](#one-nat-gateway-per-subnet-(default)) + - [Single NAT Gateway](#single-nat-gateway) + - [One NAT Gateway per availability zone](#one-nat-gateway-per-availability-zone) - ["private" versus "intra" subnets](#"private"-versus-"intra"-subnets) -- [Conditional creation](#Conditional-creation) -- [This VPC will not be created](#This-VPC-will-not-be-created) -- [... omitted](#...-omitted) -- [Public access to RDS instances](#Public-access-to-RDS-instances) -- [Network Access Control Lists (ACL or NACL)](#Network-Access-Control-Lists-(ACL-or-NACL)) -- [Public access to Redshift cluster](#Public-access-to-Redshift-cluster) -- [<= By default Redshift subnets will be associated with the private route table](#<=-By-default-Redshift-subnets-will-be-associated-with-the-private-route-table) -- [Examples](#Examples) -- [Inputs](#Inputs) -- [Outputs](#Outputs) -- [Tests](#Tests) -- [Authors](#Authors) -- [License](#License) +- [Conditional creation](#conditional-creation) +- [Public access to RDS instances](#public-access-to-rds-instances) +- [Network Access Control Lists (ACL or NACL)](#network-access-control-lists-(acl-or-nacl)) +- [Public access to Redshift cluster](#public-access-to-redshift-cluster) +- [Examples](#examples) +- [Inputs](#inputs) +- [Outputs](#outputs) +- [Tests](#tests) +- [Authors](#authors) +- [License](#license) <!-- END mktoc --> <!-- START doctoc generated TOC please keep comment here to allow auto update --> diff --git a/tests/files/README_04_code_block_issues.md b/tests/files/README_04_code_block_issues.md new file mode 100644 index 0000000..c327a37 --- /dev/null +++ b/tests/files/README_04_code_block_issues.md @@ -0,0 +1,24 @@ +# Test file + +<!-- BEGIN mktoc --> +- [Test for issue #1](#test-for-issue-#1) + - [Test Heading 3](#test-heading-3) + - [Test Heading 4](#test-heading-4) +<!-- END mktoc --> + +## Test for issue #1 + +``` +# This comment should not be added to the ToC +function test(A,B) { + return A==B; +} +``` + +### Test Heading 3 + +#### Test Heading 4 + +##### Test Heading 5 + +###### Test Heading 6
\ No newline at end of file |