use std::{
collections::{HashMap, HashSet},
ops::Range,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Doc {
pub blocks: Vec<Block>,
}
impl Doc {
pub(crate) fn renumber(&mut self) {
let mut expected: Vec<Option<u64>> = Vec::new();
let mut runs = Runs::default();
let mut above: Option<Run> = None;
for block in &mut self.blocks {
let indent = block.indent as usize;
let run = runs.next(block.quote, block.indent);
if run.is_none_or(|run| run.first) {
let edge = [above, run]
.into_iter()
.flatten()
.map(|run| run.level as usize)
.min();
if let Some(edge) = edge {
expected.truncate(edge);
}
}
above = run;
expected.truncate(indent + 1);
expected.resize(indent + 1, None);
if let BlockKind::Ordered { number, .. } = &mut block.kind {
if let Some(next) = expected[indent] {
*number = next;
}
expected[indent] = Some(number.saturating_add(1));
} else {
expected[indent] = None;
}
}
}
pub fn anchors(&self) -> Vec<(String, usize)> {
let mut taken = HashSet::new();
let mut next: HashMap<String, usize> = HashMap::new();
self.blocks
.iter()
.enumerate()
.filter_map(|(ix, block)| match &block.kind {
BlockKind::Heading { text, .. } => Some((slug(&text.text), ix)),
_ => None,
})
.map(|(base, ix)| {
let from = next.get(&base).copied().unwrap_or(0);
let (n, anchor) = (from..)
.map(|n| match n {
0 => (n, base.clone()),
n => (n, format!("{base}-{n}")),
})
.find(|(_, anchor)| !taken.contains(anchor))
.expect("an unbounded run of suffixes");
next.insert(base, n + 1);
taken.insert(anchor.clone());
(anchor, ix)
})
.collect()
}
}
pub fn slug(text: &str) -> String {
text.chars()
.flat_map(char::to_lowercase)
.filter_map(|c| match c {
' ' => Some('-'),
'-' | '_' => Some(c),
c if c.is_alphanumeric() => Some(c),
_ => None,
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Block {
pub kind: BlockKind,
pub indent: u8,
pub quote: Option<Quoted>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Quoted {
pub alert: Option<QuoteKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Run {
pub level: u8,
pub first: bool,
}
pub(crate) fn quote_runs(
blocks: impl IntoIterator<Item = (Option<Quoted>, u8)>,
) -> Vec<Option<Run>> {
let mut runs = Runs::default();
blocks
.into_iter()
.map(|(quote, indent)| runs.next(quote, indent))
.collect()
}
#[derive(Default)]
pub(crate) struct Runs {
open: Option<(Quoted, u8)>,
}
impl Runs {
pub fn next(&mut self, quote: Option<Quoted>, indent: u8) -> Option<Run> {
let Some(quote) = quote else {
self.open = None;
return None;
};
match self.open {
Some((current, level)) if current == quote && indent >= level => Some(Run {
level,
first: false,
}),
_ => {
self.open = Some((quote, indent));
Some(Run {
level: indent,
first: true,
})
}
}
}
}
impl From<BlockKind> for Block {
fn from(kind: BlockKind) -> Self {
Self::new(kind)
}
}
impl Block {
pub fn new(kind: BlockKind) -> Self {
Self::at(kind, 0)
}
pub fn at(kind: BlockKind, indent: u8) -> Self {
Self {
kind,
indent,
quote: None,
}
}
pub fn quoted(self, quote: Quoted) -> Self {
Self {
quote: Some(quote),
..self
}
}
pub fn text_at(&self, part: Part) -> Option<&Text> {
match (&self.kind, part) {
(
BlockKind::Paragraph(text)
| BlockKind::Heading { text, .. }
| BlockKind::Bullet(text)
| BlockKind::Ordered { text, .. }
| BlockKind::Task { text, .. },
Part::Body,
) => Some(text),
(BlockKind::Code { code, .. }, Part::Code) => Some(code),
(BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
(BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => header.get(column),
(BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
rows.get(row - 1)?.get(column)
}
_ => None,
}
}
pub fn text_at_mut(&mut self, part: Part) -> Option<&mut Text> {
match (&mut self.kind, part) {
(
BlockKind::Paragraph(text)
| BlockKind::Heading { text, .. }
| BlockKind::Bullet(text)
| BlockKind::Ordered { text, .. }
| BlockKind::Task { text, .. },
Part::Body,
) => Some(text),
(BlockKind::Code { code, .. }, Part::Code) => Some(code),
(BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
(BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => {
header.get_mut(column)
}
(BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
rows.get_mut(row - 1)?.get_mut(column)
}
_ => None,
}
}
pub fn parts(&self) -> Vec<Part> {
match &self.kind {
BlockKind::Paragraph(_)
| BlockKind::Heading { .. }
| BlockKind::Bullet(_)
| BlockKind::Ordered { .. }
| BlockKind::Task { .. } => vec![Part::Body],
BlockKind::Code { .. } => vec![Part::Code],
BlockKind::Image { .. } => vec![Part::Caption],
BlockKind::Table { header, rows, .. } => {
let mut parts = Vec::new();
if !header.is_empty() {
parts.extend((0..header.len()).map(|column| Part::Cell { row: 0, column }));
}
for (ix, row) in rows.iter().enumerate() {
parts.extend((0..row.len()).map(|column| Part::Cell {
row: ix + 1,
column,
}));
}
parts
}
BlockKind::Bookmark { .. } | BlockKind::Rule => Vec::new(),
}
}
pub(crate) fn blank(&self) -> bool {
matches!(&self.kind, BlockKind::Paragraph(text) if text.is_empty() && text.marks.is_empty())
&& !self.quote.is_some_and(|quote| quote.alert.is_some())
}
pub fn opaque(&self) -> bool {
matches!(
self.kind,
BlockKind::Image { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Part {
#[default]
Body,
Code,
Caption,
Cell {
row: usize,
column: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum BlockKind {
Paragraph(Text),
Heading {
level: u8,
text: Text,
},
Bullet(Text),
Ordered {
number: u64,
text: Text,
},
Task {
checked: bool,
text: Text,
},
Code {
language: Option<String>,
code: Text,
height: Option<u32>,
},
Image {
url: String,
alt: Text,
width: Option<u32>,
},
Bookmark {
url: String,
form: Form,
},
Table {
align: Vec<Align>,
header: Vec<Text>,
rows: Vec<Vec<Text>>,
},
Rule,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuoteKind {
Note,
Tip,
Important,
Warning,
Caution,
}
impl QuoteKind {
pub fn marker(self) -> &'static str {
match self {
Self::Note => "[!NOTE]",
Self::Tip => "[!TIP]",
Self::Important => "[!IMPORTANT]",
Self::Warning => "[!WARNING]",
Self::Caution => "[!CAUTION]",
}
}
pub fn label(self) -> &'static str {
match self {
Self::Note => "Note",
Self::Tip => "Tip",
Self::Important => "Important",
Self::Warning => "Warning",
Self::Caution => "Caution",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Align {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct Text {
pub text: String,
pub marks: Vec<MarkSpan>,
}
impl From<&str> for Text {
fn from(text: &str) -> Self {
Self::plain(text)
}
}
impl From<String> for Text {
fn from(text: String) -> Self {
Self::plain(text)
}
}
impl Text {
pub fn plain(text: impl Into<String>) -> Self {
Self {
text: text.into(),
marks: Vec::new(),
}
}
pub fn link(url: &str) -> Self {
Self {
text: url.to_string(),
marks: vec![MarkSpan {
range: 0..url.len(),
mark: Mark::Link(url.to_string()),
}],
}
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub(crate) fn mention_around(&self, offset: usize) -> Option<Range<usize>> {
self.marks
.iter()
.find(|span| {
matches!(span.mark, Mark::Mention { .. })
&& span.range.start < offset
&& offset < span.range.end
})
.map(|span| span.range.clone())
}
pub(crate) fn alone(&self, ix: usize) -> bool {
let span = &self.marks[ix].range;
self.marks.iter().enumerate().all(|(other, mark)| {
other == ix || mark.range.end <= span.start || mark.range.start >= span.end
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MarkSpan {
pub range: Range<usize>,
pub mark: Mark,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Form {
Auto,
Chip,
Embed(Option<u32>),
}
impl Form {
pub(crate) fn title(self) -> Option<String> {
match self {
Self::Auto => None,
Self::Chip => Some("chip".to_owned()),
Self::Embed(None) => Some("embed".to_owned()),
Self::Embed(Some(height)) => Some(format!("embed {height}")),
}
}
pub(crate) fn from_title(title: &str) -> Option<Self> {
let (name, height) = crate::parse::split_size(title);
match (name, height) {
("chip", None) => Some(Self::Chip),
("embed", height) => Some(Self::Embed(height)),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Mark {
Bold,
Italic,
Strike,
Code,
Link(String),
Mention {
url: String,
form: Form,
},
Custom(String),
Image(String),
}