Add rss feed
This commit is contained in:
parent
f5c7c14d2e
commit
64332a137b
132
Cargo.lock
generated
132
Cargo.lock
generated
@ -114,6 +114,19 @@ version = "1.0.95"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atom_syndication"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec03a6e158ee0f38bfba811976ae909bc2505a4a2f4049c7e8df47df3497b119"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"derive_builder",
|
||||||
|
"diligent-date-parser",
|
||||||
|
"never",
|
||||||
|
"quick-xml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -351,6 +364,41 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@ -367,6 +415,37 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_test"
|
name = "derive_test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -390,6 +469,24 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diligent-date-parser"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@ -805,6 +902,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ignore"
|
name = "ignore"
|
||||||
version = "0.4.23"
|
version = "0.4.23"
|
||||||
@ -1031,6 +1134,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "never"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@ -1308,6 +1417,16 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
@ -1385,6 +1504,18 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rss"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "531af70fce504d369cf42ac0a9645f5a62a8ea9265de71cfa25087e9f6080c7c"
|
||||||
|
dependencies = [
|
||||||
|
"atom_syndication",
|
||||||
|
"derive_builder",
|
||||||
|
"never",
|
||||||
|
"quick-xml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@ -1960,6 +2091,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rss",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tera",
|
"tera",
|
||||||
|
@ -41,6 +41,7 @@ notify = "7.0.0"
|
|||||||
once_cell = "1.20.2"
|
once_cell = "1.20.2"
|
||||||
pulldown-cmark = "0.12.2"
|
pulldown-cmark = "0.12.2"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
rss = { version = "2.0.11", features = ["atom"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tera = "1.20.0"
|
tera = "1.20.0"
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
|
@ -64,7 +64,7 @@ use synchronicity::*;
|
|||||||
|
|
||||||
// use a struct for this, not a type alias, because generic bounds of type aliases aren't enforced
|
// use a struct for this, not a type alias, because generic bounds of type aliases aren't enforced
|
||||||
struct NodeGraph<S: Synchronicity>(StableDiGraph<ErasedNode<S>, (), u32>);
|
struct NodeGraph<S: Synchronicity>(StableDiGraph<ErasedNode<S>, (), u32>);
|
||||||
type NodeId = petgraph::stable_graph::NodeIndex<u32>;
|
pub type NodeId = petgraph::stable_graph::NodeIndex<u32>;
|
||||||
|
|
||||||
impl<S: Synchronicity> NodeGraph<S> {
|
impl<S: Synchronicity> NodeGraph<S> {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
@ -1,28 +1,43 @@
|
|||||||
mod heading_anchors;
|
mod heading_anchors;
|
||||||
mod highlight;
|
mod highlight;
|
||||||
mod sidenotes;
|
mod sidenotes;
|
||||||
|
mod simple_footnotes;
|
||||||
|
|
||||||
use pulldown_cmark::{Event, Options, Parser, html};
|
use pulldown_cmark::{Event, Options, Parser, html};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub fn render_undecorated(s: &str, writer: impl Write) {
|
||||||
|
html::write_html_io(writer, parse_undecorated(s)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(s: &str, writer: impl Write) {
|
pub fn render(s: &str, writer: impl Write) {
|
||||||
html::write_html_io(writer, parse(s)).unwrap();
|
html::write_html_io(writer, parse(s)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse<'a>(s: &'a str) -> impl Iterator<Item = Event<'a>> {
|
pub fn parse_base(s: &str) -> impl Iterator<Item = Event<'_>> {
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||||
let parser = Parser::new_ext(s, options);
|
let parser = Parser::new_ext(s, options);
|
||||||
// TODO: revisit which of these stages are necessary, remove unused (and url crate dep)
|
let highlight = highlight::new(parser);
|
||||||
let heading_anchors = heading_anchors::new(parser);
|
|
||||||
let sidenotes = sidenotes::new(heading_anchors);
|
|
||||||
let highlight = highlight::new(sidenotes);
|
|
||||||
highlight
|
highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse(s: &str) -> impl Iterator<Item = Event<'_>> {
|
||||||
|
let parsed = parse_base(s);
|
||||||
|
let heading_anchors = heading_anchors::new(parsed);
|
||||||
|
let sidenotes = sidenotes::new(heading_anchors);
|
||||||
|
sidenotes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_undecorated(s: &str) -> impl Iterator<Item = Event<'_>> {
|
||||||
|
let parsed = parse_base(s);
|
||||||
|
let simple_footnotes = simple_footnotes::new(parsed);
|
||||||
|
simple_footnotes
|
||||||
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
// #[cfg(test)]
|
||||||
// mod tests {
|
// mod tests {
|
||||||
// fn render(s: &str) -> String {
|
// fn render(s: &str) -> String {
|
||||||
|
@ -202,15 +202,6 @@ mod tests {
|
|||||||
super::AdjoinFootnoteDefinitions::new(Parser::new_ext(s, options)).collect()
|
super::AdjoinFootnoteDefinitions::new(Parser::new_ext(s, options)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sidenote_containers(s: &'static str) -> Vec<Event<'static>> {
|
|
||||||
let mut options = Options::empty();
|
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
|
||||||
super::InsertSidenoteContainers::new(super::AdjoinFootnoteDefinitions::new(
|
|
||||||
Parser::new_ext(s, options),
|
|
||||||
))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(s: &'static str) -> Vec<Event<'static>> {
|
fn render(s: &'static str) -> Vec<Event<'static>> {
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
|
48
src/generator/markdown/simple_footnotes.rs
Normal file
48
src/generator/markdown/simple_footnotes.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
|
||||||
|
|
||||||
|
pub fn new<'a>(inner: impl Iterator<Item = Event<'a>>) -> impl Iterator<Item = Event<'a>> {
|
||||||
|
CollectFootnotes {
|
||||||
|
inner,
|
||||||
|
in_footnote_definition: false,
|
||||||
|
footnote_events: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollectFootnotes<'a, I: Iterator<Item = Event<'a>>> {
|
||||||
|
inner: I,
|
||||||
|
in_footnote_definition: bool,
|
||||||
|
footnote_events: VecDeque<Event<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CollectFootnotes<'a, I> {
|
||||||
|
type Item = Event<'a>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.inner.next() {
|
||||||
|
Some(Event::Start(Tag::FootnoteDefinition(id))) => {
|
||||||
|
self.in_footnote_definition = true;
|
||||||
|
let html = format!(
|
||||||
|
r#"<div class="footnote-definition" id="{id}"><span class="footnote-definition-label">{id}. </span>"#
|
||||||
|
);
|
||||||
|
self.footnote_events.push_back(Event::Html(html.into()));
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
Some(Event::End(TagEnd::FootnoteDefinition)) => {
|
||||||
|
self.in_footnote_definition = false;
|
||||||
|
self.footnote_events
|
||||||
|
.push_back(Event::Html(CowStr::Borrowed("</div>")));
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
Some(e) => {
|
||||||
|
if self.in_footnote_definition {
|
||||||
|
self.footnote_events.push_back(e);
|
||||||
|
self.next()
|
||||||
|
} else {
|
||||||
|
Some(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => self.footnote_events.pop_front(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ mod css;
|
|||||||
mod home;
|
mod home;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod posts;
|
mod posts;
|
||||||
|
mod rss;
|
||||||
mod static_files;
|
mod static_files;
|
||||||
mod tags;
|
mod tags;
|
||||||
mod templates;
|
mod templates;
|
||||||
@ -44,7 +45,7 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
|
|||||||
|
|
||||||
let tags = tags::make_graph(
|
let tags = tags::make_graph(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
posts,
|
posts.clone(),
|
||||||
default_template.clone(),
|
default_template.clone(),
|
||||||
&mut *watcher.borrow_mut(),
|
&mut *watcher.borrow_mut(),
|
||||||
);
|
);
|
||||||
@ -60,6 +61,8 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
|
|||||||
|
|
||||||
let statics = static_files::make_graph(&mut builder, Rc::clone(&watcher));
|
let statics = static_files::make_graph(&mut builder, Rc::clone(&watcher));
|
||||||
|
|
||||||
|
let rss = rss::make_graph(&mut builder, posts);
|
||||||
|
|
||||||
let output = Combine::make(&mut builder, &[
|
let output = Combine::make(&mut builder, &[
|
||||||
void_outputs,
|
void_outputs,
|
||||||
archive,
|
archive,
|
||||||
@ -67,6 +70,7 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
|
|||||||
home,
|
home,
|
||||||
css,
|
css,
|
||||||
statics,
|
statics,
|
||||||
|
rss,
|
||||||
]);
|
]);
|
||||||
builder.set_existing_output(output);
|
builder.set_existing_output(output);
|
||||||
Ok(builder.build()?)
|
Ok(builder.build()?)
|
||||||
|
@ -6,12 +6,13 @@ use std::path::PathBuf;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use compute_graph::{
|
use compute_graph::{
|
||||||
|
NodeId,
|
||||||
builder::GraphBuilder,
|
builder::GraphBuilder,
|
||||||
input::{DynamicInput, Input, InputVisitable},
|
input::{DynamicInput, Input, InputVisitable},
|
||||||
rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext, Rule},
|
rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext, Rule},
|
||||||
synchronicity::Asynchronous,
|
synchronicity::Asynchronous,
|
||||||
};
|
};
|
||||||
use content::{HtmlContent, Post};
|
use content::{AnyContent, HtmlContent, Post, PostContent};
|
||||||
use log::error;
|
use log::error;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
@ -37,6 +38,8 @@ pub fn make_graph(
|
|||||||
|
|
||||||
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files, Rc::clone(&watcher)));
|
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files, Rc::clone(&watcher)));
|
||||||
|
|
||||||
|
let html_posts = builder.add_dynamic_rule(MakeConvertToHTML::new(posts.clone()));
|
||||||
|
|
||||||
// let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone()));
|
// let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone()));
|
||||||
|
|
||||||
let article_path = content_path("layout/article.html");
|
let article_path = content_path("layout/article.html");
|
||||||
@ -50,12 +53,12 @@ pub fn make_graph(
|
|||||||
.watch(article_path, move || invalidate_template.invalidate());
|
.watch(article_path, move || invalidate_template.invalidate());
|
||||||
|
|
||||||
let write_posts =
|
let write_posts =
|
||||||
builder.add_dynamic_rule(MakeWritePosts::new(posts.clone(), article_template));
|
builder.add_dynamic_rule(MakeWritePosts::new(html_posts.clone(), article_template));
|
||||||
|
|
||||||
(
|
(
|
||||||
builder.add_rule(MapDynamicToVoid(write_posts)),
|
builder.add_rule(MapDynamicToVoid(write_posts)),
|
||||||
posts.clone(),
|
posts,
|
||||||
builder.add_rule(AllPosts(posts)),
|
builder.add_rule(AllPosts(html_posts)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +127,7 @@ impl DynamicRule for MakeReadNodes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ReadPostOutput = Option<Post<HtmlContent>>;
|
pub type ReadPostOutput = Option<Post<AnyContent>>;
|
||||||
|
|
||||||
#[derive(InputVisitable)]
|
#[derive(InputVisitable)]
|
||||||
struct ReadPost {
|
struct ReadPost {
|
||||||
@ -137,7 +140,7 @@ impl Rule for ReadPost {
|
|||||||
fn evaluate(&mut self) -> Self::Output {
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
let buffer = std::fs::read_to_string(&self.path).ok()?;
|
let buffer = std::fs::read_to_string(&self.path).ok()?;
|
||||||
match Post::new(self.path.clone(), &buffer) {
|
match Post::new(self.path.clone(), &buffer) {
|
||||||
Ok(post) => Some(post.to_html()),
|
Ok(post) => Some(post),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error parsing post {e:?}");
|
error!("Error parsing post {e:?}");
|
||||||
None
|
None
|
||||||
@ -159,6 +162,40 @@ impl Rule for ReadPost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct MakeConvertToHTML {
|
||||||
|
posts: DynamicInput<ReadPostOutput>,
|
||||||
|
factory: DynamicNodeFactory<NodeId, Option<Post<HtmlContent>>>,
|
||||||
|
}
|
||||||
|
impl MakeConvertToHTML {
|
||||||
|
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
|
||||||
|
Self {
|
||||||
|
posts,
|
||||||
|
factory: DynamicNodeFactory::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DynamicRule for MakeConvertToHTML {
|
||||||
|
type ChildOutput = Option<Post<HtmlContent>>;
|
||||||
|
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
for post_input in self.posts.value().inputs.iter() {
|
||||||
|
self.factory.add_node(ctx, post_input.node_id(), |ctx| {
|
||||||
|
ctx.add_rule(ConvertToHTML(post_input.clone()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.factory.all_nodes(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct ConvertToHTML(Input<ReadPostOutput>);
|
||||||
|
impl Rule for ConvertToHTML {
|
||||||
|
type Output = Option<Post<HtmlContent>>;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.input_0().clone().map(|post| post.to_html())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #[derive(InputVisitable)]
|
// #[derive(InputVisitable)]
|
||||||
// struct MakeExtractMetadatas {
|
// struct MakeExtractMetadatas {
|
||||||
// posts: DynamicInput<ReadPostOutput>,
|
// posts: DynamicInput<ReadPostOutput>,
|
||||||
@ -208,7 +245,7 @@ impl Rule for ReadPost {
|
|||||||
|
|
||||||
#[derive(InputVisitable)]
|
#[derive(InputVisitable)]
|
||||||
struct MakeWritePosts {
|
struct MakeWritePosts {
|
||||||
posts: DynamicInput<ReadPostOutput>,
|
posts: DynamicInput<Option<Post<HtmlContent>>>,
|
||||||
article_template: Input<Templates>,
|
article_template: Input<Templates>,
|
||||||
permalink_factory: DynamicNodeFactory<PathBuf, String>,
|
permalink_factory: DynamicNodeFactory<PathBuf, String>,
|
||||||
output_path_factory: DynamicNodeFactory<PathBuf, PathBuf>,
|
output_path_factory: DynamicNodeFactory<PathBuf, PathBuf>,
|
||||||
@ -216,7 +253,7 @@ struct MakeWritePosts {
|
|||||||
render_factory: DynamicNodeFactory<PathBuf, ()>,
|
render_factory: DynamicNodeFactory<PathBuf, ()>,
|
||||||
}
|
}
|
||||||
impl MakeWritePosts {
|
impl MakeWritePosts {
|
||||||
fn new(posts: DynamicInput<ReadPostOutput>, templates: Input<Templates>) -> Self {
|
fn new(posts: DynamicInput<Option<Post<HtmlContent>>>, templates: Input<Templates>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
posts,
|
posts,
|
||||||
article_template: templates,
|
article_template: templates,
|
||||||
@ -276,8 +313,8 @@ impl DynamicRule for MakeWritePosts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(InputVisitable)]
|
#[derive(InputVisitable)]
|
||||||
struct PostPermalink(Input<ReadPostOutput>);
|
struct PostPermalink<C: PostContent>(Input<Option<Post<C>>>);
|
||||||
impl Rule for PostPermalink {
|
impl<C: PostContent + 'static> Rule for PostPermalink<C> {
|
||||||
type Output = String;
|
type Output = String;
|
||||||
fn evaluate(&mut self) -> Self::Output {
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
self.input_0().as_ref().unwrap().permalink()
|
self.input_0().as_ref().unwrap().permalink()
|
||||||
@ -297,7 +334,7 @@ impl Rule for PostOutputPath {
|
|||||||
|
|
||||||
/// Flattens Vec<Option<Post<HtmlContent>>> into Vec<Post<HtmlContent>>
|
/// Flattens Vec<Option<Post<HtmlContent>>> into Vec<Post<HtmlContent>>
|
||||||
#[derive(InputVisitable)]
|
#[derive(InputVisitable)]
|
||||||
struct AllPosts(DynamicInput<ReadPostOutput>);
|
struct AllPosts(DynamicInput<Option<Post<HtmlContent>>>);
|
||||||
impl Rule for AllPosts {
|
impl Rule for AllPosts {
|
||||||
type Output = Vec<Post<HtmlContent>>;
|
type Output = Vec<Post<HtmlContent>>;
|
||||||
fn evaluate(&mut self) -> Self::Output {
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
@ -43,7 +43,7 @@ impl Post<AnyContent> {
|
|||||||
path,
|
path,
|
||||||
metadata,
|
metadata,
|
||||||
slug: slug.to_owned(),
|
slug: slug.to_owned(),
|
||||||
word_count: None,
|
word_count: Some(content.word_count()),
|
||||||
excerpt: None,
|
excerpt: None,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
@ -51,17 +51,21 @@ impl Post<AnyContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<C: PostContent> Post<C> {
|
impl<C: PostContent> Post<C> {
|
||||||
pub fn to_html(self) -> Post<HtmlContent> {
|
pub fn map_content<C2: PostContent, F: FnOnce(C) -> C2>(self, make_content: F) -> Post<C2> {
|
||||||
Post {
|
Post {
|
||||||
path: self.path,
|
path: self.path,
|
||||||
metadata: self.metadata,
|
metadata: self.metadata,
|
||||||
slug: self.slug,
|
slug: self.slug,
|
||||||
word_count: self.word_count,
|
word_count: self.word_count,
|
||||||
excerpt: self.excerpt,
|
excerpt: self.excerpt,
|
||||||
content: self.content.to_html(),
|
content: make_content(self.content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_html(self) -> Post<HtmlContent> {
|
||||||
|
self.map_content(|c| c.to_html())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn permalink(&self) -> String {
|
pub fn permalink(&self) -> String {
|
||||||
format!("/{}/{}/", self.metadata.date.year(), self.slug)
|
format!("/{}/{}/", self.metadata.date.year(), self.slug)
|
||||||
}
|
}
|
||||||
@ -92,6 +96,14 @@ impl PostContent for AnyContent {
|
|||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct MarkdownContent(String);
|
pub struct MarkdownContent(String);
|
||||||
|
|
||||||
|
impl MarkdownContent {
|
||||||
|
pub fn to_html_undecorated(self) -> HtmlContent {
|
||||||
|
let mut buf = vec![];
|
||||||
|
markdown::render_undecorated(&self.0, &mut buf);
|
||||||
|
HtmlContent(String::from_utf8(buf).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PostContent for MarkdownContent {
|
impl PostContent for MarkdownContent {
|
||||||
fn to_html(self) -> HtmlContent {
|
fn to_html(self) -> HtmlContent {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
|
173
src/generator/rss.rs
Normal file
173
src/generator/rss.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use compute_graph::{
|
||||||
|
NodeId,
|
||||||
|
builder::GraphBuilder,
|
||||||
|
input::{DynamicInput, Input, InputVisitable},
|
||||||
|
node::NodeValue,
|
||||||
|
rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext, Rule},
|
||||||
|
synchronicity::Asynchronous,
|
||||||
|
};
|
||||||
|
use rss::{Category, ChannelBuilder, Guid, Item, ItemBuilder};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
posts::{
|
||||||
|
ReadPostOutput,
|
||||||
|
content::{AnyContent, HtmlContent, Post},
|
||||||
|
},
|
||||||
|
util::output_writer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn make_graph(
|
||||||
|
builder: &mut GraphBuilder<(), Asynchronous>,
|
||||||
|
posts: DynamicInput<ReadPostOutput>,
|
||||||
|
) -> Input<()> {
|
||||||
|
let recents = builder.add_dynamic_rule(RecentPosts::new(posts));
|
||||||
|
let with_rss_html = builder.add_dynamic_rule(ConvertRecentsToRSSContent::new(recents));
|
||||||
|
builder.add_rule(RenderRSSFeed(with_rss_html))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct RecentPosts {
|
||||||
|
posts: DynamicInput<ReadPostOutput>,
|
||||||
|
factory: DynamicNodeFactory<NodeId, ReadPostOutput>,
|
||||||
|
}
|
||||||
|
impl RecentPosts {
|
||||||
|
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
|
||||||
|
Self {
|
||||||
|
posts,
|
||||||
|
factory: DynamicNodeFactory::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DynamicRule for RecentPosts {
|
||||||
|
type ChildOutput = ReadPostOutput;
|
||||||
|
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
let mut posts = vec![];
|
||||||
|
for post_input in self.posts().inputs.iter() {
|
||||||
|
if let Some(post) = post_input.value().as_ref() {
|
||||||
|
posts.push((post_input.clone(), post.metadata.date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
posts.sort_by_key(|post_and_date| post_and_date.1);
|
||||||
|
posts.reverse();
|
||||||
|
for (post_input, _) in posts.into_iter().take(10) {
|
||||||
|
self.factory.add_node(ctx, post_input.node_id(), |ctx| {
|
||||||
|
ctx.add_rule(Identity(post_input))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.factory.all_nodes(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct Identity<T>(Input<T>);
|
||||||
|
impl<T: Clone + NodeValue + 'static> Rule for Identity<T> {
|
||||||
|
type Output = T;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.input_0().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct ConvertRecentsToRSSContent {
|
||||||
|
posts: DynamicInput<ReadPostOutput>,
|
||||||
|
factory: DynamicNodeFactory<NodeId, Option<Post<HtmlContent>>>,
|
||||||
|
}
|
||||||
|
impl ConvertRecentsToRSSContent {
|
||||||
|
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
|
||||||
|
Self {
|
||||||
|
posts,
|
||||||
|
factory: DynamicNodeFactory::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DynamicRule for ConvertRecentsToRSSContent {
|
||||||
|
type ChildOutput = Option<Post<HtmlContent>>;
|
||||||
|
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
|
||||||
|
for post_input in self.posts.value().inputs.iter() {
|
||||||
|
self.factory.add_node(ctx, post_input.node_id(), |ctx| {
|
||||||
|
ctx.add_rule(ConvertPostToRSSContent(post_input.clone()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.factory.all_nodes(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct ConvertPostToRSSContent(Input<ReadPostOutput>);
|
||||||
|
impl Rule for ConvertPostToRSSContent {
|
||||||
|
type Output = Option<Post<HtmlContent>>;
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
self.input_0()
|
||||||
|
.as_ref()
|
||||||
|
.map(|post| post.clone().map_content(convert_html_to_rss_html))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_html_to_rss_html(content: AnyContent) -> HtmlContent {
|
||||||
|
match content {
|
||||||
|
AnyContent::Html(html) => html,
|
||||||
|
AnyContent::Markdown(markdown) => markdown.to_html_undecorated(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct RenderRSSFeed(DynamicInput<Option<Post<HtmlContent>>>);
|
||||||
|
impl Rule for RenderRSSFeed {
|
||||||
|
type Output = ();
|
||||||
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
|
let posts_ = self.input_0();
|
||||||
|
let mut posts = posts_
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|inp| inp.value().as_ref().map(|post| (inp, post.metadata.date)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
posts.sort_by_key(|post_and_date| post_and_date.1);
|
||||||
|
posts.reverse();
|
||||||
|
let items = posts
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(inp, _)| inp.value().as_ref().map(to_rss_item))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let builder = ChannelBuilder::default()
|
||||||
|
.title("Shadowfacts")
|
||||||
|
.link("https://shadowfacts.net")
|
||||||
|
.last_build_date(Some(rfc_822(Utc::now())))
|
||||||
|
.items(items)
|
||||||
|
.build();
|
||||||
|
let writer = output_writer("/feed.xml").expect("opening feed.xml");
|
||||||
|
builder
|
||||||
|
.pretty_write_to(writer, b' ', 2)
|
||||||
|
.expect("writing feed.xml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rfc_822(datetime: DateTime<Utc>) -> String {
|
||||||
|
datetime.format("%a, %d %b %Y %H:%M:%S %z").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_rss_item(post: &Post<HtmlContent>) -> Item {
|
||||||
|
ItemBuilder::default()
|
||||||
|
.title(Some(post.metadata.title.clone()))
|
||||||
|
.link(Some(format!("https://shadowfacts.net{}", post.permalink())))
|
||||||
|
.guid(Some(Guid {
|
||||||
|
value: format!("https://shadowfacts.net{}", post.permalink()),
|
||||||
|
permalink: true,
|
||||||
|
}))
|
||||||
|
.pub_date(Some(rfc_822(post.metadata.date.into())))
|
||||||
|
.categories(
|
||||||
|
post.metadata
|
||||||
|
.tags
|
||||||
|
.as_ref()
|
||||||
|
.map(|tags| {
|
||||||
|
tags.iter()
|
||||||
|
.map(|t| Category {
|
||||||
|
name: t.name.clone(),
|
||||||
|
domain: None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or(vec![]),
|
||||||
|
)
|
||||||
|
.content(Some(post.content.html().to_owned()))
|
||||||
|
.build()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user