use regex::Regex;
use similar::TextDiff;
use std::{
fs,
io::{self, BufRead, BufReader},
path::Path,
sync::LazyLock,
};
static LISTINGS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^```.*?\n(?<code>[^`]+?\n)?```\n\(\[(?<title>[\s\S]+?)\]\((?<link>.*?)\)")
.unwrap()
});
static HTTP: LazyLock<reqwest::blocking::Client> =
LazyLock::new(|| reqwest::blocking::Client::builder().build().unwrap());
pub fn listings(path: impl AsRef<Path>) -> io::Result<Vec<Listing>> {
let mut listings = Vec::new();
let text = fs::read_to_string(path)?;
for m in LISTINGS.captures_iter(&text) {
let Some(code) = m.name("code") else { continue };
let Some(title) = m.name("title") else {
continue;
};
let Some(link) = m.name("link") else { continue };
let url = String::from(link.as_str());
listings.push(Listing {
title: String::from(title.as_str()),
local: String::from(code.as_str()),
url: String::from(url.as_str()),
});
}
Ok(listings)
}
pub struct Listing {
pub title: String,
pub local: String,
pub url: String,
}
impl Listing {
pub fn check(self) -> reqwest::Result<CheckedListing> {
let resp = HTTP.get(self.url.clone() + "?raw=true").send()?;
resp.error_for_status_ref()?;
Ok(CheckedListing {
title: self.title,
local: self.local,
remote: resp.text()?,
url: self.url,
})
}
}
pub struct CheckedListing {
pub title: String,
pub local: String,
pub remote: String,
pub url: String,
}
#[must_use]
pub fn diff(local: &str, remote: &str) -> Option<String> {
if remote.contains(local)
| remote.contains(&indent(local, " "))
| remote.contains(&indent(local, "\t"))
{
None
} else {
let diff = TextDiff::from_lines(local, remote);
Some(format!("{}", diff.unified_diff()))
}
}
fn indent(input: &str, prefix: &'static str) -> String {
let mut output = String::new();
let input = input.as_bytes();
let input = BufReader::new(input);
for line_res in input.lines() {
let line = line_res.unwrap();
if !line.is_empty() {
output.push_str(prefix);
output.push_str(&line);
}
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indent_indents_nonblank_lines_by_given_prefix() {
let input = "foo\n bar\n\nbaz\n";
assert_eq!(
indent(input, " "),
" foo\n bar\n\n baz\n",
"wrong indentation"
);
assert_eq!(
indent(input, "\t"),
"\tfoo\n\t bar\n\n\tbaz\n",
"wrong indentation"
);
}
#[test]
fn diff_matches_indented_code() {
let local = "#[test]\nfn foo() {}";
let remote = " #[test]\n fn foo() {}\n";
assert_eq!(diff(local, remote), None);
}
}