Skip to main content

lean_ctx/core/
wrapped_share.rs

1//! Opt-in, self-hostable Wrapped share page.
2//!
3//! Produces a single standalone HTML file with the Wrapped SVG embedded inline (renders
4//! offline, anywhere) plus Open Graph / Twitter card meta so that *when hosted* the link
5//! unfurls into a rich preview. Zero network calls, zero telemetry — the user explicitly
6//! runs `gain --share` and chooses where to host it (their site / GH Pages / a gist),
7//! which is the permalink. Nothing is uploaded by lean-ctx.
8//!
9//! Social networks (Twitter/X) do not render SVG `og:image`, so the image meta points at
10//! a sibling `lean-ctx-wrapped.png` under the supplied `--base-url`; we never fabricate a
11//! URL — image meta is emitted only when a base URL is provided.
12
13use crate::core::wrapped::{format_tokens, WrappedReport};
14
15impl WrappedReport {
16    /// Renders the self-contained share page. `base_url` (optional) is the location the
17    /// user will host the page at; when present, absolute OG/Twitter image meta is emitted.
18    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 &rarr;</a>
48</body>
49</html>
50"#,
51            desc_attr = escape(&desc),
52        )
53    }
54
55    /// A ready-to-post one-liner for `gain --copy`. The opt-in permalink `url`
56    /// (once published) is appended when present. Honest about the estimate marker.
57    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
81/// Builds the Open Graph / Twitter meta block. Image meta only when a base URL is given.
82fn 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
117/// HTML/attribute escaping for data-derived strings.
118fn escape(s: &str) -> String {
119    s.replace('&', "&amp;")
120        .replace('<', "&lt;")
121        .replace('>', "&gt;")
122        .replace('"', "&quot;")
123        .replace('\'', "&#39;")
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        }
146    }
147
148    #[test]
149    fn page_is_self_contained_and_branded() {
150        let html = sample().to_share_html(None);
151        assert!(html.starts_with("<!DOCTYPE html>"));
152        assert!(html.contains("<svg"), "SVG must be embedded inline");
153        assert!(html.contains("</html>"));
154        assert!(
155            html.contains("leanctx.com"),
156            "viral CTA must link the brand"
157        );
158        assert!(html.contains("348.3M"), "must show the headline metric");
159    }
160
161    #[test]
162    fn without_base_url_no_image_meta() {
163        let html = sample().to_share_html(None);
164        assert!(
165            !html.contains("og:image"),
166            "must not fabricate an image URL without a base"
167        );
168        assert!(html.contains("name=\"twitter:card\" content=\"summary\""));
169    }
170
171    #[test]
172    fn with_base_url_emits_absolute_image_meta() {
173        let html = sample().to_share_html(Some("https://me.dev/wrapped/"));
174        assert!(html.contains("og:image\" content=\"https://me.dev/wrapped/lean-ctx-wrapped.png\""));
175        assert!(html.contains("summary_large_image"));
176        // Trailing slash on the base must be normalised (no double slash).
177        assert!(!html.contains("wrapped//lean-ctx-wrapped.png"));
178    }
179
180    #[test]
181    fn base_url_is_attribute_escaped() {
182        let html = sample().to_share_html(Some("https://me.dev/w?a=1&b=2"));
183        assert!(
184            html.contains("a=1&amp;b=2"),
185            "ampersands in the base URL must be escaped in attributes"
186        );
187        assert!(
188            !html.contains("a=1&b=2\""),
189            "a raw unescaped ampersand must not survive into an attribute"
190        );
191    }
192
193    #[test]
194    fn share_text_is_postable_and_honest() {
195        let txt = sample().share_text(None);
196        assert!(
197            txt.contains("348.3M"),
198            "headline metric must be in the share line"
199        );
200        assert!(txt.contains("lean-ctx"), "must name the brand");
201        assert!(!txt.contains("http"), "no URL when none is supplied");
202    }
203
204    #[test]
205    fn share_text_appends_permalink_and_estimate_marker() {
206        let mut r = sample();
207        r.pricing_estimated = true;
208        let txt = r.share_text(Some("https://leanctx.com/w/abc123"));
209        assert!(txt.ends_with("https://leanctx.com/w/abc123"));
210        assert!(
211            txt.contains("(est.)"),
212            "estimated pricing must be disclosed"
213        );
214    }
215}