1use ishou_tokens::{Rgb, TokenSet};
48
49#[must_use]
55pub fn render(t: &TokenSet) -> String {
56 let c = &t.color;
57
58 let base00 = c.polar_night_0;
61 let base01 = c.polar_night_1;
62 let base02 = c.polar_night_2;
63 let base03 = c.polar_night_3;
64 let base05 = c.snow_storm_1;
65 let base08 = c.aurora_red;
66 let base09 = c.aurora_orange;
67 let base0a = c.aurora_yellow;
68 let base0b = c.aurora_green;
69 let base0c = c.frost_1;
70 let base0d = c.frost_2;
71 let base0e = c.aurora_purple;
72 let base0f = c.frost_3;
73
74 let mut out = String::new();
75 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
76 out.push_str(
77 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
78 \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",
79 );
80 out.push_str("<plist version=\"1.0\">\n");
81 out.push_str("<dict>\n");
82 out.push_str("\t<key>name</key>\n");
83 out.push_str("\t<string>Nord (pleme-io / ishou)</string>\n");
84 out.push_str("\t<key>author</key>\n");
85 out.push_str("\t<string>Arctic Ice Studio; mapped by pleme-io ishou</string>\n");
86 out.push_str("\t<key>semanticClass</key>\n");
87 out.push_str("\t<string>theme.dark.nord_pleme_io_ishou</string>\n");
88 out.push_str("\t<key>colorSpaceName</key>\n");
89 out.push_str("\t<string>sRGB</string>\n");
90 out.push_str("\t<key>settings</key>\n");
91 out.push_str("\t<array>\n");
92
93 out.push_str("\t\t<dict>\n");
95 out.push_str("\t\t\t<key>settings</key>\n");
96 out.push_str("\t\t\t<dict>\n");
97 push_setting(&mut out, "background", base00);
98 push_setting(&mut out, "caret", base05);
99 push_setting(&mut out, "foreground", base05);
100 push_setting(&mut out, "invisibles", base03);
101 push_setting(&mut out, "lineHighlight", base01);
102 push_setting(&mut out, "selection", base02);
103 push_setting(&mut out, "gutterForeground", base03);
104 out.push_str("\t\t\t</dict>\n");
105 out.push_str("\t\t</dict>\n");
106
107 push_rule(&mut out, "Comment", "comment", base03);
110 push_rule(&mut out, "String", "string", base0b);
111 push_rule(&mut out, "Number", "constant.numeric", base09);
112 push_rule(
113 &mut out,
114 "Built-in constant",
115 "constant.language",
116 base09,
117 );
118 push_rule(
119 &mut out,
120 "User-defined constant",
121 "constant.character, constant.other",
122 base09,
123 );
124 push_rule(&mut out, "Variable", "variable", base08);
125 push_rule(&mut out, "Keyword", "keyword", base0e);
126 push_rule(&mut out, "Storage", "storage", base0e);
127 push_rule(
128 &mut out,
129 "Storage type",
130 "storage.type",
131 base0d,
132 );
133 push_rule(
134 &mut out,
135 "Class name",
136 "entity.name.class",
137 base0a,
138 );
139 push_rule(
140 &mut out,
141 "Inherited class",
142 "entity.other.inherited-class",
143 base0c,
144 );
145 push_rule(
146 &mut out,
147 "Function name",
148 "entity.name.function",
149 base0d,
150 );
151 push_rule(
152 &mut out,
153 "Function argument",
154 "variable.parameter",
155 base09,
156 );
157 push_rule(
158 &mut out,
159 "Tag name",
160 "entity.name.tag",
161 base08,
162 );
163 push_rule(
164 &mut out,
165 "Tag attribute",
166 "entity.other.attribute-name",
167 base0a,
168 );
169 push_rule(
170 &mut out,
171 "Library function",
172 "support.function",
173 base0d,
174 );
175 push_rule(
176 &mut out,
177 "Library constant",
178 "support.constant",
179 base0c,
180 );
181 push_rule(
182 &mut out,
183 "Library class/type",
184 "support.type, support.class",
185 base0a,
186 );
187 push_rule(
188 &mut out,
189 "Invalid",
190 "invalid",
191 base08,
192 );
193 push_rule(
194 &mut out,
195 "Invalid deprecated",
196 "invalid.deprecated",
197 base0f,
198 );
199
200 out.push_str("\t</array>\n");
201 out.push_str("\t<key>uuid</key>\n");
202 out.push_str("\t<string>nord-pleme-io-ishou</string>\n");
203 out.push_str("</dict>\n");
204 out.push_str("</plist>\n");
205 out
206}
207
208fn push_setting(out: &mut String, key: &str, rgb: Rgb) {
211 out.push_str(&format!("\t\t\t\t<key>{key}</key>\n"));
212 out.push_str(&format!("\t\t\t\t<string>{}</string>\n", hex(rgb)));
213}
214
215fn push_rule(out: &mut String, name: &str, scope: &str, rgb: Rgb) {
226 out.push_str("\t\t<dict>\n");
227 out.push_str("\t\t\t<key>name</key>\n");
228 out.push_str(&format!("\t\t\t<string>{name}</string>\n"));
229 out.push_str("\t\t\t<key>scope</key>\n");
230 out.push_str(&format!("\t\t\t<string>{scope}</string>\n"));
231 out.push_str("\t\t\t<key>settings</key>\n");
232 out.push_str("\t\t\t<dict>\n");
233 out.push_str("\t\t\t\t<key>foreground</key>\n");
234 out.push_str(&format!("\t\t\t\t<string>{}</string>\n", hex(rgb)));
235 out.push_str("\t\t\t</dict>\n");
236 out.push_str("\t\t</dict>\n");
237}
238
239fn hex(rgb: Rgb) -> String {
241 format!("#{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use ishou_tokens::TokenSet;
248
249 #[test]
250 fn output_is_deterministic() {
251 let t = TokenSet::pleme();
252 assert_eq!(render(&t), render(&t));
253 }
254
255 #[test]
256 fn output_is_non_empty() {
257 assert!(!render(&TokenSet::pleme()).is_empty());
258 }
259
260 #[test]
261 fn output_is_a_tmtheme_plist() {
262 let out = render(&TokenSet::pleme());
263 assert!(out.starts_with("<?xml version=\"1.0\""), "got:\n{out}");
264 assert!(out.contains("<plist version=\"1.0\">"), "got:\n{out}");
265 assert!(out.contains("<key>settings</key>"), "got:\n{out}");
266 assert!(out.trim_end().ends_with("</plist>"), "got:\n{out}");
267 }
268
269 #[test]
270 fn background_is_base00_polar_night_0() {
271 let out = render(&TokenSet::pleme());
272 assert!(
275 out.contains("<key>background</key>\n\t\t\t\t<string>#2e3440</string>"),
276 "background must be base00 (#2e3440); got:\n{out}"
277 );
278 }
279
280 #[test]
281 fn foreground_is_base05_snow_storm_1() {
282 let out = render(&TokenSet::pleme());
283 assert!(
284 out.contains("<key>foreground</key>\n\t\t\t\t<string>#e5e9f0</string>"),
285 "foreground must be base05 (#e5e9f0); got:\n{out}"
286 );
287 }
288
289 #[test]
290 fn strings_are_base0b_aurora_green() {
291 let out = render(&TokenSet::pleme());
292 assert!(out.contains("<string>#a3be8c</string>"), "got:\n{out}");
294 }
295
296 #[test]
297 fn hex_is_hash_prefixed_lowercase() {
298 let out = render(&TokenSet::pleme());
299 for line in out.lines() {
303 let trimmed = line.trim();
304 if let Some(v) = trimmed
305 .strip_prefix("<string>#")
306 .and_then(|r| r.strip_suffix("</string>"))
307 {
308 assert_eq!(v.len(), 6, "wrong hex length in: {line}");
309 assert!(
310 v.chars()
311 .all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase()),
312 "expected lowercase hex in: {line}"
313 );
314 }
315 }
316 }
317}