get authors also from feed data + some css

This commit is contained in:
Thomas Koch 2025-01-12 20:12:27 +02:00
parent 433afd364f
commit 603b4f2a29
5 changed files with 140 additions and 39 deletions

View File

@ -1,5 +1,5 @@
p, h1, h2, h3, h4, h5, h6, small { p, h1, h2, h3, h4, h5, h6, small {
max-width: 48em; max-width: 48em;
} }
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
@ -9,34 +9,71 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
ul, ol { ul, ol {
/* account for the 1em -webkit-margin-start for the list icon */ /* account for the 1em -webkit-margin-start for the list icon */
max-width: 45em; max-width: 45em;
} }
ul,ol,dl, p { ul,ol,dl, p {
line-height: 1.4; margin-top: 0.3em;
margin-bottom: 0.3em;
line-height: 1.2;
} }
body { ul, ol {
margin-top: 1em; padding-inline-start: 1.5em;
margin-bottom: 1em;
} }
#bodydiv {
margin: auto;
max-width: 80em;
}
#maincontainer aside img {
max-width: 10em;
}
#maincontainer main blockquote {
margin-left: 0;
margin-right: 10px;
box-shadow: 10px 0px 0px 0px #C4C4C4;
}
blockquote, pre code {
padding: 0.5ex 0;
display: block;
background-color: #EEE;
}
#maincontainer main * {
max-width: 100%;
}
#maincontainer main pre {
overflow-x: auto;
}
.entry_meta {
margin-bottom: 1em;
}
@media only screen and (min-width: 1024px) {
#maincontainer { #maincontainer {
display: flex; display: flex;
max-width: 80em;
} }
#maincontainer main { #maincontainer main {
max-width: 50em; max-width: 50em;
} flex: 5;
#maincontainer main * {
max-width: 50em;
} }
#maincontainer aside { #maincontainer aside {
margin-left: 5em; margin-left: 5em;
max-width: 25em; max-width: 15em;
flex: 1;
}
#maincontainer aside img {
margin: auto;
display: block;
} }
article > h2.entry_header { article > h2.entry_header {
@ -44,7 +81,6 @@ article > h2.entry_header {
} }
.entry_meta { .entry_meta {
border: 1px thin;
padding: 3px 0; padding: 3px 0;
background-color: LightBlue; background-color: LightBlue;
} }
@ -53,7 +89,8 @@ hr.entry_sep {
border: none; border: none;
} }
hr.entry_sep::before { hr.entry_sep::before {
content: '* * *'; content: '* * *';
display: block; display: block;
text-align: center; text-align: center;
}
} }

View File

@ -4,6 +4,7 @@ use feed_rs::model::Entry;
use feed_rs::model::Feed; use feed_rs::model::Feed;
use ron::ser::{to_string_pretty, PrettyConfig}; use ron::ser::{to_string_pretty, PrettyConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::AsRef; use std::convert::AsRef;
use std::fs; use std::fs;
use std::io::BufReader; use std::io::BufReader;
@ -13,9 +14,6 @@ use ureq::http::Response;
use ureq::Body; use ureq::Body;
use url::Url; use url::Url;
/// How many feed entries should be included in the planet
const ENTRIES_LEN: usize = 10;
#[derive(Deserialize, Serialize, Default)] #[derive(Deserialize, Serialize, Default)]
pub struct FetchData { pub struct FetchData {
pub etag: String, pub etag: String,
@ -135,8 +133,12 @@ impl FeedStore {
Ok(Some(parser.parse(BufReader::new(file))?)) Ok(Some(parser.parse(BufReader::new(file))?))
} }
pub fn collect(&self, feed_configs: &Vec<super::FeedConfig>) -> (Vec<Feed>, Vec<Entry>) { pub fn collect(
let mut feeds = Vec::new(); &self,
feed_configs: &Vec<super::FeedConfig>,
max_entries: usize,
) -> (HashMap<String, Feed>, Vec<Entry>) {
let mut feeds = HashMap::new();
let mut entries = Vec::new(); let mut entries = Vec::new();
for feed_config in feed_configs { for feed_config in feed_configs {
@ -154,21 +156,24 @@ impl FeedStore {
Ok(None) => continue, Ok(None) => continue,
Ok(Some(f)) => f, Ok(Some(f)) => f,
}; };
for entry in &mut feed.entries {
entry.source = Some(feed_config.url.clone());
}
entries.append(&mut std::mem::take(&mut feed.entries)); entries.append(&mut std::mem::take(&mut feed.entries));
feeds.push(feed); feeds.insert(feed_config.url.clone(), feed);
// optimization to reduce memory usage // optimization to reduce memory usage
if entries.len() > 4 * ENTRIES_LEN { if entries.len() > 4 * max_entries {
entries = trim_entries(entries); entries = trim_entries(entries, max_entries);
} }
} }
(feeds, trim_entries(entries)) (feeds, trim_entries(entries, max_entries))
} }
} }
fn trim_entries(mut entries: Vec<Entry>) -> Vec<Entry> { fn trim_entries(mut entries: Vec<Entry>, max_entries: usize) -> Vec<Entry> {
entries.sort_by_key(|e| std::cmp::Reverse(e.updated.or(e.published).unwrap_or_default())); entries.sort_by_key(|e| std::cmp::Reverse(e.updated.or(e.published).unwrap_or_default()));
entries.truncate(ENTRIES_LEN); entries.truncate(max_entries);
entries entries
} }

View File

@ -66,6 +66,8 @@ struct Config {
out_dir: String, out_dir: String,
/// templates folder /// templates folder
templates_dir: String, templates_dir: String,
/// How many feed entries should be included in the planet
max_entries: usize,
} }
pub fn to_checked_pathbuf(dir: &str) -> PathBuf { pub fn to_checked_pathbuf(dir: &str) -> PathBuf {

View File

@ -2,21 +2,25 @@ use crate::feed_store::FeedStore;
use crate::to_checked_pathbuf; use crate::to_checked_pathbuf;
use crate::Config; use crate::Config;
use anyhow::Result; use anyhow::Result;
use feed_rs::model::Feed;
use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use tera::Tera; use tera::{from_value, Tera};
pub fn build(config: &Config, feed_store: &FeedStore) -> Result<()> { pub fn build(config: &Config, feed_store: &FeedStore) -> Result<()> {
let tera = create_tera(&config.templates_dir)?; let mut tera = create_tera(&config.templates_dir)?;
let out_dir = to_checked_pathbuf(&config.out_dir); let out_dir = to_checked_pathbuf(&config.out_dir);
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let (feeds, entries) = feed_store.collect(&config.feeds); let (feeds, entries): (HashMap<String, Feed>, _) =
feed_store.collect(&config.feeds, config.max_entries);
context.insert("feeds", &feeds); context.insert("feeds", &feeds);
context.insert("entries", &entries); context.insert("entries", &entries);
context.insert("PKG_AUTHORS", env!("CARGO_PKG_AUTHORS")); context.insert("PKG_AUTHORS", env!("CARGO_PKG_AUTHORS"));
context.insert("PKG_HOMEPAGE", env!("CARGO_PKG_HOMEPAGE")); context.insert("PKG_HOMEPAGE", env!("CARGO_PKG_HOMEPAGE"));
context.insert("PKG_NAME", env!("CARGO_PKG_NAME")); context.insert("PKG_NAME", env!("CARGO_PKG_NAME"));
context.insert("PKG_VERSION", env!("CARGO_PKG_VERSION")); context.insert("PKG_VERSION", env!("CARGO_PKG_VERSION"));
tera.register_function("get_author", GetAuthorFunction { feeds });
for name in tera.get_template_names() { for name in tera.get_template_names() {
debug!("Processing template {name}"); debug!("Processing template {name}");
@ -33,3 +37,54 @@ fn create_tera(templates_dir: &str) -> Result<Tera> {
tera.autoescape_on(vec![]); tera.autoescape_on(vec![]);
Ok(tera) Ok(tera)
} }
struct GetAuthorFunction {
feeds: HashMap<String, Feed>,
}
impl tera::Function for GetAuthorFunction {
fn call(&self, args: &HashMap<String, tera::Value>) -> Result<tera::Value, tera::Error> {
let entry_val: tera::Map<_, _> = match args.get("entry") {
None => {
return Err(tera::Error::msg(
"No argument of name 'entry' given to function.",
))
}
Some(val) => from_value(val.clone())?,
};
let feed_url: String = from_value(entry_val.get("source").unwrap().clone())?;
let authors_val: Vec<tera::Map<_, _>> =
from_value(entry_val.get("authors").unwrap().clone())?;
let mut authors: Vec<String> = Vec::new();
for author_val in authors_val {
let name: String = from_value(author_val.get("name").unwrap().clone())?;
if is_valid_name(&name) {
authors.push(name.clone());
}
}
if authors.is_empty() {
authors.append(&mut self.find_authors_from_feed(&feed_url));
}
Ok(tera::Value::String(authors.join(", ")))
}
}
impl GetAuthorFunction {
fn find_authors_from_feed(&self, feed_url: &str) -> Vec<String> {
let feed = self.feeds.get(feed_url).unwrap();
feed.authors
.clone()
.into_iter()
.map(|x| x.name)
.filter(is_valid_name)
.collect()
}
}
fn is_valid_name(n: &String) -> bool {
!n.is_empty() && n != "unknown" && n != "author"
}

View File

@ -1,5 +1,4 @@
{% set dateformat = "%d.%m.%Y %H:%M" -%} {% set dateformat = "%d.%m.%Y %H:%M" -%}
<html> <html>
<head> <head>
<title>Planet TVL</title> <title>Planet TVL</title>
@ -10,6 +9,7 @@
<link rel="alternate" type="application/xml+atom" title="Planet Haskell Atom Feed" href="atom.xml"> <link rel="alternate" type="application/xml+atom" title="Planet Haskell Atom Feed" href="atom.xml">
</head> </head>
<body> <body>
<div id="bodydiv">
<header> <header>
<h1>Planet TVL</h1> <h1>Planet TVL</h1>
</header> </header>
@ -42,20 +42,20 @@
<span>{{ entry.published | date(format=dateformat) }}</span> <span>{{ entry.published | date(format=dateformat) }}</span>
{% endif -%} {% endif -%}
</date> </date>
{% if entry.authors -%} {% set author = get_author(entry=entry) -%}
&mdash; <span class="entry_author">{{ entry.authors.0.name | striptags }}</span> {% if author -%}
&mdash; <span class="entry_author">{{ author | striptags }}</span>
{% endif -%} {% endif -%}
</div> </div>
{% if entry.summary -%}
<div class="entry_summary">
{{ entry.summary.content }}
</div>
{% endif -%}
{% if entry.content -%} {% if entry.content -%}
<div class="entry_content"> <div class="entry_content">
{{ entry.content.body }} {{ entry.content.body }}
</div> </div>
{% elif entry.summary -%}
<div class="entry_summary">
{{ entry.summary.content }}
</div>
{% endif -%} {% endif -%}
</article> </article>
{% endfor -%} {% endfor -%}
@ -65,7 +65,7 @@
<img src="logo.svg"> <img src="logo.svg">
<p>Last updated: {{now()|date(format="%Y-%m-%d %H:%M")}}</p> <p>Last updated: {{now()|date(format="%Y-%m-%d %H:%M")}}</p>
<ul> <ul>
{% for feed in feeds %} {% for feed_url, feed in feeds %}
<li> <li>
<a {% if feed.links.0 %}href="{{feed.links.0.href}}"{% endif -%}> <a {% if feed.links.0 %}href="{{feed.links.0.href}}"{% endif -%}>
{% if feed.title -%} {% if feed.title -%}
@ -74,10 +74,12 @@
{{ feed.authors.0.name }} {{ feed.authors.0.name }}
{% endif -%} {% endif -%}
</a> </a>
(<a href="{{feed_url}}">feed</a>)
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</aside> </aside>
</div> </div>
</div>
</body> </body>
</html> </html>