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