use crate::engine::strip::{MarkerId, MarkerKind};
use crate::title::Title;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct TextRange {
pub(crate) start: u32,
pub(crate) end: u32,
}
impl TextRange {
pub(crate) fn new(start: u32, end: u32) -> Self {
debug_assert!(start <= end);
Self { start, end }
}
pub(crate) fn slice(self, text: &str) -> &str {
&text[self.start as usize..self.end as usize]
}
pub(crate) fn is_empty(self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum InlineContent {
Raw(TextRange),
Parsed(Vec<Inline>),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Block {
Paragraph { lines: Vec<InlineContent> },
Heading {
level: u8,
line: TextRange,
inner: TextRange,
content: InlineContent,
},
IndentPre { lines: Vec<InlineContent> },
HorizontalRule,
List {
kind: ListKind,
items: Vec<ListItem>,
},
Table(Table),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ListKind {
Bullet,
Ordered,
Definition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ListItemKind {
Item,
Term,
Definition,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ListItem {
pub(crate) kind: ListItemKind,
pub(crate) content: Option<InlineContent>,
pub(crate) children: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct Table {
pub(crate) attributes: Option<TextRange>,
pub(crate) caption: Option<TableCaption>,
pub(crate) rows: Vec<TableRow>,
pub(crate) closed: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TableCaption {
pub(crate) attributes: Option<TextRange>,
pub(crate) content: InlineContent,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct TableRow {
pub(crate) attributes: Option<TextRange>,
pub(crate) cells: Vec<TableCell>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TableCell {
pub(crate) header: bool,
pub(crate) attributes: Option<TextRange>,
pub(crate) content: InlineContent,
pub(crate) children: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Inline {
Text(TextRange),
Entity { range: TextRange, decoded: char },
Marker { id: MarkerId, kind: MarkerKind },
Link {
target: Title,
written: TextRange,
fragment: Option<String>,
label: Option<Vec<Inline>>,
trail: Option<TextRange>,
pipe_trick: bool,
},
Category {
category: Title,
sortkey: Option<String>,
},
InterLanguage {
prefix: String,
target: String,
escaped: bool,
},
Image {
target: Title,
params: Vec<TextRange>,
caption: Option<Vec<Inline>>,
},
ExternalLink {
url: String,
label: Option<Vec<Inline>>,
},
AutoUrl { url: String, range: TextRange },
MagicLink {
kind: MagicLinkKind,
range: TextRange,
},
Italic(Vec<Inline>),
Bold(Vec<Inline>),
HtmlTag {
name: String,
closing: bool,
self_closing: bool,
attributes: Option<TextRange>,
},
BehaviorSwitch { id: String, range: TextRange },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MagicLinkKind {
Rfc,
Pmid,
Isbn,
}
#[derive(Debug, Default)]
pub(crate) struct DocTree {
pub(crate) blocks: Vec<Block>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_ranges_slice_flattened_text() {
let text = "аб cd";
let range = TextRange::new(0, 4);
assert_eq!(range.slice(text), "аб");
assert!(!range.is_empty());
assert!(TextRange::new(2, 2).is_empty());
}
#[test]
fn inline_content_transitions_raw_to_parsed() {
let raw = InlineContent::Raw(TextRange::new(0, 4));
let parsed = InlineContent::Parsed(vec![Inline::Text(TextRange::new(0, 4))]);
assert_ne!(raw, parsed);
assert_eq!(raw.clone(), raw);
}
#[test]
fn block_tree_nests_lists_and_tables() {
let item = ListItem {
kind: ListItemKind::Item,
content: Some(InlineContent::Raw(TextRange::new(2, 8))),
children: vec![Block::List {
kind: ListKind::Ordered,
items: vec![ListItem {
kind: ListItemKind::Item,
content: None,
children: Vec::new(),
}],
}],
};
let table = Table {
attributes: Some(TextRange::new(2, 20)),
caption: Some(TableCaption {
attributes: None,
content: InlineContent::Raw(TextRange::new(24, 30)),
}),
rows: vec![TableRow {
attributes: None,
cells: vec![TableCell {
header: true,
attributes: Some(TextRange::new(33, 40)),
content: InlineContent::Raw(TextRange::new(41, 50)),
children: Vec::new(),
}],
}],
closed: true,
};
let doc = DocTree {
blocks: vec![
Block::Heading {
level: 2,
line: TextRange::new(0, 10),
inner: TextRange::new(3, 10),
content: InlineContent::Raw(TextRange::new(3, 10)),
},
Block::List {
kind: ListKind::Bullet,
items: vec![item],
},
Block::Table(table),
Block::HorizontalRule,
Block::IndentPre {
lines: vec![InlineContent::Raw(TextRange::new(60, 70))],
},
Block::Paragraph {
lines: vec![InlineContent::Raw(TextRange::new(71, 80))],
},
],
};
assert_eq!(doc.blocks.len(), 6);
let debug = format!("{doc:?}");
assert!(debug.contains("Heading") && debug.contains("Table"));
assert_ne!(ListKind::Bullet, ListKind::Definition);
assert_ne!(ListItemKind::Term, ListItemKind::Definition);
assert_eq!(Table::default().rows.len(), 0);
assert_eq!(TableRow::default().cells.len(), 0);
}
#[test]
fn inline_variants_cover_the_construct_families() {
use crate::config::SiteConfig;
let config = SiteConfig::builder("en", "W")
.namespace(crate::config::NamespaceDef::main())
.build()
.unwrap();
let title = Title::parse("Page", &config).unwrap();
let nodes = [
Inline::Text(TextRange::new(0, 1)),
Inline::Entity {
range: TextRange::new(1, 7),
decoded: '\u{a0}',
},
Inline::Marker {
id: MarkerId(0),
kind: MarkerKind::ExtTag,
},
Inline::Link {
target: title.clone(),
written: TextRange::new(2, 6),
fragment: Some("s".to_owned()),
label: Some(vec![Inline::Text(TextRange::new(9, 10))]),
trail: Some(TextRange::new(12, 13)),
pipe_trick: false,
},
Inline::Category {
category: title.clone(),
sortkey: Some("k".to_owned()),
},
Inline::InterLanguage {
prefix: "fr".to_owned(),
target: "Chat".to_owned(),
escaped: false,
},
Inline::Image {
target: title.clone(),
params: vec![TextRange::new(20, 25)],
caption: Some(Vec::new()),
},
Inline::ExternalLink {
url: "https://example.com".to_owned(),
label: None,
},
Inline::AutoUrl {
url: "https://example.com".to_owned(),
range: TextRange::new(30, 49),
},
Inline::MagicLink {
kind: MagicLinkKind::Isbn,
range: TextRange::new(50, 64),
},
Inline::Italic(vec![Inline::Bold(Vec::new())]),
Inline::HtmlTag {
name: "br".to_owned(),
closing: false,
self_closing: true,
attributes: None,
},
Inline::BehaviorSwitch {
id: "notoc".to_owned(),
range: TextRange::new(70, 79),
},
];
assert_eq!(nodes.len(), 13);
assert_ne!(MagicLinkKind::Rfc, MagicLinkKind::Pmid);
assert_eq!(nodes[0].clone(), nodes[0]);
}
}