mod html;
mod links;
mod quotes;
mod scan;
use crate::config::SiteConfig;
use crate::engine::ast::{Inline, TextRange};
use crate::warn::WarningCollector;
const MAX_DEPTH: u8 = 8;
pub(crate) fn parse_inline(
flat: &str,
line: TextRange,
config: &SiteConfig,
warnings: &mut WarningCollector,
) -> Vec<Inline> {
parse_at_depth(flat, line, config, warnings, 0)
}
fn parse_at_depth(
flat: &str,
range: TextRange,
config: &SiteConfig,
warnings: &mut WarningCollector,
depth: u8,
) -> Vec<Inline> {
if range.is_empty() {
return Vec::new();
}
if depth > MAX_DEPTH {
return vec![Inline::Text(range)];
}
let tokens = scan::scan(flat, range, config, warnings, depth);
quotes::do_quotes(flat, tokens)
}
#[cfg(test)]
mod testutil {
use crate::config::{MagicLinks, MagicWordDef, NamespaceDef, SiteConfig, SiteConfigBuilder};
use crate::engine::ast::{Inline, TextRange};
use crate::warn::{Warning, WarningCollector};
fn builder() -> SiteConfigBuilder {
SiteConfig::builder("ru", "Тест")
.namespace(NamespaceDef::main())
.namespace(NamespaceDef::new(10, "Template", "Шаблон", ["Ш"], true))
.namespace(NamespaceDef::new(14, "Category", "Категория", ["К"], true))
.namespace(NamespaceDef::new(
6,
"File",
"Файл",
["Image", "Изображение"],
true,
))
.namespace(NamespaceDef::new(
828,
"Module",
"Модуль",
Vec::<String>::new(),
true,
))
.interwiki(["fr", "de"])
.link_trail("a-zа-яё")
.magic_word(MagicWordDef::new(
"redirect",
["#REDIRECT", "#ПЕРЕНАПРАВЛЕНИЕ"],
false,
))
.magic_word(MagicWordDef::new("notoc", ["__NOTOC__"], true))
.magic_word(MagicWordDef::new("toc", ["__TOC__"], true))
.magic_word(MagicWordDef::new("!", ["!"], true))
.magic_word(MagicWordDef::new("=", ["="], true))
}
pub(super) fn config() -> SiteConfig {
builder().build().unwrap()
}
pub(super) fn config_no_magic() -> SiteConfig {
builder()
.magic_links(MagicLinks {
rfc: false,
pmid: false,
isbn: false,
})
.build()
.unwrap()
}
pub(super) fn parse(src: &str) -> (Vec<Inline>, Vec<Warning>) {
parse_with(src, &config())
}
pub(super) fn parse_with(src: &str, config: &SiteConfig) -> (Vec<Inline>, Vec<Warning>) {
let mut warnings = WarningCollector::new();
let out = super::parse_inline(
src,
TextRange::new(0, src.len() as u32),
config,
&mut warnings,
);
(out, warnings.into_vec())
}
pub(super) fn check(src: &str, expected: &str) {
let (nodes, warnings) = parse(src);
assert_eq!(dump(src, &nodes), expected, "input: {src:?}");
assert_eq!(warnings, Vec::new(), "input: {src:?}");
}
pub(super) fn dump(flat: &str, nodes: &[Inline]) -> String {
let parts: Vec<String> = nodes.iter().map(|n| dump_one(flat, n)).collect();
parts.join(" ")
}
fn dump_one(flat: &str, node: &Inline) -> String {
match node {
Inline::Text(r) => format!("Text({})", r.slice(flat)),
Inline::Entity { range, decoded } => {
format!("Ent({}={decoded})", range.slice(flat))
}
Inline::Marker { id, kind } => format!("Marker({kind:?}#{})", id.0),
Inline::Link {
target,
written: _,
fragment,
label,
trail,
pipe_trick,
} => {
assert!(!pipe_trick);
let mut s = format!("Link({}", target.text());
if let Some(f) = fragment {
s.push('#');
s.push_str(f);
}
if let Some(l) = label {
s.push('|');
s.push_str(&dump(flat, l));
}
if let Some(t) = trail {
s.push('+');
s.push_str(t.slice(flat));
}
s.push(')');
s
}
Inline::Category { category, sortkey } => match sortkey {
Some(k) => format!("Cat({}|{k})", category.text()),
None => format!("Cat({})", category.text()),
},
Inline::InterLanguage {
prefix,
target,
escaped,
} => {
let colon = if *escaped { ":" } else { "" };
format!("Iw({colon}{prefix}:{target})")
}
Inline::Image {
target,
params,
caption,
} => {
let ps: Vec<&str> = params.iter().map(|p| p.slice(flat)).collect();
match caption {
Some(c) => {
format!(
"Img({}|[{}]|{})",
target.text(),
ps.join(","),
dump(flat, c)
)
}
None => format!("Img({}|[{}])", target.text(), ps.join(",")),
}
}
Inline::ExternalLink { url, label } => match label {
Some(l) => format!("Ext({url}|{})", dump(flat, l)),
None => format!("Ext({url})"),
},
Inline::AutoUrl { url, range } => format!("Auto({url}={})", range.slice(flat)),
Inline::MagicLink { kind, range } => format!("Magic({kind:?}:{})", range.slice(flat)),
Inline::Italic(inner) => format!("I[{}]", dump(flat, inner)),
Inline::Bold(inner) => format!("B[{}]", dump(flat, inner)),
Inline::HtmlTag {
name,
closing,
self_closing,
attributes,
} => {
let mut s = format!("Html({name}");
if *closing {
s.push_str("/c");
}
if *self_closing {
s.push_str("/s");
}
if let Some(a) = attributes {
s.push(' ');
s.push_str(a.slice(flat));
}
s.push(')');
s
}
Inline::BehaviorSwitch { id, range } => format!("Sw({id}:{})", range.slice(flat)),
}
}
}
#[cfg(test)]
mod tests {
use super::testutil::{check, config, parse};
use super::{MAX_DEPTH, parse_at_depth};
use crate::engine::ast::{Inline, TextRange};
use crate::warn::WarningCollector;
#[test]
fn empty_line_yields_no_nodes() {
let (nodes, warnings) = parse("");
assert_eq!(nodes, Vec::new());
assert_eq!(warnings, Vec::new());
}
#[test]
fn plain_text_is_one_node() {
check("hello мир", "Text(hello мир)");
}
#[test]
fn depth_cap_returns_literal_text() {
let cfg = config();
let mut warnings = WarningCollector::new();
let src = "''x''";
let out = parse_at_depth(
src,
TextRange::new(0, src.len() as u32),
&cfg,
&mut warnings,
MAX_DEPTH + 1,
);
assert_eq!(out, vec![Inline::Text(TextRange::new(0, 5))]);
assert_eq!(warnings.into_vec(), Vec::new());
}
#[test]
fn deeply_nested_captions_go_literal_past_the_cap() {
let mut src = String::new();
for _ in 0..10 {
src.push_str("[[File:A.jpg|");
}
src.push_str("deep");
src.push_str(&"]]".repeat(10));
let (nodes, warnings) = parse(&src);
let dumped = super::testutil::dump(&src, &nodes);
assert!(dumped.contains("Text([[File:A.jpg|"), "dump: {dumped}");
assert_eq!(warnings, Vec::new());
}
#[test]
fn kitchen_sink_line() {
check(
"a ''b'' [[c]] & <br/> __NOTOC__ https://x.org RFC 1",
"Text(a ) I[Text(b)] Text( ) Link(C) Text( ) Ent(&=&) Text( ) Html(br/s) \
Text( ) Sw(notoc:__NOTOC__) Text( ) Auto(https://x.org=https://x.org) Text( ) \
Magic(Rfc:RFC 1)",
);
}
}