Skip to content

Commit

Permalink
feat: improve post body rendering on Web UI (#106)
Browse files Browse the repository at this point in the history
Previously, the "Rendered" view was parsed from the raw XML feed on the
front-end. The logic on determining which field to show as posts' bodies
is arbitrary. Since #100, I changed the logic to recognize more post
body types. However, such change was not reflected on the Web UI. This
PR updates the Web UI to show the expected body content.

Besides, I made some visual tweaks to the Rendered view to show more
information including post's publication date, feed's description, etc.

In addition, The status bar will now show the number of posts on
successful fetches.
  • Loading branch information
shouya authored Apr 5, 2024
1 parent 877b6c9 commit eaefb9a
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 76 deletions.
101 changes: 39 additions & 62 deletions inspector/src/FeedInspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ export class FeedInspector {
this.filter_schema = null;
this.current_endpoint = null;
this.raw_editor = null;
this.raw_feed_content = null;
this.json_preview_editor = null;
this.json_preview_content = null;
this.preview = null;
}

async init() {
Expand Down Expand Up @@ -237,58 +236,73 @@ export class FeedInspector {
$(`#feed-preview #${mode}`).classList.add("hidden");
}

const raw_feed_xml_xml = this.raw_feed_content;
const preview = this.preview;
const function_name = `render_feed_${mode}`;
if (this[function_name]) {
this[function_name](raw_feed_xml_xml);
this[function_name](preview);
}
});
}

async render_feed_rendered(raw_feed_xml_xml) {
const parsed = parse_feed(raw_feed_xml_xml);
if (!parsed) {
console.error("Failed to parse feed");
return;
}

async render_feed_rendered({ unified }) {
$("#feed-preview #rendered").innerHTML = "";
const title_node = elt("h2", { class: "feed-title" }, parsed.title);
const title_node = elt(
"h3",
{ class: "feed-title" },
elt("a", { href: unified.link }, unified.title),
);
const description_node = elt(
"div",
{ class: "feed-description" },
unified.description,
);

$("#feed-preview #rendered").appendChild(title_node);
$("#feed-preview #rendered").appendChild(description_node);

const sanitizer = new HtmlSanitizer({});

for (const post of parsed.posts) {
const post_content = elt("p", { class: "feed-post-content" }, []);
post_content.innerHTML = sanitizer.sanitizeHtml(post.content || "");
for (const post of unified.posts) {
const post_body = elt("div", { class: "feed-post-body" }, []);
post_body.innerHTML = sanitizer.sanitizeHtml(post.body || "");

let expand = elt("span", { class: "feed-post-show-all" }, "(expand)");
expand.addEventListener("click", (e) => {
post_body.classList.toggle("expanded");
expand.innerText = post_body.classList.contains("expanded")
? "(collapse)"
: "(expand)";
});

const post_node = elt("div", { class: "feed-post" }, [
elt(
"h3",
{ class: "feed-post-title" },
elt("a", { class: "feed-post-link", href: post.link }, post.title),
),
post_content,
elt("p", { class: "feed-post-date" }, post.date),
elt("div", { class: "feed-post-date" }, post.date),
post_body,
expand,
]);
$("#feed-preview #rendered").appendChild(post_node);
}
}

async render_feed_raw(raw_feed_xml_xml) {
if (this.raw_editor.state.doc.toString() === raw_feed_xml_xml) {
async render_feed_raw({ raw }) {
if (this.raw_editor.state.doc.toString() === raw) {
return;
}
this.raw_editor.dispatch({
changes: {
from: 0,
to: this.raw_editor.state.doc.length,
insert: raw_feed_xml_xml,
insert: raw,
},
});
}

async render_feed_json(_unused) {
const json = JSON.stringify(this.json_preview_content, null, 2);
async render_feed_json({ json }) {
json = JSON.stringify(json, null, 2);
if (this.json_preview_editor.state.doc.toString() === json) {
return;
}
Expand Down Expand Up @@ -424,15 +438,13 @@ export class FeedInspector {
let status_text = "";

if (resp.status != 200) {
status_text = `Failed fetching ${path}`;
status_text += ` (status: ${resp.status} ${resp.statusText})`;
status_text = `Failed fetching ${path} (status: ${resp.status} ${resp.statusText})`;
this.update_feed_error(await resp.text());
} else {
this.update_feed_error(null);
const preview = await resp.json();
this.raw_feed_content = preview.content;
this.json_preview_content = preview.json;
status_text = `Fetched ${path} (${preview.content_type})`;
this.preview = preview;
status_text = `Fetched feed with ${preview.post_count} posts from ${path} (${preview.content_type})`;
}

status_text += ` in ${performance.now() - time_start}ms.`;
Expand Down Expand Up @@ -493,38 +505,3 @@ export class FeedInspector {
setTimeout(() => node.remove(), 4000);
}
}

// return {title: string, posts: [post]}
// post: {title: string, link: string, date: string, content: string}
function parse_feed(xml) {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");

if (doc.documentElement.tagName == "rss") {
const title = doc.querySelector("channel > title").textContent.trim();
const posts = Array.from(doc.querySelectorAll("item")).map((item) => {
return {
title: item.querySelector("title")?.textContent?.trim(),
link: item.querySelector("link")?.textContent?.trim(),
date: item.querySelector("pubDate")?.textContent?.trim(),
content: item.querySelector("description")?.textContent?.trim(),
};
});

return { title, posts };
} else if (doc.documentElement.tagName == "feed") {
const title = doc.querySelector("feed > title").textContent.trim();
const posts = Array.from(doc.querySelectorAll("entry")).map((entry) => {
return {
title: entry.querySelector("title")?.textContent?.trim(),
link: entry.querySelector("link")?.getAttribute("href"),
date: entry.querySelector("published")?.textContent?.trim(),
content: entry.querySelector("content")?.textContent?.trim(),
};
});

return { title, posts };
}

return null;
}
42 changes: 37 additions & 5 deletions inspector/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -445,22 +445,54 @@ ul#filter-list {
.feed-title {
margin: 0;
padding: 0.5rem;
width: 100%;
background-color: #fff;
}

.feed-post {
.feed-description {
padding: 0.5rem;
font-size: 0.9em;
}

.feed-post {
padding: 0.8rem;
border-radius: 0.5rem;
border: 1px solid #ddd;
border-radius: 0.3rem;
background-color: #fafafa;
max-width: 90%;
margin: 0.5rem 0.5rem;

.feed-post-show-all {
cursor: pointer;
font-size: 0.8em;
margin-right: 0.5rem;
color: #666;
}

.feed-post-title {
margin: 0;
}

.feed-post-date {
font-size: 0.9em;
color: #666;
margin-top: 0.6rem;
padding-bottom: 0.5rem;
font-size: 1.2em;
border-bottom: 1px solid #ddd;
}

.feed-post-body {
padding: 1rem;
font-size: 0.9rem;
max-height: 20rem;
overflow-y: scroll;

img {
max-width: 100%;
}
}

.feed-post-body.expanded {
max-height: initial;
}
}
}

Expand Down
89 changes: 81 additions & 8 deletions src/feed.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod conversion;
mod extension;
mod preview;

use chrono::DateTime;
use paste::paste;
Expand All @@ -16,6 +17,9 @@ use crate::util::Result;

use extension::ExtensionExt;

use self::preview::FeedPreview;
use self::preview::PostPreview;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum Feed {
Expand All @@ -42,6 +46,13 @@ impl Feed {
}
}

pub fn post_count(&self) -> usize {
match self {
Feed::Rss(channel) => channel.items.len(),
Feed::Atom(feed) => feed.entries.len(),
}
}

pub fn into_format(self, format: FeedFormat) -> Self {
use conversion::W;

Expand All @@ -62,6 +73,33 @@ impl Feed {
}
}

pub fn preview(&self) -> FeedPreview {
let title = self.title().to_string();
let link = self.link().to_string();
let description = self.description().map(String::from);

// TODO: inefficient clone, consider using references
let posts = match self {
Feed::Rss(channel) => channel
.items
.iter()
.map(|item| Post::Rss(item.clone()).preview())
.collect(),
Feed::Atom(feed) => feed
.entries
.iter()
.map(|entry| Post::Atom(entry.clone()).preview())
.collect(),
};

FeedPreview {
title,
link,
description,
posts,
}
}

pub fn from_rss_content(content: &[u8]) -> Result<Self> {
let cursor = std::io::Cursor::new(content);
let channel = rss::Channel::read_from(cursor)?;
Expand Down Expand Up @@ -168,14 +206,6 @@ impl Feed {
}
}

#[allow(unused)]
pub fn title(&self) -> &str {
match self {
Feed::Rss(channel) => &channel.title,
Feed::Atom(feed) => feed.title.as_str(),
}
}

pub fn merge(&mut self, other: Feed) -> Result<()> {
match (self, other) {
(Feed::Rss(channel), Feed::Rss(other)) => {
Expand Down Expand Up @@ -281,6 +311,34 @@ impl From<&FromScratch> for Feed {
}
}

// Generic field accessors
impl Feed {
pub fn title(&self) -> &str {
match self {
Feed::Rss(channel) => &channel.title,
Feed::Atom(feed) => feed.title.as_str(),
}
}

fn link(&self) -> &str {
match self {
Feed::Rss(channel) => &channel.link,
Feed::Atom(feed) => feed
.links
.first()
.map(|link| link.href.as_str())
.unwrap_or_default(),
}
}

fn description(&self) -> Option<&str> {
match self {
Feed::Rss(channel) => Some(channel.description.as_str()),
Feed::Atom(feed) => feed.subtitle.as_ref().map(|s| s.value.as_str()),
}
}
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum Post {
Expand All @@ -296,6 +354,21 @@ enum PostField {
}

impl Post {
pub fn preview(&self) -> PostPreview {
let title = self.title().map(String::from).unwrap_or_default();
let author = self.author().map(String::from);
let link = self.link().map(String::from).unwrap_or_default();
let body = self.first_body().map(String::from);
let published = self.pub_date();

PostPreview {
title,
author,
link,
body,
date: published,
}
}
pub fn set_pub_date(&mut self, date: DateTime<chrono::FixedOffset>) {
match self {
Post::Rss(item) => {
Expand Down
19 changes: 19 additions & 0 deletions src/feed/preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use chrono::{DateTime, FixedOffset};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct FeedPreview {
pub title: String,
pub link: String,
pub description: Option<String>,
pub posts: Vec<PostPreview>,
}

#[derive(Debug, Serialize)]
pub struct PostPreview {
pub title: String,
pub author: Option<String>,
pub link: String,
pub body: Option<String>,
pub date: Option<DateTime<FixedOffset>>,
}
4 changes: 3 additions & 1 deletion src/server/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ async fn preview_handler(
let feed = endpoint_service.run(endpoint_param).await?;
let body = json!({
"content_type": feed.content_type(),
"content": feed.serialize(true)?,
"post_count": feed.post_count(),
"unified": feed.preview(),
"raw": feed.serialize(true)?,
"json": feed
});
Ok(Json(body))
Expand Down

0 comments on commit eaefb9a

Please sign in to comment.