nml/src/elements/link.rs

332 lines
8 KiB
Rust
Raw Normal View History

2024-07-24 13:20:29 +02:00
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
2024-08-01 16:15:10 +02:00
use crate::document::element::ContainerElement;
2024-07-24 13:20:29 +02:00
use crate::document::element::ElemKind;
use crate::document::element::Element;
2024-08-04 10:25:51 +02:00
use crate::lua::kernel::CTX;
2024-07-24 13:20:29 +02:00
use crate::parser::parser::Parser;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
2024-08-01 16:15:10 +02:00
use crate::parser::source::VirtualSource;
2024-07-24 13:20:29 +02:00
use crate::parser::util;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
2024-08-04 10:25:51 +02:00
use mlua::Error::BadArgument;
2024-07-24 13:20:29 +02:00
use mlua::Function;
use mlua::Lua;
use regex::Captures;
use regex::Regex;
use std::ops::Range;
use std::rc::Rc;
2024-08-04 10:25:51 +02:00
use std::sync::Arc;
2024-07-19 11:52:12 +02:00
#[derive(Debug)]
pub struct Link {
2024-08-02 13:36:04 +02:00
pub location: Token,
2024-08-01 16:15:10 +02:00
/// Display content of link
2024-08-02 13:36:04 +02:00
pub display: Vec<Box<dyn Element>>,
2024-08-01 16:15:10 +02:00
/// Url of link
2024-08-02 13:36:04 +02:00
pub url: String,
2024-07-19 11:52:12 +02:00
}
2024-07-24 13:20:29 +02:00
impl Element for Link {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Link" }
2024-08-01 16:15:10 +02:00
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> {
2024-07-24 13:20:29 +02:00
match compiler.target() {
2024-08-01 16:15:10 +02:00
Target::HTML => {
let mut result = format!(
"<a href=\"{}\">",
Compiler::sanitize(compiler.target(), self.url.as_str())
);
2024-08-02 10:34:56 +02:00
for elem in &self.display {
result += elem.compile(compiler, document)?.as_str();
}
2024-08-01 16:15:10 +02:00
result += "</a>";
Ok(result)
}
_ => todo!(""),
}
}
fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) }
}
impl ContainerElement for Link {
2024-08-02 10:34:56 +02:00
fn contained(&self) -> &Vec<Box<dyn Element>> { &self.display }
2024-08-01 16:15:10 +02:00
fn push(&mut self, elem: Box<dyn Element>) -> Result<(), String> {
if elem.downcast_ref::<Link>().is_some() {
return Err("Tried to push a link inside of a link".to_string());
2024-07-24 13:20:29 +02:00
}
2024-08-02 10:34:56 +02:00
self.display.push(elem);
Ok(())
2024-07-24 13:20:29 +02:00
}
2024-07-19 11:52:12 +02:00
}
pub struct LinkRule {
re: [Regex; 1],
}
impl LinkRule {
pub fn new() -> Self {
2024-07-24 13:20:29 +02:00
Self {
re: [Regex::new(r"\[((?:\\.|[^\\\\])*?)\]\(((?:\\.|[^\\\\])*?)\)").unwrap()],
}
2024-07-19 11:52:12 +02:00
}
}
impl RegexRule for LinkRule {
fn name(&self) -> &'static str { "Link" }
fn regexes(&self) -> &[Regex] { &self.re }
2024-07-24 13:20:29 +02:00
fn on_regex_match<'a>(
&self,
_: usize,
parser: &dyn Parser,
2024-08-01 16:15:10 +02:00
document: &'a (dyn Document<'a> + 'a),
2024-07-24 13:20:29 +02:00
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
2024-08-01 16:15:10 +02:00
let mut reports = vec![];
let link_display = match matches.get(1) {
Some(display) => {
if display.as_str().is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
2024-07-24 13:20:29 +02:00
.with_message("Empty link name")
.with_label(
2024-08-01 16:15:10 +02:00
Label::new((token.source().clone(), display.range()))
2024-07-24 13:20:29 +02:00
.with_message("Link name is empty")
.with_color(parser.colors().error),
)
.finish(),
);
2024-08-01 16:15:10 +02:00
return reports;
2024-07-24 13:20:29 +02:00
}
2024-08-01 16:15:10 +02:00
let processed = util::process_escaped('\\', "]", display.as_str());
if processed.is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
2024-07-24 13:20:29 +02:00
.with_message("Empty link name")
.with_label(
2024-08-01 16:15:10 +02:00
Label::new((token.source(), display.range()))
2024-07-24 13:20:29 +02:00
.with_message(format!(
"Link name is empty. Once processed, `{}` yields `{}`",
2024-08-01 16:15:10 +02:00
display.as_str().fg(parser.colors().highlight),
processed.fg(parser.colors().highlight),
2024-07-24 13:20:29 +02:00
))
.with_color(parser.colors().error),
)
.finish(),
);
2024-08-01 16:15:10 +02:00
return reports;
}
let source = Rc::new(VirtualSource::new(
Token::new(display.range(), token.source()),
"Link Display".to_string(),
processed,
));
match util::parse_paragraph(parser, source, document) {
Err(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
.with_message("Failed to parse link display")
.with_label(
Label::new((token.source(), display.range()))
.with_message(err.to_string())
.with_color(parser.colors().error),
)
.finish(),
);
return reports;
}
2024-08-02 10:34:56 +02:00
Ok(mut paragraph) => std::mem::replace(&mut paragraph.content, vec![]),
2024-07-24 13:20:29 +02:00
}
}
_ => panic!("Empty link name"),
};
2024-07-19 11:52:12 +02:00
2024-07-24 13:20:29 +02:00
let link_url = match matches.get(2) {
Some(url) => {
if url.as_str().is_empty() {
2024-08-01 16:15:10 +02:00
reports.push(
2024-07-19 11:52:12 +02:00
Report::build(ReportKind::Error, token.source(), url.start())
2024-07-24 13:20:29 +02:00
.with_message("Empty link url")
.with_label(
Label::new((token.source(), url.range()))
.with_message("Link url is empty")
.with_color(parser.colors().error),
)
.finish(),
);
2024-08-01 16:15:10 +02:00
return reports;
2024-07-24 13:20:29 +02:00
}
let text_content = util::process_text(document, url.as_str());
2024-07-19 11:52:12 +02:00
2024-07-24 13:20:29 +02:00
if text_content.as_str().is_empty() {
2024-08-01 16:15:10 +02:00
reports.push(
2024-07-19 11:52:12 +02:00
Report::build(ReportKind::Error, token.source(), url.start())
2024-07-24 13:20:29 +02:00
.with_message("Empty link url")
.with_label(
Label::new((token.source(), url.range()))
.with_message(format!(
"Link url is empty. Once processed, `{}` yields `{}`",
url.as_str().fg(parser.colors().highlight),
text_content.as_str().fg(parser.colors().highlight),
))
.with_color(parser.colors().error),
)
.finish(),
);
2024-08-01 16:15:10 +02:00
return reports;
2024-07-24 13:20:29 +02:00
}
2024-07-19 11:52:12 +02:00
text_content
2024-07-24 13:20:29 +02:00
}
_ => panic!("Empty link url"),
};
2024-07-19 11:52:12 +02:00
2024-07-24 13:20:29 +02:00
parser.push(
document,
2024-08-01 16:15:10 +02:00
Box::new(Link {
location: token,
display: link_display,
url: link_url,
}),
2024-07-24 13:20:29 +02:00
);
2024-07-19 11:52:12 +02:00
2024-08-01 16:15:10 +02:00
return reports;
2024-07-19 11:52:12 +02:00
}
2024-07-21 15:56:56 +02:00
2024-08-04 10:25:51 +02:00
fn lua_bindings<'lua>(&self, lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> {
let mut bindings = vec![];
bindings.push((
"push".to_string(),
lua.create_function(|_, (display, url): (String, String)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let source = Rc::new(VirtualSource::new(
ctx.location.clone(),
"Link Display".to_string(),
display,
));
let display_content =
match util::parse_paragraph(ctx.parser, source, ctx.document) {
Err(err) => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("display".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Failed to parse link display: {err}"
))),
});
return;
}
Ok(mut paragraph) => {
std::mem::replace(&mut paragraph.content, vec![])
}
};
ctx.parser.push(
ctx.document,
Box::new(Link {
location: ctx.location.clone(),
display: display_content,
url,
}),
);
})
});
result
})
.unwrap(),
));
Some(bindings)
}
2024-07-19 11:52:12 +02:00
}
2024-08-01 16:15:10 +02:00
#[cfg(test)]
mod tests {
2024-08-02 10:34:56 +02:00
use crate::elements::paragraph::Paragraph;
2024-08-01 16:15:10 +02:00
use crate::elements::style::Style;
2024-08-02 10:32:00 +02:00
use crate::elements::text::Text;
2024-08-01 16:15:10 +02:00
use crate::parser::langparser::LangParser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
Some [link](url).
[**BOLD link**](another url)
"#
.to_string(),
None,
));
let parser = LangParser::default();
let doc = parser.parse(source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "Some " };
Link { url == "url" } { Text { content == "link" }; };
Text { content == "." };
Link { url == "another url" } {
Style;
Text { content == "BOLD link" };
Style;
};
};
);
}
2024-08-04 10:25:51 +02:00
#[test]
fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
Some %<nml.link.push("link", "url")>%.
%<
nml.link.push("**BOLD link**", "another url")
>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let doc = parser.parse(source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "Some " };
Link { url == "url" } { Text { content == "link" }; };
Text { content == "." };
Link { url == "another url" } {
Style;
Text { content == "BOLD link" };
Style;
};
};
);
}
2024-08-01 16:15:10 +02:00
}