use std::collections::BTreeMap;
use crate::config::SiteConfig;
use crate::engine::pp::{Part, PpDom, PpKind, PpNodeId};
use crate::engine::source::{SourceId, SourceStore, Span};
use crate::model::{ModuleInvocation, Origin, Span as ModelSpan, TemplateCall};
use crate::title::{Namespace, Title};
use crate::warn::WarningCollector;
pub(crate) fn template_calls(
dom: &PpDom,
store: &SourceStore,
config: &SiteConfig,
_warnings: &mut WarningCollector,
) -> (Vec<TemplateCall>, Vec<ModuleInvocation>) {
let mut calls = Vec::new();
let mut invocations = Vec::new();
let mut stack: Vec<PpNodeId> = Vec::new();
push_reversed(&mut stack, dom.root());
while let Some(id) = stack.pop() {
match &dom.node(id).kind {
PpKind::Template { parts } => {
classify(
dom,
store,
config,
parts,
dom.node(id).span,
&mut calls,
&mut invocations,
);
push_parts(&mut stack, parts);
}
PpKind::TplArg { parts } => push_parts(&mut stack, parts),
PpKind::Text | PpKind::Comment | PpKind::Ignored | PpKind::ExtTag { .. } => {}
}
}
(calls, invocations)
}
fn classify(
dom: &PpDom,
store: &SourceStore,
config: &SiteConfig,
parts: &[Part],
span: Span,
calls: &mut Vec<TemplateCall>,
invocations: &mut Vec<ModuleInvocation>,
) {
let Some(name_part) = parts.first() else {
return; };
let Some(raw) = literal_text(dom, store, &name_part.children) else {
return; };
let name_raw = raw.trim().to_owned();
if name_raw.contains('\n') {
return;
}
let target = strip_subst(&name_raw);
if let Some(function) = target.strip_prefix('#') {
extract_invocation(dom, store, config, parts, function, invocations);
return;
}
if parts.len() == 1 && config.resolve_magic_word(target).is_some() {
return;
}
if let Some((prefix, _)) = target.split_once(':') {
if config
.resolve_magic_word(&target[..prefix.len() + 1])
.is_some()
{
return;
}
}
let Ok(title) = Title::parse_with_default(target, config, Namespace::TEMPLATE) else {
return; };
calls.push(TemplateCall::new(
title,
name_raw,
param_map(dom, store, parts.get(1..).unwrap_or(&[])),
Origin::Input,
model_span(span),
false,
));
}
fn extract_invocation(
dom: &PpDom,
store: &SourceStore,
config: &SiteConfig,
parts: &[Part],
function: &str,
invocations: &mut Vec<ModuleInvocation>,
) {
let Some((id, module_name)) = function.split_once(':') else {
return; };
let id = id.trim();
let is_invoke = match config.resolve_magic_word(id) {
Some(word) => word == "invoke",
None => id.eq_ignore_ascii_case("invoke"),
};
if !is_invoke {
return;
}
let Ok(module) = Title::parse_with_default(module_name, config, Namespace::MODULE) else {
return; };
let function_name = parts
.get(1)
.map(|part| loose_text(dom, store, &part.children).trim().to_owned())
.unwrap_or_default();
let args = param_map(dom, store, parts.get(2..).unwrap_or(&[]));
invocations.push(ModuleInvocation::new(module, function_name, args));
}
fn param_map(dom: &PpDom, store: &SourceStore, parts: &[Part]) -> BTreeMap<String, String> {
let mut params = BTreeMap::new();
let mut positional = 0_usize;
for part in parts {
if let Some((key, value)) = named_param(dom, store, part) {
params.insert(key, value);
} else {
positional += 1;
params.insert(positional.to_string(), store.slice(part.span).to_owned());
}
}
params
}
fn named_param(dom: &PpDom, store: &SourceStore, part: &Part) -> Option<(String, String)> {
let eq = part.eq?;
let eq_node = part.children.get(eq)?;
let key = loose_text(dom, store, &part.children[..eq])
.trim()
.to_owned();
let value_start = dom
.node(*eq_node)
.span
.end
.clamp(part.span.start, part.span.end);
let value = store
.slice(Span::new(part.span.src, value_start, part.span.end))
.trim()
.to_owned();
Some((key, value))
}
fn literal_text(dom: &PpDom, store: &SourceStore, children: &[PpNodeId]) -> Option<String> {
let mut out = String::new();
for &id in children {
let node = dom.node(id);
match &node.kind {
PpKind::Text => out.push_str(store.slice(node.span)),
PpKind::Comment | PpKind::Ignored => {}
PpKind::Template { .. } | PpKind::TplArg { .. } | PpKind::ExtTag { .. } => {
return None;
}
}
}
Some(out)
}
fn loose_text(dom: &PpDom, store: &SourceStore, children: &[PpNodeId]) -> String {
let mut out = String::new();
for &id in children {
let node = dom.node(id);
if matches!(node.kind, PpKind::Text) {
out.push_str(store.slice(node.span));
}
}
out
}
fn strip_subst(name: &str) -> &str {
for prefix in ["safesubst:", "subst:"] {
if let Some(rest) = strip_ci_prefix(name, prefix) {
return rest.trim_start();
}
}
name
}
fn strip_ci_prefix<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
let head = text.get(..prefix.len())?;
if head.eq_ignore_ascii_case(prefix) {
Some(&text[prefix.len()..])
} else {
None
}
}
fn model_span(span: Span) -> Option<ModelSpan> {
(span.src == SourceId(0)).then_some(ModelSpan {
start: span.start as usize,
end: span.end as usize,
})
}
fn push_parts(stack: &mut Vec<PpNodeId>, parts: &[Part]) {
for part in parts.iter().rev() {
push_reversed(stack, &part.children);
}
}
fn push_reversed(stack: &mut Vec<PpNodeId>, ids: &[PpNodeId]) {
stack.extend(ids.iter().rev().copied());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{MagicWordDef, NamespaceDef};
use crate::engine::pp::PpNode;
fn cfg() -> SiteConfig {
SiteConfig::builder("ru", "Википедия")
.namespace(NamespaceDef::main())
.namespace(NamespaceDef::new(10, "Template", "Шаблон", ["Ш"], true))
.namespace(NamespaceDef::new(
14,
"Category",
"Категория",
Vec::<String>::new(),
true,
))
.namespace(NamespaceDef::new(
828,
"Module",
"Модуль",
Vec::<String>::new(),
true,
))
.magic_word(MagicWordDef::new("pagename", ["PAGENAME"], true))
.magic_word(MagicWordDef::new("!", ["!"], true))
.magic_word(MagicWordDef::new("=", ["="], true))
.magic_word(MagicWordDef::new("if", ["if"], false))
.magic_word(MagicWordDef::new("uc", ["UC:"], false))
.magic_word(MagicWordDef::new("invoke", ["invoke", "вызвать"], false))
.build()
.unwrap()
}
fn cfg_without_invoke() -> SiteConfig {
SiteConfig::builder("en", "Wiki")
.namespace(NamespaceDef::main())
.namespace(NamespaceDef::new(10, "Template", "Template", ["T"], true))
.namespace(NamespaceDef::new(
828,
"Module",
"Модуль",
Vec::<String>::new(),
true,
))
.build()
.unwrap()
}
fn at(text: &str, needle: &str) -> (u32, u32) {
let start = text.find(needle).unwrap();
(start as u32, (start + needle.len()) as u32)
}
struct Dom {
store: SourceStore,
dom: PpDom,
src: SourceId,
text: String,
}
impl Dom {
fn new(text: &str) -> Self {
let mut store = SourceStore::new();
let (src, _) = store.push(text, 1 << 20);
Self {
store,
dom: PpDom::new(),
src,
text: text.to_owned(),
}
}
fn span(&self, start: u32, end: u32) -> Span {
Span::new(self.src, start, end)
}
fn tn(&mut self, start: u32, end: u32) -> PpNodeId {
self.node(PpKind::Text, start, end)
}
fn text_at(&mut self, needle: &str) -> PpNodeId {
let (start, end) = at(&self.text, needle);
self.tn(start, end)
}
fn node(&mut self, kind: PpKind, start: u32, end: u32) -> PpNodeId {
let span = self.span(start, end);
self.dom.push(PpNode { kind, span })
}
fn part(&self, children: Vec<PpNodeId>, eq: Option<usize>, start: u32, end: u32) -> Part {
Part {
children,
eq,
span: self.span(start, end),
}
}
fn part_at(&self, children: Vec<PpNodeId>, eq: Option<usize>, needle: &str) -> Part {
let (start, end) = at(&self.text, needle);
self.part(children, eq, start, end)
}
fn template(&mut self, parts: Vec<Part>, start: u32, end: u32) -> PpNodeId {
self.node(PpKind::Template { parts }, start, end)
}
fn root(&mut self, id: PpNodeId) {
self.dom.push_root(id);
}
fn run(&self, config: &SiteConfig) -> (Vec<TemplateCall>, Vec<ModuleInvocation>) {
let mut warnings = WarningCollector::new();
template_calls(&self.dom, &self.store, config, &mut warnings)
}
}
fn params(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|&(k, v)| (k.to_owned(), v.to_owned()))
.collect()
}
#[test]
fn named_and_positional_params_follow_whitespace_rules() {
let mut d = Dom::new("{{t| a |k = v |x=1|x=2}}");
let name = d.tn(2, 3);
let p0 = d.part(vec![name], None, 2, 3);
let v1 = d.tn(4, 7);
let p1 = d.part(vec![v1], None, 4, 7);
let k = d.tn(8, 10);
let k_eq = d.tn(10, 11);
let k_v = d.tn(11, 14);
let p2 = d.part(vec![k, k_eq, k_v], Some(1), 8, 14);
let x1 = d.tn(15, 16);
let x1_eq = d.tn(16, 17);
let x1_v = d.tn(17, 18);
let p3 = d.part(vec![x1, x1_eq, x1_v], Some(1), 15, 18);
let x2 = d.tn(19, 20);
let x2_eq = d.tn(20, 21);
let x2_v = d.tn(21, 22);
let p4 = d.part(vec![x2, x2_eq, x2_v], Some(1), 19, 22);
let tpl = d.template(vec![p0, p1, p2, p3, p4], 0, 24);
d.root(tpl);
let (calls, invocations) = d.run(&cfg());
assert!(invocations.is_empty());
assert_eq!(calls.len(), 1);
let call = &calls[0];
assert_eq!(call.title().text(), "Шаблон:T");
assert_eq!(call.name_raw(), "t");
assert_eq!(
call.params(),
¶ms(&[("1", " a "), ("k", "v"), ("x", "2")])
);
assert_eq!(call.origin(), &Origin::Input);
assert_eq!(call.span(), Some(ModelSpan { start: 0, end: 24 }));
assert!(!call.resolved());
}
#[test]
fn comments_and_ignored_content_vanish_from_the_name() {
let mut d = Dom::new("{{a<!--c-->b<noinclude>z</noinclude>}}");
let a = d.tn(2, 3);
let comment = d.node(PpKind::Comment, 3, 11);
let b = d.tn(11, 12);
let ignored = d.node(PpKind::Ignored, 12, 36);
let p0 = d.part(vec![a, comment, b, ignored], None, 2, 36);
let tpl = d.template(vec![p0], 0, 38);
d.root(tpl);
let (calls, _) = d.run(&cfg());
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].title().text(), "Шаблон:Ab");
assert_eq!(calls[0].name_raw(), "ab");
}
#[test]
fn nested_call_inside_a_part_is_extracted_in_document_order() {
let mut d = Dom::new("{{a|{{b}}}}");
let b_name = d.tn(6, 7);
let b_p0 = d.part(vec![b_name], None, 6, 7);
let inner = d.template(vec![b_p0], 4, 9);
let a_name = d.tn(2, 3);
let p0 = d.part(vec![a_name], None, 2, 3);
let p1 = d.part(vec![inner], None, 4, 9);
let outer = d.template(vec![p0, p1], 0, 11);
d.root(outer);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:A", "Шаблон:B"]);
assert_eq!(calls[0].params(), ¶ms(&[("1", "{{b}}")]));
}
#[test]
fn subst_prefixes_are_stripped_for_resolution_only() {
let mut d = Dom::new("{{subst:X}}{{SafeSubst: Y }}");
let x = d.tn(2, 9);
let px = d.part(vec![x], None, 2, 9);
let t1 = d.template(vec![px], 0, 11);
let y = d.tn(13, 26);
let py = d.part(vec![y], None, 13, 26);
let t2 = d.template(vec![py], 11, 28);
d.root(t1);
d.root(t2);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:X", "Шаблон:Y"]);
assert_eq!(calls[0].name_raw(), "subst:X");
assert_eq!(calls[1].name_raw(), "SafeSubst: Y");
}
#[test]
fn leading_colon_forces_the_main_namespace() {
let mut d = Dom::new("{{:MainPage}}");
let name = d.tn(2, 11);
let p0 = d.part(vec![name], None, 2, 11);
let tpl = d.template(vec![p0], 0, 13);
d.root(tpl);
let (calls, _) = d.run(&cfg());
assert_eq!(calls.len(), 1);
assert!(calls[0].title().namespace().is_main());
assert_eq!(calls[0].title().text(), "MainPage");
assert_eq!(calls[0].name_raw(), ":MainPage");
}
#[test]
fn bare_magic_words_are_skipped() {
let mut d = Dom::new("{{PAGENAME}}{{!}}{{=}}");
let n1 = d.tn(2, 10);
let p1 = d.part(vec![n1], None, 2, 10);
let t1 = d.template(vec![p1], 0, 12);
let n2 = d.tn(14, 15);
let p2 = d.part(vec![n2], None, 14, 15);
let t2 = d.template(vec![p2], 12, 17);
let n3 = d.tn(19, 20);
let p3 = d.part(vec![n3], None, 19, 20);
let t3 = d.template(vec![p3], 17, 22);
d.root(t1);
d.root(t2);
d.root(t3);
let (calls, invocations) = d.run(&cfg());
assert!(calls.is_empty());
assert!(invocations.is_empty());
}
#[test]
fn colon_form_parser_functions_are_skipped_but_their_parts_are_walked() {
let mut d = Dom::new("{{uc:hello|{{b}}}}");
let b_name = d.tn(13, 14);
let b_p0 = d.part(vec![b_name], None, 13, 14);
let inner = d.template(vec![b_p0], 11, 16);
let name = d.tn(2, 10);
let p0 = d.part(vec![name], None, 2, 10);
let p1 = d.part(vec![inner], None, 11, 16);
let outer = d.template(vec![p0, p1], 0, 18);
d.root(outer);
let (calls, invocations) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:B"]);
assert!(invocations.is_empty());
}
#[test]
fn wrong_case_or_extra_parts_make_a_magic_word_a_template() {
let mut d = Dom::new("{{pagename}}{{!|x}}");
let n1 = d.tn(2, 10);
let p1 = d.part(vec![n1], None, 2, 10);
let t1 = d.template(vec![p1], 0, 12);
let n2 = d.tn(14, 15);
let p2 = d.part(vec![n2], None, 14, 15);
let x = d.tn(16, 17);
let p3 = d.part(vec![x], None, 16, 17);
let t2 = d.template(vec![p2, p3], 12, 19);
d.root(t1);
d.root(t2);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:Pagename", "Шаблон:!"]);
assert_eq!(calls[1].params(), ¶ms(&[("1", "x")]));
}
#[test]
fn parser_functions_are_skipped_but_their_parts_are_walked() {
let mut d = Dom::new("{{#if:x|{{b}}}}{{#expr:1+1}}{{#invoke}}");
let b_name = d.tn(10, 11);
let b_p0 = d.part(vec![b_name], None, 10, 11);
let inner = d.template(vec![b_p0], 8, 13);
let if_name = d.tn(2, 7);
let if_p0 = d.part(vec![if_name], None, 2, 7);
let if_p1 = d.part(vec![inner], None, 8, 13);
let t1 = d.template(vec![if_p0, if_p1], 0, 15);
let expr_name = d.tn(17, 26);
let expr_p0 = d.part(vec![expr_name], None, 17, 26);
let t2 = d.template(vec![expr_p0], 15, 28);
let inv_name = d.tn(30, 37);
let inv_p0 = d.part(vec![inv_name], None, 30, 37);
let t3 = d.template(vec![inv_p0], 28, 39);
d.root(t1);
d.root(t2);
d.root(t3);
let (calls, invocations) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:B"]);
assert!(invocations.is_empty());
}
#[test]
fn invoke_builds_a_module_invocation_with_args() {
let mut d = Dom::new("{{#invoke:Строка|replace|xx|yy}}");
let name = d.text_at("#invoke:Строка");
let p0 = d.part_at(vec![name], None, "#invoke:Строка");
let f = d.text_at("replace");
let p1 = d.part_at(vec![f], None, "replace");
let a1 = d.text_at("xx");
let p2 = d.part_at(vec![a1], None, "xx");
let a2 = d.text_at("yy");
let p3 = d.part_at(vec![a2], None, "yy");
let end = d.text.len() as u32;
let tpl = d.template(vec![p0, p1, p2, p3], 0, end);
d.root(tpl);
let (calls, invocations) = d.run(&cfg());
assert!(calls.is_empty());
assert_eq!(invocations.len(), 1);
let inv = &invocations[0];
assert_eq!(inv.module().text(), "Модуль:Строка");
assert!(inv.module().namespace().is_module());
assert_eq!(inv.function(), "replace");
assert_eq!(inv.args(), ¶ms(&[("1", "xx"), ("2", "yy")]));
}
#[test]
fn invoke_variants_cover_localized_missing_function_and_invalid_module() {
let mut d1 = Dom::new("{{#вызвать:Тест|f}}");
let name = d1.text_at("#вызвать:Тест");
let p0 = d1.part_at(vec![name], None, "#вызвать:Тест");
let f = d1.text_at("|f");
let p1 = {
let (start, end) = at(&d1.text, "|f");
d1.part(vec![f], None, start + 1, end)
};
let end = d1.text.len() as u32;
let tpl = d1.template(vec![p0, p1], 0, end);
d1.root(tpl);
let (_, invocations) = d1.run(&cfg());
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].module().text(), "Модуль:Тест");
let mut d2 = Dom::new("{{#invoke:M}}");
let name = d2.tn(2, 11);
let p0 = d2.part(vec![name], None, 2, 11);
let tpl = d2.template(vec![p0], 0, 13);
d2.root(tpl);
let (_, invocations) = d2.run(&cfg());
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].function(), "");
assert!(invocations[0].args().is_empty());
let mut d3 = Dom::new("{{#invoke:[x|f}}");
let name = d3.tn(2, 12);
let p0 = d3.part(vec![name], None, 2, 12);
let f = d3.tn(13, 14);
let p1 = d3.part(vec![f], None, 13, 14);
let tpl = d3.template(vec![p0, p1], 0, 16);
d3.root(tpl);
let (calls, invocations) = d3.run(&cfg());
assert!(calls.is_empty());
assert!(invocations.is_empty());
let mut d4 = Dom::new("{{#invoke:N|g|k=v}}");
let name = d4.tn(2, 11);
let p0 = d4.part(vec![name], None, 2, 11);
let g = d4.tn(12, 13);
let p1 = d4.part(vec![g], None, 12, 13);
let k = d4.tn(14, 15);
let k_eq = d4.tn(15, 16);
let k_v = d4.tn(16, 17);
let p2 = d4.part(vec![k, k_eq, k_v], Some(1), 14, 17);
let tpl = d4.template(vec![p0, p1, p2], 0, 19);
d4.root(tpl);
let (_, invocations) = d4.run(&cfg());
assert_eq!(invocations[0].args(), ¶ms(&[("k", "v")]));
}
#[test]
fn invoke_falls_back_to_the_literal_english_name() {
let mut d = Dom::new("{{#INVOKE:M|f}}");
let name = d.tn(2, 11);
let p0 = d.part(vec![name], None, 2, 11);
let f = d.tn(12, 13);
let p1 = d.part(vec![f], None, 12, 13);
let tpl = d.template(vec![p0, p1], 0, 15);
d.root(tpl);
let (_, invocations) = d.run(&cfg_without_invoke());
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].module().text(), "Модуль:M");
let mut d2 = Dom::new("{{#вызвать:Т|f}}");
let name = d2.text_at("#вызвать:Т");
let p0 = d2.part_at(vec![name], None, "#вызвать:Т");
let end = d2.text.len() as u32;
let tpl = d2.template(vec![p0], 0, end);
d2.root(tpl);
let (calls, invocations) = d2.run(&cfg_without_invoke());
assert!(calls.is_empty());
assert!(invocations.is_empty());
}
#[test]
fn newline_in_the_name_is_not_a_call_but_nested_calls_survive() {
let mut d = Dom::new("{{fo\no|{{b}}}}");
let b_name = d.tn(9, 10);
let b_p0 = d.part(vec![b_name], None, 9, 10);
let inner = d.template(vec![b_p0], 7, 12);
let name = d.tn(2, 6);
let p0 = d.part(vec![name], None, 2, 6);
let p1 = d.part(vec![inner], None, 7, 12);
let outer = d.template(vec![p0, p1], 0, 14);
d.root(outer);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:B"]);
}
#[test]
fn invalid_title_is_skipped_but_nested_calls_survive() {
let mut d = Dom::new("{{a[b|{{c}}}}");
let c_name = d.tn(8, 9);
let c_p0 = d.part(vec![c_name], None, 8, 9);
let inner = d.template(vec![c_p0], 6, 11);
let name = d.tn(2, 5);
let p0 = d.part(vec![name], None, 2, 5);
let p1 = d.part(vec![inner], None, 6, 11);
let outer = d.template(vec![p0, p1], 0, 13);
d.root(outer);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:C"]);
}
#[test]
fn dynamic_names_are_skipped_but_walked() {
let mut d = Dom::new("{{{{a}}b}}");
let a_name = d.tn(4, 5);
let a_p0 = d.part(vec![a_name], None, 4, 5);
let inner = d.template(vec![a_p0], 2, 7);
let b = d.tn(7, 8);
let p0 = d.part(vec![inner, b], None, 2, 8);
let outer = d.template(vec![p0], 0, 10);
d.root(outer);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:A"]);
let mut d2 = Dom::new("{{x<ref>y</ref>}}");
let x = d2.tn(2, 3);
let tag = d2.node(
PpKind::ExtTag {
name: Span::new(d2.src, 4, 7),
attributes: None,
body: Some(Span::new(d2.src, 8, 9)),
closed: true,
},
3,
15,
);
let p0 = d2.part(vec![x, tag], None, 2, 15);
let tpl = d2.template(vec![p0], 0, 17);
d2.root(tpl);
let (calls, invocations) = d2.run(&cfg());
assert!(calls.is_empty());
assert!(invocations.is_empty());
}
#[test]
fn tplarg_parts_are_walked_but_not_recorded() {
let mut d = Dom::new("{{{p|{{d}}}}}");
let d_name = d.tn(7, 8);
let d_p0 = d.part(vec![d_name], None, 7, 8);
let inner = d.template(vec![d_p0], 5, 10);
let p_name = d.tn(3, 4);
let arg_p0 = d.part(vec![p_name], None, 3, 4);
let arg_p1 = d.part(vec![inner], None, 5, 10);
let arg = d.node(
PpKind::TplArg {
parts: vec![arg_p0, arg_p1],
},
0,
13,
);
d.root(arg);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:D"]);
}
#[test]
fn defensive_shapes_extract_nothing_or_degrade_gracefully() {
let mut d = Dom::new("xxxx");
let empty_tpl = d.template(vec![], 0, 4);
let text = d.tn(0, 4);
let comment = d.node(PpKind::Comment, 0, 4);
let ignored = d.node(PpKind::Ignored, 0, 4);
let tag = d.node(
PpKind::ExtTag {
name: Span::new(d.src, 0, 4),
attributes: None,
body: None,
closed: false,
},
0,
4,
);
for id in [empty_tpl, text, comment, ignored, tag] {
d.root(id);
}
let (calls, invocations) = d.run(&cfg());
assert!(calls.is_empty());
assert!(invocations.is_empty());
let mut d2 = Dom::new("{{|v}}");
let p0 = d2.part(vec![], None, 2, 2);
let v = d2.tn(3, 4);
let p1 = d2.part(vec![v], None, 3, 4);
let tpl = d2.template(vec![p0, p1], 0, 6);
d2.root(tpl);
let (calls, _) = d2.run(&cfg());
assert!(calls.is_empty());
let mut d3 = Dom::new("{{t|v}}");
let name = d3.tn(2, 3);
let p0 = d3.part(vec![name], None, 2, 3);
let v = d3.tn(4, 5);
let p1 = d3.part(vec![v], Some(7), 4, 5);
let tpl = d3.template(vec![p0, p1], 0, 7);
d3.root(tpl);
let (calls, _) = d3.run(&cfg());
assert_eq!(calls[0].params(), ¶ms(&[("1", "v")]));
let mut d4 = Dom::new("{{s|=v}}");
let name = d4.tn(2, 3);
let p0 = d4.part(vec![name], None, 2, 3);
let eq = d4.tn(4, 5);
let v = d4.tn(5, 6);
let p1 = d4.part(vec![eq, v], Some(0), 4, 6);
let tpl = d4.template(vec![p0, p1], 0, 8);
d4.root(tpl);
let (calls, _) = d4.run(&cfg());
assert_eq!(calls[0].params(), ¶ms(&[("", "v")]));
}
#[test]
fn malformed_eq_spans_are_clamped_into_the_part() {
let mut d = Dom::new("{{t|k=v}}");
let name = d.tn(2, 3);
let p0 = d.part(vec![name], None, 2, 3);
let k = d.tn(4, 5);
let bad_eq = d.tn(5, 99);
let v = d.tn(6, 7);
let p1 = d.part(vec![k, bad_eq, v], Some(1), 4, 7);
let tpl = d.template(vec![p0, p1], 0, 9);
d.root(tpl);
let (calls, _) = d.run(&cfg());
assert_eq!(calls[0].params(), ¶ms(&[("k", "")]));
let mut d2 = Dom::new("{{u|m=n}}");
let name = d2.tn(2, 3);
let p0 = d2.part(vec![name], None, 2, 3);
let m = d2.tn(4, 5);
let bad_eq = d2.tn(0, 0);
let n = d2.tn(6, 7);
let p1 = d2.part(vec![m, bad_eq, n], Some(1), 4, 7);
let tpl = d2.template(vec![p0, p1], 0, 9);
d2.root(tpl);
let (calls, _) = d2.run(&cfg());
assert_eq!(calls[0].params(), ¶ms(&[("m", "m=n")]));
}
#[test]
fn spans_are_reported_only_for_source_zero() {
let mut store = SourceStore::new();
let (_src0, _) = store.push("padding", 1 << 20);
let (src1, _) = store.push("{{q}}", 1 << 20);
let mut dom = PpDom::new();
let name = dom.push(PpNode {
kind: PpKind::Text,
span: Span::new(src1, 2, 3),
});
let p0 = Part {
children: vec![name],
eq: None,
span: Span::new(src1, 2, 3),
};
let tpl = dom.push(PpNode {
kind: PpKind::Template { parts: vec![p0] },
span: Span::new(src1, 0, 5),
});
dom.push_root(tpl);
let mut warnings = WarningCollector::new();
let (calls, _) = template_calls(&dom, &store, &cfg(), &mut warnings);
assert_eq!(calls[0].title().text(), "Шаблон:Q");
assert_eq!(calls[0].span(), None);
}
#[test]
fn dynamic_content_in_keys_is_skipped_and_still_walked() {
let mut d = Dom::new("{{t|a{{x}}b=v}}");
let x_name = d.tn(7, 8);
let x_p0 = d.part(vec![x_name], None, 7, 8);
let inner = d.template(vec![x_p0], 5, 10);
let name = d.tn(2, 3);
let p0 = d.part(vec![name], None, 2, 3);
let a = d.tn(4, 5);
let b = d.tn(10, 11);
let eq = d.tn(11, 12);
let v = d.tn(12, 13);
let p1 = d.part(vec![a, inner, b, eq, v], Some(3), 4, 13);
let tpl = d.template(vec![p0, p1], 0, 15);
d.root(tpl);
let (calls, _) = d.run(&cfg());
let titles: Vec<String> = calls.iter().map(|c| c.title().text()).collect();
assert_eq!(titles, ["Шаблон:T", "Шаблон:X"]);
assert_eq!(calls[0].params(), ¶ms(&[("ab", "v")]));
}
}