mod blocks;
use pulldown_cmark::{Event, Options, Parser};
use serde::{Deserialize, Serialize};
use crate::block::{BlockKind, BlockNode, EditablePolicy, HeadingNode};
use crate::fingerprint::{BlockFingerprint, BlockId};
use crate::island::{RawIsland, RawIslandEditPolicy, RawIslandType};
use crate::range::ByteRange;
use crate::trivia::SourceTrivia;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkdownDiagnostic {
pub message: String,
pub range: Option<ByteRange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkdownIndex {
pub document_revision: u64,
pub blocks: Vec<BlockNode>,
pub headings: Vec<HeadingNode>,
pub raw_islands: Vec<RawIsland>,
pub diagnostics: Vec<MarkdownDiagnostic>,
}
impl MarkdownIndex {
pub fn build(text: &str, document_revision: u64) -> Self {
let mut builder = Builder {
text,
revision: document_revision,
blocks: Vec::new(),
headings: Vec::new(),
diagnostics: Vec::new(),
island_types: Vec::new(),
};
let body_offset = match detect_front_matter(text) {
Some(end) => {
builder.push_front_matter(end);
end
}
None => 0,
};
let body = &text[body_offset..];
let events: Vec<(Event, std::ops::Range<usize>)> = Parser::new_ext(body, parse_options())
.into_offset_iter()
.collect();
blocks::consume_top_level(&mut builder, &events, body_offset);
let raw_islands = builder.collect_islands();
MarkdownIndex {
document_revision,
blocks: builder.blocks,
headings: builder.headings,
raw_islands,
diagnostics: builder.diagnostics,
}
}
pub fn resolve_block(&self, id: &BlockId) -> Option<&BlockNode> {
let candidate = self.blocks.get(id.ordinal as usize)?;
if candidate.kind == id.kind && candidate.block_id.fingerprint == id.fingerprint {
Some(candidate)
} else {
None
}
}
}
fn parse_options() -> Options {
Options::ENABLE_TABLES
| Options::ENABLE_TASKLISTS
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_MATH
}
pub(crate) fn detect_front_matter(text: &str) -> Option<usize> {
let (open, close_alt) = if text.starts_with("---\n") || text.starts_with("---\r\n") {
("---", Some("..."))
} else if text.starts_with("+++\n") || text.starts_with("+++\r\n") {
("+++", None)
} else {
return None;
};
let first_nl = text.find('\n')?;
let mut offset = first_nl + 1;
for line in text[offset..].split_inclusive('\n') {
let trimmed = line.trim_end_matches(['\n', '\r']);
let line_end = offset + line.len();
if trimmed == open || close_alt == Some(trimmed) {
return Some(line_end);
}
offset = line_end;
}
None
}
pub(crate) struct Builder<'a> {
pub text: &'a str,
pub revision: u64,
pub blocks: Vec<BlockNode>,
pub headings: Vec<HeadingNode>,
pub diagnostics: Vec<MarkdownDiagnostic>,
island_types: Vec<(u32, RawIslandType, String)>,
}
impl Builder<'_> {
fn push_front_matter(&mut self, end: usize) {
let trimmed_end = blocks::trim_trailing_newlines(self.text, end);
self.push_block(blocks::PendingBlock {
kind: BlockKind::FrontMatter,
start: 0,
end: trimmed_end,
content_range: None,
editable_policy: EditablePolicy::RawIslandOnly,
heading_level: None,
items: Vec::new(),
code_language: None,
fence_style: None,
list_marker_style: None,
island: Some((RawIslandType::FrontMatter, "front matter".to_string())),
});
}
pub(crate) fn push_block(&mut self, pending: blocks::PendingBlock) {
let ordinal = self.blocks.len() as u32;
let fingerprint = BlockFingerprint::compute(self.text, pending.start, pending.end);
let mut trivia = SourceTrivia::compute(self.text, pending.start, pending.end);
trivia.code_fence_style = pending.fence_style;
trivia.list_marker_style = pending.list_marker_style;
self.blocks.push(BlockNode {
block_id: BlockId {
revision_created: self.revision,
ordinal,
kind: pending.kind,
fingerprint,
},
kind: pending.kind,
source_range: ByteRange::new(pending.start, pending.end),
content_range: pending.content_range,
trivia,
editable_policy: pending.editable_policy,
heading_level: pending.heading_level,
items: pending.items,
code_language: pending.code_language,
});
if let Some((island_type, reason)) = pending.island {
self.blocks.last_mut().expect("just pushed").editable_policy =
EditablePolicy::RawIslandOnly;
self.diagnostics.push(MarkdownDiagnostic {
message: format!("raw island ({reason})"),
range: Some(ByteRange::new(pending.start, pending.end)),
});
self.island_types.push((ordinal, island_type, reason));
}
}
fn collect_islands(&self) -> Vec<RawIsland> {
self.island_types
.iter()
.map(|(ordinal, island_type, reason)| {
let block = &self.blocks[*ordinal as usize];
RawIsland {
block_id: block.block_id,
island_type: *island_type,
source_range: block.source_range,
edit_policy: RawIslandEditPolicy::RawEditable,
reason: reason.clone(),
}
})
.collect()
}
}