1use std::collections::HashMap;
4use std::error::Error as StdError;
5use std::path::{Path, PathBuf};
6
7use crate::value::QuillValue;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct FieldSchema {
12 pub r#type: Option<String>,
14 pub required: bool,
16 pub description: String,
18 pub example: Option<QuillValue>,
20 pub default: Option<QuillValue>,
22}
23
24impl FieldSchema {
25 pub fn new(description: String) -> Self {
27 Self {
28 r#type: None,
29 required: false,
30 description,
31 example: None,
32 default: None,
33 }
34 }
35
36 pub fn from_quill_value(value: &QuillValue) -> Result<Self, String> {
38 let obj = value
39 .as_object()
40 .ok_or_else(|| "Field schema must be an object".to_string())?;
41
42 let description = obj
43 .get("description")
44 .and_then(|v| v.as_str())
45 .unwrap_or("")
46 .to_string();
47
48 let required = obj
49 .get("required")
50 .and_then(|v| v.as_bool())
51 .unwrap_or(false);
52
53 let field_type = obj
54 .get("type")
55 .and_then(|v| v.as_str())
56 .map(|s| s.to_string());
57
58 let example = obj.get("example").map(|v| QuillValue::from_json(v.clone()));
59
60 let default = obj.get("default").map(|v| QuillValue::from_json(v.clone()));
61
62 Ok(Self {
63 r#type: field_type,
64 required,
65 description,
66 example,
67 default,
68 })
69 }
70
71 pub fn to_quill_value(&self) -> QuillValue {
73 let mut map = serde_json::Map::new();
74
75 map.insert(
76 "description".to_string(),
77 serde_json::Value::String(self.description.clone()),
78 );
79
80 map.insert(
81 "required".to_string(),
82 serde_json::Value::Bool(self.required),
83 );
84
85 if let Some(ref field_type) = self.r#type {
86 map.insert(
87 "type".to_string(),
88 serde_json::Value::String(field_type.clone()),
89 );
90 }
91
92 if let Some(ref example) = self.example {
93 map.insert("example".to_string(), example.as_json().clone());
94 }
95
96 if let Some(ref default) = self.default {
97 map.insert("default".to_string(), default.as_json().clone());
98 }
99
100 QuillValue::from_json(serde_json::Value::Object(map))
101 }
102}
103
104#[derive(Debug, Clone)]
106pub enum FileTreeNode {
107 File {
109 contents: Vec<u8>,
111 },
112 Directory {
114 files: HashMap<String, FileTreeNode>,
116 },
117}
118
119impl FileTreeNode {
120 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
122 let path = path.as_ref();
123
124 if path == Path::new("") {
126 return Some(self);
127 }
128
129 let components: Vec<_> = path
131 .components()
132 .filter_map(|c| {
133 if let std::path::Component::Normal(s) = c {
134 s.to_str()
135 } else {
136 None
137 }
138 })
139 .collect();
140
141 if components.is_empty() {
142 return Some(self);
143 }
144
145 let mut current_node = self;
147 for component in components {
148 match current_node {
149 FileTreeNode::Directory { files } => {
150 current_node = files.get(component)?;
151 }
152 FileTreeNode::File { .. } => {
153 return None; }
155 }
156 }
157
158 Some(current_node)
159 }
160
161 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
163 match self.get_node(path)? {
164 FileTreeNode::File { contents } => Some(contents.as_slice()),
165 FileTreeNode::Directory { .. } => None,
166 }
167 }
168
169 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
171 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
172 }
173
174 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
176 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
177 }
178
179 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
181 match self.get_node(dir_path) {
182 Some(FileTreeNode::Directory { files }) => files
183 .iter()
184 .filter_map(|(name, node)| {
185 if matches!(node, FileTreeNode::File { .. }) {
186 Some(name.clone())
187 } else {
188 None
189 }
190 })
191 .collect(),
192 _ => Vec::new(),
193 }
194 }
195
196 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
198 match self.get_node(dir_path) {
199 Some(FileTreeNode::Directory { files }) => files
200 .iter()
201 .filter_map(|(name, node)| {
202 if matches!(node, FileTreeNode::Directory { .. }) {
203 Some(name.clone())
204 } else {
205 None
206 }
207 })
208 .collect(),
209 _ => Vec::new(),
210 }
211 }
212
213 pub fn insert<P: AsRef<Path>>(
215 &mut self,
216 path: P,
217 node: FileTreeNode,
218 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
219 let path = path.as_ref();
220
221 let components: Vec<_> = path
223 .components()
224 .filter_map(|c| {
225 if let std::path::Component::Normal(s) = c {
226 s.to_str().map(|s| s.to_string())
227 } else {
228 None
229 }
230 })
231 .collect();
232
233 if components.is_empty() {
234 return Err("Cannot insert at root path".into());
235 }
236
237 let mut current_node = self;
239 for component in &components[..components.len() - 1] {
240 match current_node {
241 FileTreeNode::Directory { files } => {
242 current_node =
243 files
244 .entry(component.clone())
245 .or_insert_with(|| FileTreeNode::Directory {
246 files: HashMap::new(),
247 });
248 }
249 FileTreeNode::File { .. } => {
250 return Err("Cannot traverse into a file".into());
251 }
252 }
253 }
254
255 let filename = &components[components.len() - 1];
257 match current_node {
258 FileTreeNode::Directory { files } => {
259 files.insert(filename.clone(), node);
260 Ok(())
261 }
262 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
263 }
264 }
265
266 fn from_json_value(value: &serde_json::Value) -> Result<Self, Box<dyn StdError + Send + Sync>> {
268 if let Some(contents_str) = value.get("contents").and_then(|v| v.as_str()) {
269 Ok(FileTreeNode::File {
271 contents: contents_str.as_bytes().to_vec(),
272 })
273 } else if let Some(bytes_array) = value.get("contents").and_then(|v| v.as_array()) {
274 let contents: Vec<u8> = bytes_array
276 .iter()
277 .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
278 .collect();
279 Ok(FileTreeNode::File { contents })
280 } else if let Some(obj) = value.as_object() {
281 let mut files = HashMap::new();
283 for (name, child_value) in obj {
284 files.insert(name.clone(), Self::from_json_value(child_value)?);
285 }
286 Ok(FileTreeNode::Directory { files })
288 } else {
289 Err(format!("Invalid file tree node: {:?}", value).into())
290 }
291 }
292}
293
294#[derive(Debug, Clone)]
296pub struct QuillIgnore {
297 patterns: Vec<String>,
298}
299
300impl QuillIgnore {
301 pub fn new(patterns: Vec<String>) -> Self {
303 Self { patterns }
304 }
305
306 pub fn from_content(content: &str) -> Self {
308 let patterns = content
309 .lines()
310 .map(|line| line.trim())
311 .filter(|line| !line.is_empty() && !line.starts_with('#'))
312 .map(|line| line.to_string())
313 .collect();
314 Self::new(patterns)
315 }
316
317 pub fn is_ignored<P: AsRef<Path>>(&self, path: P) -> bool {
319 let path = path.as_ref();
320 let path_str = path.to_string_lossy();
321
322 for pattern in &self.patterns {
323 if self.matches_pattern(pattern, &path_str) {
324 return true;
325 }
326 }
327 false
328 }
329
330 fn matches_pattern(&self, pattern: &str, path: &str) -> bool {
332 if pattern.ends_with('/') {
334 let pattern_prefix = &pattern[..pattern.len() - 1];
335 return path.starts_with(pattern_prefix)
336 && (path.len() == pattern_prefix.len()
337 || path.chars().nth(pattern_prefix.len()) == Some('/'));
338 }
339
340 if !pattern.contains('*') {
342 return path == pattern || path.ends_with(&format!("/{}", pattern));
343 }
344
345 if pattern == "*" {
347 return true;
348 }
349
350 let pattern_parts: Vec<&str> = pattern.split('*').collect();
352 if pattern_parts.len() == 2 {
353 let (prefix, suffix) = (pattern_parts[0], pattern_parts[1]);
354 if prefix.is_empty() {
355 return path.ends_with(suffix);
356 } else if suffix.is_empty() {
357 return path.starts_with(prefix);
358 } else {
359 return path.starts_with(prefix) && path.ends_with(suffix);
360 }
361 }
362
363 false
364 }
365}
366
367#[derive(Debug, Clone)]
369pub struct Quill {
370 pub glue_template: String,
372 pub metadata: HashMap<String, QuillValue>,
374 pub name: String,
376 pub backend: String,
378 pub glue_file: String,
380 pub example: Option<String>,
382 pub field_schemas: HashMap<String, FieldSchema>,
384 pub files: FileTreeNode,
386}
387
388impl Quill {
389 pub fn from_path<P: AsRef<std::path::Path>>(
391 path: P,
392 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
393 use std::fs;
394
395 let path = path.as_ref();
396 let name = path
397 .file_name()
398 .and_then(|n| n.to_str())
399 .unwrap_or("unnamed")
400 .to_string();
401
402 let quillignore_path = path.join(".quillignore");
404 let ignore = if quillignore_path.exists() {
405 let ignore_content = fs::read_to_string(&quillignore_path)
406 .map_err(|e| format!("Failed to read .quillignore: {}", e))?;
407 QuillIgnore::from_content(&ignore_content)
408 } else {
409 QuillIgnore::new(vec![
411 ".git/".to_string(),
412 ".gitignore".to_string(),
413 ".quillignore".to_string(),
414 "target/".to_string(),
415 "node_modules/".to_string(),
416 ])
417 };
418
419 let root = Self::load_directory_as_tree(path, path, &ignore)?;
421
422 Self::from_tree(root, Some(name))
424 }
425
426 pub fn from_tree(
444 root: FileTreeNode,
445 default_name: Option<String>,
446 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
447 let quill_toml_bytes = root
449 .get_file("Quill.toml")
450 .ok_or("Quill.toml not found in file tree")?;
451
452 let quill_toml_content = String::from_utf8(quill_toml_bytes.to_vec())
453 .map_err(|e| format!("Quill.toml is not valid UTF-8: {}", e))?;
454
455 let quill_toml: toml::Value = toml::from_str(&quill_toml_content)
456 .map_err(|e| format!("Failed to parse Quill.toml: {}", e))?;
457
458 let mut metadata = HashMap::new();
459 let mut glue_file = "glue.typ".to_string(); let mut template_file: Option<String> = None;
461 let mut quill_name = default_name.unwrap_or_else(|| "unnamed".to_string());
462 let mut backend = String::new();
463 let mut field_schemas = HashMap::new();
464
465 if let Some(quill_section) = quill_toml.get("Quill") {
467 if let Some(name_val) = quill_section.get("name").and_then(|v| v.as_str()) {
469 quill_name = name_val.to_string();
470 }
471
472 if let Some(backend_val) = quill_section.get("backend").and_then(|v| v.as_str()) {
473 backend = backend_val.to_string();
474 match QuillValue::from_toml(&toml::Value::String(backend_val.to_string())) {
475 Ok(quill_value) => {
476 metadata.insert("backend".to_string(), quill_value);
477 }
478 Err(e) => {
479 eprintln!("Warning: Failed to convert backend field: {}", e);
480 }
481 }
482 }
483
484 if let Some(glue_val) = quill_section.get("glue").and_then(|v| v.as_str()) {
485 glue_file = glue_val.to_string();
486 }
487
488 if let Some(example_val) = quill_section.get("example").and_then(|v| v.as_str()) {
489 template_file = Some(example_val.to_string());
490 }
491
492 let description = quill_section
494 .get("description")
495 .and_then(|v| v.as_str())
496 .ok_or("Missing required 'description' field in [Quill] section")?;
497
498 if description.trim().is_empty() {
499 return Err("'description' field in [Quill] section cannot be empty".into());
500 }
501
502 if let toml::Value::Table(table) = quill_section {
504 for (key, value) in table {
505 if key != "name"
506 && key != "backend"
507 && key != "glue"
508 && key != "example"
509 && key != "version"
510 {
511 match QuillValue::from_toml(value) {
512 Ok(quill_value) => {
513 metadata.insert(key.clone(), quill_value);
514 }
515 Err(e) => {
516 eprintln!("Warning: Failed to convert field '{}': {}", key, e);
517 }
518 }
519 }
520 }
521 }
522 }
523
524 if let Some(typst_section) = quill_toml.get("typst") {
526 if let toml::Value::Table(table) = typst_section {
527 for (key, value) in table {
528 match QuillValue::from_toml(value) {
529 Ok(quill_value) => {
530 metadata.insert(format!("typst_{}", key), quill_value);
531 }
532 Err(e) => {
533 eprintln!("Warning: Failed to convert typst field '{}': {}", key, e);
534 }
535 }
536 }
537 }
538 }
539
540 if let Some(fields_section) = quill_toml.get("fields") {
542 if let toml::Value::Table(fields_table) = fields_section {
543 for (field_name, field_schema) in fields_table {
544 match QuillValue::from_toml(field_schema) {
545 Ok(quill_value) => match FieldSchema::from_quill_value(&quill_value) {
546 Ok(schema) => {
547 field_schemas.insert(field_name.clone(), schema);
548 }
549 Err(e) => {
550 eprintln!(
551 "Warning: Failed to parse field schema '{}': {}",
552 field_name, e
553 );
554 }
555 },
556 Err(e) => {
557 eprintln!(
558 "Warning: Failed to convert field schema '{}': {}",
559 field_name, e
560 );
561 }
562 }
563 }
564 }
565 }
566
567 let glue_bytes = root
569 .get_file(&glue_file)
570 .ok_or_else(|| format!("Glue file '{}' not found in file tree", glue_file))?;
571
572 let template_content = String::from_utf8(glue_bytes.to_vec())
573 .map_err(|e| format!("Glue file '{}' is not valid UTF-8: {}", glue_file, e))?;
574
575 let template_content_opt = if let Some(ref template_file_name) = template_file {
577 root.get_file(template_file_name).and_then(|bytes| {
578 String::from_utf8(bytes.to_vec())
579 .map_err(|e| {
580 eprintln!(
581 "Warning: Template file '{}' is not valid UTF-8: {}",
582 template_file_name, e
583 );
584 e
585 })
586 .ok()
587 })
588 } else {
589 None
590 };
591
592 let quill = Quill {
593 glue_template: template_content,
594 metadata,
595 name: quill_name,
596 backend,
597 glue_file,
598 example: template_content_opt,
599 field_schemas,
600 files: root,
601 };
602
603 quill.validate()?;
605
606 Ok(quill)
607 }
608
609 pub fn from_json(json_str: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
616 use serde_json::Value as JsonValue;
617
618 let json: JsonValue =
619 serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {}", e))?;
620
621 let obj = json.as_object().ok_or_else(|| "Root must be an object")?;
622
623 let default_name = obj
625 .get("metadata")
626 .and_then(|m| m.get("name"))
627 .and_then(|v| v.as_str())
628 .map(String::from);
629
630 let files_obj = obj
632 .get("files")
633 .and_then(|v| v.as_object())
634 .ok_or_else(|| "Missing or invalid 'files' key")?;
635
636 let mut root_files = HashMap::new();
638 for (key, value) in files_obj {
639 root_files.insert(key.clone(), FileTreeNode::from_json_value(value)?);
640 }
641
642 let root = FileTreeNode::Directory { files: root_files };
643
644 Self::from_tree(root, default_name)
646 }
647
648 fn load_directory_as_tree(
650 current_dir: &Path,
651 base_dir: &Path,
652 ignore: &QuillIgnore,
653 ) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
654 use std::fs;
655
656 if !current_dir.exists() {
657 return Ok(FileTreeNode::Directory {
658 files: HashMap::new(),
659 });
660 }
661
662 let mut files = HashMap::new();
663
664 for entry in fs::read_dir(current_dir)? {
665 let entry = entry?;
666 let path = entry.path();
667 let relative_path = path
668 .strip_prefix(base_dir)
669 .map_err(|e| format!("Failed to get relative path: {}", e))?
670 .to_path_buf();
671
672 if ignore.is_ignored(&relative_path) {
674 continue;
675 }
676
677 let filename = path
679 .file_name()
680 .and_then(|n| n.to_str())
681 .ok_or_else(|| format!("Invalid filename: {}", path.display()))?
682 .to_string();
683
684 if path.is_file() {
685 let contents = fs::read(&path)
686 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
687
688 files.insert(filename, FileTreeNode::File { contents });
689 } else if path.is_dir() {
690 let subdir_tree = Self::load_directory_as_tree(&path, base_dir, ignore)?;
692 files.insert(filename, subdir_tree);
693 }
694 }
695
696 Ok(FileTreeNode::Directory { files })
697 }
698
699 pub fn typst_packages(&self) -> Vec<String> {
701 self.metadata
702 .get("typst_packages")
703 .and_then(|v| v.as_array())
704 .map(|arr| {
705 arr.iter()
706 .filter_map(|v| v.as_str().map(|s| s.to_string()))
707 .collect()
708 })
709 .unwrap_or_default()
710 }
711
712 pub fn validate(&self) -> Result<(), Box<dyn StdError + Send + Sync>> {
714 if !self.files.file_exists(&self.glue_file) {
716 return Err(format!("Glue file '{}' does not exist", self.glue_file).into());
717 }
718 Ok(())
719 }
720
721 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
723 self.files.get_file(path)
724 }
725
726 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
728 self.files.file_exists(path)
729 }
730
731 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
733 self.files.dir_exists(path)
734 }
735
736 pub fn list_files<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
738 self.files.list_files(path)
739 }
740
741 pub fn list_subdirectories<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
743 self.files.list_subdirectories(path)
744 }
745
746 pub fn list_directory<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
748 let dir_path = dir_path.as_ref();
749 let filenames = self.files.list_files(dir_path);
750
751 filenames
753 .iter()
754 .map(|name| {
755 if dir_path == Path::new("") {
756 PathBuf::from(name)
757 } else {
758 dir_path.join(name)
759 }
760 })
761 .collect()
762 }
763
764 pub fn list_directories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
766 let dir_path = dir_path.as_ref();
767 let subdirs = self.files.list_subdirectories(dir_path);
768
769 subdirs
771 .iter()
772 .map(|name| {
773 if dir_path == Path::new("") {
774 PathBuf::from(name)
775 } else {
776 dir_path.join(name)
777 }
778 })
779 .collect()
780 }
781
782 pub fn find_files<P: AsRef<Path>>(&self, pattern: P) -> Vec<PathBuf> {
784 let pattern_str = pattern.as_ref().to_string_lossy();
785 let mut matches = Vec::new();
786
787 let glob_pattern = match glob::Pattern::new(&pattern_str) {
789 Ok(pat) => pat,
790 Err(_) => return matches, };
792
793 self.find_files_recursive(&self.files, Path::new(""), &glob_pattern, &mut matches);
795
796 matches.sort();
797 matches
798 }
799
800 fn find_files_recursive(
802 &self,
803 node: &FileTreeNode,
804 current_path: &Path,
805 pattern: &glob::Pattern,
806 matches: &mut Vec<PathBuf>,
807 ) {
808 match node {
809 FileTreeNode::File { .. } => {
810 let path_str = current_path.to_string_lossy();
811 if pattern.matches(&path_str) {
812 matches.push(current_path.to_path_buf());
813 }
814 }
815 FileTreeNode::Directory { files } => {
816 for (name, child_node) in files {
817 let child_path = if current_path == Path::new("") {
818 PathBuf::from(name)
819 } else {
820 current_path.join(name)
821 };
822 self.find_files_recursive(child_node, &child_path, pattern, matches);
823 }
824 }
825 }
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832 use std::fs;
833 use tempfile::TempDir;
834
835 #[test]
836 fn test_quillignore_parsing() {
837 let ignore_content = r#"
838# This is a comment
839*.tmp
840target/
841node_modules/
842.git/
843"#;
844 let ignore = QuillIgnore::from_content(ignore_content);
845 assert_eq!(ignore.patterns.len(), 4);
846 assert!(ignore.patterns.contains(&"*.tmp".to_string()));
847 assert!(ignore.patterns.contains(&"target/".to_string()));
848 }
849
850 #[test]
851 fn test_quillignore_matching() {
852 let ignore = QuillIgnore::new(vec![
853 "*.tmp".to_string(),
854 "target/".to_string(),
855 "node_modules/".to_string(),
856 ".git/".to_string(),
857 ]);
858
859 assert!(ignore.is_ignored("test.tmp"));
861 assert!(ignore.is_ignored("path/to/file.tmp"));
862 assert!(!ignore.is_ignored("test.txt"));
863
864 assert!(ignore.is_ignored("target"));
866 assert!(ignore.is_ignored("target/debug"));
867 assert!(ignore.is_ignored("target/debug/deps"));
868 assert!(!ignore.is_ignored("src/target.rs"));
869
870 assert!(ignore.is_ignored("node_modules"));
871 assert!(ignore.is_ignored("node_modules/package"));
872 assert!(!ignore.is_ignored("my_node_modules"));
873 }
874
875 #[test]
876 fn test_in_memory_file_system() {
877 let temp_dir = TempDir::new().unwrap();
878 let quill_dir = temp_dir.path();
879
880 fs::write(
882 quill_dir.join("Quill.toml"),
883 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test quill\"",
884 )
885 .unwrap();
886 fs::write(quill_dir.join("glue.typ"), "test template").unwrap();
887
888 let assets_dir = quill_dir.join("assets");
889 fs::create_dir_all(&assets_dir).unwrap();
890 fs::write(assets_dir.join("test.txt"), "asset content").unwrap();
891
892 let packages_dir = quill_dir.join("packages");
893 fs::create_dir_all(&packages_dir).unwrap();
894 fs::write(packages_dir.join("package.typ"), "package content").unwrap();
895
896 let quill = Quill::from_path(quill_dir).unwrap();
898
899 assert!(quill.file_exists("glue.typ"));
901 assert!(quill.file_exists("assets/test.txt"));
902 assert!(quill.file_exists("packages/package.typ"));
903 assert!(!quill.file_exists("nonexistent.txt"));
904
905 let asset_content = quill.get_file("assets/test.txt").unwrap();
907 assert_eq!(asset_content, b"asset content");
908
909 let asset_files = quill.list_directory("assets");
911 assert_eq!(asset_files.len(), 1);
912 assert!(asset_files.contains(&PathBuf::from("assets/test.txt")));
913 }
914
915 #[test]
916 fn test_quillignore_integration() {
917 let temp_dir = TempDir::new().unwrap();
918 let quill_dir = temp_dir.path();
919
920 fs::write(quill_dir.join(".quillignore"), "*.tmp\ntarget/\n").unwrap();
922
923 fs::write(
925 quill_dir.join("Quill.toml"),
926 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test quill\"",
927 )
928 .unwrap();
929 fs::write(quill_dir.join("glue.typ"), "test template").unwrap();
930 fs::write(quill_dir.join("should_ignore.tmp"), "ignored").unwrap();
931
932 let target_dir = quill_dir.join("target");
933 fs::create_dir_all(&target_dir).unwrap();
934 fs::write(target_dir.join("debug.txt"), "also ignored").unwrap();
935
936 let quill = Quill::from_path(quill_dir).unwrap();
938
939 assert!(quill.file_exists("glue.typ"));
941 assert!(!quill.file_exists("should_ignore.tmp"));
942 assert!(!quill.file_exists("target/debug.txt"));
943 }
944
945 #[test]
946 fn test_find_files_pattern() {
947 let temp_dir = TempDir::new().unwrap();
948 let quill_dir = temp_dir.path();
949
950 fs::write(
952 quill_dir.join("Quill.toml"),
953 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test quill\"",
954 )
955 .unwrap();
956 fs::write(quill_dir.join("glue.typ"), "template").unwrap();
957
958 let assets_dir = quill_dir.join("assets");
959 fs::create_dir_all(&assets_dir).unwrap();
960 fs::write(assets_dir.join("image.png"), "png data").unwrap();
961 fs::write(assets_dir.join("data.json"), "json data").unwrap();
962
963 let fonts_dir = assets_dir.join("fonts");
964 fs::create_dir_all(&fonts_dir).unwrap();
965 fs::write(fonts_dir.join("font.ttf"), "font data").unwrap();
966
967 let quill = Quill::from_path(quill_dir).unwrap();
969
970 let all_assets = quill.find_files("assets/*");
972 assert!(all_assets.len() >= 3); let typ_files = quill.find_files("*.typ");
975 assert_eq!(typ_files.len(), 1);
976 assert!(typ_files.contains(&PathBuf::from("glue.typ")));
977 }
978
979 #[test]
980 fn test_new_standardized_toml_format() {
981 let temp_dir = TempDir::new().unwrap();
982 let quill_dir = temp_dir.path();
983
984 let toml_content = r#"[Quill]
986name = "my-custom-quill"
987backend = "typst"
988glue = "custom_glue.typ"
989description = "Test quill with new format"
990author = "Test Author"
991"#;
992 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
993 fs::write(
994 quill_dir.join("custom_glue.typ"),
995 "= Custom Template\n\nThis is a custom template.",
996 )
997 .unwrap();
998
999 let quill = Quill::from_path(quill_dir).unwrap();
1001
1002 assert_eq!(quill.name, "my-custom-quill");
1004
1005 assert_eq!(quill.glue_file, "custom_glue.typ");
1007
1008 assert!(quill.metadata.contains_key("backend"));
1010 if let Some(backend_val) = quill.metadata.get("backend") {
1011 if let Some(backend_str) = backend_val.as_str() {
1012 assert_eq!(backend_str, "typst");
1013 } else {
1014 panic!("Backend value is not a string");
1015 }
1016 }
1017
1018 assert!(quill.metadata.contains_key("description"));
1020 assert!(quill.metadata.contains_key("author"));
1021 assert!(!quill.metadata.contains_key("version")); assert!(quill.glue_template.contains("Custom Template"));
1025 assert!(quill.glue_template.contains("custom template"));
1026 }
1027
1028 #[test]
1029 fn test_typst_packages_parsing() {
1030 let temp_dir = TempDir::new().unwrap();
1031 let quill_dir = temp_dir.path();
1032
1033 let toml_content = r#"
1034[Quill]
1035name = "test-quill"
1036backend = "typst"
1037glue = "glue.typ"
1038description = "Test quill for packages"
1039
1040[typst]
1041packages = ["@preview/bubble:0.2.2", "@preview/example:1.0.0"]
1042"#;
1043
1044 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1045 fs::write(quill_dir.join("glue.typ"), "test").unwrap();
1046
1047 let quill = Quill::from_path(quill_dir).unwrap();
1048 let packages = quill.typst_packages();
1049
1050 assert_eq!(packages.len(), 2);
1051 assert_eq!(packages[0], "@preview/bubble:0.2.2");
1052 assert_eq!(packages[1], "@preview/example:1.0.0");
1053 }
1054
1055 #[test]
1056 fn test_template_loading() {
1057 let temp_dir = TempDir::new().unwrap();
1058 let quill_dir = temp_dir.path();
1059
1060 let toml_content = r#"[Quill]
1062name = "test-with-template"
1063backend = "typst"
1064glue = "glue.typ"
1065example = "example.md"
1066description = "Test quill with template"
1067"#;
1068 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1069 fs::write(quill_dir.join("glue.typ"), "glue content").unwrap();
1070 fs::write(
1071 quill_dir.join("example.md"),
1072 "---\ntitle: Test\n---\n\nThis is a test template.",
1073 )
1074 .unwrap();
1075
1076 let quill = Quill::from_path(quill_dir).unwrap();
1078
1079 assert!(quill.example.is_some());
1081 let example = quill.example.unwrap();
1082 assert!(example.contains("title: Test"));
1083 assert!(example.contains("This is a test template"));
1084
1085 assert_eq!(quill.glue_template, "glue content");
1087 }
1088
1089 #[test]
1090 fn test_template_optional() {
1091 let temp_dir = TempDir::new().unwrap();
1092 let quill_dir = temp_dir.path();
1093
1094 let toml_content = r#"[Quill]
1096name = "test-without-template"
1097backend = "typst"
1098glue = "glue.typ"
1099description = "Test quill without template"
1100"#;
1101 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1102 fs::write(quill_dir.join("glue.typ"), "glue content").unwrap();
1103
1104 let quill = Quill::from_path(quill_dir).unwrap();
1106
1107 assert_eq!(quill.example, None);
1109
1110 assert_eq!(quill.glue_template, "glue content");
1112 }
1113
1114 #[test]
1115 fn test_from_tree() {
1116 let mut root_files = HashMap::new();
1118
1119 let quill_toml = r#"[Quill]
1121name = "test-from-tree"
1122backend = "typst"
1123glue = "glue.typ"
1124description = "A test quill from tree"
1125"#;
1126 root_files.insert(
1127 "Quill.toml".to_string(),
1128 FileTreeNode::File {
1129 contents: quill_toml.as_bytes().to_vec(),
1130 },
1131 );
1132
1133 let glue_content = "= Test Template\n\nThis is a test.";
1135 root_files.insert(
1136 "glue.typ".to_string(),
1137 FileTreeNode::File {
1138 contents: glue_content.as_bytes().to_vec(),
1139 },
1140 );
1141
1142 let root = FileTreeNode::Directory { files: root_files };
1143
1144 let quill = Quill::from_tree(root, Some("test-from-tree".to_string())).unwrap();
1146
1147 assert_eq!(quill.name, "test-from-tree");
1149 assert_eq!(quill.glue_file, "glue.typ");
1150 assert_eq!(quill.glue_template, glue_content);
1151 assert!(quill.metadata.contains_key("backend"));
1152 assert!(quill.metadata.contains_key("description"));
1153 }
1154
1155 #[test]
1156 fn test_from_tree_with_template() {
1157 let mut root_files = HashMap::new();
1158
1159 let quill_toml = r#"[Quill]
1161name = "test-tree-template"
1162backend = "typst"
1163glue = "glue.typ"
1164example = "template.md"
1165description = "Test tree with template"
1166"#;
1167 root_files.insert(
1168 "Quill.toml".to_string(),
1169 FileTreeNode::File {
1170 contents: quill_toml.as_bytes().to_vec(),
1171 },
1172 );
1173
1174 root_files.insert(
1176 "glue.typ".to_string(),
1177 FileTreeNode::File {
1178 contents: b"glue content".to_vec(),
1179 },
1180 );
1181
1182 let template_content = "# {{ title }}\n\n{{ body }}";
1184 root_files.insert(
1185 "template.md".to_string(),
1186 FileTreeNode::File {
1187 contents: template_content.as_bytes().to_vec(),
1188 },
1189 );
1190
1191 let root = FileTreeNode::Directory { files: root_files };
1192
1193 let quill = Quill::from_tree(root, None).unwrap();
1195
1196 assert_eq!(quill.example, Some(template_content.to_string()));
1198 }
1199
1200 #[test]
1201 fn test_from_json() {
1202 let json_str = r#"{
1204 "metadata": {
1205 "name": "test-from-json"
1206 },
1207 "files": {
1208 "Quill.toml": {
1209 "contents": "[Quill]\nname = \"test-from-json\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test quill from JSON\"\n"
1210 },
1211 "glue.typ": {
1212 "contents": "= Test Glue\n\nThis is test content."
1213 }
1214 }
1215 }"#;
1216
1217 let quill = Quill::from_json(json_str).unwrap();
1219
1220 assert_eq!(quill.name, "test-from-json");
1222 assert_eq!(quill.glue_file, "glue.typ");
1223 assert!(quill.glue_template.contains("Test Glue"));
1224 assert!(quill.metadata.contains_key("backend"));
1225 }
1226
1227 #[test]
1228 fn test_from_json_with_byte_array() {
1229 let json_str = r#"{
1231 "files": {
1232 "Quill.toml": {
1233 "contents": [91, 81, 117, 105, 108, 108, 93, 10, 110, 97, 109, 101, 32, 61, 32, 34, 116, 101, 115, 116, 34, 10, 98, 97, 99, 107, 101, 110, 100, 32, 61, 32, 34, 116, 121, 112, 115, 116, 34, 10, 103, 108, 117, 101, 32, 61, 32, 34, 103, 108, 117, 101, 46, 116, 121, 112, 34, 10, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 32, 61, 32, 34, 84, 101, 115, 116, 32, 113, 117, 105, 108, 108, 34, 10]
1234 },
1235 "glue.typ": {
1236 "contents": "test glue"
1237 }
1238 }
1239 }"#;
1240
1241 let quill = Quill::from_json(json_str).unwrap();
1243
1244 assert_eq!(quill.name, "test");
1246 assert_eq!(quill.glue_file, "glue.typ");
1247 }
1248
1249 #[test]
1250 fn test_from_json_missing_files() {
1251 let json_str = r#"{
1253 "metadata": {
1254 "name": "test"
1255 }
1256 }"#;
1257
1258 let result = Quill::from_json(json_str);
1259 assert!(result.is_err());
1260 assert!(result.unwrap_err().to_string().contains("files"));
1262 }
1263
1264 #[test]
1265 fn test_from_json_tree_structure() {
1266 let json_str = r#"{
1268 "files": {
1269 "Quill.toml": {
1270 "contents": "[Quill]\nname = \"test-tree-json\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test tree JSON\"\n"
1271 },
1272 "glue.typ": {
1273 "contents": "= Test Glue\n\nTree structure content."
1274 }
1275 }
1276 }"#;
1277
1278 let quill = Quill::from_json(json_str).unwrap();
1279
1280 assert_eq!(quill.name, "test-tree-json");
1281 assert!(quill.glue_template.contains("Tree structure content"));
1282 assert!(quill.metadata.contains_key("backend"));
1283 }
1284
1285 #[test]
1286 fn test_from_json_nested_tree_structure() {
1287 let json_str = r#"{
1289 "files": {
1290 "Quill.toml": {
1291 "contents": "[Quill]\nname = \"nested-test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Nested test\"\n"
1292 },
1293 "glue.typ": {
1294 "contents": "glue"
1295 },
1296 "src": {
1297 "main.rs": {
1298 "contents": "fn main() {}"
1299 },
1300 "lib.rs": {
1301 "contents": "// lib"
1302 }
1303 }
1304 }
1305 }"#;
1306
1307 let quill = Quill::from_json(json_str).unwrap();
1308
1309 assert_eq!(quill.name, "nested-test");
1310 assert!(quill.file_exists("src/main.rs"));
1312 assert!(quill.file_exists("src/lib.rs"));
1313
1314 let main_rs = quill.get_file("src/main.rs").unwrap();
1315 assert_eq!(main_rs, b"fn main() {}");
1316 }
1317
1318 #[test]
1319 fn test_from_tree_structure_direct() {
1320 let mut root_files = HashMap::new();
1322
1323 root_files.insert(
1324 "Quill.toml".to_string(),
1325 FileTreeNode::File {
1326 contents:
1327 b"[Quill]\nname = \"direct-tree\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Direct tree test\"\n"
1328 .to_vec(),
1329 },
1330 );
1331
1332 root_files.insert(
1333 "glue.typ".to_string(),
1334 FileTreeNode::File {
1335 contents: b"glue content".to_vec(),
1336 },
1337 );
1338
1339 let mut src_files = HashMap::new();
1341 src_files.insert(
1342 "main.rs".to_string(),
1343 FileTreeNode::File {
1344 contents: b"fn main() {}".to_vec(),
1345 },
1346 );
1347
1348 root_files.insert(
1349 "src".to_string(),
1350 FileTreeNode::Directory { files: src_files },
1351 );
1352
1353 let root = FileTreeNode::Directory { files: root_files };
1354
1355 let quill = Quill::from_tree(root, None).unwrap();
1356
1357 assert_eq!(quill.name, "direct-tree");
1358 assert!(quill.file_exists("src/main.rs"));
1359 assert!(quill.file_exists("glue.typ"));
1360 }
1361
1362 #[test]
1363 fn test_from_json_with_metadata_override() {
1364 let json_str = r#"{
1366 "metadata": {
1367 "name": "override-name"
1368 },
1369 "files": {
1370 "Quill.toml": {
1371 "contents": "[Quill]\nname = \"toml-name\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"TOML name test\"\n"
1372 },
1373 "glue.typ": {
1374 "contents": "= glue"
1375 }
1376 }
1377 }"#;
1378
1379 let quill = Quill::from_json(json_str).unwrap();
1380 assert_eq!(quill.name, "toml-name");
1383 }
1384
1385 #[test]
1386 fn test_from_json_empty_directory() {
1387 let json_str = r#"{
1389 "files": {
1390 "Quill.toml": {
1391 "contents": "[Quill]\nname = \"empty-dir-test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Empty directory test\"\n"
1392 },
1393 "glue.typ": {
1394 "contents": "glue"
1395 },
1396 "empty_dir": {}
1397 }
1398 }"#;
1399
1400 let quill = Quill::from_json(json_str).unwrap();
1401 assert_eq!(quill.name, "empty-dir-test");
1402 assert!(quill.dir_exists("empty_dir"));
1403 assert!(!quill.file_exists("empty_dir"));
1404 }
1405
1406 #[test]
1407 fn test_dir_exists_and_list_apis() {
1408 let mut root_files = HashMap::new();
1409
1410 root_files.insert(
1412 "Quill.toml".to_string(),
1413 FileTreeNode::File {
1414 contents: b"[Quill]\nname = \"test\"\nbackend = \"typst\"\nglue = \"glue.typ\"\ndescription = \"Test quill\"\n"
1415 .to_vec(),
1416 },
1417 );
1418
1419 root_files.insert(
1421 "glue.typ".to_string(),
1422 FileTreeNode::File {
1423 contents: b"glue content".to_vec(),
1424 },
1425 );
1426
1427 let mut assets_files = HashMap::new();
1429 assets_files.insert(
1430 "logo.png".to_string(),
1431 FileTreeNode::File {
1432 contents: vec![137, 80, 78, 71],
1433 },
1434 );
1435 assets_files.insert(
1436 "icon.svg".to_string(),
1437 FileTreeNode::File {
1438 contents: b"<svg></svg>".to_vec(),
1439 },
1440 );
1441
1442 let mut fonts_files = HashMap::new();
1444 fonts_files.insert(
1445 "font.ttf".to_string(),
1446 FileTreeNode::File {
1447 contents: b"font data".to_vec(),
1448 },
1449 );
1450 assets_files.insert(
1451 "fonts".to_string(),
1452 FileTreeNode::Directory { files: fonts_files },
1453 );
1454
1455 root_files.insert(
1456 "assets".to_string(),
1457 FileTreeNode::Directory {
1458 files: assets_files,
1459 },
1460 );
1461
1462 root_files.insert(
1464 "empty".to_string(),
1465 FileTreeNode::Directory {
1466 files: HashMap::new(),
1467 },
1468 );
1469
1470 let root = FileTreeNode::Directory { files: root_files };
1471 let quill = Quill::from_tree(root, None).unwrap();
1472
1473 assert!(quill.dir_exists("assets"));
1475 assert!(quill.dir_exists("assets/fonts"));
1476 assert!(quill.dir_exists("empty"));
1477 assert!(!quill.dir_exists("nonexistent"));
1478 assert!(!quill.dir_exists("glue.typ")); assert!(quill.file_exists("glue.typ"));
1482 assert!(quill.file_exists("assets/logo.png"));
1483 assert!(quill.file_exists("assets/fonts/font.ttf"));
1484 assert!(!quill.file_exists("assets")); let root_files_list = quill.list_files("");
1488 assert_eq!(root_files_list.len(), 2); assert!(root_files_list.contains(&"Quill.toml".to_string()));
1490 assert!(root_files_list.contains(&"glue.typ".to_string()));
1491
1492 let assets_files_list = quill.list_files("assets");
1493 assert_eq!(assets_files_list.len(), 2); assert!(assets_files_list.contains(&"logo.png".to_string()));
1495 assert!(assets_files_list.contains(&"icon.svg".to_string()));
1496
1497 let root_subdirs = quill.list_subdirectories("");
1499 assert_eq!(root_subdirs.len(), 2); assert!(root_subdirs.contains(&"assets".to_string()));
1501 assert!(root_subdirs.contains(&"empty".to_string()));
1502
1503 let assets_subdirs = quill.list_subdirectories("assets");
1504 assert_eq!(assets_subdirs.len(), 1); assert!(assets_subdirs.contains(&"fonts".to_string()));
1506
1507 let empty_subdirs = quill.list_subdirectories("empty");
1508 assert_eq!(empty_subdirs.len(), 0);
1509 }
1510
1511 #[test]
1512 fn test_field_schemas_parsing() {
1513 let mut root_files = HashMap::new();
1514
1515 let quill_toml = r#"[Quill]
1517name = "taro"
1518backend = "typst"
1519glue = "glue.typ"
1520example = "taro.md"
1521description = "Test template for field schemas"
1522
1523[fields]
1524author = {description = "Author of document", required = true}
1525ice_cream = {description = "favorite ice cream flavor"}
1526title = {description = "title of document", required = true}
1527"#;
1528 root_files.insert(
1529 "Quill.toml".to_string(),
1530 FileTreeNode::File {
1531 contents: quill_toml.as_bytes().to_vec(),
1532 },
1533 );
1534
1535 let glue_content = "= Test Template\n\nThis is a test.";
1537 root_files.insert(
1538 "glue.typ".to_string(),
1539 FileTreeNode::File {
1540 contents: glue_content.as_bytes().to_vec(),
1541 },
1542 );
1543
1544 root_files.insert(
1546 "taro.md".to_string(),
1547 FileTreeNode::File {
1548 contents: b"# Template".to_vec(),
1549 },
1550 );
1551
1552 let root = FileTreeNode::Directory { files: root_files };
1553
1554 let quill = Quill::from_tree(root, Some("taro".to_string())).unwrap();
1556
1557 assert_eq!(quill.field_schemas.len(), 3);
1559 assert!(quill.field_schemas.contains_key("author"));
1560 assert!(quill.field_schemas.contains_key("ice_cream"));
1561 assert!(quill.field_schemas.contains_key("title"));
1562
1563 let author_schema = quill.field_schemas.get("author").unwrap();
1565 assert_eq!(author_schema.description, "Author of document");
1566 assert_eq!(author_schema.required, true);
1567
1568 let ice_cream_schema = quill.field_schemas.get("ice_cream").unwrap();
1570 assert_eq!(ice_cream_schema.description, "favorite ice cream flavor");
1571 assert_eq!(ice_cream_schema.required, false);
1572
1573 let title_schema = quill.field_schemas.get("title").unwrap();
1575 assert_eq!(title_schema.description, "title of document");
1576 assert_eq!(title_schema.required, true);
1577 }
1578
1579 #[test]
1580 fn test_field_schema_struct() {
1581 let schema1 = FieldSchema::new("Test description".to_string());
1583 assert_eq!(schema1.description, "Test description");
1584 assert_eq!(schema1.required, false);
1585 assert_eq!(schema1.r#type, None);
1586 assert_eq!(schema1.example, None);
1587 assert_eq!(schema1.default, None);
1588
1589 let yaml_str = r#"
1591description: "Full field schema"
1592required: true
1593type: "string"
1594example: "Example value"
1595default: "Default value"
1596"#;
1597 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
1598 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
1599 let schema2 = FieldSchema::from_quill_value(&quill_value).unwrap();
1600 assert_eq!(schema2.description, "Full field schema");
1601 assert_eq!(schema2.required, true);
1602 assert_eq!(schema2.r#type, Some("string".to_string()));
1603 assert_eq!(
1604 schema2.example.as_ref().and_then(|v| v.as_str()),
1605 Some("Example value")
1606 );
1607 assert_eq!(
1608 schema2.default.as_ref().and_then(|v| v.as_str()),
1609 Some("Default value")
1610 );
1611
1612 let quill_value = schema2.to_quill_value();
1614 let obj = quill_value.as_object().unwrap();
1615 assert_eq!(
1616 obj.get("description").unwrap().as_str().unwrap(),
1617 "Full field schema"
1618 );
1619 assert_eq!(obj.get("required").unwrap().as_bool().unwrap(), true);
1620 assert_eq!(obj.get("type").unwrap().as_str().unwrap(), "string");
1621 }
1622}