use crate::model::{
CategoryLink, ExternalLink, InterLanguageLink, InternalLink, ModuleInvocation, Redirect,
Section, StructuralStats, TemplateCall,
};
use crate::source::ExistenceMap;
use crate::title::Title;
use crate::warn::Warning;
pub trait DocumentView {
fn templates(&self) -> &[TemplateCall];
fn links(&self) -> &[InternalLink];
fn categories(&self) -> &[CategoryLink];
fn external_links(&self) -> &[ExternalLink];
fn warnings(&self) -> &[Warning];
fn stats(&self) -> &StructuralStats;
fn plain_text(&self) -> &str;
fn redirect(&self) -> Option<&Redirect>;
fn source(&self) -> &str;
}
#[derive(Debug)]
pub struct ParsedDocument {
source: String,
templates: Vec<TemplateCall>,
modules: Vec<ModuleInvocation>,
links: Vec<InternalLink>,
categories: Vec<CategoryLink>,
interlanguage: Vec<InterLanguageLink>,
external_links: Vec<ExternalLink>,
warnings: Vec<Warning>,
stats: StructuralStats,
plain_text: String,
redirect: Option<Redirect>,
sections: Vec<Section>,
}
pub(crate) struct DocumentParts {
pub(crate) templates: Vec<TemplateCall>,
pub(crate) modules: Vec<ModuleInvocation>,
pub(crate) links: Vec<InternalLink>,
pub(crate) categories: Vec<CategoryLink>,
pub(crate) interlanguage: Vec<InterLanguageLink>,
pub(crate) external_links: Vec<ExternalLink>,
pub(crate) warnings: Vec<Warning>,
pub(crate) stats: StructuralStats,
pub(crate) plain_text: String,
pub(crate) redirect: Option<Redirect>,
pub(crate) sections: Vec<Section>,
}
impl ParsedDocument {
pub(crate) fn from_parts(source: String, parts: DocumentParts) -> Self {
Self {
source,
templates: parts.templates,
modules: parts.modules,
links: parts.links,
categories: parts.categories,
interlanguage: parts.interlanguage,
external_links: parts.external_links,
warnings: parts.warnings,
stats: parts.stats,
plain_text: parts.plain_text,
redirect: parts.redirect,
sections: parts.sections,
}
}
#[must_use]
pub fn modules(&self) -> &[ModuleInvocation] {
&self.modules
}
#[must_use]
pub fn sections(&self) -> &[Section] {
&self.sections
}
#[must_use]
pub fn interlanguage_links(&self) -> &[InterLanguageLink] {
&self.interlanguage
}
}
impl DocumentView for ParsedDocument {
fn templates(&self) -> &[TemplateCall] {
&self.templates
}
fn links(&self) -> &[InternalLink] {
&self.links
}
fn categories(&self) -> &[CategoryLink] {
&self.categories
}
fn external_links(&self) -> &[ExternalLink] {
&self.external_links
}
fn warnings(&self) -> &[Warning] {
&self.warnings
}
fn stats(&self) -> &StructuralStats {
&self.stats
}
fn plain_text(&self) -> &str {
&self.plain_text
}
fn redirect(&self) -> Option<&Redirect> {
self.redirect.as_ref()
}
fn source(&self) -> &str {
&self.source
}
}
#[derive(Debug)]
pub struct ExpandedDocument {
source: String,
templates: Vec<TemplateCall>,
closure: Vec<Title>,
modules: Vec<ModuleInvocation>,
links: Vec<InternalLink>,
categories: Vec<CategoryLink>,
interlanguage: Vec<InterLanguageLink>,
external_links: Vec<ExternalLink>,
warnings: Vec<Warning>,
stats: StructuralStats,
plain_text: String,
redirect: Option<Redirect>,
default_sort: Option<String>,
display_title: Option<String>,
wanted_existence: Vec<Title>,
}
pub(crate) struct ExpandedParts {
pub(crate) source: String,
pub(crate) templates: Vec<TemplateCall>,
pub(crate) closure: Vec<Title>,
pub(crate) modules: Vec<ModuleInvocation>,
pub(crate) links: Vec<InternalLink>,
pub(crate) categories: Vec<CategoryLink>,
pub(crate) interlanguage: Vec<InterLanguageLink>,
pub(crate) external_links: Vec<ExternalLink>,
pub(crate) warnings: Vec<Warning>,
pub(crate) stats: StructuralStats,
pub(crate) plain_text: String,
pub(crate) redirect: Option<Redirect>,
pub(crate) default_sort: Option<String>,
pub(crate) display_title: Option<String>,
pub(crate) wanted_existence: Vec<Title>,
}
impl ExpandedDocument {
pub(crate) fn from_parts(parts: ExpandedParts) -> Self {
Self {
source: parts.source,
templates: parts.templates,
closure: parts.closure,
modules: parts.modules,
links: parts.links,
categories: parts.categories,
interlanguage: parts.interlanguage,
external_links: parts.external_links,
warnings: parts.warnings,
stats: parts.stats,
plain_text: parts.plain_text,
redirect: parts.redirect,
default_sort: parts.default_sort,
display_title: parts.display_title,
wanted_existence: parts.wanted_existence,
}
}
#[must_use]
pub fn transclusion_closure(&self) -> &[Title] {
&self.closure
}
#[must_use]
pub fn modules(&self) -> &[ModuleInvocation] {
&self.modules
}
#[must_use]
pub fn interlanguage_links(&self) -> &[InterLanguageLink] {
&self.interlanguage
}
#[must_use]
pub fn template_calls(&self) -> &[TemplateCall] {
&self.templates
}
#[must_use]
pub fn default_sort(&self) -> Option<&str> {
self.default_sort.as_deref()
}
#[must_use]
pub fn display_title(&self) -> Option<&str> {
self.display_title.as_deref()
}
#[must_use]
pub fn titles_needing_existence(&self) -> &[Title] {
&self.wanted_existence
}
pub fn apply_existence(&mut self, facts: &ExistenceMap) {
for link in &mut self.links {
if let Some(record) = facts.get(link.target()) {
link.set_exists(record.exists);
}
}
for category in &mut self.categories {
if let Some(record) = facts.get(category.category()) {
category.set_facts(!record.exists, record.hidden_category);
}
}
}
}
impl DocumentView for ExpandedDocument {
fn templates(&self) -> &[TemplateCall] {
&self.templates
}
fn links(&self) -> &[InternalLink] {
&self.links
}
fn categories(&self) -> &[CategoryLink] {
&self.categories
}
fn external_links(&self) -> &[ExternalLink] {
&self.external_links
}
fn warnings(&self) -> &[Warning] {
&self.warnings
}
fn stats(&self) -> &StructuralStats {
&self.stats
}
fn plain_text(&self) -> &str {
&self.plain_text
}
fn redirect(&self) -> Option<&Redirect> {
self.redirect.as_ref()
}
fn source(&self) -> &str {
&self.source
}
}
#[derive(Debug)]
pub enum ExpandOutcome {
Complete(Box<ExpandedDocument>),
MissingTemplates(MissingTemplates),
}
#[derive(Debug, Clone)]
pub struct MissingTemplates {
titles: Vec<Title>,
}
impl MissingTemplates {
pub(crate) fn new(titles: Vec<Title>) -> Self {
Self { titles }
}
#[must_use]
pub fn titles(&self) -> &[Title] {
&self.titles
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::{DocumentView, ExpandOutcome};
use crate::config::{ExtTagContent, MagicWordDef, NamespaceDef, SiteConfig};
use crate::engine::expand::UnresolvedPolicy;
use crate::parser::{PageContext, WikitextParser};
use crate::source::{ExistenceMap, ExistenceRecord, InMemoryTemplateStore};
use crate::title::Title;
fn config() -> SiteConfig {
SiteConfig::builder("en", "Test Wiki")
.namespace(NamespaceDef::main())
.namespace(NamespaceDef::new(10, "Template", "Template", ["T"], true))
.namespace(NamespaceDef::new(
14,
"Category",
"Category",
Vec::<String>::new(),
true,
))
.namespace(NamespaceDef::new(
828,
"Module",
"Module",
Vec::<String>::new(),
true,
))
.magic_word(MagicWordDef::new("defaultsort", ["DEFAULTSORT:"], true))
.magic_word(MagicWordDef::new("displaytitle", ["DISPLAYTITLE:"], false))
.magic_word(MagicWordDef::new("ifexist", ["ifexist"], false))
.magic_word(MagicWordDef::new("invoke", ["invoke"], false))
.interwiki(["fr"])
.ext_tag("ref", ExtTagContent::InlineWikitext)
.ext_tag("nowiki", ExtTagContent::Opaque)
.build()
.unwrap()
}
fn to_result(
outcome: ExpandOutcome,
) -> Result<Box<super::ExpandedDocument>, super::MissingTemplates> {
match outcome {
ExpandOutcome::Complete(doc) => Ok(doc),
ExpandOutcome::MissingTemplates(missing) => Err(missing),
}
}
fn expanded(page: &str) -> Box<super::ExpandedDocument> {
let store = InMemoryTemplateStore::new();
store.insert(Title::parse("Template:Tmpl", &config()).unwrap(), "T");
let parser = WikitextParser::builder()
.site_config(config())
.template_source(Arc::new(store))
.build()
.unwrap();
to_result(parser.expand(page, &PageContext::default())).expect("expected a complete doc")
}
#[test]
fn expanded_document_exposes_every_accessor() {
let doc = expanded(concat!(
"{{DEFAULTSORT:Sortkey}}{{DISPLAYTITLE:Shown}}",
"[[Category:Cats]] [[Link]] [[fr:Chat]] {{Tmpl|a=b}} ",
"{{#invoke:M|f}} {{#ifexist:Wanted|yes|no}} <ref>[[Cited]]</ref>",
" <nowiki>[[NotALink]]</nowiki>"
));
assert_eq!(doc.default_sort(), Some("Sortkey"));
assert_eq!(doc.display_title(), Some("Shown"));
assert!(
doc.template_calls()
.iter()
.any(|c| c.title().text() == "Template:Tmpl")
);
assert!(
doc.modules()
.iter()
.any(|m| m.module().text() == "Module:M")
);
assert!(doc.interlanguage_links().iter().any(|l| l.prefix() == "fr"));
assert_eq!(
doc.titles_needing_existence()
.iter()
.map(Title::text)
.collect::<Vec<_>>(),
["Wanted"]
);
assert!(
doc.transclusion_closure()
.iter()
.any(|t| t.text() == "Template:Tmpl")
);
let view: &dyn DocumentView = doc.as_ref();
assert!(!view.templates().is_empty());
assert!(view.links().iter().any(|l| l.target().text() == "Link"));
assert!(
view.categories()
.iter()
.any(|c| c.category().text() == "Category:Cats")
);
let _ = view.external_links();
let _ = view.warnings();
let _ = view.stats();
assert!(view.plain_text().contains("Cited") || !view.plain_text().is_empty());
assert!(view.redirect().is_none());
assert!(view.source().contains("DEFAULTSORT"));
assert!(view.links().iter().any(|l| l.target().text() == "Cited"));
}
#[test]
fn apply_existence_enriches_links_and_categories() {
let mut doc = expanded("[[Link]] [[Category:Cats]]");
let facts: ExistenceMap = [
(
Title::parse("Link", &config()).unwrap(),
ExistenceRecord {
exists: true,
hidden_category: None,
},
),
(
Title::parse("Category:Cats", &config()).unwrap(),
ExistenceRecord {
exists: true,
hidden_category: Some(true),
},
),
]
.into_iter()
.collect();
doc.apply_existence(&facts);
assert!(doc.links().iter().any(|l| l.target().text() == "Link"));
assert!(format!("{doc:?}").contains("ExpandedDocument"));
}
#[test]
fn halt_policy_reports_missing_templates() {
let parser = WikitextParser::builder()
.site_config(config())
.on_unresolved(UnresolvedPolicy::Halt)
.build()
.unwrap();
let title = Title::parse("Page", &config()).unwrap();
let missing = to_result(parser.expand("{{Needed|x}}", &PageContext::for_title(title)))
.expect_err("expected a halt");
let mt = missing.clone();
assert_eq!(
mt.titles().iter().map(Title::text).collect::<Vec<_>>(),
["Template:Needed"]
);
assert!(format!("{mt:?}").contains("MissingTemplates"));
}
}