use crate::data::{PageInfo, PageRef};
use crate::layout::Layout;
use crate::render::{Render, html::HtmlRender, text::TextRender};
use crate::settings::{WikitextMode, WikitextSettings};
use crate::tree::{
Alignment, AnchorTarget, AttributeMap, BibliographyList, ClearFloat, CodeBlock,
Container, ContainerType, Element, FileSource, FloatAlignment, Heading, HeadingLevel,
LinkLabel, LinkLocation, LinkType, ListItem, ListType, Module, SyntaxTree,
attribute::SAFE_ATTRIBUTES,
};
use proptest::option;
use proptest::prelude::*;
use std::borrow::Cow;
use std::num::NonZeroU32;
use std::sync::LazyLock;
static SAFE_ATTRIBUTES_VEC: LazyLock<Vec<&'static str>> =
LazyLock::new(|| SAFE_ATTRIBUTES.iter().map(|s| s.as_ref()).collect());
const SIMPLE_EMAIL_REGEX: &str = r"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";
const SIMPLE_URL_REGEX: &str = r"https?://([-.]\w)+";
macro_rules! select {
($items:expr) => {
proptest::sample::select(&$items[..])
};
}
macro_rules! cow {
($strategy:expr) => {
$strategy.prop_map(Cow::Owned)
};
}
fn arb_attribute_map() -> impl Strategy<Value = AttributeMap<'static>> {
proptest::collection::btree_map(
prop_oneof![
select!(SAFE_ATTRIBUTES_VEC).prop_map(|s| Cow::Owned(str!(s))),
cow!(r"[A-Za-z0-9-]+"),
],
cow!(".*"),
0..12,
)
.prop_map(AttributeMap::from)
}
#[inline]
fn arb_optional_str() -> impl Strategy<Value = Option<Cow<'static, str>>> {
option::of(cow!(".*"))
}
fn arb_module() -> impl Strategy<Value = Element<'static>> {
let join = (arb_optional_str(), arb_attribute_map()).prop_map(
|(button_text, attributes)| Module::Join {
button_text,
attributes,
},
);
let page_tree = (
arb_optional_str(),
any::<bool>(),
any::<u32>().prop_map(NonZeroU32::new),
)
.prop_map(|(root, show_root, depth)| Module::PageTree {
root,
show_root,
depth,
});
prop_oneof![
Just(Module::Rate),
arb_optional_str().prop_map(|page| Module::Backlinks { page }),
any::<bool>().prop_map(|include_hidden| Module::Categories { include_hidden }),
join,
page_tree,
]
.prop_map(Element::Module)
}
fn arb_target() -> impl Strategy<Value = Option<AnchorTarget>> {
option::of(select!([
AnchorTarget::NewTab,
AnchorTarget::Parent,
AnchorTarget::Top,
AnchorTarget::Same,
]))
}
fn arb_page_ref() -> impl Strategy<Value = PageRef> {
let site = option::of(r"[\w\-\. ]+");
let page = r"[\w\-\._ :]+";
(site, page).prop_map(|(site, page)| PageRef::new(site, page))
}
fn arb_link_location_url() -> impl Strategy<Value = LinkLocation<'static>> {
cow!(".+").prop_map(LinkLocation::Url)
}
fn arb_link_location_page() -> impl Strategy<Value = LinkLocation<'static>> {
arb_page_ref().prop_map(LinkLocation::Page)
}
fn arb_link_location() -> impl Strategy<Value = LinkLocation<'static>> {
prop_oneof![arb_link_location_url(), arb_link_location_page()]
}
fn arb_link_type() -> impl Strategy<Value = LinkType> {
select!([
LinkType::Direct,
LinkType::Page,
LinkType::Interwiki,
LinkType::Anchor,
LinkType::TableOfContents,
])
}
fn arb_link_element() -> impl Strategy<Value = Element<'static>> {
let label_and_link = prop_oneof![
(cow!(".*").prop_map(LinkLabel::Text), arb_link_location()),
(cow!(".*").prop_map(LinkLabel::Slug), arb_link_location()),
(Just(LinkLabel::Url), arb_link_location_url()),
(Just(LinkLabel::Page), arb_link_location_page()),
];
(arb_link_type(), label_and_link, arb_target()).prop_map(
|(ltype, (label, link), target)| Element::Link {
ltype,
link,
label,
target,
},
)
}
fn arb_image() -> impl Strategy<Value = Element<'static>> {
let source = prop_oneof![
cow!(SIMPLE_URL_REGEX).prop_map(FileSource::Url),
cow!(".*").prop_map(|file| FileSource::File1 { file }),
(cow!(".*"), cow!(".*"))
.prop_map(|(page, file)| FileSource::File2 { page, file }),
(cow!(".*"), cow!(".*"), cow!(".*"))
.prop_map(|(site, page, file)| FileSource::File3 { site, page, file }),
];
let alignment = select!([Alignment::Left, Alignment::Right, Alignment::Center]);
let image_alignment = option::of(
(alignment, any::<bool>())
.prop_map(|(align, float)| FloatAlignment { align, float }),
);
(
source,
option::of(arb_link_location()),
image_alignment,
arb_attribute_map(),
)
.prop_map(|(source, link, alignment, attributes)| Element::Image {
source,
link,
alignment,
attributes,
})
}
fn arb_list<S>(elements: S) -> impl Strategy<Value = Element<'static>>
where
S: Strategy<Value = Vec<Element<'static>>> + 'static,
{
macro_rules! make_list {
($items:expr) => {{
let ltype = select!([ListType::Bullet, ListType::Numbered]);
let items = $items;
let attributes = arb_attribute_map();
(ltype, items, attributes).prop_map(|(ltype, items, attributes)| {
Element::List {
ltype,
items,
attributes,
}
})
}};
}
let list_item = (elements, arb_attribute_map()).prop_map(|(elements, attributes)| {
ListItem::Elements {
elements,
attributes,
}
});
let leaf = make_list!(proptest::collection::vec(list_item, 1..10));
leaf.prop_recursive(
5, 30, 10, |inner| {
make_list!(inner.prop_map(|element| {
let element = Box::new(element);
vec![ListItem::SubList { element }]
}))
},
)
}
fn arb_code() -> impl Strategy<Value = Element<'static>> {
(cow!(".*"), arb_optional_str(), arb_optional_str()).prop_map(
|(contents, language, name)| {
Element::Code(CodeBlock {
contents,
language,
name,
})
},
)
}
fn arb_checkbox() -> impl Strategy<Value = Element<'static>> {
(any::<bool>(), arb_attribute_map()).prop_map(|(checked, attributes)| {
Element::CheckBox {
checked,
attributes,
}
})
}
fn arb_container<S>(elements: S) -> impl Strategy<Value = Element<'static>>
where
S: Strategy<Value = Vec<Element<'static>>>,
{
let alignment = select!([
Alignment::Left,
Alignment::Right,
Alignment::Center,
Alignment::Justify,
]);
let heading = {
let has_toc = select!([true, false]);
let level = select!([
HeadingLevel::One,
HeadingLevel::Two,
HeadingLevel::Three,
HeadingLevel::Four,
HeadingLevel::Five,
HeadingLevel::Six,
]);
(level, has_toc).prop_map(|(level, has_toc)| Heading { level, has_toc })
};
let container_type = prop_oneof![
Just(ContainerType::Bold),
Just(ContainerType::Italics),
Just(ContainerType::Underline),
Just(ContainerType::Superscript),
Just(ContainerType::Subscript),
Just(ContainerType::Strikethrough),
Just(ContainerType::Monospace),
Just(ContainerType::Span),
Just(ContainerType::Div),
Just(ContainerType::Mark),
Just(ContainerType::Blockquote),
Just(ContainerType::Insertion),
Just(ContainerType::Deletion),
Just(ContainerType::Hidden),
Just(ContainerType::Invisible),
Just(ContainerType::Size),
Just(ContainerType::Paragraph),
alignment.prop_map(ContainerType::Align),
heading.prop_map(ContainerType::Header),
];
(container_type, elements, arb_attribute_map()).prop_map(
|(ctype, elements, attributes)| {
Element::Container(Container::new(ctype, elements, attributes))
},
)
}
fn arb_collapsible<S>(elements: S) -> impl Strategy<Value = Element<'static>>
where
S: Strategy<Value = Vec<Element<'static>>>,
{
(
elements,
arb_attribute_map(),
any::<bool>(),
arb_optional_str(),
arb_optional_str(),
any::<bool>(),
any::<bool>(),
)
.prop_map(
|(
elements,
attributes,
start_open,
show_text,
hide_text,
show_top,
show_bottom,
)| Element::Collapsible {
elements,
attributes,
start_open,
show_text,
hide_text,
show_top,
show_bottom,
},
)
}
fn arb_element_leaf() -> impl Strategy<Value = Element<'static>> {
prop_oneof![
cow!(".*").prop_map(Element::Text),
cow!(".*").prop_map(Element::Raw),
cow!(SIMPLE_EMAIL_REGEX).prop_map(Element::Email),
arb_module(),
arb_link_element(),
arb_image(),
arb_checkbox(),
arb_code(),
(cow!(".*"), arb_attribute_map()).prop_map(|(contents, attributes)| {
Element::Html {
contents,
attributes,
}
}),
Just(Element::LineBreak),
(1..50_u32)
.prop_map(|count| Element::LineBreaks(NonZeroU32::new(count).unwrap())),
select!([ClearFloat::Both, ClearFloat::Left, ClearFloat::Right])
.prop_map(Element::ClearFloat),
Just(Element::HorizontalRule),
]
}
fn arb_tree() -> impl Strategy<Value = SyntaxTree<'static>> {
let leaf = arb_element_leaf();
let element = leaf.prop_recursive(
5, 50, 10, |inner| {
macro_rules! elements {
() => {
proptest::collection::vec(inner.clone(), 1..20)
};
}
prop_oneof![
arb_container(elements!()),
arb_list(elements!()),
arb_collapsible(elements!()),
]
},
);
let toc_elements = proptest::collection::vec(arb_element_leaf(), 1..5);
let toc_heading = arb_list(toc_elements);
let footnote = proptest::collection::vec(element.clone(), 5..10);
(
proptest::collection::vec(element, 1..100),
proptest::collection::vec(toc_heading, 0..2),
proptest::collection::vec(footnote, 0..2),
any::<bool>(),
0..250usize,
)
.prop_map(
|(
elements,
table_of_contents,
footnotes,
mut needs_footnote_block,
wikitext_len,
)| {
needs_footnote_block &= !footnotes.is_empty();
SyntaxTree {
elements,
html_blocks: Vec::new(),
code_blocks: Vec::new(), table_of_contents,
footnotes,
needs_footnote_block,
bibliographies: BibliographyList::new(), wikitext_len,
}
},
)
}
fn arb_page_info() -> impl Strategy<Value = PageInfo<'static>> {
(
cow!(".+"),
arb_optional_str(),
cow!(".+"),
cow!(".+"),
arb_optional_str(),
any::<f64>(),
proptest::collection::vec(cow!(".+"), 0..20),
cow!(r"[a-z\-]+"),
)
.prop_map(
|(page, category, site, title, alt_title, score, tags, language)| PageInfo {
page,
category,
site,
title,
alt_title,
score: score.into(),
tags,
language,
},
)
}
fn render<R: Render>(
render: R,
tree: SyntaxTree<'static>,
page_info: PageInfo<'static>,
) -> R::Output {
let settings = WikitextSettings::from_mode(WikitextMode::Page, Layout::Wikidot);
render.render(&tree, &page_info, &settings)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(16))]
#[test]
#[ignore = "slow test"]
fn render_html_prop(page_info in arb_page_info(), tree in arb_tree()) {
let out = render(HtmlRender, tree, page_info);
assert!(out.meta.len() >= 4);
}
#[test]
#[ignore = "slow test"]
fn render_text_prop(page_info in arb_page_info(), tree in arb_tree()) {
let _ = render(TextRender, tree, page_info);
}
}