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