adding document mode
I've been thinking a lot about the treehouse and I feel like it's time to say goodbye to the tree format.
This commit is contained in:
parent
550c062327
commit
36705e7c1e
31 changed files with 940 additions and 409 deletions
|
@ -152,6 +152,8 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn page_url(&self, page: &str) -> String {
|
||||
// We don't want .dj appearing in URLs, though it exists as a disambiguator in [page:] links.
|
||||
let page = page.strip_suffix(".dj").unwrap_or(page);
|
||||
format!("{}/{}", self.site, page)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod atom;
|
||||
mod dir_helper;
|
||||
mod doc;
|
||||
mod include_static_helper;
|
||||
mod simple_template;
|
||||
mod tree;
|
||||
|
@ -7,6 +8,7 @@ mod tree;
|
|||
use std::{ops::ControlFlow, sync::Arc};
|
||||
|
||||
use atom::FeedDir;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dir_helper::DirHelper;
|
||||
use handlebars::{handlebars_helper, Handlebars};
|
||||
use include_static_helper::IncludeStaticHelper;
|
||||
|
@ -18,13 +20,14 @@ use crate::{
|
|||
dirs::Dirs,
|
||||
fun::seasons::Season,
|
||||
generate::{
|
||||
doc::DocDir,
|
||||
simple_template::SimpleTemplateDir,
|
||||
tree::{DirIndex, TreehouseDir},
|
||||
},
|
||||
sources::Sources,
|
||||
vfs::{
|
||||
self, layered_dir, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, Entries,
|
||||
HtmlCanonicalize, MemDir, Overlay, ToDynDir, VPath, VPathBuf,
|
||||
self, layered_dir, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, HtmlCanonicalize,
|
||||
MemDir, ToDynDir, VPath, VPathBuf,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -54,8 +57,10 @@ fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
|
|||
let mut handlebars = Handlebars::new();
|
||||
|
||||
handlebars_helper!(cat: |a: String, b: String| a + &b);
|
||||
handlebars_helper!(iso_date: |d: DateTime<Utc>| d.format("%F").to_string());
|
||||
|
||||
handlebars.register_helper("cat", Box::new(cat));
|
||||
handlebars.register_helper("iso_date", Box::new(iso_date));
|
||||
handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone())));
|
||||
handlebars.register_helper(
|
||||
"include_static",
|
||||
|
@ -103,7 +108,13 @@ pub fn target(dirs: Arc<Dirs>, sources: Arc<Sources>) -> DynDir {
|
|||
|
||||
let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x));
|
||||
let treehouse_dir = layered_dir(&[
|
||||
TreehouseDir::new(dirs, sources.clone(), handlebars.clone(), dir_index).to_dyn(),
|
||||
TreehouseDir::new(dirs.clone(), sources.clone(), handlebars.clone(), dir_index).to_dyn(),
|
||||
DocDir {
|
||||
sources: sources.clone(),
|
||||
dirs,
|
||||
handlebars: handlebars.clone(),
|
||||
}
|
||||
.to_dyn(),
|
||||
SimpleTemplateDir::new(sources.clone(), handlebars.clone()).to_dyn(),
|
||||
]);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
html::djot::{self, resolve_link},
|
||||
sources::Sources,
|
||||
state::FileId,
|
||||
tree::SemaBranchId,
|
||||
tree::{feed, SemaBranchId},
|
||||
vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
|
||||
};
|
||||
|
||||
|
@ -155,7 +155,7 @@ fn extract_entries(sources: &Sources, dirs: &Dirs, file_id: FileId) -> Vec<Entry
|
|||
let branch = sources.treehouse.tree.branch(branch_id);
|
||||
|
||||
let text = &sources.treehouse.source(file_id).input()[branch.content.clone()];
|
||||
let parsed = parse_entry(sources, dirs, file_id, jotdown::Parser::new(text));
|
||||
let parsed = feed::parse_entry(sources, dirs, file_id, jotdown::Parser::new(text));
|
||||
|
||||
let mut summary = String::new();
|
||||
branches_to_html_simple(&mut summary, sources, dirs, file_id, &branch.children);
|
||||
|
@ -177,94 +177,6 @@ fn extract_entries(sources: &Sources, dirs: &Dirs, file_id: FileId) -> Vec<Entry
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ParsedEntry {
|
||||
title: Option<String>,
|
||||
link: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_entry(
|
||||
sources: &Sources,
|
||||
dirs: &Dirs,
|
||||
file_id: FileId,
|
||||
parser: jotdown::Parser,
|
||||
) -> ParsedEntry {
|
||||
let mut parser = parser.into_offset_iter();
|
||||
while let Some((event, span)) = parser.next() {
|
||||
if let jotdown::Event::Start(jotdown::Container::Heading { .. }, _attrs) = &event {
|
||||
let mut events = vec![(event, span)];
|
||||
for (event, span) in parser.by_ref() {
|
||||
// To my knowledge headings cannot nest, so it's okay not keeping a stack here.
|
||||
let is_heading = matches!(
|
||||
event,
|
||||
jotdown::Event::End(jotdown::Container::Heading { .. })
|
||||
);
|
||||
events.push((event, span));
|
||||
if is_heading {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let title_events: Vec<_> = events
|
||||
.iter()
|
||||
.filter(|(event, _)| {
|
||||
!matches!(
|
||||
event,
|
||||
// A little repetitive, but I don't mind.
|
||||
// The point of this is not to include extra <h3> and <a> in the link text,
|
||||
// but preserve other formatting such as bold, italic, code, etc.
|
||||
jotdown::Event::Start(
|
||||
jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. },
|
||||
_
|
||||
) | jotdown::Event::End(
|
||||
jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. }
|
||||
)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut title = String::new();
|
||||
let _render_diagnostics = djot::Renderer {
|
||||
config: &sources.config,
|
||||
dirs,
|
||||
treehouse: &sources.treehouse,
|
||||
file_id,
|
||||
|
||||
// How. Just, stop.
|
||||
page_id: "liquidex-you-reeeeeal-dummy".into(),
|
||||
}
|
||||
.render(&title_events, &mut title);
|
||||
|
||||
let link = events.iter().find_map(|(event, _)| {
|
||||
if let jotdown::Event::Start(jotdown::Container::Link(link, link_type), _) = event {
|
||||
Some(link_url(sources, dirs, link, *link_type))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
return ParsedEntry {
|
||||
title: (!title.is_empty()).then_some(title),
|
||||
link,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ParsedEntry {
|
||||
title: None,
|
||||
link: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn link_url(sources: &Sources, dirs: &Dirs, url: &str, link_type: jotdown::LinkType) -> String {
|
||||
if let jotdown::LinkType::Span(jotdown::SpanLinkType::Unresolved) = link_type {
|
||||
if let Some(url) = resolve_link(&sources.config, &sources.treehouse, dirs, url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
url.to_owned()
|
||||
}
|
||||
|
||||
/// Extremely simple HTML renderer without the treehouse's fancy branch folding and linking features.
|
||||
fn branches_to_html_simple(
|
||||
s: &mut String,
|
||||
|
|
242
src/generate/doc.rs
Normal file
242
src/generate/doc.rs
Normal file
|
@ -0,0 +1,242 @@
|
|||
use std::{
|
||||
fmt::{self},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::{DateTime, Utc};
|
||||
use handlebars::Handlebars;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use crate::{
|
||||
dirs::Dirs,
|
||||
generate::BaseTemplateData,
|
||||
html::djot,
|
||||
sources::Sources,
|
||||
state::{report_diagnostics, toml_error_to_diagnostic, FileId, TomlError},
|
||||
tree::{attributes::Picture, feed},
|
||||
vfs::{Content, Dir, Query, VPath},
|
||||
};
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
struct Attributes {
|
||||
/// Template to use for generating the page.
|
||||
/// Defaults to `_tree.hbs`.
|
||||
#[serde(default)]
|
||||
template: Option<String>,
|
||||
|
||||
/// Title of the page.
|
||||
/// The only necessary field.
|
||||
/// Unlike tree pages, doc pages always have titles.
|
||||
title: String,
|
||||
|
||||
/// ID of picture attached to the page, to be used as a thumbnail.
|
||||
#[serde(default)]
|
||||
thumbnail: Option<Picture>,
|
||||
|
||||
/// Additional scripts to load into to the page.
|
||||
/// These are relative to the /static/js directory.
|
||||
#[serde(default)]
|
||||
scripts: Vec<String>,
|
||||
|
||||
/// Additional styles to load into to the page.
|
||||
/// These are relative to the /static/css directory.
|
||||
#[serde(default)]
|
||||
styles: Vec<String>,
|
||||
|
||||
/// If not `None`, the page will get an additional 'feed' field in template data, containing
|
||||
/// updates from the news feed of the specified name.
|
||||
#[serde(default)]
|
||||
include_feed: Option<IncludeFeed>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IncludeFeed {
|
||||
/// The name of the feed (within the treehouse database.)
|
||||
name: String,
|
||||
|
||||
/// The title of the feed shown on the page.
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Page {
|
||||
title: String,
|
||||
thumbnail: Option<Thumbnail>,
|
||||
scripts: Vec<String>,
|
||||
styles: Vec<String>,
|
||||
tree_path: String,
|
||||
doc: String,
|
||||
feed: Option<Feed>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Thumbnail {
|
||||
url: String,
|
||||
alt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Feed {
|
||||
title: String,
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Entry {
|
||||
title: String,
|
||||
url: String,
|
||||
updated: DateTime<Utc>,
|
||||
categories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PageTemplateData<'a> {
|
||||
#[serde(flatten)]
|
||||
base: &'a BaseTemplateData<'a>,
|
||||
page: Page,
|
||||
}
|
||||
|
||||
pub struct DocDir {
|
||||
pub sources: Arc<Sources>,
|
||||
pub dirs: Arc<Dirs>,
|
||||
|
||||
pub handlebars: Arc<Handlebars<'static>>,
|
||||
}
|
||||
|
||||
impl DocDir {
|
||||
#[instrument("DocDir::content", skip(self))]
|
||||
pub fn content(&self, path: &VPath) -> Option<Content> {
|
||||
if let Some(file_id) = self
|
||||
.sources
|
||||
.treehouse
|
||||
.files_by_doc_path
|
||||
.get(&path.with_extension("dj"))
|
||||
{
|
||||
let source = self.sources.treehouse.source(*file_id).input();
|
||||
return Some(Content::new(
|
||||
"text/html",
|
||||
self.generate(*file_id, path, source).into_bytes(),
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn generate(&self, file_id: FileId, path: &VPath, source: &str) -> String {
|
||||
let (front_matter, text) = source.split_once("+++").unwrap_or(("", source));
|
||||
let attributes: Attributes =
|
||||
toml_edit::de::from_str(front_matter).unwrap_or_else(|error| {
|
||||
_ = report_diagnostics(
|
||||
&self.sources.treehouse,
|
||||
&[toml_error_to_diagnostic(TomlError {
|
||||
message: error.message().to_owned(),
|
||||
span: error.span(),
|
||||
file_id,
|
||||
input_range: 0..front_matter.len(),
|
||||
})],
|
||||
);
|
||||
Attributes::default()
|
||||
});
|
||||
|
||||
let events: Vec<_> = jotdown::Parser::new(text).into_offset_iter().collect();
|
||||
let mut rendered_markup = String::new();
|
||||
let render_diagnostics = djot::Renderer {
|
||||
config: &self.sources.config,
|
||||
dirs: &self.dirs,
|
||||
treehouse: &self.sources.treehouse,
|
||||
file_id,
|
||||
page_id: path.to_string(),
|
||||
}
|
||||
.render(&events, &mut rendered_markup);
|
||||
|
||||
let template_name = attributes.template.as_deref().unwrap_or("_doc.hbs");
|
||||
|
||||
let render_result = self
|
||||
.handlebars
|
||||
.render(
|
||||
template_name,
|
||||
&PageTemplateData {
|
||||
base: &BaseTemplateData::new(&self.sources),
|
||||
page: Page {
|
||||
title: attributes.title,
|
||||
thumbnail: attributes.thumbnail.map(|pic| Thumbnail {
|
||||
url: self.sources.config.pic_url(&*self.dirs.pic, &pic.id),
|
||||
alt: pic.alt,
|
||||
}),
|
||||
scripts: attributes.scripts,
|
||||
styles: attributes.styles,
|
||||
tree_path: path.to_string(),
|
||||
doc: rendered_markup,
|
||||
feed: attributes.include_feed.and_then(|feed| {
|
||||
Some(Feed {
|
||||
title: feed.title,
|
||||
entries: self
|
||||
.generate_feed(&feed.name)
|
||||
.inspect_err(|e| {
|
||||
error!("generating feed for {path} failed: {e}")
|
||||
})
|
||||
.ok()?,
|
||||
})
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
.context("template rendering failed");
|
||||
match render_result {
|
||||
Ok(rendered) => rendered,
|
||||
Err(error) => format!("{error:#?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_feed(&self, name: &str) -> anyhow::Result<Vec<Entry>> {
|
||||
let file_id = *self
|
||||
.sources
|
||||
.treehouse
|
||||
.feeds_by_name
|
||||
.get(name)
|
||||
.context("no feed with the given name")?;
|
||||
let roots = &self.sources.treehouse.roots[&file_id];
|
||||
|
||||
Ok(roots
|
||||
.branches
|
||||
.iter()
|
||||
.flat_map(|&branch_id| {
|
||||
let branch = self.sources.treehouse.tree.branch(branch_id);
|
||||
|
||||
let text = &self.sources.treehouse.source(file_id).input()[branch.content.clone()];
|
||||
let parsed = feed::parse_entry(
|
||||
&self.sources,
|
||||
&self.dirs,
|
||||
file_id,
|
||||
jotdown::Parser::new(text),
|
||||
);
|
||||
|
||||
let updated = branch
|
||||
.attributes
|
||||
.timestamp()
|
||||
.unwrap_or(DateTime::UNIX_EPOCH); // if you see the Unix epoch... oops
|
||||
|
||||
parsed.link.map(|url| Entry {
|
||||
updated,
|
||||
url,
|
||||
title: parsed.title.unwrap_or_else(|| "untitled".into()),
|
||||
categories: branch.attributes.tags.clone(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Dir for DocDir {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.try_provide(|| self.content(path));
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DocDir {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("DocDir")
|
||||
}
|
||||
}
|
|
@ -106,21 +106,6 @@ impl<'a> Writer<'a> {
|
|||
range: Range<usize>,
|
||||
out: &mut String,
|
||||
) -> std::fmt::Result {
|
||||
if let Event::Start(Container::Footnote { label: _ }, ..) = e {
|
||||
self.diagnostics.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: Some("djot".into()),
|
||||
message: "Djot footnotes are not supported".into(),
|
||||
labels: vec![Label {
|
||||
style: LabelStyle::Primary,
|
||||
file_id: self.renderer.file_id,
|
||||
range: range.clone(),
|
||||
message: "".into(),
|
||||
}],
|
||||
notes: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
if matches!(&e, Event::Start(Container::LinkDefinition { .. }, ..)) {
|
||||
self.ignore_next_event = true;
|
||||
return Ok(());
|
||||
|
@ -163,7 +148,7 @@ impl<'a> Writer<'a> {
|
|||
} => {
|
||||
out.push_str("<ol");
|
||||
if *start > 1 {
|
||||
write!(out, r#" start="{}""#, start)?;
|
||||
write!(out, r#" start="{start}""#)?;
|
||||
}
|
||||
if let Some(ty) = match numbering {
|
||||
Decimal => None,
|
||||
|
@ -172,7 +157,7 @@ impl<'a> Writer<'a> {
|
|||
RomanLower => Some('i'),
|
||||
RomanUpper => Some('I'),
|
||||
} {
|
||||
write!(out, r#" type="{}""#, ty)?;
|
||||
write!(out, r#" type="{ty}""#)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +167,7 @@ impl<'a> Writer<'a> {
|
|||
}
|
||||
Container::DescriptionList => out.push_str("<dl"),
|
||||
Container::DescriptionDetails => out.push_str("<dd"),
|
||||
Container::Footnote { .. } => unreachable!(),
|
||||
Container::Footnote { label } => out.push_str(label),
|
||||
Container::Table => out.push_str("<table"),
|
||||
Container::TableRow { .. } => out.push_str("<tr"),
|
||||
Container::Section { .. } => {}
|
||||
|
@ -193,7 +178,7 @@ impl<'a> Writer<'a> {
|
|||
}
|
||||
out.push_str("<p");
|
||||
}
|
||||
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||||
Container::Heading { level, .. } => write!(out, "<h{level}")?,
|
||||
Container::TableCell { head: false, .. } => out.push_str("<td"),
|
||||
Container::TableCell { head: true, .. } => out.push_str("<th"),
|
||||
Container::Caption => out.push_str("<caption"),
|
||||
|
@ -275,7 +260,7 @@ impl<'a> Writer<'a> {
|
|||
.into_iter()
|
||||
.filter(|(a, _)| !(*a == "class" || a.starts_with(':')))
|
||||
{
|
||||
write!(out, r#" {}=""#, key)?;
|
||||
write!(out, r#" {key}=""#)?;
|
||||
value.parts().for_each(|part| write_attr(part, out));
|
||||
out.push('"');
|
||||
}
|
||||
|
@ -338,7 +323,7 @@ impl<'a> Writer<'a> {
|
|||
Alignment::Center => "center",
|
||||
Alignment::Right => "right",
|
||||
};
|
||||
write!(out, r#" style="text-align: {};">"#, a)?;
|
||||
write!(out, r#" style="text-align: {a};">"#)?;
|
||||
}
|
||||
Container::CodeBlock { language } => {
|
||||
if language.is_empty() {
|
||||
|
@ -444,7 +429,7 @@ impl<'a> Writer<'a> {
|
|||
}
|
||||
Container::DescriptionList => out.push_str("</dl>"),
|
||||
Container::DescriptionDetails => out.push_str("</dd>"),
|
||||
Container::Footnote { .. } => unreachable!(),
|
||||
Container::Footnote { label } => out.push_str(label),
|
||||
Container::Table => out.push_str("</table>"),
|
||||
Container::TableRow { .. } => out.push_str("</tr>"),
|
||||
Container::Section { .. } => {}
|
||||
|
@ -455,7 +440,7 @@ impl<'a> Writer<'a> {
|
|||
}
|
||||
out.push_str("</p>");
|
||||
}
|
||||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||
Container::Heading { level, .. } => write!(out, "</h{level}>")?,
|
||||
Container::TableCell { head: false, .. } => out.push_str("</td>"),
|
||||
Container::TableCell { head: true, .. } => out.push_str("</th>"),
|
||||
Container::Caption => out.push_str("</caption>"),
|
||||
|
@ -537,19 +522,8 @@ impl<'a> Writer<'a> {
|
|||
Raw::Html => out.push_str(s),
|
||||
Raw::Other => {}
|
||||
},
|
||||
Event::FootnoteReference(_label) => {
|
||||
self.diagnostics.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
code: Some("djot".into()),
|
||||
message: "Djot footnotes are unsupported".into(),
|
||||
labels: vec![Label {
|
||||
style: LabelStyle::Primary,
|
||||
file_id: self.renderer.file_id,
|
||||
range,
|
||||
message: "".into(),
|
||||
}],
|
||||
notes: vec![],
|
||||
});
|
||||
Event::FootnoteReference(label) => {
|
||||
out.push_str(label);
|
||||
}
|
||||
Event::Symbol(sym) => {
|
||||
if let Some(vpath) = self.renderer.config.emoji.get(sym.as_ref()) {
|
||||
|
@ -624,7 +598,7 @@ impl<'a> Writer<'a> {
|
|||
}
|
||||
out.push_str("<hr");
|
||||
for (a, v) in attrs {
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
write!(out, r#" {a}=""#)?;
|
||||
v.parts().for_each(|part| write_attr(part, out));
|
||||
out.push('"');
|
||||
}
|
||||
|
@ -655,7 +629,7 @@ fn write_escape(mut s: &str, escape_quotes: bool, out: &mut String) {
|
|||
'"' if escape_quotes => Some("""),
|
||||
_ => None,
|
||||
}
|
||||
.map_or(false, |s| {
|
||||
.is_some_and(|s| {
|
||||
ent = s;
|
||||
true
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, ops::ControlFlow};
|
|||
|
||||
use anyhow::{anyhow, Context};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use tracing::{info_span, instrument};
|
||||
use tracing::{error, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
|
@ -66,14 +66,19 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
|
|||
let mut parsed_trees = HashMap::new();
|
||||
|
||||
let mut paths = vec![];
|
||||
let mut doc_paths = vec![];
|
||||
|
||||
vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| {
|
||||
if path.extension() == Some("tree") {
|
||||
paths.push(path.to_owned());
|
||||
match path.extension() {
|
||||
Some("tree") => paths.push(path.to_owned()),
|
||||
Some("dj") => doc_paths.push(path.to_owned()),
|
||||
_ => (),
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
|
||||
// Trees
|
||||
|
||||
// NOTE: Sources are filled in later; they can be left out until a call to report_diagnostics.
|
||||
let file_ids: Vec<_> = paths
|
||||
.iter()
|
||||
|
@ -132,5 +137,18 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
|
|||
|
||||
report_diagnostics(&treehouse, &diagnostics)?;
|
||||
|
||||
// Docs
|
||||
|
||||
for path in doc_paths {
|
||||
if let Some(input) =
|
||||
vfs::query::<Content>(&dirs.content, &path).and_then(|c| c.string().ok())
|
||||
{
|
||||
let file_id = treehouse.add_file(path.clone(), Source::Other(input));
|
||||
treehouse.files_by_doc_path.insert(path, file_id);
|
||||
} else {
|
||||
error!("doc {path} does not exist in content directory even though it was enumerated via walk_dir_rec");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(treehouse)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,8 @@ pub struct FileId(usize);
|
|||
/// Treehouse compilation context.
|
||||
pub struct Treehouse {
|
||||
pub files: Vec<File>,
|
||||
pub files_by_tree_path: HashMap<VPathBuf, FileId>,
|
||||
pub files_by_tree_path: HashMap<VPathBuf, FileId>, // trees only
|
||||
pub files_by_doc_path: HashMap<VPathBuf, FileId>, // docs only
|
||||
pub feeds_by_name: HashMap<String, FileId>,
|
||||
|
||||
pub tree: SemaTree,
|
||||
|
@ -83,6 +84,7 @@ impl Treehouse {
|
|||
Self {
|
||||
files: vec![],
|
||||
files_by_tree_path: HashMap::new(),
|
||||
files_by_doc_path: HashMap::new(),
|
||||
feeds_by_name: HashMap::new(),
|
||||
|
||||
tree: SemaTree::default(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod ast;
|
||||
pub mod attributes;
|
||||
pub mod feed;
|
||||
pub mod mini_template;
|
||||
pub mod pull;
|
||||
|
||||
|
|
|
@ -23,10 +23,6 @@ pub struct RootAttributes {
|
|||
#[serde(default = "default_icon")]
|
||||
pub icon: String,
|
||||
|
||||
/// Summary of the generated .html page.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// ID of picture attached to the page, to be used as a thumbnail.
|
||||
#[serde(default)]
|
||||
pub thumbnail: Option<Picture>,
|
||||
|
@ -50,7 +46,7 @@ pub struct RootAttributes {
|
|||
#[serde(default)]
|
||||
pub timestamps: Option<Timestamps>,
|
||||
|
||||
/// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`.
|
||||
/// When specified, this page will have a corresponding Atom feed under `feed/{feed}.atom`.
|
||||
///
|
||||
/// In feeds, top-level branches are expected to have a single heading containing the post title.
|
||||
/// Their children are turned into the post description
|
||||
|
|
94
src/tree/feed.rs
Normal file
94
src/tree/feed.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use crate::{
|
||||
dirs::Dirs,
|
||||
html::djot::{self, resolve_link},
|
||||
sources::Sources,
|
||||
state::FileId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedEntry {
|
||||
pub title: Option<String>,
|
||||
pub link: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_entry(
|
||||
sources: &Sources,
|
||||
dirs: &Dirs,
|
||||
file_id: FileId,
|
||||
parser: jotdown::Parser,
|
||||
) -> ParsedEntry {
|
||||
let mut parser = parser.into_offset_iter();
|
||||
while let Some((event, span)) = parser.next() {
|
||||
if let jotdown::Event::Start(jotdown::Container::Heading { .. }, _attrs) = &event {
|
||||
let mut events = vec![(event, span)];
|
||||
for (event, span) in parser.by_ref() {
|
||||
// To my knowledge headings cannot nest, so it's okay not keeping a stack here.
|
||||
let is_heading = matches!(
|
||||
event,
|
||||
jotdown::Event::End(jotdown::Container::Heading { .. })
|
||||
);
|
||||
events.push((event, span));
|
||||
if is_heading {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let title_events: Vec<_> = events
|
||||
.iter()
|
||||
.filter(|(event, _)| {
|
||||
!matches!(
|
||||
event,
|
||||
// A little repetitive, but I don't mind.
|
||||
// The point of this is not to include extra <h3> and <a> in the link text,
|
||||
// but preserve other formatting such as bold, italic, code, etc.
|
||||
jotdown::Event::Start(
|
||||
jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. },
|
||||
_
|
||||
) | jotdown::Event::End(
|
||||
jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. }
|
||||
)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut title = String::new();
|
||||
let _render_diagnostics = djot::Renderer {
|
||||
config: &sources.config,
|
||||
dirs,
|
||||
treehouse: &sources.treehouse,
|
||||
file_id,
|
||||
|
||||
// How. Just, stop.
|
||||
page_id: "liquidex-you-reeeeeal-dummy".into(),
|
||||
}
|
||||
.render(&title_events, &mut title);
|
||||
|
||||
let link = events.iter().find_map(|(event, _)| {
|
||||
if let jotdown::Event::Start(jotdown::Container::Link(link, link_type), _) = event {
|
||||
Some(link_url(sources, dirs, link, *link_type))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
return ParsedEntry {
|
||||
title: (!title.is_empty()).then_some(title),
|
||||
link,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ParsedEntry {
|
||||
title: None,
|
||||
link: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn link_url(sources: &Sources, dirs: &Dirs, url: &str, link_type: jotdown::LinkType) -> String {
|
||||
if let jotdown::LinkType::Span(jotdown::SpanLinkType::Unresolved) = link_type {
|
||||
if let Some(url) = resolve_link(&sources.config, &sources.treehouse, dirs, url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
url.to_owned()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue