1#![feature(let_chains)]
2#![doc = include_str!("../README.md")]
3
4macro_rules! re {
5 ($name:ident, $e:expr) => {
6 use regex::Regex;
7 use std::sync::LazyLock;
8 static $name: LazyLock<Regex> = LazyLock::new(|| Regex::new($e).unwrap());
9 };
10}
11
12use std::{
13 borrow::Cow,
14 collections::HashMap,
15 io::Write
16};
17
18pub use kdl;
19
20use kdl::{KdlDocument, KdlNode, KdlValue};
21use regex::Captures;
22
23pub struct PluginContext<'a, 'b: 'a> {
27 pub indent: &'a str,
29 pub writer: &'a mut Writer<'b>,
31 pub emitter: &'a HtmlEmitter<'a>
33}
34
35pub trait IPlugin {
37 fn dyn_clone(&self) -> Box<dyn IPlugin>;
38 fn emit_node(&self, node: &KdlNode, context: PluginContext) -> EmitResult<bool>;
39}
40
41pub type Writer<'a> = &'a mut dyn Write;
43
44pub type EmitResult<T = ()> = Result<T, Error>;
46
47pub type Indent = usize;
52
53type Text<'b> = Cow<'b, str>;
54
55struct Plugin(Box<dyn IPlugin>);
56
57impl Plugin {
58 pub fn new<P: IPlugin + 'static>(plugin: P) -> Self {
59 Self(Box::new(plugin))
60 }
61}
62
63impl Clone for Plugin {
64 fn clone(&self) -> Self {
65 Self(self.0.dyn_clone())
66 }
67}
68
69mod error;
70
71pub use error::Error;
72
73const VOID_TAGS: &[&str] = &[
74 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
75 "track", "wbr",
76 "!DOCTYPE" ];
78
79#[derive(Clone, Default)]
81pub struct HtmlEmitterBuilder {
82 indent: Indent,
83 plugins: Vec<Plugin>,
84}
85
86impl HtmlEmitterBuilder {
87 pub fn new() -> Self {
89 Self { indent: 4, ..Self::default() }
90 }
91
92 pub fn indent(&mut self, indent: Indent) -> &mut Self {
94 self.indent = indent;
95 self
96 }
97
98 pub fn minify(&mut self) -> &mut Self {
100 self.indent = 0;
101 self
102 }
103
104 pub fn add_plugin<P: IPlugin + 'static>(&mut self, plugin: P) -> &mut Self {
106 self.plugins.push(Plugin::new(plugin));
107 self
108 }
109
110 pub fn build<'a>(&self) -> HtmlEmitter<'a> {
113 HtmlEmitter {
114 current_level: 0,
115 indent: self.indent,
116 plugins: self.plugins.clone(),
117 vars: Default::default(),
118 }
119 }
120
121 }
125
126#[derive(Clone)]
147pub struct HtmlEmitter<'a> {
148 pub indent: Indent,
149 pub current_level: Indent,
150 pub vars: HashMap<&'a str, Text<'a>>,
151 plugins: Vec<Plugin>,
152}
153
154impl<'a> HtmlEmitter<'a> {
155 pub fn builder() -> HtmlEmitterBuilder {
159 HtmlEmitterBuilder::new()
160 }
161
162 pub fn subemitter(&self) -> Self {
165 Self {
166 current_level: self.current_level + 1,
167 ..self.clone()
169 }
170 }
171
172 pub fn is_pretty(&self) -> bool {
174 self.indent > 0
175 }
176
177 pub fn write_line(&self, writer: Writer) -> EmitResult {
179 if self.is_pretty() {
180 writeln!(writer)?;
181 }
182 Ok(())
183 }
184
185 pub fn indent(&self) -> String {
195 " ".repeat(self.current_level * self.indent)
196 }
197
198 pub fn expand_string<'b>(&self, text: &'b str) -> Text<'b> {
200 re!(VAR, r"\$(\w+)");
201 VAR.replace(text, |captures: &Captures| {
202 self.vars
203 .get(&captures[1])
204 .map(ToString::to_string)
205 .unwrap_or_default()
206 })
207 }
208
209 pub fn expand_value<'b>(&self, value: &'b KdlValue) -> Text<'b> {
212 match value {
213 KdlValue::RawString(content) | KdlValue::String(content) => self.expand_string(content),
214 _ => todo!(),
215 }
216 }
217
218 pub fn emit_tag(
221 &self,
222 node: &KdlNode,
223 name: &str,
224 indent: &str,
225 writer: Writer
226 ) -> EmitResult {
227 let is_void = VOID_TAGS.contains(&name);
228
229 write!(writer, "{}<{}", indent, name)?;
231 let args = node
233 .entries()
234 .iter()
235 .map(|arg| self.expand_string(&arg.to_string()).into_owned())
236 .collect::<Vec<_>>()
237 .join("");
238
239 write!(writer, "{}", args)?;
240
241 if is_void {
242 write!(writer, ">")?;
243 self.write_line(writer)?;
244 } else {
245 write!(writer, ">")?;
246 if let Some(doc) = node.children() {
248 self.write_line(writer)?;
249 self.subemitter().emit(doc, writer)?;
250 write!(writer, "{}", indent)?;
251 }
252 write!(writer, "</{}>", name)?;
253 self.write_line(writer)?;
254 }
255 Ok(())
256 }
257
258 fn call_plugin<'b: 'a>(&'b self, node: &'a KdlNode, indent: &'b str, mut writer: Writer<'b>) -> EmitResult<bool> {
259 for plug in &self.plugins {
260 let ctx = PluginContext {
261 indent,
262 emitter: self,
263 writer: &mut writer,
264 };
265 if plug.0.emit_node(node, ctx)? {
266 return Ok(true)
267 }
268 }
269 Ok(false)
270 }
271
272 pub fn emit_text_node(&self, indent: &str, content: &KdlValue, writer: Writer) -> EmitResult {
288 write!(
289 writer,
290 "{}{}",
291 indent,
292 html_escape::encode_text(&self.expand_value(content))
293 )?;
294 self.write_line(writer)?;
295 Ok(())
296 }
297
298 pub fn emit<'b: 'a>(&mut self, document: &'b KdlDocument, writer: Writer) -> EmitResult {
320 let indent = self.indent();
321
322 for node in document.nodes() {
323 let name = node.name().value();
324
325 if name.starts_with("$")
327 && let Some(val) = node.get(0)
328 {
329 self.vars.insert(&name[1..], self.expand_value(val.value()));
330 continue;
331 }
332
333 if name == "text"
335 && let Some(content) = node.get(0)
336 {
337 self.emit_text_node(&indent, content.value(), writer)?;
338 continue;
339 }
340
341 if self.call_plugin(node, &indent, writer)? {
343 continue
344 }
345
346 self.emit_tag(node, name, &indent, writer)?
348 }
349 self.vars.clear();
351 Ok(())
352 }
353}
354
355
356#[doc(hidden)]
357pub fn emit_as_str(builder: &HtmlEmitterBuilder, input: &str) -> String {
358 let doc: kdl::KdlDocument = input.parse().expect("Failed to parse as kdl doc");
359 let mut buf = Vec::<u8>::new();
360 let mut emitter = builder.build();
361 emitter.emit(&doc, &mut buf).expect("Failed to emit HTML");
362 String::from_utf8(buf).expect("Invalid utf8 found")
363}
364#[cfg(test)]
365pub mod tests {
366 use super::*;
367 use htmeta_auto_test::*;
368
369 auto_html_test!(basic_test);
370 auto_html_test!(basic_var);
371 auto_html_test!(var_scopes);
372
373 fn minified() -> HtmlEmitterBuilder {
374 let mut builder = HtmlEmitter::builder();
375 builder.minify();
376 builder
377 }
378
379 auto_html_test!(minified_basic, minified());
380 auto_html_test!(minified_var_scopes, minified());
381
382 struct ShouterPlugin;
383
384 impl IPlugin for ShouterPlugin {
385 fn dyn_clone(&self) -> Box<dyn IPlugin> {
386 Box::new(ShouterPlugin)
387 }
388 fn emit_node(&self, node: &KdlNode, context: PluginContext) -> EmitResult<bool> {
389 let name = node.name().value();
390 context.emitter.emit_tag(node, &name.to_uppercase(), context.indent, context.writer)?;
391 Ok(true)
392 }
393 }
394
395 fn with_plugin() -> HtmlEmitterBuilder {
396 let mut builder = HtmlEmitter::builder();
397 builder.add_plugin(ShouterPlugin);
398 builder
399 }
400
401 auto_html_test!(shouter_basic, with_plugin());
402}