use crate::note_editor::rich_text::RichText;
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub enum HeadingLevel {
H1 = 1,
H2,
H3,
H4,
H5,
H6,
}
impl From<pulldown_cmark::HeadingLevel> for HeadingLevel {
fn from(value: pulldown_cmark::HeadingLevel) -> Self {
match value {
pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum BlockQuoteKind {
Note,
Abstract,
Info,
Todo,
Tip,
Success,
Question,
Warning,
Failure,
Danger,
Bug,
Example,
Quote,
}
impl From<pulldown_cmark::BlockQuoteKind> for BlockQuoteKind {
fn from(value: pulldown_cmark::BlockQuoteKind) -> Self {
use pulldown_cmark::BlockQuoteKind as Gfm;
match value {
Gfm::Note => BlockQuoteKind::Note,
Gfm::Tip | Gfm::Important => BlockQuoteKind::Tip,
Gfm::Warning | Gfm::Caution => BlockQuoteKind::Warning,
}
}
}
impl BlockQuoteKind {
fn from_name(name: &str) -> Self {
use BlockQuoteKind::*;
match name.trim().to_ascii_lowercase().as_str() {
"abstract" | "summary" | "tldr" => Abstract,
"info" => Info,
"todo" => Todo,
"tip" | "hint" | "important" => Tip,
"success" | "check" | "done" => Success,
"question" | "help" | "faq" => Question,
"warning" | "caution" | "attention" => Warning,
"failure" | "fail" | "missing" => Failure,
"danger" | "error" => Danger,
"bug" => Bug,
"example" => Example,
"quote" | "cite" => Quote,
_ => Note,
}
}
}
pub struct CalloutMarker {
pub kind: BlockQuoteKind,
pub title: Option<String>,
}
pub fn parse_callout_marker(line: &str) -> Option<CalloutMarker> {
let rest = line.trim_start().strip_prefix("[!")?;
let close = rest.find(']')?;
let kind = BlockQuoteKind::from_name(&rest[..close]);
let tail = &rest[close + 1..];
let title = tail.strip_prefix(['-', '+']).unwrap_or(tail).trim();
Some(CalloutMarker {
kind,
title: (!title.is_empty()).then(|| title.to_string()),
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum ItemKind {
Ordered(u64),
Unordered,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TaskKind {
Checked,
Unchecked,
LooselyChecked,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Alignment {
None,
Left,
Center,
Right,
}
impl From<pulldown_cmark::Alignment> for Alignment {
fn from(value: pulldown_cmark::Alignment) -> Self {
match value {
pulldown_cmark::Alignment::None => Alignment::None,
pulldown_cmark::Alignment::Left => Alignment::Left,
pulldown_cmark::Alignment::Center => Alignment::Center,
pulldown_cmark::Alignment::Right => Alignment::Right,
}
}
}
pub type SourceRange<Idx> = std::ops::Range<Idx>;
#[derive(Clone, Debug, PartialEq)]
pub enum Node {
Heading {
level: HeadingLevel,
text: RichText,
source_range: SourceRange<usize>,
},
Paragraph {
text: RichText,
source_range: SourceRange<usize>,
},
CodeBlock {
lang: Option<String>,
text: RichText,
source_range: SourceRange<usize>,
},
BlockQuote {
kind: Option<BlockQuoteKind>,
title: Option<String>,
nodes: Vec<Node>,
source_range: SourceRange<usize>,
},
List {
nodes: Vec<Node>,
source_range: SourceRange<usize>,
},
Item {
kind: ItemKind,
nodes: Vec<Node>,
source_range: SourceRange<usize>,
},
Task {
kind: TaskKind,
nodes: Vec<Node>,
source_range: SourceRange<usize>,
},
Table {
alignments: Vec<Alignment>,
head: Vec<RichText>,
rows: Vec<Vec<RichText>>,
source_range: SourceRange<usize>,
},
}
impl Node {
pub fn source_range(&self) -> &SourceRange<usize> {
match self {
Self::Heading { source_range, .. }
| Self::CodeBlock { source_range, .. }
| Self::Paragraph { source_range, .. }
| Self::List { source_range, .. }
| Self::BlockQuote { source_range, .. }
| Self::Item { source_range, .. }
| Self::Task { source_range, .. }
| Self::Table { source_range, .. } => source_range,
}
}
pub fn set_source_range(&mut self, new_range: SourceRange<usize>) {
match self {
Self::Heading { source_range, .. }
| Self::CodeBlock { source_range, .. }
| Self::Paragraph { source_range, .. }
| Self::List { source_range, .. }
| Self::BlockQuote { source_range, .. }
| Self::Item { source_range, .. }
| Self::Task { source_range, .. }
| Self::Table { source_range, .. } => *source_range = new_range,
}
}
pub fn rich_text(&self) -> Option<&RichText> {
match self {
Self::Heading { text, .. }
| Self::Paragraph { text, .. }
| Self::CodeBlock { text, .. } => Some(text),
_ => None,
}
}
pub fn children_as_mut(&mut self) -> Option<&mut Vec<Self>> {
match self {
Self::List { nodes, .. }
| Self::Item { nodes, .. }
| Self::Task { nodes, .. }
| Self::BlockQuote { nodes, .. } => Some(nodes),
_ => None,
}
}
}
pub fn nodes_to_sexp(nodes: &[Node], indent_level: usize) -> String {
nodes
.iter()
.map(|node| node_to_sexp(node, indent_level))
.collect::<Vec<_>>()
.join("\n")
}
pub fn node_to_sexp(node: &Node, indent_level: usize) -> String {
let indent_increment = 2;
match node {
Node::Heading {
level,
text,
source_range,
} => {
format!(
"{:indent$}(heading {:?} @{:?}\n{})",
"",
level,
source_range,
rich_text_to_sexp(text, indent_level + indent_increment),
indent = indent_level
)
}
Node::Paragraph { text, source_range } => {
format!(
"{:indent$}(paragraph @{:?}\n{})",
"",
source_range,
rich_text_to_sexp(text, indent_level + indent_increment),
indent = indent_level
)
}
Node::BlockQuote {
kind,
title,
nodes,
source_range,
} => {
format!(
"{:indent$}(blockquote {:?} {:?} @{:?}\n{})",
"",
kind,
title,
source_range,
nodes_to_sexp(nodes, indent_level + indent_increment),
indent = indent_level
)
}
Node::CodeBlock {
lang,
text,
source_range,
} => {
format!(
"{:indent$}(codeblock {} @{:?}\n{})",
"",
lang.clone().unwrap_or(String::new()),
source_range,
rich_text_to_sexp(text, indent_level + indent_increment),
indent = indent_level,
)
}
Node::List {
nodes,
source_range,
} => {
format!(
"{:indent$}(list @{:?}\n{})",
"",
source_range,
nodes_to_sexp(nodes, indent_level + indent_increment),
indent = indent_level
)
}
Node::Item {
kind,
nodes,
source_range,
} => {
format!(
"{:indent$}(item {:?} @{:?}\n{})",
"",
kind,
source_range,
nodes_to_sexp(nodes, indent_level + indent_increment),
indent = indent_level
)
}
Node::Task {
kind,
nodes,
source_range,
} => {
format!(
"{:indent$}(task {:?} @{:?}\n{})",
"",
kind,
source_range,
nodes_to_sexp(nodes, indent_level + indent_increment),
indent = indent_level
)
}
Node::Table {
alignments,
head,
rows,
source_range,
} => {
let cells = |row: &[RichText]| {
row.iter()
.map(|cell| format!("\"{cell}\""))
.collect::<Vec<_>>()
.join(" ")
};
let inner_indent = indent_level + indent_increment;
let body = rows
.iter()
.map(|row| format!("{:inner_indent$}(row {})", "", cells(row)))
.collect::<Vec<_>>()
.join("\n");
format!(
"{:indent$}(table {:?} @{:?}\n{:inner_indent$}(head {})\n{})",
"",
alignments,
source_range,
"",
cells(head),
body,
indent = indent_level,
)
}
}
}
pub fn rich_text_to_sexp(rich_text: &RichText, indent_level: usize) -> String {
rich_text
.segments()
.iter()
.map(|segment| match &segment.style {
Some(style) => format!(
"{:indent$}({} \"{}\")",
"",
style,
segment,
indent = indent_level
),
None => format!("{:indent$}\"{}\"", "", segment, indent = indent_level),
})
.collect::<Vec<_>>()
.join("\n")
}