Skip to main content

max / makenotwork

9.2 KB · 253 lines History Blame Raw
1 //! Askama templates for the public embed widgets (`/embed/*`). Ported from
2 //! hand-rolled `format!` HTML so user-derived fields (titles, creator names,
3 //! descriptions, URLs) are autoescaped by Askama rather than per-field by hand.
4
5 use askama::Template;
6
7 /// The spacing layer the embeds inline, written by `build.rs` from
8 /// makeover-geometry at Pointer density.
9 ///
10 /// An embed cannot link a stylesheet: it renders inside an iframe on somebody
11 /// else's page, so its whole design system has to arrive in the document. That
12 /// is why these five templates carry a `<style>` block when nothing else in the
13 /// tree does. What it must not mean is that the values were copied by hand,
14 /// which is what it meant until this existed.
15 pub const EMBED_GEOMETRY_CSS: &str = include_str!("../../static/embed-geometry.css");
16
17 /// The colour layer for an embed: the platform default theme, resolved through
18 /// makeover the same way every page's is.
19 ///
20 /// Always the default. Serving a creator's chosen theme into a third-party page
21 /// is a separate product question, and this is not the place to answer it by
22 /// accident.
23 pub fn embed_theme_css() -> &'static str {
24 crate::theming::theme_css(None)
25 }
26
27 #[derive(Template)]
28 #[template(path = "embed/item_button.html")]
29 pub struct EmbedItemButtonTemplate {
30 pub title: String,
31 pub price_display: String,
32 pub purchase_url: String,
33 pub button_text: String,
34 pub cover_image_url: Option<String>,
35 /// Design-system layers, inlined because an embed cannot link a sheet.
36 pub theme_css: &'static str,
37 pub geometry_css: &'static str,
38 }
39
40 #[derive(Template)]
41 #[template(path = "embed/item_card.html")]
42 pub struct EmbedItemCardTemplate {
43 pub title: String,
44 pub price_display: String,
45 pub purchase_url: String,
46 pub button_text: String,
47 pub cover_image_url: Option<String>,
48 pub creator_display_name: String,
49 pub profile_url: String,
50 /// Empty string = no description block.
51 pub description_excerpt: String,
52 pub is_horizontal: bool,
53 /// Design-system layers, inlined because an embed cannot link a sheet.
54 pub theme_css: &'static str,
55 pub geometry_css: &'static str,
56 }
57
58 #[derive(Template)]
59 #[template(path = "embed/item_player.html")]
60 pub struct EmbedItemPlayerTemplate {
61 pub title: String,
62 pub price_display: String,
63 pub purchase_url: String,
64 pub button_text: String,
65 pub creator_display_name: String,
66 pub cover_image_url: Option<String>,
67 pub preview_url: String,
68 /// Design-system layers, inlined because an embed cannot link a sheet.
69 pub theme_css: &'static str,
70 pub geometry_css: &'static str,
71 }
72
73 #[derive(Template)]
74 #[template(path = "embed/tip_button.html")]
75 pub struct EmbedTipButtonTemplate {
76 pub display_name: String,
77 pub username: String,
78 pub tip_url: String,
79 pub avatar_url: Option<String>,
80 /// Design-system layers, inlined because an embed cannot link a sheet.
81 pub theme_css: &'static str,
82 pub geometry_css: &'static str,
83 }
84
85 #[derive(Template)]
86 #[template(path = "embed/project_card.html")]
87 pub struct EmbedProjectCardTemplate {
88 pub title: String,
89 pub creator_display_name: String,
90 pub profile_url: String,
91 pub project_url: String,
92 pub cover_image_url: Option<String>,
93 /// Empty string = no description block.
94 pub description_excerpt: String,
95 pub item_count: usize,
96 pub category_label: String,
97 /// Design-system layers, inlined because an embed cannot link a sheet.
98 pub theme_css: &'static str,
99 pub geometry_css: &'static str,
100 }
101
102 #[cfg(test)]
103 mod tests {
104 use super::*;
105
106 /// Everything a template needs that is not the design system. The values are
107 /// deliberately hex-free so a literal in the output came from the CSS.
108 fn sample() -> (
109 EmbedItemButtonTemplate,
110 EmbedItemCardTemplate,
111 EmbedItemPlayerTemplate,
112 EmbedTipButtonTemplate,
113 EmbedProjectCardTemplate,
114 ) {
115 let url = || "https://makenot.work/i/one".to_string();
116 (
117 EmbedItemButtonTemplate {
118 title: "Item".into(),
119 price_display: "$9".into(),
120 purchase_url: url(),
121 button_text: "Buy".into(),
122 cover_image_url: Some(url()),
123 theme_css: embed_theme_css(),
124 geometry_css: EMBED_GEOMETRY_CSS,
125 },
126 EmbedItemCardTemplate {
127 title: "Item".into(),
128 price_display: "$9".into(),
129 purchase_url: url(),
130 button_text: "Buy".into(),
131 cover_image_url: Some(url()),
132 creator_display_name: "Creator".into(),
133 profile_url: url(),
134 description_excerpt: "About it.".into(),
135 is_horizontal: true,
136 theme_css: embed_theme_css(),
137 geometry_css: EMBED_GEOMETRY_CSS,
138 },
139 EmbedItemPlayerTemplate {
140 title: "Item".into(),
141 price_display: "$9".into(),
142 purchase_url: url(),
143 button_text: "Buy".into(),
144 creator_display_name: "Creator".into(),
145 cover_image_url: Some(url()),
146 preview_url: url(),
147 theme_css: embed_theme_css(),
148 geometry_css: EMBED_GEOMETRY_CSS,
149 },
150 EmbedTipButtonTemplate {
151 display_name: "Creator".into(),
152 username: "creator".into(),
153 tip_url: url(),
154 avatar_url: Some(url()),
155 theme_css: embed_theme_css(),
156 geometry_css: EMBED_GEOMETRY_CSS,
157 },
158 EmbedProjectCardTemplate {
159 title: "Project".into(),
160 creator_display_name: "Creator".into(),
161 profile_url: url(),
162 project_url: url(),
163 cover_image_url: Some(url()),
164 description_excerpt: "About it.".into(),
165 item_count: 3,
166 category_label: "Music".into(),
167 theme_css: embed_theme_css(),
168 geometry_css: EMBED_GEOMETRY_CSS,
169 },
170 )
171 }
172
173 /// Strip the injected `:root { ... }` blocks. What is left is the template's
174 /// own CSS, which is where a hand-copied value would be.
175 fn without_injected_roots(rendered: &str) -> String {
176 let mut out = String::with_capacity(rendered.len());
177 let mut rest = rendered;
178 while let Some(start) = rest.find(":root") {
179 out.push_str(&rest[..start]);
180 let Some(open) = rest[start..].find('{') else {
181 break;
182 };
183 let Some(close) = rest[start + open..].find('}') else {
184 break;
185 };
186 rest = &rest[start + open + close + 1..];
187 }
188 out.push_str(rest);
189 out
190 }
191
192 fn hex_literals(css: &str) -> Vec<String> {
193 css.split('#')
194 .skip(1)
195 .map(|tail| {
196 tail.chars()
197 .take_while(char::is_ascii_hexdigit)
198 .collect::<String>()
199 })
200 .filter(|run| run.len() == 3 || run.len() == 6)
201 .map(|run| format!("#{run}"))
202 .collect()
203 }
204
205 /// The regression guard the drift got past. `#5a4bd6` sat in all five
206 /// templates as the button hover violet and matched no token anywhere in the
207 /// tree: the real `--action-hover` is a different colour. Nothing could have
208 /// caught it, because nothing was looking.
209 #[test]
210 fn no_embed_template_writes_its_own_colour() {
211 let (button, card, player, tip, project) = sample();
212 for (name, rendered) in [
213 ("item_button", button.render().expect("render")),
214 ("item_card", card.render().expect("render")),
215 ("item_player", player.render().expect("render")),
216 ("tip_button", tip.render().expect("render")),
217 ("project_card", project.render().expect("render")),
218 ] {
219 let own = without_injected_roots(&rendered);
220 let found = hex_literals(&own);
221 assert!(
222 found.is_empty(),
223 "{name} writes its own colour: {found:?}. Use the token instead; \
224 a literal here drifts from the theme and nothing will report it.",
225 );
226 }
227 }
228
229 /// The stripper only earns its assertion if the blocks it strips were there.
230 /// Without this, a handler that stopped passing `theme_css` would leave the
231 /// test above passing on an empty document.
232 #[test]
233 fn every_embed_injects_both_design_system_layers() {
234 let (button, card, player, tip, project) = sample();
235 for (name, rendered) in [
236 ("item_button", button.render().expect("render")),
237 ("item_card", card.render().expect("render")),
238 ("item_player", player.render().expect("render")),
239 ("tip_button", tip.render().expect("render")),
240 ("project_card", project.render().expect("render")),
241 ] {
242 assert!(
243 rendered.contains("--action-hover"),
244 "{name} did not inject the theme layer",
245 );
246 assert!(
247 rendered.contains("--geometry-base"),
248 "{name} did not inject the spacing layer",
249 );
250 }
251 }
252 }
253