use std::{any::Any, path::PathBuf, sync::Arc};
use rustc_hash::FxHashMap;
mod highlight;
pub mod markdown;
mod slugger;
pub mod tracked;
use crate::{
assets::RouteAssets,
route::{DynamicRouteContext, PageContext, PageParams},
};
pub use markdown::{
components::{
BlockQuoteKind, BlockquoteComponent, CodeComponent, EmphasisComponent, HardBreakComponent,
HeadingComponent, HorizontalRuleComponent, ImageComponent, LinkComponent, LinkType,
ListComponent, ListItemComponent, ListType, MarkdownComponents, ParagraphComponent,
StrikethroughComponent, StrongComponent, TableAlignment, TableCellComponent,
TableComponent, TableHeadComponent, TableRowComponent, TaskListMarkerComponent,
},
*,
};
pub use highlight::{HighlightOptions, highlight_code};
pub use tracked::TrackedContentSource;
pub use maudit_macros::markdown_entry;
#[derive(Debug, Clone)]
pub enum Dependency {
File(PathBuf),
}
pub struct EntryInner<T> {
pub id: String,
render: OptionalContentRenderFn,
pub raw_content: Option<String>,
data_loader: Option<DataLoadingFn<T>>,
cached_data: std::sync::OnceLock<T>,
pub dependencies: Vec<Dependency>,
}
pub type Entry<T> = Arc<EntryInner<T>>;
pub trait ContentEntry<T> {
fn create(
id: String,
render: OptionalContentRenderFn,
raw_content: Option<String>,
data: T,
dependencies: Vec<Dependency>,
) -> Entry<T> {
Arc::new(EntryInner {
id,
render,
raw_content,
data_loader: None,
cached_data: std::sync::OnceLock::from(data),
dependencies,
})
}
fn create_lazy(
id: String,
render: OptionalContentRenderFn,
raw_content: Option<String>,
data_loader: DataLoadingFn<T>,
dependencies: Vec<Dependency>,
) -> Entry<T> {
Arc::new(EntryInner {
id,
render,
raw_content,
data_loader: Some(data_loader),
cached_data: std::sync::OnceLock::new(),
dependencies,
})
}
}
impl<T> ContentEntry<T> for Entry<T> {}
pub trait ContentContext {
fn content(&self) -> &ContentSources;
fn assets(&mut self) -> &mut RouteAssets;
}
impl ContentContext for PageContext<'_> {
fn content(&self) -> &ContentSources {
self.content
}
fn assets(&mut self) -> &mut RouteAssets {
self.assets
}
}
impl ContentContext for DynamicRouteContext<'_> {
fn content(&self) -> &ContentSources {
self.content
}
fn assets(&mut self) -> &mut RouteAssets {
self.assets
}
}
type DataLoadingFn<T> = Box<dyn Fn(&mut dyn ContentContext) -> T + Send + Sync>;
type OptionalContentRenderFn =
Option<Box<dyn Fn(&str, &mut crate::route::PageContext) -> String + Send + Sync>>;
impl<T> EntryInner<T> {
pub fn data<C: ContentContext>(&self, ctx: &mut C) -> &T {
self.cached_data.get_or_init(|| {
if let Some(ref loader) = self.data_loader {
loader(ctx)
} else {
panic!("No data loader available and no cached data")
}
})
}
pub fn render(&self, ctx: &mut PageContext) -> String {
(self.render.as_ref().unwrap())(self.raw_content.as_ref().unwrap(), ctx)
}
}
pub type Untyped = FxHashMap<String, String>;
pub struct ContentSources(pub Vec<Box<dyn ContentSourceInternal>>);
impl From<Vec<Box<dyn ContentSourceInternal>>> for ContentSources {
fn from(content_sources: Vec<Box<dyn ContentSourceInternal>>) -> Self {
Self(content_sources)
}
}
impl ContentSources {
pub fn new(content_sources: Vec<Box<dyn ContentSourceInternal>>) -> Self {
Self(content_sources)
}
pub fn sources(&self) -> &Vec<Box<dyn ContentSourceInternal>> {
&self.0
}
pub fn sources_mut(&mut self) -> &mut Vec<Box<dyn ContentSourceInternal>> {
&mut self.0
}
pub fn init_all(&mut self) {
for source in &mut self.0 {
source.init();
}
}
pub fn get_untyped_source(&self, name: &str) -> &ContentSource<Untyped> {
self.get_source::<Untyped>(name)
}
pub fn get_untyped_source_safe(&self, name: &str) -> Option<&ContentSource<Untyped>> {
self.get_source_safe::<Untyped>(name)
}
pub fn get_source<T: 'static>(&self, name: &str) -> &ContentSource<T> {
self.0
.iter()
.find_map(
|source| match source.as_any().downcast_ref::<ContentSource<T>>() {
Some(source) if source.name == name => Some(source),
_ => None,
},
)
.unwrap_or_else(|| panic!("Content source with name '{}' not found", name))
}
pub fn get_source_safe<T: 'static>(&self, name: &str) -> Option<&ContentSource<T>> {
self.0.iter().find_map(
|source| match source.as_any().downcast_ref::<ContentSource<T>>() {
Some(source) if source.name == name => Some(source),
_ => None,
},
)
}
}
type ContentSourceInitMethod<T> = Box<dyn Fn() -> Vec<Arc<EntryInner<T>>> + Send + Sync>;
pub struct ContentSource<T = Untyped> {
pub name: String,
pub entries: FxHashMap<String, Arc<EntryInner<T>>>,
pub(crate) init_method: ContentSourceInitMethod<T>,
}
impl<T> ContentSource<T> {
pub fn new<P>(name: P, entries: ContentSourceInitMethod<T>) -> Self
where
P: Into<String>,
{
Self {
name: name.into(),
entries: FxHashMap::default(),
init_method: entries,
}
}
pub fn get_entry(&self, id: &str) -> &Entry<T> {
self.entries
.get(id)
.unwrap_or_else(|| panic!("Entry with id '{}' not found", id))
}
pub fn get_entry_safe(&self, id: &str) -> Option<&Entry<T>> {
self.entries.get(id)
}
pub fn into_params<P>(&self, cb: impl FnMut(&Entry<T>) -> P) -> Vec<P>
where
P: Into<PageParams>,
{
self.entries.values().map(cb).collect()
}
pub fn into_pages<Params, Props>(
&self,
cb: impl FnMut(&Entry<T>) -> crate::route::Page<Params, Props>,
) -> crate::route::Pages<Params, Props>
where
Params: Into<PageParams>,
{
self.entries.values().map(cb).collect()
}
}
#[doc(hidden)]
pub trait ContentSourceInternal: Send + Sync {
fn init(&mut self);
fn get_name(&self) -> &str;
fn as_any(&self) -> &dyn Any;
fn entry_file_info(&self) -> Vec<(String, Vec<PathBuf>)>;
fn entry_raw_content(&self) -> FxHashMap<String, &str> {
FxHashMap::default()
}
fn entry_ids(&self) -> Vec<String>;
}
impl<T: 'static + Sync + Send> ContentSourceInternal for ContentSource<T> {
fn init(&mut self) {
self.entries = (self.init_method)()
.into_iter()
.map(|e| (e.id.clone(), e))
.collect();
}
fn get_name(&self) -> &str {
&self.name
}
fn as_any(&self) -> &dyn Any {
self
}
fn entry_file_info(&self) -> Vec<(String, Vec<PathBuf>)> {
self.entries
.values()
.map(|e| {
let files = e
.dependencies
.iter()
.map(|d| match d {
Dependency::File(p) => p.clone(),
})
.collect();
(e.id.clone(), files)
})
.collect()
}
fn entry_raw_content(&self) -> FxHashMap<String, &str> {
self.entries
.values()
.filter_map(|e| e.raw_content.as_deref().map(|rc| (e.id.clone(), rc)))
.collect()
}
fn entry_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.entries.keys().cloned().collect();
ids.sort();
ids
}
}