use crate::{clean_link, full_link, Error, Result, Wikicode, WikinodeIterator};
use kuchiki::{Attribute, ExpandedName, NodeRef};
use std::ops::Deref;
use urlencoding::{decode, encode};
#[derive(Debug, Clone)]
pub enum Wikinode {
BehaviorSwitch(BehaviorSwitch),
Category(Category),
Comment(Comment),
ExtLink(ExtLink),
Heading(Heading),
HtmlEntity(HtmlEntity),
InterwikiLink(InterwikiLink),
LanguageLink(LanguageLink),
Nowiki(Nowiki),
Redirect(Redirect),
Section(Section),
WikiLink(WikiLink),
Generic(Wikicode),
}
impl Deref for Wikinode {
type Target = NodeRef;
fn deref(&self) -> &Self::Target {
self.as_node()
}
}
impl Wikinode {
pub(crate) fn new_from_node(node: &NodeRef) -> Self {
if node.as_comment().is_some() {
return Self::Comment(Comment::new_from_node(node));
}
if let Some(element) = node.as_element() {
let tag_name = element.name.local.clone();
let attributes = element.attributes.borrow();
match tag_name {
local_name!("a") => {
if let Some(rel) = attributes.get("rel") {
match rel {
WikiLink::REL => {
return Self::WikiLink(
WikiLink::new_from_node(node),
);
}
ExtLink::REL => {
return Self::ExtLink(ExtLink::new_from_node(
node,
))
}
InterwikiLink::REL => {
return Self::InterwikiLink(
InterwikiLink::new_from_node(node),
);
}
_ => {}
}
}
}
local_name!("h1")
| local_name!("h2")
| local_name!("h3")
| local_name!("h4")
| local_name!("h5")
| local_name!("h6") => {
return Self::Heading(Heading::new_from_node(node));
}
local_name!("link") => {
if let Some(rel) = attributes.get("rel") {
match rel {
Category::REL => {
return Self::Category(
Category::new_from_node(node),
);
}
LanguageLink::REL => {
return Self::LanguageLink(
LanguageLink::new_from_node(node),
);
}
Redirect::REL => {
return Self::Redirect(
Redirect::new_from_node(node),
);
}
_ => {}
}
}
}
local_name!("meta") => {
if let Some(property) = attributes.get("property") {
if property.starts_with("mw:PageProp/") {
return Self::BehaviorSwitch(
BehaviorSwitch::new_from_node(node),
);
}
}
}
local_name!("section") => {
return Self::Section(Section::new_from_node(node));
}
local_name!("span") => {
if let Some(type_of) = attributes.get("typeof") {
match type_of {
Nowiki::TYPEOF => {
return Self::Nowiki(Nowiki::new_from_node(
node,
));
}
HtmlEntity::TYPEOF => {
return Self::HtmlEntity(
HtmlEntity::new_from_node(node),
);
}
_ => {}
}
}
}
_ => {}
}
}
Self::Generic(Wikicode::new_from_node(node))
}
pub fn as_behavior_switch(&self) -> Option<BehaviorSwitch> {
match self {
Self::BehaviorSwitch(switch) => Some(switch.clone()),
_ => None,
}
}
pub fn as_category(&self) -> Option<Category> {
match self {
Self::Category(category) => Some(category.clone()),
_ => None,
}
}
pub fn as_comment(&self) -> Option<Comment> {
match self {
Self::Comment(comment) => Some(comment.clone()),
_ => None,
}
}
pub fn as_extlink(&self) -> Option<ExtLink> {
match self {
Self::ExtLink(extlink) => Some(extlink.clone()),
_ => None,
}
}
pub fn as_generic(&self) -> Option<Wikicode> {
match self {
Self::Generic(node) => Some(node.clone()),
_ => None,
}
}
pub fn as_heading(&self) -> Option<Heading> {
match self {
Self::Heading(heading) => Some(heading.clone()),
_ => None,
}
}
pub fn as_html_entity(&self) -> Option<HtmlEntity> {
match self {
Self::HtmlEntity(entity) => Some(entity.clone()),
_ => None,
}
}
pub fn as_interwiki_link(&self) -> Option<InterwikiLink> {
match self {
Self::InterwikiLink(link) => Some(link.clone()),
_ => None,
}
}
pub fn as_language_link(&self) -> Option<LanguageLink> {
match self {
Self::LanguageLink(link) => Some(link.clone()),
_ => None,
}
}
pub fn as_nowiki(&self) -> Option<Nowiki> {
match self {
Self::Nowiki(nowiki) => Some(nowiki.clone()),
_ => None,
}
}
pub fn as_redirect(&self) -> Option<Redirect> {
match self {
Self::Redirect(redirect) => Some(redirect.clone()),
_ => None,
}
}
pub fn as_section(&self) -> Option<Section> {
match self {
Self::Section(section) => Some(section.clone()),
_ => None,
}
}
pub fn as_wikilink(&self) -> Option<WikiLink> {
match self {
Self::WikiLink(wikilink) => Some(wikilink.clone()),
_ => None,
}
}
}
impl WikinodeIterator for Wikinode {
fn as_node(&self) -> &NodeRef {
match self {
Self::BehaviorSwitch(switch) => switch,
Self::Category(category) => category,
Self::Comment(comment) => comment,
Self::ExtLink(extlink) => extlink,
Self::Heading(heading) => heading,
Self::HtmlEntity(entity) => entity,
Self::InterwikiLink(link) => link,
Self::LanguageLink(link) => link,
Self::Nowiki(nowiki) => nowiki,
Self::Redirect(redirect) => redirect,
Self::Section(section) => section,
Self::WikiLink(wikilink) => wikilink,
Self::Generic(code) => code,
}
}
}
macro_rules! impl_traits {
( $name:ident ) => {
impl From<$name> for Wikinode {
fn from(node: $name) -> Self {
Self::$name(node)
}
}
impl Deref for $name {
type Target = NodeRef;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl WikinodeIterator for $name {
fn as_node(&self) -> &NodeRef {
&self.0
}
}
};
}
impl_traits!(BehaviorSwitch);
impl_traits!(Category);
impl_traits!(Comment);
impl_traits!(ExtLink);
impl_traits!(Heading);
impl_traits!(HtmlEntity);
impl_traits!(InterwikiLink);
impl_traits!(LanguageLink);
impl_traits!(Nowiki);
impl_traits!(Redirect);
impl_traits!(Section);
impl_traits!(WikiLink);
#[derive(Debug, Clone)]
pub struct Comment(NodeRef);
impl Comment {
pub fn new(text: &str) -> Self {
Self(NodeRef::new_comment(text))
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_comment().is_none() {
unreachable!("Non-comment node passed");
}
Self(element.clone())
}
pub fn text(&self) -> String {
self.as_comment().unwrap().borrow().to_string()
}
pub fn set_text(&self, text: &str) {
self.as_comment().unwrap().replace(text.into());
}
}
#[derive(Debug, Clone)]
pub struct WikiLink(NodeRef);
impl WikiLink {
const REL: &'static str = "mw:WikiLink";
pub(crate) const SELECTOR: &'static str = "[rel=\"mw:WikiLink\"]";
pub fn new(target: &str, text: &NodeRef) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("a")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: full_link(target),
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
element.append(text.clone());
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn raw_target(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string()
}
pub fn target(&self) -> String {
clean_link(&self.raw_target())
}
pub fn set_target(&self, target: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", full_link(target));
}
}
#[derive(Debug, Clone)]
pub struct ExtLink(NodeRef);
impl ExtLink {
const REL: &'static str = "mw:ExtLink";
pub(crate) const SELECTOR: &'static str = "[rel=\"mw:ExtLink\"]";
pub fn new(target: &str, text: &NodeRef) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("a")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: target.to_string(),
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
element.append(text.clone());
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn target(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string()
}
pub fn set_target(&self, target: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", target.to_string());
}
}
#[derive(Debug, Clone)]
pub struct InterwikiLink(NodeRef);
impl InterwikiLink {
const REL: &'static str = "mw:WikiLink/Interwiki";
pub fn new(target: &str, text: &NodeRef) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("a")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: target.to_string(),
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
element.append(text.clone());
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn target(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string()
}
pub fn set_target(&self, target: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", target.to_string());
}
}
#[derive(Debug, Clone)]
pub struct Nowiki(NodeRef);
impl Nowiki {
const TYPEOF: &'static str = "mw:Nowiki";
pub fn new(text: &str) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("span")),
vec![(
ExpandedName::new(ns!(), "typeof"),
Attribute {
prefix: None,
value: Self::TYPEOF.to_string(),
},
)],
);
element.append(NodeRef::new_text(text));
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
Self(element.clone())
}
}
#[derive(Debug, Clone)]
pub struct HtmlEntity(NodeRef);
impl HtmlEntity {
const TYPEOF: &'static str = "mw:Entity";
pub fn new(text: &str) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("span")),
vec![(
ExpandedName::new(ns!(), "typeof"),
Attribute {
prefix: None,
value: Self::TYPEOF.to_string(),
},
)],
);
element.append(NodeRef::new_text(text));
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
Self(element.clone())
}
}
#[derive(Debug, Clone)]
pub struct Section(NodeRef);
impl Section {
pub(crate) const SELECTOR: &'static str = "section";
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
Self(element.clone())
}
pub fn section_id(&self) -> i32 {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("data-mw-section-id")
.expect("No data-mw-section-id attribute on section")
.parse()
.expect("Invalid data-mw-section-id attribute")
}
pub fn is_editable(&self) -> bool {
self.section_id() >= 0
}
pub fn is_pseudo_section(&self) -> bool {
let id = self.section_id();
id == -2 || id == 0
}
pub fn heading(&self) -> Option<Heading> {
if !self.is_pseudo_section() {
self.select_first(Heading::SELECTOR)
.map(|node| Heading::new_from_node(&node))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct Heading(NodeRef);
impl Heading {
pub(crate) const SELECTOR: &'static str = "h1, h2, h3, h4, h5, h6";
pub fn new(level: u32, contents: &NodeRef) -> Result<Self> {
if !(1..=6).contains(&level) {
return Err(Error::InvalidHeadingLevel(level));
}
let element = NodeRef::new_element(
crate::build_qual_name(format!("h{}", level).into()),
vec![],
);
element.append(contents.clone());
Ok(Self(element))
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
Self(element.clone())
}
pub fn get_level(&self) -> u32 {
match self.as_element().unwrap().name.local {
local_name!("h1") => 1,
local_name!("h2") => 2,
local_name!("h3") => 3,
local_name!("h4") => 4,
local_name!("h5") => 5,
local_name!("h6") => 6,
_ => unreachable!("Non h[1-6] used in Heading"),
}
}
}
#[derive(Debug, Clone)]
pub struct Category(NodeRef);
impl Category {
const REL: &'static str = "mw:PageProp/Category";
pub(crate) const SELECTOR: &'static str = "[rel=\"mw:PageProp/Category\"]";
pub fn new(category: &str, sortkey: Option<&str>) -> Self {
let href = Self::build_href(
&full_link(category),
sortkey.map(encode).as_deref(),
);
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("link")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: href,
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
fn split_href(&self) -> (String, Option<String>) {
let href = self
.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string();
let sp: Vec<_> = href.splitn(2, '#').collect();
if sp.len() == 2 {
(sp[0].to_string(), Some(sp[1].to_string()))
} else {
(sp[0].to_string(), None)
}
}
fn build_href(category: &str, sortkey: Option<&str>) -> String {
match sortkey {
Some(sortkey) => {
format!("{}#{}", category, sortkey)
}
None => category.to_string(),
}
}
pub fn category(&self) -> String {
let (category, _) = self.split_href();
clean_link(&category)
}
pub fn sort_key(&self) -> Option<String> {
let (_, sort_key) = self.split_href();
sort_key.map(|key| decode(&key).expect("Unable to decode sort key"))
}
pub fn set_category(&self, category: &str) {
let (_, sort_key) = self.split_href();
self.set_href(&Self::build_href(
&full_link(category),
sort_key.as_deref(),
));
}
pub fn set_sort_key(&self, sort_key: Option<&str>) {
let (category, _) = self.split_href();
self.set_href(&Self::build_href(
&category,
sort_key.map(encode).as_deref(),
));
}
fn set_href(&self, href: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", href.to_string());
}
}
#[derive(Debug, Clone)]
pub struct LanguageLink(NodeRef);
impl LanguageLink {
const REL: &'static str = "mw:PageProp/Language";
pub fn new(target: &str) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("link")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: target.to_string(),
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn target(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string()
}
pub fn set_target(&self, target: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", target.to_string());
}
}
#[derive(Debug, Clone)]
pub struct BehaviorSwitch(NodeRef);
impl BehaviorSwitch {
pub fn new(property: &str, content: Option<&str>) -> Self {
let mut attributes = vec![(
ExpandedName::new(ns!(), local_name!("property")),
Attribute {
prefix: None,
value: format!("mw:PageProp/{}", property),
},
)];
if let Some(content) = content {
attributes.push((
ExpandedName::new(ns!(), local_name!("content")),
Attribute {
prefix: None,
value: content.to_string(),
},
));
}
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("meta")),
attributes,
);
Self(element)
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn property(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("property")
.unwrap()
.trim_start_matches("mw:PageProp/")
.to_string()
}
pub fn content(&self) -> Option<String> {
match self
.as_element()
.unwrap()
.attributes
.borrow()
.get("content")
{
Some(content) => Some(content.to_string()),
None => None,
}
}
pub fn set_content(&self, content: &str) {
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("content", content.to_string());
}
}
#[derive(Debug, Clone)]
pub struct Redirect(NodeRef);
impl Redirect {
const REL: &'static str = "mw:PageProp/redirect";
pub(crate) const SELECTOR: &'static str = "[rel=\"mw:PageProp/redirect\"]";
pub fn new(target: &str) -> Self {
let element = NodeRef::new_element(
crate::build_qual_name(local_name!("link")),
vec![
(
ExpandedName::new(ns!(), local_name!("href")),
Attribute {
prefix: None,
value: "".to_string(),
},
),
(
ExpandedName::new(ns!(), local_name!("rel")),
Attribute {
prefix: None,
value: Self::REL.to_string(),
},
),
],
);
let redirect = Self(element);
redirect.set_target(target);
redirect
}
pub(crate) fn new_from_node(element: &NodeRef) -> Self {
if element.as_element().is_none() {
unreachable!("Non-element node passed");
}
Self(element.clone())
}
pub fn is_external(&self) -> bool {
!self.raw_target().starts_with("./")
}
pub fn raw_target(&self) -> String {
self.as_element()
.unwrap()
.attributes
.borrow()
.get("href")
.unwrap()
.to_string()
}
pub fn target(&self) -> String {
let raw = self.raw_target();
if raw.starts_with("./") {
clean_link(&raw)
} else {
raw
}
}
pub fn set_target(&self, target: &str) {
let new = if target.starts_with("http://")
|| target.starts_with("https://")
{
target.to_string()
} else {
full_link(target)
};
self.as_element()
.unwrap()
.attributes
.borrow_mut()
.insert("href", new);
}
}