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            percentile: Some(99),
146        }
147    }
148
149    #[test]
150    fn page_is_self_contained_and_branded() {
151        let html = sample().to_share_html(None);
152        assert!(html.starts_with("<!DOCTYPE html>"));
153        assert!(html.contains("<svg"), "SVG must be embedded inline");
154        assert!(html.contains("</html>"));
155        assert!(
156            html.contains("leanctx.com"),
157            "viral CTA must link the brand"
158        );
159        assert!(html.contains("348.3M"), "must show the headline metric");
160    }
161
162    #[test]
163    fn without_base_url_no_image_meta() {
164        let html = sample().to_share_html(None);
165        assert!(
166            !html.contains("og:image"),
167            "must not fabricate an image URL without a base"
168        );
169        assert!(html.contains("name=\"twitter:card\" content=\"summary\""));
170    }
171
172    #[test]
173    fn with_base_url_emits_absolute_image_meta() {
174        let html = sample().to_share_html(Some("https://me.dev/wrapped/"));
175        assert!(html.contains("og:image\" content=\"https://me.dev/wrapped/lean-ctx-wrapped.png\""));
176        assert!(html.contains("summary_large_image"));
177        // Trailing slash on the base must be normalised (no double slash).
178        assert!(!html.contains("wrapped//lean-ctx-wrapped.png"));
179    }
180
181    #[test]
182    fn base_url_is_attribute_escaped() {
183        let html = sample().to_share_html(Some("https://me.dev/w?a=1&b=2"));
184        assert!(
185            html.contains("a=1&amp;b=2"),
186            "ampersands in the base URL must be escaped in attributes"
187        );
188        assert!(
189            !html.contains("a=1&b=2\""),
190            "a raw unescaped ampersand must not survive into an attribute"
191        );
192    }
193
194    #[test]
195    fn share_text_is_postable_and_honest() {
196        let txt = sample().share_text(None);
197        assert!(
198            txt.contains("348.3M"),
199            "headline metric must be in the share line"
200        );
201        assert!(txt.contains("lean-ctx"), "must name the brand");
202        assert!(!txt.contains("http"), "no URL when none is supplied");
203    }
204
205    #[test]
206    fn share_text_appends_permalink_and_estimate_marker() {
207        let mut r = sample();
208        r.pricing_estimated = true;
209        let txt = r.share_text(Some("https://leanctx.com/w/abc123"));
210        assert!(txt.ends_with("https://leanctx.com/w/abc123"));
211        assert!(
212            txt.contains("(est.)"),
213            "estimated pricing must be disclosed"
214        );
215    }
216}