mod sectiondata; mod tags; pub use sectiondata::SectionData; pub use tags::Tags; use core::fmt; use std::str::FromStr; use camino::{Utf8Path, Utf8PathBuf}; use tokio::sync::RwLock; use crate::{WikError, WikFile, fs::Open, git::GitContext, quotes_decode, quotes_encode}; pub struct Tables { posts: Table, sections: Table, } impl Tables { const POSTS: &'static str = ".wikarden/table/posts"; const SECTIONS: &'static str = ".wikarden/table/sections"; pub async fn new(root: &Utf8Path) -> Result { Self::init(root)?; let posts_path = root.join(Self::POSTS); let sections_path = root.join(Self::SECTIONS); let posts = Table::new_and_load(posts_path).await?; let sections = Table::new_and_load(sections_path).await?; Ok(Self { posts, sections }) } fn init(root: &Utf8Path) -> Result<(), WikError> { let mut parent = root.join(Self::POSTS); parent.pop(); if let Err(_e) = std::fs::create_dir_all(&parent) { Err(WikError::DirectoryCreation { path: parent }) } else { Ok(()) } } pub async fn has_page>(&self, name: S) -> bool { self.posts.has(name).await } pub async fn has_section>(&self, section: S) -> bool { self.sections.has(section).await } pub async fn get_sections(&self) -> Vec> { self.sections.entries().await } pub async fn add_page>(&self, name: S, data: Tags) -> Result<(), WikError> { self.posts .add(TableEntry { name: name.into(), data, }) .await; self.posts.save().await } pub async fn add_section>( &self, section: S, data: SectionData, ) -> Result<(), WikError> { self.sections .add(TableEntry { name: section.into(), data, }) .await; self.sections.save().await } pub async fn del_page>(&self, name: S) -> Result<(), WikError> { self.posts.del(name.as_ref()).await; self.posts.save().await } pub async fn del_section>(&self, section: S) -> Result<(), WikError> { self.sections.del(section.as_ref()).await; self.sections.save().await } pub async fn replace_sections( &self, data: Vec>, ) -> Result<(), WikError> { self.sections.replace_entries(data).await; self.sections.save().await } pub fn sections(&self) -> &Table { &self.sections } pub fn posts(&self) -> &Table { &self.posts } pub fn git_add(gtx: &mut GitContext) { gtx.add_rel(Self::POSTS.into()).unwrap(); gtx.add_rel(Self::SECTIONS.into()).unwrap(); } } pub(crate) struct Table { path: Utf8PathBuf, entries: RwLock>>, } impl Table { pub async fn new_and_load(path: Utf8PathBuf) -> Result { let entries = Self::load(&path).await?; Ok(Self { path, entries: RwLock::new(entries), }) } pub async fn replace_entries(&self, entries: Vec>) { let mut lock = self.entries.write().await; *lock = entries; } pub async fn has>(&self, name: S) -> bool { let lock = self.entries.read().await; lock.iter() .position(|te| te.name.eq(name.as_ref())) .is_some() } //TODO: gen 2026-08: this should insert in sorted order instead of then sorting pub async fn add(&self, entry: TableEntry) { let mut lock = self.entries.write().await; lock.push(entry); Self::sort(&mut lock); } pub async fn del(&self, name: &str) -> Option> { let mut lock = self.entries.write().await; match lock.iter().position(|e| e.name == name) { None => return None, Some(idx) => Some(lock.remove(idx)), } } pub async fn entry_data(&self, name: &str) -> Option { let lock = self.entries.read().await; match lock.iter().position(|e| e.name == name) { None => return None, Some(idx) => Some(lock[idx].data.clone()), } } pub async fn modify_entry(&self, name: &str, f: F) -> Result where F: FnOnce(&mut TableEntry), { let mut lock = self.entries.write().await; match lock.iter().position(|e| e.name == name) { None => return Ok(false), Some(idx) => { let mut entry = &mut lock[idx]; f(&mut entry); Self::sort(&mut lock); drop(lock); self.save().await?; Ok(true) } } } pub async fn modify_entry_data(&self, name: &str, f: F) -> Result where F: FnOnce(&mut D), { let mut lock = self.entries.write().await; let actioned = match lock.iter().position(|e| e.name == name) { None => false, Some(idx) => { let entry = &mut lock[idx]; f(&mut entry.data); true } }; drop(lock); self.save().await.map(|_| actioned) } pub async fn entries(&self) -> Vec> { let lock = self.entries.read().await; lock.clone() } fn sort(entries: &mut [TableEntry]) { entries.sort_by(|a, b| a.name.cmp(&b.name)); } pub async fn save(&self) -> Result<(), WikError> { let mut lock = self.entries.write().await; Self::sort(&mut lock); let mut file = WikFile::open(self.path.clone(), Open::Create).await?; file.write_all_strs(lock.iter()).await } async fn load(path: &Utf8Path) -> Result>, WikError> { let file = WikFile::open(path.to_owned(), Open::CreateAppend).await?; let content = file.read_to_string().await?; let mut entries = vec![]; for line in content.lines() { let entry = TableEntry::from_str(line).unwrap(); entries.push(entry); } Self::sort(&mut entries); Ok(entries) } } pub trait TableData: std::fmt::Debug + std::fmt::Display + Clone { fn parse(raw: &str) -> Result where Self: Sized; } #[derive(Clone, Debug)] pub struct TableEntry { name: String, data: D, } impl TableEntry { pub fn new(name: String, data: D) -> Self { Self { name, data } } pub fn into_name(self) -> String { self.name } pub fn name(&self) -> &str { &self.name } pub fn set_name(&mut self, name: String) { self.name = name; } pub fn data(&self) -> &D { &self.data } } impl fmt::Display for TableEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let quoted = quotes_encode(&self.name); writeln!(f, "{quoted}\t{}", self.data) } } impl FromStr for TableEntry { type Err = &'static str; fn from_str(raw: &str) -> Result { let decode = quotes_decode(raw); match decode { None => Err("string does not start with a quote"), Some((name, rest)) => { let data_str = rest.trim_start(); let data = D::parse(data_str)?; Ok(Self { name, data }) } } } } /* impl FromStr for JournalLine { type Err = &'static str; fn from_str(raw: &str) -> Result { let decode = quotes_decode(raw); match decode { None => Err("string does not start with quote"), Some((path, rest)) => { let time_str = rest.trim_start(); let time: i64 = match time_str.parse() { Err(_) => return Err("time is not a number"), Ok(stamp) => stamp, }; match OffsetDateTime::from_unix_timestamp(time) { Err(_) => Err("timestamp is not valid"), Ok(time) => Ok(Self { path: path.into(), time, }), } } } } } */