Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use clap::Parser;
use lol_html::html_content::ContentType;
use lol_html::{element, rewrite_str, text, RewriteStrSettings};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::sync::{Arc, Mutex};
use thiserror::Error;
/// Pipelined HTML ops
#[derive(Clone, Debug, Parser)]
#[clap(author, version, about)]
pub struct CliArgs {
/// Path to the input html file
#[clap(short, long, value_parser)]
input: String,
/// Path to write the transformed html to. If not present and --inline is not specified, the output will be written to stdout
#[clap(short, long, value_parser)]
output: Option<String>,
/// Path to the file defining operations to perform on the html
#[clap(short = 'f', long, value_parser)]
opfile: String,
/// Write the transformed html back to the original file. This will cause --output to be ignored
#[clap(short = 'l', long, value_parser, default_value_t = false)]
inline: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "action")]
pub enum ConfigEntry {
#[serde(rename = "wrap")]
Wrap { template: String },
}
pub type ConfigFile = HashMap<String, ConfigEntry>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Failed to open file: {0}")]
FailedToOpen(String),
#[error("Failed to read file: {0}")]
FailedToRead(String),
#[error("{0}")]
IoError(#[from] std::io::Error),
#[error("Opfile was not valid: {0}")]
InvalidOpfile(toml::de::Error),
#[error("Failed to match selector {0}")]
SelectorFailed(String),
}
pub type AppResult<T> = Result<T, AppError>;
pub fn read_and_drop(path: &String) -> AppResult<String> {
let mut file_handle = File::open(path).map_err(|_| AppError::FailedToOpen(path.clone()))?;
let length = file_handle.metadata()?.len();
let mut file_buffer = String::with_capacity(length as usize);
file_handle
.read_to_string(&mut file_buffer)
.map_err(|_| AppError::FailedToRead(path.clone()))?;
Ok(file_buffer)
}
fn main() -> AppResult<()> {
let args: CliArgs = CliArgs::parse();
let input_data = read_and_drop(&args.input)?;
let opfile_data = read_and_drop(&args.opfile)?;
let ops: ConfigFile = toml::from_str(opfile_data.as_str()).map_err(AppError::InvalidOpfile)?;
let html = rewrite_str(
input_data.as_str(),
RewriteStrSettings {
element_content_handlers: ops
.into_iter()
.flat_map(|(selector, op)| match op {
ConfigEntry::Wrap { template } => {
let el_buffer = Arc::new(Mutex::new(String::new()));
let txt_buffer = el_buffer.clone();
vec![
element!(selector, move |e| {
el_buffer.lock().unwrap().clear();
let attr_list = e
.attributes()
.iter()
.map(|a| format!(r#"{}="{}""#, a.name(), a.value()))
.collect::<Vec<String>>()
.join(" ");
el_buffer.lock().unwrap().push_str(&format!(
"<{} {}>",
e.tag_name(),
attr_list
));
e.remove();
let inner_buffer = el_buffer.clone();
let inner_template = template.clone();
e.on_end_tag(move |end_tag| {
inner_buffer
.lock()
.unwrap()
.push_str(&format!("</{}>", end_tag.name()));
end_tag.after(
&inner_template.replace(
"{{{}}}",
inner_buffer.lock().unwrap().as_str(),
),
ContentType::Html,
);
end_tag.remove();
Ok(())
})
.expect("Failed to handle end tag");
Ok(())
}),
text!(selector, move |t| {
txt_buffer.lock().unwrap().push_str(t.as_str());
t.remove();
Ok(())
}),
]
}
})
.collect(),
document_content_handlers: vec![],
strict: true,
enable_esi_tags: true,
},
);
println!("{}", html.unwrap());
Ok(())
}