init
This commit is contained in:
commit
c4f493966a
5 changed files with 893 additions and 0 deletions
27
src/args.rs
Normal file
27
src/args.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use clap::{arg, command, ArgMatches};
|
||||
|
||||
pub fn get_args() -> ArgMatches {
|
||||
command!()
|
||||
.about("Patch the frontmatter of markdown files")
|
||||
.arg(arg!([markdown] "Markdown File").required(true))
|
||||
.arg(arg!(-p --patch <PATCH> "Apply JSON Patch file. If set to '-' read from stdin").required(false).conflicts_with("merge"))
|
||||
.arg(arg!(--merge <FILE> "Merge Markdown frontmatter. If set to '-' read from stdin").required(false).conflicts_with("patch"))
|
||||
.arg(arg!(-a --add "Only patch add operations").required(false).conflicts_with("merge"))
|
||||
.arg(arg!(-m --modify "Only patch modify operations").required(false).conflicts_with("merge"))
|
||||
.arg(arg!(-d --delete "Only patch delete operations").required(false).conflicts_with("merge"))
|
||||
.arg(arg!(-v --verbose "Print out what changes will be made to the document").required(false))
|
||||
.arg(arg!(-n --dryrun "Dont modify the input file. Only print what would be done").required(false))
|
||||
.arg(arg!(-o --output <OUTPUT> "Write patched file to output path. Dont modify the input file directly. If set to '-' output to stdout"))
|
||||
.arg(arg!(--notest "Ignore tests in JSON Patch files").conflicts_with("merge"))
|
||||
.arg(
|
||||
arg!(-x --exclude <JSONPATH>... "Exclude json path from patch")
|
||||
.required(false)
|
||||
.conflicts_with("keys"),
|
||||
)
|
||||
.arg(
|
||||
arg!(-k --keys <JSONPATH>... "Only include the specified json paths in patch")
|
||||
.required(false)
|
||||
.conflicts_with("exclude"),
|
||||
)
|
||||
.get_matches()
|
||||
}
|
364
src/main.rs
Normal file
364
src/main.rs
Normal file
|
@ -0,0 +1,364 @@
|
|||
use std::io::Read;
|
||||
|
||||
use json_patch::PatchOperation;
|
||||
use serde_json::Value;
|
||||
|
||||
mod args;
|
||||
|
||||
trait ErrorExit {
|
||||
type T;
|
||||
|
||||
fn quit_on_error(self, msg: &str) -> Self::T;
|
||||
}
|
||||
|
||||
impl<T, E> ErrorExit for Result<T, E> {
|
||||
type T = T;
|
||||
|
||||
fn quit_on_error(self, msg: &str) -> T {
|
||||
self.map_or_else(
|
||||
|_| {
|
||||
eprintln!("{} {msg}", console::Style::new().red().apply_to("Error:"));
|
||||
std::process::exit(1);
|
||||
},
|
||||
|res| res,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// seperate markdown string into frontmatter and markdown content
|
||||
#[must_use]
|
||||
pub fn markdown_with_frontmatter(markdown: &str) -> (Option<String>, String) {
|
||||
let frontmatter_regex = regex::Regex::new(r"(?s)^---\s*\n(.*?)\n---\s*\n(.*)$").unwrap();
|
||||
|
||||
frontmatter_regex.captures(markdown).map_or_else(
|
||||
|| (None, markdown.to_string()),
|
||||
|captures| {
|
||||
let frontmatter = captures.get(1).map(|m| m.as_str().to_string());
|
||||
let remaining_markdown = captures
|
||||
.get(2)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
(frontmatter, remaining_markdown)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn assemble_markdown_doc(content: &str, frontmatter: serde_json::Value) -> String {
|
||||
let frontmatter = serde_yaml::to_string(&serde_yaml::to_value(frontmatter).unwrap()).unwrap();
|
||||
format!("---\n{frontmatter}---\n\n{content}")
|
||||
}
|
||||
|
||||
fn remove_test_operations(patch: Vec<PatchOperation>) -> Vec<PatchOperation> {
|
||||
patch
|
||||
.into_iter()
|
||||
.filter(|x| !matches!(x, json_patch::PatchOperation::Test(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remove_add_operations(patch: Vec<PatchOperation>) -> Vec<PatchOperation> {
|
||||
patch
|
||||
.into_iter()
|
||||
.filter(|x| !matches!(x, json_patch::PatchOperation::Add(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remove_modify_operations(patch: Vec<PatchOperation>) -> Vec<PatchOperation> {
|
||||
patch
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
!matches!(
|
||||
x,
|
||||
json_patch::PatchOperation::Replace(_)
|
||||
| json_patch::PatchOperation::Move(_)
|
||||
| json_patch::PatchOperation::Copy(_)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remove_delete_operations(patch: Vec<PatchOperation>) -> Vec<PatchOperation> {
|
||||
patch
|
||||
.into_iter()
|
||||
.filter(|x| !matches!(x, json_patch::PatchOperation::Remove(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn exclude_json_paths(patch: &serde_json::Value, paths: &[String]) -> serde_json::Value {
|
||||
let filtered: Vec<serde_json::Value> = patch
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
let mut is_excluded = false;
|
||||
for exclude in paths {
|
||||
if x.as_object()
|
||||
.unwrap()
|
||||
.get("path")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with(exclude)
|
||||
{
|
||||
is_excluded = true;
|
||||
}
|
||||
}
|
||||
!is_excluded
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
serde_json::to_value(filtered).unwrap()
|
||||
}
|
||||
|
||||
fn include_only_json_paths(patch: &serde_json::Value, paths: &[String]) -> serde_json::Value {
|
||||
let filtered: Vec<serde_json::Value> = patch
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
let mut is_included = false;
|
||||
for include in paths {
|
||||
if x.as_object()
|
||||
.unwrap()
|
||||
.get("path")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with(include)
|
||||
{
|
||||
is_included = true;
|
||||
}
|
||||
}
|
||||
is_included
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
serde_json::to_value(filtered).unwrap()
|
||||
}
|
||||
|
||||
fn only_include_json_pointers(paths: &[String], json_value: &Value) -> Value {
|
||||
match json_value {
|
||||
Value::Object(obj) => {
|
||||
let mut filtered_obj = serde_json::Map::new();
|
||||
for path in paths {
|
||||
if let Some(key) = path.strip_prefix('/') {
|
||||
if let Some(value) = obj.get(key) {
|
||||
filtered_obj
|
||||
.insert(key.to_string(), only_include_json_pointers(paths, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Object(filtered_obj)
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
let filtered_arr: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|item| only_include_json_pointers(paths, item))
|
||||
.collect();
|
||||
Value::Array(filtered_arr)
|
||||
}
|
||||
_ => json_value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exclude_json_by_paths(paths: &[String], json_value: &Value) -> Value {
|
||||
match json_value {
|
||||
Value::Object(obj) => {
|
||||
let mut filtered_obj = serde_json::Map::new();
|
||||
for (key, value) in obj {
|
||||
if !paths.iter().any(|path| path == &("/".to_string() + key)) {
|
||||
filtered_obj.insert(key.clone(), exclude_json_by_paths(paths, value));
|
||||
}
|
||||
}
|
||||
Value::Object(filtered_obj)
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
let filtered_arr: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|item| exclude_json_by_paths(paths, item))
|
||||
.collect();
|
||||
Value::Array(filtered_arr)
|
||||
}
|
||||
_ => json_value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = args::get_args();
|
||||
|
||||
let markdown_file = args.get_one::<String>("markdown").unwrap();
|
||||
|
||||
let (frontmatter, markdown_content) = markdown_with_frontmatter(
|
||||
&std::fs::read_to_string(markdown_file).quit_on_error("Could not read markdown file"),
|
||||
);
|
||||
let frontmatter = frontmatter
|
||||
.ok_or(0)
|
||||
.quit_on_error("Could not parse frontmatter");
|
||||
let mut frontmatter = serde_json::to_value(
|
||||
&serde_yaml::from_str::<serde_yaml::Value>(&frontmatter)
|
||||
.quit_on_error("Frontmatter is no valid yaml"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if let Some(patch_file) = args.get_one::<String>("patch") {
|
||||
let mut patch: Vec<json_patch::PatchOperation> = serde_json::from_str(
|
||||
&(if patch_file == "-" {
|
||||
let mut str = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut str)
|
||||
.quit_on_error("Coult not read stdin");
|
||||
str
|
||||
} else {
|
||||
std::fs::read_to_string(patch_file).quit_on_error("Could not read patch file")
|
||||
}),
|
||||
)
|
||||
.quit_on_error("Could not parse patch file");
|
||||
|
||||
if args.get_flag("add") | args.get_flag("modify") | args.get_flag("delete") {
|
||||
if !args.get_flag("add") {
|
||||
patch = remove_add_operations(patch);
|
||||
}
|
||||
if !args.get_flag("modify") {
|
||||
patch = remove_modify_operations(patch);
|
||||
}
|
||||
if !args.get_flag("delete") {
|
||||
patch = remove_delete_operations(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if args.get_flag("notest") {
|
||||
patch = remove_test_operations(patch);
|
||||
}
|
||||
|
||||
let mut patch = serde_json::to_value(patch).unwrap();
|
||||
|
||||
if let Some(excludes) = args.get_many::<String>("exclude") {
|
||||
let excludes: Vec<String> = excludes.cloned().collect();
|
||||
patch = exclude_json_paths(&patch, &excludes);
|
||||
}
|
||||
|
||||
if let Some(includes) = args.get_many::<String>("keys") {
|
||||
let includes: Vec<String> = includes.cloned().collect();
|
||||
patch = include_only_json_paths(&patch, &includes);
|
||||
}
|
||||
|
||||
let patch: Vec<json_patch::PatchOperation> = serde_json::from_value(patch).unwrap();
|
||||
|
||||
if args.get_flag("verbose") | args.get_flag("dryrun") {
|
||||
for op in patch.clone() {
|
||||
match op {
|
||||
PatchOperation::Add(op) => {
|
||||
eprintln!(
|
||||
"Add {} at {}",
|
||||
console::Style::new().green().apply_to(op.value),
|
||||
console::Style::new().bright().red().apply_to(op.path)
|
||||
);
|
||||
}
|
||||
PatchOperation::Remove(op) => {
|
||||
eprintln!(
|
||||
"Remove value at {}",
|
||||
console::Style::new().bright().red().apply_to(op.path)
|
||||
);
|
||||
}
|
||||
PatchOperation::Replace(op) => {
|
||||
eprintln!(
|
||||
"Replace value at {} with {}",
|
||||
console::Style::new().bright().red().apply_to(op.path),
|
||||
console::Style::new().green().apply_to(op.value)
|
||||
);
|
||||
}
|
||||
PatchOperation::Move(op) => {
|
||||
eprintln!(
|
||||
"Move value from {} to {}",
|
||||
console::Style::new().bright().red().apply_to(op.from),
|
||||
console::Style::new().green().apply_to(op.path)
|
||||
);
|
||||
}
|
||||
PatchOperation::Copy(op) => {
|
||||
eprintln!(
|
||||
"Copy value from {} to {}",
|
||||
console::Style::new().bright().red().apply_to(op.from),
|
||||
console::Style::new().green().apply_to(op.path)
|
||||
);
|
||||
}
|
||||
PatchOperation::Test(op) => {
|
||||
eprintln!(
|
||||
"Test value at {} for {}",
|
||||
console::Style::new().bright().red().apply_to(op.path),
|
||||
console::Style::new().green().apply_to(op.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !args.get_flag("dryrun") {
|
||||
if let Err(e) = json_patch::patch(&mut frontmatter, &patch) {
|
||||
eprintln!(
|
||||
"{} Patch failed",
|
||||
console::Style::new().red().apply_to("Error:")
|
||||
);
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let markdown_doc = assemble_markdown_doc(&markdown_content, frontmatter);
|
||||
if let Some(output_path) = args.get_one::<String>("output") {
|
||||
if output_path == "-" {
|
||||
print!("{markdown_doc}");
|
||||
} else {
|
||||
std::fs::write(output_path, markdown_doc)
|
||||
.quit_on_error("Could not write output file");
|
||||
}
|
||||
} else {
|
||||
std::fs::write(markdown_file, markdown_doc)
|
||||
.quit_on_error("Could not write patched file");
|
||||
}
|
||||
}
|
||||
} else if let Some(merge_file) = args.get_one::<String>("merge") {
|
||||
// TODO : Add support for merging frontmatter from markdown file
|
||||
let mut merge_doc: serde_json::Value = serde_json::from_str(
|
||||
&(if merge_file == "-" {
|
||||
let mut str = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut str)
|
||||
.quit_on_error("Coult not read stdin");
|
||||
str
|
||||
} else {
|
||||
std::fs::read_to_string(merge_file).quit_on_error("Could not read merge file")
|
||||
}),
|
||||
)
|
||||
.quit_on_error("Could not parse merge file");
|
||||
|
||||
if let Some(excludes) = args.get_many::<String>("exclude") {
|
||||
let excludes: Vec<String> = excludes.cloned().collect();
|
||||
merge_doc = exclude_json_by_paths(&excludes, &merge_doc);
|
||||
}
|
||||
|
||||
if let Some(includes) = args.get_many::<String>("keys") {
|
||||
let includes: Vec<String> = includes.cloned().collect();
|
||||
merge_doc = only_include_json_pointers(&includes, &merge_doc);
|
||||
}
|
||||
|
||||
if args.get_flag("verbose") | args.get_flag("dryrun") {
|
||||
eprintln!("Merging {}", serde_json::to_string(&merge_doc).unwrap());
|
||||
}
|
||||
|
||||
if !args.get_flag("dryrun") {
|
||||
json_patch::merge(&mut frontmatter, &merge_doc);
|
||||
|
||||
let markdown_doc = assemble_markdown_doc(&markdown_content, frontmatter);
|
||||
if let Some(output_path) = args.get_one::<String>("output") {
|
||||
if output_path == "-" {
|
||||
print!("{markdown_doc}");
|
||||
} else {
|
||||
std::fs::write(output_path, markdown_doc)
|
||||
.quit_on_error("Could not write output file");
|
||||
}
|
||||
} else {
|
||||
std::fs::write(markdown_file, markdown_doc)
|
||||
.quit_on_error("Could not write patched file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue