// prowl -- personal knowledge base // Copyright (c) 2025 zenmaya // // 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 . 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, pub aliases: Vec>, pub tags: Vec>, pub refs: Vec>, 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

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 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 { 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)], } 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) -> 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::>().into_boxed_slice(); let events = Box::into_raw(events); ParseTree { src, events } } } pub fn ast(&self) -> &[(Event<'_>, Range)] { // 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 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 document pub struct Document { meta: DocumentMeta, parse_tree: ParseTree, pub version: Option, } 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)> { 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)> { 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)], 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)], ) -> Result, std::ops::Range)>, &'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) -> 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) } }