quillmark_core/
normalize.rs1use crate::document::Card;
14use unicode_normalization::UnicodeNormalization;
15
16pub fn normalize_field_name(name: &str) -> String {
19 name.nfc().collect()
20}
21
22pub fn normalize_document(
30 doc: crate::document::Document,
31) -> Result<crate::document::Document, crate::error::ParseError> {
32 use crate::document::Document;
33
34 let main = normalize_card(doc.main());
35 let normalized_cards: Vec<Card> = doc.cards().iter().map(normalize_card).collect();
36
37 Ok(Document::from_main_and_cards(main, normalized_cards))
38}
39
40fn normalize_card(card: &Card) -> Card {
43 use crate::document::PayloadItem;
44 let mut payload = card.payload().clone();
45 for item in payload.items_mut() {
46 if let PayloadItem::Field { key, .. } = item {
47 let normalized = normalize_field_name(key);
48 if normalized != *key {
49 *key = normalized;
50 }
51 }
52 }
53 Card::from_parts(payload, card.body().clone())
54}
55
56#[cfg(test)]
57mod tests {
58
59 #[test]
60 fn test_normalize_document_basic() {
61 use crate::document::Document;
62
63 let doc = Document::parse(
64 "~~~card-yaml\n$quill: test\n$kind: main\ntitle: <<placeholder>>\n~~~\n\n<<content>> \u{202D}**bold**",
65 )
66 .unwrap()
67 .document;
68 let normalized = super::normalize_document(doc).unwrap();
69
70 assert_eq!(
71 normalized
72 .main()
73 .payload()
74 .get("title")
75 .unwrap()
76 .as_str()
77 .unwrap(),
78 "<<placeholder>>"
79 );
80
81 assert_eq!(normalized.main().body_markdown(), "\\<> **bold**");
82 }
83
84 #[test]
85 fn test_normalize_document_preserves_quill_tag() {
86 use crate::document::Document;
87
88 let doc = Document::parse("~~~card-yaml\n$quill: custom_quill\n$kind: main\n~~~\n")
89 .unwrap()
90 .document;
91 let normalized = super::normalize_document(doc).unwrap();
92
93 assert_eq!(normalized.quill_reference().name, "custom_quill");
94 }
95
96 #[test]
97 fn test_normalize_document_idempotent() {
98 use crate::document::Document;
99
100 let doc =
101 Document::parse("~~~card-yaml\n$quill: test\n$kind: main\n~~~\n\n<<content>>")
102 .unwrap()
103 .document;
104 let normalized_once = super::normalize_document(doc).unwrap();
105 let normalized_twice = super::normalize_document(normalized_once.clone()).unwrap();
106
107 assert_eq!(
108 normalized_once.main().body_markdown(),
109 normalized_twice.main().body_markdown()
110 );
111 }
112
113 #[test]
114 fn test_normalize_document_yaml_field_bidi_preserved() {
115 use crate::document::Document;
116
117 let doc = Document::parse(
118 "~~~card-yaml\n$quill: test\n$kind: main\ntitle: a\u{202D}b\n~~~\n",
119 )
120 .unwrap()
121 .document;
122 let normalized = super::normalize_document(doc).unwrap();
123 assert_eq!(
124 normalized
125 .main()
126 .payload()
127 .get("title")
128 .unwrap()
129 .as_str()
130 .unwrap(),
131 "a\u{202D}b"
132 );
133 }
134
135 #[test]
136 fn test_normalize_document_card_body_bidi_stripped() {
137 use crate::document::Document;
138
139 let md = "~~~card-yaml\n$quill: test\n$kind: main\n~~~\n\nbody\n\n~~~card-yaml\n$kind: note\n~~~\ncard\u{202D}body\n";
140 let doc = Document::parse(md).unwrap().document;
141 assert_eq!(doc.cards().len(), 1, "expected 1 card");
142 let normalized = super::normalize_document(doc).unwrap();
143 assert_eq!(normalized.cards()[0].body_markdown(), "cardbody");
144 }
145
146 #[test]
147 fn test_normalize_document_card_field_bidi_preserved() {
148 use crate::document::Document;
149
150 let md = "~~~card-yaml\n$quill: test\n$kind: main\n~~~\n\nbody\n\n~~~card-yaml\n$kind: note\nname: Ali\u{202D}ce\n~~~\n";
151 let doc = Document::parse(md).unwrap().document;
152 assert_eq!(doc.cards().len(), 1, "expected 1 card");
153 let normalized = super::normalize_document(doc).unwrap();
154 assert_eq!(
155 normalized.cards()[0]
156 .payload()
157 .get("name")
158 .unwrap()
159 .as_str()
160 .unwrap(),
161 "Ali\u{202D}ce"
162 );
163 }
164
165 #[test]
166 fn test_normalize_document_card_body_html_comment_repair() {
167 use crate::document::Document;
168
169 let md = "~~~card-yaml\n$quill: test\n$kind: main\n~~~\n\n~~~card-yaml\n$kind: note\n~~~\n<!-- comment -->Trailing text\n";
170 let doc = Document::parse(md).unwrap().document;
171 let normalized = super::normalize_document(doc).unwrap();
172 assert_eq!(normalized.cards()[0].body_markdown(), "Trailing text");
173 }
174
175 #[test]
176 fn test_normalize_document_toplevel_body_html_comment_repair() {
177 use crate::document::Document;
178
179 let md = "~~~card-yaml\n$quill: test\n$kind: main\n~~~\n\n<!-- note -->Content here";
180 let doc = Document::parse(md).unwrap().document;
181 let normalized = super::normalize_document(doc).unwrap();
182 assert_eq!(normalized.main().body_markdown(), "Content here");
183 }
184}