use super::Preprocessor;
use crate::{DocumentAttributes, Options, grammar::verbatim_paragraph_style};
#[cfg(feature = "setext")]
pub(super) fn setext_enabled(options: &Options) -> bool {
options.setext
}
#[cfg(not(feature = "setext"))]
pub(super) fn setext_enabled(_options: &Options) -> bool {
false
}
fn is_line_comment(line: &str) -> bool {
line.starts_with("//") && !line.starts_with("///")
}
fn is_setext_underline(line: &str) -> bool {
let mut chars = line.chars();
match chars.next() {
Some(first) => {
crate::grammar::setext::char_to_level(first).is_some() && chars.all(|c| c == first)
}
None => false,
}
}
fn is_title_line(line: &str, setext: bool) -> bool {
let rest = line.trim_start_matches(['=', '#']);
if rest.len() < line.len() && rest.starts_with(' ') {
return true;
}
setext && is_setext_underline(line)
}
fn is_attaching_content(line: &str, setext: bool) -> bool {
!line.trim().is_empty()
&& !is_line_comment(line)
&& line.trim() != "+"
&& !is_title_line(line, setext)
}
fn delimiter_key(line: &str) -> Option<(u8, usize)> {
line.bytes().next().map(|byte| (byte, line.len()))
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum ParagraphContext {
#[default]
None,
Pending,
Active,
}
#[derive(Debug, Default, Clone)]
pub(super) struct BlockContext {
verbatim: Option<(u8, usize)>,
table: Option<(u8, usize)>,
block_comment: Option<(u8, usize)>,
compound: Vec<(u8, usize)>,
paragraph: ParagraphContext,
title_width: Option<usize>,
}
pub(super) struct CommentScanner {
blocks: BlockContext,
prev_attaches: bool,
setext: bool,
}
impl CommentScanner {
pub(super) fn new(setext: bool, blocks: BlockContext) -> Self {
Self {
blocks,
prev_attaches: false,
setext,
}
}
pub(super) fn at_verbatim_delimiter(&mut self, line: &str) -> bool {
if self.blocks.table.is_some()
|| self.blocks.paragraph == ParagraphContext::Active
|| self.at_setext_heading(line)
{
return false;
}
match Preprocessor::is_verbatim_delimiter(line).and_then(delimiter_key) {
Some(delimiter) if self.blocks.verbatim == Some(delimiter) => {
self.blocks.verbatim = None;
true
}
Some(delimiter) if self.blocks.verbatim.is_none() => {
self.blocks.verbatim = Some(delimiter);
true
}
Some(_) => true,
None => false,
}
}
pub(super) fn at_table_delimiter(&mut self, line: &str) -> bool {
if let Some(delimiter) = self.blocks.table {
if delimiter_key(line) == Some(delimiter)
&& line.bytes().skip(1).all(|byte| byte == b'=')
{
self.blocks.table = None;
return true;
}
return false;
}
if self.blocks.verbatim.is_some() || self.blocks.paragraph == ParagraphContext::Active {
return false;
}
let mut bytes = line.bytes();
if line.len() >= 4
&& matches!(bytes.next(), Some(b'|' | b'!' | b',' | b':'))
&& bytes.all(|byte| byte == b'=')
{
self.blocks.table = delimiter_key(line);
return true;
}
false
}
pub(super) fn preserves_comment_line(&mut self, line: &str) -> bool {
if let Some(delimiter) = self.blocks.block_comment {
if delimiter_key(line) == Some(delimiter) && line.bytes().all(|byte| byte == b'/') {
self.blocks.block_comment = None;
}
return true;
}
if self.blocks.verbatim.is_none()
&& self.blocks.table.is_none()
&& self.blocks.paragraph != ParagraphContext::Active
&& line.len() >= 4
&& line.bytes().all(|byte| byte == b'/')
{
self.blocks.block_comment = delimiter_key(line);
return true;
}
false
}
pub(super) fn block_context(&self) -> BlockContext {
self.blocks.clone()
}
pub(super) fn record_preprocessed(&mut self, text: &str, blocks: BlockContext) {
self.blocks = blocks;
if let Some(line) = text.lines().last() {
self.prev_attaches = is_attaching_content(line, self.setext);
}
}
pub(super) fn record_expansion(&mut self, text: &str, attributes: &DocumentAttributes<'_>) {
for line in text.lines() {
if !self.preserves_comment_line(line) && !self.at_table_delimiter(line) {
self.at_verbatim_delimiter(line);
}
self.record(line, attributes);
}
}
pub(super) fn drops(&self, line: &str) -> bool {
self.blocks.verbatim.is_none()
&& self.blocks.paragraph != ParagraphContext::Active
&& is_line_comment(line)
&& !super::tag::is_tag_directive_line(line)
&& (self.prev_attaches || self.blocks.paragraph == ParagraphContext::Pending)
}
pub(super) fn record(&mut self, line: &str, attributes: &DocumentAttributes<'_>) {
let heading = self.at_setext_heading(line);
let first_content = self.blocks.paragraph == ParagraphContext::Pending
|| self.blocks.paragraph == ParagraphContext::None && !self.prev_attaches;
self.blocks.title_width = (self.setext
&& first_content
&& !line.is_empty()
&& !line.starts_with(['[', ':', '.', '/', '=', '#'])
&& !crate::grammar::helpers::title_looks_like_description_list(line))
.then(|| line.trim().chars().count());
self.prev_attaches = is_attaching_content(line, self.setext);
if self.blocks.verbatim.is_some()
|| self.blocks.table.is_some()
|| self.blocks.block_comment.is_some()
{
self.blocks.paragraph = ParagraphContext::None;
return;
}
if heading {
self.blocks.paragraph = ParagraphContext::None;
self.blocks.title_width = None;
return;
}
let compound = delimiter_key(line).filter(|&(byte, length)| {
(matches!(byte, b'=' | b'*' | b'_') && length >= 4 || byte == b'-' && length == 2)
&& line.bytes().all(|character| character == byte)
});
if compound.is_some() && compound.as_ref() == self.blocks.compound.last() {
self.blocks.compound.pop();
self.blocks.paragraph = ParagraphContext::None;
} else if self.blocks.paragraph == ParagraphContext::Active {
if line.is_empty() || line == "+" {
self.blocks.paragraph = ParagraphContext::None;
}
} else if let Some(compound) = compound {
self.blocks.compound.push(compound);
self.blocks.paragraph = ParagraphContext::None;
} else if Preprocessor::is_verbatim_delimiter(line).is_some() || is_title_line(line, false)
{
self.blocks.paragraph = ParagraphContext::None;
} else if line.starts_with('[') && line.ends_with(']') {
if let Some(verbatim) = verbatim_paragraph_style(line, attributes) {
self.blocks.paragraph = if verbatim {
ParagraphContext::Pending
} else {
ParagraphContext::None
};
}
} else if self.blocks.paragraph == ParagraphContext::Pending
&& !line.is_empty()
&& !is_line_comment(line)
&& !super::attribute::is_declaration(line)
&& !line
.strip_prefix('.')
.is_some_and(|title| !title.is_empty() && !title.starts_with([' ', '\t', '.']))
{
self.blocks.paragraph = ParagraphContext::Active;
}
}
fn at_setext_heading(&self, line: &str) -> bool {
self.setext
&& self.blocks.verbatim.is_none()
&& self.blocks.table.is_none()
&& self.blocks.compound.is_empty()
&& (self.blocks.paragraph != ParagraphContext::Active || !line.starts_with('='))
&& is_setext_underline(line)
&& self
.blocks
.title_width
.is_some_and(|width| crate::grammar::setext::width_ok(width, line.chars().count()))
}
}