From 1f5b829742f4b0a355587ecc8f33d2a8bfe13118 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 20 Jan 2020 21:41:39 -0800 Subject: [PATCH] Add table of contents to readme type: documentation --- .github/workflows/rust.yaml | 13 ++- Cargo.lock | 10 +- Cargo.toml | 15 +-- README.md | 85 +++++++------- bin/generate-bep-table.rs | 190 -------------------------------- bin/update-readme/Cargo.toml | 11 ++ bin/update-readme/src/bep.rs | 7 ++ bin/update-readme/src/common.rs | 15 +++ bin/update-readme/src/main.rs | 10 ++ bin/update-readme/src/opt.rs | 188 +++++++++++++++++++++++++++++++ bin/update-readme/src/status.rs | 37 +++++++ justfile | 17 ++- 12 files changed, 354 insertions(+), 244 deletions(-) delete mode 100644 bin/generate-bep-table.rs create mode 100644 bin/update-readme/Cargo.toml create mode 100644 bin/update-readme/src/bep.rs create mode 100644 bin/update-readme/src/common.rs create mode 100644 bin/update-readme/src/main.rs create mode 100644 bin/update-readme/src/opt.rs create mode 100644 bin/update-readme/src/status.rs diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 509d5d2..8006f6f 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -26,13 +26,18 @@ jobs: cargo --version cargo clippy --version - name: Build - run: cargo build --verbose + run: cargo build --all --verbose - name: Test - run: cargo test --verbose + run: cargo test --all --verbose - name: Clippy - run: cargo clippy + run: cargo clippy --all - name: Format - run: cargo fmt -- --check + run: cargo fmt --all -- --check - name: Lint if: matrix.os != 'windows-latest' run: "! grep --color -REn 'FIXME|TODO|XXX' src" + - name: Readme + if: matrix.os != 'windows-latest' + run: | + cargo run --package update-readme toc + git diff --no-ext-diff --quiet --exit-code diff --git a/Cargo.lock b/Cargo.lock index d8e140f..a2661e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,7 +139,6 @@ dependencies = [ "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "md5 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -518,6 +517,15 @@ name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "update-readme" +version = "0.0.0" +dependencies = [ + "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "url" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index 97cd4d7..6070cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,13 +33,8 @@ walkdir = "2" version = "1" features = ["derive"] -[dev-dependencies] -glob = "0.3.0" -regex = "1.3.3" - -# generates the table of supported BEPs in README.md -# not an example, but included as an example and not -# a binary because examples can use dev dependencies -[[example]] -name = "generate-bep-table" -path = "bin/generate-bep-table.rs" +[workspace] +members = [ + # generate table of contents and table of supported BEPs in README.md + "bin/update-readme" +] diff --git a/README.md b/README.md index 91e50d4..76bad7a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,53 @@ # intermodal: a 40' shipping container for the Internet +## Manual + +- [General](#general) + - [Semantic Versioning](#semantic-versioning) + - [Unstable Features](#unstable-features) + - [Colored Output](#colored-output) +- [Bittorrent](#bittorrent) + - [BEP Support](#bep-support) + +## General + +### Semantic Versioning + +Intermodal follows [semantic versioning](https://semver.org/). + +In particular: + +- v0.0.X: Breaking changes may be introduced at any time. +- v0.X.Y: Breaking changes may only be introduced with a minor version number + bump. +- vX.Y.Z: Breaking changes may only be introduced with a major version number + bump + +### Unstable Features + +To avoid premature stabilization and excessive version churn, unstable features +are unavailable unless the `--unstable` / `-u` flag is passed. Unstable +features may be changed or removed at any time. + +``` +$ imdl torrent stats --input tmp +error: Feature `torrent stats subcommand` cannot be used without passing the `--unstable` flag +$ imdl --unstable torrent stats tmp +Torrents processed: 0 +Read failed: 0 +Decode failed: 0 +``` + +### Colored Output + +Intermodal features colored help, error, and informational output. Colored +output is disabled if Intermodal detects that it is not printing to a TTY. + +To disable colored output, set the `NO_COLOR` environment variable to any +value or pass `--use-color never` on the command line. + +To force colored output, pass `--use-color always` on the command line. + ## Bittorrent ### BEP Support @@ -67,40 +115,3 @@ | [53](http://bittorrent.org/beps/bep_0053.html) | :x: | Magnet URI extension - Select specific file indices for download | | [54](http://bittorrent.org/beps/bep_0054.html) | :heavy_minus_sign: | The lt_donthave extension | | [55](http://bittorrent.org/beps/bep_0055.html) | :heavy_minus_sign: | Holepunch extension | - -## General Functionality - -### Colored Output - -Intermodal features colored help, error, and informational output. Colored -output is disabled if Intermodal detects that it is not printing to a TTY. - -To disable colored output, set the `NO_COLOR` environment variable to any -valu or pass `--use-color never` on the command line. - -To force colored output, pass `--use-color always` on the command line. - -## Semantic Versioning and Unstable Features - -Intermodal follows [semantic versioning](https://semver.org/). - -In particular: - -- v0.0.X: Breaking changes may be introduced at any time. -- v0.X.Y: Breaking changes may only be introduced with a minor version number - bump. -- vX.Y.Z: Breaking changes may only be introduced with a major version number - bump - -To avoid premature stabilization and excessive version churn, unstable features -are unavailable unless the `--unstable` / `-u` flag is passed. Unstable -features may be changed or removed at any time. - -``` -$ imdl torrent stats --input tmp -error: Feature `torrent stats subcommand` cannot be used without passing the `--unstable` flag -$ imdl --unstable torrent stats tmp -Torrents processed: 0 -Read failed: 0 -Decode failed: 0 -``` diff --git a/bin/generate-bep-table.rs b/bin/generate-bep-table.rs deleted file mode 100644 index 51e5ecd..0000000 --- a/bin/generate-bep-table.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::{ - error::Error, - fmt::{self, Display, Formatter}, - fs, - str::FromStr, -}; - -use glob::glob; -use regex::Regex; - -const README: &str = "README.md"; - -struct Bep { - number: usize, - title: String, - status: Status, -} - -enum Status { - Unknown, - NotApplicable, - Supported, - NotSupported, -} - -impl FromStr for Status { - type Err = String; - - fn from_str(text: &str) -> Result { - match text { - "x" => Ok(Self::NotSupported), - "+" => Ok(Self::Supported), - "-" => Ok(Self::NotApplicable), - "?" => Ok(Self::Unknown), - ":x:" => Ok(Self::NotSupported), - ":white_check_mark:" => Ok(Self::Supported), - ":heavy_minus_sign:" => Ok(Self::NotApplicable), - ":grey_question:" => Ok(Self::Unknown), - _ => Err(format!("invalid status: {}", text)), - } - } -} - -impl Display for Status { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Unknown => write!(f, ":grey_question:"), - Self::NotApplicable => write!(f, ":heavy_minus_sign:"), - Self::Supported => write!(f, ":white_check_mark:"), - Self::NotSupported => write!(f, ":x:"), - } - } -} - -fn main() -> Result<(), Box> { - let title_re = Regex::new("(?m)^:Title: (?P.*)$")?; - - let mut beps = Vec::new(); - - for result in glob("tmp/bittorrent.org/beps/bep_*.rst")? { - let path = result?; - - let number = path - .file_stem() - .unwrap() - .to_string_lossy() - .split('_') - .nth(1) - .unwrap() - .parse::<usize>()?; - - if number == 1000 { - continue; - } - - let rst = fs::read_to_string(path)?; - - let title = title_re - .captures(&rst) - .unwrap() - .name("title") - .unwrap() - .as_str() - .trim() - .to_owned(); - - beps.push(Bep { - status: Status::Unknown, - number, - title, - }); - } - - beps.sort_by_key(|bep| bep.number); - - let table_re = Regex::new( - r"(?mx) - ^[|]\ BEP.* - ( - \n - [|] - .* - )* - ", - )?; - - let readme = fs::read_to_string(README)?; - - let parts = table_re.split(&readme).into_iter().collect::<Vec<&str>>(); - - assert_eq!(parts.len(), 2); - - let before = parts[0]; - let after = parts[1]; - let original = table_re - .captures(&readme) - .unwrap() - .get(0) - .unwrap() - .as_str() - .trim(); - - let row_re = Regex::new( - r"(?x) - ^ - \| - \s* - \[ - (?P<number>[0-9]+) - \] - .* - \s* - \| - (?P<status>.*) - \| - (?P<title>.*) - \| - $ - ", - )?; - - let mut originals = Vec::new(); - - for row in original.lines().skip(2) { - let captures = row_re.captures(row).unwrap(); - originals.push(Bep { - number: captures.name("number").unwrap().as_str().parse()?, - status: captures.name("status").unwrap().as_str().trim().parse()?, - title: captures.name("title").unwrap().as_str().to_owned(), - }); - } - - assert_eq!(originals.len(), beps.len()); - - let mut lines = Vec::new(); - - let width = beps.iter().map(|bep| bep.title.len()).max().unwrap_or(0); - - lines.push(format!( - "| BEP | Status | {:width$} |", - "Title", - width = width - )); - - lines.push(format!( - "|:----------------------------------------------:|:------------------:|:{:-<width$}-|", - "", - width = width - )); - - for (bep, original) in beps.into_iter().zip(originals) { - assert_eq!(bep.number, original.number); - lines.push(format!( - "| [{:02}](http://bittorrent.org/beps/bep_{:04}.html) | {:18} | {:width$} |", - bep.number, - bep.number, - original.status.to_string(), - bep.title, - width = width - )); - } - - let table = lines.join("\n"); - - let readme = &[before.trim(), "", &table, "", after.trim(), ""].join("\n"); - - fs::write(README, readme)?; - - Ok(()) -} diff --git a/bin/update-readme/Cargo.toml b/bin/update-readme/Cargo.toml new file mode 100644 index 0000000..414aedd --- /dev/null +++ b/bin/update-readme/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "update-readme" +version = "0.0.0" +authors = ["Casey Rodarmor <casey@rodarmor.com>"] +edition = "2018" +publish = false + +[dependencies] +glob = "0.3.0" +regex = "1.3.3" +structopt = "0.3" diff --git a/bin/update-readme/src/bep.rs b/bin/update-readme/src/bep.rs new file mode 100644 index 0000000..714dd23 --- /dev/null +++ b/bin/update-readme/src/bep.rs @@ -0,0 +1,7 @@ +use crate::common::*; + +pub(crate) struct Bep { + pub(crate) number: usize, + pub(crate) title: String, + pub(crate) status: Status, +} diff --git a/bin/update-readme/src/common.rs b/bin/update-readme/src/common.rs new file mode 100644 index 0000000..f6d89e1 --- /dev/null +++ b/bin/update-readme/src/common.rs @@ -0,0 +1,15 @@ +// stdlib +pub(crate) use std::{ + error::Error, + fmt::{self, Display, Formatter}, + fs, + str::FromStr, +}; + +// crates.io +pub(crate) use glob::glob; +pub(crate) use regex::Regex; +pub(crate) use structopt::StructOpt; + +// local +pub(crate) use crate::{bep::Bep, opt::Opt, status::Status}; diff --git a/bin/update-readme/src/main.rs b/bin/update-readme/src/main.rs new file mode 100644 index 0000000..0a7dade --- /dev/null +++ b/bin/update-readme/src/main.rs @@ -0,0 +1,10 @@ +mod bep; +mod common; +mod opt; +mod status; + +use crate::common::*; + +fn main() -> Result<(), Box<dyn Error>> { + Opt::from_args().run() +} diff --git a/bin/update-readme/src/opt.rs b/bin/update-readme/src/opt.rs new file mode 100644 index 0000000..2e29756 --- /dev/null +++ b/bin/update-readme/src/opt.rs @@ -0,0 +1,188 @@ +use crate::common::*; + +const README: &str = "README.md"; + +const HEADING_PATTERN: &str = "(?m)^(?P<MARKER>#+) (?P<TEXT>.*)$"; + +const TOC_PATTERN: &str = "(?ms)## Manual.*## General"; + +#[derive(StructOpt)] +pub(crate) enum Opt { + SupportedBeps, + Toc, +} + +impl Opt { + pub(crate) fn run(self) -> Result<(), Box<dyn Error>> { + match self { + Self::Toc => Self::update_toc(), + Self::SupportedBeps => Self::update_supported_beps(), + } + } + + fn update_toc() -> Result<(), Box<dyn Error>> { + let readme = fs::read_to_string(README)?; + + let header_re = Regex::new(HEADING_PATTERN)?; + + let mut toc = Vec::new(); + for captures in header_re.captures_iter(&readme).skip(2) { + let marker = captures.name("MARKER").unwrap().as_str(); + let text = captures.name("TEXT").unwrap().as_str(); + let level = marker.len(); + let indentation = " ".repeat((level - 2) * 2); + let slug = text.to_lowercase().replace(' ', "-"); + toc.push(format!("{}- [{}](#{})", indentation, text, slug)); + } + + let toc = toc.join("\n"); + + let toc_re = Regex::new(TOC_PATTERN)?; + + let readme = toc_re.replace( + &readme, + format!("## Manual\n\n{}\n\n## General", toc).as_str(), + ); + + fs::write(README, readme.as_bytes())?; + + Ok(()) + } + + fn update_supported_beps() -> Result<(), Box<dyn Error>> { + let title_re = Regex::new("(?m)^:Title: (?P<title>.*)$")?; + + let mut beps = Vec::new(); + + for result in glob("tmp/bittorrent.org/beps/bep_*.rst")? { + let path = result?; + + let number = path + .file_stem() + .unwrap() + .to_string_lossy() + .split('_') + .nth(1) + .unwrap() + .parse::<usize>()?; + + if number == 1000 { + continue; + } + + let rst = fs::read_to_string(path)?; + + let title = title_re + .captures(&rst) + .unwrap() + .name("title") + .unwrap() + .as_str() + .trim() + .to_owned(); + + beps.push(Bep { + status: Status::Unknown, + number, + title, + }); + } + + beps.sort_by_key(|bep| bep.number); + + let table_re = Regex::new( + r"(?mx) + ^[|]\ BEP.* + ( + \n + [|] + .* + )* + ", + )?; + + let readme = fs::read_to_string(README)?; + + let parts = table_re.split(&readme).collect::<Vec<&str>>(); + + assert_eq!(parts.len(), 2); + + let before = parts[0]; + let after = parts[1]; + let original = table_re + .captures(&readme) + .unwrap() + .get(0) + .unwrap() + .as_str() + .trim(); + + let row_re = Regex::new( + r"(?x) + ^ + \| + \s* + \[ + (?P<number>[0-9]+) + \] + .* + \s* + \| + (?P<status>.*) + \| + (?P<title>.*) + \| + $ + ", + )?; + + let mut originals = Vec::new(); + + for row in original.lines().skip(2) { + let captures = row_re.captures(row).unwrap(); + originals.push(Bep { + number: captures.name("number").unwrap().as_str().parse()?, + status: captures.name("status").unwrap().as_str().trim().parse()?, + title: captures.name("title").unwrap().as_str().to_owned(), + }); + } + + assert_eq!(originals.len(), beps.len()); + + let mut lines = Vec::new(); + + let width = beps.iter().map(|bep| bep.title.len()).max().unwrap_or(0); + + lines.push(format!( + "| BEP | Status | {:width$} |", + "Title", + width = width + )); + + lines.push(format!( + "|:----------------------------------------------:|:------------------:|:{:-<width$}-|", + "", + width = width + )); + + for (bep, original) in beps.into_iter().zip(originals) { + assert_eq!(bep.number, original.number); + lines.push(format!( + "| [{:02}](http://bittorrent.org/beps/bep_{:04}.html) | {:18} | {:width$} |", + bep.number, + bep.number, + original.status.to_string(), + bep.title, + width = width + )); + } + + let table = lines.join("\n"); + + let readme = &[before.trim(), "", &table, after.trim()].join("\n"); + + fs::write(README, readme.as_bytes())?; + + Ok(()) + } +} diff --git a/bin/update-readme/src/status.rs b/bin/update-readme/src/status.rs new file mode 100644 index 0000000..61ff930 --- /dev/null +++ b/bin/update-readme/src/status.rs @@ -0,0 +1,37 @@ +use crate::common::*; + +pub(crate) enum Status { + Unknown, + NotApplicable, + Supported, + NotSupported, +} + +impl FromStr for Status { + type Err = String; + + fn from_str(text: &str) -> Result<Self, Self::Err> { + match text.replace('\\', "").as_str() { + "x" => Ok(Self::NotSupported), + "+" => Ok(Self::Supported), + "-" => Ok(Self::NotApplicable), + "?" => Ok(Self::Unknown), + ":x:" => Ok(Self::NotSupported), + ":white_check_mark:" => Ok(Self::Supported), + ":heavy_minus_sign:" => Ok(Self::NotApplicable), + ":grey_question:" => Ok(Self::Unknown), + _ => Err(format!("invalid status: {}", text)), + } + } +} + +impl Display for Status { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Unknown => write!(f, ":grey_question:"), + Self::NotApplicable => write!(f, ":heavy_minus_sign:"), + Self::Supported => write!(f, ":white_check_mark:"), + Self::NotSupported => write!(f, ":x:"), + } + } +} diff --git a/justfile b/justfile index a080c61..1e85764 100644 --- a/justfile +++ b/justfile @@ -29,8 +29,21 @@ preview-readme: dev-deps: brew install grip -generate-bep-table: - cargo run --example generate-bep-table +# update readme table of contents +update-toc: + cargo run --package update-readme toc + +# update readme table of supported BEPs +update-supported-beps: + cargo run --package update-readme supported-beps + +check: + cargo test --all + cargo clippy --all + cargo fmt --all -- --check + ! grep --color -REn 'FIXME|TODO|XXX' src + cargo run --package update-readme toc + git diff --no-ext-diff --quiet --exit-code # retrieve large collection of torrents from the Internet Archive get-torrents: