Skip to content

Commit 6acdf53

Browse files
committed
libify
1 parent 62275b6 commit 6acdf53

File tree

7 files changed

+207
-37
lines changed

7 files changed

+207
-37
lines changed

.github/workflows/audit.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Security audit
2+
on:
3+
schedule:
4+
- cron: '0 0 * * *'
5+
push:
6+
paths:
7+
- '**/Cargo.toml'
8+
- '**/Cargo.lock'
9+
jobs:
10+
security_audit:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: EmbarkStudios/cargo-deny-action@v1

.github/workflows/ci.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI
2+
on: [pull_request, workflow_dispatch]
3+
env:
4+
CARGO_TERM_COLOR: always
5+
6+
jobs:
7+
lint:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: dtolnay/rust-toolchain@stable
12+
with:
13+
components: "clippy, rustfmt"
14+
- uses: Swatinem/rust-cache@v2
15+
- run: cargo fmt --all -- --check
16+
- run: cargo clippy --all-targets --all-features -- -D clippy::pedantic -D warnings
17+
18+
test:
19+
strategy:
20+
matrix:
21+
os: [ubuntu-latest, macos-latest, windows-latest]
22+
runs-on: ${{ matrix.os }}
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: dtolnay/rust-toolchain@stable
26+
- uses: Swatinem/rust-cache@v2
27+
- run: cargo test --all-features

.github/workflows/nightly.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Nightly
2+
on: [pull_request, workflow_dispatch]
3+
env:
4+
CARGO_TERM_COLOR: always
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: dtolnay/rust-toolchain@nightly
12+
- uses: Swatinem/rust-cache@v2
13+
- run: cargo test --all-features

Cargo.toml

+16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
[package]
22
name = "up2code"
33
version = "0.1.0"
4+
authors = ["John Arundel <[email protected]>"]
45
edition = "2021"
6+
description = """
7+
up2code is a tool to check code listings in Markdown files against a GitHub repository.
8+
"""
9+
keywords = ["web", "cli", "utility", "text"]
10+
categories = ["command-line-utilities"]
11+
license = "MIT OR Apache-2.0"
12+
readme = "README.md"
13+
documentation = "https://docs.rs/up2code"
14+
homepage = "https://github.com/bitfield/up2code"
15+
repository = "https://github.com/bitfield/up2code"
16+
exclude = ["/.github/"]
17+
18+
[badges]
19+
github = { repository = "bitfield/up2code", workflow = "CI" }
20+
maintenance = { status = "actively-developed" }
521

622
[dependencies]
723
anyhow = "1.0.89"

README.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[![Crate](https://img.shields.io/crates/v/up2code.svg)](https://crates.io/crates/up2code)
2+
[![Docs](https://docs.rs/up2code/badge.svg)](https://docs.rs/up2code)
3+
![CI](https://github.com/bitfield/up2code/actions/workflows/ci.yml/badge.svg)
4+
![Audit](https://github.com/bitfield/up2code/actions/workflows/audit.yml/badge.svg)
5+
![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)
6+
7+
# up2code
8+
9+
`up2code` is a tool for checking code listings in Markdown files, to make sure they're up to date with, and in sync with, canonical versions stored in a GitHub repo.
10+
11+
### Installation
12+
13+
```sh
14+
cargo install up2code
15+
```
16+
17+
### Usage
18+
19+
Run:
20+
21+
```sh
22+
up2code book/*.md
23+
```
24+
25+
`up2code` reads all the Markdown files you specify, looking for what it considers a “listing”: a fenced code block immediately followed by a web link. For example:
26+
27+
```rust
28+
fn main() {
29+
println!("Hello, world!")
30+
}
31+
```
32+
[Listing `hello/1`](https://github.com/bitfield/example/blob/src/main.rs))
33+
34+
It will try to fetch the raw code page from the specified URL (appending "?raw=true"), reporting any errors. If the fetch succeeds, it will check that the Markdown listing is an exact substring of the GitHub listing, reporting any mismatch as a unified diff:
35+
36+
```
37+
tests/data/test.md: Listing `counter_2`
38+
@@ -6,8 +13,8 @@
39+
40+
#[test]
41+
fn count_lines_fn_counts_lines_in_input() {
42+
- let input = io::Cursor::new("line 1\nline2\n");
43+
+ let input = io::Cursor::new("line 1\nline 2\n");
44+
let lines = count_lines(input);
45+
- assert_eq!(2, lines);
46+
+ assert_eq!(lines, 2);
47+
}
48+
}
49+
```

src/lib.rs

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use regex::Regex;
2+
use similar::TextDiff;
3+
4+
use std::{fs, io, path::Path, sync::LazyLock};
5+
6+
static LISTINGS: LazyLock<Regex> = LazyLock::new(|| {
7+
Regex::new(
8+
r"(?m)^```.*?\n(?<code>[^`]+?\n)?```\n\(\[(?<title>[\s\S]+?)\]\((?<link>.*?)\)",
9+
)
10+
.unwrap()
11+
});
12+
13+
static HTTP: LazyLock<reqwest::blocking::Client> =
14+
LazyLock::new(|| reqwest::blocking::Client::builder().build().unwrap());
15+
16+
/// Returns all the listings in the file at `path`.
17+
///
18+
/// # Errors
19+
///
20+
/// Any errors returned by [`fs::read_to_string`].
21+
pub fn listings(path: impl AsRef<Path>) -> io::Result<Vec<Listing>> {
22+
let mut listings = Vec::new();
23+
let text = fs::read_to_string(path)?;
24+
for m in LISTINGS.captures_iter(&text) {
25+
let Some(code) = m.name("code") else { continue };
26+
let Some(title) = m.name("title") else {
27+
continue;
28+
};
29+
let Some(link) = m.name("link") else { continue };
30+
let url = String::from(link.as_str());
31+
listings.push(Listing {
32+
title: String::from(title.as_str()),
33+
code: String::from(code.as_str()),
34+
url: String::from(url.as_str()),
35+
});
36+
}
37+
Ok(listings)
38+
}
39+
40+
pub struct Listing {
41+
pub title: String,
42+
pub code: String,
43+
pub url: String,
44+
}
45+
46+
impl Listing {
47+
/// Fetches the canonical listing from its URL and stores the text.
48+
///
49+
/// # Errors
50+
///
51+
/// Any errors returned by the `reqwest` client.
52+
pub fn check(self) -> reqwest::Result<CheckedListing> {
53+
let resp = HTTP.get(self.url.clone() + "?raw=true").send()?;
54+
resp.error_for_status_ref()?;
55+
Ok(CheckedListing {
56+
title: self.title,
57+
code: self.code,
58+
text: resp.text()?
59+
})
60+
}
61+
}
62+
63+
pub struct CheckedListing {
64+
pub title: String,
65+
pub code: String,
66+
pub text: String,
67+
}
68+
69+
impl CheckedListing {
70+
#[must_use]
71+
pub fn diff(&self) -> Option<String> {
72+
if self.text.contains(&self.code) {
73+
None
74+
} else {
75+
let diff = TextDiff::from_lines(&self.code, &self.text);
76+
Some(format!("{}", diff.unified_diff()))
77+
}
78+
}
79+
}

src/main.rs

+9-37
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,21 @@
1-
use regex::Regex;
2-
use similar::TextDiff;
1+
use std::env;
32

4-
use std::{env, fs, sync::LazyLock};
5-
6-
static LISTINGS: LazyLock<Regex> = LazyLock::new(|| {
7-
Regex::new(
8-
r"(?m)^```rust\n(?<code>[^`]+?\n)?```\n\(\[Listing `(?<listing>[\s\S]+?)`\]\((?<link>.*?)\)",
9-
)
10-
.unwrap()
11-
});
3+
use up2code::listings;
124

135
fn main() -> anyhow::Result<()> {
146
let paths: Vec<String> = env::args().skip(1).collect();
157
if paths.is_empty() {
168
eprintln!("Usage: up2code [PATH, ...]");
179
return Ok(());
1810
}
19-
let http = reqwest::blocking::Client::builder().build()?;
20-
for path in &paths {
21-
let text = fs::read_to_string(path)?;
22-
for m in LISTINGS.captures_iter(&text) {
23-
let Some(code) = m.name("code") else { continue };
24-
let Some(listing) = m.name("listing") else {
25-
continue;
26-
};
27-
let Some(link) = m.name("link") else { continue };
28-
let raw_url = String::from(link.as_str());
29-
match http.get(raw_url + "?raw=true").send() {
30-
Err(e) => {
31-
println!("{path}: {e}");
32-
}
33-
Ok(resp) => {
34-
if let Err(e) = resp.error_for_status_ref() {
35-
println!("{path}: {e}");
36-
continue
37-
}
38-
let text = resp.text()?;
39-
if !text.contains(code.as_str()) {
40-
println!("{path}: Listing {}", listing.as_str());
41-
let diff = TextDiff::from_lines(code.as_str(), &text);
42-
print!("{}", diff.unified_diff());
43-
}
44-
}
11+
for path in paths {
12+
for listing in listings(&path)? {
13+
let listing = listing.check()?;
14+
if let Some(diff) = listing.diff() {
15+
println!("{path}: {}", listing.title);
16+
println!("{diff}");
4517
}
4618
}
4719
}
4820
Ok(())
49-
}
21+
}

0 commit comments

Comments
 (0)