#[warn(missing_docs)]
#[cfg(test)]
mod test;
mod include_ref;
mod includer;
mod parse;
pub use self::include_ref::IncludeRef;
pub use self::includer::{DebugIncluder, FetchedPage, Includer, NullIncluder};
use self::parse::parse_include_block;
use crate::data::PageRef;
use crate::settings::WikitextSettings;
use crate::tree::VariableMap;
use regex::{Regex, RegexBuilder};
use std::sync::LazyLock;
static INCLUDE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
RegexBuilder::new(r"^\[\[\s*include\s+")
.case_insensitive(true)
.multi_line(true)
.dot_matches_new_line(true)
.build()
.unwrap()
});
static VARIABLE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{\$(?P<name>[a-zA-Z0-9_\-]+)\}").unwrap());
pub fn include<'t, I, E, F>(
input: &'t str,
settings: &WikitextSettings,
mut includer: I,
invalid_return: F,
) -> Result<(String, Vec<PageRef>), E>
where
I: Includer<'t, Error = E>,
F: FnOnce() -> E,
{
if !settings.enable_page_syntax {
debug!("Includes are disabled for this input, skipping");
let output = str!(input);
let pages = vec![];
return Ok((output, pages));
}
info!(
"Inserting text for all include blocks in text ({} bytes)",
input.len(),
);
let mut ranges = Vec::new();
let mut includes = Vec::new();
for mtch in INCLUDE_REGEX.find_iter(input) {
let start = mtch.start();
trace!(
"Found include regex match (start {}, slice '{}')",
start,
mtch.as_str(),
);
match parse_include_block(input, start) {
Ok((include, end)) => {
ranges.push(start..end);
includes.push(include);
}
Err(_) => warn!("Unable to parse include regex match"),
}
}
let fetched_pages = includer.include_pages(&includes)?;
if includes.len() != fetched_pages.len() {
return Err(invalid_return());
}
let ranges_iter = ranges.into_iter();
let includes_iter = includes.into_iter();
let fetched_iter = fetched_pages.into_iter();
let joined_iter = ranges_iter.zip(includes_iter).zip(fetched_iter).rev();
let mut output = String::from(input);
let mut pages = Vec::new();
for ((range, include), fetched) in joined_iter {
let (page_ref, variables) = include.into();
debug!(
"Replacing range for included page ({}..{})",
range.start, range.end,
);
if page_ref != fetched.page_ref {
return Err(invalid_return());
}
let replace_with = match fetched.content {
Some(mut content) => {
replace_variables(content.to_mut(), &variables);
content
}
None => includer.no_such_include(&page_ref)?,
};
pages.push(page_ref);
output.replace_range(range, &replace_with);
}
pages.reverse();
Ok((output, pages))
}
fn replace_variables(content: &mut String, variables: &VariableMap) {
let mut matches = Vec::new();
for capture in VARIABLE_REGEX.captures_iter(content) {
let mtch = capture.get(0).unwrap();
let name = &capture["name"];
if let Some(value) = variables.get(name) {
matches.push((value, mtch.range()));
}
}
matches.reverse();
for (value, range) in matches {
content.replace_range(range, value);
}
}