diff --git a/internal/explainer/explainer.go b/internal/explainer/explainer.go index bc70da0..40840d5 100644 --- a/internal/explainer/explainer.go +++ b/internal/explainer/explainer.go @@ -139,29 +139,36 @@ func (e *Explainer) summarize(headline, body string) (string, error) { func formatSummary(raw string) (plain, htmlBody string) { lines := strings.Split(raw, "\n") var bullets []string + sawMarker := false for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } - // Normalize common bullet markers - for _, prefix := range []string{"- ", "* ", "• ", "·"} { - if strings.HasPrefix(line, prefix) { - line = strings.TrimSpace(line[len(prefix):]) - break - } - } // Skip header-ish lines the model sometimes adds low := strings.ToLower(line) if strings.HasPrefix(low, "summary:") || strings.HasPrefix(low, "tl;dr") { continue } + // Normalize common bullet markers; only count this line if it had one. + hadMarker := false + for _, prefix := range []string{"- ", "* ", "• ", "·"} { + if strings.HasPrefix(line, prefix) { + line = strings.TrimSpace(line[len(prefix):]) + hadMarker = true + break + } + } + if !hadMarker { + continue + } + sawMarker = true if line == "" { continue } bullets = append(bullets, line) } - if len(bullets) == 0 { + if !sawMarker || len(bullets) == 0 { // Model produced something but we couldn't parse bullets — pass through. return raw, "
" + html.EscapeString(raw) + "
" } diff --git a/internal/explainer/explainer_test.go b/internal/explainer/explainer_test.go new file mode 100644 index 0000000..4d1b5d4 --- /dev/null +++ b/internal/explainer/explainer_test.go @@ -0,0 +1,110 @@ +package explainer + +import ( + "strings" + "testing" +) + +func TestFormatSummary(t *testing.T) { + cases := []struct { + name string + in string + wantPlain string + wantHTML string + }{ + { + name: "dash bullets", + in: "- First point.\n- Second point.\n- Third point.", + wantPlain: "• First point.\n• Second point.\n• Third point.", + wantHTML: "", + }, + { + name: "asterisk bullets", + in: "* alpha\n* beta", + wantPlain: "• alpha\n• beta", + wantHTML: "", + }, + { + name: "unicode bullets", + in: "• one\n• two", + wantPlain: "• one\n• two", + wantHTML: "", + }, + { + name: "blank lines ignored", + in: "- a\n\n- b\n\n\n- c", + wantPlain: "• a\n• b\n• c", + wantHTML: "", + }, + { + name: "summary header stripped", + in: "Summary:\n- first\n- second", + wantPlain: "• first\n• second", + wantHTML: "", + }, + { + name: "tldr header stripped", + in: "TL;DR of the article\n- key point\n- other point", + wantPlain: "• key point\n• other point", + wantHTML: "", + }, + { + name: "single bullet", + in: "- just one", + wantPlain: "• just one", + wantHTML: "", + }, + { + name: "html escaped in bullets", + in: `- A "quote" & `, + wantPlain: `• A "quote" & `, + wantHTML: "", + }, + { + name: "leading whitespace trimmed", + in: " - first\n - second ", + wantPlain: "• first\n• second", + wantHTML: "", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + gotPlain, gotHTML := formatSummary(c.in) + if gotPlain != c.wantPlain { + t.Errorf("plain:\n got %q\n want %q", gotPlain, c.wantPlain) + } + if gotHTML != c.wantHTML { + t.Errorf("html:\n got %q\n want %q", gotHTML, c.wantHTML) + } + }) + } +} + +func TestFormatSummary_NoBulletsPassthrough(t *testing.T) { + in := "Just a single paragraph with no bullets at all." + plain, htmlBody := formatSummary(in) + if plain != in { + t.Errorf("plain should pass through raw, got %q", plain) + } + if !strings.HasPrefix(htmlBody, "
") || !strings.HasSuffix(htmlBody, "
") { + t.Errorf("html should wrap raw in
, got %q", htmlBody)
+	}
+	if !strings.Contains(htmlBody, "single paragraph") {
+		t.Errorf("html should contain the body text, got %q", htmlBody)
+	}
+}
+
+func TestIsQuestionReaction(t *testing.T) {
+	want := []string{"❓", "❔", "⁉", "⁉️", "🤔", "?", "?"}
+	for _, k := range want {
+		if !IsQuestionReaction(k) {
+			t.Errorf("expected %q to trigger explain", k)
+		}
+	}
+	notQuestion := []string{"👍", "👎", "🔥", "", "??", "❤️"}
+	for _, k := range notQuestion {
+		if IsQuestionReaction(k) {
+			t.Errorf("expected %q to NOT trigger explain", k)
+		}
+	}
+}