use mlua::{Error::BadArgument, Function, Lua}; use regex::Regex; use crate::{compiler::compiler::Target, lua::kernel::CTX, parser::{parser::Parser, rule::RegexRule, source::{Source, Token}}}; use ariadne::{Report, Fmt, Label, ReportKind}; use crate::{compiler::compiler::Compiler, document::{document::Document, element::{ElemKind, Element, ReferenceableElement}}}; use std::{ops::Range, rc::Rc, sync::Arc}; #[derive(Debug)] pub struct Section { pub(self) location: Token, pub(self) title: String, // Section title pub(self) depth: usize, // Section depth pub(self) kind: u8, // Section kind, e.g numbered, unnumbred, ... pub(self) reference: Option, // Section reference name } impl Element for Section { fn location(&self) -> &Token { &self.location } fn kind(&self) -> ElemKind { ElemKind::Block } fn element_name(&self) -> &'static str { "Section" } fn to_string(&self) -> String { format!("{self:#?}") } fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) } fn compile(&self, compiler: &Compiler, _document: &Document) -> Result { match compiler.target() { Target::HTML => { Ok(format!("{1}", self.depth, compiler.sanitize(self.title.as_str()))) } Target::LATEX => Err("Unimplemented compiler".to_string()) } } } impl ReferenceableElement for Section { fn reference_name(&self) -> Option<&String> { self.reference.as_ref() } } pub struct SectionRule { re: [Regex; 1], } impl SectionRule { pub fn new() -> Self { Self { re: [Regex::new(r"(?:^|\n)(#{1,})(?:\{(.*)\})?((\*|\+){1,})?(.*)").unwrap()] } } } pub mod section_kind { pub const NONE : u8 = 0x00; pub const NO_TOC : u8 = 0x01; pub const NO_NUMBER : u8 = 0x02; } impl RegexRule for SectionRule { fn name(&self) -> &'static str { "Section" } fn regexes(&self) -> &[Regex] { &self.re } fn on_regex_match(&self, _: usize, parser: &dyn Parser, document: &Document, token: Token, matches: regex::Captures) -> Vec, Range)>> { let mut result = vec![]; let section_depth = match matches.get(1) { Some(depth) => { if depth.len() > 6 { result.push( Report::build(ReportKind::Error, token.source(), depth.start()) .with_message("Invalid section depth") .with_label( Label::new((token.source(), depth.range())) .with_message(format!("Section is of depth {}, which is greather than {} (maximum depth allowed)", depth.len().fg(parser.colors().info), 6.fg(parser.colors().info))) .with_color(parser.colors().error)) .finish()); return result; } depth.len() }, _ => panic!("Empty section depth"), }; // [Optional] Reference name let section_refname = matches.get(2).map_or_else(|| None, |refname| { // Check for duplicate reference if let Some((ref_doc, reference)) = document.get_reference(refname.as_str()) { result.push( Report::build(ReportKind::Warning, token.source(), refname.start()) .with_message("Duplicate reference name") .with_label( Label::new((token.source(), refname.range())) .with_message(format!("Reference with name `{}` is already defined in `{}`", refname.as_str().fg(parser.colors().highlight), ref_doc.source().name().as_str().fg(parser.colors().highlight))) .with_message(format!("`{}` conflicts with previously defined reference to {}", refname.as_str().fg(parser.colors().highlight), reference.element_name().fg(parser.colors().highlight))) .with_color(parser.colors().warning)) .with_label( Label::new((ref_doc.source(), reference.location().start()+1..reference.location().end() )) .with_message(format!("`{}` previously defined here", refname.as_str().fg(parser.colors().highlight))) .with_color(parser.colors().warning)) .with_note(format!("Previous reference was overwritten")) .finish()); } Some(refname.as_str().to_string()) }); // Section kind let section_kind = match matches.get(3) { Some(kind) => { match kind.as_str() { "*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC, "*" => section_kind::NO_NUMBER, "+" => section_kind::NO_TOC, "" => section_kind::NONE, _ => { result.push( Report::build(ReportKind::Error, token.source(), kind.start()) .with_message("Invalid section numbering kind") .with_label( Label::new((token.source(), kind.range())) .with_message(format!("Section numbering kind must be a combination of `{}` for unnumbered, and `{}` for non-listing; got `{}`", "*".fg(parser.colors().info), "+".fg(parser.colors().info), kind.as_str().fg(parser.colors().highlight))) .with_color(parser.colors().error)) .with_help(format!("Leave empty for a numbered listed section")) .finish()); return result; } } } _ => section_kind::NONE, }; // Spacing + Section name let section_name = match matches.get(5) { Some(name) => { let split = name.as_str().chars() .position(|c| !c.is_whitespace()) .unwrap_or(0); let section_name = &name.as_str()[split..]; if section_name.is_empty() // No name { result.push( Report::build(ReportKind::Error, token.source(), name.start()) .with_message("Missing section name") .with_label( Label::new((token.source(), name.range())) .with_message("Sections require a name before line end") .with_color(parser.colors().error)) .finish()); return result; } // No spacing if split == 0 { result.push( Report::build(ReportKind::Warning, token.source(), name.start()) .with_message("Missing section spacing") .with_label( Label::new((token.source(), name.range())) .with_message("Sections require at least one whitespace before the section's name") .with_color(parser.colors().warning)) .with_help(format!("Add a space before `{}`", section_name.fg(parser.colors().highlight))) .finish()); return result; } section_name.to_string() }, _ => panic!("Empty section name") }; parser.push(document, Box::new( Section { location: token.clone(), title: section_name, depth: section_depth, kind: section_kind, reference: section_refname } )); return result; } fn lua_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> { let mut bindings = vec![]; bindings.push(("push".to_string(), lua.create_function( |_, (title, depth, kind, reference) : (String, usize, String, Option)| { let kind = match kind.as_str() { "*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC, "*" => section_kind::NO_NUMBER, "+" => section_kind::NO_TOC, "" => section_kind::NONE, _ => return Err(BadArgument { to: Some("push".to_string()), pos: 3, name: Some("kind".to_string()), cause: Arc::new(mlua::Error::external( format!("Unknown section kind specified")))}) }; CTX.with_borrow(|ctx| ctx.as_ref().map(|ctx| { ctx.parser.push(ctx.document, Box::new(Section { location: ctx.location.clone(), title, depth, kind, reference })); })); Ok(()) }).unwrap())); bindings } }