1use serde_json::{Map, Value, json};
2use std::path::{Component, Path, PathBuf};
3
4use crate::engine::{compile::CompileOutput, config::EngineConfig};
5
6#[derive(Debug, Default)]
8pub struct EngineReport {
9 pub collections: Vec<CollectionReport>,
10 pub errors: Vec<EngineError>,
11}
12
13#[derive(Debug)]
16pub struct EngineError {
17 pub file: PathBuf,
18 pub message: String,
19}
20
21#[derive(Debug, Default)]
23pub struct CollectionReport {
24 pub name: String,
25 pub records: usize,
26 pub output_path: PathBuf,
27}
28
29pub fn build_schema_ctx(path: &Path, root: &Path, compiled: &CompileOutput, cfg: &EngineConfig) -> dmc_schema::Ctx {
32 let mut ctx = dmc_schema::Ctx::new(path.to_path_buf(), root.to_path_buf(), compiled.content.clone());
33 ctx.html = Some(compiled.html.clone());
34 ctx.mdx_body = Some(compiled.body.clone());
35 ctx.toc = Some(serde_json::to_value(&compiled.toc).unwrap_or(Value::Array(vec![])));
36 ctx.plain_text = Some(compiled.excerpt.clone());
37 if let (Some(dir), Some(base)) = (&cfg.compile.output_assets, &cfg.compile.output_base) {
38 let mut p = dmc_schema::AssetPipeline::new(dir.into(), base.into());
39 if let Some(t) = &cfg.output_name {
40 p.name_template = t.into();
41 }
42 ctx.assets = Some(p);
43 }
44 ctx
45}
46
47pub fn build_velite_record(
50 compiled: CompileOutput,
51 frontmatter: Value,
52 path: &Path,
53 base: &Path,
54 collection: &str,
55 include_html: bool,
56) -> Value {
57 let rel = path.strip_prefix(base).unwrap_or(path);
58 let rel_str = rel.to_string_lossy().to_string();
59 let source_file_path = path.to_string_lossy().to_string();
60 let source_file_name = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
61 let source_file_dir = path
62 .parent()
63 .map(|p| {
64 let mut comps: Vec<String> = p.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect();
65 if comps.len() >= 2 {
66 let last2 = comps.split_off(comps.len() - 2);
67 last2.join("/")
68 } else {
69 comps.join("/")
70 }
71 })
72 .unwrap_or_default();
73 let content_type = path.extension().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
74 let permalink = velite_permalink(&source_file_path, &rel_str, collection);
75 let flattened_path = permalink.clone();
76 let slug = if permalink.is_empty() {
77 collection.to_lowercase()
78 } else {
79 format!("{}/{}", collection.to_lowercase(), permalink)
80 };
81
82 let mut map: Map<String, Value> = Map::new();
83 if let Value::Object(fm) = frontmatter {
84 for (k, v) in fm {
85 map.insert(k, v);
86 }
87 }
88
89 map.insert("body".into(), Value::String(compiled.body));
90 map.insert("content".into(), Value::String(compiled.content));
91 if include_html {
92 map.insert("html".into(), Value::String(compiled.html.clone()));
93 }
94 map.insert("excerpt".into(), Value::String(compiled.excerpt));
95 map.insert("metadata".into(), serde_json::to_value(&compiled.metadata).unwrap_or(json!({})));
96 map.insert("toc".into(), serde_json::to_value(&compiled.toc).unwrap_or(Value::Array(vec![])));
97 map.insert("contentType".into(), Value::String(content_type));
98 map.insert("flattenedPath".into(), Value::String(flattened_path));
99 map.insert("permalink".into(), Value::String(permalink));
100 map.insert("slug".into(), Value::String(slug));
101 map.insert("sourceFileDir".into(), Value::String(source_file_dir));
102 map.insert("sourceFileName".into(), Value::String(source_file_name));
103 map.insert("sourceFilePath".into(), Value::String(source_file_path));
104
105 Value::Object(map)
106}
107
108pub fn wrap_mdx_module(body: &str, imports: &[String]) -> String {
111 let mut stripped = body.to_string();
113 for imp in imports {
114 let trimmed = imp.trim_end_matches('\n');
115 if !trimmed.is_empty() {
116 stripped = stripped.replacen(trimmed, "", 1);
117 }
118 }
119 let mut stripped = stripped.trim_end_matches('\n').to_string();
122 if let Some(idx) = stripped.rfind("return { default:") {
123 stripped.truncate(idx);
124 } else {
125 stripped = stripped
126 .trim_end_matches("return _createMdxContent(arguments[0]);")
127 .trim_end_matches("return _createMdxContent(arguments[0])")
128 .to_string();
129 }
130 let stripped = stripped.trim_end().to_string();
131 let stripped = stripped.replace("arguments[0]", "__runtime");
134
135 let mut out = String::new();
136 out.push_str("import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'\n");
137 for i in imports {
138 out.push_str(i);
139 if !i.ends_with('\n') {
140 out.push('\n');
141 }
142 }
143 out.push_str("const __runtime = { Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs };\n");
144 out.push_str(&stripped);
145 out.push_str("\nexport default function MDXContent(props) { return _createMdxContent(props); }\n");
146 out
147}
148
149pub fn minify_js(src: &str) -> String {
153 #[derive(Clone, Copy, PartialEq)]
154 enum St {
155 Code,
156 Squote,
157 Dquote,
158 Btick,
159 LineComment,
160 BlockComment,
161 }
162 let mut out = String::with_capacity(src.len());
163 let mut st = St::Code;
164 let mut prev_ws = false;
165 let mut chars = src.chars().peekable();
166 while let Some(c) = chars.next() {
167 match st {
168 St::Code => {
169 if c == '/' {
170 if matches!(chars.peek(), Some('/')) {
171 chars.next();
172 st = St::LineComment;
173 continue;
174 }
175 if matches!(chars.peek(), Some('*')) {
176 chars.next();
177 st = St::BlockComment;
178 continue;
179 }
180 }
181 if c == '"' {
182 st = St::Dquote;
183 out.push(c);
184 prev_ws = false;
185 continue;
186 }
187 if c == '\'' {
188 st = St::Squote;
189 out.push(c);
190 prev_ws = false;
191 continue;
192 }
193 if c == '`' {
194 st = St::Btick;
195 out.push(c);
196 prev_ws = false;
197 continue;
198 }
199 if c == '\n' || c == '\t' || c == ' ' {
200 if prev_ws {
201 continue;
202 }
203 prev_ws = true;
204 out.push(' ');
205 continue;
206 }
207 prev_ws = false;
208 out.push(c);
209 },
210 St::Squote => {
211 out.push(c);
212 if c == '\\' {
213 if let Some(n) = chars.next() {
214 out.push(n);
215 }
216 continue;
217 }
218 if c == '\'' {
219 st = St::Code;
220 }
221 },
222 St::Dquote => {
223 out.push(c);
224 if c == '\\' {
225 if let Some(n) = chars.next() {
226 out.push(n);
227 }
228 continue;
229 }
230 if c == '"' {
231 st = St::Code;
232 }
233 },
234 St::Btick => {
235 out.push(c);
236 if c == '\\' {
237 if let Some(n) = chars.next() {
238 out.push(n);
239 }
240 continue;
241 }
242 if c == '`' {
243 st = St::Code;
244 }
245 },
246 St::LineComment => {
247 if c == '\n' {
248 st = St::Code;
249 }
250 },
251 St::BlockComment => {
252 if c == '*' && matches!(chars.peek(), Some('/')) {
253 chars.next();
254 st = St::Code;
255 }
256 },
257 }
258 }
259 out
260}
261
262fn velite_permalink(abs: &str, rel: &str, collection: &str) -> String {
265 let lc = collection.to_lowercase();
266 let needle = format!("/{lc}/");
267 let after = if let Some(idx) = abs.rfind(&needle) { &abs[idx + needle.len()..] } else { rel };
268 after.trim_end_matches(".mdx").trim_end_matches(".md").to_string()
269}
270
271pub fn is_js_ident(s: &str) -> bool {
273 let mut chars = s.chars();
274 match chars.next() {
275 Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {},
276 _ => return false,
277 }
278 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
279}
280
281pub fn pascal_case(name: &str) -> String {
284 let mut out = String::with_capacity(name.len());
285 let mut upper = true;
286 for ch in name.chars() {
287 if ch == '-' || ch == '_' || ch == ' ' {
288 upper = true;
289 continue;
290 }
291 if upper {
292 out.extend(ch.to_uppercase());
293 upper = false;
294 } else {
295 out.push(ch);
296 }
297 }
298 if out.is_empty() { "Doc".into() } else { out }
299}
300
301pub fn relative_from(from_dir: &Path, target: &Path) -> String {
304 let from_abs = from_dir.canonicalize().unwrap_or_else(|_| from_dir.to_path_buf());
305 let to_abs = target.canonicalize().unwrap_or_else(|_| target.to_path_buf());
306 let from_parts: Vec<Component<'_>> = from_abs.components().collect();
307 let to_parts: Vec<Component<'_>> = to_abs.components().collect();
308 let common = from_parts.iter().zip(&to_parts).take_while(|(a, b)| a == b).count();
309 let ups = from_parts.len().saturating_sub(common);
310 let mut out = String::new();
311 for _ in 0..ups {
312 out.push_str("../");
313 }
314 if ups == 0 {
315 out.push_str("./");
316 }
317 let tail: Vec<String> = to_parts[common..]
318 .iter()
319 .filter_map(|c| match c {
320 Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
321 _ => None,
322 })
323 .collect();
324 out.push_str(&tail.join("/"));
325 out
326}