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