Something went wrong. Try again.
personal knowledge base
djot markdown knowledge wiki
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467// prowl -- personal knowledge base// Copyright (c) 2025 zenmaya <maya@zenmaya.xyz>//// This file is part of prowl.//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
use async_lsp::lsp_types::{Position, Url};use jotdown::{AttributeKind, Attributes, Container, Event};use parse_tree::ParseTree;
#[derive(Debug, Clone, PartialEq, Eq)]pub struct DocumentMeta { pub title: Box<str>, pub aliases: Vec<Box<str>>, pub tags: Vec<Box<str>>, pub refs: Vec<Box<str>>, pub uri: Url,}
impl DocumentMeta { pub fn from_attributes(attrs: Attributes, uri: &Url) -> DocumentMeta { let default_title = Path::new(uri.path()) .file_stem() .expect("file should have a name") .to_string_lossy() .into();
let mut title = default_title; let mut aliases = vec![]; let mut tags = vec![]; let mut refs = vec![];
for (kind, value) in attrs { match kind { AttributeKind::Class => {} AttributeKind::Id => {} // TODO(max): also support first <h1> as an alternative AttributeKind::Pair { key: "title" } => { title = value.to_string().into(); } // djot itself doesn't support duplicate attributes, the djot reference implementation // however jotdown supports this explicitly // see <https://github.com/hellux/jotdown/blob/eb53e2a8f5643c8d7d514b38c7e5bdf5900e9857/src/attr.rs#L218> AttributeKind::Pair { key: "alias" } => { aliases.push(value.to_string().into()); } AttributeKind::Pair { key: "tag" } => { tags.push(value.to_string().into()); } AttributeKind::Pair { key: "ref" } => { refs.push(value.to_string().into()); } _ => { dbg!("Unknown attribute: ", kind, value); } } } DocumentMeta { title, aliases, tags, refs, uri: uri.clone(), } }
#[allow(dead_code)] pub fn to_attributes(&self) -> Attributes { todo!("Serialization"); }
pub fn get_relative_url_to_me(&self, url_other: &Url) -> String { let url = self.uri.make_relative(url_other).unwrap(); let url = if &url == "" { // special case where we link to ourselves // QUESTION maybe we don't want to suggest this at all? url_other.path_segments().unwrap().last().unwrap() } else { &url }; url.to_string() }}
impl mlua::IntoLua for DocumentMeta { fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> { let table = lua.create_table()?; table.set("title", self.title)?; { let aliases = lua.create_table()?; for alias in self.aliases { aliases.push(alias)?; } table.set("aliases", aliases)?; } { let tags = lua.create_table()?; for tag in self.tags { tags.push(tag)?; } table.set("tags", tags)?; } { let refs = lua.create_table()?; for r in self.refs { refs.push(r)?; } table.set("refs", refs)?; } table.set("uri", self.uri.to_string())?; table.into_lua(lua) }}
mod parse_tree { //! Module is a safety boundary
use std::ops::Range;
use jotdown::Event;
pub struct ParseTree { src: *mut str, // SAFETY: The lifetime `'static` is a placeholder, the actual lifetime // is `'self` and we must ensure we enforce it on every public method // which returns a reference. events: *mut [(Event<'static>, Range<usize>)], }
impl Clone for ParseTree { fn clone(&self) -> Self { Self::new(self.src().to_string().into_boxed_str()) } }
impl Drop for ParseTree { fn drop(&mut self) { // SAFETY: `src` must outlive references to `src` from `events`. // Here we drop `events` first and in `ParseTree::ast` we limit the // lifetime to `'self` ensuring there are no outstanding references // to `events` or `src` at this point. unsafe { drop(Box::from_raw(self.events)); drop(Box::from_raw(self.src)); } } }
// SAFETY: `ParseTree` is and `Send` and `Sync` because Box, Event and Range // are `Send` and `Sync`. We never pass pointers out of `ParseTree` and we only // use them to require `unsafe` to manipulate them. // For the code outside of this module, `ParseTree` behaves exactly like a `Box` unsafe impl Send for ParseTree {} unsafe impl Sync for ParseTree {}
impl ParseTree { pub fn new(src: Box<str>) -> ParseTree { let src: *mut str = Box::into_raw(src); // SAFETY: There is no way to specify a `'self` lifetime, so we use // unsafe to lie and get a `Event<'static>` out of the parser. We // manually ensure that those events don't outlive `'self`. unsafe { let parser = jotdown::Parser::new(&*src).into_offset_iter(); let events = parser.collect::<Vec<_>>().into_boxed_slice(); let events = Box::into_raw(events); ParseTree { src, events } } }
pub fn ast(&self) -> &[(Event<'_>, Range<usize>)] { // SAFETY: lifetimes are limited to `&self` unsafe { &*self.events } }
pub fn src(&self) -> &str { // SAFETY: lifetimes are limited to `&self` unsafe { &*self.src } } }}
#[derive(Debug)]#[allow(dead_code)]pub enum DocumentOpenError { IoError(String), MissingIdButOtherwiseHasFrontmatter(Position), MissingFrontmatter,}
impl From<tokio::io::Error> for DocumentOpenError { fn from(value: tokio::io::Error) -> Self { Self::IoError(value.to_string()) }}
impl From<&std::io::Error> for DocumentOpenError { fn from(value: &std::io::Error) -> Self { Self::IoError(value.to_string()) }}
#[derive(Debug, Clone, Copy)]pub enum Encoding { Utf8, Utf16,}
#[derive(Clone)]/// Represents in memory state of the current documentpub struct Document { meta: DocumentMeta, parse_tree: ParseTree, pub version: Option<i32>,}
impl Document { pub fn meta(&self) -> &DocumentMeta { &self.meta }
pub fn parse_tree(&self) -> &ParseTree { &self.parse_tree }
fn get_char_offset_utf8(&self, pos: Position) -> usize { // lines start at 0 // columns start at 0 // \n are not columns let line = pos.line as usize; let column = pos.character as usize; let src = self.parse_tree().src(); let mut char_offset = 0; let mut line_count = 0; for (i, c) in src.chars().enumerate() { if line_count == line { char_offset = i; break; } if c == '\n' { line_count += 1; } } char_offset + column }
fn get_char_offset_utf16(&self, pos: Position) -> usize { let mut current_line = 0; let mut char_offset = 0; let mut utf16_column = 0;
for c in self.parse_tree().src().chars() { if current_line == pos.line && utf16_column == pos.character { return char_offset; }
if c == '\n' { current_line += 1; utf16_column = 0; if current_line > pos.line { break; } } else { utf16_column += c.len_utf16() as u32; } char_offset += 1; }
char_offset }
pub fn get_char_offset(&self, pos: Position, encoding: Encoding) -> usize { match encoding { Encoding::Utf8 => self.get_char_offset_utf8(pos), Encoding::Utf16 => self.get_char_offset_utf16(pos), } }
fn get_position_utf8(&self, char_offset: usize) -> Position { let src = self.parse_tree().src(); let mut line_count = 0; let mut column_count = 0; for (i, c) in src.chars().enumerate() { if i == char_offset { break; } if c == '\n' { line_count += 1; column_count = 0; } else { column_count += 1; } } Position::new(line_count as u32, column_count as u32) }
fn get_position_utf16(&self, char_offset: usize) -> Position { let mut current_line = 0; let mut current_column = 0; let mut current_char = 0;
for c in self.parse_tree().src().chars() { if current_char == char_offset { break; }
if c == '\n' { current_line += 1; current_column = 0; } else { current_column += c.len_utf16() as u32; } current_char += 1; } Position::new(current_line as u32, current_column) }
pub fn get_position(&self, char_offset: usize, encoding: Encoding) -> Position { match encoding { Encoding::Utf8 => self.get_position_utf8(char_offset), Encoding::Utf16 => self.get_position_utf16(char_offset), } }
pub fn filter_events_by_range( &self, start: usize, end: usize, ) -> Vec<(&Event<'_>, &std::ops::Range<usize>)> { let ast = self.parse_tree().ast(); let mut matched_events = Vec::with_capacity(ast.len()); for (event, pos) in ast.iter() { if start <= pos.start && pos.end <= end { matched_events.push((event, pos)); } } matched_events }
/// Returns the events that completely surround the given range /// The range is inclusive on both ends /// And matches the Event::Start and Event::End events #[allow(dead_code)] pub fn filter_surrounding_events_by_range( &self, start: usize, end: usize, ) -> Vec<(&Event<'_>, &std::ops::Range<usize>)> { let ast = self.parse_tree().ast(); let mut stack = Vec::with_capacity(ast.len()); let mut matched_events = Vec::with_capacity(ast.len()); for (event, pos) in ast.iter() { match event { Event::Start(c, _) => { stack.push((c, pos.start, (event, pos))); } Event::End(c) => { if let Some((top, e_start, e_event)) = stack.pop() { if top == c { let full_range = e_start..pos.end;
if full_range.start <= start && full_range.end >= end { matched_events.push(e_event); matched_events.push((event, pos)); } } } } _ => { if pos.start <= start && pos.end >= end { matched_events.push((event, pos)); } } } } matched_events }
#[allow(dead_code)] pub fn find_matching_event_pair( events: &[(Event<'_>, std::ops::Range<usize>)], searched_event: &Container<'_>, ) -> (usize, usize) { let mut stack = Vec::with_capacity(events.len()); let mut begin = None; for (index, (event, _)) in events.iter().enumerate() { if let Some(begin) = begin { match event { Event::Start(c, _) => { stack.push(c); } Event::End(c) => { if let Some(top) = stack.pop() { assert_eq!(top, searched_event, "Unmatched event"); } else { assert_eq!(searched_event, c, "Unmatched event"); return (begin, index); } } _ => {} } } else { if let Event::Start(c, _) = event { if c == searched_event { begin = Some(index); } } } } unreachable!( "Should never reach here, if we are searching for a start event, we should find it" ) }
fn get_attributes<'a>( ast: &'a [(Event<'a>, std::ops::Range<usize>)], ) -> Result<Option<(Attributes<'a>, std::ops::Range<usize>)>, &'a Event<'a>> { ast.iter() .filter_map(|(event, pos)| match event { Event::ThematicBreak(attrs) => Some(Ok((attrs, pos))), Event::Blankline | Event::Softbreak | Event::Hardbreak => None, e => Some(Err(e)), }) .next() .transpose() .map(|v| v.map(|o| (o.0.clone(), o.1.clone()))) }
/// Opens the document and returns a `DocumentOpenResult` /// adds entry to the `DocumentStore` pub fn open(contents: &str, uri: &Url, version: Option<i32>) -> Document { let parse_tree = ParseTree::new(contents.to_string().into_boxed_str()); let ast = parse_tree.ast();
// For now lets remove the requirement for attributes let (attributes, _pos) = match Self::get_attributes(ast) { Ok(Some((ref attrs, ref pos))) => (attrs.clone(), pos.clone()), _ => (Attributes::new(), 0..0), }; let meta = DocumentMeta::from_attributes(attributes, uri); Document { meta, parse_tree, version, } }}
impl std::fmt::Debug for Document { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Document({url})", url = self.meta.uri) }}