use crate::{
InlineMacro, InlineNode, Substitution,
grammar::{
ParserState,
inline_boundaries::check_constrained_opening_boundary,
state::{InlineRules, InlineUrlBoundary},
},
};
const EMAIL_LOCAL_PART_MAX: usize = 64;
pub(super) fn catalog_macro_allowed(state: &ParserState<'_>, start: usize) -> bool {
!state.inline_ctx.rules.contains(InlineRules::INDEX_CATALOG)
|| [
"image:",
"icon:",
"kbd:",
"btn:",
"menu:",
"stem:",
"latexmath:",
"asciimath:",
"pass:",
]
.iter()
.any(|prefix| state.input[start..].starts_with(prefix))
}
pub(super) fn catalog_escape_allowed(state: &ParserState<'_>, start: usize) -> bool {
if !state.inline_ctx.rules.contains(InlineRules::INDEX_CATALOG) {
return true;
}
let tail = state.input[start..].trim_start_matches('\\');
match tail.as_bytes().first() {
Some(b'*' | b'_' | b'#' | b'`' | b'^' | b'~') => state
.inline_ctx
.substitutions
.enabled(&Substitution::Quotes),
Some(b'{') => state
.inline_ctx
.substitutions
.enabled(&Substitution::Attributes),
_ => true,
}
}
pub(super) const fn is_plain_text_safe(b: u8) -> bool {
matches!(
b,
b'A'..=b'Z'
| b'0'..=b'9'
| b'c' | b'd' | b'e' | b'g' | b'j'
| b'n' | b'o' | b'q' | b'r' | b't'
| b'u' | b'v' | b'w' | b'y' | b'z'
| b' '
)
}
pub(super) fn has_at_sign_ahead(state: &ParserState, pos: usize) -> bool {
use crate::grammar::state::AtLookahead;
if let Some(cache) = state.next_at_sign_cache.get() {
match cache.first_at {
Some(at) if at >= pos => return at < pos + EMAIL_LOCAL_PART_MAX,
None if pos + EMAIL_LOCAL_PART_MAX <= cache.scanned_up_to => return false,
Some(_) | None => {}
}
}
let input = state.input.as_bytes();
let start = pos.min(input.len());
let scan_end = (pos + 1024).min(input.len());
let window = input.get(start..scan_end).unwrap_or(&[]);
let first_at = window
.iter()
.position(|&b| b == b'@')
.map(|off| start + off);
state.next_at_sign_cache.set(Some(AtLookahead {
scanned_up_to: scan_end,
first_at,
}));
first_at.is_some_and(|at| at < pos + EMAIL_LOCAL_PART_MAX)
}
pub(super) fn byte_came_from_attribute(state: &ParserState<'_>, position: usize) -> bool {
let range_index = state
.attribute_value_ranges
.partition_point(|range| range.end <= position);
state
.attribute_value_ranges
.get(range_index)
.is_some_and(|range| range.contains(&position))
}
pub(super) fn structural_token_allowed(
state: &ParserState<'_>,
substitution: &Substitution,
start: usize,
len: usize,
) -> bool {
!state
.inline_ctx
.substitutions
.precedes(substitution, &Substitution::Attributes)
|| (!(start..start + len).any(|position| byte_came_from_attribute(state, position))
&& !state.empty_attribute_offsets.iter()
.any(|offset| start < *offset && *offset < start + len))
}
pub(super) fn macro_token_allowed(state: &ParserState<'_>, start: usize, len: usize) -> bool {
structural_token_allowed(state, &Substitution::Macros, start, len)
}
pub(super) fn url_opening_allowed(
state: &ParserState<'_>,
position: usize,
bare: bool,
escaped: bool,
) -> bool {
let prefix = &state.input[..position];
let prefix = if escaped {
prefix.trim_end_matches('\\')
} else {
prefix
};
if state
.inline_ctx
.substitutions
.precedes(&Substitution::Macros, &Substitution::Attributes)
&& state
.late_attribute_sources
.iter()
.any(|(range, _)| range.end == prefix.len())
{
return false;
}
let previous = match state.last_url_boundary {
Some(InlineUrlBoundary::Formatted(end)) if end == prefix.len() => return true,
Some(InlineUrlBoundary::Protected(end)) if end == prefix.len() => return false,
Some(InlineUrlBoundary::Text(end, previous)) if end == prefix.len() => previous,
_ => prefix.chars().next_back(),
};
previous.is_none_or(|previous| {
match previous {
'"' | '\'' => !bare || escaped,
'<' | '>' | '(' | ')' | '[' | ']' | ';' => true,
_ => {
previous.is_whitespace()
&& !matches!(
previous,
'\u{b}' | '\u{c}' | '\u{85}' | '\u{2028}' | '\u{2029}'
)
}
}
})
}
pub(super) fn record_url_boundary(
state: &mut ParserState<'_>,
node: &InlineNode<'_>,
end: usize,
substitution: &Substitution,
) {
let formatted = matches!(
node,
InlineNode::BoldText(_)
| InlineNode::ItalicText(_)
| InlineNode::MonospaceText(_)
| InlineNode::HighlightText(_)
| InlineNode::SubscriptText(_)
| InlineNode::SuperscriptText(_)
) && state
.inline_ctx
.substitutions
.precedes(substitution, &Substitution::Macros);
state.last_url_boundary = if formatted {
Some(InlineUrlBoundary::Formatted(end))
} else if matches!(node, InlineNode::Macro(InlineMacro::Pass(_))) {
Some(InlineUrlBoundary::Protected(end))
} else if *substitution == Substitution::Attributes {
let text = if let InlineNode::PlainText(text) = node {
Some(text.content)
} else if let InlineNode::RawText(text) = node {
Some(text.content)
} else {
None
};
text.map(|text| InlineUrlBoundary::Text(end, text.chars().next_back()))
} else {
None
};
}
pub(super) fn index_content_present(state: &ParserState<'_>, start: usize, text: &str) -> bool {
!text.is_empty()
|| (state
.inline_ctx
.substitutions
.precedes(&Substitution::Macros, &Substitution::Attributes)
&& state
.late_attribute_sources
.iter()
.any(|(range, _)| range.start == start && range.is_empty()))
}
pub(super) fn has_inline_line_break_prefix(state: &ParserState<'_>, span_start: usize) -> bool {
let absolute_pos = span_start + state.inline_ctx.offset;
let preceded_by_content_or_line_end = absolute_pos > 0
&& state
.input
.as_bytes()
.get(absolute_pos.saturating_sub(1))
.is_some_and(|&byte| !byte.is_ascii_whitespace() || matches!(byte, b'\n' | b'\r'));
preceded_by_content_or_line_end
|| state
.empty_attribute_offsets
.binary_search(&span_start)
.is_ok()
}
pub(super) fn code_followed_by_attribute(state: &ParserState<'_>, position: usize) -> bool {
state
.inline_ctx
.substitutions
.precedes(&Substitution::Quotes, &Substitution::Attributes)
&& state
.late_attribute_sources
.iter()
.any(|(range, _)| range.start == position)
}
pub(super) fn check_code_opening_boundary(state: &ParserState<'_>, position: usize) -> bool {
if state
.inline_ctx
.substitutions
.precedes(&Substitution::Quotes, &Substitution::Attributes)
&& state
.late_attribute_sources
.iter()
.any(|(range, _)| range.end == position)
{
return false;
}
check_constrained_opening_boundary(
position,
state.input.as_bytes(),
state.outer_constrained_delimiter,
b'`',
)
}