lean_ctx/core/
wrapped_share.rs1use crate::core::wrapped::{WrappedReport, format_tokens};
14
15impl WrappedReport {
16 pub fn to_share_html(&self, base_url: Option<&str>) -> String {
19 let title = "lean-ctx Wrapped";
20 let period_label = match self.period.as_str() {
21 "week" => "this week",
22 "month" => "this month",
23 _ => "with lean-ctx",
24 };
25 let desc = format!(
26 "I saved {} tokens (~${:.2}) {period_label}. Your AI saw only what mattered.",
27 format_tokens(self.tokens_saved),
28 self.cost_avoided_usd,
29 );
30 let svg = self.to_svg();
31 let meta = social_meta(title, &desc, base_url);
32
33 format!(
34 r#"<!DOCTYPE html>
35<html lang="en">
36<head>
37 <meta charset="utf-8"/>
38 <meta name="viewport" content="width=device-width, initial-scale=1"/>
39 <title>{title}</title>
40 <meta name="description" content="{desc_attr}"/>
41 <style>body{{margin:0;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;background:#0b1020;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}}.card{{width:min(1200px,94vw)}}.card svg{{width:100%;height:auto;display:block;border-radius:12px}}a.cta{{color:#34d399;text-decoration:none;font-size:18px;font-weight:600}}</style>
42{meta}</head>
43<body>
44 <div class="card">
45{svg}
46 </div>
47 <a class="cta" href="https://leanctx.com">Get lean-ctx — your AI saw only what mattered →</a>
48</body>
49</html>
50"#,
51 desc_attr = escape(&desc),
52 )
53 }
54
55 pub fn share_text(&self, url: Option<&str>) -> String {
58 let period_label = match self.period.as_str() {
59 "week" => " this week",
60 "month" => " this month",
61 _ => "",
62 };
63 let est = if self.pricing_estimated {
64 " (est.)"
65 } else {
66 ""
67 };
68 let mut s = format!(
69 "I saved {} tokens (~${:.2}{est}){period_label} with lean-ctx — my AI saw only what mattered.",
70 format_tokens(self.tokens_saved),
71 self.cost_avoided_usd,
72 );
73 if let Some(u) = url {
74 s.push(' ');
75 s.push_str(u);
76 }
77 s
78 }
79}
80
81fn social_meta(title: &str, desc: &str, base_url: Option<&str>) -> String {
83 let mut m = String::new();
84 m.push_str(&tag_prop("og:title", title));
85 m.push_str(&tag_prop("og:description", desc));
86 m.push_str(" <meta property=\"og:type\" content=\"website\"/>\n");
87 m.push_str(&tag_name("twitter:title", title));
88 m.push_str(&tag_name("twitter:description", desc));
89
90 if let Some(base) = base_url {
91 let base = base.trim_end_matches('/');
92 let image = format!("{base}/lean-ctx-wrapped.png");
93 m.push_str(&tag_prop("og:url", base));
94 m.push_str(&tag_prop("og:image", &image));
95 m.push_str(" <meta name=\"twitter:card\" content=\"summary_large_image\"/>\n");
96 m.push_str(&tag_name("twitter:image", &image));
97 } else {
98 m.push_str(" <meta name=\"twitter:card\" content=\"summary\"/>\n");
99 }
100 m
101}
102
103fn tag_prop(property: &str, content: &str) -> String {
104 format!(
105 " <meta property=\"{property}\" content=\"{}\"/>\n",
106 escape(content)
107 )
108}
109
110fn tag_name(name: &str, content: &str) -> String {
111 format!(
112 " <meta name=\"{name}\" content=\"{}\"/>\n",
113 escape(content)
114 )
115}
116
117fn escape(s: &str) -> String {
119 s.replace('&', "&")
120 .replace('<', "<")
121 .replace('>', ">")
122 .replace('"', """)
123 .replace('\'', "'")
124}
125
126#[cfg(test)]
127mod tests {
128 use crate::core::wrapped::WrappedReport;
129
130 fn sample() -> WrappedReport {
131 WrappedReport {
132 period: "all".into(),
133 tokens_saved: 348_300_000,
134 tokens_input: 580_000_000,
135 cost_avoided_usd: 870.81,
136 total_commands: 17_055,
137 sessions_count: 67,
138 top_commands: vec![("ctx_search".into(), 100, 60.0)],
139 compression_rate_pct: 60.2,
140 files_touched: 1_234,
141 daily_savings: vec![10, 50, 30, 80, 20, 40, 60],
142 bounce_tokens: 0,
143 model_key: "claude-3.5-sonnet".into(),
144 pricing_estimated: false,
145 percentile: Some(99),
146 security: None,
147 }
148 }
149
150 #[test]
151 fn page_is_self_contained_and_branded() {
152 let html = sample().to_share_html(None);
153 assert!(html.starts_with("<!DOCTYPE html>"));
154 assert!(html.contains("<svg"), "SVG must be embedded inline");
155 assert!(html.contains("</html>"));
156 assert!(
157 html.contains("leanctx.com"),
158 "viral CTA must link the brand"
159 );
160 assert!(html.contains("348.3M"), "must show the headline metric");
161 }
162
163 #[test]
164 fn without_base_url_no_image_meta() {
165 let html = sample().to_share_html(None);
166 assert!(
167 !html.contains("og:image"),
168 "must not fabricate an image URL without a base"
169 );
170 assert!(html.contains("name=\"twitter:card\" content=\"summary\""));
171 }
172
173 #[test]
174 fn with_base_url_emits_absolute_image_meta() {
175 let html = sample().to_share_html(Some("https://me.dev/wrapped/"));
176 assert!(
177 html.contains("og:image\" content=\"https://me.dev/wrapped/lean-ctx-wrapped.png\"")
178 );
179 assert!(html.contains("summary_large_image"));
180 assert!(!html.contains("wrapped//lean-ctx-wrapped.png"));
182 }
183
184 #[test]
185 fn base_url_is_attribute_escaped() {
186 let html = sample().to_share_html(Some("https://me.dev/w?a=1&b=2"));
187 assert!(
188 html.contains("a=1&b=2"),
189 "ampersands in the base URL must be escaped in attributes"
190 );
191 assert!(
192 !html.contains("a=1&b=2\""),
193 "a raw unescaped ampersand must not survive into an attribute"
194 );
195 }
196
197 #[test]
198 fn share_text_is_postable_and_honest() {
199 let txt = sample().share_text(None);
200 assert!(
201 txt.contains("348.3M"),
202 "headline metric must be in the share line"
203 );
204 assert!(txt.contains("lean-ctx"), "must name the brand");
205 assert!(!txt.contains("http"), "no URL when none is supplied");
206 }
207
208 #[test]
209 fn share_text_appends_permalink_and_estimate_marker() {
210 let mut r = sample();
211 r.pricing_estimated = true;
212 let txt = r.share_text(Some("https://leanctx.com/w/abc123"));
213 assert!(txt.ends_with("https://leanctx.com/w/abc123"));
214 assert!(
215 txt.contains("(est.)"),
216 "estimated pricing must be disclosed"
217 );
218 }
219}