use super::Expander;
use crate::engine::expand::pf::strings::{self, UrlEncodeKind};
use crate::engine::expand::pf::{expr, time};
use crate::engine::pp::Part;
use crate::engine::source::Span;
use crate::engine::strip::StripPayload;
use crate::model::ModuleInvocation;
use crate::title::{Namespace, Title};
use crate::warn::Warning;
impl Expander<'_> {
pub(super) fn dispatch(
&mut self,
dom_idx: usize,
name: &str,
parts: &[Part],
span: Span,
frame: usize,
out: &mut String,
) {
let target = strip_subst(name);
if let Some(rest) = target.strip_prefix('#') {
self.parser_function(dom_idx, rest, parts, span, frame, out);
return;
}
if let Some((head, tail)) = target.split_once(':') {
let colon = &target[..head.len() + 1];
if let Some(id) = self
.config
.resolve_magic_word(colon)
.or_else(|| self.config.resolve_magic_word(head))
{
let id = id.to_owned();
self.colon_function(dom_idx, &id, tail.trim(), parts, frame, out);
return;
}
} else if parts.len() == 1 {
if let Some(id) = self.config.resolve_magic_word(target) {
let id = id.to_owned();
if let Some(value) = self.variable(&id, None) {
out.push_str(&value);
return;
}
return;
}
}
let Ok(title) = Title::parse_with_default(target, self.config, Namespace::TEMPLATE) else {
out.push_str(self.store.slice(span));
return;
};
self.transclude(title, dom_idx, parts, frame, out);
}
fn parser_function(
&mut self,
dom_idx: usize,
rest: &str,
parts: &[Part],
span: Span,
frame: usize,
out: &mut String,
) {
let (raw_id, arg0) = match rest.split_once(':') {
Some((id, arg0)) => (id.trim(), arg0.trim()),
None => (rest.trim(), ""),
};
let id = self
.config
.resolve_magic_word(raw_id)
.map(str::to_owned)
.unwrap_or_else(|| raw_id.to_ascii_lowercase());
match id.as_str() {
"if" => {
let branch = if arg0.is_empty() { 2 } else { 1 };
out.push_str(&self.arg(dom_idx, parts, branch, frame));
}
"ifeq" => {
let b = self.arg(dom_idx, parts, 1, frame);
let branch = if values_equal(arg0, &b) { 2 } else { 3 };
out.push_str(&self.arg(dom_idx, parts, branch, frame));
}
"ifexpr" => match expr::eval(arg0) {
Ok(value) => {
let branch = if is_truthy(&value) { 1 } else { 2 };
out.push_str(&self.arg(dom_idx, parts, branch, frame));
}
Err(err) => out.push_str(err.message()),
},
"iferror" => {
let branch = if arg0.contains("class=\"error\"") {
2
} else {
1
};
if branch == 1 && parts.len() < 3 {
out.push_str(arg0);
} else {
let idx = if branch == 2 { 1 } else { 2 };
out.push_str(&self.arg(dom_idx, parts, idx, frame));
}
}
"ifexist" => self.if_exist(dom_idx, arg0, parts, frame, out),
"switch" => self.switch(dom_idx, arg0, parts, frame, out),
"expr" => match expr::eval(arg0) {
Ok(value) => out.push_str(&value),
Err(err) => out.push_str(err.message()),
},
"time" | "timel" => self.time(dom_idx, arg0, parts, frame, out),
"tag" => self.tag(dom_idx, arg0, parts, frame, out),
"invoke" => self.invoke(dom_idx, arg0, parts, frame, out),
_ => {
self.warnings.emit(Warning::UnsupportedParserFunction {
name: format!("#{raw_id}"),
});
out.push_str(self.store.slice(span));
}
}
}
fn if_exist(
&mut self,
dom_idx: usize,
arg0: &str,
parts: &[Part],
frame: usize,
out: &mut String,
) {
let exists = if !self.charge_expensive() {
false
} else if let Ok(title) = Title::parse(arg0, self.config) {
match self
.ctx
.existence
.map(|p| p.lookup(std::slice::from_ref(&title)))
{
Some(Ok(map)) => map.get(&title).is_some_and(|r| r.exists),
_ => {
self.warnings.emit(Warning::ExistenceUnknown {
title: title.text(),
});
self.record_wanted(title);
false
}
}
} else {
false
};
let branch = if exists { 1 } else { 2 };
out.push_str(&self.arg(dom_idx, parts, branch, frame));
}
fn switch(
&mut self,
dom_idx: usize,
value: &str,
parts: &[Part],
frame: usize,
out: &mut String,
) {
let cases = &parts[1.min(parts.len())..];
let last = cases.len().saturating_sub(1);
let mut matched = false;
let mut default: Option<String> = None;
for (i, part) in cases.iter().enumerate() {
let (name, result) = self.switch_case(dom_idx, part, frame);
match name {
Some(name) => {
if matched || name == value {
out.push_str(&result);
return;
}
if name == "#default" {
default = Some(result);
}
}
None => {
if matched {
out.push_str(&result);
return;
}
if result == value {
matched = true;
} else if i == last {
default = Some(result);
}
}
}
}
if let Some(default) = default {
out.push_str(&default);
}
}
fn switch_case(
&mut self,
dom_idx: usize,
part: &Part,
frame: usize,
) -> (Option<String>, String) {
match part.eq {
Some(eq) if part.children.get(eq).is_some() => {
let name = self
.expand_nodes_owned(dom_idx, &part.children[..eq], frame)
.trim()
.to_owned();
let value = self
.expand_nodes_owned(dom_idx, &part.children[eq + 1..], frame)
.trim()
.to_owned();
(Some(name), value)
}
_ => (None, self.arg_part(dom_idx, part, frame)),
}
}
fn colon_function(
&mut self,
dom_idx: usize,
id: &str,
arg0: &str,
parts: &[Part],
frame: usize,
out: &mut String,
) {
match id {
"lc" => out.push_str(&strings::lc(arg0)),
"uc" => out.push_str(&strings::uc(arg0)),
"lcfirst" => out.push_str(&strings::lcfirst(arg0)),
"ucfirst" => out.push_str(&strings::ucfirst(arg0)),
"padleft" => {
let width = self
.arg(dom_idx, parts, 1, frame)
.parse::<usize>()
.unwrap_or(0);
let pad = self.arg_or(dom_idx, parts, 2, frame, "0");
out.push_str(&strings::padleft(arg0, width, &pad));
}
"padright" => {
let width = self
.arg(dom_idx, parts, 1, frame)
.parse::<usize>()
.unwrap_or(0);
let pad = self.arg_or(dom_idx, parts, 2, frame, "0");
out.push_str(&strings::padright(arg0, width, &pad));
}
"urlencode" => {
let kind = UrlEncodeKind::parse(&self.arg(dom_idx, parts, 1, frame));
out.push_str(&strings::urlencode(arg0, kind));
}
"anchorencode" => out.push_str(&strings::anchorencode(arg0)),
"ns" => {
if let Some(name) = self.namespace_name(arg0) {
out.push_str(&name);
}
}
"defaultsort" => self.default_sort = Some(arg0.to_owned()),
"displaytitle" => self.display_title = Some(arg0.to_owned()),
"formatnum" => out.push_str(arg0),
_ => {
if let Some(value) = self.variable(id, Some(arg0)) {
out.push_str(&value);
}
}
}
}
fn tag(&mut self, dom_idx: usize, name: &str, parts: &[Part], frame: usize, out: &mut String) {
if name.is_empty() {
return;
}
let body = parts
.get(1)
.map(|part| self.arg_part(dom_idx, part, frame))
.filter(|_| parts.len() > 1);
let mut attr = String::new();
for part in parts.iter().skip(2) {
let (key, value) = self.switch_case(dom_idx, part, frame);
if let Some(key) = key {
attr.push(' ');
attr.push_str(&key);
attr.push_str("=\"");
attr.push_str(&value);
attr.push('"');
}
}
let attributes = (!attr.is_empty()).then(|| {
let src = self.store.push_raw(attr);
Span::new(src, 0, self.store.text(src).len() as u32)
});
let body = body.map(|text| {
let src = self.store.push_raw(text);
Span::new(src, 0, self.store.text(src).len() as u32)
});
self.strips.add(
out,
StripPayload::ExtTag {
name: name.to_lowercase(),
attributes,
body,
},
);
}
fn invoke(
&mut self,
dom_idx: usize,
module_name: &str,
parts: &[Part],
frame: usize,
out: &mut String,
) {
let _ = out;
let Ok(module) = Title::parse_with_default(module_name, self.config, Namespace::MODULE)
else {
return;
};
self.record_used(module.clone());
self.warnings.emit(Warning::UnsupportedModuleInvocation {
module: module.text(),
});
let function = self.arg(dom_idx, parts, 1, frame);
let mut args = std::collections::BTreeMap::new();
let mut positional = 0_usize;
for part in parts.iter().skip(2) {
let (name, value) = self.switch_case(dom_idx, part, frame);
match name {
Some(name) => {
args.insert(name, value);
}
None => {
positional += 1;
args.insert(positional.to_string(), value);
}
}
}
self.modules
.push(ModuleInvocation::new(module, function, args));
}
fn time(
&mut self,
dom_idx: usize,
format: &str,
parts: &[Part],
frame: usize,
out: &mut String,
) {
let spec = self.arg(dom_idx, parts, 1, frame);
let unix = parse_time_spec(&spec, self.ctx.now.unix());
let result = time::format_time(format, unix);
if result.unsupported {
self.warnings.emit(Warning::UnsupportedTimeFormat);
}
out.push_str(&result.text);
}
fn variable(&self, id: &str, arg: Option<&str>) -> Option<String> {
let page = match arg {
Some(arg) => Title::parse(arg, self.config).ok()?,
None => self.ctx.title.clone(),
};
let pagename = page.dbkey().replace('_', " ");
let value = match id {
"!" => "|".to_owned(),
"=" => "=".to_owned(),
"sitename" => self.config.site_name().to_owned(),
"contentlanguage" | "pagelanguage" => self.config.lang().to_owned(),
"pagename" => pagename,
"pagenamee" => page.dbkey().to_owned(),
"fullpagename" => page.text(),
"namespace" => self
.config
.namespace_name(page.namespace())
.unwrap_or("")
.to_owned(),
"namespacenumber" => page.namespace().id().to_string(),
"basepagename" => base_pagename(&pagename),
"subpagename" => sub_pagename(&pagename),
"rootpagename" => root_pagename(&pagename),
"currentyear" => self.current("Y"),
"currentmonth" | "currentmonth2" => self.current("m"),
"currentmonth1" => self.current("n"),
"currentmonthname" => self.current("F"),
"currentmonthabbrev" => self.current("M"),
"currentday" => self.current("j"),
"currentday2" => self.current("d"),
"currentdow" => self.current("w"),
"currentdayname" => self.current("l"),
"currenttime" => self.current("H:i"),
"currenthour" => self.current("H"),
"currenttimestamp" => self.current("YmdHis"),
"currentweek" => self.current("W"),
_ => return None,
};
Some(value)
}
fn current(&self, format: &str) -> String {
time::format_time(format, self.ctx.now.unix()).text
}
fn namespace_name(&self, arg: &str) -> Option<String> {
let ns = match arg.trim().parse::<i32>() {
Ok(id) => Namespace::new(id),
Err(_) => self.config.resolve_namespace(arg.trim())?,
};
self.config.namespace_name(ns).map(str::to_owned)
}
fn arg(&mut self, dom_idx: usize, parts: &[Part], index: usize, frame: usize) -> String {
match parts.get(index) {
Some(part) => self.arg_part(dom_idx, part, frame),
None => String::new(),
}
}
fn arg_or(
&mut self,
dom_idx: usize,
parts: &[Part],
index: usize,
frame: usize,
fallback: &str,
) -> String {
match parts.get(index) {
Some(part) => self.arg_part(dom_idx, part, frame),
None => fallback.to_owned(),
}
}
fn arg_part(&mut self, dom_idx: usize, part: &Part, frame: usize) -> String {
self.expand_part_all(dom_idx, part, frame).trim().to_owned()
}
}
fn strip_subst(name: &str) -> &str {
for prefix in ["safesubst:", "subst:"] {
if let Some(head) = name.get(..prefix.len()) {
if head.eq_ignore_ascii_case(prefix) {
return name[prefix.len()..].trim_start();
}
}
}
name
}
fn values_equal(a: &str, b: &str) -> bool {
match (a.trim().parse::<f64>(), b.trim().parse::<f64>()) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
fn is_truthy(value: &str) -> bool {
match value.trim().parse::<f64>() {
Ok(n) => n != 0.0,
Err(_) => !value.trim().is_empty(),
}
}
fn parse_time_spec(spec: &str, now: i64) -> i64 {
let spec = spec.trim();
if spec.is_empty() {
return now;
}
if let Some(seconds) = spec.strip_prefix('@') {
if let Ok(value) = seconds.trim().parse::<i64>() {
return value;
}
}
now
}
fn base_pagename(name: &str) -> String {
match name.rfind('/') {
Some(slash) => name[..slash].to_owned(),
None => name.to_owned(),
}
}
fn sub_pagename(name: &str) -> String {
match name.rfind('/') {
Some(slash) => name[slash + 1..].to_owned(),
None => name.to_owned(),
}
}
fn root_pagename(name: &str) -> String {
match name.find('/') {
Some(slash) => name[..slash].to_owned(),
None => name.to_owned(),
}
}