use regex::Regex;
use std::sync::LazyLock;
#[inline]
fn position_in_spans(position: usize, spans: &[(usize, usize)]) -> bool {
for &(start, end) in spans {
if position < start {
return false;
}
if position < end {
return true;
}
}
false
}
#[inline]
fn find_regex_spans(line: &str, pattern: &Regex) -> Vec<(usize, usize)> {
pattern.find_iter(line).map(|m| (m.start(), m.end())).collect()
}
fn find_single_delim_spans(line: &str, delim: char, double_spans: &[(usize, usize)]) -> Vec<(usize, usize)> {
let mut spans = Vec::new();
let mut chars = line.char_indices().peekable();
let delim_len = delim.len_utf8();
while let Some((start_byte, ch)) = chars.next() {
if position_in_spans(start_byte, double_spans) {
continue;
}
if ch != delim {
continue;
}
if chars.peek().is_some_and(|(_, c)| *c == delim) {
chars.next();
continue;
}
let mut found_content = false;
let mut has_whitespace = false;
for (byte_pos, inner_ch) in chars.by_ref() {
if position_in_spans(byte_pos, double_spans) {
break;
}
if inner_ch == delim {
let is_double = chars.peek().is_some_and(|(_, c)| *c == delim);
if !is_double && found_content && !has_whitespace {
spans.push((start_byte, byte_pos + delim_len));
}
break;
}
found_content = true;
if inner_ch.is_whitespace() {
has_whitespace = true;
}
}
}
spans
}
static INLINE_HILITE_SHEBANG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^#!([a-zA-Z][a-zA-Z0-9_+-]*)").unwrap());
#[inline]
pub fn is_inline_hilite_content(content: &str) -> bool {
INLINE_HILITE_SHEBANG.is_match(content)
}
static KEYS_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\+\+([a-zA-Z0-9_-]+(?:\+[a-zA-Z0-9_-]+)*)\+\+").unwrap());
fn find_keys_spans(line: &str) -> Vec<(usize, usize)> {
if !line.contains("++") {
return Vec::new();
}
find_regex_spans(line, &KEYS_PATTERN)
}
fn is_in_keys(line: &str, position: usize) -> bool {
position_in_spans(position, &find_keys_spans(line))
}
static INSERT_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\^\^[^\^]+(?:\^[^\^]+)*\^\^").unwrap());
fn find_insert_spans(line: &str) -> Vec<(usize, usize)> {
if !line.contains("^^") {
return Vec::new();
}
find_regex_spans(line, &INSERT_PATTERN)
}
fn is_in_caret_markup(line: &str, position: usize) -> bool {
if !line.contains('^') {
return false;
}
let insert_spans = find_insert_spans(line);
if position_in_spans(position, &insert_spans) {
return true;
}
let super_spans = find_single_delim_spans(line, '^', &insert_spans);
position_in_spans(position, &super_spans)
}
static STRIKETHROUGH_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"~~[^~]+(?:~[^~]+)*~~").unwrap());
fn find_strikethrough_spans(line: &str) -> Vec<(usize, usize)> {
if !line.contains("~~") {
return Vec::new();
}
find_regex_spans(line, &STRIKETHROUGH_PATTERN)
}
fn is_in_tilde_markup(line: &str, position: usize) -> bool {
if !line.contains('~') {
return false;
}
let strike_spans = find_strikethrough_spans(line);
if position_in_spans(position, &strike_spans) {
return true;
}
let sub_spans = find_single_delim_spans(line, '~', &strike_spans);
position_in_spans(position, &sub_spans)
}
static MARK_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"==([^=]+)==").unwrap());
fn find_mark_spans(line: &str) -> Vec<(usize, usize)> {
if !line.contains("==") {
return Vec::new();
}
find_regex_spans(line, &MARK_PATTERN)
}
pub fn is_in_mark(line: &str, position: usize) -> bool {
position_in_spans(position, &find_mark_spans(line))
}
static SMART_SYMBOL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?:\(c\)|\(C\)|\(r\)|\(R\)|\(tm\)|\(TM\)|\(p\)|\.\.\.|-{2,3}|<->|<-|->|<=>|<=|=>|1/4|1/2|3/4|\+-|!=)")
.unwrap()
});
fn find_smart_symbol_spans(line: &str) -> Vec<(usize, usize)> {
if !line.contains('(')
&& !line.contains("...")
&& !line.contains("--")
&& !line.contains("->")
&& !line.contains("<-")
&& !line.contains("=>")
&& !line.contains("<=")
&& !line.contains("1/")
&& !line.contains("3/")
&& !line.contains("+-")
&& !line.contains("!=")
{
return Vec::new();
}
find_regex_spans(line, &SMART_SYMBOL_PATTERN)
}
fn is_in_smart_symbol(line: &str, position: usize) -> bool {
position_in_spans(position, &find_smart_symbol_spans(line))
}
pub fn is_in_pymdown_markup(line: &str, position: usize) -> bool {
is_in_keys(line, position)
|| is_in_caret_markup(line, position)
|| is_in_tilde_markup(line, position)
|| is_in_mark(line, position)
|| is_in_smart_symbol(line, position)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_in_spans_empty() {
assert!(!position_in_spans(0, &[]));
assert!(!position_in_spans(100, &[]));
}
#[test]
fn test_position_in_spans_early_exit() {
let spans = [(10, 20), (30, 40)];
assert!(!position_in_spans(5, &spans)); assert!(!position_in_spans(25, &spans)); assert!(!position_in_spans(50, &spans)); }
#[test]
fn test_position_in_spans_inside() {
let spans = [(10, 20), (30, 40)];
assert!(position_in_spans(10, &spans)); assert!(position_in_spans(15, &spans)); assert!(position_in_spans(19, &spans)); assert!(!position_in_spans(20, &spans)); assert!(position_in_spans(30, &spans)); }
#[test]
fn test_is_inline_hilite_content() {
assert!(is_inline_hilite_content("#!python print()"));
assert!(is_inline_hilite_content("#!js code"));
assert!(!is_inline_hilite_content("regular code"));
assert!(!is_inline_hilite_content(" #!python with space"));
}
#[test]
fn test_is_in_keys() {
let line = "Press ++ctrl++ here";
assert!(!is_in_keys(line, 0)); assert!(!is_in_keys(line, 5)); assert!(is_in_keys(line, 6)); assert!(is_in_keys(line, 10)); assert!(is_in_keys(line, 13)); assert!(!is_in_keys(line, 14)); }
#[test]
fn test_is_in_caret_markup() {
let line = "Text ^super^ here";
assert!(!is_in_caret_markup(line, 0));
assert!(is_in_caret_markup(line, 5)); assert!(is_in_caret_markup(line, 8)); assert!(!is_in_caret_markup(line, 13));
let line2 = "Text ^^insert^^ here";
assert!(is_in_caret_markup(line2, 5)); assert!(is_in_caret_markup(line2, 10)); }
#[test]
fn test_is_in_tilde_markup() {
let line = "Text ~sub~ here";
assert!(!is_in_tilde_markup(line, 0));
assert!(is_in_tilde_markup(line, 5)); assert!(is_in_tilde_markup(line, 7)); assert!(!is_in_tilde_markup(line, 12));
let line2 = "Text ~~strike~~ here";
assert!(is_in_tilde_markup(line2, 5)); assert!(is_in_tilde_markup(line2, 10)); }
#[test]
fn test_find_strikethrough_spans_triple_tilde() {
let line = "~~~a~~~";
let spans = find_strikethrough_spans(line);
assert_eq!(spans.len(), 1);
assert_eq!(&line[spans[0].0..spans[0].1], "~~a~~");
}
#[test]
fn test_find_strikethrough_spans_internal_single_tilde() {
let line = "~~a~b~~";
let spans = find_strikethrough_spans(line);
assert_eq!(spans.len(), 1);
assert_eq!(&line[spans[0].0..spans[0].1], "~~a~b~~");
let sub_spans = find_single_delim_spans(line, '~', &spans);
assert!(sub_spans.is_empty());
}
#[test]
fn test_is_in_mark() {
let line = "Text ==highlight== more";
assert!(!is_in_mark(line, 0));
assert!(is_in_mark(line, 5)); assert!(is_in_mark(line, 10)); assert!(!is_in_mark(line, 19)); }
#[test]
fn test_is_in_smart_symbol() {
let line = "Copyright (c) text";
assert!(!is_in_smart_symbol(line, 0));
assert!(is_in_smart_symbol(line, 10)); assert!(is_in_smart_symbol(line, 11)); assert!(is_in_smart_symbol(line, 12)); assert!(!is_in_smart_symbol(line, 14)); }
#[test]
fn test_is_in_pymdown_markup() {
assert!(is_in_pymdown_markup("++ctrl++", 2));
assert!(is_in_pymdown_markup("^super^", 1));
assert!(is_in_pymdown_markup("~sub~", 1));
assert!(is_in_pymdown_markup("~~strike~~", 2));
assert!(is_in_pymdown_markup("==mark==", 2));
assert!(is_in_pymdown_markup("(c)", 1));
assert!(!is_in_pymdown_markup("plain text", 5));
}
#[test]
fn test_empty_line() {
assert!(!is_in_pymdown_markup("", 0));
assert!(!is_in_mark("", 0));
assert!(!is_inline_hilite_content(""));
}
}