use regex::Captures; use regex::Regex; use regex::RegexBuilder; use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Target; use crate::document::document::Document; use crate::document::element::ElemKind; use crate::document::element::Element; use crate::elements::section::section_kind; use crate::elements::section::Section; use crate::lsp::semantic::Semantics; use crate::lua::kernel::CTX; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; use crate::parser::reports::Report; use crate::parser::rule::RegexRule; use crate::parser::source::Token; #[derive(Debug)] struct Toc { pub(self) location: Token, pub(self) title: Option, } impl Element for Toc { fn location(&self) -> &Token { &self.location } fn kind(&self) -> ElemKind { ElemKind::Block } fn element_name(&self) -> &'static str { "Toc" } fn compile( &self, compiler: &Compiler, document: &dyn Document, _cursor: usize, ) -> Result { let mut result = String::new(); let mut sections: Vec<(&Section, usize)> = vec![]; // Find last section with given depth fn last_matching(depth: usize, sections: &Vec<(&Section, usize)>) -> Option { for (idx, (section, _number)) in sections.iter().rev().enumerate() { if section.depth < depth { return None; } else if section.depth == depth { return Some(sections.len() - idx - 1); } } None } let content_borrow = document.content().borrow(); for elem in content_borrow.iter() { if let Some(section) = elem.downcast_ref::
() { if section.kind & section_kind::NO_TOC != 0 { continue; } let last = last_matching(section.depth, §ions); if let Some(last) = last { if sections[last].0.kind & section_kind::NO_NUMBER != 0 { sections.push((section, sections[last].1)); } else { sections.push((section, sections[last].1 + 1)) } } else { sections.push((section, 1)); } } } if sections.is_empty() { return Ok("".into()); } match compiler.target() { Target::HTML => { let match_depth = |current: usize, target: usize| -> String { let mut result = String::new(); for _ in current..target { result += "
    "; } for _ in target..current { result += "
"; } result }; result += "
"; result += format!( "{}", Compiler::sanitize( compiler.target(), self.title.as_ref().unwrap_or(&String::new()) ) ) .as_str(); let mut current_depth = 0; for (section, number) in sections { result += match_depth(current_depth, section.depth).as_str(); if section.kind & section_kind::NO_NUMBER != 0 { result += format!( "
  • {}
  • ", Compiler::refname(compiler.target(), section.title.as_str()), Compiler::sanitize(compiler.target(), section.title.as_str()) ) .as_str(); } else { result += format!( "
  • {}
  • ", Compiler::refname(compiler.target(), section.title.as_str()), Compiler::sanitize(compiler.target(), section.title.as_str()) ) .as_str(); } current_depth = section.depth; } match_depth(current_depth, 0); result += "
    "; } _ => todo!(""), } Ok(result) } } #[auto_registry::auto_registry(registry = "rules", path = "crate::elements::toc")] pub struct TocRule { re: [Regex; 1], } impl TocRule { pub fn new() -> Self { Self { re: [ RegexBuilder::new(r"(?:^|\n)(?:[^\S\n]*)#\+TABLE_OF_CONTENT(.*)") .multi_line(true) .build() .unwrap(), ], } } } impl RegexRule for TocRule { fn name(&self) -> &'static str { "Toc" } fn previous(&self) -> Option<&'static str> { Some("Layout") } fn regexes(&self) -> &[regex::Regex] { &self.re } fn enabled(&self, mode: &ParseMode, _id: usize) -> bool { !mode.paragraph_only } fn on_regex_match( &self, _index: usize, state: &ParserState, document: &dyn Document, token: Token, matches: Captures, ) -> Vec { let mut reports = vec![]; let name = matches.get(1).unwrap().as_str().trim_start().trim_end(); state.push( document, Box::new(Toc { location: token.clone(), title: (!name.is_empty()).then_some(name.to_string()), }), ); if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) { let start = matches .get(0) .map(|m| m.start() + token.source().content()[m.start()..].find('#').unwrap()) .unwrap(); sems.add(start..start + 2, tokens.toc_sep); sems.add( start + 2..start + 2 + "TABLE_OF_CONTENT".len(), tokens.toc_token, ); sems.add(matches.get(1).unwrap().range(), tokens.toc_title); } reports } fn register_bindings<'lua>(&self, lua: &'lua mlua::Lua) -> Vec<(String, mlua::Function<'lua>)> { let mut bindings = vec![]; bindings.push(( "push".to_string(), lua.create_function(|_, title: Option| { CTX.with_borrow(|ctx| { ctx.as_ref().map(|ctx| { ctx.state.push( ctx.document, Box::new(Toc { location: ctx.location.clone(), title, }), ) }); }); Ok(()) }) .unwrap(), )); bindings } } #[cfg(test)] mod tests { use std::rc::Rc; use crate::parser::langparser::LangParser; use crate::parser::parser::Parser; use crate::parser::source::SourceFile; use crate::validate_document; use crate::validate_semantics; use super::*; #[test] fn parser() { let source = Rc::new(SourceFile::with_content( "".to_string(), r#" #+TABLE_OF_CONTENT TOC # Section1 ## SubSection "# .to_string(), None, )); let parser = LangParser::default(); let (doc, _) = parser.parse( ParserState::new(&parser, None), source, None, ParseMode::default(), ); validate_document!(doc.content().borrow(), 0, Toc { title == Some("TOC".to_string()) }; Section; Section; ); } #[test] fn lua() { let source = Rc::new(SourceFile::with_content( "".to_string(), r#" %% %% "# .to_string(), None, )); let parser = LangParser::default(); let (doc, _) = parser.parse( ParserState::new(&parser, None), source, None, ParseMode::default(), ); validate_document!(doc.content().borrow(), 0, Toc { title == Some("TOC".to_string()) }; Toc { title == Option::::None }; ); } #[test] fn semantic() { let source = Rc::new(SourceFile::with_content( "".to_string(), r#" #+TABLE_OF_CONTENT TOC "# .to_string(), None, )); let parser = LangParser::default(); let (_, state) = parser.parse( ParserState::new_with_semantics(&parser, None), source.clone(), None, ParseMode::default(), ); validate_semantics!(state, source.clone(), 0, toc_sep { delta_line == 1, delta_start == 0, length == 2 }; toc_token { delta_line == 0, delta_start == 2, length == 16 }; toc_title { delta_line == 0, delta_start == 16, length == 4 }; ); } }