use rowan::GreenNodeBuilder;
use smallvec::SmallVec;
use crate::syntax::SyntaxKind;
use super::super::block_dispatcher::BlockContext;
use super::super::utils::container_stack::{Container, byte_index_at_column, leading_indent};
use super::blockquotes::strip_n_blockquote_markers;
#[derive(Copy, Clone, Debug)]
pub(crate) enum StripOp {
ListAdvance(u32),
BlockQuoteMarker,
ContentIndent(u32),
}
const INLINE_STRIP_OPS: usize = 8;
#[derive(Clone, Debug, Default)]
pub(crate) struct ContainerPrefix {
ops: SmallVec<[StripOp; INLINE_STRIP_OPS]>,
pub list_marker_consumed_on_line_0: bool,
}
impl ContainerPrefix {
pub fn from_stack(stack: &[Container], list_marker_consumed_on_line_0: bool) -> Self {
let mut ops: SmallVec<[StripOp; INLINE_STRIP_OPS]> = SmallVec::new();
let mut pending_list_advance: Option<u32> = None;
for c in stack {
match c {
Container::BlockQuote { .. } => {
if let Some(la) = pending_list_advance.take() {
ops.push(StripOp::ListAdvance(la));
}
ops.push(StripOp::BlockQuoteMarker);
}
Container::FootnoteDefinition { content_col, .. }
| Container::Definition { content_col, .. } => {
if let Some(la) = pending_list_advance.take() {
ops.push(StripOp::ListAdvance(la));
}
ops.push(StripOp::ContentIndent(*content_col as u32));
}
Container::ListItem { content_col, .. } => {
pending_list_advance = Some(*content_col as u32);
}
_ => {}
}
}
if let Some(la) = pending_list_advance {
ops.push(StripOp::ListAdvance(la));
}
Self {
ops,
list_marker_consumed_on_line_0,
}
}
pub fn from_ctx(ctx: &BlockContext) -> Self {
let list_content_col = ctx
.list_indent_info
.as_ref()
.map(|i| i.content_col)
.unwrap_or(0);
let bq_depth = ctx.blockquote_depth;
let content_indent = ctx.content_indent;
let mut ops: SmallVec<[StripOp; INLINE_STRIP_OPS]> = SmallVec::new();
if list_content_col > 0 {
ops.push(StripOp::ListAdvance(list_content_col as u32));
}
for _ in 0..bq_depth {
ops.push(StripOp::BlockQuoteMarker);
}
if content_indent > 0 {
ops.push(StripOp::ContentIndent(content_indent as u32));
}
Self {
ops,
list_marker_consumed_on_line_0: false,
}
}
#[allow(dead_code)]
pub fn bq_only(bq_depth: usize) -> Self {
let mut ops: SmallVec<[StripOp; INLINE_STRIP_OPS]> = SmallVec::new();
for _ in 0..bq_depth {
ops.push(StripOp::BlockQuoteMarker);
}
Self {
ops,
list_marker_consumed_on_line_0: false,
}
}
pub fn from_scalars(
bq_depth: usize,
list_content_col: usize,
bq_outer: bool,
content_indent: usize,
list_marker_consumed_on_line_0: bool,
) -> Self {
let mut ops: SmallVec<[StripOp; INLINE_STRIP_OPS]> = SmallVec::new();
if bq_outer {
for _ in 0..bq_depth {
ops.push(StripOp::BlockQuoteMarker);
}
if list_content_col > 0 {
ops.push(StripOp::ListAdvance(list_content_col as u32));
}
} else {
if list_content_col > 0 {
ops.push(StripOp::ListAdvance(list_content_col as u32));
}
for _ in 0..bq_depth {
ops.push(StripOp::BlockQuoteMarker);
}
}
if content_indent > 0 {
ops.push(StripOp::ContentIndent(content_indent as u32));
}
Self {
ops,
list_marker_consumed_on_line_0,
}
}
pub fn ops(&self) -> &[StripOp] {
&self.ops
}
pub fn bq_depth(&self) -> usize {
self.ops()
.iter()
.filter(|op| matches!(op, StripOp::BlockQuoteMarker))
.count()
}
pub fn list_content_col(&self) -> usize {
self.ops()
.iter()
.rev()
.find_map(|op| match op {
StripOp::ListAdvance(n) => Some(*n as usize),
_ => None,
})
.unwrap_or(0)
}
#[allow(dead_code)]
pub fn content_indent(&self) -> usize {
self.ops()
.iter()
.map(|op| match op {
StripOp::ContentIndent(n) => *n as usize,
_ => 0,
})
.sum()
}
#[cfg(test)]
pub fn from_ops(ops_slice: &[StripOp], list_marker_consumed_on_line_0: bool) -> Self {
Self {
ops: SmallVec::from_slice(ops_slice),
list_marker_consumed_on_line_0,
}
}
pub fn strip<'a>(&self, line: &'a str) -> &'a str {
let mut s = line;
for op in self.ops() {
s = apply_op(s, *op);
}
s
}
pub fn strip_line_0_for_emission<'a>(&self, line: &'a str) -> &'a str {
self.strip_line_0_with_indent_emit(line).0
}
#[allow(dead_code)]
pub fn strip_line_0_with_indent_emit<'a>(&self, line: &'a str) -> (&'a str, Option<&'a str>) {
let last_list_idx = self
.ops()
.iter()
.rposition(|op| matches!(op, StripOp::ListAdvance(_)));
let mut s = line;
let mut emit: Option<&'a str> = None;
for (i, op) in self.ops().iter().enumerate() {
match op {
StripOp::ListAdvance(n) => {
if Some(i) == last_list_idx && !self.list_marker_consumed_on_line_0 {
} else {
s = advance_columns(s, *n as usize);
}
}
StripOp::BlockQuoteMarker => {
s = strip_n_blockquote_markers(s, 1);
}
StripOp::ContentIndent(n) => {
let (next, e) = strip_content_indent(s, *n as usize);
s = next;
if e.is_some() {
emit = e;
}
}
}
}
(s, emit)
}
#[allow(dead_code)]
pub fn split<'a>(&self, line: &'a str) -> (&'a str, &'a str, &'a str) {
let mut s = line;
let mut list_consumed = 0usize;
let mut bq_consumed = 0usize;
let mut phase = 0; for op in self.ops() {
match op {
StripOp::ListAdvance(n) if phase == 0 => {
let after = advance_columns(s, *n as usize);
list_consumed = s.len() - after.len();
s = after;
phase = 1;
}
StripOp::BlockQuoteMarker if phase <= 1 => {
let after = strip_n_blockquote_markers(s, 1);
bq_consumed += s.len() - after.len();
s = after;
phase = 1;
}
_ => {
phase = 2;
break;
}
}
}
let _ = phase;
(
&line[..list_consumed],
&line[list_consumed..list_consumed + bq_consumed],
s,
)
}
}
fn apply_op(line: &str, op: StripOp) -> &str {
match op {
StripOp::ListAdvance(n) => advance_columns(line, n as usize),
StripOp::BlockQuoteMarker => strip_n_blockquote_markers(line, 1),
StripOp::ContentIndent(n) => strip_content_indent(line, n as usize).0,
}
}
pub(crate) fn strip_content_indent(line: &str, content_indent: usize) -> (&str, Option<&str>) {
if content_indent == 0 {
return (line, None);
}
let (indent_cols, _) = leading_indent(line);
if indent_cols >= content_indent {
let idx = byte_index_at_column(line, content_indent);
(&line[idx..], Some(&line[..idx]))
} else {
let trimmed_start = line.trim_start();
let ws_len = line.len() - trimmed_start.len();
if ws_len > 0 {
(trimmed_start, Some(&line[..ws_len]))
} else {
(line, None)
}
}
}
pub(crate) struct StrippedLines<'a, 'p> {
raw: &'a [&'a str],
base: usize,
dispatch: usize,
prefix: &'p ContainerPrefix,
}
#[allow(dead_code)]
impl<'a, 'p> StrippedLines<'a, 'p> {
pub fn new(raw: &'a [&'a str], base: usize, prefix: &'p ContainerPrefix) -> Self {
Self {
raw,
base,
dispatch: base,
prefix,
}
}
pub fn with_dispatch(
raw: &'a [&'a str],
base: usize,
dispatch: usize,
prefix: &'p ContainerPrefix,
) -> Self {
Self {
raw,
base,
dispatch,
prefix,
}
}
pub fn first(&self) -> &'a str {
self.prefix.strip_line_0_for_emission(self.raw[self.base])
}
#[allow(dead_code)]
pub fn get(&self, i: usize) -> &'a str {
let line = self.raw[self.base + i];
if i == 0 {
self.prefix.strip_line_0_for_emission(line)
} else {
self.prefix.strip(line)
}
}
#[allow(dead_code)]
pub fn first_unconditional(&self) -> &'a str {
self.prefix.strip(self.raw[self.base])
}
#[allow(dead_code)]
pub fn raw(&self) -> &'a [&'a str] {
self.raw
}
#[allow(dead_code)]
pub fn raw_at(&self, i: usize) -> &'a str {
self.raw[self.base + i]
}
#[allow(dead_code)]
pub fn pos(&self) -> usize {
self.base
}
#[allow(dead_code)]
pub fn prefix(&self) -> &ContainerPrefix {
self.prefix
}
pub fn dispatch_pos(&self) -> usize {
self.dispatch
}
pub fn strip_at(&self, i: usize) -> &'a str {
let line = self.raw[i];
if i == self.dispatch {
self.prefix.strip_line_0_for_emission(line)
} else {
self.prefix.strip(line)
}
}
pub fn strip_all(&self) -> Vec<&'a str> {
(0..self.raw.len()).map(|i| self.strip_at(i)).collect()
}
pub fn emit_prefix_at(&self, builder: &mut GreenNodeBuilder<'static>, i: usize) -> &'a str {
emit_content_line_prefixes(
builder,
self.raw[i],
self.prefix.bq_depth(),
self.prefix.list_content_col(),
bq_outer_of_list(self.prefix),
self.prefix.content_indent(),
)
}
pub fn dispatch_tail(&self) -> &'a str {
self.prefix
.strip_line_0_for_emission(self.raw[self.dispatch])
}
pub fn emit_or_dispatch_tail(
&self,
builder: &mut GreenNodeBuilder<'static>,
i: usize,
) -> &'a str {
if i == self.dispatch {
self.dispatch_tail()
} else {
self.emit_prefix_at(builder, i)
}
}
pub fn iter_from_base(&self) -> impl Iterator<Item = (usize, &'a str, &'a str)> + '_ {
(self.base..self.raw.len()).map(move |i| (i, self.raw[i], self.strip_at(i)))
}
}
pub(crate) fn strip_list_indent(line: &str, list_content_col: usize) -> &str {
if list_content_col == 0 {
return line;
}
let idx = byte_index_at_column(line, list_content_col);
&line[idx..]
}
pub(crate) fn bq_outer_of_list(prefix: &ContainerPrefix) -> bool {
for op in prefix.ops() {
match op {
StripOp::BlockQuoteMarker => return true,
StripOp::ListAdvance(_) => return false,
StripOp::ContentIndent(_) => {}
}
}
false
}
pub(crate) fn emit_blockquote_prefix_tokens(builder: &mut GreenNodeBuilder<'static>, prefix: &str) {
for ch in prefix.chars() {
if ch == '>' {
builder.token(SyntaxKind::BLOCK_QUOTE_MARKER.into(), ">");
} else {
let mut buf = [0u8; 4];
builder.token(SyntaxKind::WHITESPACE.into(), ch.encode_utf8(&mut buf));
}
}
}
pub(crate) fn emit_content_line_prefixes<'a>(
builder: &mut GreenNodeBuilder<'static>,
content_line: &'a str,
bq_depth: usize,
list_content_col: usize,
bq_outer: bool,
content_indent: usize,
) -> &'a str {
let mut s = content_line;
let mut pending_ws_start: Option<usize> = None;
let flush_ws = |builder: &mut GreenNodeBuilder<'static>,
pending: &mut Option<usize>,
current_offset: usize| {
if let Some(start) = *pending
&& current_offset > start
{
builder.token(
SyntaxKind::WHITESPACE.into(),
&content_line[start..current_offset],
);
*pending = None;
}
};
let strip_and_remember_list =
|s: &mut &'a str, pending: &mut Option<usize>, list_content_col: usize| {
if list_content_col == 0 {
return;
}
let stripped = strip_list_indent(s, list_content_col);
let consumed = s.len() - stripped.len();
if consumed > 0 {
let start = content_line.len() - s.len();
if pending.is_none() {
*pending = Some(start);
}
*s = stripped;
}
};
let strip_and_emit_bq = |builder: &mut GreenNodeBuilder<'static>,
s: &mut &'a str,
pending: &mut Option<usize>,
bq_depth: usize| {
if bq_depth == 0 {
return;
}
let current_offset = content_line.len() - s.len();
flush_ws(builder, pending, current_offset);
let stripped = strip_n_blockquote_markers(s, bq_depth);
let prefix_len = s.len() - stripped.len();
if prefix_len > 0 {
emit_blockquote_prefix_tokens(builder, &s[..prefix_len]);
}
*s = stripped;
};
if bq_outer {
strip_and_emit_bq(builder, &mut s, &mut pending_ws_start, bq_depth);
strip_and_remember_list(&mut s, &mut pending_ws_start, list_content_col);
} else {
strip_and_remember_list(&mut s, &mut pending_ws_start, list_content_col);
strip_and_emit_bq(builder, &mut s, &mut pending_ws_start, bq_depth);
}
if content_indent > 0 {
let indent_bytes = byte_index_at_column(s, content_indent);
if s.len() >= indent_bytes && indent_bytes > 0 {
let start = content_line.len() - s.len();
if pending_ws_start.is_none() {
pending_ws_start = Some(start);
}
s = &s[indent_bytes..];
}
}
let final_offset = content_line.len() - s.len();
flush_ws(builder, &mut pending_ws_start, final_offset);
s
}
pub(in crate::parser::blocks) fn advance_columns(line: &str, target: usize) -> &str {
if target == 0 {
return line;
}
let mut col = 0usize;
for (i, ch) in line.char_indices() {
if col >= target {
return &line[i..];
}
match ch {
'\n' | '\r' => return "",
'\t' => {
let next = (col / 4 + 1) * 4;
if next > target {
return &line[i..];
}
col = next;
}
_ => {
col += 1;
}
}
}
""
}
pub(crate) struct ContainerPrefixState {
pub prefixes: Vec<ContainerPrefixLine>,
pub line_idx: usize,
pub at_line_start: bool,
}
impl ContainerPrefixState {
pub fn new(prefixes: Vec<ContainerPrefixLine>) -> Option<Self> {
if prefixes.iter().all(ContainerPrefixLine::is_empty) {
None
} else {
Some(Self {
prefixes,
line_idx: 0,
at_line_start: true,
})
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ContainerPrefixLine {
pub list_indent: String,
pub bq_prefix: String,
}
impl ContainerPrefixLine {
pub fn is_empty(&self) -> bool {
self.list_indent.is_empty() && self.bq_prefix.is_empty()
}
pub fn bq_only(bq_prefix: String) -> Self {
Self {
list_indent: String::new(),
bq_prefix,
}
}
pub fn list_only(list_indent: String) -> Self {
Self {
list_indent,
bq_prefix: String::new(),
}
}
}
pub(crate) fn emit_container_prefix_tokens(
builder: &mut GreenNodeBuilder<'static>,
line: &ContainerPrefixLine,
) {
if !line.list_indent.is_empty() {
builder.token(SyntaxKind::WHITESPACE.into(), &line.list_indent);
}
for ch in line.bq_prefix.chars() {
if ch == '>' {
builder.token(SyntaxKind::BLOCK_QUOTE_MARKER.into(), ">");
} else {
let mut buf = [0u8; 4];
builder.token(SyntaxKind::WHITESPACE.into(), ch.encode_utf8(&mut buf));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_bq_only_matches_legacy() {
let p = ContainerPrefix::bq_only(1);
assert_eq!(p.strip("> foo"), "foo");
assert_eq!(p.strip(">> foo"), "> foo");
assert_eq!(p.strip("> "), "");
assert_eq!(p.strip("plain"), "plain");
}
#[test]
fn from_scalars_round_trips_marker_line_caller_combos() {
let check = |bq: usize, lcc: usize, ci: usize, lmc0: bool| {
let bq_outer = bq > 0;
let p = ContainerPrefix::from_scalars(bq, lcc, bq_outer, ci, lmc0);
assert_eq!(p.bq_depth(), bq, "bq_depth");
assert_eq!(p.list_content_col(), lcc, "list_content_col");
assert_eq!(p.content_indent(), ci, "content_indent");
assert_eq!(bq_outer_of_list(&p), bq_outer, "bq_outer_of_list");
assert_eq!(
p.list_marker_consumed_on_line_0, lmc0,
"list_marker_consumed_on_line_0"
);
};
check(0, 4, 0, true);
check(1, 4, 0, true);
check(0, 0, 4, false);
check(2, 0, 4, false);
}
#[test]
fn strip_list_marker_line() {
let p =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], false);
assert_eq!(p.strip("- > <div>"), "<div>");
}
#[test]
fn strip_list_continuation_line() {
let p =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], false);
assert_eq!(p.strip(" > hello"), "hello");
}
#[test]
fn strip_tab_indent_rounds_to_four() {
let p = ContainerPrefix::from_ops(&[StripOp::ListAdvance(4)], false);
assert_eq!(p.strip("\tfoo"), "foo");
}
#[test]
fn strip_short_line_yields_empty() {
let p = ContainerPrefix::from_ops(&[StripOp::ListAdvance(4)], false);
assert_eq!(p.strip(""), "");
assert_eq!(p.strip("\n"), "");
}
#[test]
fn advance_columns_lands_on_char_boundary_for_multibyte() {
assert_eq!(advance_columns("├── x", 2), "─ x");
assert_eq!(advance_columns("éxy", 1), "xy");
assert_eq!(advance_columns("黑x", 1), "x");
assert_eq!(advance_columns("😄.", 1), ".");
assert_eq!(advance_columns("├──", 5), "");
assert_eq!(advance_columns(" > hello", 2), "> hello");
assert_eq!(advance_columns("\tfoo", 4), "foo");
}
#[test]
fn stripped_lines_first_matches_strip_line_0_for_emission() {
let prefix =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], true);
let raw = ["- > <div>", " > foo"];
let lines = StrippedLines::new(&raw, 0, &prefix);
assert_eq!(lines.first(), "<div>");
assert_eq!(lines.first(), prefix.strip_line_0_for_emission(raw[0]));
}
#[test]
fn stripped_lines_first_skips_list_col_only_when_marker_consumed() {
let prefix_continuation = ContainerPrefix::from_ops(&[StripOp::ListAdvance(2)], false);
let raw = [" continuation"];
let lines = StrippedLines::new(&raw, 0, &prefix_continuation);
assert_eq!(lines.first(), " continuation");
assert_eq!(lines.first_unconditional(), "continuation");
let prefix_marker = ContainerPrefix::from_ops(&[StripOp::ListAdvance(2)], true);
let lines = StrippedLines::new(&raw, 0, &prefix_marker);
assert_eq!(lines.first(), "continuation");
}
#[test]
fn stripped_lines_get_uses_unconditional_strip_after_line_0() {
let prefix = ContainerPrefix::from_ops(&[StripOp::ListAdvance(2)], false);
let raw = [" foo", " bar", " baz"];
let lines = StrippedLines::new(&raw, 0, &prefix);
assert_eq!(lines.get(0), " foo");
assert_eq!(lines.get(1), "bar");
assert_eq!(lines.get(2), "baz");
}
#[test]
fn stripped_lines_raw_access_is_unstripped() {
let prefix =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], true);
let raw = ["- > foo", " > bar"];
let lines = StrippedLines::new(&raw, 0, &prefix);
assert_eq!(lines.raw_at(0), "- > foo");
assert_eq!(lines.raw_at(1), " > bar");
assert_eq!(lines.raw().len(), 2);
assert_eq!(lines.pos(), 0);
}
#[test]
fn stripped_lines_respects_base_offset() {
let prefix = ContainerPrefix::default();
let raw = ["pre", "first", "second"];
let lines = StrippedLines::new(&raw, 1, &prefix);
assert_eq!(lines.first(), "first");
assert_eq!(lines.get(0), "first");
assert_eq!(lines.get(1), "second");
assert_eq!(lines.pos(), 1);
assert_eq!(lines.raw_at(0), "first");
}
#[test]
fn strip_all_matches_hand_rolled_table_closure() {
let prefix =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], true);
let raw = ["- > | a |", " > |---|", " > | 1 |"];
let dispatch = 0;
let lines = StrippedLines::with_dispatch(&raw, 0, dispatch, &prefix);
let expected: Vec<&str> = (0..raw.len())
.map(|i| {
if i == dispatch {
prefix.strip_line_0_for_emission(raw[i])
} else {
prefix.strip(raw[i])
}
})
.collect();
assert_eq!(lines.strip_all(), expected);
}
#[test]
fn strip_at_honors_dispatch_offset_past_base() {
let prefix =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], true);
let raw = [": caption", "- > header", " > sep"];
let dispatch = 1;
let lines = StrippedLines::with_dispatch(&raw, 0, dispatch, &prefix);
assert_eq!(lines.strip_at(0), prefix.strip(raw[0]));
assert_eq!(lines.strip_at(2), prefix.strip(raw[2]));
assert_eq!(
lines.strip_at(dispatch),
prefix.strip_line_0_for_emission(raw[dispatch])
);
assert_eq!(
lines.dispatch_tail(),
prefix.strip_line_0_for_emission(raw[dispatch])
);
assert_eq!(lines.dispatch_pos(), dispatch);
}
#[test]
fn iter_from_base_yields_absolute_index_and_peek() {
let prefix = ContainerPrefix::default();
let raw = ["pre", "first", "second"];
let lines = StrippedLines::new(&raw, 1, &prefix);
let collected: Vec<(usize, &str, &str)> = lines.iter_from_base().collect();
assert_eq!(
collected,
vec![(1, "first", "first"), (2, "second", "second")]
);
}
#[test]
fn emit_prefix_at_returns_continuation_tail() {
let prefix =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], true);
let raw = ["- > header", " > hello"];
let lines = StrippedLines::new(&raw, 0, &prefix);
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::DOCUMENT.into());
let tail = lines.emit_prefix_at(&mut builder, 1);
builder.finish_node();
assert_eq!(tail, "hello");
assert_eq!(
tail,
emit_content_line_prefixes(
&mut GreenNodeBuilder::new(),
raw[1],
prefix.bq_depth(),
prefix.list_content_col(),
bq_outer_of_list(&prefix),
prefix.content_indent(),
)
);
}
#[test]
fn strip_content_indent_only() {
let p = ContainerPrefix::from_ops(&[StripOp::ContentIndent(4)], false);
assert_eq!(p.strip(" continuation"), "continuation");
assert_eq!(
p.strip_line_0_for_emission(" continuation"),
"continuation"
);
}
#[test]
fn strip_content_indent_inside_blockquote() {
let p = ContainerPrefix::from_ops(
&[StripOp::BlockQuoteMarker, StripOp::ContentIndent(4)],
false,
);
assert_eq!(p.strip("> continuation"), "continuation");
}
#[test]
fn strip_blockquote_inside_content_indent() {
let p = ContainerPrefix::from_ops(
&[StripOp::ContentIndent(4), StripOp::BlockQuoteMarker],
false,
);
assert_eq!(p.strip(" >quoted"), "quoted");
}
#[test]
fn strip_definition_above_list_above_bq() {
let p = ContainerPrefix::from_ops(
&[
StripOp::ContentIndent(4),
StripOp::ListAdvance(2),
StripOp::BlockQuoteMarker,
],
false,
);
assert_eq!(p.strip(" - > a"), "a");
}
#[test]
fn strip_content_indent_lazy_continuation() {
let p = ContainerPrefix::from_ops(&[StripOp::ContentIndent(4)], false);
let (stripped, emit) = p.strip_line_0_with_indent_emit(" short");
assert_eq!(stripped, "short");
assert_eq!(emit, Some(" "));
}
#[test]
fn strip_content_indent_with_list_marker_consumed() {
let p =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::ContentIndent(4)], true);
assert_eq!(
p.strip_line_0_for_emission("- footnote text"),
"footnote text"
);
}
#[test]
fn strip_content_indent_zero_is_passthrough() {
let p = ContainerPrefix::default();
assert_eq!(p.strip("no indent here"), "no indent here");
let (stripped, emit) = p.strip_line_0_with_indent_emit("no indent here");
assert_eq!(stripped, "no indent here");
assert_eq!(emit, None);
}
#[test]
fn from_stack_picks_only_innermost_list_item() {
use crate::parser::blocks::lists::ListMarker;
use crate::parser::utils::list_item_buffer::ListItemBuffer;
let stack = vec![
Container::List {
marker: ListMarker::Bullet('-'),
base_indent_cols: 0,
has_blank_between_items: false,
},
Container::ListItem {
content_col: 2,
buffer: ListItemBuffer::new(),
marker_only: false,
virtual_marker_space: false,
},
Container::List {
marker: ListMarker::Bullet('-'),
base_indent_cols: 2,
has_blank_between_items: false,
},
Container::ListItem {
content_col: 4,
buffer: ListItemBuffer::new(),
marker_only: false,
virtual_marker_space: false,
},
];
let p = ContainerPrefix::from_stack(&stack, false);
assert_eq!(p.strip("- - foo"), "foo");
}
#[test]
fn split_captures_consumed_bytes() {
let p =
ContainerPrefix::from_ops(&[StripOp::ListAdvance(2), StripOp::BlockQuoteMarker], false);
let (li, bq, inner) = p.split(" > hello");
assert_eq!(li, " ");
assert_eq!(bq, "> ");
assert_eq!(inner, "hello");
}
}