1use std::collections::HashMap;
4use std::error::Error as StdError;
5use std::path::{Path, PathBuf};
6
7use crate::schema::build_schema_from_fields;
8use crate::value::QuillValue;
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct UiSchema {
13 pub group: Option<String>,
15 pub order: Option<i32>,
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct FieldSchema {
22 pub name: String,
23 pub title: Option<String>,
25 pub r#type: Option<String>,
27 pub description: String,
29 pub default: Option<QuillValue>,
31 pub examples: Option<QuillValue>,
33 pub ui: Option<UiSchema>,
35}
36
37impl FieldSchema {
38 pub fn new(name: String, description: String) -> Self {
40 Self {
41 name,
42 title: None,
43 r#type: None,
44 description,
45 default: None,
46 examples: None,
47 ui: None,
48 }
49 }
50
51 pub fn from_quill_value(key: String, value: &QuillValue) -> Result<Self, String> {
53 let obj = value
54 .as_object()
55 .ok_or_else(|| "Field schema must be an object".to_string())?;
56
57 for key in obj.keys() {
59 match key.as_str() {
60 "name" | "title" | "type" | "description" | "examples" | "default" | "ui" => {}
61 _ => {
62 eprintln!("Warning: Unknown key '{}' in field schema", key);
64 }
65 }
66 }
67
68 let name = key.clone();
69
70 let title = obj
71 .get("title")
72 .and_then(|v| v.as_str())
73 .map(|s| s.to_string());
74
75 let description = obj
76 .get("description")
77 .and_then(|v| v.as_str())
78 .unwrap_or("")
79 .to_string();
80
81 let field_type = obj
82 .get("type")
83 .and_then(|v| v.as_str())
84 .map(|s| s.to_string());
85
86 let default = obj.get("default").map(|v| QuillValue::from_json(v.clone()));
87
88 let examples = obj
89 .get("examples")
90 .map(|v| QuillValue::from_json(v.clone()));
91
92 let ui = if let Some(ui_value) = obj.get("ui") {
94 if let Some(ui_obj) = ui_value.as_object() {
95 let group = ui_obj
96 .get("group")
97 .and_then(|v| v.as_str())
98 .map(|s| s.to_string());
99
100 for key in ui_obj.keys() {
102 match key.as_str() {
103 "group" => {}
104 _ => {
105 eprintln!(
106 "Warning: Unknown UI property '{}'. Only 'group' is supported.",
107 key
108 );
109 }
110 }
111 }
112
113 Some(UiSchema {
114 group,
115 order: None, })
117 } else {
118 return Err("UI field must be an object".to_string());
119 }
120 } else {
121 None
122 };
123
124 Ok(Self {
125 name,
126 title,
127 r#type: field_type,
128 description,
129 default,
130 examples,
131 ui,
132 })
133 }
134}
135
136#[derive(Debug, Clone)]
138pub enum FileTreeNode {
139 File {
141 contents: Vec<u8>,
143 },
144 Directory {
146 files: HashMap<String, FileTreeNode>,
148 },
149}
150
151impl FileTreeNode {
152 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
154 let path = path.as_ref();
155
156 if path == Path::new("") {
158 return Some(self);
159 }
160
161 let components: Vec<_> = path
163 .components()
164 .filter_map(|c| {
165 if let std::path::Component::Normal(s) = c {
166 s.to_str()
167 } else {
168 None
169 }
170 })
171 .collect();
172
173 if components.is_empty() {
174 return Some(self);
175 }
176
177 let mut current_node = self;
179 for component in components {
180 match current_node {
181 FileTreeNode::Directory { files } => {
182 current_node = files.get(component)?;
183 }
184 FileTreeNode::File { .. } => {
185 return None; }
187 }
188 }
189
190 Some(current_node)
191 }
192
193 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
195 match self.get_node(path)? {
196 FileTreeNode::File { contents } => Some(contents.as_slice()),
197 FileTreeNode::Directory { .. } => None,
198 }
199 }
200
201 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
203 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
204 }
205
206 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
208 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
209 }
210
211 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
213 match self.get_node(dir_path) {
214 Some(FileTreeNode::Directory { files }) => files
215 .iter()
216 .filter_map(|(name, node)| {
217 if matches!(node, FileTreeNode::File { .. }) {
218 Some(name.clone())
219 } else {
220 None
221 }
222 })
223 .collect(),
224 _ => Vec::new(),
225 }
226 }
227
228 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
230 match self.get_node(dir_path) {
231 Some(FileTreeNode::Directory { files }) => files
232 .iter()
233 .filter_map(|(name, node)| {
234 if matches!(node, FileTreeNode::Directory { .. }) {
235 Some(name.clone())
236 } else {
237 None
238 }
239 })
240 .collect(),
241 _ => Vec::new(),
242 }
243 }
244
245 pub fn insert<P: AsRef<Path>>(
247 &mut self,
248 path: P,
249 node: FileTreeNode,
250 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
251 let path = path.as_ref();
252
253 let components: Vec<_> = path
255 .components()
256 .filter_map(|c| {
257 if let std::path::Component::Normal(s) = c {
258 s.to_str().map(|s| s.to_string())
259 } else {
260 None
261 }
262 })
263 .collect();
264
265 if components.is_empty() {
266 return Err("Cannot insert at root path".into());
267 }
268
269 let mut current_node = self;
271 for component in &components[..components.len() - 1] {
272 match current_node {
273 FileTreeNode::Directory { files } => {
274 current_node =
275 files
276 .entry(component.clone())
277 .or_insert_with(|| FileTreeNode::Directory {
278 files: HashMap::new(),
279 });
280 }
281 FileTreeNode::File { .. } => {
282 return Err("Cannot traverse into a file".into());
283 }
284 }
285 }
286
287 let filename = &components[components.len() - 1];
289 match current_node {
290 FileTreeNode::Directory { files } => {
291 files.insert(filename.clone(), node);
292 Ok(())
293 }
294 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
295 }
296 }
297
298 fn from_json_value(value: &serde_json::Value) -> Result<Self, Box<dyn StdError + Send + Sync>> {
300 if let Some(contents_str) = value.get("contents").and_then(|v| v.as_str()) {
301 Ok(FileTreeNode::File {
303 contents: contents_str.as_bytes().to_vec(),
304 })
305 } else if let Some(bytes_array) = value.get("contents").and_then(|v| v.as_array()) {
306 let contents: Vec<u8> = bytes_array
308 .iter()
309 .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
310 .collect();
311 Ok(FileTreeNode::File { contents })
312 } else if let Some(obj) = value.as_object() {
313 let mut files = HashMap::new();
315 for (name, child_value) in obj {
316 files.insert(name.clone(), Self::from_json_value(child_value)?);
317 }
318 Ok(FileTreeNode::Directory { files })
320 } else {
321 Err(format!("Invalid file tree node: {:?}", value).into())
322 }
323 }
324
325 pub fn print_tree(&self) -> String {
326 self.__print_tree("", "", true)
327 }
328
329 pub fn __print_tree(&self, name: &str, prefix: &str, is_last: bool) -> String {
330 let mut result = String::new();
331
332 let connector = if is_last { "└── " } else { "├── " };
334 let extension = if is_last { " " } else { "│ " };
335
336 match self {
337 FileTreeNode::File { .. } => {
338 result.push_str(&format!("{}{}{}\n", prefix, connector, name));
339 }
340 FileTreeNode::Directory { files } => {
341 result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
343
344 let child_prefix = format!("{}{}", prefix, extension);
345 let count = files.len();
346
347 for (i, (child_name, node)) in files.iter().enumerate() {
348 let is_last_child = i == count - 1;
349 result.push_str(&node.__print_tree(child_name, &child_prefix, is_last_child));
350 }
351 }
352 }
353
354 result
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct QuillIgnore {
361 patterns: Vec<String>,
362}
363
364impl QuillIgnore {
365 pub fn new(patterns: Vec<String>) -> Self {
367 Self { patterns }
368 }
369
370 pub fn from_content(content: &str) -> Self {
372 let patterns = content
373 .lines()
374 .map(|line| line.trim())
375 .filter(|line| !line.is_empty() && !line.starts_with('#'))
376 .map(|line| line.to_string())
377 .collect();
378 Self::new(patterns)
379 }
380
381 pub fn is_ignored<P: AsRef<Path>>(&self, path: P) -> bool {
383 let path = path.as_ref();
384 let path_str = path.to_string_lossy();
385
386 for pattern in &self.patterns {
387 if self.matches_pattern(pattern, &path_str) {
388 return true;
389 }
390 }
391 false
392 }
393
394 fn matches_pattern(&self, pattern: &str, path: &str) -> bool {
396 if pattern.ends_with('/') {
398 let pattern_prefix = &pattern[..pattern.len() - 1];
399 return path.starts_with(pattern_prefix)
400 && (path.len() == pattern_prefix.len()
401 || path.chars().nth(pattern_prefix.len()) == Some('/'));
402 }
403
404 if !pattern.contains('*') {
406 return path == pattern || path.ends_with(&format!("/{}", pattern));
407 }
408
409 if pattern == "*" {
411 return true;
412 }
413
414 let pattern_parts: Vec<&str> = pattern.split('*').collect();
416 if pattern_parts.len() == 2 {
417 let (prefix, suffix) = (pattern_parts[0], pattern_parts[1]);
418 if prefix.is_empty() {
419 return path.ends_with(suffix);
420 } else if suffix.is_empty() {
421 return path.starts_with(prefix);
422 } else {
423 return path.starts_with(prefix) && path.ends_with(suffix);
424 }
425 }
426
427 false
428 }
429}
430
431#[derive(Debug, Clone)]
433pub struct Quill {
434 pub metadata: HashMap<String, QuillValue>,
436 pub name: String,
438 pub backend: String,
440 pub plate: Option<String>,
442 pub example: Option<String>,
444 pub schema: QuillValue,
446 pub defaults: HashMap<String, QuillValue>,
448 pub examples: HashMap<String, Vec<QuillValue>>,
450 pub files: FileTreeNode,
452}
453
454#[derive(Debug, Clone)]
456pub struct QuillConfig {
457 pub name: String,
459 pub description: String,
461 pub backend: String,
463 pub version: Option<String>,
465 pub author: Option<String>,
467 pub example_file: Option<String>,
469 pub plate_file: Option<String>,
471 pub fields: HashMap<String, FieldSchema>,
473 pub metadata: HashMap<String, QuillValue>,
475 pub typst_config: HashMap<String, QuillValue>,
477}
478
479impl QuillConfig {
480 pub fn from_toml(toml_content: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
482 let quill_toml: toml::Value = toml::from_str(toml_content)
483 .map_err(|e| format!("Failed to parse Quill.toml: {}", e))?;
484
485 let field_order: Vec<String> = toml_content
487 .parse::<toml_edit::DocumentMut>()
488 .ok()
489 .and_then(|doc| {
490 doc.get("fields")
491 .and_then(|item| item.as_table())
492 .map(|table| table.iter().map(|(k, _)| k.to_string()).collect())
493 })
494 .unwrap_or_default();
495
496 let quill_section = quill_toml
498 .get("Quill")
499 .ok_or("Missing required [Quill] section in Quill.toml")?;
500
501 let name = quill_section
503 .get("name")
504 .and_then(|v| v.as_str())
505 .ok_or("Missing required 'name' field in [Quill] section")?
506 .to_string();
507
508 let backend = quill_section
509 .get("backend")
510 .and_then(|v| v.as_str())
511 .ok_or("Missing required 'backend' field in [Quill] section")?
512 .to_string();
513
514 let description = quill_section
515 .get("description")
516 .and_then(|v| v.as_str())
517 .ok_or("Missing required 'description' field in [Quill] section")?;
518
519 if description.trim().is_empty() {
520 return Err("'description' field in [Quill] section cannot be empty".into());
521 }
522 let description = description.to_string();
523
524 let version = quill_section
526 .get("version")
527 .and_then(|v| v.as_str())
528 .map(|s| s.to_string());
529
530 let author = quill_section
531 .get("author")
532 .and_then(|v| v.as_str())
533 .map(|s| s.to_string());
534
535 let example_file = quill_section
536 .get("example_file")
537 .and_then(|v| v.as_str())
538 .map(|s| s.to_string());
539
540 let plate_file = quill_section
541 .get("plate_file")
542 .and_then(|v| v.as_str())
543 .map(|s| s.to_string());
544
545 let mut metadata = HashMap::new();
547 if let toml::Value::Table(table) = quill_section {
548 for (key, value) in table {
549 if key != "name"
551 && key != "backend"
552 && key != "description"
553 && key != "version"
554 && key != "author"
555 && key != "example_file"
556 && key != "plate_file"
557 {
558 match QuillValue::from_toml(value) {
559 Ok(quill_value) => {
560 metadata.insert(key.clone(), quill_value);
561 }
562 Err(e) => {
563 eprintln!("Warning: Failed to convert field '{}': {}", key, e);
564 }
565 }
566 }
567 }
568 }
569
570 let mut typst_config = HashMap::new();
572 if let Some(typst_section) = quill_toml.get("typst") {
573 if let toml::Value::Table(table) = typst_section {
574 for (key, value) in table {
575 match QuillValue::from_toml(value) {
576 Ok(quill_value) => {
577 typst_config.insert(key.clone(), quill_value);
578 }
579 Err(e) => {
580 eprintln!("Warning: Failed to convert typst field '{}': {}", key, e);
581 }
582 }
583 }
584 }
585 }
586
587 let mut fields = HashMap::new();
589 if let Some(fields_section) = quill_toml.get("fields") {
590 if let toml::Value::Table(fields_table) = fields_section {
591 let mut order_counter = 0;
592 for (field_name, field_schema) in fields_table {
593 let order = if let Some(idx) = field_order.iter().position(|k| k == field_name)
595 {
596 idx as i32
597 } else {
598 let o = field_order.len() as i32 + order_counter;
599 order_counter += 1;
600 o
601 };
602
603 match QuillValue::from_toml(field_schema) {
604 Ok(quill_value) => {
605 match FieldSchema::from_quill_value(field_name.clone(), &quill_value) {
606 Ok(mut schema) => {
607 if schema.ui.is_none() {
609 schema.ui = Some(UiSchema {
610 group: None,
611 order: Some(order),
612 });
613 } else if let Some(ui) = &mut schema.ui {
614 ui.order = Some(order);
615 }
616
617 fields.insert(field_name.clone(), schema);
618 }
619 Err(e) => {
620 eprintln!(
621 "Warning: Failed to parse field schema '{}': {}",
622 field_name, e
623 );
624 }
625 }
626 }
627 Err(e) => {
628 eprintln!(
629 "Warning: Failed to convert field schema '{}': {}",
630 field_name, e
631 );
632 }
633 }
634 }
635 }
636 }
637
638 Ok(QuillConfig {
639 name,
640 description,
641 backend,
642 version,
643 author,
644 example_file,
645 plate_file,
646 fields,
647 metadata,
648 typst_config,
649 })
650 }
651}
652
653impl Quill {
654 pub fn from_path<P: AsRef<std::path::Path>>(
656 path: P,
657 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
658 use std::fs;
659
660 let path = path.as_ref();
661 let name = path
662 .file_name()
663 .and_then(|n| n.to_str())
664 .unwrap_or("unnamed")
665 .to_string();
666
667 let quillignore_path = path.join(".quillignore");
669 let ignore = if quillignore_path.exists() {
670 let ignore_content = fs::read_to_string(&quillignore_path)
671 .map_err(|e| format!("Failed to read .quillignore: {}", e))?;
672 QuillIgnore::from_content(&ignore_content)
673 } else {
674 QuillIgnore::new(vec![
676 ".git/".to_string(),
677 ".gitignore".to_string(),
678 ".quillignore".to_string(),
679 "target/".to_string(),
680 "node_modules/".to_string(),
681 ])
682 };
683
684 let root = Self::load_directory_as_tree(path, path, &ignore)?;
686
687 Self::from_tree(root, Some(name))
689 }
690
691 pub fn from_tree(
708 root: FileTreeNode,
709 _default_name: Option<String>,
710 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
711 let quill_toml_bytes = root
713 .get_file("Quill.toml")
714 .ok_or("Quill.toml not found in file tree")?;
715
716 let quill_toml_content = String::from_utf8(quill_toml_bytes.to_vec())
717 .map_err(|e| format!("Quill.toml is not valid UTF-8: {}", e))?;
718
719 let config = QuillConfig::from_toml(&quill_toml_content)?;
721
722 Self::from_config(config, root)
724 }
725
726 fn from_config(
743 config: QuillConfig,
744 root: FileTreeNode,
745 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
746 let mut metadata = config.metadata.clone();
748
749 metadata.insert(
751 "backend".to_string(),
752 QuillValue::from_json(serde_json::Value::String(config.backend.clone())),
753 );
754
755 metadata.insert(
757 "description".to_string(),
758 QuillValue::from_json(serde_json::Value::String(config.description.clone())),
759 );
760
761 if let Some(ref author) = config.author {
763 metadata.insert(
764 "author".to_string(),
765 QuillValue::from_json(serde_json::Value::String(author.clone())),
766 );
767 }
768
769 for (key, value) in &config.typst_config {
771 metadata.insert(format!("typst_{}", key), value.clone());
772 }
773
774 let schema = build_schema_from_fields(&config.fields)
776 .map_err(|e| format!("Failed to build JSON schema from field schemas: {}", e))?;
777
778 let plate_content: Option<String> = if let Some(ref plate_file_name) = config.plate_file {
780 let plate_bytes = root.get_file(plate_file_name).ok_or_else(|| {
781 format!("Plate file '{}' not found in file tree", plate_file_name)
782 })?;
783
784 let content = String::from_utf8(plate_bytes.to_vec()).map_err(|e| {
785 format!("Plate file '{}' is not valid UTF-8: {}", plate_file_name, e)
786 })?;
787 Some(content)
788 } else {
789 None
791 };
792
793 let example_content = if let Some(ref example_file_name) = config.example_file {
795 root.get_file(example_file_name).and_then(|bytes| {
796 String::from_utf8(bytes.to_vec())
797 .map_err(|e| {
798 eprintln!(
799 "Warning: Example file '{}' is not valid UTF-8: {}",
800 example_file_name, e
801 );
802 e
803 })
804 .ok()
805 })
806 } else {
807 None
808 };
809
810 let defaults = crate::schema::extract_defaults_from_schema(&schema);
812 let examples = crate::schema::extract_examples_from_schema(&schema);
813
814 let quill = Quill {
815 metadata,
816 name: config.name,
817 backend: config.backend,
818 plate: plate_content,
819 example: example_content,
820 schema,
821 defaults,
822 examples: examples,
823 files: root,
824 };
825
826 Ok(quill)
827 }
828
829 pub fn from_json(json_str: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
836 use serde_json::Value as JsonValue;
837
838 let json: JsonValue =
839 serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {}", e))?;
840
841 let obj = json.as_object().ok_or_else(|| "Root must be an object")?;
842
843 let default_name = obj
845 .get("metadata")
846 .and_then(|m| m.get("name"))
847 .and_then(|v| v.as_str())
848 .map(String::from);
849
850 let files_obj = obj
852 .get("files")
853 .and_then(|v| v.as_object())
854 .ok_or_else(|| "Missing or invalid 'files' key")?;
855
856 let mut root_files = HashMap::new();
858 for (key, value) in files_obj {
859 root_files.insert(key.clone(), FileTreeNode::from_json_value(value)?);
860 }
861
862 let root = FileTreeNode::Directory { files: root_files };
863
864 Self::from_tree(root, default_name)
866 }
867
868 fn load_directory_as_tree(
870 current_dir: &Path,
871 base_dir: &Path,
872 ignore: &QuillIgnore,
873 ) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
874 use std::fs;
875
876 if !current_dir.exists() {
877 return Ok(FileTreeNode::Directory {
878 files: HashMap::new(),
879 });
880 }
881
882 let mut files = HashMap::new();
883
884 for entry in fs::read_dir(current_dir)? {
885 let entry = entry?;
886 let path = entry.path();
887 let relative_path = path
888 .strip_prefix(base_dir)
889 .map_err(|e| format!("Failed to get relative path: {}", e))?
890 .to_path_buf();
891
892 if ignore.is_ignored(&relative_path) {
894 continue;
895 }
896
897 let filename = path
899 .file_name()
900 .and_then(|n| n.to_str())
901 .ok_or_else(|| format!("Invalid filename: {}", path.display()))?
902 .to_string();
903
904 if path.is_file() {
905 let contents = fs::read(&path)
906 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
907
908 files.insert(filename, FileTreeNode::File { contents });
909 } else if path.is_dir() {
910 let subdir_tree = Self::load_directory_as_tree(&path, base_dir, ignore)?;
912 files.insert(filename, subdir_tree);
913 }
914 }
915
916 Ok(FileTreeNode::Directory { files })
917 }
918
919 pub fn typst_packages(&self) -> Vec<String> {
921 self.metadata
922 .get("typst_packages")
923 .and_then(|v| v.as_array())
924 .map(|arr| {
925 arr.iter()
926 .filter_map(|v| v.as_str().map(|s| s.to_string()))
927 .collect()
928 })
929 .unwrap_or_default()
930 }
931
932 pub fn extract_defaults(&self) -> &HashMap<String, QuillValue> {
940 &self.defaults
941 }
942
943 pub fn extract_examples(&self) -> &HashMap<String, Vec<QuillValue>> {
948 &self.examples
949 }
950
951 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
953 self.files.get_file(path)
954 }
955
956 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
958 self.files.file_exists(path)
959 }
960
961 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
963 self.files.dir_exists(path)
964 }
965
966 pub fn list_files<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
968 self.files.list_files(path)
969 }
970
971 pub fn list_subdirectories<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
973 self.files.list_subdirectories(path)
974 }
975
976 pub fn list_directory<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
978 let dir_path = dir_path.as_ref();
979 let filenames = self.files.list_files(dir_path);
980
981 filenames
983 .iter()
984 .map(|name| {
985 if dir_path == Path::new("") {
986 PathBuf::from(name)
987 } else {
988 dir_path.join(name)
989 }
990 })
991 .collect()
992 }
993
994 pub fn list_directories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
996 let dir_path = dir_path.as_ref();
997 let subdirs = self.files.list_subdirectories(dir_path);
998
999 subdirs
1001 .iter()
1002 .map(|name| {
1003 if dir_path == Path::new("") {
1004 PathBuf::from(name)
1005 } else {
1006 dir_path.join(name)
1007 }
1008 })
1009 .collect()
1010 }
1011
1012 pub fn find_files<P: AsRef<Path>>(&self, pattern: P) -> Vec<PathBuf> {
1014 let pattern_str = pattern.as_ref().to_string_lossy();
1015 let mut matches = Vec::new();
1016
1017 let glob_pattern = match glob::Pattern::new(&pattern_str) {
1019 Ok(pat) => pat,
1020 Err(_) => return matches, };
1022
1023 self.find_files_recursive(&self.files, Path::new(""), &glob_pattern, &mut matches);
1025
1026 matches.sort();
1027 matches
1028 }
1029
1030 fn find_files_recursive(
1032 &self,
1033 node: &FileTreeNode,
1034 current_path: &Path,
1035 pattern: &glob::Pattern,
1036 matches: &mut Vec<PathBuf>,
1037 ) {
1038 match node {
1039 FileTreeNode::File { .. } => {
1040 let path_str = current_path.to_string_lossy();
1041 if pattern.matches(&path_str) {
1042 matches.push(current_path.to_path_buf());
1043 }
1044 }
1045 FileTreeNode::Directory { files } => {
1046 for (name, child_node) in files {
1047 let child_path = if current_path == Path::new("") {
1048 PathBuf::from(name)
1049 } else {
1050 current_path.join(name)
1051 };
1052 self.find_files_recursive(child_node, &child_path, pattern, matches);
1053 }
1054 }
1055 }
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use std::fs;
1063 use tempfile::TempDir;
1064
1065 #[test]
1066 fn test_quillignore_parsing() {
1067 let ignore_content = r#"
1068# This is a comment
1069*.tmp
1070target/
1071node_modules/
1072.git/
1073"#;
1074 let ignore = QuillIgnore::from_content(ignore_content);
1075 assert_eq!(ignore.patterns.len(), 4);
1076 assert!(ignore.patterns.contains(&"*.tmp".to_string()));
1077 assert!(ignore.patterns.contains(&"target/".to_string()));
1078 }
1079
1080 #[test]
1081 fn test_quillignore_matching() {
1082 let ignore = QuillIgnore::new(vec![
1083 "*.tmp".to_string(),
1084 "target/".to_string(),
1085 "node_modules/".to_string(),
1086 ".git/".to_string(),
1087 ]);
1088
1089 assert!(ignore.is_ignored("test.tmp"));
1091 assert!(ignore.is_ignored("path/to/file.tmp"));
1092 assert!(!ignore.is_ignored("test.txt"));
1093
1094 assert!(ignore.is_ignored("target"));
1096 assert!(ignore.is_ignored("target/debug"));
1097 assert!(ignore.is_ignored("target/debug/deps"));
1098 assert!(!ignore.is_ignored("src/target.rs"));
1099
1100 assert!(ignore.is_ignored("node_modules"));
1101 assert!(ignore.is_ignored("node_modules/package"));
1102 assert!(!ignore.is_ignored("my_node_modules"));
1103 }
1104
1105 #[test]
1106 fn test_in_memory_file_system() {
1107 let temp_dir = TempDir::new().unwrap();
1108 let quill_dir = temp_dir.path();
1109
1110 fs::write(
1112 quill_dir.join("Quill.toml"),
1113 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1114 )
1115 .unwrap();
1116 fs::write(quill_dir.join("plate.typ"), "test plate").unwrap();
1117
1118 let assets_dir = quill_dir.join("assets");
1119 fs::create_dir_all(&assets_dir).unwrap();
1120 fs::write(assets_dir.join("test.txt"), "asset content").unwrap();
1121
1122 let packages_dir = quill_dir.join("packages");
1123 fs::create_dir_all(&packages_dir).unwrap();
1124 fs::write(packages_dir.join("package.typ"), "package content").unwrap();
1125
1126 let quill = Quill::from_path(quill_dir).unwrap();
1128
1129 assert!(quill.file_exists("plate.typ"));
1131 assert!(quill.file_exists("assets/test.txt"));
1132 assert!(quill.file_exists("packages/package.typ"));
1133 assert!(!quill.file_exists("nonexistent.txt"));
1134
1135 let asset_content = quill.get_file("assets/test.txt").unwrap();
1137 assert_eq!(asset_content, b"asset content");
1138
1139 let asset_files = quill.list_directory("assets");
1141 assert_eq!(asset_files.len(), 1);
1142 assert!(asset_files.contains(&PathBuf::from("assets/test.txt")));
1143 }
1144
1145 #[test]
1146 fn test_quillignore_integration() {
1147 let temp_dir = TempDir::new().unwrap();
1148 let quill_dir = temp_dir.path();
1149
1150 fs::write(quill_dir.join(".quillignore"), "*.tmp\ntarget/\n").unwrap();
1152
1153 fs::write(
1155 quill_dir.join("Quill.toml"),
1156 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1157 )
1158 .unwrap();
1159 fs::write(quill_dir.join("plate.typ"), "test template").unwrap();
1160 fs::write(quill_dir.join("should_ignore.tmp"), "ignored").unwrap();
1161
1162 let target_dir = quill_dir.join("target");
1163 fs::create_dir_all(&target_dir).unwrap();
1164 fs::write(target_dir.join("debug.txt"), "also ignored").unwrap();
1165
1166 let quill = Quill::from_path(quill_dir).unwrap();
1168
1169 assert!(quill.file_exists("plate.typ"));
1171 assert!(!quill.file_exists("should_ignore.tmp"));
1172 assert!(!quill.file_exists("target/debug.txt"));
1173 }
1174
1175 #[test]
1176 fn test_find_files_pattern() {
1177 let temp_dir = TempDir::new().unwrap();
1178 let quill_dir = temp_dir.path();
1179
1180 fs::write(
1182 quill_dir.join("Quill.toml"),
1183 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1184 )
1185 .unwrap();
1186 fs::write(quill_dir.join("plate.typ"), "template").unwrap();
1187
1188 let assets_dir = quill_dir.join("assets");
1189 fs::create_dir_all(&assets_dir).unwrap();
1190 fs::write(assets_dir.join("image.png"), "png data").unwrap();
1191 fs::write(assets_dir.join("data.json"), "json data").unwrap();
1192
1193 let fonts_dir = assets_dir.join("fonts");
1194 fs::create_dir_all(&fonts_dir).unwrap();
1195 fs::write(fonts_dir.join("font.ttf"), "font data").unwrap();
1196
1197 let quill = Quill::from_path(quill_dir).unwrap();
1199
1200 let all_assets = quill.find_files("assets/*");
1202 assert!(all_assets.len() >= 3); let typ_files = quill.find_files("*.typ");
1205 assert_eq!(typ_files.len(), 1);
1206 assert!(typ_files.contains(&PathBuf::from("plate.typ")));
1207 }
1208
1209 #[test]
1210 fn test_new_standardized_toml_format() {
1211 let temp_dir = TempDir::new().unwrap();
1212 let quill_dir = temp_dir.path();
1213
1214 let toml_content = r#"[Quill]
1216name = "my-custom-quill"
1217backend = "typst"
1218plate_file = "custom_plate.typ"
1219description = "Test quill with new format"
1220author = "Test Author"
1221"#;
1222 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1223 fs::write(
1224 quill_dir.join("custom_plate.typ"),
1225 "= Custom Template\n\nThis is a custom template.",
1226 )
1227 .unwrap();
1228
1229 let quill = Quill::from_path(quill_dir).unwrap();
1231
1232 assert_eq!(quill.name, "my-custom-quill");
1234
1235 assert!(quill.metadata.contains_key("backend"));
1237 if let Some(backend_val) = quill.metadata.get("backend") {
1238 if let Some(backend_str) = backend_val.as_str() {
1239 assert_eq!(backend_str, "typst");
1240 } else {
1241 panic!("Backend value is not a string");
1242 }
1243 }
1244
1245 assert!(quill.metadata.contains_key("description"));
1247 assert!(quill.metadata.contains_key("author"));
1248 assert!(!quill.metadata.contains_key("version")); assert!(quill.plate.unwrap().contains("Custom Template"));
1252 }
1253
1254 #[test]
1255 fn test_typst_packages_parsing() {
1256 let temp_dir = TempDir::new().unwrap();
1257 let quill_dir = temp_dir.path();
1258
1259 let toml_content = r#"
1260[Quill]
1261name = "test-quill"
1262backend = "typst"
1263plate_file = "plate.typ"
1264description = "Test quill for packages"
1265
1266[typst]
1267packages = ["@preview/bubble:0.2.2", "@preview/example:1.0.0"]
1268"#;
1269
1270 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1271 fs::write(quill_dir.join("plate.typ"), "test").unwrap();
1272
1273 let quill = Quill::from_path(quill_dir).unwrap();
1274 let packages = quill.typst_packages();
1275
1276 assert_eq!(packages.len(), 2);
1277 assert_eq!(packages[0], "@preview/bubble:0.2.2");
1278 assert_eq!(packages[1], "@preview/example:1.0.0");
1279 }
1280
1281 #[test]
1282 fn test_template_loading() {
1283 let temp_dir = TempDir::new().unwrap();
1284 let quill_dir = temp_dir.path();
1285
1286 let toml_content = r#"[Quill]
1288name = "test-with-template"
1289backend = "typst"
1290plate_file = "plate.typ"
1291example_file = "example.md"
1292description = "Test quill with template"
1293"#;
1294 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1295 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1296 fs::write(
1297 quill_dir.join("example.md"),
1298 "---\ntitle: Test\n---\n\nThis is a test template.",
1299 )
1300 .unwrap();
1301
1302 let quill = Quill::from_path(quill_dir).unwrap();
1304
1305 assert!(quill.example.is_some());
1307 let example = quill.example.unwrap();
1308 assert!(example.contains("title: Test"));
1309 assert!(example.contains("This is a test template"));
1310
1311 assert_eq!(quill.plate.unwrap(), "plate content");
1313 }
1314
1315 #[test]
1316 fn test_template_optional() {
1317 let temp_dir = TempDir::new().unwrap();
1318 let quill_dir = temp_dir.path();
1319
1320 let toml_content = r#"[Quill]
1322name = "test-without-template"
1323backend = "typst"
1324plate_file = "plate.typ"
1325description = "Test quill without template"
1326"#;
1327 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1328 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1329
1330 let quill = Quill::from_path(quill_dir).unwrap();
1332
1333 assert_eq!(quill.example, None);
1335
1336 assert_eq!(quill.plate.unwrap(), "plate content");
1338 }
1339
1340 #[test]
1341 fn test_from_tree() {
1342 let mut root_files = HashMap::new();
1344
1345 let quill_toml = r#"[Quill]
1347name = "test-from-tree"
1348backend = "typst"
1349plate_file = "plate.typ"
1350description = "A test quill from tree"
1351"#;
1352 root_files.insert(
1353 "Quill.toml".to_string(),
1354 FileTreeNode::File {
1355 contents: quill_toml.as_bytes().to_vec(),
1356 },
1357 );
1358
1359 let plate_content = "= Test Template\n\nThis is a test.";
1361 root_files.insert(
1362 "plate.typ".to_string(),
1363 FileTreeNode::File {
1364 contents: plate_content.as_bytes().to_vec(),
1365 },
1366 );
1367
1368 let root = FileTreeNode::Directory { files: root_files };
1369
1370 let quill = Quill::from_tree(root, Some("test-from-tree".to_string())).unwrap();
1372
1373 assert_eq!(quill.name, "test-from-tree");
1375 assert_eq!(quill.plate.unwrap(), plate_content);
1376 assert!(quill.metadata.contains_key("backend"));
1377 assert!(quill.metadata.contains_key("description"));
1378 }
1379
1380 #[test]
1381 fn test_from_tree_with_template() {
1382 let mut root_files = HashMap::new();
1383
1384 let quill_toml = r#"[Quill]
1386name = "test-tree-template"
1387backend = "typst"
1388plate_file = "plate.typ"
1389example_file = "template.md"
1390description = "Test tree with template"
1391"#;
1392 root_files.insert(
1393 "Quill.toml".to_string(),
1394 FileTreeNode::File {
1395 contents: quill_toml.as_bytes().to_vec(),
1396 },
1397 );
1398
1399 root_files.insert(
1401 "plate.typ".to_string(),
1402 FileTreeNode::File {
1403 contents: b"plate content".to_vec(),
1404 },
1405 );
1406
1407 let template_content = "# {{ title }}\n\n{{ body }}";
1409 root_files.insert(
1410 "template.md".to_string(),
1411 FileTreeNode::File {
1412 contents: template_content.as_bytes().to_vec(),
1413 },
1414 );
1415
1416 let root = FileTreeNode::Directory { files: root_files };
1417
1418 let quill = Quill::from_tree(root, None).unwrap();
1420
1421 assert_eq!(quill.example, Some(template_content.to_string()));
1423 }
1424
1425 #[test]
1426 fn test_from_json() {
1427 let json_str = r#"{
1429 "metadata": {
1430 "name": "test-from-json"
1431 },
1432 "files": {
1433 "Quill.toml": {
1434 "contents": "[Quill]\nname = \"test-from-json\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill from JSON\"\n"
1435 },
1436 "plate.typ": {
1437 "contents": "= Test Plate\n\nThis is test content."
1438 }
1439 }
1440 }"#;
1441
1442 let quill = Quill::from_json(json_str).unwrap();
1444
1445 assert_eq!(quill.name, "test-from-json");
1447 assert!(quill.plate.unwrap().contains("Test Plate"));
1448 assert!(quill.metadata.contains_key("backend"));
1449 }
1450
1451 #[test]
1452 fn test_from_json_with_byte_array() {
1453 let json_str = r#"{
1455 "files": {
1456 "Quill.toml": {
1457 "contents": "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"\n"
1458 },
1459 "plate.typ": {
1460 "contents": "test plate"
1461 }
1462 }
1463 }"#;
1464
1465 let quill = Quill::from_json(json_str).unwrap();
1467
1468 assert_eq!(quill.name, "test");
1470 assert_eq!(quill.plate.unwrap(), "test plate");
1471 }
1472
1473 #[test]
1474 fn test_from_json_missing_files() {
1475 let json_str = r#"{
1477 "metadata": {
1478 "name": "test"
1479 }
1480 }"#;
1481
1482 let result = Quill::from_json(json_str);
1483 assert!(result.is_err());
1484 assert!(result.unwrap_err().to_string().contains("files"));
1486 }
1487
1488 #[test]
1489 fn test_from_json_tree_structure() {
1490 let json_str = r#"{
1492 "files": {
1493 "Quill.toml": {
1494 "contents": "[Quill]\nname = \"test-tree-json\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test tree JSON\"\n"
1495 },
1496 "plate.typ": {
1497 "contents": "= Test Plate\n\nTree structure content."
1498 }
1499 }
1500 }"#;
1501
1502 let quill = Quill::from_json(json_str).unwrap();
1503
1504 assert_eq!(quill.name, "test-tree-json");
1505 assert!(quill.plate.unwrap().contains("Tree structure content"));
1506 assert!(quill.metadata.contains_key("backend"));
1507 }
1508
1509 #[test]
1510 fn test_from_json_nested_tree_structure() {
1511 let json_str = r#"{
1513 "files": {
1514 "Quill.toml": {
1515 "contents": "[Quill]\nname = \"nested-test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Nested test\"\n"
1516 },
1517 "plate.typ": {
1518 "contents": "plate"
1519 },
1520 "src": {
1521 "main.rs": {
1522 "contents": "fn main() {}"
1523 },
1524 "lib.rs": {
1525 "contents": "// lib"
1526 }
1527 }
1528 }
1529 }"#;
1530
1531 let quill = Quill::from_json(json_str).unwrap();
1532
1533 assert_eq!(quill.name, "nested-test");
1534 assert!(quill.file_exists("src/main.rs"));
1536 assert!(quill.file_exists("src/lib.rs"));
1537
1538 let main_rs = quill.get_file("src/main.rs").unwrap();
1539 assert_eq!(main_rs, b"fn main() {}");
1540 }
1541
1542 #[test]
1543 fn test_from_tree_structure_direct() {
1544 let mut root_files = HashMap::new();
1546
1547 root_files.insert(
1548 "Quill.toml".to_string(),
1549 FileTreeNode::File {
1550 contents:
1551 b"[Quill]\nname = \"direct-tree\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Direct tree test\"\n"
1552 .to_vec(),
1553 },
1554 );
1555
1556 root_files.insert(
1557 "plate.typ".to_string(),
1558 FileTreeNode::File {
1559 contents: b"plate content".to_vec(),
1560 },
1561 );
1562
1563 let mut src_files = HashMap::new();
1565 src_files.insert(
1566 "main.rs".to_string(),
1567 FileTreeNode::File {
1568 contents: b"fn main() {}".to_vec(),
1569 },
1570 );
1571
1572 root_files.insert(
1573 "src".to_string(),
1574 FileTreeNode::Directory { files: src_files },
1575 );
1576
1577 let root = FileTreeNode::Directory { files: root_files };
1578
1579 let quill = Quill::from_tree(root, None).unwrap();
1580
1581 assert_eq!(quill.name, "direct-tree");
1582 assert!(quill.file_exists("src/main.rs"));
1583 assert!(quill.file_exists("plate.typ"));
1584 }
1585
1586 #[test]
1587 fn test_from_json_with_metadata_override() {
1588 let json_str = r#"{
1590 "metadata": {
1591 "name": "override-name"
1592 },
1593 "files": {
1594 "Quill.toml": {
1595 "contents": "[Quill]\nname = \"toml-name\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"TOML name test\"\n"
1596 },
1597 "plate.typ": {
1598 "contents": "= plate"
1599 }
1600 }
1601 }"#;
1602
1603 let quill = Quill::from_json(json_str).unwrap();
1604 assert_eq!(quill.name, "toml-name");
1607 }
1608
1609 #[test]
1610 fn test_from_json_empty_directory() {
1611 let json_str = r#"{
1613 "files": {
1614 "Quill.toml": {
1615 "contents": "[Quill]\nname = \"empty-dir-test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Empty directory test\"\n"
1616 },
1617 "plate.typ": {
1618 "contents": "plate"
1619 },
1620 "empty_dir": {}
1621 }
1622 }"#;
1623
1624 let quill = Quill::from_json(json_str).unwrap();
1625 assert_eq!(quill.name, "empty-dir-test");
1626 assert!(quill.dir_exists("empty_dir"));
1627 assert!(!quill.file_exists("empty_dir"));
1628 }
1629
1630 #[test]
1631 fn test_dir_exists_and_list_apis() {
1632 let mut root_files = HashMap::new();
1633
1634 root_files.insert(
1636 "Quill.toml".to_string(),
1637 FileTreeNode::File {
1638 contents: b"[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"\n"
1639 .to_vec(),
1640 },
1641 );
1642
1643 root_files.insert(
1645 "plate.typ".to_string(),
1646 FileTreeNode::File {
1647 contents: b"plate content".to_vec(),
1648 },
1649 );
1650
1651 let mut assets_files = HashMap::new();
1653 assets_files.insert(
1654 "logo.png".to_string(),
1655 FileTreeNode::File {
1656 contents: vec![137, 80, 78, 71],
1657 },
1658 );
1659 assets_files.insert(
1660 "icon.svg".to_string(),
1661 FileTreeNode::File {
1662 contents: b"<svg></svg>".to_vec(),
1663 },
1664 );
1665
1666 let mut fonts_files = HashMap::new();
1668 fonts_files.insert(
1669 "font.ttf".to_string(),
1670 FileTreeNode::File {
1671 contents: b"font data".to_vec(),
1672 },
1673 );
1674 assets_files.insert(
1675 "fonts".to_string(),
1676 FileTreeNode::Directory { files: fonts_files },
1677 );
1678
1679 root_files.insert(
1680 "assets".to_string(),
1681 FileTreeNode::Directory {
1682 files: assets_files,
1683 },
1684 );
1685
1686 root_files.insert(
1688 "empty".to_string(),
1689 FileTreeNode::Directory {
1690 files: HashMap::new(),
1691 },
1692 );
1693
1694 let root = FileTreeNode::Directory { files: root_files };
1695 let quill = Quill::from_tree(root, None).unwrap();
1696
1697 assert!(quill.dir_exists("assets"));
1699 assert!(quill.dir_exists("assets/fonts"));
1700 assert!(quill.dir_exists("empty"));
1701 assert!(!quill.dir_exists("nonexistent"));
1702 assert!(!quill.dir_exists("plate.typ")); assert!(quill.file_exists("plate.typ"));
1706 assert!(quill.file_exists("assets/logo.png"));
1707 assert!(quill.file_exists("assets/fonts/font.ttf"));
1708 assert!(!quill.file_exists("assets")); let root_files_list = quill.list_files("");
1712 assert_eq!(root_files_list.len(), 2); assert!(root_files_list.contains(&"Quill.toml".to_string()));
1714 assert!(root_files_list.contains(&"plate.typ".to_string()));
1715
1716 let assets_files_list = quill.list_files("assets");
1717 assert_eq!(assets_files_list.len(), 2); assert!(assets_files_list.contains(&"logo.png".to_string()));
1719 assert!(assets_files_list.contains(&"icon.svg".to_string()));
1720
1721 let root_subdirs = quill.list_subdirectories("");
1723 assert_eq!(root_subdirs.len(), 2); assert!(root_subdirs.contains(&"assets".to_string()));
1725 assert!(root_subdirs.contains(&"empty".to_string()));
1726
1727 let assets_subdirs = quill.list_subdirectories("assets");
1728 assert_eq!(assets_subdirs.len(), 1); assert!(assets_subdirs.contains(&"fonts".to_string()));
1730
1731 let empty_subdirs = quill.list_subdirectories("empty");
1732 assert_eq!(empty_subdirs.len(), 0);
1733 }
1734
1735 #[test]
1736 fn test_field_schemas_parsing() {
1737 let mut root_files = HashMap::new();
1738
1739 let quill_toml = r#"[Quill]
1741name = "taro"
1742backend = "typst"
1743plate_file = "plate.typ"
1744example_file = "taro.md"
1745description = "Test template for field schemas"
1746
1747[fields]
1748author = {description = "Author of document" }
1749ice_cream = {description = "favorite ice cream flavor"}
1750title = {description = "title of document" }
1751"#;
1752 root_files.insert(
1753 "Quill.toml".to_string(),
1754 FileTreeNode::File {
1755 contents: quill_toml.as_bytes().to_vec(),
1756 },
1757 );
1758
1759 let plate_content = "= Test Template\n\nThis is a test.";
1761 root_files.insert(
1762 "plate.typ".to_string(),
1763 FileTreeNode::File {
1764 contents: plate_content.as_bytes().to_vec(),
1765 },
1766 );
1767
1768 root_files.insert(
1770 "taro.md".to_string(),
1771 FileTreeNode::File {
1772 contents: b"# Template".to_vec(),
1773 },
1774 );
1775
1776 let root = FileTreeNode::Directory { files: root_files };
1777
1778 let quill = Quill::from_tree(root, Some("taro".to_string())).unwrap();
1780
1781 assert_eq!(quill.schema["properties"].as_object().unwrap().len(), 3);
1783 assert!(quill.schema["properties"]
1784 .as_object()
1785 .unwrap()
1786 .contains_key("author"));
1787 assert!(quill.schema["properties"]
1788 .as_object()
1789 .unwrap()
1790 .contains_key("ice_cream"));
1791 assert!(quill.schema["properties"]
1792 .as_object()
1793 .unwrap()
1794 .contains_key("title"));
1795
1796 let author_schema = quill.schema["properties"]["author"].as_object().unwrap();
1798 assert_eq!(author_schema["description"], "Author of document");
1799
1800 let ice_cream_schema = quill.schema["properties"]["ice_cream"].as_object().unwrap();
1802 assert_eq!(ice_cream_schema["description"], "favorite ice cream flavor");
1803
1804 let title_schema = quill.schema["properties"]["title"].as_object().unwrap();
1806 assert_eq!(title_schema["description"], "title of document");
1807 }
1808
1809 #[test]
1810 fn test_field_schema_struct() {
1811 let schema1 = FieldSchema::new("test_name".to_string(), "Test description".to_string());
1813 assert_eq!(schema1.description, "Test description");
1814 assert_eq!(schema1.r#type, None);
1815 assert_eq!(schema1.examples, None);
1816 assert_eq!(schema1.default, None);
1817
1818 let yaml_str = r#"
1820description: "Full field schema"
1821type: "string"
1822examples:
1823 - "Example value"
1824default: "Default value"
1825"#;
1826 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
1827 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
1828 let schema2 = FieldSchema::from_quill_value("test_name".to_string(), &quill_value).unwrap();
1829 assert_eq!(schema2.name, "test_name");
1830 assert_eq!(schema2.description, "Full field schema");
1831 assert_eq!(schema2.r#type, Some("string".to_string()));
1832 assert_eq!(
1833 schema2
1834 .examples
1835 .as_ref()
1836 .and_then(|v| v.as_array())
1837 .and_then(|arr| arr.first())
1838 .and_then(|v| v.as_str()),
1839 Some("Example value")
1840 );
1841 assert_eq!(
1842 schema2.default.as_ref().and_then(|v| v.as_str()),
1843 Some("Default value")
1844 );
1845 }
1846
1847 #[test]
1848 fn test_quill_without_plate_file() {
1849 let mut root_files = HashMap::new();
1851
1852 let quill_toml = r#"[Quill]
1854name = "test-no-plate"
1855backend = "typst"
1856description = "Test quill without plate file"
1857"#;
1858 root_files.insert(
1859 "Quill.toml".to_string(),
1860 FileTreeNode::File {
1861 contents: quill_toml.as_bytes().to_vec(),
1862 },
1863 );
1864
1865 let root = FileTreeNode::Directory { files: root_files };
1866
1867 let quill = Quill::from_tree(root, None).unwrap();
1869
1870 assert!(quill.plate.clone().is_none());
1872 assert_eq!(quill.name, "test-no-plate");
1873 }
1874
1875 #[test]
1876 fn test_quill_config_from_toml() {
1877 let toml_content = r#"[Quill]
1879name = "test-config"
1880backend = "typst"
1881description = "Test configuration parsing"
1882version = "1.0.0"
1883author = "Test Author"
1884plate_file = "plate.typ"
1885example_file = "example.md"
1886
1887[typst]
1888packages = ["@preview/bubble:0.2.2"]
1889
1890[fields]
1891title = {description = "Document title", type = "string"}
1892author = {description = "Document author"}
1893"#;
1894
1895 let config = QuillConfig::from_toml(toml_content).unwrap();
1896
1897 assert_eq!(config.name, "test-config");
1899 assert_eq!(config.backend, "typst");
1900 assert_eq!(config.description, "Test configuration parsing");
1901
1902 assert_eq!(config.version, Some("1.0.0".to_string()));
1904 assert_eq!(config.author, Some("Test Author".to_string()));
1905 assert_eq!(config.plate_file, Some("plate.typ".to_string()));
1906 assert_eq!(config.example_file, Some("example.md".to_string()));
1907
1908 assert!(config.typst_config.contains_key("packages"));
1910
1911 assert_eq!(config.fields.len(), 2);
1913 assert!(config.fields.contains_key("title"));
1914 assert!(config.fields.contains_key("author"));
1915
1916 let title_field = &config.fields["title"];
1917 assert_eq!(title_field.description, "Document title");
1918 assert_eq!(title_field.r#type, Some("string".to_string()));
1919 }
1920
1921 #[test]
1922 fn test_quill_config_missing_required_fields() {
1923 let toml_missing_name = r#"[Quill]
1925backend = "typst"
1926description = "Missing name"
1927"#;
1928 let result = QuillConfig::from_toml(toml_missing_name);
1929 assert!(result.is_err());
1930 assert!(result
1931 .unwrap_err()
1932 .to_string()
1933 .contains("Missing required 'name'"));
1934
1935 let toml_missing_backend = r#"[Quill]
1936name = "test"
1937description = "Missing backend"
1938"#;
1939 let result = QuillConfig::from_toml(toml_missing_backend);
1940 assert!(result.is_err());
1941 assert!(result
1942 .unwrap_err()
1943 .to_string()
1944 .contains("Missing required 'backend'"));
1945
1946 let toml_missing_description = r#"[Quill]
1947name = "test"
1948backend = "typst"
1949"#;
1950 let result = QuillConfig::from_toml(toml_missing_description);
1951 assert!(result.is_err());
1952 assert!(result
1953 .unwrap_err()
1954 .to_string()
1955 .contains("Missing required 'description'"));
1956 }
1957
1958 #[test]
1959 fn test_quill_config_empty_description() {
1960 let toml_empty_description = r#"[Quill]
1962name = "test"
1963backend = "typst"
1964description = " "
1965"#;
1966 let result = QuillConfig::from_toml(toml_empty_description);
1967 assert!(result.is_err());
1968 assert!(result
1969 .unwrap_err()
1970 .to_string()
1971 .contains("description' field in [Quill] section cannot be empty"));
1972 }
1973
1974 #[test]
1975 fn test_quill_config_missing_quill_section() {
1976 let toml_no_section = r#"[fields]
1978title = {description = "Title"}
1979"#;
1980 let result = QuillConfig::from_toml(toml_no_section);
1981 assert!(result.is_err());
1982 assert!(result
1983 .unwrap_err()
1984 .to_string()
1985 .contains("Missing required [Quill] section"));
1986 }
1987
1988 #[test]
1989 fn test_quill_from_config_metadata() {
1990 let mut root_files = HashMap::new();
1992
1993 let quill_toml = r#"[Quill]
1994name = "metadata-test"
1995backend = "typst"
1996description = "Test metadata flow"
1997author = "Test Author"
1998custom_field = "custom_value"
1999
2000[typst]
2001packages = ["@preview/bubble:0.2.2"]
2002"#;
2003 root_files.insert(
2004 "Quill.toml".to_string(),
2005 FileTreeNode::File {
2006 contents: quill_toml.as_bytes().to_vec(),
2007 },
2008 );
2009
2010 let root = FileTreeNode::Directory { files: root_files };
2011 let quill = Quill::from_tree(root, None).unwrap();
2012
2013 assert!(quill.metadata.contains_key("backend"));
2015 assert!(quill.metadata.contains_key("description"));
2016 assert!(quill.metadata.contains_key("author"));
2017
2018 assert!(quill.metadata.contains_key("custom_field"));
2020 assert_eq!(
2021 quill.metadata.get("custom_field").unwrap().as_str(),
2022 Some("custom_value")
2023 );
2024
2025 assert!(quill.metadata.contains_key("typst_packages"));
2027 }
2028
2029 #[test]
2030 fn test_extract_defaults_method() {
2031 let mut root_files = HashMap::new();
2033
2034 let quill_toml = r#"[Quill]
2035name = "defaults-test"
2036backend = "typst"
2037description = "Test defaults extraction"
2038
2039[fields]
2040title = {description = "Title"}
2041author = {description = "Author", default = "Anonymous"}
2042status = {description = "Status", default = "draft"}
2043"#;
2044
2045 root_files.insert(
2046 "Quill.toml".to_string(),
2047 FileTreeNode::File {
2048 contents: quill_toml.as_bytes().to_vec(),
2049 },
2050 );
2051
2052 let root = FileTreeNode::Directory { files: root_files };
2053 let quill = Quill::from_tree(root, None).unwrap();
2054
2055 let defaults = quill.extract_defaults();
2057
2058 assert_eq!(defaults.len(), 2);
2060 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
2062 assert!(defaults.contains_key("status"));
2063
2064 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
2066 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
2067 }
2068
2069 #[test]
2070 fn test_field_order_preservation() {
2071 let toml_content = r#"[Quill]
2072name = "order-test"
2073backend = "typst"
2074description = "Test field order"
2075
2076[fields]
2077first = {description = "First field"}
2078second = {description = "Second field"}
2079third = {description = "Third field", ui = {group = "Test Group"}}
2080fourth = {description = "Fourth field"}
2081"#;
2082
2083 let config = QuillConfig::from_toml(toml_content).unwrap();
2084
2085 let first = config.fields.get("first").unwrap();
2089 assert_eq!(first.ui.as_ref().unwrap().order, Some(0));
2090
2091 let second = config.fields.get("second").unwrap();
2092 assert_eq!(second.ui.as_ref().unwrap().order, Some(1));
2093
2094 let third = config.fields.get("third").unwrap();
2095 assert_eq!(third.ui.as_ref().unwrap().order, Some(2));
2096 assert_eq!(
2097 third.ui.as_ref().unwrap().group,
2098 Some("Test Group".to_string())
2099 );
2100
2101 let fourth = config.fields.get("fourth").unwrap();
2102 assert_eq!(fourth.ui.as_ref().unwrap().order, Some(3));
2103 }
2104
2105 #[test]
2106 fn test_quill_with_all_ui_properties() {
2107 let toml_content = r#"[Quill]
2108name = "full-ui-test"
2109backend = "typst"
2110description = "Test all UI properties"
2111
2112[fields.author]
2113description = "The full name of the document author"
2114type = "str"
2115
2116[fields.author.ui]
2117group = "Author Info"
2118"#;
2119
2120 let config = QuillConfig::from_toml(toml_content).unwrap();
2121
2122 let author_field = &config.fields["author"];
2123 let ui = author_field.ui.as_ref().unwrap();
2124 assert_eq!(ui.group, Some("Author Info".to_string()));
2125 assert_eq!(ui.order, Some(0)); }
2127 #[test]
2128 fn test_field_schema_with_title_and_description() {
2129 let yaml = r#"
2131title: "Field Title"
2132description: "Detailed field description"
2133type: "string"
2134examples:
2135 - "Example value"
2136ui:
2137 group: "Test Group"
2138"#;
2139 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
2140 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
2141 let schema = FieldSchema::from_quill_value("test_field".to_string(), &quill_value).unwrap();
2142
2143 assert_eq!(schema.title, Some("Field Title".to_string()));
2144 assert_eq!(schema.description, "Detailed field description");
2145
2146 assert_eq!(
2147 schema
2148 .examples
2149 .as_ref()
2150 .and_then(|v| v.as_array())
2151 .and_then(|arr| arr.first())
2152 .and_then(|v| v.as_str()),
2153 Some("Example value")
2154 );
2155
2156 let ui = schema.ui.as_ref().unwrap();
2157 assert_eq!(ui.group, Some("Test Group".to_string()));
2158 }
2159}