diff --git a/src/epub/chapters.rs b/src/epub/chapters.rs index e165993..ac416e9 100644 --- a/src/epub/chapters.rs +++ b/src/epub/chapters.rs @@ -370,6 +370,14 @@ fn source_line(article: &Article) -> String { } } +/// The index's meta lead: the author, when known, ahead of [`source_line`]. +fn index_source_line(article: &Article) -> String { + match article.author_label() { + Some(author) => format!("{author} · {}", source_line(article)), + None => source_line(article), + } +} + /// "In This Issue": per section, each article's title, source, reading time and /// summary, linked to its chapter (§3.10). pub fn render_in_this_issue(issue: &Issue) -> Result { @@ -382,7 +390,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result { .map(|pick| IndexEntry { href: article_href(pick), title: pick.article.title.clone(), - source: source_line(&pick.article), + source: index_source_line(&pick.article), reading_minutes: pick.article.reading_minutes(), summary: summary_for(issue, pick).unwrap_or_default().to_string(), why: pick.why.clone(), @@ -891,14 +899,16 @@ mod tests { let mut issue = issue(); issue.lineup.picks[0].article.publication = Some("Example Journal".into()); issue.lineup.picks[1].article.publication = Some("Example Feed".into()); + issue.lineup.picks[1].article.author = Some("example feed".into()); let index = render_in_this_issue(&issue).unwrap(); assert!( index .xhtml - .contains("Example Feed · Example Journal · 6 min read") + .contains("A. Writer · Example Feed · Example Journal · 6 min read") ); assert!(!index.xhtml.contains("Example Feed · Example Feed")); + assert!(!index.xhtml.contains("example feed · Example Feed")); let article = render_article( &issue, diff --git a/src/types.rs b/src/types.rs index 65d88e0..e2b40a9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -167,6 +167,13 @@ impl Article { .then_some(publication) } + /// The trimmed author, unless empty or identical to the feed name. + pub fn author_label(&self) -> Option { + let author = self.author.as_deref()?.trim(); + (!author.is_empty() && !author.eq_ignore_ascii_case(self.feed_title.trim())) + .then(|| author.to_string()) + } + /// Stable EPUB chapter id used by TOC and rating links (implementation notes §12). pub fn chapter_id(&self) -> String { format!("art-{}", self.best_entry_id) @@ -929,6 +936,19 @@ mod tests { assert_eq!(article.publication_label(), None); } + #[test] + fn author_label_trims_and_omits_empty_or_matching_authors() { + let mut article = publication_article(); + article.author = Some(" Jane Doe ".into()); + assert_eq!(article.author_label().as_deref(), Some("Jane Doe")); + + article.author = Some(" ".into()); + assert_eq!(article.author_label(), None); + + article.author = Some(" a FEED ".into()); + assert_eq!(article.author_label(), None); + } + #[test] fn composite_social_score_matches_spec_formula() { let refs = vec![ diff --git a/src/web/issue.rs b/src/web/issue.rs index 1251ceb..8825cbe 100644 --- a/src/web/issue.rs +++ b/src/web/issue.rs @@ -876,6 +876,7 @@ struct FullEntry { title: String, href: String, dashboard_href: String, + author: Option, source: Source, reading_minutes: i64, is_lead: bool, @@ -1021,6 +1022,7 @@ pub async fn render_full( title: pick.article.title.clone(), href: article_href(date, pick.article.id), dashboard_href: format!("/dashboard/articles/{}", pick.article.id), + author: pick.article.author_label(), source: Source::new(&pick.article, &config, is_admin), reading_minutes: pick.article.reading_minutes(), is_lead: pick.is_lead, @@ -1946,6 +1948,13 @@ mod tests { #[tokio::test] async fn signed_in_full_issue_article_world_and_behind_render_private_content() { let (_dir, db, source) = seeded_issue(true).await; + let matching_author = &source.lineup.picks[1].article; + sqlx::query("UPDATE articles SET author = ? WHERE id = ?") + .bind(format!(" {} ", matching_author.feed_title.to_lowercase())) + .bind(matching_author.id) + .execute(db.pool()) + .await + .unwrap(); crate::web::users::add(&db, "reader", "correct horse battery", false) .await .unwrap(); @@ -1993,6 +2002,8 @@ mod tests { assert!(issue.contains("A short abstract for the second piece")); assert!(issue.contains("Why it")); assert!(issue.contains("Interests: Filesystems, Rust")); + assert!(issue.contains("A. Writer · Example Feed")); + assert!(!issue.contains("example feed · Example Feed")); assert!(issue.contains("World Briefing")); assert!(issue.contains("Behind the paper")); assert!(!issue.contains("Was this a good pick?")); diff --git a/src/web/public.rs b/src/web/public.rs index 73ba0c5..e98d61d 100644 --- a/src/web/public.rs +++ b/src/web/public.rs @@ -114,7 +114,7 @@ impl From<&Issue> for PublicIssue { PublicEntry { title: article.title.clone(), url: article.canonical_url.clone(), - author: article.author.clone(), + author: article.author_label(), source: article.feed_title.clone(), publication: article.publication_label(), reading_minutes: article.reading_minutes(), diff --git a/src/web/templates/feed_entry.html b/src/web/templates/feed_entry.html index ba31c70..b909c5b 100644 --- a/src/web/templates/feed_entry.html +++ b/src/web/templates/feed_entry.html @@ -1 +1 @@ -{% for section in issue.sections %}

{{ section.name }}

    {% for entry in section.entries %}
  • {{ entry.title }} — {{ entry.source }}{% match entry.publication %}{% when Some with (publication) %} · {{ publication }}{% when None %}{% endmatch %}{% match entry.summary %}{% when Some with (summary) %}

    {{ summary }}

    {% when None %}{% endmatch %}{% match entry.why %}{% when Some with (why) %}

    Why it's here: {{ why }}

    {% when None %}{% endmatch %}{% if !entry.comment_links.is_empty() %} · {% for link in entry.comment_links %}{{ link.label }}{% endfor %}{% endif %}
  • {% endfor %}
{% endfor %} +{% for section in issue.sections %}

{{ section.name }}

    {% for entry in section.entries %}
  • {{ entry.title }} — {% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{{ entry.source }}{% match entry.publication %}{% when Some with (publication) %} · {{ publication }}{% when None %}{% endmatch %}{% match entry.summary %}{% when Some with (summary) %}

    {{ summary }}

    {% when None %}{% endmatch %}{% match entry.why %}{% when Some with (why) %}

    Why it's here: {{ why }}

    {% when None %}{% endmatch %}{% if !entry.comment_links.is_empty() %} · {% for link in entry.comment_links %}{{ link.label }}{% endfor %}{% endif %}
  • {% endfor %}
{% endfor %} diff --git a/src/web/templates/issue_full.html b/src/web/templates/issue_full.html index 3222e8a..252d9c5 100644 --- a/src/web/templates/issue_full.html +++ b/src/web/templates/issue_full.html @@ -5,7 +5,7 @@

The Brief

{{ front_page_html|safe }}
{% if (page.is_admin() && read_href.is_some()) || downloads.is_some() %}
{% if page.is_admin() %}{% match read_href %}{% when Some with (href) %}Read in BookOrbit{% when None %}{% endmatch %}{% endif %}{% match downloads %}{% when Some with (downloads) %}{% if downloads.others.is_empty() %}Download {{ downloads.primary.label }}{% else %}{% endif %}{% when None %}{% endmatch %}
{% endif %}

In This Issue

- {% for section in sections %}

{{ section.name }}

    {% for entry in section.entries %}
  • {{ entry.title }}

    {% let source = entry.source %}{% include "_source.html" %} · {{ entry.reading_minutes }} min read{% if page.is_admin() %} · dashboard{% endif %}

    {% if !entry.summary.is_empty() %}

    {{ entry.summary }}

    {% endif %}{% match entry.why %}{% when Some with (why) %}

    Why it's here: {{ why }}

    {% when None %}{% endmatch %}{% match entry.understanding %}{% when Some with (line) %}

    {{ line }}

    {% when None %}{% endmatch %}{% match entry.rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}
  • {% endfor %}
{% endfor %} + {% for section in sections %}

{{ section.name }}

    {% for entry in section.entries %}
  • {{ entry.title }}

    {% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{% let source = entry.source %}{% include "_source.html" %} · {{ entry.reading_minutes }} min read{% if page.is_admin() %} · dashboard{% endif %}

    {% if !entry.summary.is_empty() %}

    {{ entry.summary }}

    {% endif %}{% match entry.why %}{% when Some with (why) %}

    Why it's here: {{ why }}

    {% when None %}{% endmatch %}{% match entry.understanding %}{% when Some with (line) %}

    {{ line }}

    {% when None %}{% endmatch %}{% match entry.rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}
  • {% endfor %}
{% endfor %}
{% if has_world || has_behind %}{% endif %}