#[derive(Clone, Debug, Default, PartialEq)]
pub struct Document {
pub blocks: Vec<Block>,
}
impl Document {
pub fn plain_text(&self) -> String {
let mut out = String::new();
for block in &self.blocks {
block.push_plain(&mut out);
}
out.trim_end().to_string()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Block {
Heading(Heading),
Byline(Vec<Inline>),
Paragraph(Vec<Inline>),
Lines(Vec<Vec<Inline>>),
List(List),
Definitions(Vec<Definition>),
Code(CodeBlock),
Table(TableBlock),
Quote {
blocks: Vec<Block>,
attribution: Option<Vec<Inline>>,
},
Admonition {
kind: AdmonitionKind,
title: Option<Vec<Inline>>,
blocks: Vec<Block>,
},
Aside {
title: Option<Vec<Inline>>,
blocks: Vec<Block>,
},
Indent { amount: usize, blocks: Vec<Block> },
Rule,
Image { target: String, alt: String },
Properties {
name: Option<String>,
entries: Vec<(String, String)>,
},
Banner {
left: String,
center: String,
right: String,
},
Caption(Vec<Inline>),
}
impl Block {
fn push_plain(&self, out: &mut String) {
match self {
Block::Heading(heading) => {
if let Some(keyword) = &heading.keyword {
out.push_str(&keyword.text);
out.push(' ');
}
push_inlines(&heading.content, out);
out.push('\n');
}
Block::Byline(inlines) | Block::Paragraph(inlines) | Block::Caption(inlines) => {
push_inlines(inlines, out);
out.push('\n');
}
Block::Lines(lines) => {
for line in lines {
push_inlines(line, out);
out.push('\n');
}
}
Block::List(list) => {
for item in &list.items {
for block in &item.blocks {
block.push_plain(out);
}
}
}
Block::Definitions(definitions) => {
for definition in definitions {
push_inlines(&definition.term, out);
out.push('\n');
for block in &definition.blocks {
block.push_plain(out);
}
}
}
Block::Code(code) => {
out.push_str(&code.code);
out.push('\n');
}
Block::Table(table) => {
for row in table.header.iter().chain(&table.rows) {
let cells: Vec<String> = row.iter().map(|cell| inlines_text(cell)).collect();
out.push_str(&cells.join(" | "));
out.push('\n');
}
}
Block::Quote {
blocks,
attribution,
} => {
for block in blocks {
block.push_plain(out);
}
if let Some(attribution) = attribution {
push_inlines(attribution, out);
out.push('\n');
}
}
Block::Admonition { title, blocks, .. } | Block::Aside { title, blocks } => {
if let Some(title) = title {
push_inlines(title, out);
out.push('\n');
}
for block in blocks {
block.push_plain(out);
}
}
Block::Indent { blocks, .. } => {
for block in blocks {
block.push_plain(out);
}
}
Block::Rule => out.push_str("---\n"),
Block::Image { alt, .. } => {
out.push_str(alt);
out.push('\n');
}
Block::Properties { name, entries } => {
if let Some(name) = name {
out.push_str(name);
out.push('\n');
}
for (key, value) in entries {
out.push_str(key);
out.push_str(": ");
out.push_str(value);
out.push('\n');
}
}
Block::Banner {
left,
center,
right,
} => {
out.push_str(&format!("{left} {center} {right}\n"));
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Heading {
pub level: u8,
pub content: Vec<Inline>,
pub keyword: Option<Keyword>,
pub priority: Option<String>,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Keyword {
pub text: String,
pub done: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct List {
pub kind: ListKind,
pub items: Vec<ListItem>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ListKind {
Bullet,
Ordered { start: u64 },
Callout,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ListItem {
pub checkbox: Option<Checkbox>,
pub number: Option<u64>,
pub blocks: Vec<Block>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Checkbox {
Unchecked,
Checked,
Partial,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Definition {
pub term: Vec<Inline>,
pub blocks: Vec<Block>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CodeBlock {
pub language: Option<String>,
pub code: String,
pub callouts: Vec<Callout>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Callout {
pub line: usize,
pub start: usize,
pub end: usize,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TableBlock {
pub header: Option<Vec<Vec<Inline>>>,
pub rows: Vec<Vec<Vec<Inline>>>,
pub align: Vec<Align>,
pub sections: Vec<usize>,
pub title: Option<Vec<Inline>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Align {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AdmonitionKind {
Note,
Tip,
Important,
Warning,
Caution,
Other(String),
}
impl AdmonitionKind {
pub fn from_label(label: &str) -> Option<Self> {
Some(match label.to_ascii_uppercase().as_str() {
"NOTE" => AdmonitionKind::Note,
"TIP" | "HINT" => AdmonitionKind::Tip,
"IMPORTANT" => AdmonitionKind::Important,
"WARNING" => AdmonitionKind::Warning,
"CAUTION" | "DANGER" => AdmonitionKind::Caution,
_ => return None,
})
}
pub fn label(&self) -> String {
match self {
AdmonitionKind::Note => "Note".into(),
AdmonitionKind::Tip => "Tip".into(),
AdmonitionKind::Important => "Important".into(),
AdmonitionKind::Warning => "Warning".into(),
AdmonitionKind::Caution => "Caution".into(),
AdmonitionKind::Other(label) => label.clone(),
}
}
pub fn style_key(&self) -> &'static str {
match self {
AdmonitionKind::Note => "document.admonition.note",
AdmonitionKind::Tip => "document.admonition.tip",
AdmonitionKind::Important => "document.admonition.important",
AdmonitionKind::Warning => "document.admonition.warning",
AdmonitionKind::Caution => "document.admonition.caution",
AdmonitionKind::Other(_) => "document.admonition",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Inline {
Text(String),
Emphasis(Vec<Inline>),
Strong(Vec<Inline>),
Underline(Vec<Inline>),
Strike(Vec<Inline>),
Code(String),
Link {
target: String,
content: Vec<Inline>,
},
Image {
target: String,
alt: String,
},
LineBreak,
Styled {
style: String,
content: Vec<Inline>,
},
}
impl Inline {
pub fn text(text: impl Into<String>) -> Self {
Inline::Text(text.into())
}
pub fn styled(style: impl Into<String>, content: Vec<Inline>) -> Self {
Inline::Styled {
style: style.into(),
content,
}
}
}
pub fn inlines_text(inlines: &[Inline]) -> String {
let mut out = String::new();
push_inlines(inlines, &mut out);
out
}
fn push_inlines(inlines: &[Inline], out: &mut String) {
for inline in inlines {
match inline {
Inline::Text(text) | Inline::Code(text) => out.push_str(text),
Inline::Emphasis(content)
| Inline::Strong(content)
| Inline::Underline(content)
| Inline::Strike(content)
| Inline::Styled { content, .. } => push_inlines(content, out),
Inline::Link { target, content } => {
if content.is_empty() {
out.push_str(target);
} else {
push_inlines(content, out);
}
}
Inline::Image { alt, .. } => out.push_str(alt),
Inline::LineBreak => out.push('\n'),
}
}
}