Show notes cut off mid-tag give way to the item's description
libsyn served Daily Meditation Podcast's content:encoded cut at the '>' inside a Tailwind class pasted from a web app, so 57 items began halfway through a tag and the page showed the rest of it as text. Their description was whole. A body that closes an attribute list before any tag opens now falls back to the description, for RSS and Atom alike. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
This commit is contained in:
@@ -17,6 +17,11 @@ The long form, with what was wrong before and how it was found, is in
|
|||||||
- The database no longer records when accounts, subscriptions and sign-ins were created. Nothing
|
- The database no longer records when accounts, subscriptions and sign-ins were created. Nothing
|
||||||
ever read it, and an existing database drops the columns on its next start.
|
ever read it, and an existing database drops the columns on its next start.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Show notes that the podcast's host cut off in the middle of a tag no longer open with a scrap of
|
||||||
|
HTML: the item's other copy of its notes is shown instead. Daily Meditation Podcast had 57.
|
||||||
|
|
||||||
## [0.5.1] - 2026-09-12
|
## [0.5.1] - 2026-09-12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
39
src/feed.rs
39
src/feed.rs
@@ -340,7 +340,7 @@ fn from_rss(ch: rss::Channel, bytes: &[u8]) -> ParsedFeed {
|
|||||||
link: non_empty(item.link()),
|
link: non_empty(item.link()),
|
||||||
published: item.pub_date().and_then(parse_date),
|
published: item.pub_date().and_then(parse_date),
|
||||||
// Content wins over description, as __getEntries preferred entry.content.
|
// Content wins over description, as __getEntries preferred entry.content.
|
||||||
description: non_empty(item.content()).or_else(|| non_empty(item.description())),
|
description: body(item.content(), item.description()),
|
||||||
categories: item
|
categories: item
|
||||||
.categories()
|
.categories()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -406,11 +406,7 @@ fn from_atom(feed: atom_syndication::Feed) -> ParsedFeed {
|
|||||||
title: non_empty(Some(e.title().as_str())),
|
title: non_empty(Some(e.title().as_str())),
|
||||||
link: alt.map(str::to_owned),
|
link: alt.map(str::to_owned),
|
||||||
published: e.published().or(Some(e.updated())).map(|d| d.timestamp()),
|
published: e.published().or(Some(e.updated())).map(|d| d.timestamp()),
|
||||||
description: e
|
description: body(e.content().and_then(|c| c.value()), e.summary().map(|s| s.as_str())),
|
||||||
.content()
|
|
||||||
.and_then(|c| c.value())
|
|
||||||
.or_else(|| e.summary().map(|s| s.as_str()))
|
|
||||||
.map(str::to_owned),
|
|
||||||
categories: e.categories().iter().map(|c| c.term().to_owned()).collect(),
|
categories: e.categories().iter().map(|c| c.term().to_owned()).collect(),
|
||||||
explicit: false,
|
explicit: false,
|
||||||
image: None,
|
image: None,
|
||||||
@@ -455,6 +451,25 @@ fn non_empty(s: Option<&str>) -> Option<String> {
|
|||||||
s.map(str::trim).filter(|s| !s.is_empty()).map(str::to_owned)
|
s.map(str::trim).filter(|s| !s.is_empty()).map(str::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An item's show notes: its full body when that is whole, else its description.
|
||||||
|
///
|
||||||
|
/// libsyn served Daily Meditation Podcast's `content:encoded` cut at the `>` inside a class name
|
||||||
|
/// pasted from a web app (`[&:has([data-writing-block])>*]:pointer-events-auto`), so the body
|
||||||
|
/// began halfway through a tag and the page showed the rest of the tag as text. The same item's
|
||||||
|
/// `description` was whole. With no description to fall back on, a damaged body beats none.
|
||||||
|
fn body(content: Option<&str>, description: Option<&str>) -> Option<String> {
|
||||||
|
non_empty(content)
|
||||||
|
.filter(|c| !starts_mid_tag(c))
|
||||||
|
.or_else(|| non_empty(description))
|
||||||
|
.or_else(|| non_empty(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text that closes an attribute list (`">`) before any tag has opened is the tail of a tag whose
|
||||||
|
/// start was cut off.
|
||||||
|
fn starts_mid_tag(html: &str) -> bool {
|
||||||
|
html[..html.find('<').unwrap_or(html.len())].contains("\">")
|
||||||
|
}
|
||||||
|
|
||||||
/// The picture to show beside an item, in order of how deliberate it is:
|
/// The picture to show beside an item, in order of how deliberate it is:
|
||||||
/// `itunes:image`, then Media RSS `media:thumbnail`, then a `media:content` that is an
|
/// `itunes:image`, then Media RSS `media:thumbnail`, then a `media:content` that is an
|
||||||
/// image, and finally an image enclosure -- which is how a blog's article picture arrives
|
/// image, and finally an image enclosure -- which is how a blog's article picture arrives
|
||||||
@@ -548,6 +563,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_body_cut_off_mid_tag_gives_way_to_the_description() {
|
||||||
|
// How libsyn served Daily Meditation Podcast #3477: content:encoded began inside a tag.
|
||||||
|
let cut = r#"*]:pointer-events-auto R6Vx5W_threadScrollVars" dir="auto" data-turn="assistant"> <p>What if</p>"#;
|
||||||
|
let whole = r#"<div class="[&:has([data-writing-block])>*]:pointer-events-auto"><p>What if</p></div>"#;
|
||||||
|
assert_eq!(body(Some(cut), Some(whole)).as_deref(), Some(whole));
|
||||||
|
assert_eq!(body(Some("<p>Notes</p>"), Some("Summary")).as_deref(), Some("<p>Notes</p>"), "a whole body wins");
|
||||||
|
assert_eq!(body(Some("Plain notes, no tags."), Some("Summary")).as_deref(), Some("Plain notes, no tags."));
|
||||||
|
assert_eq!(body(Some(cut), None).as_deref(), Some(cut), "a damaged body beats none");
|
||||||
|
assert_eq!(body(None, Some("Summary")).as_deref(), Some("Summary"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn feed_level_explicit_overrides_entries() {
|
fn feed_level_explicit_overrides_entries() {
|
||||||
let xml = br#"<?xml version="1.0"?>
|
let xml = br#"<?xml version="1.0"?>
|
||||||
|
|||||||
Reference in New Issue
Block a user