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(&mut out, "Built-in constant", "constant.language", base09);
113 push_rule(
114 &mut out,
115 "User-defined constant",
116 "constant.character, constant.other",
117 base09,
118 );
119 push_rule(&mut out, "Variable", "variable", base08);
120 push_rule(&mut out, "Keyword", "keyword", base0e);
121 push_rule(&mut out, "Storage", "storage", base0e);
122 push_rule(&mut out, "Storage type", "storage.type", base0d);
123 push_rule(&mut out, "Class name", "entity.name.class", base0a);
124 push_rule(
125 &mut out,
126 "Inherited class",
127 "entity.other.inherited-class",
128 base0c,
129 );
130 push_rule(&mut out, "Function name", "entity.name.function", base0d);
131 push_rule(&mut out, "Function argument", "variable.parameter", base09);
132 push_rule(&mut out, "Tag name", "entity.name.tag", base08);
133 push_rule(
134 &mut out,
135 "Tag attribute",
136 "entity.other.attribute-name",
137 base0a,
138 );
139 push_rule(&mut out, "Library function", "support.function", base0d);
140 push_rule(&mut out, "Library constant", "support.constant", base0c);
141 push_rule(
142 &mut out,
143 "Library class/type",
144 "support.type, support.class",
145 base0a,
146 );
147 push_rule(&mut out, "Invalid", "invalid", base08);
148 push_rule(&mut out, "Invalid deprecated", "invalid.deprecated", base0f);
149
150 out.push_str("\t</array>\n");
151 out.push_str("\t<key>uuid</key>\n");
152 out.push_str("\t<string>nord-pleme-io-ishou</string>\n");
153 out.push_str("</dict>\n");
154 out.push_str("</plist>\n");
155 out
156}
157
158fn push_setting(out: &mut String, key: &str, rgb: Rgb) {
161 out.push_str(&format!("\t\t\t\t<key>{key}</key>\n"));
162 out.push_str(&format!("\t\t\t\t<string>{}</string>\n", hex(rgb)));
163}
164
165fn push_rule(out: &mut String, name: &str, scope: &str, rgb: Rgb) {
176 out.push_str("\t\t<dict>\n");
177 out.push_str("\t\t\t<key>name</key>\n");
178 out.push_str(&format!("\t\t\t<string>{name}</string>\n"));
179 out.push_str("\t\t\t<key>scope</key>\n");
180 out.push_str(&format!("\t\t\t<string>{scope}</string>\n"));
181 out.push_str("\t\t\t<key>settings</key>\n");
182 out.push_str("\t\t\t<dict>\n");
183 out.push_str("\t\t\t\t<key>foreground</key>\n");
184 out.push_str(&format!("\t\t\t\t<string>{}</string>\n", hex(rgb)));
185 out.push_str("\t\t\t</dict>\n");
186 out.push_str("\t\t</dict>\n");
187}
188
189fn hex(rgb: Rgb) -> String {
191 format!("#{:02x}{:02x}{:02x}", rgb.r, rgb.g, rgb.b)
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use ishou_tokens::TokenSet;
198
199 #[test]
200 fn output_is_deterministic() {
201 let t = TokenSet::pleme();
202 assert_eq!(render(&t), render(&t));
203 }
204
205 #[test]
206 fn output_is_non_empty() {
207 assert!(!render(&TokenSet::pleme()).is_empty());
208 }
209
210 #[test]
211 fn output_is_a_tmtheme_plist() {
212 let out = render(&TokenSet::pleme());
213 assert!(out.starts_with("<?xml version=\"1.0\""), "got:\n{out}");
214 assert!(out.contains("<plist version=\"1.0\">"), "got:\n{out}");
215 assert!(out.contains("<key>settings</key>"), "got:\n{out}");
216 assert!(out.trim_end().ends_with("</plist>"), "got:\n{out}");
217 }
218
219 #[test]
220 fn background_is_base00_polar_night_0() {
221 let out = render(&TokenSet::pleme());
222 assert!(
225 out.contains("<key>background</key>\n\t\t\t\t<string>#2e3440</string>"),
226 "background must be base00 (#2e3440); got:\n{out}"
227 );
228 }
229
230 #[test]
231 fn foreground_is_base05_snow_storm_1() {
232 let out = render(&TokenSet::pleme());
233 assert!(
234 out.contains("<key>foreground</key>\n\t\t\t\t<string>#e5e9f0</string>"),
235 "foreground must be base05 (#e5e9f0); got:\n{out}"
236 );
237 }
238
239 #[test]
240 fn strings_are_base0b_aurora_green() {
241 let out = render(&TokenSet::pleme());
242 assert!(out.contains("<string>#a3be8c</string>"), "got:\n{out}");
244 }
245
246 #[test]
247 fn hex_is_hash_prefixed_lowercase() {
248 let out = render(&TokenSet::pleme());
249 for line in out.lines() {
253 let trimmed = line.trim();
254 if let Some(v) = trimmed
255 .strip_prefix("<string>#")
256 .and_then(|r| r.strip_suffix("</string>"))
257 {
258 assert_eq!(v.len(), 6, "wrong hex length in: {line}");
259 assert!(
260 v.chars()
261 .all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase()),
262 "expected lowercase hex in: {line}"
263 );
264 }
265 }
266 }
267}