#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::fmt::{Display, Formatter};
#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Wikitext {
pub root_section: Section,
}
impl Wikitext {
pub fn print_headlines(&self) {
self.root_section.print_headlines();
}
pub fn list_headlines(&self) -> Vec<Headline> {
let mut result = Vec::new();
self.root_section.list_headlines(&mut result);
result
}
pub fn list_double_brace_expressions(&self) -> Vec<TextPiece> {
let mut result = Vec::new();
self.root_section.list_double_brace_expressions(&mut result);
result
}
pub fn list_plain_text(&self) -> Vec<TextPiece> {
let mut result = Vec::new();
self.root_section.list_plain_text(&mut result);
result
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Section {
pub headline: Headline,
pub paragraphs: Vec<Paragraph>,
pub subsections: Vec<Section>,
}
impl Section {
pub fn print_headlines(&self) {
println!(
"{0} {1} {0}",
"=".repeat(self.headline.level.into()),
self.headline.label
);
for subsection in &self.subsections {
subsection.print_headlines();
}
}
pub fn list_headlines(&self, result: &mut Vec<Headline>) {
result.push(self.headline.clone());
for subsection in &self.subsections {
subsection.list_headlines(result);
}
}
pub fn iter_text_pieces(&self) -> impl Iterator<Item = &'_ TextPiece> {
self.paragraphs
.iter()
.flat_map(|paragraph| paragraph.lines.iter())
.flat_map(|line| match line {
Line::Normal { text } => text.pieces.iter(),
Line::List { text, .. } => text.pieces.iter(),
})
}
pub fn list_double_brace_expressions(&self, result: &mut Vec<TextPiece>) {
for text_piece in self.iter_text_pieces() {
if matches!(text_piece, TextPiece::DoubleBraceExpression { .. }) {
result.push(text_piece.clone());
}
}
for subsection in &self.subsections {
subsection.list_double_brace_expressions(result);
}
}
pub fn list_plain_text(&self, result: &mut Vec<TextPiece>) {
for text_piece in self.iter_text_pieces() {
if matches!(text_piece, TextPiece::Text { .. }) {
result.push(text_piece.clone());
}
}
for subsection in &self.subsections {
subsection.list_plain_text(result);
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Headline {
pub label: String,
pub level: u8,
}
impl Headline {
pub fn new(label: impl Into<String>, level: u8) -> Self {
Self {
label: label.into(),
level,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Paragraph {
pub lines: Vec<Line>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Line {
Normal { text: Text },
List { list_prefix: String, text: Text },
}
impl Line {
pub fn is_empty(&self) -> bool {
match self {
Line::Normal { text } => text.is_empty(),
Line::List { .. } => false,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Text {
pub pieces: Vec<TextPiece>,
}
impl Text {
pub fn new() -> Self {
Default::default()
}
pub fn is_empty(&self) -> bool {
self.pieces.is_empty()
}
pub fn extend_with_formatted_text(&mut self, text_formatting: TextFormatting, text: &str) {
if let Some(TextPiece::Text {
formatting: last_formatting,
text: last,
}) = self.pieces.last_mut()
{
if text_formatting == *last_formatting {
last.push_str(text);
return;
}
}
self.pieces.push(TextPiece::Text {
formatting: text_formatting,
text: text.to_string(),
});
}
pub fn trim_self(&mut self) {
self.trim_self_start();
self.trim_self_end();
}
pub fn trim_self_start(&mut self) {
let mut offset = 0;
while offset < self.pieces.len() {
match &mut self.pieces[offset] {
TextPiece::Text { text, .. } => {
*text = text.trim_start().to_string();
if !text.is_empty() {
break;
}
}
TextPiece::DoubleBraceExpression { .. }
| TextPiece::InternalLink { .. }
| TextPiece::ListItem { .. } => break,
}
offset += 1;
}
self.pieces.drain(..offset);
}
pub fn trim_self_end(&mut self) {
let mut limit = self.pieces.len();
while limit > 0 {
match &mut self.pieces[limit - 1] {
TextPiece::Text { text, .. } => {
*text = text.trim_end().to_string();
if !text.is_empty() {
break;
}
}
TextPiece::DoubleBraceExpression { .. } | TextPiece::InternalLink { .. } => break,
TextPiece::ListItem { text, .. } => {
text.trim_self_end();
break;
}
}
limit -= 1;
}
self.pieces.drain(limit..);
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TextPiece {
Text {
formatting: TextFormatting,
text: String,
},
DoubleBraceExpression {
tag: Text,
attributes: Vec<Attribute>,
},
InternalLink {
target: Text,
options: Vec<String>,
label: Option<Text>,
},
ListItem {
list_prefix: String,
text: Text,
},
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Attribute {
pub name: Option<String>,
pub value: Text,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[allow(missing_docs)]
pub enum TextFormatting {
Normal,
Italic,
Bold,
ItalicBold,
}
impl TextFormatting {
pub fn next_formatting(&self, apostrophe_length: usize) -> Self {
match (self, apostrophe_length) {
(TextFormatting::Normal, 2) => TextFormatting::Italic,
(TextFormatting::Normal, 3) => TextFormatting::Bold,
(TextFormatting::Normal, 5) => TextFormatting::ItalicBold,
(TextFormatting::Italic, 2) => TextFormatting::Normal,
(TextFormatting::Italic, 3) => TextFormatting::ItalicBold,
(TextFormatting::Italic, 5) => TextFormatting::Bold,
(TextFormatting::Bold, 2) => TextFormatting::ItalicBold,
(TextFormatting::Bold, 3) => TextFormatting::Normal,
(TextFormatting::Bold, 5) => TextFormatting::Italic,
(TextFormatting::ItalicBold, 2) => TextFormatting::Bold,
(TextFormatting::ItalicBold, 3) => TextFormatting::Italic,
(TextFormatting::ItalicBold, 5) => TextFormatting::Normal,
(_, apostrophe_length) => unreachable!("Unused apostrophe length: {apostrophe_length}"),
}
}
}
impl PartialOrd for TextFormatting {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(TextFormatting::Normal, TextFormatting::Normal) => Some(Ordering::Equal),
(TextFormatting::Normal, TextFormatting::Italic) => Some(Ordering::Less),
(TextFormatting::Normal, TextFormatting::Bold) => Some(Ordering::Less),
(TextFormatting::Normal, TextFormatting::ItalicBold) => Some(Ordering::Less),
(TextFormatting::Italic, TextFormatting::Normal) => Some(Ordering::Greater),
(TextFormatting::Italic, TextFormatting::Italic) => Some(Ordering::Equal),
(TextFormatting::Italic, TextFormatting::Bold) => None,
(TextFormatting::Italic, TextFormatting::ItalicBold) => Some(Ordering::Less),
(TextFormatting::Bold, TextFormatting::Normal) => Some(Ordering::Greater),
(TextFormatting::Bold, TextFormatting::Italic) => None,
(TextFormatting::Bold, TextFormatting::Bold) => Some(Ordering::Equal),
(TextFormatting::Bold, TextFormatting::ItalicBold) => Some(Ordering::Less),
(TextFormatting::ItalicBold, TextFormatting::Normal) => Some(Ordering::Greater),
(TextFormatting::ItalicBold, TextFormatting::Italic) => Some(Ordering::Greater),
(TextFormatting::ItalicBold, TextFormatting::Bold) => Some(Ordering::Greater),
(TextFormatting::ItalicBold, TextFormatting::ItalicBold) => Some(Ordering::Equal),
}
}
}
impl Display for Text {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
for text_piece in &self.pieces {
write!(fmt, "{text_piece}")?;
}
Ok(())
}
}
impl Display for TextPiece {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
TextPiece::Text { text, formatting } => {
write!(fmt, "{}", formatting)?;
write!(fmt, "{text}")?;
write!(fmt, "{}", formatting)
}
TextPiece::DoubleBraceExpression {
tag,
attributes: parameters,
} => {
write!(fmt, "{{{{{tag}")?;
for parameter in parameters {
write!(fmt, "|{parameter}")?;
}
write!(fmt, "}}}}")
}
TextPiece::InternalLink {
target: url,
options,
label,
} => {
write!(fmt, "[[{url}")?;
for option in options {
write!(fmt, "|{option}")?;
}
if let Some(label) = label {
write!(fmt, "|{label}")?;
}
write!(fmt, "]]")
}
TextPiece::ListItem { list_prefix, text } => {
write!(fmt, "{list_prefix} {text}")
}
}
}
}
impl Display for Attribute {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if let Some(name) = &self.name {
write!(fmt, "{name}=")?;
}
write!(fmt, "{}", self.value)
}
}
impl Display for TextFormatting {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
match self {
TextFormatting::Normal => Ok(()),
TextFormatting::Italic => write!(fmt, "''"),
TextFormatting::Bold => write!(fmt, "'''"),
TextFormatting::ItalicBold => write!(fmt, "'''''"),
}
}
}