use markdown_it::{
parser::{
block::{BlockRule, BlockState},
inline::InlineRoot,
},
plugins::cmark::block::paragraph::{Paragraph, ParagraphScanner},
MarkdownIt, Node, NodeValue, Renderer,
};
pub fn add(md: &mut MarkdownIt) {
md.block
.add_rule::<DefinitionListScanner>()
.before::<ParagraphScanner>();
}
#[derive(Debug)]
pub struct DefinitionList;
impl NodeValue for DefinitionList {
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
fmt.cr();
fmt.open("dl", &node.attrs);
fmt.cr();
fmt.contents(&node.children);
fmt.cr();
fmt.close("dl");
fmt.cr();
}
}
#[derive(Debug)]
pub struct DefinitionTerm;
impl NodeValue for DefinitionTerm {
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
fmt.cr();
fmt.open("dt", &node.attrs);
fmt.contents(&node.children);
fmt.close("dt");
fmt.cr();
}
}
#[derive(Debug)]
pub struct DefinitionDescription;
impl NodeValue for DefinitionDescription {
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
fmt.cr();
fmt.open("dd", &node.attrs);
fmt.contents(&node.children);
fmt.close("dd");
fmt.cr();
}
}
pub struct DefinitionListScanner;
impl BlockRule for DefinitionListScanner {
fn check(state: &mut BlockState) -> Option<()> {
if state.line_indent(state.line) >= state.md.max_indent {
return None;
}
if !state.node.is::<DefinitionDescription>() {
return None;
}
check_for_description(state, state.line)?;
Some(())
}
fn run(state: &mut BlockState) -> Option<(Node, usize)> {
if state.line_indent(state.line) >= state.md.max_indent {
return None;
}
let start_line = state.line;
let mut next_line = state.line + 1;
if next_line >= state.line_max {
return None;
}
if state.is_empty(next_line) {
next_line += 1;
if next_line >= state.line_max {
return None;
}
}
if state.line_offsets[next_line].indent_nonspace < state.blk_indent as i32 {
return None;
}
let mut dd_first_nonspace = check_for_description(state, next_line)?;
let mut node_dl = Node::new(DefinitionList);
let mut dl_tight = true;
let mut dt_line = state.line;
let mut dd_line = next_line;
'terms: loop {
let mut prev_empty_end = false;
let mut node_dt = Node::new(DefinitionTerm);
node_dt.srcmap = state.get_map(dt_line, dt_line);
let (content, mapping) = state.get_lines(dt_line, dt_line + 1, state.blk_indent, false);
node_dt
.children
.push(Node::new(InlineRoot::new(content, mapping)));
node_dl.children.push(node_dt);
'descriptions: loop {
let mut dd_indent_nonspace = dd_first_nonspace as i32
- state.line_offsets[dd_line].first_nonspace as i32
+ state.line_offsets[dd_line].indent_nonspace;
while dd_first_nonspace < state.line_offsets[dd_line].line_end {
let c = state.src[dd_first_nonspace..].chars().next()?;
if c == ' ' {
dd_indent_nonspace += 1;
} else if c == '\t' {
dd_indent_nonspace += 4 - dd_indent_nonspace % 4;
} else {
break;
}
dd_first_nonspace += 1;
}
let cached_state = CachedState {
tight: state.tight,
blk_indent: state.blk_indent,
dd_indent_nonspace: state.line_offsets[dd_line].indent_nonspace,
dd_first_nonspace: state.line_offsets[dd_line].first_nonspace,
};
state.tight = true;
state.blk_indent = state.line_offsets[dd_line].indent_nonspace as usize + 2;
state.line_offsets[dd_line].indent_nonspace = dd_indent_nonspace;
state.line_offsets[dd_line].first_nonspace = dd_first_nonspace;
state.line = dd_line;
let cached_node =
std::mem::replace(&mut state.node, Node::new(DefinitionDescription));
state.md.block.tokenize(state);
let mut node_dd = std::mem::replace(&mut state.node, cached_node);
node_dd.srcmap = state.get_map(next_line, state.line - 1);
node_dl.children.push(node_dd);
if !state.tight || prev_empty_end {
dl_tight = false;
}
prev_empty_end = (state.line - dd_line) > 1 && state.is_empty(state.line - 1);
state.tight = cached_state.tight;
state.blk_indent = cached_state.blk_indent;
state.line_offsets[dd_line].indent_nonspace = cached_state.dd_indent_nonspace;
state.line_offsets[dd_line].first_nonspace = cached_state.dd_first_nonspace;
next_line = state.line;
if next_line >= state.line_max
|| state.line_offsets[next_line].indent_nonspace < state.blk_indent as i32
{
break 'terms;
}
match check_for_description(state, next_line) {
Some(pos) => dd_first_nonspace = pos,
None => break 'descriptions,
}
dd_line = next_line;
}
dt_line = next_line;
dd_line = dt_line + 1;
if next_line >= state.line_max
|| state.is_empty(dt_line)
|| state.line_offsets[dt_line].indent_nonspace < state.blk_indent as i32
|| dd_line >= state.line_max
{
break 'terms;
}
if state.is_empty(dd_line) {
dd_line += 1;
}
if dd_line >= state.line_max
|| state.line_offsets[dd_line].indent_nonspace < state.blk_indent as i32
{
break 'terms;
}
match check_for_description(state, dd_line) {
Some(pos) => dd_first_nonspace = pos,
None => break 'terms,
}
}
if dl_tight {
for child in node_dl.children.iter_mut() {
mark_tight_paragraphs(&mut child.children);
}
}
state.line = start_line;
Some((node_dl, next_line - start_line))
}
}
struct CachedState {
tight: bool,
blk_indent: usize,
dd_indent_nonspace: i32,
dd_first_nonspace: usize,
}
fn check_for_description(state: &mut BlockState, line: usize) -> Option<usize> {
let mut chars = state.get_line(line).chars();
let first_char = chars.next()?;
if first_char != ':' && first_char != '~' {
return None;
}
let second_char = chars.next()?;
if second_char != ' ' && second_char != '\t' {
return None;
}
loop {
match chars.next() {
Some(' ' | '\t') => {}
Some(_) => break,
None => return None,
}
}
Some(state.line_offsets[line].first_nonspace + 1)
}
fn mark_tight_paragraphs(nodes: &mut Vec<Node>) {
let mut idx = 0;
while idx < nodes.len() {
if nodes[idx].is::<Paragraph>() {
let children = std::mem::take(&mut nodes[idx].children);
let len = children.len();
nodes.splice(idx..idx + 1, children);
idx += len;
} else {
idx += 1;
}
}
}
#[cfg(test)]
mod tests {
use markdown_it::plugins::cmark;
use super::*;
#[test]
fn test_definition_list() {
let mut md = MarkdownIt::new();
cmark::add(&mut md);
add(&mut md);
println!("test\n : foo\n : bar\n");
let ast = md.parse("test\n : foo\n : bar\n");
println!("{}", ast.render());
}
}