use std::collections::BTreeMap;
use crate::title::Title;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Span {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Origin {
Input,
Transclusion(Title),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct TemplateCall {
title: Title,
name_raw: String,
params: BTreeMap<String, String>,
origin: Origin,
span: Option<Span>,
resolved: bool,
}
impl TemplateCall {
pub(crate) fn new(
title: Title,
name_raw: String,
params: BTreeMap<String, String>,
origin: Origin,
span: Option<Span>,
resolved: bool,
) -> Self {
Self {
title,
name_raw,
params,
origin,
span,
resolved,
}
}
#[must_use]
pub fn title(&self) -> &Title {
&self.title
}
#[must_use]
pub fn name_raw(&self) -> &str {
&self.name_raw
}
#[must_use]
pub fn params(&self) -> &BTreeMap<String, String> {
&self.params
}
#[must_use]
pub fn param(&self, key: &str) -> Option<&str> {
self.params.get(key).map(String::as_str)
}
#[must_use]
pub fn origin(&self) -> &Origin {
&self.origin
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.span
}
#[must_use]
pub fn resolved(&self) -> bool {
self.resolved
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct ModuleInvocation {
module: Title,
function: String,
args: BTreeMap<String, String>,
}
impl ModuleInvocation {
pub(crate) fn new(module: Title, function: String, args: BTreeMap<String, String>) -> Self {
Self {
module,
function,
args,
}
}
#[must_use]
pub fn module(&self) -> &Title {
&self.module
}
#[must_use]
pub fn function(&self) -> &str {
&self.function
}
#[must_use]
pub fn args(&self) -> &BTreeMap<String, String> {
&self.args
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct InternalLink {
target: Title,
fragment: Option<String>,
anchor_text: Option<String>,
exists: Option<bool>,
span: Option<Span>,
}
impl InternalLink {
pub(crate) fn new(
target: Title,
fragment: Option<String>,
anchor_text: Option<String>,
span: Option<Span>,
) -> Self {
Self {
target,
fragment,
anchor_text,
exists: None,
span,
}
}
#[must_use]
pub fn target(&self) -> &Title {
&self.target
}
#[must_use]
pub fn fragment(&self) -> Option<&str> {
self.fragment.as_deref()
}
#[must_use]
pub fn anchor_text(&self) -> Option<&str> {
self.anchor_text.as_deref()
}
#[must_use]
pub fn exists(&self) -> Option<bool> {
self.exists
}
#[allow(dead_code, reason = "consumed by the existence-enrichment stage")]
pub(crate) fn set_exists(&mut self, exists: bool) {
self.exists = Some(exists);
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.span
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct CategoryLink {
category: Title,
sortkey: String,
hidden: Option<bool>,
missing: Option<bool>,
span: Option<Span>,
}
impl CategoryLink {
pub(crate) fn new(category: Title, sortkey: String, span: Option<Span>) -> Self {
Self {
category,
sortkey,
hidden: None,
missing: None,
span,
}
}
#[must_use]
pub fn category(&self) -> &Title {
&self.category
}
#[must_use]
pub fn sortkey(&self) -> &str {
&self.sortkey
}
#[must_use]
pub fn hidden(&self) -> Option<bool> {
self.hidden
}
#[must_use]
pub fn missing(&self) -> Option<bool> {
self.missing
}
#[allow(dead_code, reason = "consumed by the existence-enrichment stage")]
pub(crate) fn set_facts(&mut self, missing: bool, hidden: Option<bool>) {
self.missing = Some(missing);
self.hidden = hidden;
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.span
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct ExternalLink {
url: String,
display: Option<String>,
span: Option<Span>,
}
impl ExternalLink {
pub(crate) fn new(url: String, display: Option<String>, span: Option<Span>) -> Self {
Self { url, display, span }
}
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
pub fn display(&self) -> Option<&str> {
self.display.as_deref()
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.span
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct InterLanguageLink {
prefix: String,
target: String,
}
impl InterLanguageLink {
pub(crate) fn new(prefix: String, target: String) -> Self {
Self { prefix, target }
}
#[must_use]
pub fn prefix(&self) -> &str {
&self.prefix
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Redirect {
target: Title,
fragment: Option<String>,
}
impl Redirect {
pub(crate) fn new(target: Title, fragment: Option<String>) -> Self {
Self { target, fragment }
}
#[must_use]
pub fn target(&self) -> &Title {
&self.target
}
#[must_use]
pub fn fragment(&self) -> Option<&str> {
self.fragment.as_deref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct StructuralStats {
pub(crate) tables: u32,
pub(crate) table_rows: u32,
pub(crate) table_cells: u32,
pub(crate) headings: u32,
pub(crate) refs: u32,
pub(crate) list_items: u32,
pub(crate) images: u32,
pub(crate) internal_links: u32,
pub(crate) external_links: u32,
pub(crate) parser_errors: u32,
}
impl StructuralStats {
#[must_use]
pub fn tables(&self) -> u32 {
self.tables
}
#[must_use]
pub fn table_rows(&self) -> u32 {
self.table_rows
}
#[must_use]
pub fn table_cells(&self) -> u32 {
self.table_cells
}
#[must_use]
pub fn headings(&self) -> u32 {
self.headings
}
#[must_use]
pub fn refs(&self) -> u32 {
self.refs
}
#[must_use]
pub fn list_items(&self) -> u32 {
self.list_items
}
#[must_use]
pub fn images(&self) -> u32 {
self.images
}
#[must_use]
pub fn internal_links(&self) -> u32 {
self.internal_links
}
#[must_use]
pub fn external_links(&self) -> u32 {
self.external_links
}
#[must_use]
pub fn parser_errors(&self) -> u32 {
self.parser_errors
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct Section {
level: u8,
heading_span: Span,
title_span: Span,
body_span: Span,
title_text: String,
children: Vec<Section>,
}
impl Section {
pub(crate) fn new(
level: u8,
heading_span: Span,
title_span: Span,
body_span: Span,
title_text: String,
children: Vec<Section>,
) -> Self {
Self {
level,
heading_span,
title_span,
body_span,
title_text,
children,
}
}
#[must_use]
pub fn level(&self) -> u8 {
self.level
}
#[must_use]
pub fn heading_span(&self) -> Span {
self.heading_span
}
#[must_use]
pub fn title_span(&self) -> Span {
self.title_span
}
#[must_use]
pub fn body_span(&self) -> Span {
self.body_span
}
#[must_use]
pub fn title_text(&self) -> &str {
&self.title_text
}
#[must_use]
pub fn children(&self) -> &[Section] {
&self.children
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{NamespaceDef, SiteConfig};
fn config() -> 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,
))
.build()
.unwrap()
}
fn title(text: &str) -> Title {
Title::parse(text, &config()).unwrap()
}
#[test]
fn template_call_exposes_all_facets() {
let mut params = BTreeMap::new();
params.insert("1".to_owned(), "позиционный".to_owned());
params.insert("имя".to_owned(), "значение".to_owned());
let call = TemplateCall::new(
title("Шаблон:Карточка"),
"карточка".to_owned(),
params,
Origin::Input,
Some(Span { start: 4, end: 42 }),
true,
);
assert_eq!(call.title().text(), "Шаблон:Карточка");
assert_eq!(call.name_raw(), "карточка");
assert_eq!(call.param("имя"), Some("значение"));
assert_eq!(call.param("нет"), None);
assert_eq!(call.params().len(), 2);
assert_eq!(call.origin(), &Origin::Input);
assert_eq!(call.span(), Some(Span { start: 4, end: 42 }));
assert!(call.resolved());
assert_eq!(call.clone(), call);
}
#[test]
fn transclusion_origin_names_the_source_page() {
let origin = Origin::Transclusion(title("Шаблон:Родитель"));
let call = TemplateCall::new(
title("Шаблон:Вложенный"),
"Вложенный".to_owned(),
BTreeMap::new(),
origin.clone(),
None,
false,
);
assert_eq!(call.origin(), &origin);
assert_eq!(call.span(), None);
assert!(!call.resolved());
assert_ne!(origin, Origin::Input);
}
#[test]
fn module_invocation_accessors() {
let mut args = BTreeMap::new();
args.insert("1".to_owned(), "x".to_owned());
let invoke = ModuleInvocation::new(title("Модуль:Math"), "round".to_owned(), args);
assert_eq!(invoke.module().namespace().id(), 828);
assert_eq!(invoke.function(), "round");
assert_eq!(invoke.args().get("1").map(String::as_str), Some("x"));
assert_eq!(invoke.clone(), invoke);
}
#[test]
fn internal_link_enriches_existence() {
let mut link = InternalLink::new(
title("Страница"),
Some("Раздел".to_owned()),
Some("метка".to_owned()),
Some(Span { start: 0, end: 20 }),
);
assert_eq!(link.target().text(), "Страница");
assert_eq!(link.fragment(), Some("Раздел"));
assert_eq!(link.anchor_text(), Some("метка"));
assert_eq!(link.exists(), None);
link.set_exists(true);
assert_eq!(link.exists(), Some(true));
assert_eq!(link.span(), Some(Span { start: 0, end: 20 }));
}
#[test]
fn category_link_enriches_facts() {
let mut cat = CategoryLink::new(title("Категория:Люди"), String::new(), None);
assert_eq!(cat.category().namespace().id(), 14);
assert_eq!(cat.sortkey(), "");
assert_eq!((cat.hidden(), cat.missing()), (None, None));
cat.set_facts(false, Some(true));
assert_eq!((cat.hidden(), cat.missing()), (Some(true), Some(false)));
assert_eq!(cat.span(), None);
let keyed = CategoryLink::new(title("Категория:Люди"), "Ключ".to_owned(), None);
assert_eq!(keyed.sortkey(), "Ключ");
}
#[test]
fn interlanguage_link_accessors() {
let link = InterLanguageLink::new("fr".to_owned(), "Chat".to_owned());
assert_eq!(link.prefix(), "fr");
assert_eq!(link.target(), "Chat");
assert_eq!(link.clone(), link);
assert!(format!("{link:?}").contains("InterLanguageLink"));
}
#[test]
fn external_link_and_redirect_accessors() {
let ext = ExternalLink::new(
"https://example.com/x".to_owned(),
Some("пример".to_owned()),
Some(Span { start: 1, end: 30 }),
);
assert_eq!(ext.url(), "https://example.com/x");
assert_eq!(ext.display(), Some("пример"));
assert_eq!(ext.span().map(|s| s.end), Some(30));
let redirect = Redirect::new(title("Цель"), Some("Якорь".to_owned()));
assert_eq!(redirect.target().text(), "Цель");
assert_eq!(redirect.fragment(), Some("Якорь"));
assert_eq!(redirect.clone(), redirect);
}
#[test]
fn stats_default_to_zero_and_expose_every_counter() {
let mut stats = StructuralStats::default();
assert_eq!(stats.tables(), 0);
stats.tables = 2;
stats.table_rows = 10;
stats.table_cells = 40;
stats.headings = 5;
stats.refs = 7;
stats.list_items = 12;
stats.images = 3;
stats.internal_links = 90;
stats.external_links = 4;
stats.parser_errors = 1;
assert_eq!(
(
stats.tables(),
stats.table_rows(),
stats.table_cells(),
stats.headings(),
stats.refs(),
stats.list_items(),
stats.images(),
stats.internal_links(),
stats.external_links(),
stats.parser_errors(),
),
(2, 10, 40, 5, 7, 12, 3, 90, 4, 1)
);
assert_ne!(stats, StructuralStats::default());
}
#[cfg(feature = "serde")]
#[test]
fn domain_types_serialize() {
let call = TemplateCall::new(
title("Шаблон:X"),
"X".to_owned(),
BTreeMap::new(),
Origin::Input,
None,
true,
);
let json = serde_json::to_string(&call).unwrap();
assert!(json.contains("\"name_raw\":\"X\""), "{json}");
let stats_json = serde_json::to_string(&StructuralStats::default()).unwrap();
assert!(stats_json.contains("\"tables\":0"), "{stats_json}");
}
}