use std::ops::Range;
use jotdown::{Container, Event, Parser};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
Section,
Heading(u8),
List,
ListItem,
Blockquote,
CodeBlock,
Div,
Paragraph,
Table,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeNode {
pub range: Range<usize>,
pub kind: NodeKind,
pub children: Vec<TreeNode>,
}
fn node_kind_of(c: &Container) -> Option<NodeKind> {
Some(match c {
Container::Section { .. } => NodeKind::Section,
Container::Heading { level, .. } => NodeKind::Heading(*level as u8),
Container::List { .. } => NodeKind::List,
Container::ListItem | Container::TaskListItem { .. } => NodeKind::ListItem,
Container::Blockquote => NodeKind::Blockquote,
Container::CodeBlock { .. } => NodeKind::CodeBlock,
Container::Div { .. } => NodeKind::Div,
Container::Paragraph => NodeKind::Paragraph,
Container::Table => NodeKind::Table,
_ => return None,
})
}
pub fn container_tree(src: &str) -> Vec<TreeNode> {
let mut stack: Vec<(NodeKind, usize, Vec<TreeNode>)> = Vec::new();
let mut roots: Vec<TreeNode> = Vec::new();
for (event, range) in Parser::new(src).into_offset_iter() {
match event {
Event::Start(c, _) => {
if let Some(kind) = node_kind_of(&c) {
stack.push((kind, range.start, Vec::new()));
}
}
Event::End(c) => {
if node_kind_of(&c).is_some() {
if let Some((kind, start, children)) = stack.pop() {
let node = TreeNode {
range: start..range.end,
kind,
children,
};
match stack.last_mut() {
Some((_, _, siblings)) => siblings.push(node),
None => roots.push(node),
}
}
}
}
_ => {}
}
}
roots
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fold {
pub range: Range<usize>,
pub kind: NodeKind,
}
pub fn folds(src: &str) -> Vec<Fold> {
fn walk(nodes: &[TreeNode], src: &str, out: &mut Vec<Fold>) {
for n in nodes {
let foldable = matches!(
n.kind,
NodeKind::Section
| NodeKind::List
| NodeKind::Blockquote
| NodeKind::CodeBlock
| NodeKind::Div
);
if foldable && src[n.range.clone()].contains('\n') {
out.push(Fold {
range: n.range.clone(),
kind: n.kind,
});
}
walk(&n.children, src, out);
}
}
let mut out = Vec::new();
walk(&container_tree(src), src, &mut out);
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutlineItem {
pub range: Range<usize>,
pub level: u8,
pub text: String,
}
pub fn outline(src: &str) -> Vec<OutlineItem> {
let mut items = Vec::new();
let mut current: Option<(usize, u8, String)> = None;
for (event, range) in Parser::new(src).into_offset_iter() {
match event {
Event::Start(Container::Heading { level, .. }, _) => {
current = Some((range.start, level as u8, String::new()));
}
Event::Str(s) => {
if let Some((_, _, text)) = current.as_mut() {
text.push_str(s.as_ref());
}
}
Event::End(Container::Heading { .. }) => {
if let Some((start, level, text)) = current.take() {
items.push(OutlineItem {
range: start..range.end,
level,
text,
});
}
}
_ => {}
}
}
items
}
pub fn expand_selection(src: &str, selection: Range<usize>) -> Option<Range<usize>> {
fn collect(nodes: &[TreeNode], out: &mut Vec<Range<usize>>) {
for n in nodes {
out.push(n.range.clone());
collect(&n.children, out);
}
}
let mut ranges = Vec::new();
collect(&container_tree(src), &mut ranges);
ranges
.into_iter()
.filter(|r| r.start <= selection.start && selection.end <= r.end && *r != selection)
.min_by_key(|r| r.end - r.start)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "# Title\n\nIntro paragraph.\n\n## Section A\n\n- one\n- two\n\n> a quote\n\n```rust\nfn x() {}\n```\n";
#[test]
fn dump_tree() {
fn walk(nodes: &[TreeNode], src: &str, depth: usize) {
for n in nodes {
eprintln!(
"{:indent$}{:?} {:?} {:?}",
"",
n.kind,
n.range.clone(),
&src[n.range.clone()].replace('\n', "\\n"),
indent = depth * 2
);
walk(&n.children, src, depth + 1);
}
}
walk(&container_tree(SAMPLE), SAMPLE, 0);
}
#[test]
fn outline_lists_headings_with_levels() {
let items = outline(SAMPLE);
let pairs: Vec<_> = items.iter().map(|i| (i.level, i.text.as_str())).collect();
assert_eq!(pairs, vec![(1, "Title"), (2, "Section A")]);
}
#[test]
fn folds_cover_multiline_regions() {
let f = folds(SAMPLE);
assert!(
f.iter()
.any(|fold| fold.kind == NodeKind::CodeBlock && SAMPLE[fold.range.clone()].contains('\n')),
"expected a code-block fold, got {f:?}"
);
assert!(
f.iter().any(|fold| fold.kind == NodeKind::List),
"expected a list fold, got {f:?}"
);
}
#[test]
fn expand_selection_grows_to_the_enclosing_container() {
let at = SAMPLE.find("one").unwrap();
let caret = at..at;
let item = expand_selection(SAMPLE, caret.clone()).unwrap();
assert!(item.start <= at && at < item.end, "{item:?}");
let bigger = expand_selection(SAMPLE, item.clone()).unwrap();
assert!(
bigger.start <= item.start && item.end <= bigger.end && bigger != item,
"expand should grow: {item:?} -> {bigger:?}"
);
}
#[test]
fn empty_document_has_no_structure() {
assert!(container_tree("").is_empty());
assert!(outline("").is_empty());
assert!(folds("").is_empty());
}
}