kimun_notes/components/
query_vars.rs1use kimun_core::nfs::VaultPath;
6use kimun_core::{expand_bare_note_prefixes, quote_query_term, strip_order_directive};
7
8pub const VAR_NOTE: &str = "{note}";
13
14#[derive(Clone, Default)]
20pub struct QueryContext {
21 pub current_note: Option<VaultPath>,
25}
26
27impl QueryContext {
28 pub fn with_note(current_note: Option<VaultPath>) -> Self {
30 Self { current_note }
31 }
32
33 fn has_note(&self) -> bool {
36 self.current_note
37 .as_ref()
38 .is_some_and(|p| !p.is_root_or_empty())
39 }
40
41 fn note_term(&self) -> String {
42 self.current_note
43 .as_ref()
44 .map(|p| quote_query_term(&p.get_clean_name()))
45 .unwrap_or_default()
46 }
47}
48
49pub fn query_has_variables(template: &str) -> bool {
53 expand_bare_note_prefixes(template, VAR_NOTE).contains(VAR_NOTE)
54}
55
56pub fn query_is_unresolvable(template: &str, ctx: &QueryContext) -> bool {
65 if ctx.has_note() {
66 return false;
67 }
68 let expanded = expand_bare_note_prefixes(template, VAR_NOTE);
69 expanded.contains(VAR_NOTE)
70 && strip_order_directive(&expanded)
71 .split_whitespace()
72 .all(|token| token.contains(VAR_NOTE))
73}
74
75pub fn resolve_query(template: &str, ctx: &QueryContext) -> String {
83 expand_bare_note_prefixes(template, VAR_NOTE).replace(VAR_NOTE, &ctx.note_term())
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn with(p: &VaultPath) -> QueryContext {
92 QueryContext::with_note(Some(p.clone()))
93 }
94
95 fn none() -> QueryContext {
97 QueryContext::default()
98 }
99
100 #[test]
101 fn detects_variables() {
102 assert!(query_has_variables("<{note}"));
103 assert!(query_has_variables("#todo <{note}"));
104 assert!(!query_has_variables("#todo"));
105 }
106
107 #[test]
108 fn resolves_note_variable() {
109 let p = VaultPath::note_path_from("work/spec.md");
110 assert_eq!(resolve_query("<{note}", &with(&p)), "<spec");
111 assert_eq!(resolve_query("#todo <{note}", &with(&p)), "#todo <spec");
112 }
113
114 #[test]
115 fn resolves_note_with_spaces_quoted() {
116 let p = VaultPath::note_path_from("work/my note.md");
117 assert_eq!(resolve_query("<{note}", &with(&p)), "<\"my note\"");
120 }
121
122 #[test]
123 fn resolves_to_empty_without_note() {
124 assert_eq!(resolve_query("<{note}", &none()), "<");
125 assert_eq!(resolve_query("#todo", &none()), "#todo");
126 }
127
128 #[test]
129 fn bare_operators_expand_to_note_variable() {
130 let p = VaultPath::note_path_from("work/spec.md");
131 assert_eq!(resolve_query("<", &with(&p)), "<spec");
132 assert_eq!(resolve_query(">", &with(&p)), ">spec");
133 assert_eq!(resolve_query("=", &with(&p)), "=spec");
134 assert_eq!(resolve_query("#todo <", &with(&p)), "#todo <spec");
135 assert_eq!(resolve_query("< #todo", &with(&p)), "<spec #todo");
136 }
137
138 #[test]
139 fn bare_long_forms_and_exclusions_expand_too() {
140 let p = VaultPath::note_path_from("work/spec.md");
141 assert_eq!(resolve_query("lk:", &with(&p)), "lk:spec");
142 assert_eq!(resolve_query("fwd:", &with(&p)), "fwd:spec");
143 assert_eq!(resolve_query("name:", &with(&p)), "name:spec");
144 assert_eq!(resolve_query("-<", &with(&p)), "-<spec");
145 }
146
147 #[test]
148 fn apostrophe_in_term_does_not_suppress_expansion() {
149 let p = VaultPath::note_path_from("work/spec.md");
153 assert_eq!(resolve_query("don't <", &with(&p)), "don't <spec");
154 assert_eq!(resolve_query("= don't <", &with(&p)), "=spec don't <spec");
155 }
156
157 #[test]
158 fn operators_with_targets_stay_untouched() {
159 let p = VaultPath::note_path_from("work/spec.md");
160 assert_eq!(resolve_query("<projects", &with(&p)), "<projects");
161 assert_eq!(resolve_query(">projects", &with(&p)), ">projects");
162 assert_eq!(resolve_query("=projects", &with(&p)), "=projects");
163 }
164
165 #[test]
166 fn bare_operator_inside_quotes_stays_untouched() {
167 let p = VaultPath::note_path_from("work/spec.md");
168 assert_eq!(resolve_query("\"a < b\"", &with(&p)), "\"a < b\"");
169 assert_eq!(resolve_query("'a = b'", &with(&p)), "'a = b'");
170 }
171
172 #[test]
173 fn unresolvable_only_when_purely_note_dependent() {
174 let p = VaultPath::note_path_from("work/spec.md");
175 assert!(!query_is_unresolvable("<", &with(&p)));
177 assert!(!query_is_unresolvable("<{note}", &with(&p)));
178 assert!(query_is_unresolvable("<", &none()));
181 assert!(query_is_unresolvable("<{note}", &none()));
182 assert!(query_is_unresolvable("< or:title", &none()));
183 assert!(query_is_unresolvable("<", &with(&VaultPath::empty())));
184 assert!(!query_is_unresolvable("widget <", &none()));
186 assert!(!query_is_unresolvable("#todo <{note}", &none()));
187 assert!(!query_is_unresolvable("widget", &none()));
189 assert!(!query_is_unresolvable("", &none()));
190 }
191
192 #[test]
193 fn bare_operators_count_as_variables() {
194 assert!(query_has_variables("<"));
195 assert!(query_has_variables(">"));
196 assert!(query_has_variables("="));
197 assert!(query_has_variables("#todo <"));
198 assert!(!query_has_variables("<projects"));
199 assert!(!query_has_variables("\"a < b\""));
200 }
201}