Add feed/reader mode with full-article capture at ingest
Reader mode presents the stories on a page one at a time in a focused overlay, marking each read as it's shown. Left/right arrows (or the header book button / `f`) page through them; read stories dim on the grid. Read state is device-local in localStorage. Backing this required actually capturing article bodies, which Pete wasn't doing — it kept only the RSS <description> lede and discarded content:encoded: - stories.content column (idempotent migration; old rows fall back to lede) - parser keeps content:encoded as paragraph-preserving text - article fetch already done for paywall detection now also returns its body, so ingest stores the richer of feed-content vs scraped body with no extra request (prefers the archive snapshot body for paywalled stories) - GET /api/article?id= serves the stored text; card queries now select id and expose it as data-id for the reader Tests cover content extraction, the storage round-trip, and the article endpoint + card rendering end to end.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package ingestion
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractLede(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -69,3 +72,26 @@ func TestExtractLede(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractContentText(t *testing.T) {
|
||||
if got := extractContentText(""); got != "" {
|
||||
t.Errorf("empty input = %q, want empty", got)
|
||||
}
|
||||
if got := extractContentText(" \n "); got != "" {
|
||||
t.Errorf("whitespace-only input = %q, want empty", got)
|
||||
}
|
||||
|
||||
in := `<p>First paragraph with <a href="/x">a link</a> & entity.</p>` +
|
||||
`<p>Second line one.<br>line two.</p><ul><li>item</li></ul>`
|
||||
got := extractContentText(in)
|
||||
want := "First paragraph with a link & entity.\n\nSecond line one.\nline two.\n\nitem"
|
||||
if got != want {
|
||||
t.Errorf("extractContentText:\n got %q\n want %q", got, want)
|
||||
}
|
||||
|
||||
// Output is capped so a runaway body can't bloat the row.
|
||||
long := "<p>" + strings.Repeat("x", maxContentChars+500) + "</p>"
|
||||
if n := len(extractContentText(long)); n > maxContentChars {
|
||||
t.Errorf("capped length = %d, want <= %d", n, maxContentChars)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user