use regex::Regex;
use std::sync::LazyLock;
static INLINE_CODE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(`+)([^`]|[^`].*?[^`])(`+)").unwrap());
static INLINE_MATH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$[^$]*\$\$|\$[^$\n]*\$").unwrap());
static DOC_METADATA_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*\*?\s*\*\*(?:[^*\s][^*]*[^*\s]|[^*\s])\*\*\s*:").unwrap());
static BOLD_TEXT_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\*\*[^*\s][^*]*[^*\s]\*\*|\*\*[^*\s]\*\*").unwrap());
static QUICK_DOC_CHECK: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\*\s+\*").unwrap());
static QUICK_BOLD_CHECK: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*\*[^*\s]").unwrap());
static TEMPLATE_SHORTCODE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\*.*\*\}").unwrap());
#[derive(Debug, Clone, PartialEq)]
pub struct EmphasisMarker {
pub marker_type: u8, pub count: u8, pub start_pos: usize, }
impl EmphasisMarker {
#[inline]
pub fn end_pos(&self) -> usize {
self.start_pos + self.count as usize
}
#[inline]
pub fn as_char(&self) -> char {
self.marker_type as char
}
}
#[derive(Debug, Clone)]
pub struct EmphasisSpan {
pub opening: EmphasisMarker,
pub closing: EmphasisMarker,
pub content: String,
pub has_leading_space: bool,
pub has_trailing_space: bool,
}
pub(crate) struct EmphasisEdges {
pub(crate) opening: EmphasisMarker,
pub(crate) closing: EmphasisMarker,
pub(crate) has_leading_space: bool,
pub(crate) has_trailing_space: bool,
}
fn owned_span(
opening: &EmphasisMarker,
closing: &EmphasisMarker,
content: &str,
has_leading_space: bool,
has_trailing_space: bool,
) -> EmphasisSpan {
EmphasisSpan {
opening: opening.clone(),
closing: closing.clone(),
content: content.to_owned(),
has_leading_space,
has_trailing_space,
}
}
fn span_edges(
opening: &EmphasisMarker,
closing: &EmphasisMarker,
_content: &str,
has_leading_space: bool,
has_trailing_space: bool,
) -> EmphasisEdges {
EmphasisEdges {
opening: opening.clone(),
closing: closing.clone(),
has_leading_space,
has_trailing_space,
}
}
#[inline]
pub fn replace_inline_code(line: &str) -> String {
replace_inline_code_cow(line).into_owned()
}
pub(crate) fn replace_inline_code_cow(line: &str) -> std::borrow::Cow<'_, str> {
mask_matches(line, '`', &INLINE_CODE, b'X')
}
pub fn replace_inline_math(line: &str) -> String {
replace_inline_math_cow(line).into_owned()
}
pub(crate) fn replace_inline_math_cow(line: &str) -> std::borrow::Cow<'_, str> {
mask_matches(line, '$', &INLINE_MATH, b'M')
}
fn mask_matches<'a>(line: &'a str, signal: char, regex: &Regex, placeholder: u8) -> std::borrow::Cow<'a, str> {
if !line.contains(signal) {
return std::borrow::Cow::Borrowed(line);
}
let mut bytes = None;
for matched in regex.find_iter(line) {
bytes.get_or_insert_with(|| line.as_bytes().to_vec())[matched.range()].fill(placeholder);
}
match bytes {
Some(bytes) => std::borrow::Cow::Owned(String::from_utf8(bytes).expect("ASCII-only span masking")),
None => std::borrow::Cow::Borrowed(line),
}
}
#[inline]
pub fn find_emphasis_markers(line: &str) -> Vec<EmphasisMarker> {
let mut markers = Vec::new();
find_emphasis_markers_into(line, &mut markers);
markers
}
pub(crate) fn find_emphasis_markers_into(line: &str, markers: &mut Vec<EmphasisMarker>) {
if !line.contains('*') && !line.contains('_') {
return;
}
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
let byte = bytes[i];
if byte == b'*' || byte == b'_' {
let start_pos = i;
let mut count = 1u8;
while i + (count as usize) < bytes.len() && bytes[i + (count as usize)] == byte && count < 3 {
count += 1;
}
if count == 1 || count == 2 {
markers.push(EmphasisMarker {
marker_type: byte,
count,
start_pos,
});
}
i += count as usize;
} else {
i += 1;
}
}
}
pub fn find_single_emphasis_spans(line: &str, markers: &[EmphasisMarker]) -> Vec<EmphasisSpan> {
find_single_emphasis_spans_with(line, markers, owned_span)
}
pub(crate) fn find_single_emphasis_edges(line: &str, markers: &[EmphasisMarker]) -> Vec<EmphasisEdges> {
find_single_emphasis_spans_with(line, markers, span_edges)
}
fn find_single_emphasis_spans_with<T>(
line: &str,
markers: &[EmphasisMarker],
mut emit: impl FnMut(&EmphasisMarker, &EmphasisMarker, &str, bool, bool) -> T,
) -> Vec<T> {
if markers.len() < 2 {
return Vec::new();
}
let bytes = line.as_bytes();
let is_ws = |b: u8| b == b' ' || b == b'\t';
let can_open = |m: &EmphasisMarker| {
let after = m.end_pos();
after < bytes.len() && !is_ws(bytes[after])
};
let can_close = |m: &EmphasisMarker| m.start_pos > 0 && !is_ws(bytes[m.start_pos - 1]);
let mut spans = Vec::new();
let mut used_markers = vec![false; markers.len()];
for i in 0..markers.len() {
if used_markers[i] || markers[i].count != 1 || !can_open(&markers[i]) {
continue;
}
let opening = &markers[i];
for j in (i + 1)..markers.len() {
if used_markers[j] {
continue;
}
let closing = &markers[j];
if closing.marker_type == opening.marker_type && closing.count == 1 && can_close(closing) {
let content_start = opening.end_pos();
let content_end = closing.start_pos;
if content_end > content_start {
let content = &line[content_start..content_end];
if is_valid_emphasis_content_fast(content) && is_valid_emphasis_span_fast(line, opening, closing) {
let crosses_markers = markers[i + 1..j].iter().any(|marker| {
marker.marker_type == opening.marker_type
&& marker.count == 1
&& (can_open(marker) || can_close(marker))
});
if !crosses_markers {
let has_leading_space = content.starts_with(' ') || content.starts_with('\t');
let has_trailing_space = content.ends_with(' ') || content.ends_with('\t');
spans.push(emit(opening, closing, content, has_leading_space, has_trailing_space));
used_markers[i] = true;
used_markers[j] = true;
break;
}
}
}
}
}
}
spans
}
pub fn find_emphasis_spans(line: &str, markers: &[EmphasisMarker]) -> Vec<EmphasisSpan> {
find_emphasis_spans_with(line, markers, owned_span)
}
pub(crate) fn find_emphasis_edges(line: &str, markers: &[EmphasisMarker]) -> Vec<EmphasisEdges> {
find_emphasis_spans_with(line, markers, span_edges)
}
fn find_emphasis_spans_with<T>(
line: &str,
markers: &[EmphasisMarker],
mut emit: impl FnMut(&EmphasisMarker, &EmphasisMarker, &str, bool, bool) -> T,
) -> Vec<T> {
if markers.len() < 2 {
return Vec::new();
}
let mut spans = Vec::new();
let mut used_markers = vec![false; markers.len()];
for i in 0..markers.len() {
if used_markers[i] {
continue;
}
let opening = &markers[i];
for j in (i + 1)..markers.len() {
if used_markers[j] {
continue;
}
let closing = &markers[j];
if closing.marker_type == opening.marker_type && closing.count == opening.count {
let content_start = opening.end_pos();
let content_end = closing.start_pos;
if content_end > content_start {
let content = &line[content_start..content_end];
if is_valid_emphasis_content_fast(content) && is_valid_emphasis_span_fast(line, opening, closing) {
let crosses_markers = markers[i + 1..j]
.iter()
.any(|marker| marker.marker_type == opening.marker_type);
if !crosses_markers {
let has_leading_space = content.starts_with(' ') || content.starts_with('\t');
let has_trailing_space = content.ends_with(' ') || content.ends_with('\t');
spans.push(emit(opening, closing, content, has_leading_space, has_trailing_space));
used_markers[i] = true;
used_markers[j] = true;
break;
}
}
}
}
}
}
spans
}
pub fn find_valid_emphasis_ranges(line: &str, markers: &[EmphasisMarker]) -> Vec<(usize, usize)> {
if markers.len() < 2 {
return Vec::new();
}
let bytes = line.as_bytes();
let is_ws = |b: u8| b == b' ' || b == b'\t';
let can_open = |m: &EmphasisMarker| {
let after = m.end_pos();
after < bytes.len() && !is_ws(bytes[after])
};
let can_close = |m: &EmphasisMarker| m.start_pos > 0 && !is_ws(bytes[m.start_pos - 1]);
let mut ranges = Vec::new();
let mut used = vec![false; markers.len()];
for i in 0..markers.len() {
if used[i] || !can_open(&markers[i]) {
continue;
}
let opening = &markers[i];
for j in (i + 1)..markers.len() {
if used[j] {
continue;
}
let closing = &markers[j];
if closing.marker_type == opening.marker_type && closing.count == opening.count && can_close(closing) {
let content_start = opening.end_pos();
let content_end = closing.start_pos;
if content_end > content_start {
let content = &line[content_start..content_end];
if is_valid_emphasis_content_fast(content) && is_valid_emphasis_span_fast(line, opening, closing) {
let crosses = markers[i + 1..j]
.iter()
.any(|m| m.marker_type == opening.marker_type && (can_open(m) || can_close(m)));
if !crosses {
ranges.push((opening.start_pos, closing.end_pos()));
used[i] = true;
used[j] = true;
break;
}
}
}
}
}
}
ranges
}
#[inline]
fn is_valid_emphasis_span_fast(line: &str, opening: &EmphasisMarker, closing: &EmphasisMarker) -> bool {
let content_start = opening.end_pos();
let content_end = closing.start_pos;
if content_end <= content_start {
return false;
}
let content = &line[content_start..content_end];
if content.trim().is_empty() {
return false;
}
let bytes = line.as_bytes();
let valid_opening = opening.start_pos == 0
|| matches!(
bytes.get(opening.start_pos.saturating_sub(1)),
Some(&b' ')
| Some(&b'\t')
| Some(&b'(')
| Some(&b'[')
| Some(&b'{')
| Some(&b'"')
| Some(&b'\'')
| Some(&b'>')
);
let valid_closing = closing.end_pos() >= bytes.len()
|| matches!(
bytes.get(closing.end_pos()),
Some(&b' ')
| Some(&b'\t')
| Some(&b')')
| Some(&b']')
| Some(&b'}')
| Some(&b'"')
| Some(&b'\'')
| Some(&b'.')
| Some(&b',')
| Some(&b'!')
| Some(&b'?')
| Some(&b';')
| Some(&b':')
| Some(&b'<')
);
valid_opening && valid_closing && !content.contains('\n')
}
#[inline]
fn is_valid_emphasis_content_fast(content: &str) -> bool {
!content.trim().is_empty()
}
pub fn has_doc_patterns(line: &str) -> bool {
if line.contains("{*") && TEMPLATE_SHORTCODE_PATTERN.is_match(line) {
return true;
}
(QUICK_DOC_CHECK.is_match(line) || QUICK_BOLD_CHECK.is_match(line))
&& (DOC_METADATA_PATTERN.is_match(line) || BOLD_TEXT_PATTERN.is_match(line))
}
#[cfg(test)]
mod masking_contract_tests {
use super::*;
use proptest::prelude::*;
fn original_mask(source: &str, regex: &Regex, placeholder: &str) -> String {
let mut result = source.to_owned();
for captures in regex.captures_iter(source) {
if placeholder == "X" && (1..=3).any(|group| captures.get(group).is_none()) {
continue;
}
let matched = captures.get(0).unwrap();
result.replace_range(matched.range(), &placeholder.repeat(matched.len()));
}
result
}
fn assert_contract(source: &str) {
assert_eq!(
replace_inline_code_cow(source),
original_mask(source, &INLINE_CODE, "X")
);
assert_eq!(
replace_inline_math_cow(source),
original_mask(source, &INLINE_MATH, "M")
);
assert_eq!(replace_inline_code_cow(source).len(), source.len());
assert_eq!(replace_inline_math_cow(source).len(), source.len());
}
proptest! {
#[test]
fn source_masks_match_original_replacements(source in prop_oneof!["(?s).{0,128}", "[`$*_aé \\r\\n]{0,1024}"]) {
assert_contract(&source);
}
}
#[test]
fn literal_and_mismatched_delimiters_preserve_source() {
for source in [
"plain text",
"`unclosed",
"$$",
"é🦀 `é🦀` _text_",
"`` a ` b `` $a$ _x_",
"`a`` and ```b`` $x$$y$",
"`$` _x_ $y$",
"`é`\r\n$$ z $$",
"`a` _x_ `b`",
"$$ a $$ $$ b $$",
] {
assert_contract(source);
}
assert!(matches!(
replace_inline_code_cow("literal"),
std::borrow::Cow::Borrowed(_)
));
assert!(matches!(
replace_inline_code_cow("`unclosed"),
std::borrow::Cow::Borrowed(_)
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emphasis_marker_parsing() {
let markers = find_emphasis_markers("This has *single* and **double** emphasis");
assert_eq!(markers.len(), 4);
let markers = find_emphasis_markers("*start* and *end*");
assert_eq!(markers.len(), 4); }
#[test]
fn test_single_emphasis_span_detection() {
let markers = find_emphasis_markers("This has *valid* emphasis and **strong** too");
let spans = find_single_emphasis_spans("This has *valid* emphasis and **strong** too", &markers);
assert_eq!(spans.len(), 1); assert_eq!(spans[0].content, "valid");
assert!(!spans[0].has_leading_space);
assert!(!spans[0].has_trailing_space);
}
#[test]
fn test_emphasis_with_spaces() {
let markers = find_emphasis_markers("This has * invalid * emphasis");
let spans = find_emphasis_spans("This has * invalid * emphasis", &markers);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].content, " invalid ");
assert!(spans[0].has_leading_space);
assert!(spans[0].has_trailing_space);
}
#[test]
fn test_single_emphasis_rejects_whitespace_flanked_runs() {
let line = "foo * bar * baz";
let markers = find_emphasis_markers(line);
let spans = find_single_emphasis_spans(line, &markers);
assert!(
spans.is_empty(),
"whitespace-flanked run must not be a single-emphasis span: {spans:?}"
);
let md037_spans = find_emphasis_spans(line, &markers);
assert_eq!(
md037_spans.len(),
1,
"MD037's span finder must still detect the spaced run: {md037_spans:?}"
);
assert_eq!(md037_spans[0].content, " bar ");
}
#[test]
fn test_valid_emphasis_ranges() {
let ranges = |line: &str| {
let markers = find_emphasis_markers(line);
find_valid_emphasis_ranges(line, &markers)
};
assert_eq!(ranges("a *foo* b"), vec![(2, 7)]);
assert_eq!(ranges("a **foo** b"), vec![(2, 9)]);
assert_eq!(ranges("*foo * bar*"), vec![(0, 11)]);
assert_eq!(ranges("**foo ** bar**"), vec![(0, 14)]);
assert!(ranges("foo * bar * baz").is_empty());
assert!(ranges("** spaced **").is_empty());
assert!(ranges("* item only").is_empty());
}
#[test]
fn test_single_emphasis_spans_literal_marker_inside_emphasis() {
let line = "*foo * bar*";
let markers = find_emphasis_markers(line);
let spans = find_single_emphasis_spans(line, &markers);
assert_eq!(spans.len(), 1, "outer emphasis must be detected: {spans:?}");
assert_eq!(spans[0].content, "foo * bar");
let line = "*a *b*";
let markers = find_emphasis_markers(line);
let spans = find_single_emphasis_spans(line, &markers);
assert_eq!(spans.len(), 1, "only inner emphasis: {spans:?}");
assert_eq!(spans[0].content, "b");
}
#[test]
fn test_mixed_markers() {
let markers = find_emphasis_markers("This has *asterisk* and _underscore_ emphasis");
let spans = find_single_emphasis_spans("This has *asterisk* and _underscore_ emphasis", &markers);
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].opening.as_char(), '*');
assert_eq!(spans[1].opening.as_char(), '_');
}
#[test]
fn test_template_shortcode_detection() {
assert!(has_doc_patterns(
"{* ../../docs_src/cookie_param_models/tutorial001.py hl[9:12,16] *}"
));
assert!(has_doc_patterns(
"{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *}"
));
assert!(has_doc_patterns("{* file.py *}"));
assert!(has_doc_patterns("{* ../path/to/file.py ln[1-10] *}"));
assert!(!has_doc_patterns("This has *emphasis* text"));
assert!(!has_doc_patterns("This has * spaces * in emphasis"));
assert!(!has_doc_patterns("{* incomplete"));
}
#[test]
fn test_doc_pattern_rejects_spaced_bold_metadata() {
assert!(has_doc_patterns("**Key**: value"));
assert!(has_doc_patterns("**Name**: another value"));
assert!(has_doc_patterns("**X**: single char"));
assert!(has_doc_patterns("* **Key**: list item with bold key"));
assert!(!has_doc_patterns("** Key**: value"));
assert!(!has_doc_patterns("**Key **: value"));
assert!(!has_doc_patterns("** Key **: value"));
assert!(!has_doc_patterns(
"** Explicit Import**: Convert markdownlint configs to rumdl format:"
));
}
}