Switch an item between its HTML and its Markdown (#285)
A feed that sends <source:markdown> beside its HTML (Scripting News) has its Markdown kept in entries.markdown, added to existing databases by an ALTER. A stored item gets it when a scan finds it told differently, the Markdown counting as a change, the way corrections arrive (#141). The API sends it as written and made HTML, cleaned by ammonia like the description. The item's page has a choice between its meta line and its buttons: HTML, Markdown or Markdown source, remembered in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,12 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
}
|
||||
if orm.execute_unprepared("SELECT markdown FROM entries LIMIT 0").await.is_err() {
|
||||
// Stored items get theirs as a scan finds them told differently, by the Markdown alone.
|
||||
orm.execute_unprepared("ALTER TABLE entries ADD COLUMN markdown TEXT")
|
||||
.await
|
||||
.context("adding entries.markdown")?;
|
||||
}
|
||||
if orm.execute_unprepared("SELECT body_hash FROM feeds LIMIT 0").await.is_err() {
|
||||
for sql in ["ALTER TABLE feeds ADD COLUMN body_hash TEXT", "ALTER TABLE feeds ADD COLUMN same_since BIGINT"] {
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
@@ -734,8 +740,8 @@ impl Db {
|
||||
.exec(
|
||||
"INSERT INTO entries
|
||||
(feed_id, guid, title, link, published, description, first_seen,
|
||||
image, duration, episode, season)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
image, duration, episode, season, markdown)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT DO NOTHING",
|
||||
vec![
|
||||
feed_id.into(),
|
||||
@@ -749,6 +755,7 @@ impl Db {
|
||||
e.duration.into(),
|
||||
e.episode.into(),
|
||||
e.season.into(),
|
||||
e.markdown.clone().into(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -760,7 +767,8 @@ impl Db {
|
||||
image = coalesce($5, image),
|
||||
duration = coalesce($6, duration),
|
||||
episode = coalesce($7, episode),
|
||||
season = coalesce($8, season)
|
||||
season = coalesce($8, season),
|
||||
markdown = coalesce($9, markdown)
|
||||
WHERE feed_id = $1 AND guid = $2",
|
||||
vec![
|
||||
feed_id.into(),
|
||||
@@ -771,6 +779,7 @@ impl Db {
|
||||
e.duration.into(),
|
||||
e.episode.into(),
|
||||
e.season.into(),
|
||||
e.markdown.clone().into(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -1131,6 +1140,10 @@ pub struct EntryRow {
|
||||
pub link: Option<String>,
|
||||
pub published: Option<i64>,
|
||||
pub description: Option<String>,
|
||||
/// The item's Markdown as written, when its feed sends it beside the HTML, and that Markdown
|
||||
/// made HTML and cleaned: the page switches between the three (#285).
|
||||
pub markdown: Option<String>,
|
||||
pub markdown_html: Option<String>,
|
||||
pub read: bool,
|
||||
pub flagged: bool,
|
||||
pub image: Option<String>,
|
||||
@@ -1409,7 +1422,7 @@ impl Db {
|
||||
let (limit, offset) = (a.p(limit), a.p(offset));
|
||||
let user = a.p(user_id);
|
||||
let sql = format!(
|
||||
"SELECT e.guid, e.feed_id, e.title, e.link, e.published, e.description,
|
||||
"SELECT e.guid, e.feed_id, e.title, e.link, e.published, e.description, e.markdown,
|
||||
coalesce(s.read, false) AS read, coalesce(s.flagged, false) AS flagged, e.image,
|
||||
coalesce(s.duration, e.duration) AS duration,
|
||||
e.episode, e.season, coalesce(s.position, 0) AS position
|
||||
@@ -1430,6 +1443,8 @@ impl Db {
|
||||
link: r.try_get("", "link")?,
|
||||
published: r.try_get("", "published")?,
|
||||
description: r.try_get("", "description")?,
|
||||
markdown: r.try_get("", "markdown")?,
|
||||
markdown_html: None,
|
||||
read: r.try_get("", "read")?,
|
||||
flagged: r.try_get("", "flagged")?,
|
||||
image: r.try_get("", "image")?,
|
||||
@@ -2338,11 +2353,12 @@ impl Db {
|
||||
let feed = a.p(feed_id);
|
||||
let guids: Vec<String> = entries.iter().map(|e| a.p(e.guid.clone())).collect();
|
||||
let sql = format!(
|
||||
"SELECT guid, title, description, image, duration, episode, season FROM entries
|
||||
"SELECT guid, title, description, image, duration, episode, season, markdown FROM entries
|
||||
WHERE feed_id = {feed} AND guid IN ({})",
|
||||
guids.join(", ")
|
||||
);
|
||||
type Stored = (Option<String>, Option<String>, Option<String>, Option<i64>, Option<i64>, Option<i64>);
|
||||
type Stored =
|
||||
(Option<String>, Option<String>, Option<String>, Option<i64>, Option<i64>, Option<i64>, Option<String>);
|
||||
let mut stored = std::collections::HashMap::<String, Stored>::new();
|
||||
for r in self.rows(&sql, a.0).await? {
|
||||
stored.insert(
|
||||
@@ -2354,6 +2370,7 @@ impl Db {
|
||||
r.try_get("", "duration")?,
|
||||
r.try_get("", "episode")?,
|
||||
r.try_get("", "season")?,
|
||||
r.try_get("", "markdown")?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2370,6 +2387,7 @@ impl Db {
|
||||
|| differs(&e.duration, &s.3)
|
||||
|| differs(&e.episode, &s.4)
|
||||
|| differs(&e.season, &s.5)
|
||||
|| differs(&e.markdown, &s.6)
|
||||
})
|
||||
})
|
||||
.map(|e| e.guid.clone())
|
||||
@@ -3315,6 +3333,14 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(changed, ["a", "b"].map(String::from).into());
|
||||
assert!(db.changed_items("f", &[]).await.unwrap().is_empty());
|
||||
|
||||
// A Markdown copy the feed starts sending is a change, and is stored (#285).
|
||||
let md = crate::feed::Entry { markdown: Some("*as it was*".into()), ..item("c", "C", Some("as it was")) };
|
||||
assert_eq!(db.changed_items("f", std::slice::from_ref(&md)).await.unwrap(), ["c"].map(String::from).into());
|
||||
assert!(!db.record_entry("f", &md).await.unwrap(), "rewritten, not new");
|
||||
let order = order_sql("published", "desc");
|
||||
let rows = db.entries_in(1, Some("f"), Filter::All, None, 0, 50, &order).await.unwrap();
|
||||
assert_eq!(rows.iter().find(|r| r.guid == "c").unwrap().markdown.as_deref(), Some("*as it was*"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user