Add table of contents to readme

type: documentation
This commit is contained in:
Casey Rodarmor 2020-01-20 21:41:39 -08:00
parent 66d44155f0
commit 1f5b829742
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
12 changed files with 354 additions and 244 deletions

View File

@ -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

10
Cargo.lock generated
View File

@ -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"

View File

@ -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"
]

View File

@ -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
```

View File

@ -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<Self, Self::Err> {
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<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).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(())
}

View File

@ -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"

View File

@ -0,0 +1,7 @@
use crate::common::*;
pub(crate) struct Bep {
pub(crate) number: usize,
pub(crate) title: String,
pub(crate) status: Status,
}

View File

@ -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};

View File

@ -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()
}

View File

@ -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(())
}
}

View File

@ -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:"),
}
}
}

View File

@ -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: