summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Gimbel <hallo@kevingimbel.com>2019-12-04 17:53:38 +0100
committerKevin Gimbel <hallo@kevingimbel.com>2019-12-04 17:53:38 +0100
commita54cfc37863bdd8a4322770589d78cf9bd479b9e (patch)
tree51e6306d08ea347dc925067c994fe07d334747f0
parent5fafefc4325cd6f65fc92e9092e2e02be38351bd (diff)
Fix #1, add depth arguments
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--README.md35
-rw-r--r--mktoc.iml15
-rw-r--r--src/bin.rs8
-rw-r--r--src/lib.rs87
-rw-r--r--tests/files/README_02.md42
-rw-r--r--tests/files/README_04_code_block_issues.md24
8 files changed, 154 insertions, 61 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 313fe53..55d0635 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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)",
diff --git a/Cargo.toml b/Cargo.toml
index 4e2714f..544900d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/README.md b/README.md
index 735d499..5c31d84 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/bin.rs b/src/bin.rs
index 83638e6..29bba8b 100644
--- a/src/bin.rs
+++ b/src/bin.rs
@@ -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) => {
diff --git a/src/lib.rs b/src/lib.rs
index c285417..ce4e8d3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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