use crate::{
MarkdownFenceClosing,
MarkdownFencePolicy,
};
#[derive(Debug, Clone, Copy)]
pub(super) struct MarkdownFence {
marker: u8,
marker_len: usize,
marker_end: usize,
}
impl MarkdownFence {
fn parse_opening(input: &str) -> Option<Self> {
let indent_len = input.bytes().take_while(|byte| *byte == b' ').count();
if indent_len > 3 {
return None;
}
let marker = *input.as_bytes().get(indent_len)?;
if marker != b'`' && marker != b'~' {
return None;
}
let marker_len = input[indent_len..]
.bytes()
.take_while(|byte| *byte == marker)
.count();
(marker_len >= 3).then_some(Self {
marker,
marker_len,
marker_end: indent_len + marker_len,
})
}
#[must_use]
pub(super) fn strip_outer<'a>(
input: &'a str,
policy: &MarkdownFencePolicy,
) -> &'a str {
let (json_only, closing) = match policy {
MarkdownFencePolicy::Disabled => return input,
MarkdownFencePolicy::Any { closing } => (false, *closing),
MarkdownFencePolicy::JsonOnly { closing } => (true, *closing),
};
let Some(opening_fence) = Self::parse_opening(input) else {
return input;
};
let Some((line_end, content_start)) = Self::first_line_break(input)
else {
return input;
};
let info_string = input[opening_fence.marker_end..line_end].trim();
if json_only && !Self::is_json_info_string(info_string) {
return input;
}
let content = &input[content_start..];
if let Some(without_close) = opening_fence.strip_closing(content) {
return without_close;
}
if closing == MarkdownFenceClosing::Required {
input
} else {
content
}
}
fn first_line_break(input: &str) -> Option<(usize, usize)> {
let bytes = input.as_bytes();
let line_end = bytes
.iter()
.position(|byte| matches!(*byte, b'\n' | b'\r'))?;
let next_line_start = if bytes[line_end] == b'\r'
&& bytes.get(line_end + 1) == Some(&b'\n')
{
line_end + 2
} else {
line_end + 1
};
Some((line_end, next_line_start))
}
#[inline]
#[must_use]
fn is_json_info_string(info_string: &str) -> bool {
let language = info_string.split_whitespace().next().unwrap_or("");
language.is_empty()
|| language.eq_ignore_ascii_case("json")
|| language.eq_ignore_ascii_case("jsonc")
}
fn strip_closing<'a>(&self, content: &'a str) -> Option<&'a str> {
let trimmed_end = content.trim_end_matches([' ', '\t', '\n', '\r']);
let closing_line_start = trimmed_end
.bytes()
.rposition(|byte| matches!(byte, b'\n' | b'\r'))
.map_or(0, |index| index + 1);
let closing_line = &trimmed_end[closing_line_start..];
let indent_len = closing_line
.bytes()
.take_while(|byte| *byte == b' ')
.count();
if indent_len > 3 {
return None;
}
let marker_line = &closing_line[indent_len..];
let closing_len = self.matching_marker_len(marker_line)?;
if closing_len == marker_line.len() && closing_len >= self.marker_len {
Some(&content[..closing_line_start])
} else {
None
}
}
fn matching_marker_len(&self, line: &str) -> Option<usize> {
let count =
line.bytes().take_while(|byte| *byte == self.marker).count();
(count >= 3).then_some(count)
}
}