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 UiSchema {
12 pub group: Option<String>,
14 pub order: Option<i32>,
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct CardSchema {
21 pub name: String,
23 pub title: Option<String>,
25 pub description: String,
27 pub ui: Option<UiSchema>,
29 pub fields: HashMap<String, FieldSchema>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct FieldSchema {
36 pub name: String,
37 pub title: Option<String>,
39 pub r#type: Option<String>,
41 pub description: String,
43 pub default: Option<QuillValue>,
45 pub examples: Option<QuillValue>,
47 pub ui: Option<UiSchema>,
49 pub required: bool,
51}
52
53impl FieldSchema {
54 pub fn new(name: String, description: String) -> Self {
56 Self {
57 name,
58 title: None,
59 r#type: None,
60 description,
61 default: None,
62 examples: None,
63 ui: None,
64 required: false,
65 }
66 }
67
68 pub fn from_quill_value(key: String, value: &QuillValue) -> Result<Self, String> {
70 let obj = value
71 .as_object()
72 .ok_or_else(|| "Field schema must be an object".to_string())?;
73
74 for key in obj.keys() {
76 match key.as_str() {
77 "name" | "title" | "type" | "description" | "examples" | "default" | "ui"
78 | "required" => {}
79 _ => {
80 eprintln!("Warning: Unknown key '{}' in field schema", key);
82 }
83 }
84 }
85
86 let name = key.clone();
87
88 let title = obj
89 .get("title")
90 .and_then(|v| v.as_str())
91 .map(|s| s.to_string());
92
93 let description = obj
94 .get("description")
95 .and_then(|v| v.as_str())
96 .unwrap_or("")
97 .to_string();
98
99 let field_type = obj
100 .get("type")
101 .and_then(|v| v.as_str())
102 .map(|s| s.to_string());
103
104 let default = obj.get("default").map(|v| QuillValue::from_json(v.clone()));
105
106 let examples = obj
107 .get("examples")
108 .map(|v| QuillValue::from_json(v.clone()));
109
110 let required = obj
112 .get("required")
113 .and_then(|v| v.as_bool())
114 .unwrap_or(false);
115
116 let ui = if let Some(ui_value) = obj.get("ui") {
118 if let Some(ui_obj) = ui_value.as_object() {
119 let group = ui_obj
120 .get("group")
121 .and_then(|v| v.as_str())
122 .map(|s| s.to_string());
123
124 for key in ui_obj.keys() {
126 match key.as_str() {
127 "group" => {}
128 _ => {
129 eprintln!(
130 "Warning: Unknown UI property '{}'. Only 'group' is supported.",
131 key
132 );
133 }
134 }
135 }
136
137 Some(UiSchema {
138 group,
139 order: None, })
141 } else {
142 return Err("UI field must be an object".to_string());
143 }
144 } else {
145 None
146 };
147
148 Ok(Self {
149 name,
150 title,
151 r#type: field_type,
152 description,
153 default,
154 examples,
155 ui,
156 required,
157 })
158 }
159}
160
161#[derive(Debug, Clone)]
163pub enum FileTreeNode {
164 File {
166 contents: Vec<u8>,
168 },
169 Directory {
171 files: HashMap<String, FileTreeNode>,
173 },
174}
175
176impl FileTreeNode {
177 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
179 let path = path.as_ref();
180
181 if path == Path::new("") {
183 return Some(self);
184 }
185
186 let components: Vec<_> = path
188 .components()
189 .filter_map(|c| {
190 if let std::path::Component::Normal(s) = c {
191 s.to_str()
192 } else {
193 None
194 }
195 })
196 .collect();
197
198 if components.is_empty() {
199 return Some(self);
200 }
201
202 let mut current_node = self;
204 for component in components {
205 match current_node {
206 FileTreeNode::Directory { files } => {
207 current_node = files.get(component)?;
208 }
209 FileTreeNode::File { .. } => {
210 return None; }
212 }
213 }
214
215 Some(current_node)
216 }
217
218 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
220 match self.get_node(path)? {
221 FileTreeNode::File { contents } => Some(contents.as_slice()),
222 FileTreeNode::Directory { .. } => None,
223 }
224 }
225
226 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
228 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
229 }
230
231 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
233 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
234 }
235
236 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
238 match self.get_node(dir_path) {
239 Some(FileTreeNode::Directory { files }) => files
240 .iter()
241 .filter_map(|(name, node)| {
242 if matches!(node, FileTreeNode::File { .. }) {
243 Some(name.clone())
244 } else {
245 None
246 }
247 })
248 .collect(),
249 _ => Vec::new(),
250 }
251 }
252
253 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
255 match self.get_node(dir_path) {
256 Some(FileTreeNode::Directory { files }) => files
257 .iter()
258 .filter_map(|(name, node)| {
259 if matches!(node, FileTreeNode::Directory { .. }) {
260 Some(name.clone())
261 } else {
262 None
263 }
264 })
265 .collect(),
266 _ => Vec::new(),
267 }
268 }
269
270 pub fn insert<P: AsRef<Path>>(
272 &mut self,
273 path: P,
274 node: FileTreeNode,
275 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
276 let path = path.as_ref();
277
278 let components: Vec<_> = path
280 .components()
281 .filter_map(|c| {
282 if let std::path::Component::Normal(s) = c {
283 s.to_str().map(|s| s.to_string())
284 } else {
285 None
286 }
287 })
288 .collect();
289
290 if components.is_empty() {
291 return Err("Cannot insert at root path".into());
292 }
293
294 let mut current_node = self;
296 for component in &components[..components.len() - 1] {
297 match current_node {
298 FileTreeNode::Directory { files } => {
299 current_node =
300 files
301 .entry(component.clone())
302 .or_insert_with(|| FileTreeNode::Directory {
303 files: HashMap::new(),
304 });
305 }
306 FileTreeNode::File { .. } => {
307 return Err("Cannot traverse into a file".into());
308 }
309 }
310 }
311
312 let filename = &components[components.len() - 1];
314 match current_node {
315 FileTreeNode::Directory { files } => {
316 files.insert(filename.clone(), node);
317 Ok(())
318 }
319 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
320 }
321 }
322
323 fn from_json_value(value: &serde_json::Value) -> Result<Self, Box<dyn StdError + Send + Sync>> {
325 if let Some(contents_str) = value.get("contents").and_then(|v| v.as_str()) {
326 Ok(FileTreeNode::File {
328 contents: contents_str.as_bytes().to_vec(),
329 })
330 } else if let Some(bytes_array) = value.get("contents").and_then(|v| v.as_array()) {
331 let contents: Vec<u8> = bytes_array
333 .iter()
334 .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
335 .collect();
336 Ok(FileTreeNode::File { contents })
337 } else if let Some(obj) = value.as_object() {
338 let mut files = HashMap::new();
340 for (name, child_value) in obj {
341 files.insert(name.clone(), Self::from_json_value(child_value)?);
342 }
343 Ok(FileTreeNode::Directory { files })
345 } else {
346 Err(format!("Invalid file tree node: {:?}", value).into())
347 }
348 }
349
350 pub fn print_tree(&self) -> String {
351 self.__print_tree("", "", true)
352 }
353
354 pub fn __print_tree(&self, name: &str, prefix: &str, is_last: bool) -> String {
355 let mut result = String::new();
356
357 let connector = if is_last { "└── " } else { "├── " };
359 let extension = if is_last { " " } else { "│ " };
360
361 match self {
362 FileTreeNode::File { .. } => {
363 result.push_str(&format!("{}{}{}\n", prefix, connector, name));
364 }
365 FileTreeNode::Directory { files } => {
366 result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
368
369 let child_prefix = format!("{}{}", prefix, extension);
370 let count = files.len();
371
372 for (i, (child_name, node)) in files.iter().enumerate() {
373 let is_last_child = i == count - 1;
374 result.push_str(&node.__print_tree(child_name, &child_prefix, is_last_child));
375 }
376 }
377 }
378
379 result
380 }
381}
382
383#[derive(Debug, Clone)]
385pub struct QuillIgnore {
386 patterns: Vec<String>,
387}
388
389impl QuillIgnore {
390 pub fn new(patterns: Vec<String>) -> Self {
392 Self { patterns }
393 }
394
395 pub fn from_content(content: &str) -> Self {
397 let patterns = content
398 .lines()
399 .map(|line| line.trim())
400 .filter(|line| !line.is_empty() && !line.starts_with('#'))
401 .map(|line| line.to_string())
402 .collect();
403 Self::new(patterns)
404 }
405
406 pub fn is_ignored<P: AsRef<Path>>(&self, path: P) -> bool {
408 let path = path.as_ref();
409 let path_str = path.to_string_lossy();
410
411 for pattern in &self.patterns {
412 if self.matches_pattern(pattern, &path_str) {
413 return true;
414 }
415 }
416 false
417 }
418
419 fn matches_pattern(&self, pattern: &str, path: &str) -> bool {
421 if let Some(pattern_prefix) = pattern.strip_suffix('/') {
423 return path.starts_with(pattern_prefix)
424 && (path.len() == pattern_prefix.len()
425 || path.chars().nth(pattern_prefix.len()) == Some('/'));
426 }
427
428 if !pattern.contains('*') {
430 return path == pattern || path.ends_with(&format!("/{}", pattern));
431 }
432
433 if pattern == "*" {
435 return true;
436 }
437
438 let pattern_parts: Vec<&str> = pattern.split('*').collect();
440 if pattern_parts.len() == 2 {
441 let (prefix, suffix) = (pattern_parts[0], pattern_parts[1]);
442 if prefix.is_empty() {
443 return path.ends_with(suffix);
444 } else if suffix.is_empty() {
445 return path.starts_with(prefix);
446 } else {
447 return path.starts_with(prefix) && path.ends_with(suffix);
448 }
449 }
450
451 false
452 }
453}
454
455#[derive(Debug, Clone)]
457pub struct Quill {
458 pub metadata: HashMap<String, QuillValue>,
460 pub name: String,
462 pub backend: String,
464 pub plate: Option<String>,
466 pub example: Option<String>,
468 pub schema: QuillValue,
470 pub defaults: HashMap<String, QuillValue>,
472 pub examples: HashMap<String, Vec<QuillValue>>,
474 pub files: FileTreeNode,
476}
477
478#[derive(Debug, Clone)]
480pub struct QuillConfig {
481 pub name: String,
483 pub description: String,
485 pub backend: String,
487 pub version: Option<String>,
489 pub author: Option<String>,
491 pub example_file: Option<String>,
493 pub plate_file: Option<String>,
495 pub fields: HashMap<String, FieldSchema>,
497 pub cards: HashMap<String, CardSchema>,
499 pub metadata: HashMap<String, QuillValue>,
501 pub typst_config: HashMap<String, QuillValue>,
503}
504
505impl QuillConfig {
506 pub fn from_toml(toml_content: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
508 let quill_toml: toml::Value = toml::from_str(toml_content)
509 .map_err(|e| format!("Failed to parse Quill.toml: {}", e))?;
510
511 let (field_order, card_order): (Vec<String>, Vec<String>) = toml_content
513 .parse::<toml_edit::DocumentMut>()
514 .ok()
515 .map(|doc| {
516 let f_order = doc
517 .get("fields")
518 .and_then(|item| item.as_table())
519 .map(|table| table.iter().map(|(k, _)| k.to_string()).collect())
520 .unwrap_or_default();
521
522 let s_order = doc
523 .get("cards")
524 .and_then(|item| item.as_table())
525 .map(|table| table.iter().map(|(k, _)| k.to_string()).collect())
526 .unwrap_or_default();
527
528 (f_order, s_order)
529 })
530 .unwrap_or_default();
531
532 let quill_section = quill_toml
534 .get("Quill")
535 .ok_or("Missing required [Quill] section in Quill.toml")?;
536
537 let name = quill_section
539 .get("name")
540 .and_then(|v| v.as_str())
541 .ok_or("Missing required 'name' field in [Quill] section")?
542 .to_string();
543
544 let backend = quill_section
545 .get("backend")
546 .and_then(|v| v.as_str())
547 .ok_or("Missing required 'backend' field in [Quill] section")?
548 .to_string();
549
550 let description = quill_section
551 .get("description")
552 .and_then(|v| v.as_str())
553 .ok_or("Missing required 'description' field in [Quill] section")?;
554
555 if description.trim().is_empty() {
556 return Err("'description' field in [Quill] section cannot be empty".into());
557 }
558 let description = description.to_string();
559
560 let version = quill_section
562 .get("version")
563 .and_then(|v| v.as_str())
564 .map(|s| s.to_string());
565
566 let author = quill_section
567 .get("author")
568 .and_then(|v| v.as_str())
569 .map(|s| s.to_string());
570
571 let example_file = quill_section
572 .get("example_file")
573 .and_then(|v| v.as_str())
574 .map(|s| s.to_string());
575
576 let plate_file = quill_section
577 .get("plate_file")
578 .and_then(|v| v.as_str())
579 .map(|s| s.to_string());
580
581 let mut metadata = HashMap::new();
583 if let toml::Value::Table(table) = quill_section {
584 for (key, value) in table {
585 if key != "name"
587 && key != "backend"
588 && key != "description"
589 && key != "version"
590 && key != "author"
591 && key != "example_file"
592 && key != "plate_file"
593 {
594 match QuillValue::from_toml(value) {
595 Ok(quill_value) => {
596 metadata.insert(key.clone(), quill_value);
597 }
598 Err(e) => {
599 eprintln!("Warning: Failed to convert field '{}': {}", key, e);
600 }
601 }
602 }
603 }
604 }
605
606 let mut typst_config = HashMap::new();
608 if let Some(typst_section) = quill_toml.get("typst") {
609 if let toml::Value::Table(table) = typst_section {
610 for (key, value) in table {
611 match QuillValue::from_toml(value) {
612 Ok(quill_value) => {
613 typst_config.insert(key.clone(), quill_value);
614 }
615 Err(e) => {
616 eprintln!("Warning: Failed to convert typst field '{}': {}", key, e);
617 }
618 }
619 }
620 }
621 }
622
623 let mut fields = HashMap::new();
625 if let Some(fields_section) = quill_toml.get("fields") {
626 if let toml::Value::Table(fields_table) = fields_section {
627 let mut order_counter = 0;
628 for (field_name, field_schema) in fields_table {
629 let order = if let Some(idx) = field_order.iter().position(|k| k == field_name)
631 {
632 idx as i32
633 } else {
634 let o = field_order.len() as i32 + order_counter;
635 order_counter += 1;
636 o
637 };
638
639 match QuillValue::from_toml(field_schema) {
640 Ok(quill_value) => {
641 match FieldSchema::from_quill_value(field_name.clone(), &quill_value) {
642 Ok(mut schema) => {
643 if schema.ui.is_none() {
645 schema.ui = Some(UiSchema {
646 group: None,
647 order: Some(order),
648 });
649 } else if let Some(ui) = &mut schema.ui {
650 ui.order = Some(order);
651 }
652
653 fields.insert(field_name.clone(), schema);
654 }
655 Err(e) => {
656 eprintln!(
657 "Warning: Failed to parse field schema '{}': {}",
658 field_name, e
659 );
660 }
661 }
662 }
663 Err(e) => {
664 eprintln!(
665 "Warning: Failed to convert field schema '{}': {}",
666 field_name, e
667 );
668 }
669 }
670 }
671 }
672 }
673
674 let mut cards: HashMap<String, CardSchema> = HashMap::new();
676 if let Some(cards_section) = quill_toml.get("cards") {
677 if let toml::Value::Table(cards_table) = cards_section {
678 let current_field_count = fields.len() as i32;
679 let mut order_counter = 0;
680
681 for (card_name, card_value) in cards_table {
682 if fields.contains_key(card_name) {
684 return Err(format!(
685 "Card definition '{}' conflicts with an existing field name",
686 card_name
687 )
688 .into());
689 }
690
691 let order = if let Some(idx) = card_order.iter().position(|k| k == card_name) {
693 current_field_count + idx as i32
694 } else {
695 let o = current_field_count + card_order.len() as i32 + order_counter;
696 order_counter += 1;
697 o
698 };
699
700 let card_table = card_value.as_table().ok_or_else(|| {
702 format!("Card definition '{}' must be a table", card_name)
703 })?;
704
705 let title = card_table
706 .get("title")
707 .and_then(|v| v.as_str())
708 .map(|s| s.to_string());
709
710 let description = card_table
711 .get("description")
712 .and_then(|v| v.as_str())
713 .unwrap_or("")
714 .to_string();
715
716 let ui = if let Some(ui_value) = card_table.get("ui") {
718 if let Some(ui_table) = ui_value.as_table() {
719 let group = ui_table
720 .get("group")
721 .and_then(|v| v.as_str())
722 .map(|s| s.to_string());
723 Some(UiSchema {
724 group,
725 order: Some(order),
726 })
727 } else {
728 None
729 }
730 } else {
731 Some(UiSchema {
732 group: None,
733 order: Some(order),
734 })
735 };
736
737 let mut card_fields: HashMap<String, FieldSchema> = HashMap::new();
739 if let Some(fields_value) = card_table.get("fields") {
740 if let Some(fields_table) = fields_value.as_table() {
741 for (field_name, field_value) in fields_table {
742 match QuillValue::from_toml(field_value) {
743 Ok(quill_value) => {
744 match FieldSchema::from_quill_value(
745 field_name.clone(),
746 &quill_value,
747 ) {
748 Ok(field_schema) => {
749 card_fields
750 .insert(field_name.clone(), field_schema);
751 }
752 Err(e) => {
753 eprintln!(
754 "Warning: Failed to parse card field '{}.{}': {}",
755 card_name, field_name, e
756 );
757 }
758 }
759 }
760 Err(e) => {
761 eprintln!(
762 "Warning: Failed to convert card field '{}.{}': {}",
763 card_name, field_name, e
764 );
765 }
766 }
767 }
768 }
769 }
770
771 let card_schema = CardSchema {
772 name: card_name.clone(),
773 title,
774 description,
775 ui,
776 fields: card_fields,
777 };
778
779 cards.insert(card_name.clone(), card_schema);
780 }
781 }
782 }
783
784 Ok(QuillConfig {
785 name,
786 description,
787 backend,
788 version,
789 author,
790 example_file,
791 plate_file,
792 fields,
793 cards,
794 metadata,
795 typst_config,
796 })
797 }
798}
799
800impl Quill {
801 pub fn from_path<P: AsRef<std::path::Path>>(
803 path: P,
804 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
805 use std::fs;
806
807 let path = path.as_ref();
808 let name = path
809 .file_name()
810 .and_then(|n| n.to_str())
811 .unwrap_or("unnamed")
812 .to_string();
813
814 let quillignore_path = path.join(".quillignore");
816 let ignore = if quillignore_path.exists() {
817 let ignore_content = fs::read_to_string(&quillignore_path)
818 .map_err(|e| format!("Failed to read .quillignore: {}", e))?;
819 QuillIgnore::from_content(&ignore_content)
820 } else {
821 QuillIgnore::new(vec![
823 ".git/".to_string(),
824 ".gitignore".to_string(),
825 ".quillignore".to_string(),
826 "target/".to_string(),
827 "node_modules/".to_string(),
828 ])
829 };
830
831 let root = Self::load_directory_as_tree(path, path, &ignore)?;
833
834 Self::from_tree(root, Some(name))
836 }
837
838 pub fn from_tree(
855 root: FileTreeNode,
856 _default_name: Option<String>,
857 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
858 let quill_toml_bytes = root
860 .get_file("Quill.toml")
861 .ok_or("Quill.toml not found in file tree")?;
862
863 let quill_toml_content = String::from_utf8(quill_toml_bytes.to_vec())
864 .map_err(|e| format!("Quill.toml is not valid UTF-8: {}", e))?;
865
866 let config = QuillConfig::from_toml(&quill_toml_content)?;
868
869 Self::from_config(config, root)
871 }
872
873 fn from_config(
890 config: QuillConfig,
891 root: FileTreeNode,
892 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
893 let mut metadata = config.metadata.clone();
895
896 metadata.insert(
898 "backend".to_string(),
899 QuillValue::from_json(serde_json::Value::String(config.backend.clone())),
900 );
901
902 metadata.insert(
904 "description".to_string(),
905 QuillValue::from_json(serde_json::Value::String(config.description.clone())),
906 );
907
908 if let Some(ref author) = config.author {
910 metadata.insert(
911 "author".to_string(),
912 QuillValue::from_json(serde_json::Value::String(author.clone())),
913 );
914 }
915
916 for (key, value) in &config.typst_config {
918 metadata.insert(format!("typst_{}", key), value.clone());
919 }
920
921 let schema = crate::schema::build_schema(&config.fields, &config.cards)
923 .map_err(|e| format!("Failed to build JSON schema from field schemas: {}", e))?;
924
925 let plate_content: Option<String> = if let Some(ref plate_file_name) = config.plate_file {
927 let plate_bytes = root.get_file(plate_file_name).ok_or_else(|| {
928 format!("Plate file '{}' not found in file tree", plate_file_name)
929 })?;
930
931 let content = String::from_utf8(plate_bytes.to_vec()).map_err(|e| {
932 format!("Plate file '{}' is not valid UTF-8: {}", plate_file_name, e)
933 })?;
934 Some(content)
935 } else {
936 None
938 };
939
940 let example_content = if let Some(ref example_file_name) = config.example_file {
942 root.get_file(example_file_name).and_then(|bytes| {
943 String::from_utf8(bytes.to_vec())
944 .map_err(|e| {
945 eprintln!(
946 "Warning: Example file '{}' is not valid UTF-8: {}",
947 example_file_name, e
948 );
949 e
950 })
951 .ok()
952 })
953 } else {
954 None
955 };
956
957 let defaults = crate::schema::extract_defaults_from_schema(&schema);
959 let examples = crate::schema::extract_examples_from_schema(&schema);
960
961 let quill = Quill {
962 metadata,
963 name: config.name,
964 backend: config.backend,
965 plate: plate_content,
966 example: example_content,
967 schema,
968 defaults,
969 examples,
970 files: root,
971 };
972
973 Ok(quill)
974 }
975
976 pub fn from_json(json_str: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
983 use serde_json::Value as JsonValue;
984
985 let json: JsonValue =
986 serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {}", e))?;
987
988 let obj = json.as_object().ok_or("Root must be an object")?;
989
990 let default_name = obj
992 .get("metadata")
993 .and_then(|m| m.get("name"))
994 .and_then(|v| v.as_str())
995 .map(String::from);
996
997 let files_obj = obj
999 .get("files")
1000 .and_then(|v| v.as_object())
1001 .ok_or("Missing or invalid 'files' key")?;
1002
1003 let mut root_files = HashMap::new();
1005 for (key, value) in files_obj {
1006 root_files.insert(key.clone(), FileTreeNode::from_json_value(value)?);
1007 }
1008
1009 let root = FileTreeNode::Directory { files: root_files };
1010
1011 Self::from_tree(root, default_name)
1013 }
1014
1015 fn load_directory_as_tree(
1017 current_dir: &Path,
1018 base_dir: &Path,
1019 ignore: &QuillIgnore,
1020 ) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
1021 use std::fs;
1022
1023 if !current_dir.exists() {
1024 return Ok(FileTreeNode::Directory {
1025 files: HashMap::new(),
1026 });
1027 }
1028
1029 let mut files = HashMap::new();
1030
1031 for entry in fs::read_dir(current_dir)? {
1032 let entry = entry?;
1033 let path = entry.path();
1034 let relative_path = path
1035 .strip_prefix(base_dir)
1036 .map_err(|e| format!("Failed to get relative path: {}", e))?
1037 .to_path_buf();
1038
1039 if ignore.is_ignored(&relative_path) {
1041 continue;
1042 }
1043
1044 let filename = path
1046 .file_name()
1047 .and_then(|n| n.to_str())
1048 .ok_or_else(|| format!("Invalid filename: {}", path.display()))?
1049 .to_string();
1050
1051 if path.is_file() {
1052 let contents = fs::read(&path)
1053 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
1054
1055 files.insert(filename, FileTreeNode::File { contents });
1056 } else if path.is_dir() {
1057 let subdir_tree = Self::load_directory_as_tree(&path, base_dir, ignore)?;
1059 files.insert(filename, subdir_tree);
1060 }
1061 }
1062
1063 Ok(FileTreeNode::Directory { files })
1064 }
1065
1066 pub fn typst_packages(&self) -> Vec<String> {
1068 self.metadata
1069 .get("typst_packages")
1070 .and_then(|v| v.as_array())
1071 .map(|arr| {
1072 arr.iter()
1073 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1074 .collect()
1075 })
1076 .unwrap_or_default()
1077 }
1078
1079 pub fn extract_defaults(&self) -> &HashMap<String, QuillValue> {
1087 &self.defaults
1088 }
1089
1090 pub fn extract_examples(&self) -> &HashMap<String, Vec<QuillValue>> {
1095 &self.examples
1096 }
1097
1098 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
1100 self.files.get_file(path)
1101 }
1102
1103 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
1105 self.files.file_exists(path)
1106 }
1107
1108 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
1110 self.files.dir_exists(path)
1111 }
1112
1113 pub fn list_files<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
1115 self.files.list_files(path)
1116 }
1117
1118 pub fn list_subdirectories<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
1120 self.files.list_subdirectories(path)
1121 }
1122
1123 pub fn list_directory<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
1125 let dir_path = dir_path.as_ref();
1126 let filenames = self.files.list_files(dir_path);
1127
1128 filenames
1130 .iter()
1131 .map(|name| {
1132 if dir_path == Path::new("") {
1133 PathBuf::from(name)
1134 } else {
1135 dir_path.join(name)
1136 }
1137 })
1138 .collect()
1139 }
1140
1141 pub fn list_directories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
1143 let dir_path = dir_path.as_ref();
1144 let subdirs = self.files.list_subdirectories(dir_path);
1145
1146 subdirs
1148 .iter()
1149 .map(|name| {
1150 if dir_path == Path::new("") {
1151 PathBuf::from(name)
1152 } else {
1153 dir_path.join(name)
1154 }
1155 })
1156 .collect()
1157 }
1158
1159 pub fn find_files<P: AsRef<Path>>(&self, pattern: P) -> Vec<PathBuf> {
1161 let pattern_str = pattern.as_ref().to_string_lossy();
1162 let mut matches = Vec::new();
1163
1164 let glob_pattern = match glob::Pattern::new(&pattern_str) {
1166 Ok(pat) => pat,
1167 Err(_) => return matches, };
1169
1170 self.find_files_recursive(&self.files, Path::new(""), &glob_pattern, &mut matches);
1172
1173 matches.sort();
1174 matches
1175 }
1176
1177 fn find_files_recursive(
1179 &self,
1180 node: &FileTreeNode,
1181 current_path: &Path,
1182 pattern: &glob::Pattern,
1183 matches: &mut Vec<PathBuf>,
1184 ) {
1185 match node {
1186 FileTreeNode::File { .. } => {
1187 let path_str = current_path.to_string_lossy();
1188 if pattern.matches(&path_str) {
1189 matches.push(current_path.to_path_buf());
1190 }
1191 }
1192 FileTreeNode::Directory { files } => {
1193 for (name, child_node) in files {
1194 let child_path = if current_path == Path::new("") {
1195 PathBuf::from(name)
1196 } else {
1197 current_path.join(name)
1198 };
1199 self.find_files_recursive(child_node, &child_path, pattern, matches);
1200 }
1201 }
1202 }
1203 }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::*;
1209 use std::fs;
1210 use tempfile::TempDir;
1211
1212 #[test]
1213 fn test_quillignore_parsing() {
1214 let ignore_content = r#"
1215# This is a comment
1216*.tmp
1217target/
1218node_modules/
1219.git/
1220"#;
1221 let ignore = QuillIgnore::from_content(ignore_content);
1222 assert_eq!(ignore.patterns.len(), 4);
1223 assert!(ignore.patterns.contains(&"*.tmp".to_string()));
1224 assert!(ignore.patterns.contains(&"target/".to_string()));
1225 }
1226
1227 #[test]
1228 fn test_quillignore_matching() {
1229 let ignore = QuillIgnore::new(vec![
1230 "*.tmp".to_string(),
1231 "target/".to_string(),
1232 "node_modules/".to_string(),
1233 ".git/".to_string(),
1234 ]);
1235
1236 assert!(ignore.is_ignored("test.tmp"));
1238 assert!(ignore.is_ignored("path/to/file.tmp"));
1239 assert!(!ignore.is_ignored("test.txt"));
1240
1241 assert!(ignore.is_ignored("target"));
1243 assert!(ignore.is_ignored("target/debug"));
1244 assert!(ignore.is_ignored("target/debug/deps"));
1245 assert!(!ignore.is_ignored("src/target.rs"));
1246
1247 assert!(ignore.is_ignored("node_modules"));
1248 assert!(ignore.is_ignored("node_modules/package"));
1249 assert!(!ignore.is_ignored("my_node_modules"));
1250 }
1251
1252 #[test]
1253 fn test_in_memory_file_system() {
1254 let temp_dir = TempDir::new().unwrap();
1255 let quill_dir = temp_dir.path();
1256
1257 fs::write(
1259 quill_dir.join("Quill.toml"),
1260 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1261 )
1262 .unwrap();
1263 fs::write(quill_dir.join("plate.typ"), "test plate").unwrap();
1264
1265 let assets_dir = quill_dir.join("assets");
1266 fs::create_dir_all(&assets_dir).unwrap();
1267 fs::write(assets_dir.join("test.txt"), "asset content").unwrap();
1268
1269 let packages_dir = quill_dir.join("packages");
1270 fs::create_dir_all(&packages_dir).unwrap();
1271 fs::write(packages_dir.join("package.typ"), "package content").unwrap();
1272
1273 let quill = Quill::from_path(quill_dir).unwrap();
1275
1276 assert!(quill.file_exists("plate.typ"));
1278 assert!(quill.file_exists("assets/test.txt"));
1279 assert!(quill.file_exists("packages/package.typ"));
1280 assert!(!quill.file_exists("nonexistent.txt"));
1281
1282 let asset_content = quill.get_file("assets/test.txt").unwrap();
1284 assert_eq!(asset_content, b"asset content");
1285
1286 let asset_files = quill.list_directory("assets");
1288 assert_eq!(asset_files.len(), 1);
1289 assert!(asset_files.contains(&PathBuf::from("assets/test.txt")));
1290 }
1291
1292 #[test]
1293 fn test_quillignore_integration() {
1294 let temp_dir = TempDir::new().unwrap();
1295 let quill_dir = temp_dir.path();
1296
1297 fs::write(quill_dir.join(".quillignore"), "*.tmp\ntarget/\n").unwrap();
1299
1300 fs::write(
1302 quill_dir.join("Quill.toml"),
1303 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1304 )
1305 .unwrap();
1306 fs::write(quill_dir.join("plate.typ"), "test template").unwrap();
1307 fs::write(quill_dir.join("should_ignore.tmp"), "ignored").unwrap();
1308
1309 let target_dir = quill_dir.join("target");
1310 fs::create_dir_all(&target_dir).unwrap();
1311 fs::write(target_dir.join("debug.txt"), "also ignored").unwrap();
1312
1313 let quill = Quill::from_path(quill_dir).unwrap();
1315
1316 assert!(quill.file_exists("plate.typ"));
1318 assert!(!quill.file_exists("should_ignore.tmp"));
1319 assert!(!quill.file_exists("target/debug.txt"));
1320 }
1321
1322 #[test]
1323 fn test_find_files_pattern() {
1324 let temp_dir = TempDir::new().unwrap();
1325 let quill_dir = temp_dir.path();
1326
1327 fs::write(
1329 quill_dir.join("Quill.toml"),
1330 "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"",
1331 )
1332 .unwrap();
1333 fs::write(quill_dir.join("plate.typ"), "template").unwrap();
1334
1335 let assets_dir = quill_dir.join("assets");
1336 fs::create_dir_all(&assets_dir).unwrap();
1337 fs::write(assets_dir.join("image.png"), "png data").unwrap();
1338 fs::write(assets_dir.join("data.json"), "json data").unwrap();
1339
1340 let fonts_dir = assets_dir.join("fonts");
1341 fs::create_dir_all(&fonts_dir).unwrap();
1342 fs::write(fonts_dir.join("font.ttf"), "font data").unwrap();
1343
1344 let quill = Quill::from_path(quill_dir).unwrap();
1346
1347 let all_assets = quill.find_files("assets/*");
1349 assert!(all_assets.len() >= 3); let typ_files = quill.find_files("*.typ");
1352 assert_eq!(typ_files.len(), 1);
1353 assert!(typ_files.contains(&PathBuf::from("plate.typ")));
1354 }
1355
1356 #[test]
1357 fn test_new_standardized_toml_format() {
1358 let temp_dir = TempDir::new().unwrap();
1359 let quill_dir = temp_dir.path();
1360
1361 let toml_content = r#"[Quill]
1363name = "my-custom-quill"
1364backend = "typst"
1365plate_file = "custom_plate.typ"
1366description = "Test quill with new format"
1367author = "Test Author"
1368"#;
1369 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1370 fs::write(
1371 quill_dir.join("custom_plate.typ"),
1372 "= Custom Template\n\nThis is a custom template.",
1373 )
1374 .unwrap();
1375
1376 let quill = Quill::from_path(quill_dir).unwrap();
1378
1379 assert_eq!(quill.name, "my-custom-quill");
1381
1382 assert!(quill.metadata.contains_key("backend"));
1384 if let Some(backend_val) = quill.metadata.get("backend") {
1385 if let Some(backend_str) = backend_val.as_str() {
1386 assert_eq!(backend_str, "typst");
1387 } else {
1388 panic!("Backend value is not a string");
1389 }
1390 }
1391
1392 assert!(quill.metadata.contains_key("description"));
1394 assert!(quill.metadata.contains_key("author"));
1395 assert!(!quill.metadata.contains_key("version")); assert!(quill.plate.unwrap().contains("Custom Template"));
1399 }
1400
1401 #[test]
1402 fn test_typst_packages_parsing() {
1403 let temp_dir = TempDir::new().unwrap();
1404 let quill_dir = temp_dir.path();
1405
1406 let toml_content = r#"
1407[Quill]
1408name = "test-quill"
1409backend = "typst"
1410plate_file = "plate.typ"
1411description = "Test quill for packages"
1412
1413[typst]
1414packages = ["@preview/bubble:0.2.2", "@preview/example:1.0.0"]
1415"#;
1416
1417 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1418 fs::write(quill_dir.join("plate.typ"), "test").unwrap();
1419
1420 let quill = Quill::from_path(quill_dir).unwrap();
1421 let packages = quill.typst_packages();
1422
1423 assert_eq!(packages.len(), 2);
1424 assert_eq!(packages[0], "@preview/bubble:0.2.2");
1425 assert_eq!(packages[1], "@preview/example:1.0.0");
1426 }
1427
1428 #[test]
1429 fn test_template_loading() {
1430 let temp_dir = TempDir::new().unwrap();
1431 let quill_dir = temp_dir.path();
1432
1433 let toml_content = r#"[Quill]
1435name = "test-with-template"
1436backend = "typst"
1437plate_file = "plate.typ"
1438example_file = "example.md"
1439description = "Test quill with template"
1440"#;
1441 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1442 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1443 fs::write(
1444 quill_dir.join("example.md"),
1445 "---\ntitle: Test\n---\n\nThis is a test template.",
1446 )
1447 .unwrap();
1448
1449 let quill = Quill::from_path(quill_dir).unwrap();
1451
1452 assert!(quill.example.is_some());
1454 let example = quill.example.unwrap();
1455 assert!(example.contains("title: Test"));
1456 assert!(example.contains("This is a test template"));
1457
1458 assert_eq!(quill.plate.unwrap(), "plate content");
1460 }
1461
1462 #[test]
1463 fn test_template_optional() {
1464 let temp_dir = TempDir::new().unwrap();
1465 let quill_dir = temp_dir.path();
1466
1467 let toml_content = r#"[Quill]
1469name = "test-without-template"
1470backend = "typst"
1471plate_file = "plate.typ"
1472description = "Test quill without template"
1473"#;
1474 fs::write(quill_dir.join("Quill.toml"), toml_content).unwrap();
1475 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1476
1477 let quill = Quill::from_path(quill_dir).unwrap();
1479
1480 assert_eq!(quill.example, None);
1482
1483 assert_eq!(quill.plate.unwrap(), "plate content");
1485 }
1486
1487 #[test]
1488 fn test_from_tree() {
1489 let mut root_files = HashMap::new();
1491
1492 let quill_toml = r#"[Quill]
1494name = "test-from-tree"
1495backend = "typst"
1496plate_file = "plate.typ"
1497description = "A test quill from tree"
1498"#;
1499 root_files.insert(
1500 "Quill.toml".to_string(),
1501 FileTreeNode::File {
1502 contents: quill_toml.as_bytes().to_vec(),
1503 },
1504 );
1505
1506 let plate_content = "= Test Template\n\nThis is a test.";
1508 root_files.insert(
1509 "plate.typ".to_string(),
1510 FileTreeNode::File {
1511 contents: plate_content.as_bytes().to_vec(),
1512 },
1513 );
1514
1515 let root = FileTreeNode::Directory { files: root_files };
1516
1517 let quill = Quill::from_tree(root, Some("test-from-tree".to_string())).unwrap();
1519
1520 assert_eq!(quill.name, "test-from-tree");
1522 assert_eq!(quill.plate.unwrap(), plate_content);
1523 assert!(quill.metadata.contains_key("backend"));
1524 assert!(quill.metadata.contains_key("description"));
1525 }
1526
1527 #[test]
1528 fn test_from_tree_with_template() {
1529 let mut root_files = HashMap::new();
1530
1531 let quill_toml = r#"[Quill]
1533name = "test-tree-template"
1534backend = "typst"
1535plate_file = "plate.typ"
1536example_file = "template.md"
1537description = "Test tree with template"
1538"#;
1539 root_files.insert(
1540 "Quill.toml".to_string(),
1541 FileTreeNode::File {
1542 contents: quill_toml.as_bytes().to_vec(),
1543 },
1544 );
1545
1546 root_files.insert(
1548 "plate.typ".to_string(),
1549 FileTreeNode::File {
1550 contents: b"plate content".to_vec(),
1551 },
1552 );
1553
1554 let template_content = "# {{ title }}\n\n{{ body }}";
1556 root_files.insert(
1557 "template.md".to_string(),
1558 FileTreeNode::File {
1559 contents: template_content.as_bytes().to_vec(),
1560 },
1561 );
1562
1563 let root = FileTreeNode::Directory { files: root_files };
1564
1565 let quill = Quill::from_tree(root, None).unwrap();
1567
1568 assert_eq!(quill.example, Some(template_content.to_string()));
1570 }
1571
1572 #[test]
1573 fn test_from_json() {
1574 let json_str = r#"{
1576 "metadata": {
1577 "name": "test-from-json"
1578 },
1579 "files": {
1580 "Quill.toml": {
1581 "contents": "[Quill]\nname = \"test-from-json\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill from JSON\"\n"
1582 },
1583 "plate.typ": {
1584 "contents": "= Test Plate\n\nThis is test content."
1585 }
1586 }
1587 }"#;
1588
1589 let quill = Quill::from_json(json_str).unwrap();
1591
1592 assert_eq!(quill.name, "test-from-json");
1594 assert!(quill.plate.unwrap().contains("Test Plate"));
1595 assert!(quill.metadata.contains_key("backend"));
1596 }
1597
1598 #[test]
1599 fn test_from_json_with_byte_array() {
1600 let json_str = r#"{
1602 "files": {
1603 "Quill.toml": {
1604 "contents": "[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"\n"
1605 },
1606 "plate.typ": {
1607 "contents": "test plate"
1608 }
1609 }
1610 }"#;
1611
1612 let quill = Quill::from_json(json_str).unwrap();
1614
1615 assert_eq!(quill.name, "test");
1617 assert_eq!(quill.plate.unwrap(), "test plate");
1618 }
1619
1620 #[test]
1621 fn test_from_json_missing_files() {
1622 let json_str = r#"{
1624 "metadata": {
1625 "name": "test"
1626 }
1627 }"#;
1628
1629 let result = Quill::from_json(json_str);
1630 assert!(result.is_err());
1631 assert!(result.unwrap_err().to_string().contains("files"));
1633 }
1634
1635 #[test]
1636 fn test_from_json_tree_structure() {
1637 let json_str = r#"{
1639 "files": {
1640 "Quill.toml": {
1641 "contents": "[Quill]\nname = \"test-tree-json\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test tree JSON\"\n"
1642 },
1643 "plate.typ": {
1644 "contents": "= Test Plate\n\nTree structure content."
1645 }
1646 }
1647 }"#;
1648
1649 let quill = Quill::from_json(json_str).unwrap();
1650
1651 assert_eq!(quill.name, "test-tree-json");
1652 assert!(quill.plate.unwrap().contains("Tree structure content"));
1653 assert!(quill.metadata.contains_key("backend"));
1654 }
1655
1656 #[test]
1657 fn test_from_json_nested_tree_structure() {
1658 let json_str = r#"{
1660 "files": {
1661 "Quill.toml": {
1662 "contents": "[Quill]\nname = \"nested-test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Nested test\"\n"
1663 },
1664 "plate.typ": {
1665 "contents": "plate"
1666 },
1667 "src": {
1668 "main.rs": {
1669 "contents": "fn main() {}"
1670 },
1671 "lib.rs": {
1672 "contents": "// lib"
1673 }
1674 }
1675 }
1676 }"#;
1677
1678 let quill = Quill::from_json(json_str).unwrap();
1679
1680 assert_eq!(quill.name, "nested-test");
1681 assert!(quill.file_exists("src/main.rs"));
1683 assert!(quill.file_exists("src/lib.rs"));
1684
1685 let main_rs = quill.get_file("src/main.rs").unwrap();
1686 assert_eq!(main_rs, b"fn main() {}");
1687 }
1688
1689 #[test]
1690 fn test_from_tree_structure_direct() {
1691 let mut root_files = HashMap::new();
1693
1694 root_files.insert(
1695 "Quill.toml".to_string(),
1696 FileTreeNode::File {
1697 contents:
1698 b"[Quill]\nname = \"direct-tree\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Direct tree test\"\n"
1699 .to_vec(),
1700 },
1701 );
1702
1703 root_files.insert(
1704 "plate.typ".to_string(),
1705 FileTreeNode::File {
1706 contents: b"plate content".to_vec(),
1707 },
1708 );
1709
1710 let mut src_files = HashMap::new();
1712 src_files.insert(
1713 "main.rs".to_string(),
1714 FileTreeNode::File {
1715 contents: b"fn main() {}".to_vec(),
1716 },
1717 );
1718
1719 root_files.insert(
1720 "src".to_string(),
1721 FileTreeNode::Directory { files: src_files },
1722 );
1723
1724 let root = FileTreeNode::Directory { files: root_files };
1725
1726 let quill = Quill::from_tree(root, None).unwrap();
1727
1728 assert_eq!(quill.name, "direct-tree");
1729 assert!(quill.file_exists("src/main.rs"));
1730 assert!(quill.file_exists("plate.typ"));
1731 }
1732
1733 #[test]
1734 fn test_from_json_with_metadata_override() {
1735 let json_str = r#"{
1737 "metadata": {
1738 "name": "override-name"
1739 },
1740 "files": {
1741 "Quill.toml": {
1742 "contents": "[Quill]\nname = \"toml-name\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"TOML name test\"\n"
1743 },
1744 "plate.typ": {
1745 "contents": "= plate"
1746 }
1747 }
1748 }"#;
1749
1750 let quill = Quill::from_json(json_str).unwrap();
1751 assert_eq!(quill.name, "toml-name");
1754 }
1755
1756 #[test]
1757 fn test_from_json_empty_directory() {
1758 let json_str = r#"{
1760 "files": {
1761 "Quill.toml": {
1762 "contents": "[Quill]\nname = \"empty-dir-test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Empty directory test\"\n"
1763 },
1764 "plate.typ": {
1765 "contents": "plate"
1766 },
1767 "empty_dir": {}
1768 }
1769 }"#;
1770
1771 let quill = Quill::from_json(json_str).unwrap();
1772 assert_eq!(quill.name, "empty-dir-test");
1773 assert!(quill.dir_exists("empty_dir"));
1774 assert!(!quill.file_exists("empty_dir"));
1775 }
1776
1777 #[test]
1778 fn test_dir_exists_and_list_apis() {
1779 let mut root_files = HashMap::new();
1780
1781 root_files.insert(
1783 "Quill.toml".to_string(),
1784 FileTreeNode::File {
1785 contents: b"[Quill]\nname = \"test\"\nbackend = \"typst\"\nplate_file = \"plate.typ\"\ndescription = \"Test quill\"\n"
1786 .to_vec(),
1787 },
1788 );
1789
1790 root_files.insert(
1792 "plate.typ".to_string(),
1793 FileTreeNode::File {
1794 contents: b"plate content".to_vec(),
1795 },
1796 );
1797
1798 let mut assets_files = HashMap::new();
1800 assets_files.insert(
1801 "logo.png".to_string(),
1802 FileTreeNode::File {
1803 contents: vec![137, 80, 78, 71],
1804 },
1805 );
1806 assets_files.insert(
1807 "icon.svg".to_string(),
1808 FileTreeNode::File {
1809 contents: b"<svg></svg>".to_vec(),
1810 },
1811 );
1812
1813 let mut fonts_files = HashMap::new();
1815 fonts_files.insert(
1816 "font.ttf".to_string(),
1817 FileTreeNode::File {
1818 contents: b"font data".to_vec(),
1819 },
1820 );
1821 assets_files.insert(
1822 "fonts".to_string(),
1823 FileTreeNode::Directory { files: fonts_files },
1824 );
1825
1826 root_files.insert(
1827 "assets".to_string(),
1828 FileTreeNode::Directory {
1829 files: assets_files,
1830 },
1831 );
1832
1833 root_files.insert(
1835 "empty".to_string(),
1836 FileTreeNode::Directory {
1837 files: HashMap::new(),
1838 },
1839 );
1840
1841 let root = FileTreeNode::Directory { files: root_files };
1842 let quill = Quill::from_tree(root, None).unwrap();
1843
1844 assert!(quill.dir_exists("assets"));
1846 assert!(quill.dir_exists("assets/fonts"));
1847 assert!(quill.dir_exists("empty"));
1848 assert!(!quill.dir_exists("nonexistent"));
1849 assert!(!quill.dir_exists("plate.typ")); assert!(quill.file_exists("plate.typ"));
1853 assert!(quill.file_exists("assets/logo.png"));
1854 assert!(quill.file_exists("assets/fonts/font.ttf"));
1855 assert!(!quill.file_exists("assets")); let root_files_list = quill.list_files("");
1859 assert_eq!(root_files_list.len(), 2); assert!(root_files_list.contains(&"Quill.toml".to_string()));
1861 assert!(root_files_list.contains(&"plate.typ".to_string()));
1862
1863 let assets_files_list = quill.list_files("assets");
1864 assert_eq!(assets_files_list.len(), 2); assert!(assets_files_list.contains(&"logo.png".to_string()));
1866 assert!(assets_files_list.contains(&"icon.svg".to_string()));
1867
1868 let root_subdirs = quill.list_subdirectories("");
1870 assert_eq!(root_subdirs.len(), 2); assert!(root_subdirs.contains(&"assets".to_string()));
1872 assert!(root_subdirs.contains(&"empty".to_string()));
1873
1874 let assets_subdirs = quill.list_subdirectories("assets");
1875 assert_eq!(assets_subdirs.len(), 1); assert!(assets_subdirs.contains(&"fonts".to_string()));
1877
1878 let empty_subdirs = quill.list_subdirectories("empty");
1879 assert_eq!(empty_subdirs.len(), 0);
1880 }
1881
1882 #[test]
1883 fn test_field_schemas_parsing() {
1884 let mut root_files = HashMap::new();
1885
1886 let quill_toml = r#"[Quill]
1888name = "taro"
1889backend = "typst"
1890plate_file = "plate.typ"
1891example_file = "taro.md"
1892description = "Test template for field schemas"
1893
1894[fields]
1895author = {description = "Author of document" }
1896ice_cream = {description = "favorite ice cream flavor"}
1897title = {description = "title of document" }
1898"#;
1899 root_files.insert(
1900 "Quill.toml".to_string(),
1901 FileTreeNode::File {
1902 contents: quill_toml.as_bytes().to_vec(),
1903 },
1904 );
1905
1906 let plate_content = "= Test Template\n\nThis is a test.";
1908 root_files.insert(
1909 "plate.typ".to_string(),
1910 FileTreeNode::File {
1911 contents: plate_content.as_bytes().to_vec(),
1912 },
1913 );
1914
1915 root_files.insert(
1917 "taro.md".to_string(),
1918 FileTreeNode::File {
1919 contents: b"# Template".to_vec(),
1920 },
1921 );
1922
1923 let root = FileTreeNode::Directory { files: root_files };
1924
1925 let quill = Quill::from_tree(root, Some("taro".to_string())).unwrap();
1927
1928 assert_eq!(quill.schema["properties"].as_object().unwrap().len(), 3);
1930 assert!(quill.schema["properties"]
1931 .as_object()
1932 .unwrap()
1933 .contains_key("author"));
1934 assert!(quill.schema["properties"]
1935 .as_object()
1936 .unwrap()
1937 .contains_key("ice_cream"));
1938 assert!(quill.schema["properties"]
1939 .as_object()
1940 .unwrap()
1941 .contains_key("title"));
1942
1943 let author_schema = quill.schema["properties"]["author"].as_object().unwrap();
1945 assert_eq!(author_schema["description"], "Author of document");
1946
1947 let ice_cream_schema = quill.schema["properties"]["ice_cream"].as_object().unwrap();
1949 assert_eq!(ice_cream_schema["description"], "favorite ice cream flavor");
1950
1951 let title_schema = quill.schema["properties"]["title"].as_object().unwrap();
1953 assert_eq!(title_schema["description"], "title of document");
1954 }
1955
1956 #[test]
1957 fn test_field_schema_struct() {
1958 let schema1 = FieldSchema::new("test_name".to_string(), "Test description".to_string());
1960 assert_eq!(schema1.description, "Test description");
1961 assert_eq!(schema1.r#type, None);
1962 assert_eq!(schema1.examples, None);
1963 assert_eq!(schema1.default, None);
1964
1965 let yaml_str = r#"
1967description: "Full field schema"
1968type: "string"
1969examples:
1970 - "Example value"
1971default: "Default value"
1972"#;
1973 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
1974 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
1975 let schema2 = FieldSchema::from_quill_value("test_name".to_string(), &quill_value).unwrap();
1976 assert_eq!(schema2.name, "test_name");
1977 assert_eq!(schema2.description, "Full field schema");
1978 assert_eq!(schema2.r#type, Some("string".to_string()));
1979 assert_eq!(
1980 schema2
1981 .examples
1982 .as_ref()
1983 .and_then(|v| v.as_array())
1984 .and_then(|arr| arr.first())
1985 .and_then(|v| v.as_str()),
1986 Some("Example value")
1987 );
1988 assert_eq!(
1989 schema2.default.as_ref().and_then(|v| v.as_str()),
1990 Some("Default value")
1991 );
1992 }
1993
1994 #[test]
1995 fn test_quill_without_plate_file() {
1996 let mut root_files = HashMap::new();
1998
1999 let quill_toml = r#"[Quill]
2001name = "test-no-plate"
2002backend = "typst"
2003description = "Test quill without plate file"
2004"#;
2005 root_files.insert(
2006 "Quill.toml".to_string(),
2007 FileTreeNode::File {
2008 contents: quill_toml.as_bytes().to_vec(),
2009 },
2010 );
2011
2012 let root = FileTreeNode::Directory { files: root_files };
2013
2014 let quill = Quill::from_tree(root, None).unwrap();
2016
2017 assert!(quill.plate.clone().is_none());
2019 assert_eq!(quill.name, "test-no-plate");
2020 }
2021
2022 #[test]
2023 fn test_quill_config_from_toml() {
2024 let toml_content = r#"[Quill]
2026name = "test-config"
2027backend = "typst"
2028description = "Test configuration parsing"
2029version = "1.0.0"
2030author = "Test Author"
2031plate_file = "plate.typ"
2032example_file = "example.md"
2033
2034[typst]
2035packages = ["@preview/bubble:0.2.2"]
2036
2037[fields]
2038title = {description = "Document title", type = "string"}
2039author = {description = "Document author"}
2040"#;
2041
2042 let config = QuillConfig::from_toml(toml_content).unwrap();
2043
2044 assert_eq!(config.name, "test-config");
2046 assert_eq!(config.backend, "typst");
2047 assert_eq!(config.description, "Test configuration parsing");
2048
2049 assert_eq!(config.version, Some("1.0.0".to_string()));
2051 assert_eq!(config.author, Some("Test Author".to_string()));
2052 assert_eq!(config.plate_file, Some("plate.typ".to_string()));
2053 assert_eq!(config.example_file, Some("example.md".to_string()));
2054
2055 assert!(config.typst_config.contains_key("packages"));
2057
2058 assert_eq!(config.fields.len(), 2);
2060 assert!(config.fields.contains_key("title"));
2061 assert!(config.fields.contains_key("author"));
2062
2063 let title_field = &config.fields["title"];
2064 assert_eq!(title_field.description, "Document title");
2065 assert_eq!(title_field.r#type, Some("string".to_string()));
2066 }
2067
2068 #[test]
2069 fn test_quill_config_missing_required_fields() {
2070 let toml_missing_name = r#"[Quill]
2072backend = "typst"
2073description = "Missing name"
2074"#;
2075 let result = QuillConfig::from_toml(toml_missing_name);
2076 assert!(result.is_err());
2077 assert!(result
2078 .unwrap_err()
2079 .to_string()
2080 .contains("Missing required 'name'"));
2081
2082 let toml_missing_backend = r#"[Quill]
2083name = "test"
2084description = "Missing backend"
2085"#;
2086 let result = QuillConfig::from_toml(toml_missing_backend);
2087 assert!(result.is_err());
2088 assert!(result
2089 .unwrap_err()
2090 .to_string()
2091 .contains("Missing required 'backend'"));
2092
2093 let toml_missing_description = r#"[Quill]
2094name = "test"
2095backend = "typst"
2096"#;
2097 let result = QuillConfig::from_toml(toml_missing_description);
2098 assert!(result.is_err());
2099 assert!(result
2100 .unwrap_err()
2101 .to_string()
2102 .contains("Missing required 'description'"));
2103 }
2104
2105 #[test]
2106 fn test_quill_config_empty_description() {
2107 let toml_empty_description = r#"[Quill]
2109name = "test"
2110backend = "typst"
2111description = " "
2112"#;
2113 let result = QuillConfig::from_toml(toml_empty_description);
2114 assert!(result.is_err());
2115 assert!(result
2116 .unwrap_err()
2117 .to_string()
2118 .contains("description' field in [Quill] section cannot be empty"));
2119 }
2120
2121 #[test]
2122 fn test_quill_config_missing_quill_section() {
2123 let toml_no_section = r#"[fields]
2125title = {description = "Title"}
2126"#;
2127 let result = QuillConfig::from_toml(toml_no_section);
2128 assert!(result.is_err());
2129 assert!(result
2130 .unwrap_err()
2131 .to_string()
2132 .contains("Missing required [Quill] section"));
2133 }
2134
2135 #[test]
2136 fn test_quill_from_config_metadata() {
2137 let mut root_files = HashMap::new();
2139
2140 let quill_toml = r#"[Quill]
2141name = "metadata-test"
2142backend = "typst"
2143description = "Test metadata flow"
2144author = "Test Author"
2145custom_field = "custom_value"
2146
2147[typst]
2148packages = ["@preview/bubble:0.2.2"]
2149"#;
2150 root_files.insert(
2151 "Quill.toml".to_string(),
2152 FileTreeNode::File {
2153 contents: quill_toml.as_bytes().to_vec(),
2154 },
2155 );
2156
2157 let root = FileTreeNode::Directory { files: root_files };
2158 let quill = Quill::from_tree(root, None).unwrap();
2159
2160 assert!(quill.metadata.contains_key("backend"));
2162 assert!(quill.metadata.contains_key("description"));
2163 assert!(quill.metadata.contains_key("author"));
2164
2165 assert!(quill.metadata.contains_key("custom_field"));
2167 assert_eq!(
2168 quill.metadata.get("custom_field").unwrap().as_str(),
2169 Some("custom_value")
2170 );
2171
2172 assert!(quill.metadata.contains_key("typst_packages"));
2174 }
2175
2176 #[test]
2177 fn test_extract_defaults_method() {
2178 let mut root_files = HashMap::new();
2180
2181 let quill_toml = r#"[Quill]
2182name = "defaults-test"
2183backend = "typst"
2184description = "Test defaults extraction"
2185
2186[fields]
2187title = {description = "Title"}
2188author = {description = "Author", default = "Anonymous"}
2189status = {description = "Status", default = "draft"}
2190"#;
2191
2192 root_files.insert(
2193 "Quill.toml".to_string(),
2194 FileTreeNode::File {
2195 contents: quill_toml.as_bytes().to_vec(),
2196 },
2197 );
2198
2199 let root = FileTreeNode::Directory { files: root_files };
2200 let quill = Quill::from_tree(root, None).unwrap();
2201
2202 let defaults = quill.extract_defaults();
2204
2205 assert_eq!(defaults.len(), 2);
2207 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
2209 assert!(defaults.contains_key("status"));
2210
2211 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
2213 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
2214 }
2215
2216 #[test]
2217 fn test_field_order_preservation() {
2218 let toml_content = r#"[Quill]
2219name = "order-test"
2220backend = "typst"
2221description = "Test field order"
2222
2223[fields]
2224first = {description = "First field"}
2225second = {description = "Second field"}
2226third = {description = "Third field", ui = {group = "Test Group"}}
2227fourth = {description = "Fourth field"}
2228"#;
2229
2230 let config = QuillConfig::from_toml(toml_content).unwrap();
2231
2232 let first = config.fields.get("first").unwrap();
2236 assert_eq!(first.ui.as_ref().unwrap().order, Some(0));
2237
2238 let second = config.fields.get("second").unwrap();
2239 assert_eq!(second.ui.as_ref().unwrap().order, Some(1));
2240
2241 let third = config.fields.get("third").unwrap();
2242 assert_eq!(third.ui.as_ref().unwrap().order, Some(2));
2243 assert_eq!(
2244 third.ui.as_ref().unwrap().group,
2245 Some("Test Group".to_string())
2246 );
2247
2248 let fourth = config.fields.get("fourth").unwrap();
2249 assert_eq!(fourth.ui.as_ref().unwrap().order, Some(3));
2250 }
2251
2252 #[test]
2253 fn test_quill_with_all_ui_properties() {
2254 let toml_content = r#"[Quill]
2255name = "full-ui-test"
2256backend = "typst"
2257description = "Test all UI properties"
2258
2259[fields.author]
2260description = "The full name of the document author"
2261type = "str"
2262
2263[fields.author.ui]
2264group = "Author Info"
2265"#;
2266
2267 let config = QuillConfig::from_toml(toml_content).unwrap();
2268
2269 let author_field = &config.fields["author"];
2270 let ui = author_field.ui.as_ref().unwrap();
2271 assert_eq!(ui.group, Some("Author Info".to_string()));
2272 assert_eq!(ui.order, Some(0)); }
2274 #[test]
2275 fn test_field_schema_with_title_and_description() {
2276 let yaml = r#"
2278title: "Field Title"
2279description: "Detailed field description"
2280type: "string"
2281examples:
2282 - "Example value"
2283ui:
2284 group: "Test Group"
2285"#;
2286 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
2287 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
2288 let schema = FieldSchema::from_quill_value("test_field".to_string(), &quill_value).unwrap();
2289
2290 assert_eq!(schema.title, Some("Field Title".to_string()));
2291 assert_eq!(schema.description, "Detailed field description");
2292
2293 assert_eq!(
2294 schema
2295 .examples
2296 .as_ref()
2297 .and_then(|v| v.as_array())
2298 .and_then(|arr| arr.first())
2299 .and_then(|v| v.as_str()),
2300 Some("Example value")
2301 );
2302
2303 let ui = schema.ui.as_ref().unwrap();
2304 assert_eq!(ui.group, Some("Test Group".to_string()));
2305 }
2306
2307 #[test]
2308 fn test_parse_card_field_type() {
2309 let yaml = r#"
2311type: "string"
2312title: "Simple Field"
2313description: "A simple string field"
2314"#;
2315 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
2316 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
2317 let schema =
2318 FieldSchema::from_quill_value("simple_field".to_string(), &quill_value).unwrap();
2319
2320 assert_eq!(schema.name, "simple_field");
2321 assert_eq!(schema.r#type, Some("string".to_string()));
2322 assert_eq!(schema.title, Some("Simple Field".to_string()));
2323 assert_eq!(schema.description, "A simple string field");
2324 }
2325
2326 #[test]
2327 fn test_parse_card_with_fields_in_toml() {
2328 let toml_content = r#"[Quill]
2330name = "cards-fields-test"
2331backend = "typst"
2332description = "Test [cards.X.fields.Y] syntax"
2333
2334[cards.endorsements]
2335title = "Endorsements"
2336description = "Chain of endorsements"
2337
2338[cards.endorsements.fields.name]
2339type = "string"
2340title = "Endorser Name"
2341description = "Name of the endorsing official"
2342required = true
2343
2344[cards.endorsements.fields.org]
2345type = "string"
2346title = "Organization"
2347description = "Endorser's organization"
2348default = "Unknown"
2349"#;
2350
2351 let config = QuillConfig::from_toml(toml_content).unwrap();
2352
2353 assert!(config.cards.contains_key("endorsements"));
2355 let card = config.cards.get("endorsements").unwrap();
2356
2357 assert_eq!(card.name, "endorsements");
2358 assert_eq!(card.title, Some("Endorsements".to_string()));
2359 assert_eq!(card.description, "Chain of endorsements");
2360
2361 assert_eq!(card.fields.len(), 2);
2363
2364 let name_field = card.fields.get("name").unwrap();
2365 assert_eq!(name_field.r#type, Some("string".to_string()));
2366 assert_eq!(name_field.title, Some("Endorser Name".to_string()));
2367 assert!(name_field.required);
2368
2369 let org_field = card.fields.get("org").unwrap();
2370 assert_eq!(org_field.r#type, Some("string".to_string()));
2371 assert!(org_field.default.is_some());
2372 assert_eq!(
2373 org_field.default.as_ref().unwrap().as_str(),
2374 Some("Unknown")
2375 );
2376 }
2377
2378 #[test]
2379 fn test_field_schema_ignores_unknown_keys() {
2380 let yaml = r#"
2382type: "string"
2383description: "A string field"
2384items:
2385 sub_field:
2386 type: "string"
2387 description: "Nested field"
2388"#;
2389 let yaml_value: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
2390 let quill_value = QuillValue::from_yaml(yaml_value).unwrap();
2391 let result = FieldSchema::from_quill_value("author".to_string(), &quill_value);
2393
2394 assert!(result.is_ok());
2396 let schema = result.unwrap();
2397 assert_eq!(schema.r#type, Some("string".to_string()));
2398 }
2399
2400 #[test]
2401 fn test_quill_config_with_cards_section() {
2402 let toml_content = r#"[Quill]
2403name = "cards-test"
2404backend = "typst"
2405description = "Test [cards] section"
2406
2407[fields.regular]
2408description = "Regular field"
2409type = "string"
2410
2411[cards.indorsements]
2412title = "Routing Indorsements"
2413description = "Chain of endorsements"
2414
2415[cards.indorsements.fields.name]
2416title = "Name"
2417type = "string"
2418description = "Name field"
2419"#;
2420
2421 let config = QuillConfig::from_toml(toml_content).unwrap();
2422
2423 assert!(config.fields.contains_key("regular"));
2425 let regular = config.fields.get("regular").unwrap();
2426 assert_eq!(regular.r#type, Some("string".to_string()));
2427
2428 assert!(config.cards.contains_key("indorsements"));
2430 let card = config.cards.get("indorsements").unwrap();
2431 assert_eq!(card.title, Some("Routing Indorsements".to_string()));
2432 assert_eq!(card.description, "Chain of endorsements");
2433 assert!(card.fields.contains_key("name"));
2434 }
2435
2436 #[test]
2437 fn test_quill_config_cards_empty_fields() {
2438 let toml_content = r#"[Quill]
2440name = "cards-empty-fields-test"
2441backend = "typst"
2442description = "Test cards without fields"
2443
2444[cards.myscope]
2445description = "My scope"
2446"#;
2447
2448 let config = QuillConfig::from_toml(toml_content).unwrap();
2449 let card = config.cards.get("myscope").unwrap();
2450 assert_eq!(card.name, "myscope");
2451 assert_eq!(card.description, "My scope");
2452 assert!(card.fields.is_empty());
2453 }
2454
2455 #[test]
2456 fn test_quill_config_card_collision() {
2457 let toml_content = r#"[Quill]
2459name = "collision-test"
2460backend = "typst"
2461description = "Test collision"
2462
2463[fields.conflict]
2464description = "Field"
2465type = "string"
2466
2467[cards.conflict]
2468description = "Card"
2469items = {}
2470"#;
2471
2472 let result = QuillConfig::from_toml(toml_content);
2473 assert!(result.is_err());
2474 assert!(result
2475 .unwrap_err()
2476 .to_string()
2477 .contains("conflicts with an existing field name"));
2478 }
2479
2480 #[test]
2481 fn test_quill_config_ordering_with_cards() {
2482 let toml_content = r#"[Quill]
2484name = "ordering-test"
2485backend = "typst"
2486description = "Test ordering"
2487
2488[fields.first]
2489description = "First"
2490
2491[cards.second]
2492description = "Second"
2493
2494[fields.zero]
2495description = "Zero"
2496"#;
2497
2498 let config = QuillConfig::from_toml(toml_content).unwrap();
2499
2500 let first = config.fields.get("first").unwrap();
2501 let zero = config.fields.get("zero").unwrap();
2502 let second = config.cards.get("second").unwrap(); let ord_first = first.ui.as_ref().unwrap().order.unwrap();
2506 let ord_zero = zero.ui.as_ref().unwrap().order.unwrap();
2507 let ord_second = second.ui.as_ref().unwrap().order.unwrap();
2508
2509 assert!(ord_first < ord_second);
2514 assert!(ord_zero < ord_second);
2515
2516 assert!(ord_first < ord_zero);
2518
2519 assert_eq!(ord_first, 0);
2520 assert_eq!(ord_zero, 1);
2521 assert_eq!(ord_second, 2);
2522 }
2523}