add a tagging system to the website

This commit is contained in:
りき萌 2025-08-24 13:18:51 +02:00
parent 701da6bc4b
commit e1b6578b2a
97 changed files with 1025 additions and 979 deletions

View file

@ -7,6 +7,11 @@ use crate::{state::FileId, vfs::VPathBuf};
/// Top-level `%%` root attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct RootAttributes {
/// Unique ID of this page.
/// Required for the page to be shown in feeds.
#[serde(default)]
pub id: Option<String>,
/// Template to use for generating the page.
/// Defaults to `_tree.hbs`.
#[serde(default)]
@ -46,12 +51,9 @@ pub struct RootAttributes {
#[serde(default)]
pub timestamps: Option<Timestamps>,
/// 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
/// Tags to assign to this page.
#[serde(default)]
pub feed: Option<String>,
pub tags: Vec<String>,
}
/// A picture reference.
@ -134,15 +136,19 @@ pub struct Attributes {
pub tags: Vec<String>,
}
/// Parses the timestamp out of a branch ID.
/// Returns `None` if the ID does not contain a timestamp.
pub fn timestamp_from_id(id: &str) -> Option<DateTime<Utc>> {
Ulid::from_string(id)
.ok()
.as_ref()
.map(Ulid::timestamp_ms)
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
}
impl Attributes {
/// Parses the timestamp out of the branch's ID.
/// Returns `None` if the ID does not contain a timestamp.
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
Ulid::from_string(&self.id)
.ok()
.as_ref()
.map(Ulid::timestamp_ms)
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
timestamp_from_id(&self.id)
}
}

View file

@ -1,94 +0,0 @@
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()
}