use std::fmt;
use std::sync::Arc;
use crate::clock::{Clock, SystemClock, Timestamp};
use crate::config::{Limits, SiteConfig};
use crate::doc::{ExpandOutcome, ParsedDocument};
use crate::engine::expand::UnresolvedPolicy;
use crate::error::BuildError;
use crate::source::{EmptyTemplateSource, ExistenceProvider, TemplateSource};
use crate::title::Title;
#[derive(Clone)]
pub struct WikitextParser {
inner: Arc<Inner>,
}
struct Inner {
config: SiteConfig,
limits: Limits,
clock: Arc<dyn Clock>,
source: Arc<dyn TemplateSource>,
existence: Option<Arc<dyn ExistenceProvider>>,
policy: UnresolvedPolicy,
}
impl fmt::Debug for WikitextParser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WikitextParser")
.field("lang", &self.inner.config.lang())
.field("limits", &self.inner.limits)
.finish_non_exhaustive()
}
}
impl WikitextParser {
#[must_use]
pub fn builder() -> WikitextParserBuilder {
WikitextParserBuilder {
config: None,
limits: Limits::default(),
clock: Arc::new(SystemClock),
source: Arc::new(EmptyTemplateSource),
existence: None,
policy: UnresolvedPolicy::RedLink,
}
}
#[must_use]
pub fn site_config(&self) -> &SiteConfig {
&self.inner.config
}
#[must_use]
pub fn parse(&self, wikitext: &str, ctx: &PageContext) -> ParsedDocument {
let now = ctx.now.unwrap_or_else(|| self.inner.clock.now());
crate::engine::pipeline::parse_only(
&self.inner.config,
&self.inner.limits,
wikitext,
ctx.title.as_ref(),
now,
)
}
#[must_use]
pub fn expand(&self, wikitext: &str, ctx: &PageContext) -> ExpandOutcome {
let now = ctx.now.unwrap_or_else(|| self.inner.clock.now());
let title = ctx
.title
.clone()
.unwrap_or_else(|| Title::synthetic_main("API"));
crate::engine::pipeline::expand_page(
&self.inner.config,
&self.inner.limits,
wikitext,
&title,
now,
self.inner.source.as_ref(),
self.inner.existence.as_deref(),
self.inner.policy,
)
}
}
pub struct WikitextParserBuilder {
config: Option<SiteConfig>,
limits: Limits,
clock: Arc<dyn Clock>,
source: Arc<dyn TemplateSource>,
existence: Option<Arc<dyn ExistenceProvider>>,
policy: UnresolvedPolicy,
}
impl fmt::Debug for WikitextParserBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WikitextParserBuilder")
.field("config", &self.config.as_ref().map(SiteConfig::lang))
.field("limits", &self.limits)
.finish_non_exhaustive()
}
}
impl WikitextParserBuilder {
#[must_use]
pub fn site_config(mut self, config: SiteConfig) -> Self {
self.config = Some(config);
self
}
#[must_use]
pub fn limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
self.clock = clock;
self
}
#[must_use]
pub fn template_source(mut self, source: Arc<dyn TemplateSource>) -> Self {
self.source = source;
self
}
#[must_use]
pub fn existence_provider(mut self, provider: Arc<dyn ExistenceProvider>) -> Self {
self.existence = Some(provider);
self
}
#[must_use]
pub fn on_unresolved(mut self, policy: UnresolvedPolicy) -> Self {
self.policy = policy;
self
}
pub fn build(self) -> Result<WikitextParser, BuildError> {
self.limits.validate()?;
let config = match self.config {
Some(config) => config,
#[cfg(feature = "presets")]
None => SiteConfig::generic().clone(),
#[cfg(not(feature = "presets"))]
None => return Err(BuildError::MissingSiteConfig),
};
Ok(WikitextParser {
inner: Arc::new(Inner {
config,
limits: self.limits,
clock: self.clock,
source: self.source,
existence: self.existence,
policy: self.policy,
}),
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::clock::FixedClock;
use crate::config::NamespaceDef;
use crate::doc::DocumentView;
fn tiny_config() -> SiteConfig {
SiteConfig::builder("en", "Wiki")
.namespace(NamespaceDef::main())
.build()
.unwrap()
}
#[test]
fn builder_wires_config_limits_and_clock() {
let parser = WikitextParser::builder()
.site_config(tiny_config())
.limits(Limits::default().with_expansion_depth(40))
.clock(Arc::new(FixedClock::new(Timestamp::from_unix(42))))
.build()
.unwrap();
assert_eq!(parser.site_config().lang(), "en");
assert_eq!(parser.inner.limits.expansion_depth(), 40);
let clone = parser.clone();
assert!(format!("{clone:?}").contains("WikitextParser"));
}
#[test]
fn invalid_limits_fail_the_build() {
let err = WikitextParser::builder()
.site_config(tiny_config())
.limits(Limits::default().with_expansion_depth(0))
.build()
.unwrap_err();
assert!(matches!(err, crate::error::BuildError::InvalidLimits(_)));
}
#[cfg(feature = "presets")]
#[test]
fn default_config_is_the_generic_preset() {
let parser = WikitextParser::builder().build().unwrap();
assert_eq!(parser.site_config().lang(), "en");
let doc = parser.parse("[[Page]]", &PageContext::default());
assert_eq!(doc.links().len(), 1);
}
#[cfg(not(feature = "presets"))]
#[test]
fn missing_config_without_presets_is_an_error() {
let err = WikitextParser::builder().build().unwrap_err();
assert!(matches!(err, crate::error::BuildError::MissingSiteConfig));
}
#[test]
fn page_context_carries_title_and_frozen_now() {
let config = tiny_config();
let title = crate::title::Title::parse("Page", &config).unwrap();
let ctx = PageContext::for_title(title).at(Timestamp::from_unix(7));
assert!(format!("{ctx:?}").contains("PageContext"));
let parser = WikitextParser::builder()
.site_config(config)
.build()
.unwrap();
let doc = parser.parse("text", &ctx);
assert_eq!(doc.plain_text(), "text");
let builder_debug = format!("{:?}", WikitextParser::builder());
assert!(builder_debug.contains("WikitextParserBuilder"));
}
}
#[derive(Debug, Clone, Default)]
pub struct PageContext {
title: Option<Title>,
now: Option<Timestamp>,
}
impl PageContext {
#[must_use]
pub fn for_title(title: Title) -> Self {
Self {
title: Some(title),
now: None,
}
}
#[must_use]
pub fn at(mut self, now: Timestamp) -> Self {
self.now = Some(now);
self
}
}