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 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,
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, 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(in crate::parser::blocks) fn advance_columns(line: &str, target: usize) -> &str {
if target == 0 {
return line;
}
let bytes = line.as_bytes();
let mut col = 0usize;
let mut i = 0usize;
while i < bytes.len() {
if col >= target {
return &line[i..];
}
match bytes[i] {
b'\n' | b'\r' => return "",
b'\t' => {
let next = (col / 4 + 1) * 4;
if next > target {
return &line[i..];
}
col = next;
i += 1;
}
_ => {
col += 1;
i += 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 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 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_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");
}
}