dmc_transform/builtin/
copy_linked_files.rs1use crate::pipeline::Transformer;
2use crate::visit::{NodeAction, Visitor, walk_root};
3use dmc_diagnostic::Code;
4use dmc_diagnostic::metadata::SourceMeta;
5use dmc_parser::ast::*;
6use duck_diagnostic::{Diagnostic, Label, Span};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10
11pub struct CopyLinkedFiles {
16 pub source_dir: PathBuf,
17 pub assets_dir: PathBuf,
18 pub base_url: String,
19 pub name_template: String,
20 pub map: Arc<Mutex<HashMap<String, String>>>,
21}
22
23impl CopyLinkedFiles {
24 pub fn new(source_dir: PathBuf, assets_dir: PathBuf, base_url: String) -> Self {
25 Self {
26 source_dir,
27 assets_dir,
28 base_url,
29 name_template: "[name]-[hash:8].[ext]".into(),
30 map: Arc::new(Mutex::new(HashMap::new())),
31 }
32 }
33}
34
35enum Outcome {
36 Skip,
37 Published(String),
38 SourceMissing(PathBuf, std::io::Error),
39 CopyFailed(PathBuf, std::io::Error),
40}
41
42impl Transformer for CopyLinkedFiles {
43 fn name(&self) -> &str {
44 "copy-linked-files"
45 }
46 fn transform(
47 &self,
48 doc: &mut Document,
49 _meta: &SourceMeta,
50 diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
51 ) {
52 let mut v = Apply { config: self, pending: Vec::new() };
53 walk_root(&mut doc.children, &mut v);
54 for d in v.pending.drain(..) {
55 diag_engine.emit(d);
56 }
57 }
58}
59
60struct Apply<'a> {
61 config: &'a CopyLinkedFiles,
62 pending: Vec<Diagnostic<Code>>,
63}
64
65impl<'a> Apply<'a> {
66 fn rewrite_slot(&mut self, raw_slot: &mut String, span: Span, kind: &'static str) {
67 match self.config.publish(raw_slot) {
68 Outcome::Skip => {},
69 Outcome::Published(url) => *raw_slot = url,
70 Outcome::SourceMissing(path, err) => {
71 self.pending.push(
72 Diagnostic::new(
73 Code::AssetSourceMissing,
74 format!("copy-linked-files: cannot read {} source {} ({})", kind, path.display(), err),
75 )
76 .with_label(Label::primary(span, Some(format!("from this {}", kind)))),
77 );
78 },
79 Outcome::CopyFailed(path, err) => {
80 self.pending.push(
81 Diagnostic::new(
82 Code::AssetCopyFailed,
83 format!("copy-linked-files: failed to write asset {} ({})", path.display(), err),
84 )
85 .with_label(Label::primary(span, Some(format!("for this {}", kind)))),
86 );
87 },
88 }
89 }
90}
91
92impl<'a> Visitor for Apply<'a> {
93 fn visit_node(&mut self, node: &mut Node) -> NodeAction {
94 match node {
95 Node::Image(i) => {
96 let span = i.span.clone();
97 self.rewrite_slot(&mut i.src, span, "image");
98 },
99 Node::Link(l) if l.href.starts_with("./") || l.href.starts_with("../") => {
100 let span = l.span.clone();
101 self.rewrite_slot(&mut l.href, span, "link");
102 },
103 _ => {},
104 }
105 NodeAction::Keep
106 }
107}
108
109impl CopyLinkedFiles {
110 fn publish(&self, raw: &str) -> Outcome {
111 if raw.starts_with("http://")
112 || raw.starts_with("https://")
113 || raw.starts_with("//")
114 || raw.starts_with('/')
115 || raw.starts_with('#')
116 {
117 return Outcome::Skip;
118 }
119 {
120 let map = self.map.lock().unwrap();
121 if let Some(u) = map.get(raw) {
122 return Outcome::Published(u.clone());
123 }
124 }
125 let path = self.source_dir.join(raw);
126 let bytes = match std::fs::read(&path) {
127 Ok(b) => b,
128 Err(e) => return Outcome::SourceMissing(path, e),
129 };
130 let hash = blake3::hash(&bytes);
131 let hash8 = &hash.to_hex().to_string()[..8];
132 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("asset");
133 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("bin");
134 let filename = self.name_template.replace("[name]", stem).replace("[hash:8]", hash8).replace("[ext]", ext);
135 let dest = self.assets_dir.join(&filename);
136 if let Err(e) = std::fs::create_dir_all(&self.assets_dir) {
137 return Outcome::CopyFailed(self.assets_dir.clone(), e);
138 }
139 if !dest.exists()
140 && let Err(e) = std::fs::write(&dest, &bytes)
141 {
142 return Outcome::CopyFailed(dest, e);
143 }
144 let mut url = self.base_url.clone();
145 if !url.ends_with('/') {
146 url.push('/');
147 }
148 url.push_str(&filename);
149 self.map.lock().unwrap().insert(raw.to_string(), url.clone());
150 Outcome::Published(url)
151 }
152}