use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use crate::syntax::{HighlightTag, StyleSpan, StyleValue, TextStyle};
pub(crate) fn highlight_inline_markdown(
line_text: &str,
cursor_offset: Option<usize>,
delimiter_tag: Option<HighlightTag>,
spans: &mut Vec<StyleSpan>,
) {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(line_text, options).into_offset_iter();
let mut active_strong = None;
let mut active_emphasis = None;
let mut active_strike = None;
let mut active_link = None;
for (event, range) in parser {
match event {
Event::Start(Tag::Strong) => {
active_strong = Some(range.start);
}
Event::End(TagEnd::Strong) => {
if let Some(start) = active_strong.take() {
let end = range.end.min(line_text.len());
if start < end {
if !cursor_inside_construct(cursor_offset, start, end)
&& end >= start + 4
&& let Some(delim_tag) = delimiter_tag
{
spans.push(StyleSpan::tag(start..start + 2, delim_tag));
spans.push(StyleSpan::tag(start + 2..end - 2, HighlightTag::Bold));
spans.push(StyleSpan::tag(end - 2..end, delim_tag));
} else {
spans.push(StyleSpan::tag(start..end, HighlightTag::Bold));
}
}
}
}
Event::Start(Tag::Emphasis) => {
active_emphasis = Some(range.start);
}
Event::End(TagEnd::Emphasis) => {
if let Some(start) = active_emphasis.take() {
let end = range.end.min(line_text.len());
if start < end {
if !cursor_inside_construct(cursor_offset, start, end)
&& end >= start + 2
&& let Some(delim_tag) = delimiter_tag
{
spans.push(StyleSpan::tag(start..start + 1, delim_tag));
spans.push(StyleSpan::tag(start + 1..end - 1, HighlightTag::Italic));
spans.push(StyleSpan::tag(end - 1..end, delim_tag));
} else {
spans.push(StyleSpan::tag(start..end, HighlightTag::Italic));
}
}
}
}
Event::Start(Tag::Strikethrough) => {
active_strike = Some(range.start);
}
Event::End(TagEnd::Strikethrough) => {
if let Some(start) = active_strike.take() {
let end = range.end.min(line_text.len());
if start < end {
if !cursor_inside_construct(cursor_offset, start, end)
&& end >= start + 4
&& let Some(delim_tag) = delimiter_tag
{
spans.push(StyleSpan::tag(start..start + 2, delim_tag));
spans.push(StyleSpan::direct(
start + 2..end - 2,
TextStyle {
strikethrough: true,
..Default::default()
},
));
spans.push(StyleSpan::tag(end - 2..end, delim_tag));
} else {
spans.push(StyleSpan::direct(
start..end,
TextStyle {
strikethrough: true,
..Default::default()
},
));
}
}
}
}
Event::Code(cow) => {
let end = (range.start + cow.len() + 2).min(line_text.len());
if !cursor_inside_construct(cursor_offset, range.start, end)
&& end >= range.start + 2
&& let Some(delim_tag) = delimiter_tag
{
spans.push(StyleSpan::tag(range.start..range.start + 1, delim_tag));
spans.push(StyleSpan::tag(range.start + 1..end - 1, HighlightTag::Code));
spans.push(StyleSpan::tag(end - 1..end, delim_tag));
} else {
spans.push(StyleSpan::tag(range.start..end, HighlightTag::Code));
}
}
Event::Start(Tag::Link { .. }) => {
active_link = Some(range.start);
}
Event::End(TagEnd::Link) => {
if let Some(start) = active_link.take() {
let end = range.end.min(line_text.len());
if start < end {
if !cursor_inside_construct(cursor_offset, start, end)
&& let Some(delim_tag) = delimiter_tag
{
if let Some(bracket_idx) = line_text[start..end].find("](") {
let label_start = start + 1;
let label_end = start + bracket_idx;
spans.push(StyleSpan::tag(start..label_start, delim_tag));
spans.push(StyleSpan::tag(
label_start..label_end,
HighlightTag::Link,
));
spans.push(StyleSpan::tag(label_end..end, delim_tag));
} else if let Some(bracket_idx) = line_text[start..end].find("][") {
let label_start = start + 1;
let label_end = start + bracket_idx;
spans.push(StyleSpan::tag(start..label_start, delim_tag));
spans.push(StyleSpan::tag(
label_start..label_end,
HighlightTag::Link,
));
spans.push(StyleSpan::tag(label_end..end, delim_tag));
} else if line_text[start..end].starts_with('<')
&& line_text[start..end].ends_with('>')
{
spans.push(StyleSpan::tag(start..start + 1, delim_tag));
spans.push(StyleSpan::tag(start + 1..end - 1, HighlightTag::Link));
spans.push(StyleSpan::tag(end - 1..end, delim_tag));
} else {
spans.push(StyleSpan::tag(start..end, HighlightTag::Link));
}
} else {
spans.push(StyleSpan::tag(start..end, HighlightTag::Link));
}
}
}
}
Event::TaskListMarker(checked) => {
let tag = if checked {
HighlightTag::String
} else {
HighlightTag::Comment
};
spans.push(StyleSpan::tag(range, tag));
}
_ => {}
}
}
highlight_mark_spans(line_text, cursor_offset, delimiter_tag, spans);
}
fn cursor_inside_construct(cursor_offset: Option<usize>, start: usize, end: usize) -> bool {
cursor_offset.is_some_and(|cursor| start <= cursor && cursor < end)
}
fn highlight_mark_spans(
line_text: &str,
cursor_offset: Option<usize>,
delimiter_tag: Option<HighlightTag>,
spans: &mut Vec<StyleSpan>,
) {
let bytes = line_text.as_bytes();
let mut search_from = 0;
while search_from + 1 < bytes.len() {
let Some(open) = find_mark_delimiter(bytes, search_from, true) else {
break;
};
let content_start = open + 2;
let Some(close) = find_mark_delimiter(bytes, content_start, false) else {
break;
};
if close == content_start || overlaps_reserved(spans, open, close + 2) {
search_from = open + 1;
continue;
}
let end = close + 2;
if !cursor_inside_construct(cursor_offset, open, end)
&& let Some(delim_tag) = delimiter_tag
{
spans.push(StyleSpan::tag(open..open + 2, delim_tag));
spans.push(StyleSpan::tag(
content_start..close,
HighlightTag::Highlight,
));
spans.push(StyleSpan::tag(close..end, delim_tag));
} else {
spans.push(StyleSpan::tag(open..end, HighlightTag::Highlight));
}
search_from = end;
}
}
fn find_mark_delimiter(bytes: &[u8], from: usize, opening: bool) -> Option<usize> {
let mut index = from;
while index + 1 < bytes.len() {
if bytes[index] == b'=' && bytes[index + 1] == b'=' {
let adjacent = if opening {
index + 2
} else {
index.saturating_sub(1)
};
let adjacent_ok = bytes
.get(adjacent)
.is_none_or(|byte| !byte.is_ascii_whitespace() && *byte != b'=');
if adjacent_ok {
return Some(index);
}
index += 1;
} else {
index += 1;
}
}
None
}
fn overlaps_reserved(spans: &[StyleSpan], start: usize, end: usize) -> bool {
spans.iter().any(|span| {
matches!(
span.style,
StyleValue::Tag(HighlightTag::Code | HighlightTag::Hidden | HighlightTag::Dimmed)
) && span.range.start < end
&& start < span.range.end
})
}