use crate::tui::syntax::BlockHighlighter;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::tui) struct CodeFence {
pub(super) marker: char,
pub(super) length: usize,
}
pub(super) struct MermaidOpeningFence {
pub(super) fence: CodeFence,
}
#[derive(Clone, Default)]
pub(in crate::tui) struct CodeFenceState {
pub(super) active: Option<CodeFence>,
pub(super) language: Option<String>,
pub(super) highlighter: Option<BlockHighlighter>,
}
impl CodeFenceState {
pub(in crate::tui) fn is_open(&self) -> bool {
self.active.is_some()
}
pub(in crate::tui) fn continue_open(
fence: CodeFence,
language: Option<String>,
highlighter: Option<BlockHighlighter>,
) -> Self {
Self {
active: Some(fence),
language,
highlighter,
}
}
pub(in crate::tui) fn take_highlighter(&mut self) -> Option<BlockHighlighter> {
self.highlighter.take()
}
pub(super) fn clear_open(&mut self) {
self.active = None;
self.language = None;
self.highlighter = None;
}
pub(super) fn open_fence(&mut self, fence: CodeFence, language: Option<String>) {
self.highlighter = language.as_deref().and_then(BlockHighlighter::for_language);
self.active = Some(fence);
self.language = language;
}
}
pub(in crate::tui) fn update_code_block_state(text: &str, state: &mut CodeFenceState) {
for line in text.lines() {
if state
.active
.is_some_and(|fence| is_closing_fence(line, fence))
{
state.clear_open();
} else if state.active.is_none() {
if let Some(fence) = parse_opening_fence(line) {
state.open_fence(fence, opening_fence_info_token(line));
}
} else if let Some(highlighter) = state.highlighter.as_mut() {
highlighter.advance_line(line);
}
}
}
pub(in crate::tui) fn parse_opening_fence(line: &str) -> Option<CodeFence> {
let indent = line.len() - line.trim_start_matches(' ').len();
if indent > 3 {
return None;
}
let rest = &line[indent..];
let marker = rest.chars().next()?;
if !matches!(marker, '`' | '~') {
return None;
}
let length = rest
.chars()
.take_while(|&character| character == marker)
.count();
if length < 3 {
return None;
}
let info = &rest[length..];
if marker == '`' && info.contains('`') {
return None;
}
Some(CodeFence { marker, length })
}
pub(in crate::tui) fn is_closing_fence(line: &str, opening: CodeFence) -> bool {
let indent = line.len() - line.trim_start_matches(' ').len();
if indent > 3 {
return false;
}
let rest = &line[indent..];
let length = rest
.chars()
.take_while(|&character| character == opening.marker)
.count();
length >= opening.length && rest[length..].chars().all(char::is_whitespace)
}
pub(in crate::tui) fn opening_fence_info_token(line: &str) -> Option<String> {
let fence = parse_opening_fence(line)?;
let indent = line.len() - line.trim_start_matches(' ').len();
let rest = &line[indent + fence.length..];
rest.split_whitespace().next().map(str::to_ascii_lowercase)
}
pub(super) fn mermaid_opening_fence(line: &str) -> Option<MermaidOpeningFence> {
let fence = parse_opening_fence(line)?;
(opening_fence_info_token(line).as_deref() == Some("mermaid"))
.then_some(MermaidOpeningFence { fence })
}