Skip to content
This repository has been archived by the owner on Jan 4, 2024. It is now read-only.

Commit

Permalink
Tagged Indexes
Browse files Browse the repository at this point in the history
`- Name: !index page.md`
  • Loading branch information
Sky9x committed Nov 25, 2023
1 parent da97492 commit b2aabfa
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 8 deletions.
75 changes: 74 additions & 1 deletion src/front_matter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, ensure, Result};
use log::trace;
use serde::{Deserialize, Serialize};
use serde_yaml::value::{Tag, TaggedValue};
use serde_yaml::{Mapping, Value};

#[derive(Debug, Serialize, Deserialize, Default)]
Expand All @@ -14,15 +15,42 @@ pub struct FrontMatter {
pub nav: Vec<NavElem>,
}

// a folder is a nested structure with an index page and some child pages
// a category is a nested structure with NO index page (can't click on it directly) and some child pages

// --- defining files (optional name) ---
// file.md (file)
// just includes that file as a single, non nested page
//
//
// --- defining folders (optional name) ---
// folder/ (folder)
// semantically equivalent to `!index folder/index.md`
//
// !index page.md (tagged index)
// includes a folder with that index page (and nav defined there)
//
//
// --- defining categories ---
// Name: folder/* (include)
// equivalent to listing out each file alphabetically in a category
//
// Name: (category)
// - file1.md
// - Name: file2.md
// creates a category with all of those files

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "Value", into = "Value")]
#[serde(try_from = "Value", into = "Value")] // converting to/from yaml is much easier than to/from serde
pub enum NavElem {
// - file.md
// - Name: file.md
File { name: Option<String>, path: String },
// - folder/
// - Name: folder/
Folder { name: Option<String>, path: String },
// - Name: !index abc.md
TaggedIndex { name: Option<String>, path: String },
// - Name: folder/*
Include { name: String, path: String },
// - 'My Category':
Expand Down Expand Up @@ -66,13 +94,44 @@ fn parse_filename(name: Option<String>, mut path: String) -> Result<NavElem> {
}
}

fn parse_tagged(name: Option<String>, TaggedValue { tag, value }: TaggedValue) -> Result<NavElem> {
trace!(target: "parse_tagged", "name={name:?} tag={tag} value={value:?}");

if tag != "index" {
bail!("unknown tag: {tag}");
}
let Value::String(mut path) = value else {
bail!("!index tag takes a string path (put name before the tag?)");
};
if !path.ends_with(".md") {
bail!("!index tag {path:?} must be an md file");
}

// TODO: what happens if path is ./index.md?
// TODO: also need to figure out handling for files including themselves

if let Some(p) = path.strip_suffix("/index.md") {
// path = p, but reusing the allocation
path.truncate(p.len());

// normalize `!index folder/index.md` to `folder/`
// doesn't change semantics but fix will correct it
Ok(NavElem::Folder { name, path })
} else {
Ok(NavElem::TaggedIndex { name, path })
}
}

impl TryFrom<Value> for NavElem {
type Error = anyhow::Error;

fn try_from(value: Value) -> Result<Self> {
match value {
Value::Tagged(tag) => parse_tagged(None, *tag),

Value::String(s) => parse_filename(None, s),

// singleton map
Value::Mapping(map) if map.len() == 1 => {
let (k, v) = map.into_iter().next().unwrap();

Expand All @@ -81,6 +140,7 @@ impl TryFrom<Value> for NavElem {
};

match v {
Value::Tagged(tag) => parse_tagged(Some(name), *tag),
Value::String(path) => parse_filename(Some(name), path),
Value::Sequence(seq) => seq
.into_iter()
Expand Down Expand Up @@ -112,6 +172,12 @@ impl From<NavElem> for Value {
} => singleton_map(name, path + "/"),
NavElem::Folder { name: None, path } => Value::String(path + "/"),

NavElem::TaggedIndex {
name: Some(name),
path,
} => singleton_map(name, tagged_value("!index", path)),
NavElem::TaggedIndex { name: None, path } => tagged_value("!index", path),

NavElem::Include { name, path } => singleton_map(name, path + "/*"),

NavElem::Category { name, elems } => singleton_map(name, elems),
Expand All @@ -124,3 +190,10 @@ fn singleton_map(k: impl Into<Value>, v: impl Into<Value>) -> Value {
map.insert(k.into(), v.into());
Value::Mapping(map)
}

fn tagged_value(tag: impl Into<String>, value: impl Into<Value>) -> Value {
Value::Tagged(Box::new(TaggedValue {
tag: Tag::new(tag),
value: value.into(),
}))
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn run() -> Result<()> {
let src = Path::new(&config.file.build.source);

// this will read and process every file (specified in index navs)
let root = process_folder(&src, None)?;
let root = process_folder(&src.join("index.md"), None)?;

// print unused files
let DirCheck { unused, extra } = dir_check(&src, &root).context("dir check error")?;
Expand Down
6 changes: 3 additions & 3 deletions src/md.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@ pub fn extract_title_h1(content: &str) -> Result<String> {
Ok(title)
}

pub fn take_front_matter(content: &str) -> Result<(FrontMatter, &str)> {
pub fn take_front_matter(content: &str) -> Result<(Option<FrontMatter>, &str)> {
let Some(s) = content.strip_prefix("---") else {
return Ok((FrontMatter::default(), content));
return Ok((None, content));
};

let Some((fm, remaining)) = s.split_once("\n---") else {
bail!("unclosed front matter block")
};

Ok((serde_yaml::from_str(fm)?, remaining))
Ok((Some(serde_yaml::from_str(fm)?), remaining))
}

pub fn prepend_front_matter(fm: &FrontMatter, content: &str) -> String {
Expand Down
1 change: 1 addition & 0 deletions src/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type NavPageIter<'a> = Once<&'a NavPage>;
/// name is index.page.name (ignored for root)
#[derive(Debug)]
pub struct NavFolder {
// this may not be an index.md (if it was tagged in nav: `!index abc.md`)
pub index: NavPage,
pub children: Vec<NavItem>,
}
Expand Down
4 changes: 4 additions & 0 deletions src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ impl Path {
pub fn extension(&self) -> Option<&OsStr> {
self.inner.extension()
}

pub fn parent(&self) -> Option<Path> {
self.inner.parent().map(Path::new)
}
}

impl Debug for Path {
Expand Down
18 changes: 15 additions & 3 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::md;
use crate::nav::{NavCategory, NavFolder, NavItem, NavPage};
use crate::path::Path;
use anyhow::{anyhow, bail, ensure, Result};
use log::info;
use log::{debug, info};
use std::fs;

pub fn process_item(elem: &NavElem, dir: &Path) -> Result<NavItem> {
Expand All @@ -15,6 +15,10 @@ pub fn process_item(elem: &NavElem, dir: &Path) -> Result<NavItem> {
.map(NavItem::Page)
}
NavElem::Folder { name, path } => {
// folder/ is implicitly folder/index.md
process_folder(&dir.join(path).join("index.md"), name.clone()).map(NavItem::Folder)
}
NavElem::TaggedIndex { name, path } => {
process_folder(&dir.join(path), name.clone()).map(NavItem::Folder)
}
NavElem::Include { name, path } => {
Expand All @@ -31,6 +35,12 @@ fn process_page(path: &Path, name: Option<String>) -> Result<NavPage> {

let (fm, content) = unwrap!(md::take_front_matter(&raw), "invalid fm in {path}");

// don't debug print empty fm
if let Some(fm) = &fm {
debug!(target: "take_front_matter", "{path}: {fm:#?}");
}
let fm = fm.unwrap_or_default();

let built_content = md::build(content);
let fixed_content = md::fix(content);
let fixed_content = md::prepend_front_matter(&fm, &fixed_content);
Expand Down Expand Up @@ -65,7 +75,9 @@ fn process_page(path: &Path, name: Option<String>) -> Result<NavPage> {
}

pub fn process_folder(path: &Path, name: Option<String>) -> Result<NavFolder> {
let index = process_page(&path.join("index.md"), name)?;
let index = process_page(path, name)?;

let dir = path.parent().unwrap();

ensure!(
!index.fm.nav.is_empty(),
Expand All @@ -76,7 +88,7 @@ pub fn process_folder(path: &Path, name: Option<String>) -> Result<NavFolder> {
.fm
.nav
.iter()
.map(|elem| process_item(elem, path))
.map(|elem| process_item(elem, &dir))
.collect::<Result<Vec<NavItem>>>()?;

Ok(NavFolder { index, children })
Expand Down

0 comments on commit b2aabfa

Please sign in to comment.