1use std::collections::HashMap;
4use std::error::Error as StdError;
5use std::path::{Path, PathBuf};
6
7use serde::{Deserialize, Serialize};
8
9use crate::value::QuillValue;
10
11pub mod field_key {
15 pub const TITLE: &str = "title";
17 pub const TYPE: &str = "type";
19 pub const DESCRIPTION: &str = "description";
21 pub const DEFAULT: &str = "default";
23 pub const EXAMPLES: &str = "examples";
25 pub const UI: &str = "ui";
27 pub const REQUIRED: &str = "required";
29 pub const ENUM: &str = "enum";
31 pub const FORMAT: &str = "format";
33}
34
35pub mod ui_key {
37 pub const GROUP: &str = "group";
39 pub const ORDER: &str = "order";
41 pub const HIDE_BODY: &str = "hide_body";
43 pub const VISIBLE_WHEN: &str = "visible_when";
45 pub const COMPACT: &str = "compact";
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct UiFieldSchema {
53 pub group: Option<String>,
55 pub order: Option<i32>,
57 pub visible_when: Option<HashMap<String, Vec<String>>>,
62 pub compact: Option<bool>,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct UiContainerSchema {
69 pub hide_body: Option<bool>,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct CardSchema {
76 pub name: String,
78 pub title: Option<String>,
80 pub description: Option<String>,
82 pub fields: HashMap<String, FieldSchema>,
84 pub ui: Option<UiContainerSchema>,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "lowercase")]
91pub enum FieldType {
92 #[serde(alias = "str")]
94 String,
95 Number,
97 Boolean,
99 Array,
101 Object,
103 Date,
105 DateTime,
107 Markdown,
109}
110
111impl FieldType {
112 pub fn from_str(s: &str) -> Option<Self> {
114 match s {
115 "string" | "str" => Some(FieldType::String),
116 "number" => Some(FieldType::Number),
117 "boolean" => Some(FieldType::Boolean),
118 "array" => Some(FieldType::Array),
119 "object" | "dict" => Some(FieldType::Object),
120 "date" => Some(FieldType::Date),
121 "datetime" => Some(FieldType::DateTime),
122 "markdown" => Some(FieldType::Markdown),
123 _ => None,
124 }
125 }
126
127 pub fn as_str(&self) -> &'static str {
129 match self {
130 FieldType::String => "string",
131 FieldType::Number => "number",
132 FieldType::Boolean => "boolean",
133 FieldType::Array => "array",
134 FieldType::Object => "dict",
135 FieldType::Date => "date",
136 FieldType::DateTime => "datetime",
137 FieldType::Markdown => "markdown",
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct FieldSchema {
145 pub name: String,
146 pub title: Option<String>,
148 pub r#type: FieldType,
150 pub description: Option<String>,
152 pub default: Option<QuillValue>,
154 pub examples: Option<QuillValue>,
156 pub ui: Option<UiFieldSchema>,
158 pub required: bool,
160 pub enum_values: Option<Vec<String>>,
162 pub properties: Option<HashMap<String, Box<FieldSchema>>>,
164 pub items: Option<Box<FieldSchema>>,
166}
167
168#[derive(Debug, Deserialize)]
169#[serde(deny_unknown_fields)]
170struct FieldSchemaDef {
171 pub title: Option<String>,
172 pub r#type: FieldType,
173 pub description: Option<String>,
174 pub default: Option<QuillValue>,
175 pub examples: Option<QuillValue>,
176 pub ui: Option<UiFieldSchema>,
177 #[serde(default)]
178 pub required: bool,
179 #[serde(rename = "enum")]
180 pub enum_values: Option<Vec<String>>,
181 pub properties: Option<serde_json::Map<String, serde_json::Value>>,
184 pub items: Option<serde_json::Value>,
185}
186
187impl FieldSchema {
188 pub fn new(name: String, r#type: FieldType, description: Option<String>) -> Self {
190 Self {
191 name,
192 title: None,
193 r#type,
194 description,
195 default: None,
196 examples: None,
197 ui: None,
198 required: false,
199 enum_values: None,
200 properties: None,
201 items: None,
202 }
203 }
204
205 pub fn from_quill_value(key: String, value: &QuillValue) -> Result<Self, String> {
207 let def: FieldSchemaDef = serde_json::from_value(value.clone().into_json())
208 .map_err(|e| format!("Failed to parse field schema: {}", e))?;
209
210 Ok(Self {
211 name: key,
212 title: def.title,
213 r#type: def.r#type,
214 description: def.description,
215 default: def.default,
216 examples: def.examples,
217 ui: def.ui,
218 required: def.required,
219 enum_values: def.enum_values,
220 properties: if let Some(props) = def.properties {
221 let mut p = HashMap::new();
222 for (key, value) in props {
223 p.insert(
224 key.clone(),
225 Box::new(FieldSchema::from_quill_value(
226 key,
227 &QuillValue::from_json(value),
228 )?),
229 );
230 }
231 Some(p)
232 } else {
233 None
234 },
235 items: if let Some(item_def) = def.items {
236 Some(Box::new(FieldSchema::from_quill_value(
237 "items".to_string(),
238 &QuillValue::from_json(item_def),
239 )?))
240 } else {
241 None
242 },
243 })
244 }
245}
246
247#[derive(Debug, Clone)]
249pub enum FileTreeNode {
250 File {
252 contents: Vec<u8>,
254 },
255 Directory {
257 files: HashMap<String, FileTreeNode>,
259 },
260}
261
262impl FileTreeNode {
263 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
265 let path = path.as_ref();
266
267 if path == Path::new("") {
269 return Some(self);
270 }
271
272 let components: Vec<_> = path
274 .components()
275 .filter_map(|c| {
276 if let std::path::Component::Normal(s) = c {
277 s.to_str()
278 } else {
279 None
280 }
281 })
282 .collect();
283
284 if components.is_empty() {
285 return Some(self);
286 }
287
288 let mut current_node = self;
290 for component in components {
291 match current_node {
292 FileTreeNode::Directory { files } => {
293 current_node = files.get(component)?;
294 }
295 FileTreeNode::File { .. } => {
296 return None; }
298 }
299 }
300
301 Some(current_node)
302 }
303
304 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
306 match self.get_node(path)? {
307 FileTreeNode::File { contents } => Some(contents.as_slice()),
308 FileTreeNode::Directory { .. } => None,
309 }
310 }
311
312 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
314 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
315 }
316
317 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
319 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
320 }
321
322 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
324 match self.get_node(dir_path) {
325 Some(FileTreeNode::Directory { files }) => files
326 .iter()
327 .filter_map(|(name, node)| {
328 if matches!(node, FileTreeNode::File { .. }) {
329 Some(name.clone())
330 } else {
331 None
332 }
333 })
334 .collect(),
335 _ => Vec::new(),
336 }
337 }
338
339 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
341 match self.get_node(dir_path) {
342 Some(FileTreeNode::Directory { files }) => files
343 .iter()
344 .filter_map(|(name, node)| {
345 if matches!(node, FileTreeNode::Directory { .. }) {
346 Some(name.clone())
347 } else {
348 None
349 }
350 })
351 .collect(),
352 _ => Vec::new(),
353 }
354 }
355
356 pub fn insert<P: AsRef<Path>>(
358 &mut self,
359 path: P,
360 node: FileTreeNode,
361 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
362 let path = path.as_ref();
363
364 let components: Vec<_> = path
366 .components()
367 .filter_map(|c| {
368 if let std::path::Component::Normal(s) = c {
369 s.to_str().map(|s| s.to_string())
370 } else {
371 None
372 }
373 })
374 .collect();
375
376 if components.is_empty() {
377 return Err("Cannot insert at root path".into());
378 }
379
380 let mut current_node = self;
382 for component in &components[..components.len() - 1] {
383 match current_node {
384 FileTreeNode::Directory { files } => {
385 current_node =
386 files
387 .entry(component.clone())
388 .or_insert_with(|| FileTreeNode::Directory {
389 files: HashMap::new(),
390 });
391 }
392 FileTreeNode::File { .. } => {
393 return Err("Cannot traverse into a file".into());
394 }
395 }
396 }
397
398 let filename = &components[components.len() - 1];
400 match current_node {
401 FileTreeNode::Directory { files } => {
402 files.insert(filename.clone(), node);
403 Ok(())
404 }
405 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
406 }
407 }
408
409 fn from_json_value(value: &serde_json::Value) -> Result<Self, Box<dyn StdError + Send + Sync>> {
411 if let Some(contents_str) = value.get("contents").and_then(|v| v.as_str()) {
412 Ok(FileTreeNode::File {
414 contents: contents_str.as_bytes().to_vec(),
415 })
416 } else if let Some(bytes_array) = value.get("contents").and_then(|v| v.as_array()) {
417 let contents: Vec<u8> = bytes_array
419 .iter()
420 .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
421 .collect();
422 Ok(FileTreeNode::File { contents })
423 } else if let Some(obj) = value.as_object() {
424 let mut files = HashMap::new();
426 for (name, child_value) in obj {
427 files.insert(name.clone(), Self::from_json_value(child_value)?);
428 }
429 Ok(FileTreeNode::Directory { files })
431 } else {
432 Err(format!("Invalid file tree node: {:?}", value).into())
433 }
434 }
435
436 pub fn print_tree(&self) -> String {
437 self.print_tree_recursive("", "", true)
438 }
439
440 fn print_tree_recursive(&self, name: &str, prefix: &str, is_last: bool) -> String {
441 let mut result = String::new();
442
443 let connector = if is_last { "└── " } else { "├── " };
445 let extension = if is_last { " " } else { "│ " };
446
447 match self {
448 FileTreeNode::File { .. } => {
449 result.push_str(&format!("{}{}{}\n", prefix, connector, name));
450 }
451 FileTreeNode::Directory { files } => {
452 result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
454
455 let child_prefix = format!("{}{}", prefix, extension);
456 let count = files.len();
457
458 for (i, (child_name, node)) in files.iter().enumerate() {
459 let is_last_child = i == count - 1;
460 result.push_str(&node.print_tree_recursive(
461 child_name,
462 &child_prefix,
463 is_last_child,
464 ));
465 }
466 }
467 }
468
469 result
470 }
471}
472
473#[derive(Debug, Clone)]
475pub struct QuillIgnore {
476 patterns: Vec<String>,
477}
478
479impl QuillIgnore {
480 pub fn new(patterns: Vec<String>) -> Self {
482 Self { patterns }
483 }
484
485 pub fn from_content(content: &str) -> Self {
487 let patterns = content
488 .lines()
489 .map(|line| line.trim())
490 .filter(|line| !line.is_empty() && !line.starts_with('#'))
491 .map(|line| line.to_string())
492 .collect();
493 Self::new(patterns)
494 }
495
496 pub fn is_ignored<P: AsRef<Path>>(&self, path: P) -> bool {
498 let path = path.as_ref();
499 let path_str = path.to_string_lossy();
500
501 for pattern in &self.patterns {
502 if self.matches_pattern(pattern, &path_str) {
503 return true;
504 }
505 }
506 false
507 }
508
509 fn matches_pattern(&self, pattern: &str, path: &str) -> bool {
511 if let Some(pattern_prefix) = pattern.strip_suffix('/') {
513 return path.starts_with(pattern_prefix)
514 && (path.len() == pattern_prefix.len()
515 || path.chars().nth(pattern_prefix.len()) == Some('/'));
516 }
517
518 if !pattern.contains('*') {
520 return path == pattern || path.ends_with(&format!("/{}", pattern));
521 }
522
523 if pattern == "*" {
525 return true;
526 }
527
528 let pattern_parts: Vec<&str> = pattern.split('*').collect();
530 if pattern_parts.len() == 2 {
531 let (prefix, suffix) = (pattern_parts[0], pattern_parts[1]);
532 if prefix.is_empty() {
533 return path.ends_with(suffix);
534 } else if suffix.is_empty() {
535 return path.starts_with(prefix);
536 } else {
537 return path.starts_with(prefix) && path.ends_with(suffix);
538 }
539 }
540
541 false
542 }
543}
544
545#[derive(Debug, Clone)]
547pub struct Quill {
548 pub metadata: HashMap<String, QuillValue>,
550 pub name: String,
552 pub backend: String,
554 pub plate: Option<String>,
556 pub example: Option<String>,
558 pub schema: QuillValue,
560 pub defaults: HashMap<String, QuillValue>,
562 pub examples: HashMap<String, Vec<QuillValue>>,
564 pub files: FileTreeNode,
566}
567
568#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
570pub struct QuillConfig {
571 pub document: CardSchema,
573 pub backend: String,
575 pub version: String,
577 pub author: String,
579 pub example_file: Option<String>,
581 pub plate_file: Option<String>,
583 pub cards: HashMap<String, CardSchema>,
585 #[serde(flatten)]
587 pub metadata: HashMap<String, QuillValue>,
588 #[serde(default)]
590 pub typst_config: HashMap<String, QuillValue>,
591}
592
593#[derive(Debug, Deserialize)]
594#[serde(deny_unknown_fields)]
595struct CardSchemaDef {
596 pub title: Option<String>,
597 pub description: Option<String>,
598 pub fields: Option<serde_json::Map<String, serde_json::Value>>,
599 pub ui: Option<UiContainerSchema>,
600}
601
602impl QuillConfig {
603 fn parse_fields_with_order(
613 fields_map: &serde_json::Map<String, serde_json::Value>,
614 key_order: &[String],
615 context: &str,
616 ) -> HashMap<String, FieldSchema> {
617 let mut fields = HashMap::new();
618 let mut fallback_counter = 0;
619
620 for (field_name, field_value) in fields_map {
621 let order = if let Some(idx) = key_order.iter().position(|k| k == field_name) {
623 idx as i32
624 } else {
625 let o = key_order.len() as i32 + fallback_counter;
626 fallback_counter += 1;
627 o
628 };
629
630 let quill_value = QuillValue::from_json(field_value.clone());
631 match FieldSchema::from_quill_value(field_name.clone(), &quill_value) {
632 Ok(mut schema) => {
633 if schema.ui.is_none() {
635 schema.ui = Some(UiFieldSchema {
636 group: None,
637 order: Some(order),
638 visible_when: None,
639 compact: None,
640 });
641 } else if let Some(ui) = &mut schema.ui {
642 if ui.order.is_none() {
644 ui.order = Some(order);
645 }
646 }
647
648 fields.insert(field_name.clone(), schema);
649 }
650 Err(e) => {
651 eprintln!(
652 "Warning: Failed to parse {} '{}': {}",
653 context, field_name, e
654 );
655 }
656 }
657 }
658
659 fields
660 }
661
662 pub fn from_yaml(yaml_content: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
664 let quill_yaml_val: serde_json::Value = serde_saphyr::from_str(yaml_content)
667 .map_err(|e| format!("Failed to parse Quill.yaml: {}", e))?;
668
669 let quill_section = quill_yaml_val
671 .get("Quill")
672 .ok_or("Missing required 'Quill' section in Quill.yaml")?;
673
674 let name = quill_section
676 .get("name")
677 .and_then(|v| v.as_str())
678 .ok_or("Missing required 'name' field in 'Quill' section")?
679 .to_string();
680
681 let backend = quill_section
682 .get("backend")
683 .and_then(|v| v.as_str())
684 .ok_or("Missing required 'backend' field in 'Quill' section")?
685 .to_string();
686
687 let description = quill_section
688 .get("description")
689 .and_then(|v| v.as_str())
690 .ok_or("Missing required 'description' field in 'Quill' section")?;
691
692 if description.trim().is_empty() {
693 return Err("'description' field in 'Quill' section cannot be empty".into());
694 }
695 let description = description.to_string();
696
697 let version_val = quill_section
699 .get("version")
700 .ok_or("Missing required 'version' field in 'Quill' section")?;
701
702 let version = if let Some(s) = version_val.as_str() {
704 s.to_string()
705 } else if let Some(n) = version_val.as_f64() {
706 n.to_string()
707 } else {
708 return Err("Invalid 'version' field format".into());
709 };
710
711 use std::str::FromStr;
713 crate::version::Version::from_str(&version)
714 .map_err(|e| format!("Invalid version '{}': {}", version, e))?;
715
716 let author = quill_section
717 .get("author")
718 .and_then(|v| v.as_str())
719 .map(|s| s.to_string())
720 .unwrap_or_else(|| "Unknown".to_string()); let example_file = quill_section
723 .get("example_file")
724 .and_then(|v| v.as_str())
725 .map(|s| s.to_string());
726
727 let plate_file = quill_section
728 .get("plate_file")
729 .and_then(|v| v.as_str())
730 .map(|s| s.to_string());
731
732 let ui_section: Option<UiContainerSchema> = quill_section
733 .get("ui")
734 .cloned()
735 .and_then(|v| serde_json::from_value(v).ok());
736
737 let mut metadata = HashMap::new();
739 if let Some(table) = quill_section.as_object() {
740 for (key, value) in table {
741 if key != "name"
743 && key != "backend"
744 && key != "description"
745 && key != "version"
746 && key != "author"
747 && key != "example_file"
748 && key != "plate_file"
749 && key != "ui"
750 {
751 metadata.insert(key.clone(), QuillValue::from_json(value.clone()));
752 }
753 }
754 }
755
756 let mut typst_config = HashMap::new();
758 if let Some(typst_val) = quill_yaml_val.get("typst") {
759 if let Some(table) = typst_val.as_object() {
760 for (key, value) in table {
761 typst_config.insert(key.clone(), QuillValue::from_json(value.clone()));
762 }
763 }
764 }
765
766 let fields = if let Some(fields_val) = quill_yaml_val.get("fields") {
768 if let Some(fields_map) = fields_val.as_object() {
769 let field_order: Vec<String> = fields_map.keys().cloned().collect();
771 Self::parse_fields_with_order(fields_map, &field_order, "field schema")
772 } else {
773 HashMap::new()
774 }
775 } else {
776 HashMap::new()
777 };
778
779 let mut cards: HashMap<String, CardSchema> = HashMap::new();
781 if let Some(cards_val) = quill_yaml_val.get("cards") {
782 let cards_table = cards_val
783 .as_object()
784 .ok_or("'cards' section must be an object")?;
785
786 for (card_name, card_value) in cards_table {
787 let card_def: CardSchemaDef = serde_json::from_value(card_value.clone())
789 .map_err(|e| format!("Failed to parse card '{}': {}", card_name, e))?;
790
791 let card_fields = if let Some(card_fields_table) =
793 card_value.get("fields").and_then(|v| v.as_object())
794 {
795 let card_field_order: Vec<String> = card_fields_table.keys().cloned().collect();
796
797 Self::parse_fields_with_order(
798 card_fields_table,
799 &card_field_order,
800 &format!("card '{}' field", card_name),
801 )
802 } else if let Some(_toml_fields) = &card_def.fields {
803 HashMap::new()
804 } else {
805 HashMap::new()
806 };
807
808 let card_schema = CardSchema {
809 name: card_name.clone(),
810 title: card_def.title,
811 description: card_def.description,
812 fields: card_fields,
813 ui: card_def.ui,
814 };
815
816 cards.insert(card_name.clone(), card_schema);
817 }
818 }
819
820 let document = CardSchema {
822 name: name.clone(),
823 title: Some(name),
824 description: Some(description),
825 fields,
826 ui: ui_section,
827 };
828
829 Ok(QuillConfig {
830 document,
831 backend,
832 version,
833 author,
834 example_file,
835 plate_file,
836 cards,
837 metadata,
838 typst_config,
839 })
840 }
841}
842
843impl Quill {
844 pub fn from_path<P: AsRef<std::path::Path>>(
846 path: P,
847 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
848 use std::fs;
849
850 let path = path.as_ref();
851
852 let quillignore_path = path.join(".quillignore");
854 let ignore = if quillignore_path.exists() {
855 let ignore_content = fs::read_to_string(&quillignore_path)
856 .map_err(|e| format!("Failed to read .quillignore: {}", e))?;
857 QuillIgnore::from_content(&ignore_content)
858 } else {
859 QuillIgnore::new(vec![
861 ".git/".to_string(),
862 ".gitignore".to_string(),
863 ".quillignore".to_string(),
864 "target/".to_string(),
865 "node_modules/".to_string(),
866 ])
867 };
868
869 let root = Self::load_directory_as_tree(path, path, &ignore)?;
871
872 Self::from_tree(root)
874 }
875
876 pub fn from_tree(root: FileTreeNode) -> Result<Self, Box<dyn StdError + Send + Sync>> {
892 let quill_yaml_bytes = root
894 .get_file("Quill.yaml")
895 .ok_or("Quill.yaml not found in file tree")?;
896
897 let quill_yaml_content = String::from_utf8(quill_yaml_bytes.to_vec())
898 .map_err(|e| format!("Quill.yaml is not valid UTF-8: {}", e))?;
899
900 let config = QuillConfig::from_yaml(&quill_yaml_content)?;
902
903 Self::from_config(config, root)
905 }
906
907 fn from_config(
924 config: QuillConfig,
925 root: FileTreeNode,
926 ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
927 let mut metadata = config.metadata.clone();
929
930 metadata.insert(
932 "backend".to_string(),
933 QuillValue::from_json(serde_json::Value::String(config.backend.clone())),
934 );
935
936 metadata.insert(
937 "description".to_string(),
938 QuillValue::from_json(serde_json::Value::String(
939 config.document.description.clone().unwrap_or_default(),
940 )),
941 );
942
943 metadata.insert(
945 "author".to_string(),
946 QuillValue::from_json(serde_json::Value::String(config.author.clone())),
947 );
948
949 metadata.insert(
951 "version".to_string(),
952 QuillValue::from_json(serde_json::Value::String(config.version.clone())),
953 );
954
955 for (key, value) in &config.typst_config {
957 metadata.insert(format!("typst_{}", key), value.clone());
958 }
959
960 let schema = crate::schema::build_schema(&config.document, &config.cards)
963 .map_err(|e| format!("Failed to build JSON schema from field schemas: {}", e))?;
964
965 let plate_content: Option<String> = if let Some(ref plate_file_name) = config.plate_file {
967 let plate_bytes = root.get_file(plate_file_name).ok_or_else(|| {
968 format!("Plate file '{}' not found in file tree", plate_file_name)
969 })?;
970
971 let content = String::from_utf8(plate_bytes.to_vec()).map_err(|e| {
972 format!("Plate file '{}' is not valid UTF-8: {}", plate_file_name, e)
973 })?;
974 Some(content)
975 } else {
976 None
978 };
979
980 let example_content = if let Some(ref example_file_name) = config.example_file {
983 root.get_file(example_file_name).and_then(|bytes| {
984 String::from_utf8(bytes.to_vec())
985 .map_err(|e| {
986 eprintln!(
987 "Warning: Example file '{}' is not valid UTF-8: {}",
988 example_file_name, e
989 );
990 e
991 })
992 .ok()
993 })
994 } else if root.file_exists("example.md") {
995 root.get_file("example.md").and_then(|bytes| {
997 String::from_utf8(bytes.to_vec())
998 .map_err(|e| {
999 eprintln!(
1000 "Warning: Default example file 'example.md' is not valid UTF-8: {}",
1001 e
1002 );
1003 e
1004 })
1005 .ok()
1006 })
1007 } else {
1008 None
1009 };
1010
1011 let defaults = crate::schema::extract_defaults_from_schema(&schema);
1013 let examples = crate::schema::extract_examples_from_schema(&schema);
1014
1015 let quill = Quill {
1016 metadata,
1017 name: config.document.name,
1018 backend: config.backend,
1019 plate: plate_content,
1020 example: example_content,
1021 schema,
1022 defaults,
1023 examples,
1024 files: root,
1025 };
1026
1027 Ok(quill)
1028 }
1029
1030 pub fn from_json(json_str: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
1037 use serde_json::Value as JsonValue;
1038
1039 let json: JsonValue =
1040 serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {}", e))?;
1041
1042 let obj = json.as_object().ok_or("Root must be an object")?;
1043
1044 let files_obj = obj
1046 .get("files")
1047 .and_then(|v| v.as_object())
1048 .ok_or("Missing or invalid 'files' key")?;
1049
1050 let mut root_files = HashMap::new();
1052 for (key, value) in files_obj {
1053 root_files.insert(key.clone(), FileTreeNode::from_json_value(value)?);
1054 }
1055
1056 let root = FileTreeNode::Directory { files: root_files };
1057
1058 Self::from_tree(root)
1060 }
1061
1062 fn load_directory_as_tree(
1064 current_dir: &Path,
1065 base_dir: &Path,
1066 ignore: &QuillIgnore,
1067 ) -> Result<FileTreeNode, Box<dyn StdError + Send + Sync>> {
1068 use std::fs;
1069
1070 if !current_dir.exists() {
1071 return Ok(FileTreeNode::Directory {
1072 files: HashMap::new(),
1073 });
1074 }
1075
1076 let mut files = HashMap::new();
1077
1078 for entry in fs::read_dir(current_dir)? {
1079 let entry = entry?;
1080 let path = entry.path();
1081 let relative_path = path
1082 .strip_prefix(base_dir)
1083 .map_err(|e| format!("Failed to get relative path: {}", e))?
1084 .to_path_buf();
1085
1086 if ignore.is_ignored(&relative_path) {
1088 continue;
1089 }
1090
1091 let filename = path
1093 .file_name()
1094 .and_then(|n| n.to_str())
1095 .ok_or_else(|| format!("Invalid filename: {}", path.display()))?
1096 .to_string();
1097
1098 if path.is_file() {
1099 let contents = fs::read(&path)
1100 .map_err(|e| format!("Failed to read file '{}': {}", path.display(), e))?;
1101
1102 files.insert(filename, FileTreeNode::File { contents });
1103 } else if path.is_dir() {
1104 let subdir_tree = Self::load_directory_as_tree(&path, base_dir, ignore)?;
1106 files.insert(filename, subdir_tree);
1107 }
1108 }
1109
1110 Ok(FileTreeNode::Directory { files })
1111 }
1112
1113 pub fn typst_packages(&self) -> Vec<String> {
1115 self.metadata
1116 .get("typst_packages")
1117 .and_then(|v| v.as_array())
1118 .map(|arr| {
1119 arr.iter()
1120 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1121 .collect()
1122 })
1123 .unwrap_or_default()
1124 }
1125
1126 pub fn extract_defaults(&self) -> &HashMap<String, QuillValue> {
1134 &self.defaults
1135 }
1136
1137 pub fn extract_examples(&self) -> &HashMap<String, Vec<QuillValue>> {
1142 &self.examples
1143 }
1144
1145 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
1147 self.files.get_file(path)
1148 }
1149
1150 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
1152 self.files.file_exists(path)
1153 }
1154
1155 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
1157 self.files.dir_exists(path)
1158 }
1159
1160 pub fn list_files<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
1162 self.files.list_files(path)
1163 }
1164
1165 pub fn list_subdirectories<P: AsRef<Path>>(&self, path: P) -> Vec<String> {
1167 self.files.list_subdirectories(path)
1168 }
1169
1170 pub fn list_directory<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
1172 let dir_path = dir_path.as_ref();
1173 let filenames = self.files.list_files(dir_path);
1174
1175 filenames
1177 .iter()
1178 .map(|name| {
1179 if dir_path == Path::new("") {
1180 PathBuf::from(name)
1181 } else {
1182 dir_path.join(name)
1183 }
1184 })
1185 .collect()
1186 }
1187
1188 pub fn list_directories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<PathBuf> {
1190 let dir_path = dir_path.as_ref();
1191 let subdirs = self.files.list_subdirectories(dir_path);
1192
1193 subdirs
1195 .iter()
1196 .map(|name| {
1197 if dir_path == Path::new("") {
1198 PathBuf::from(name)
1199 } else {
1200 dir_path.join(name)
1201 }
1202 })
1203 .collect()
1204 }
1205
1206 pub fn find_files<P: AsRef<Path>>(&self, pattern: P) -> Vec<PathBuf> {
1208 let pattern_str = pattern.as_ref().to_string_lossy();
1209 let mut matches = Vec::new();
1210
1211 let glob_pattern = match glob::Pattern::new(&pattern_str) {
1213 Ok(pat) => pat,
1214 Err(_) => return matches, };
1216
1217 Self::find_files_recursive(&self.files, Path::new(""), &glob_pattern, &mut matches);
1219
1220 matches.sort();
1221 matches
1222 }
1223
1224 fn find_files_recursive(
1226 node: &FileTreeNode,
1227 current_path: &Path,
1228 pattern: &glob::Pattern,
1229 matches: &mut Vec<PathBuf>,
1230 ) {
1231 match node {
1232 FileTreeNode::File { .. } => {
1233 let path_str = current_path.to_string_lossy();
1234 if pattern.matches(&path_str) {
1235 matches.push(current_path.to_path_buf());
1236 }
1237 }
1238 FileTreeNode::Directory { files } => {
1239 for (name, child_node) in files {
1240 let child_path = if current_path == Path::new("") {
1241 PathBuf::from(name)
1242 } else {
1243 current_path.join(name)
1244 };
1245 Self::find_files_recursive(child_node, &child_path, pattern, matches);
1246 }
1247 }
1248 }
1249 }
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use std::fs;
1256 use tempfile::TempDir;
1257
1258 #[test]
1259 fn test_quillignore_parsing() {
1260 let ignore_content = r#"
1261# This is a comment
1262*.tmp
1263target/
1264node_modules/
1265.git/
1266"#;
1267 let ignore = QuillIgnore::from_content(ignore_content);
1268 assert_eq!(ignore.patterns.len(), 4);
1269 assert!(ignore.patterns.contains(&"*.tmp".to_string()));
1270 assert!(ignore.patterns.contains(&"target/".to_string()));
1271 }
1272
1273 #[test]
1274 fn test_quillignore_matching() {
1275 let ignore = QuillIgnore::new(vec![
1276 "*.tmp".to_string(),
1277 "target/".to_string(),
1278 "node_modules/".to_string(),
1279 ".git/".to_string(),
1280 ]);
1281
1282 assert!(ignore.is_ignored("test.tmp"));
1284 assert!(ignore.is_ignored("path/to/file.tmp"));
1285 assert!(!ignore.is_ignored("test.txt"));
1286
1287 assert!(ignore.is_ignored("target"));
1289 assert!(ignore.is_ignored("target/debug"));
1290 assert!(ignore.is_ignored("target/debug/deps"));
1291 assert!(!ignore.is_ignored("src/target.rs"));
1292
1293 assert!(ignore.is_ignored("node_modules"));
1294 assert!(ignore.is_ignored("node_modules/package"));
1295 assert!(!ignore.is_ignored("my_node_modules"));
1296 }
1297
1298 #[test]
1299 fn test_in_memory_file_system() {
1300 let temp_dir = TempDir::new().unwrap();
1301 let quill_dir = temp_dir.path();
1302
1303 fs::write(
1305 quill_dir.join("Quill.yaml"),
1306 "Quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
1307 )
1308 .unwrap();
1309 fs::write(quill_dir.join("plate.typ"), "test plate").unwrap();
1310
1311 let assets_dir = quill_dir.join("assets");
1312 fs::create_dir_all(&assets_dir).unwrap();
1313 fs::write(assets_dir.join("test.txt"), "asset content").unwrap();
1314
1315 let packages_dir = quill_dir.join("packages");
1316 fs::create_dir_all(&packages_dir).unwrap();
1317 fs::write(packages_dir.join("package.typ"), "package content").unwrap();
1318
1319 let quill = Quill::from_path(quill_dir).unwrap();
1321
1322 assert!(quill.file_exists("plate.typ"));
1324 assert!(quill.file_exists("assets/test.txt"));
1325 assert!(quill.file_exists("packages/package.typ"));
1326 assert!(!quill.file_exists("nonexistent.txt"));
1327
1328 let asset_content = quill.get_file("assets/test.txt").unwrap();
1330 assert_eq!(asset_content, b"asset content");
1331
1332 let asset_files = quill.list_directory("assets");
1334 assert_eq!(asset_files.len(), 1);
1335 assert!(asset_files.contains(&PathBuf::from("assets/test.txt")));
1336 }
1337
1338 #[test]
1339 fn test_quillignore_integration() {
1340 let temp_dir = TempDir::new().unwrap();
1341 let quill_dir = temp_dir.path();
1342
1343 fs::write(quill_dir.join(".quillignore"), "*.tmp\ntarget/\n").unwrap();
1345
1346 fs::write(
1348 quill_dir.join("Quill.yaml"),
1349 "Quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
1350 )
1351 .unwrap();
1352 fs::write(quill_dir.join("plate.typ"), "test template").unwrap();
1353 fs::write(quill_dir.join("should_ignore.tmp"), "ignored").unwrap();
1354
1355 let target_dir = quill_dir.join("target");
1356 fs::create_dir_all(&target_dir).unwrap();
1357 fs::write(target_dir.join("debug.txt"), "also ignored").unwrap();
1358
1359 let quill = Quill::from_path(quill_dir).unwrap();
1361
1362 assert!(quill.file_exists("plate.typ"));
1364 assert!(!quill.file_exists("should_ignore.tmp"));
1365 assert!(!quill.file_exists("target/debug.txt"));
1366 }
1367
1368 #[test]
1369 fn test_find_files_pattern() {
1370 let temp_dir = TempDir::new().unwrap();
1371 let quill_dir = temp_dir.path();
1372
1373 fs::write(
1375 quill_dir.join("Quill.yaml"),
1376 "Quill:\n name: \"test\"\n version: \"1.0\"\n backend: \"typst\"\n plate_file: \"plate.typ\"\n description: \"Test quill\"",
1377 )
1378 .unwrap();
1379 fs::write(quill_dir.join("plate.typ"), "template").unwrap();
1380
1381 let assets_dir = quill_dir.join("assets");
1382 fs::create_dir_all(&assets_dir).unwrap();
1383 fs::write(assets_dir.join("image.png"), "png data").unwrap();
1384 fs::write(assets_dir.join("data.json"), "json data").unwrap();
1385
1386 let fonts_dir = assets_dir.join("fonts");
1387 fs::create_dir_all(&fonts_dir).unwrap();
1388 fs::write(fonts_dir.join("font.ttf"), "font data").unwrap();
1389
1390 let quill = Quill::from_path(quill_dir).unwrap();
1392
1393 let all_assets = quill.find_files("assets/*");
1395 assert!(all_assets.len() >= 3); let typ_files = quill.find_files("*.typ");
1398 assert_eq!(typ_files.len(), 1);
1399 assert!(typ_files.contains(&PathBuf::from("plate.typ")));
1400 }
1401
1402 #[test]
1403 fn test_new_standardized_yaml_format() {
1404 let temp_dir = TempDir::new().unwrap();
1405 let quill_dir = temp_dir.path();
1406
1407 let yaml_content = r#"
1409Quill:
1410 name: my-custom-quill
1411 version: "1.0"
1412 backend: typst
1413 plate_file: custom_plate.typ
1414 description: Test quill with new format
1415 author: Test Author
1416"#;
1417 fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
1418 fs::write(
1419 quill_dir.join("custom_plate.typ"),
1420 "= Custom Template\n\nThis is a custom template.",
1421 )
1422 .unwrap();
1423
1424 let quill = Quill::from_path(quill_dir).unwrap();
1426
1427 assert_eq!(quill.name, "my-custom-quill");
1429
1430 assert!(quill.metadata.contains_key("backend"));
1432 if let Some(backend_val) = quill.metadata.get("backend") {
1433 if let Some(backend_str) = backend_val.as_str() {
1434 assert_eq!(backend_str, "typst");
1435 } else {
1436 panic!("Backend value is not a string");
1437 }
1438 }
1439
1440 assert!(quill.metadata.contains_key("description"));
1442 assert!(quill.metadata.contains_key("author"));
1443 assert!(quill.metadata.contains_key("version")); if let Some(version_val) = quill.metadata.get("version") {
1445 if let Some(version_str) = version_val.as_str() {
1446 assert_eq!(version_str, "1.0");
1447 }
1448 }
1449
1450 assert!(quill.plate.unwrap().contains("Custom Template"));
1452 }
1453
1454 #[test]
1455 fn test_typst_packages_parsing() {
1456 let temp_dir = TempDir::new().unwrap();
1457 let quill_dir = temp_dir.path();
1458
1459 let yaml_content = r#"
1460Quill:
1461 name: "test-quill"
1462 version: "1.0"
1463 backend: "typst"
1464 plate_file: "plate.typ"
1465 description: "Test quill for packages"
1466
1467typst:
1468 packages:
1469 - "@preview/bubble:0.2.2"
1470 - "@preview/example:1.0.0"
1471"#;
1472
1473 fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
1474 fs::write(quill_dir.join("plate.typ"), "test").unwrap();
1475
1476 let quill = Quill::from_path(quill_dir).unwrap();
1477 let packages = quill.typst_packages();
1478
1479 assert_eq!(packages.len(), 2);
1480 assert_eq!(packages[0], "@preview/bubble:0.2.2");
1481 assert_eq!(packages[1], "@preview/example:1.0.0");
1482 }
1483
1484 #[test]
1485 fn test_template_loading() {
1486 let temp_dir = TempDir::new().unwrap();
1487 let quill_dir = temp_dir.path();
1488
1489 let yaml_content = r#"Quill:
1491 name: "test-with-template"
1492 version: "1.0"
1493 backend: "typst"
1494 plate_file: "plate.typ"
1495 example_file: "example.md"
1496 description: "Test quill with template"
1497"#;
1498 fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
1499 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1500 fs::write(
1501 quill_dir.join("example.md"),
1502 "---\ntitle: Test\n---\n\nThis is a test template.",
1503 )
1504 .unwrap();
1505
1506 let quill = Quill::from_path(quill_dir).unwrap();
1508
1509 assert!(quill.example.is_some());
1511 let example = quill.example.unwrap();
1512 assert!(example.contains("title: Test"));
1513 assert!(example.contains("This is a test template"));
1514
1515 assert_eq!(quill.plate.unwrap(), "plate content");
1517 }
1518
1519 #[test]
1520 fn test_template_smart_default() {
1521 let temp_dir = TempDir::new().unwrap();
1522 let quill_dir = temp_dir.path();
1523
1524 let yaml_content = r#"Quill:
1526 name: "test-smart-default"
1527 version: "1.0"
1528 backend: "typst"
1529 plate_file: "plate.typ"
1530 description: "Test quill with smart default"
1531"#;
1532 fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
1533 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1534 fs::write(
1536 quill_dir.join("example.md"),
1537 "---\ntitle: Smart Default\n---\n\nPicked up automatically.",
1538 )
1539 .unwrap();
1540
1541 let quill = Quill::from_path(quill_dir).unwrap();
1543
1544 assert!(quill.example.is_some());
1546 let example = quill.example.unwrap();
1547 assert!(example.contains("title: Smart Default"));
1548 assert!(example.contains("Picked up automatically"));
1549 }
1550
1551 #[test]
1552 fn test_template_optional() {
1553 let temp_dir = TempDir::new().unwrap();
1554 let quill_dir = temp_dir.path();
1555
1556 let yaml_content = r#"Quill:
1558 name: "test-without-template"
1559 version: "1.0"
1560 backend: "typst"
1561 plate_file: "plate.typ"
1562 description: "Test quill without template"
1563"#;
1564 fs::write(quill_dir.join("Quill.yaml"), yaml_content).unwrap();
1565 fs::write(quill_dir.join("plate.typ"), "plate content").unwrap();
1566
1567 let quill = Quill::from_path(quill_dir).unwrap();
1569
1570 assert_eq!(quill.example, None);
1572
1573 assert_eq!(quill.plate.unwrap(), "plate content");
1575 }
1576
1577 #[test]
1578 fn test_from_tree() {
1579 let mut root_files = HashMap::new();
1581
1582 let quill_yaml = r#"Quill:
1584 name: "test-from-tree"
1585 version: "1.0"
1586 backend: "typst"
1587 plate_file: "plate.typ"
1588 description: "A test quill from tree"
1589"#;
1590 root_files.insert(
1591 "Quill.yaml".to_string(),
1592 FileTreeNode::File {
1593 contents: quill_yaml.as_bytes().to_vec(),
1594 },
1595 );
1596
1597 let plate_content = "= Test Template\n\nThis is a test.";
1599 root_files.insert(
1600 "plate.typ".to_string(),
1601 FileTreeNode::File {
1602 contents: plate_content.as_bytes().to_vec(),
1603 },
1604 );
1605
1606 let root = FileTreeNode::Directory { files: root_files };
1607
1608 let quill = Quill::from_tree(root).unwrap();
1610
1611 assert_eq!(quill.name, "test-from-tree");
1613 assert_eq!(quill.plate.unwrap(), plate_content);
1614 assert!(quill.metadata.contains_key("backend"));
1615 assert!(quill.metadata.contains_key("description"));
1616 }
1617
1618 #[test]
1619 fn test_from_tree_with_template() {
1620 let mut root_files = HashMap::new();
1621
1622 let quill_yaml = r#"
1625Quill:
1626 name: test-tree-template
1627 version: "1.0"
1628 backend: typst
1629 plate_file: plate.typ
1630 example_file: template.md
1631 description: Test tree with template
1632"#;
1633 root_files.insert(
1634 "Quill.yaml".to_string(),
1635 FileTreeNode::File {
1636 contents: quill_yaml.as_bytes().to_vec(),
1637 },
1638 );
1639
1640 root_files.insert(
1642 "plate.typ".to_string(),
1643 FileTreeNode::File {
1644 contents: b"plate content".to_vec(),
1645 },
1646 );
1647
1648 let template_content = "# {{ title }}\n\n{{ body }}";
1650 root_files.insert(
1651 "template.md".to_string(),
1652 FileTreeNode::File {
1653 contents: template_content.as_bytes().to_vec(),
1654 },
1655 );
1656
1657 let root = FileTreeNode::Directory { files: root_files };
1658
1659 let quill = Quill::from_tree(root).unwrap();
1661
1662 assert_eq!(quill.example, Some(template_content.to_string()));
1664 }
1665
1666 #[test]
1667 fn test_from_json() {
1668 let json_str = r#"{
1670 "metadata": {
1671 "name": "test_from_json"
1672 },
1673 "files": {
1674 "Quill.yaml": {
1675 "contents": "Quill:\n name: test_from_json\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Test quill from JSON\n"
1676 },
1677 "plate.typ": {
1678 "contents": "= Test Plate\n\nThis is test content."
1679 }
1680 }
1681 }"#;
1682
1683 let quill = Quill::from_json(json_str).unwrap();
1685
1686 assert_eq!(quill.name, "test_from_json");
1688 assert!(quill.plate.unwrap().contains("Test Plate"));
1689 assert!(quill.metadata.contains_key("backend"));
1690 }
1691
1692 #[test]
1693 fn test_from_json_with_byte_array() {
1694 let json_str = r#"{
1696 "files": {
1697 "Quill.yaml": {
1698 "contents": "Quill:\n name: test\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Test quill\n"
1699 },
1700 "plate.typ": {
1701 "contents": "test plate"
1702 }
1703 }
1704 }"#;
1705
1706 let quill = Quill::from_json(json_str).unwrap();
1708
1709 assert_eq!(quill.name, "test");
1711 assert_eq!(quill.plate.unwrap(), "test plate");
1712 }
1713
1714 #[test]
1715 fn test_from_json_missing_files() {
1716 let json_str = r#"{
1718 "metadata": {
1719 "name": "test"
1720 }
1721 }"#;
1722
1723 let result = Quill::from_json(json_str);
1724 assert!(result.is_err());
1725 assert!(result.unwrap_err().to_string().contains("files"));
1727 }
1728
1729 #[test]
1730 fn test_from_json_tree_structure() {
1731 let json_str = r#"{
1733 "files": {
1734 "Quill.yaml": {
1735 "contents": "Quill:\n name: test_tree_json\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Test tree JSON\n"
1736 },
1737 "plate.typ": {
1738 "contents": "= Test Plate\n\nTree structure content."
1739 }
1740 }
1741 }"#;
1742
1743 let quill = Quill::from_json(json_str).unwrap();
1744
1745 assert_eq!(quill.name, "test_tree_json");
1746 assert!(quill.plate.unwrap().contains("Tree structure content"));
1747 assert!(quill.metadata.contains_key("backend"));
1748 }
1749
1750 #[test]
1751 fn test_from_json_nested_tree_structure() {
1752 let json_str = r#"{
1754 "files": {
1755 "Quill.yaml": {
1756 "contents": "Quill:\n name: nested_test\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Nested test\n"
1757 },
1758 "plate.typ": {
1759 "contents": "plate"
1760 },
1761 "src": {
1762 "main.rs": {
1763 "contents": "fn main() {}"
1764 },
1765 "lib.rs": {
1766 "contents": "// lib"
1767 }
1768 }
1769 }
1770 }"#;
1771
1772 let quill = Quill::from_json(json_str).unwrap();
1773
1774 assert_eq!(quill.name, "nested_test");
1775 assert!(quill.file_exists("src/main.rs"));
1777 assert!(quill.file_exists("src/lib.rs"));
1778
1779 let main_rs = quill.get_file("src/main.rs").unwrap();
1780 assert_eq!(main_rs, b"fn main() {}");
1781 }
1782
1783 #[test]
1784 fn test_from_tree_structure_direct() {
1785 let mut root_files = HashMap::new();
1787
1788 root_files.insert(
1789 "Quill.yaml".to_string(),
1790 FileTreeNode::File {
1791 contents:
1792 b"Quill:\n name: direct_tree\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Direct tree test\n"
1793 .to_vec(),
1794 },
1795 );
1796
1797 root_files.insert(
1798 "plate.typ".to_string(),
1799 FileTreeNode::File {
1800 contents: b"plate content".to_vec(),
1801 },
1802 );
1803
1804 let mut src_files = HashMap::new();
1806 src_files.insert(
1807 "main.rs".to_string(),
1808 FileTreeNode::File {
1809 contents: b"fn main() {}".to_vec(),
1810 },
1811 );
1812
1813 root_files.insert(
1814 "src".to_string(),
1815 FileTreeNode::Directory { files: src_files },
1816 );
1817
1818 let root = FileTreeNode::Directory { files: root_files };
1819
1820 let quill = Quill::from_tree(root).unwrap();
1821
1822 assert_eq!(quill.name, "direct_tree");
1823 assert!(quill.file_exists("src/main.rs"));
1824 assert!(quill.file_exists("plate.typ"));
1825 }
1826
1827 #[test]
1828 fn test_from_json_with_metadata_override() {
1829 let json_str = r#"{
1831 "metadata": {
1832 "name": "override_name"
1833 },
1834 "files": {
1835 "Quill.yaml": {
1836 "contents": "Quill:\n name: toml_name\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: TOML name test\n"
1837 },
1838 "plate.typ": {
1839 "contents": "= plate"
1840 }
1841 }
1842 }"#;
1843
1844 let quill = Quill::from_json(json_str).unwrap();
1845 assert_eq!(quill.name, "toml_name");
1848 }
1849
1850 #[test]
1851 fn test_from_json_empty_directory() {
1852 let json_str = r#"{
1854 "files": {
1855 "Quill.yaml": {
1856 "contents": "Quill:\n name: empty_dir_test\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Empty directory test\n"
1857 },
1858 "plate.typ": {
1859 "contents": "plate"
1860 },
1861 "empty_dir": {}
1862 }
1863 }"#;
1864
1865 let quill = Quill::from_json(json_str).unwrap();
1866 assert_eq!(quill.name, "empty_dir_test");
1867 assert!(quill.dir_exists("empty_dir"));
1868 assert!(!quill.file_exists("empty_dir"));
1869 }
1870
1871 #[test]
1872 fn test_dir_exists_and_list_apis() {
1873 let mut root_files = HashMap::new();
1874
1875 root_files.insert(
1877 "Quill.yaml".to_string(),
1878 FileTreeNode::File {
1879 contents: b"Quill:\n name: test\n version: \"1.0\"\n backend: typst\n plate_file: plate.typ\n description: Test quill\n"
1880 .to_vec(),
1881 },
1882 );
1883
1884 root_files.insert(
1886 "plate.typ".to_string(),
1887 FileTreeNode::File {
1888 contents: b"plate content".to_vec(),
1889 },
1890 );
1891
1892 let mut assets_files = HashMap::new();
1894 assets_files.insert(
1895 "logo.png".to_string(),
1896 FileTreeNode::File {
1897 contents: vec![137, 80, 78, 71],
1898 },
1899 );
1900 assets_files.insert(
1901 "icon.svg".to_string(),
1902 FileTreeNode::File {
1903 contents: b"<svg></svg>".to_vec(),
1904 },
1905 );
1906
1907 let mut fonts_files = HashMap::new();
1909 fonts_files.insert(
1910 "font.ttf".to_string(),
1911 FileTreeNode::File {
1912 contents: b"font data".to_vec(),
1913 },
1914 );
1915 assets_files.insert(
1916 "fonts".to_string(),
1917 FileTreeNode::Directory { files: fonts_files },
1918 );
1919
1920 root_files.insert(
1921 "assets".to_string(),
1922 FileTreeNode::Directory {
1923 files: assets_files,
1924 },
1925 );
1926
1927 root_files.insert(
1929 "empty".to_string(),
1930 FileTreeNode::Directory {
1931 files: HashMap::new(),
1932 },
1933 );
1934
1935 let root = FileTreeNode::Directory { files: root_files };
1936 let quill = Quill::from_tree(root).unwrap();
1937
1938 assert!(quill.dir_exists("assets"));
1940 assert!(quill.dir_exists("assets/fonts"));
1941 assert!(quill.dir_exists("empty"));
1942 assert!(!quill.dir_exists("nonexistent"));
1943 assert!(!quill.dir_exists("plate.typ")); assert!(quill.file_exists("plate.typ"));
1947 assert!(quill.file_exists("assets/logo.png"));
1948 assert!(quill.file_exists("assets/fonts/font.ttf"));
1949 assert!(!quill.file_exists("assets")); let root_files_list = quill.list_files("");
1953 assert_eq!(root_files_list.len(), 2); assert!(root_files_list.contains(&"Quill.yaml".to_string()));
1955 assert!(root_files_list.contains(&"plate.typ".to_string()));
1956
1957 let assets_files_list = quill.list_files("assets");
1958 assert_eq!(assets_files_list.len(), 2); assert!(assets_files_list.contains(&"logo.png".to_string()));
1960 assert!(assets_files_list.contains(&"icon.svg".to_string()));
1961
1962 let root_subdirs = quill.list_subdirectories("");
1964 assert_eq!(root_subdirs.len(), 2); assert!(root_subdirs.contains(&"assets".to_string()));
1966 assert!(root_subdirs.contains(&"empty".to_string()));
1967
1968 let assets_subdirs = quill.list_subdirectories("assets");
1969 assert_eq!(assets_subdirs.len(), 1); assert!(assets_subdirs.contains(&"fonts".to_string()));
1971
1972 let empty_subdirs = quill.list_subdirectories("empty");
1973 assert_eq!(empty_subdirs.len(), 0);
1974 }
1975
1976 #[test]
1977 fn test_field_schemas_parsing() {
1978 let mut root_files = HashMap::new();
1979
1980 let quill_yaml = r#"Quill:
1982 name: "taro"
1983 version: "1.0"
1984 backend: "typst"
1985 plate_file: "plate.typ"
1986 example_file: "taro.md"
1987 description: "Test template for field schemas"
1988
1989fields:
1990 author:
1991 type: "string"
1992 description: "Author of document"
1993 ice_cream:
1994 type: "string"
1995 description: "favorite ice cream flavor"
1996 title:
1997 type: "string"
1998 description: "title of document"
1999"#;
2000 root_files.insert(
2001 "Quill.yaml".to_string(),
2002 FileTreeNode::File {
2003 contents: quill_yaml.as_bytes().to_vec(),
2004 },
2005 );
2006
2007 let plate_content = "= Test Template\n\nThis is a test.";
2009 root_files.insert(
2010 "plate.typ".to_string(),
2011 FileTreeNode::File {
2012 contents: plate_content.as_bytes().to_vec(),
2013 },
2014 );
2015
2016 root_files.insert(
2018 "taro.md".to_string(),
2019 FileTreeNode::File {
2020 contents: b"# Template".to_vec(),
2021 },
2022 );
2023
2024 let root = FileTreeNode::Directory { files: root_files };
2025
2026 let quill = Quill::from_tree(root).unwrap();
2028
2029 assert_eq!(quill.schema["properties"].as_object().unwrap().len(), 4);
2031 assert!(quill.schema["properties"]
2032 .as_object()
2033 .unwrap()
2034 .contains_key("author"));
2035 assert!(quill.schema["properties"]
2036 .as_object()
2037 .unwrap()
2038 .contains_key("ice_cream"));
2039 assert!(quill.schema["properties"]
2040 .as_object()
2041 .unwrap()
2042 .contains_key("title"));
2043 assert!(quill.schema["properties"]
2044 .as_object()
2045 .unwrap()
2046 .contains_key("BODY"));
2047
2048 let author_schema = quill.schema["properties"]["author"].as_object().unwrap();
2050 assert_eq!(author_schema["description"], "Author of document");
2051
2052 let ice_cream_schema = quill.schema["properties"]["ice_cream"].as_object().unwrap();
2054 assert_eq!(ice_cream_schema["description"], "favorite ice cream flavor");
2055
2056 let title_schema = quill.schema["properties"]["title"].as_object().unwrap();
2058 assert_eq!(title_schema["description"], "title of document");
2059 }
2060
2061 #[test]
2062 fn test_field_schema_struct() {
2063 let schema1 = FieldSchema::new(
2065 "test_name".to_string(),
2066 FieldType::String,
2067 Some("Test description".to_string()),
2068 );
2069 assert_eq!(schema1.description, Some("Test description".to_string()));
2070 assert_eq!(schema1.r#type, FieldType::String);
2071 assert_eq!(schema1.examples, None);
2072 assert_eq!(schema1.default, None);
2073
2074 let yaml_str = r#"
2076description: "Full field schema"
2077type: "string"
2078examples:
2079 - "Example value"
2080default: "Default value"
2081"#;
2082 let quill_value = QuillValue::from_yaml_str(yaml_str).unwrap();
2083 let schema2 = FieldSchema::from_quill_value("test_name".to_string(), &quill_value).unwrap();
2084 assert_eq!(schema2.name, "test_name");
2085 assert_eq!(schema2.description, Some("Full field schema".to_string()));
2086 assert_eq!(schema2.r#type, FieldType::String);
2087 assert_eq!(
2088 schema2
2089 .examples
2090 .as_ref()
2091 .and_then(|v| v.as_array())
2092 .and_then(|arr| arr.first())
2093 .and_then(|v| v.as_str()),
2094 Some("Example value")
2095 );
2096 assert_eq!(
2097 schema2.default.as_ref().and_then(|v| v.as_str()),
2098 Some("Default value")
2099 );
2100 }
2101
2102 #[test]
2103 fn test_field_schema_ui_compact() {
2104 let yaml_str = r#"
2105type: "string"
2106description: "A compact field"
2107ui:
2108 compact: true
2109"#;
2110 let quill_value = QuillValue::from_yaml_str(yaml_str).unwrap();
2111 let schema =
2112 FieldSchema::from_quill_value("compact_field".to_string(), &quill_value).unwrap();
2113 assert_eq!(schema.ui.as_ref().unwrap().compact, Some(true));
2114 }
2115
2116 #[test]
2117 fn test_quill_without_plate_file() {
2118 let mut root_files = HashMap::new();
2120
2121 let quill_yaml = r#"Quill:
2123 name: "test-no-plate"
2124 version: "1.0"
2125 backend: "typst"
2126 description: "Test quill without plate file"
2127"#;
2128 root_files.insert(
2129 "Quill.yaml".to_string(),
2130 FileTreeNode::File {
2131 contents: quill_yaml.as_bytes().to_vec(),
2132 },
2133 );
2134
2135 let root = FileTreeNode::Directory { files: root_files };
2136
2137 let quill = Quill::from_tree(root).unwrap();
2139
2140 assert!(quill.plate.clone().is_none());
2142 assert_eq!(quill.name, "test-no-plate");
2143 }
2144
2145 #[test]
2146 fn test_quill_config_from_yaml() {
2147 let yaml_content = r#"
2149Quill:
2150 name: test_config
2151 version: "1.0"
2152 backend: typst
2153 description: Test configuration parsing
2154 author: Test Author
2155 plate_file: plate.typ
2156 example_file: example.md
2157
2158typst:
2159 packages:
2160 - "@preview/bubble:0.2.2"
2161
2162fields:
2163 title:
2164 description: Document title
2165 type: string
2166 author:
2167 type: string
2168 description: Document author
2169"#;
2170
2171 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2172
2173 assert_eq!(config.document.name, "test_config");
2175 assert_eq!(config.backend, "typst");
2176 assert_eq!(
2177 config.document.description,
2178 Some("Test configuration parsing".to_string())
2179 );
2180
2181 assert_eq!(config.version, "1.0");
2183 assert_eq!(config.author, "Test Author");
2184 assert_eq!(config.plate_file, Some("plate.typ".to_string()));
2185 assert_eq!(config.example_file, Some("example.md".to_string()));
2186
2187 assert!(config.typst_config.contains_key("packages"));
2189
2190 assert_eq!(config.document.fields.len(), 2);
2192 assert!(config.document.fields.contains_key("title"));
2193 assert!(config.document.fields.contains_key("author"));
2194
2195 let title_field = &config.document.fields["title"];
2196 assert_eq!(title_field.description, Some("Document title".to_string()));
2197 assert_eq!(title_field.r#type, FieldType::String);
2198 }
2199
2200 #[test]
2201 fn test_quill_config_missing_required_fields() {
2202 let yaml_missing_name = r#"
2204Quill:
2205 backend: typst
2206 description: Missing name
2207"#;
2208 let result = QuillConfig::from_yaml(yaml_missing_name);
2209 assert!(result.is_err());
2210 assert!(result
2211 .unwrap_err()
2212 .to_string()
2213 .contains("Missing required 'name'"));
2214
2215 let yaml_missing_backend = r#"
2216Quill:
2217 name: test
2218 description: Missing backend
2219"#;
2220 let result = QuillConfig::from_yaml(yaml_missing_backend);
2221 assert!(result.is_err());
2222 assert!(result
2223 .unwrap_err()
2224 .to_string()
2225 .contains("Missing required 'backend'"));
2226
2227 let yaml_missing_description = r#"
2228Quill:
2229 name: test
2230 version: "1.0"
2231 backend: typst
2232"#;
2233 let result = QuillConfig::from_yaml(yaml_missing_description);
2234 assert!(result.is_err());
2235 assert!(result
2236 .unwrap_err()
2237 .to_string()
2238 .contains("Missing required 'description'"));
2239 }
2240
2241 #[test]
2242 fn test_quill_config_empty_description() {
2243 let yaml_empty_description = r#"
2245Quill:
2246 name: test
2247 version: "1.0"
2248 backend: typst
2249 description: " "
2250"#;
2251 let result = QuillConfig::from_yaml(yaml_empty_description);
2252 assert!(result.is_err());
2253 assert!(result
2254 .unwrap_err()
2255 .to_string()
2256 .contains("description' field in 'Quill' section cannot be empty"));
2257 }
2258
2259 #[test]
2260 fn test_quill_config_missing_quill_section() {
2261 let yaml_no_section = r#"
2263fields:
2264 title:
2265 description: Title
2266"#;
2267 let result = QuillConfig::from_yaml(yaml_no_section);
2268 assert!(result.is_err());
2269 assert!(result
2270 .unwrap_err()
2271 .to_string()
2272 .contains("Missing required 'Quill' section"));
2273 }
2274
2275 #[test]
2276 fn test_quill_from_config_metadata() {
2277 let mut root_files = HashMap::new();
2279
2280 let quill_yaml = r#"
2281Quill:
2282 name: metadata-test
2283 version: "1.0"
2284 backend: typst
2285 description: Test metadata flow
2286 author: Test Author
2287 custom_field: custom_value
2288
2289typst:
2290 packages:
2291 - "@preview/bubble:0.2.2"
2292"#;
2293 root_files.insert(
2294 "Quill.yaml".to_string(),
2295 FileTreeNode::File {
2296 contents: quill_yaml.as_bytes().to_vec(),
2297 },
2298 );
2299
2300 let root = FileTreeNode::Directory { files: root_files };
2301 let quill = Quill::from_tree(root).unwrap();
2302
2303 assert!(quill.metadata.contains_key("backend"));
2305 assert!(quill.metadata.contains_key("description"));
2306 assert!(quill.metadata.contains_key("author"));
2307
2308 assert!(quill.metadata.contains_key("custom_field"));
2310 assert_eq!(
2311 quill.metadata.get("custom_field").unwrap().as_str(),
2312 Some("custom_value")
2313 );
2314
2315 assert!(quill.metadata.contains_key("typst_packages"));
2317 }
2318
2319 #[test]
2320 fn test_extract_defaults_method() {
2321 let mut root_files = HashMap::new();
2323
2324 let quill_yaml = r#"
2325Quill:
2326 name: metadata-test-yaml
2327 version: "1.0"
2328 backend: typst
2329 description: Test metadata flow
2330 author: Test Author
2331 custom_field: custom_value
2332
2333typst:
2334 packages:
2335 - "@preview/bubble:0.2.2"
2336
2337fields:
2338 author:
2339 type: string
2340 default: Anonymous
2341 status:
2342 type: string
2343 default: draft
2344 title:
2345 type: string
2346"#;
2347 root_files.insert(
2348 "Quill.yaml".to_string(),
2349 FileTreeNode::File {
2350 contents: quill_yaml.as_bytes().to_vec(),
2351 },
2352 );
2353
2354 let root = FileTreeNode::Directory { files: root_files };
2355 let quill = Quill::from_tree(root).unwrap();
2356
2357 let defaults = quill.extract_defaults();
2359
2360 assert_eq!(defaults.len(), 2);
2362 assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
2364 assert!(defaults.contains_key("status"));
2365
2366 assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
2368 assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
2369 }
2370
2371 #[test]
2372 fn test_field_order_preservation() {
2373 let yaml_content = r#"
2374Quill:
2375 name: order-test
2376 version: "1.0"
2377 backend: typst
2378 description: Test field order
2379
2380fields:
2381 first:
2382 type: string
2383 description: First field
2384 second:
2385 type: string
2386 description: Second field
2387 third:
2388 type: string
2389 description: Third field
2390 ui:
2391 group: Test Group
2392 fourth:
2393 type: string
2394 description: Fourth field
2395"#;
2396
2397 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2398
2399 let first = config.document.fields.get("first").unwrap();
2403 assert_eq!(first.ui.as_ref().unwrap().order, Some(0));
2404
2405 let second = config.document.fields.get("second").unwrap();
2406 assert_eq!(second.ui.as_ref().unwrap().order, Some(1));
2407
2408 let third = config.document.fields.get("third").unwrap();
2409 assert_eq!(third.ui.as_ref().unwrap().order, Some(2));
2410 assert_eq!(
2411 third.ui.as_ref().unwrap().group,
2412 Some("Test Group".to_string())
2413 );
2414
2415 let fourth = config.document.fields.get("fourth").unwrap();
2416 assert_eq!(fourth.ui.as_ref().unwrap().order, Some(3));
2417 }
2418
2419 #[test]
2420 fn test_quill_with_all_ui_properties() {
2421 let yaml_content = r#"
2422Quill:
2423 name: full-ui-test
2424 version: "1.0"
2425 backend: typst
2426 description: Test all UI properties
2427
2428fields:
2429 author:
2430 description: The full name of the document author
2431 type: str
2432 ui:
2433 group: Author Info
2434"#;
2435
2436 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2437
2438 let author_field = &config.document.fields["author"];
2439 let ui = author_field.ui.as_ref().unwrap();
2440 assert_eq!(ui.group, Some("Author Info".to_string()));
2441 assert_eq!(ui.order, Some(0)); }
2443 #[test]
2444 fn test_field_schema_with_title_and_description() {
2445 let yaml = r#"
2447title: "Field Title"
2448description: "Detailed field description"
2449type: "string"
2450examples:
2451 - "Example value"
2452ui:
2453 group: "Test Group"
2454"#;
2455 let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
2456 let schema = FieldSchema::from_quill_value("test_field".to_string(), &quill_value).unwrap();
2457
2458 assert_eq!(schema.title, Some("Field Title".to_string()));
2459 assert_eq!(
2460 schema.description,
2461 Some("Detailed field description".to_string())
2462 );
2463
2464 assert_eq!(
2465 schema
2466 .examples
2467 .as_ref()
2468 .and_then(|v| v.as_array())
2469 .and_then(|arr| arr.first())
2470 .and_then(|v| v.as_str()),
2471 Some("Example value")
2472 );
2473
2474 let ui = schema.ui.as_ref().unwrap();
2475 assert_eq!(ui.group, Some("Test Group".to_string()));
2476 }
2477
2478 #[test]
2479 fn test_parse_card_field_type() {
2480 let yaml = r#"
2482type: "string"
2483title: "Simple Field"
2484description: "A simple string field"
2485"#;
2486 let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
2487 let schema =
2488 FieldSchema::from_quill_value("simple_field".to_string(), &quill_value).unwrap();
2489
2490 assert_eq!(schema.name, "simple_field");
2491 assert_eq!(schema.r#type, FieldType::String);
2492 assert_eq!(schema.title, Some("Simple Field".to_string()));
2493 assert_eq!(
2494 schema.description,
2495 Some("A simple string field".to_string())
2496 );
2497 }
2498
2499 #[test]
2500 fn test_parse_card_with_fields_in_yaml() {
2501 let yaml_content = r#"
2503Quill:
2504 name: cards-fields-test
2505 version: "1.0"
2506 backend: typst
2507 description: Test [cards.X.fields.Y] syntax
2508
2509cards:
2510 endorsements:
2511 title: Endorsements
2512 description: Chain of endorsements
2513 fields:
2514 name:
2515 type: string
2516 title: Endorser Name
2517 description: Name of the endorsing official
2518 required: true
2519 org:
2520 type: string
2521 title: Organization
2522 description: Endorser's organization
2523 default: Unknown
2524"#;
2525
2526 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2527
2528 assert!(config.cards.contains_key("endorsements"));
2530 let card = config.cards.get("endorsements").unwrap();
2531
2532 assert_eq!(card.name, "endorsements");
2533 assert_eq!(card.title, Some("Endorsements".to_string()));
2534 assert_eq!(card.description, Some("Chain of endorsements".to_string()));
2535
2536 assert_eq!(card.fields.len(), 2);
2538
2539 let name_field = card.fields.get("name").unwrap();
2540 assert_eq!(name_field.r#type, FieldType::String);
2541 assert_eq!(name_field.title, Some("Endorser Name".to_string()));
2542 assert!(name_field.required);
2543
2544 let org_field = card.fields.get("org").unwrap();
2545 assert_eq!(org_field.r#type, FieldType::String);
2546 assert!(org_field.default.is_some());
2547 assert_eq!(
2548 org_field.default.as_ref().unwrap().as_str(),
2549 Some("Unknown")
2550 );
2551 }
2552
2553 #[test]
2554 fn test_field_schema_rejects_unknown_keys() {
2555 let yaml = r#"
2557type: "string"
2558description: "A string field"
2559invalid_key:
2560 sub_field:
2561 type: "string"
2562 description: "Nested field"
2563"#;
2564 let quill_value = QuillValue::from_yaml_str(yaml).unwrap();
2565
2566 let result = FieldSchema::from_quill_value("author".to_string(), &quill_value);
2567
2568 assert!(result.is_err());
2570 let err = result.unwrap_err();
2571 assert!(
2572 err.contains("unknown field `invalid_key`"),
2573 "Error was: {}",
2574 err
2575 );
2576 }
2577
2578 #[test]
2579 fn test_quill_config_with_cards_section() {
2580 let yaml_content = r#"
2581Quill:
2582 name: cards-test
2583 version: "1.0"
2584 backend: typst
2585 description: Test [cards] section
2586
2587fields:
2588 regular:
2589 description: Regular field
2590 type: string
2591
2592cards:
2593 indorsements:
2594 title: Routing Indorsements
2595 description: Chain of endorsements
2596 fields:
2597 name:
2598 title: Name
2599 type: string
2600 description: Name field
2601"#;
2602
2603 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2604
2605 assert!(config.document.fields.contains_key("regular"));
2607 let regular = config.document.fields.get("regular").unwrap();
2608 assert_eq!(regular.r#type, FieldType::String);
2609
2610 assert!(config.cards.contains_key("indorsements"));
2612 let card = config.cards.get("indorsements").unwrap();
2613 assert_eq!(card.title, Some("Routing Indorsements".to_string()));
2614 assert_eq!(card.description, Some("Chain of endorsements".to_string()));
2615 assert!(card.fields.contains_key("name"));
2616 }
2617
2618 #[test]
2619 fn test_quill_config_cards_empty_fields() {
2620 let yaml_content = r#"
2622Quill:
2623 name: cards-empty-fields-test
2624 version: "1.0"
2625 backend: typst
2626 description: Test cards without fields
2627
2628cards:
2629 myscope:
2630 description: My scope
2631"#;
2632
2633 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2634 let card = config.cards.get("myscope").unwrap();
2635 assert_eq!(card.name, "myscope");
2636 assert_eq!(card.description, Some("My scope".to_string()));
2637 assert!(card.fields.is_empty());
2638 }
2639
2640 #[test]
2641 fn test_quill_config_allows_card_collision() {
2642 let yaml_content = r#"
2644Quill:
2645 name: collision-test
2646 version: "1.0"
2647 backend: typst
2648 description: Test collision
2649
2650fields:
2651 conflict:
2652 description: Field
2653 type: string
2654
2655cards:
2656 conflict:
2657 description: Card
2658"#;
2659
2660 let result = QuillConfig::from_yaml(yaml_content);
2661 if let Err(e) = &result {
2662 panic!(
2663 "Card name collision should be allowed, but got error: {}",
2664 e
2665 );
2666 }
2667 assert!(result.is_ok());
2668
2669 let config = result.unwrap();
2670 assert!(config.document.fields.contains_key("conflict"));
2671 assert!(config.cards.contains_key("conflict"));
2672 }
2673
2674 #[test]
2675 fn test_quill_config_ordering_with_cards() {
2676 let yaml_content = r#"
2678Quill:
2679 name: ordering-test
2680 version: "1.0"
2681 backend: typst
2682 description: Test ordering
2683
2684fields:
2685 first:
2686 type: string
2687 description: First
2688 zero:
2689 type: string
2690 description: Zero
2691
2692cards:
2693 second:
2694 description: Second
2695 fields:
2696 card_field:
2697 type: string
2698 description: A card field
2699"#;
2700
2701 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2702
2703 let first = config.document.fields.get("first").unwrap();
2704 let zero = config.document.fields.get("zero").unwrap();
2705 let second = config.cards.get("second").unwrap();
2706
2707 let ord_first = first.ui.as_ref().unwrap().order.unwrap();
2709 let ord_zero = zero.ui.as_ref().unwrap().order.unwrap();
2710
2711 assert!(ord_first < ord_zero);
2713 assert_eq!(ord_first, 0);
2714 assert_eq!(ord_zero, 1);
2715
2716 let card_field = second.fields.get("card_field").unwrap();
2718 let ord_card_field = card_field.ui.as_ref().unwrap().order.unwrap();
2719 assert_eq!(ord_card_field, 0); }
2721 #[test]
2722 fn test_card_field_order_preservation() {
2723 let yaml_content = r#"
2727Quill:
2728 name: card-order-test
2729 version: "1.0"
2730 backend: typst
2731 description: Test card field order
2732
2733cards:
2734 mycard:
2735 description: Test card
2736 fields:
2737 z_first:
2738 type: string
2739 description: Defined first
2740 a_second:
2741 type: string
2742 description: Defined second
2743"#;
2744
2745 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2746 let card = config.cards.get("mycard").unwrap();
2747
2748 let z_first = card.fields.get("z_first").unwrap();
2749 let a_second = card.fields.get("a_second").unwrap();
2750
2751 let z_order = z_first.ui.as_ref().unwrap().order.unwrap();
2753 let a_order = a_second.ui.as_ref().unwrap().order.unwrap();
2754
2755 assert_eq!(z_order, 0, "z_first should be 0 (defined first)");
2758 assert_eq!(a_order, 1, "a_second should be 1 (defined second)");
2759 }
2760 #[test]
2761 fn test_nested_schema_parsing() {
2762 let yaml_content = r#"
2763Quill:
2764 name: nested-test
2765 version: "1.0"
2766 backend: typst
2767 description: Test nested elements
2768
2769fields:
2770 my_list:
2771 type: array
2772 description: List of objects
2773 items:
2774 type: object
2775 properties:
2776 sub_a:
2777 type: string
2778 description: Subfield A
2779 sub_b:
2780 type: number
2781 description: Subfield B
2782 my_obj:
2783 type: object
2784 description: Single object
2785 properties:
2786 child:
2787 type: boolean
2788 description: Child field
2789"#;
2790
2791 let config = QuillConfig::from_yaml(yaml_content).unwrap();
2792
2793 let list_field = config.document.fields.get("my_list").unwrap();
2795 assert_eq!(list_field.r#type, FieldType::Array);
2796 assert!(list_field.items.is_some());
2797
2798 let items_schema = list_field.items.as_ref().unwrap();
2799 assert_eq!(items_schema.r#type, FieldType::Object);
2800 assert!(items_schema.properties.is_some());
2801
2802 let props = items_schema.properties.as_ref().unwrap();
2803 assert!(props.contains_key("sub_a"));
2804 assert!(props.contains_key("sub_b"));
2805 assert_eq!(props["sub_a"].r#type, FieldType::String);
2806 assert_eq!(props["sub_b"].r#type, FieldType::Number);
2807
2808 let obj_field = config.document.fields.get("my_obj").unwrap();
2810 assert_eq!(obj_field.r#type, FieldType::Object);
2811 assert!(obj_field.properties.is_some());
2812
2813 let obj_props = obj_field.properties.as_ref().unwrap();
2814 assert!(obj_props.contains_key("child"));
2815 assert_eq!(obj_props["child"].r#type, FieldType::Boolean);
2816 }
2817}