use std::str::FromStr;
use crate::error_mod::{Error, Result};
use crate::public_api_mod::{RED, RESET, YELLOW};
use crate::utils_mod::*;
use lazy_static::lazy_static;
lazy_static! {
static ref REGEX_PLANTUML_START: regex::Regex = regex::Regex::new(
r#"(?m)^\[//\]: # \(auto_plantuml start\)$"#
).expect("regex new");
static ref REGEX_PLANTUML_END: regex::Regex = regex::Regex::new(
r#"(?m)^\[//\]: # \(auto_plantuml end\)$"#
).expect("regex new");
static ref REGEX_IMG_LINK: regex::Regex = regex::Regex::new(
r#"!\[.+\]\(.+/svg_(.+)\.svg\)"#
).expect("regex new");
}
pub fn auto_plantuml(repo_url: &str) -> Result<()> {
let path = std::env::current_dir()?;
auto_plantuml_for_path(&path, repo_url)?;
Ok(())
}
pub fn auto_plantuml_for_path(path: &std::path::Path, repo_url: &str) -> Result<()> {
let path = camino::Utf8Path::from_path(path).ok_or_else(|| Error::ErrorFromStr("from_path is None"))?;
println!(" {YELLOW}Running auto_plantuml{RESET}");
let files = crate::utils_mod::traverse_dir_with_exclude_dir(
path.as_std_path(),
"/*.md",
&["/.git".to_string(), "/target".to_string(), "/docs".to_string()],
)?;
for md_filename in files {
let md_filename = camino::Utf8Path::new(&md_filename);
let mut is_changed = false;
let mut md_text_content = std::fs::read_to_string(md_filename)?;
if md_text_content.contains("\r\n") {
return Err(Error::ErrorFromString(format!(
"{RED}Error: {md_filename} has CRLF line endings instead of LF. Correct the file! {RESET}"
)));
}
let mut pos = 0;
while let Ok(marker_start) = find_pos_start_data_after_delimiter(&md_text_content, pos, "\n[//]: # (auto_plantuml start)\n") {
pos = marker_start + 34;
if let Ok(code_start) = find_pos_start_data_after_delimiter(&md_text_content, marker_start, "\n```plantuml\n") {
if let Ok(code_end) = find_pos_end_data_before_delimiter(&md_text_content, code_start, "\n```\n") {
let code_end_after = code_end + 5;
let plantuml_code = &md_text_content[code_start..code_end];
let plantuml_code_hash = hash_text(plantuml_code);
if let Ok(marker_end) =
find_pos_end_data_before_delimiter(&md_text_content, marker_start, "\n[//]: # (auto_plantuml end)\n")
{
let img_link = md_text_content[code_end_after..marker_end].trim();
let mut get_new_svg = false;
if img_link.is_empty() {
get_new_svg = true;
} else {
let cap_group = REGEX_IMG_LINK
.captures(img_link)
.ok_or_else(|| Error::ErrorFromString(format!("{RED}Error: The old img link '{img_link}' is NOT in this format ''{RESET}")))?;
let old_hash = &cap_group[1];
if old_hash != plantuml_code_hash {
get_new_svg = true;
let old_file_path = camino::Utf8PathBuf::from_str(&format!(
"{}/images/svg_{old_hash}.svg",
md_filename.parent().ok_or_else(|| Error::ErrorFromStr("parent is None"))?
))?;
if old_file_path.exists() {
std::fs::remove_file(&old_file_path)?;
}
} else {
let old_file_path = camino::Utf8PathBuf::from_str(&format!(
"{}/images/svg_{old_hash}.svg",
md_filename.parent().ok_or_else(|| Error::ErrorFromStr("parent is None"))?
))?;
if !old_file_path.exists() {
get_new_svg = true;
}
}
}
if get_new_svg {
let relative_md_filename = md_filename.strip_prefix(path)?;
println!(" {YELLOW}{relative_md_filename} get new svg {plantuml_code_hash}{RESET}");
let svg_code = request_svg(plantuml_code)?;
let new_file_path = camino::Utf8PathBuf::from_str(&format!(
"{}/images/svg_{plantuml_code_hash}.svg",
md_filename.parent().ok_or_else(|| Error::ErrorFromStr("parent is None"))?
))?;
std::fs::create_dir_all(new_file_path.parent().ok_or_else(|| Error::ErrorFromStr("parent is None"))?)?;
std::fs::write(&new_file_path, svg_code)?;
let repo_full_url = if repo_url.is_empty() {
"".to_string()
} else {
format!("{}/raw/main/", repo_url.trim_end_matches('/'))
};
let relative_svg_path = new_file_path.strip_prefix(path)?;
let img_link = format!("\n\n");
md_text_content.replace_range(code_end_after..marker_end, &img_link);
is_changed = true;
}
}
}
}
}
if is_changed {
std::fs::write(md_filename, md_text_content)?;
}
}
println!(" {YELLOW}Finished auto_plantuml{RESET}");
Ok(())
}
pub fn hash_text(text: &str) -> String {
let hash = <sha2::Sha256 as sha2::Digest>::digest(text.as_bytes());
<base64ct::Base64UrlUnpadded as base64ct::Encoding>::encode_string(&hash)
}
pub fn request_svg(plant_uml_code: &str) -> Result<String> {
let base_url = "https://plantuml.com/plantuml";
let url_parameter = compress_plant_uml_code(plant_uml_code)?;
let url = format!("{}/svg/{}", base_url, url_parameter);
Ok(reqwest::blocking::get(url)?.text_with_charset("utf-8")?)
}
pub fn compress_plant_uml_code(plant_uml_code: &str) -> Result<String> {
let data = plant_uml_code.as_bytes();
let compressed = deflate::deflate_bytes(data);
let my_cfg = radix64::CustomConfig::with_alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_")
.no_padding()
.build()
.map_err(|_| Error::ErrorFromStr("CustomConfigError"))?;
Ok(my_cfg.encode(&compressed))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
pub fn examples_plantuml_test() {
std::fs::remove_dir_all("examples/plantuml/images").unwrap_or_else(|_| ());
std::fs::copy("sample_data/input1_for_plantuml.md", "examples/plantuml/input1_for_plantuml.md").expect("example");
std::fs::copy("sample_data/input2_for_plantuml.md", "examples/plantuml/input2_for_plantuml.md").expect("example");
let path = camino::Utf8Path::new("examples/plantuml");
auto_plantuml_for_path(path.as_std_path(), "").expect("example");
let changed1 = std::fs::read_to_string("examples/plantuml/input1_for_plantuml.md").expect("example");
let output1 = std::fs::read_to_string("sample_data/output1_for_plantuml.md").expect("example");
assert_eq!(changed1, output1);
let changed2 = std::fs::read_to_string("examples/plantuml/input2_for_plantuml.md").expect("example");
let output2 = std::fs::read_to_string("sample_data/output2_for_plantuml.md").expect("example");
assert_eq!(changed2, output2);
assert!(camino::Utf8Path::new("examples/plantuml/images/svg_8eLHibrc2gjrY1qcezDiy--xk9mz1XwYyIcZwXvjlcE.svg").exists());
assert!(camino::Utf8Path::new("examples/plantuml/images/svg_H8u0SNaGZzGAaYPHeY4eDF9TfWqVXhKa7M8wiwXSe_s.svg").exists());
assert!(camino::Utf8Path::new("examples/plantuml/images/svg_KPAr4S3iGAVLbskqf6XXaqrWge8bXMlCkNk7EaimJs0.svg").exists());
assert!(camino::Utf8Path::new("examples/plantuml/images/svg_lTG8S1eNgnLTJS1PruoYJEjQVW4dCn0x6Wl-pw6yPXM.svg").exists());
assert!(camino::Utf8Path::new("examples/plantuml/images/svg_tosmzSqwSXyObaX7eRLFp9xsMzcM5UDT4NSaQSgnq-Q.svg").exists());
}
#[test]
pub fn test_hash() {
assert_eq!("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", hash_text("test"));
}
#[test]
pub fn test_compress_plant_uml_code() {
assert_eq!(
compress_plant_uml_code(
r#"@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
@enduml"#
)
.expect("test"),
"SoWkIImgAStDuNBCoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk42oYde0jM05MDHLLoGdrUSokMGcfS2D1C0"
);
}
}