use super::sink::InlineSink;
use crate::syntax::SyntaxKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Role {
pub name_end: usize,
pub fence_len: usize,
pub content_len: usize,
pub total_len: usize,
}
fn is_role_name_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '_' | '-' | '+' | ':' | '.')
}
pub(crate) fn try_parse_role(text: &str) -> Option<Role> {
let bytes = text.as_bytes();
if bytes.first() != Some(&b'{') {
return None;
}
let close_brace = text.find('}')?;
let name_inner = &text[1..close_brace];
if name_inner.is_empty() || !name_inner.chars().all(is_role_name_char) {
return None;
}
let name_end = close_brace + 1;
let after = &text[name_end..];
let fence_len = after.bytes().take_while(|&b| b == b'`').count();
if fence_len == 0 {
return None;
}
let content_start = name_end + fence_len;
let rest = bytes.get(content_start..)?;
let mut i = 0;
while i < rest.len() {
if rest[i] == b'`' {
let run = rest[i..].iter().take_while(|&&b| b == b'`').count();
if run == fence_len {
return Some(Role {
name_end,
fence_len,
content_len: i,
total_len: content_start + i + fence_len,
});
}
i += run;
} else {
i += 1;
}
}
None
}
pub(crate) fn emit_role(builder: &mut impl InlineSink, text: &str, role: Role) {
builder.start_node(SyntaxKind::MYST_ROLE.into());
builder.token(SyntaxKind::MYST_ROLE_NAME.into(), &text[..role.name_end]);
let fence_end = role.name_end + role.fence_len;
builder.token(
SyntaxKind::MYST_ROLE_MARKER.into(),
&text[role.name_end..fence_end],
);
let content_end = fence_end + role.content_len;
builder.start_node(SyntaxKind::MYST_ROLE_CONTENT.into());
if role.content_len > 0 {
builder.token(SyntaxKind::TEXT.into(), &text[fence_end..content_end]);
}
builder.finish_node();
builder.token(
SyntaxKind::MYST_ROLE_MARKER.into(),
&text[content_end..role.total_len],
);
builder.finish_node(); }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_role() {
let r = try_parse_role("{math}`a^2`").unwrap();
assert_eq!(r.name_end, "{math}".len());
assert_eq!(r.fence_len, 1);
assert_eq!(r.content_len, "a^2".len());
assert_eq!(r.total_len, "{math}`a^2`".len());
}
#[test]
fn role_with_target() {
let src = "{ref}`Text <target>`";
let r = try_parse_role(src).unwrap();
assert_eq!(r.total_len, src.len());
let content = &src[r.name_end + r.fence_len..r.name_end + r.fence_len + r.content_len];
assert_eq!(content, "Text <target>");
}
#[test]
fn double_backtick_allows_inner_backtick() {
let src = "{x}``a`b``";
let r = try_parse_role(src).unwrap();
assert_eq!(r.fence_len, 2);
assert_eq!(r.content_len, "a`b".len());
assert_eq!(r.total_len, src.len());
}
#[test]
fn rejects_non_role() {
assert!(try_parse_role("{ref} text").is_none());
assert!(try_parse_role("{}`x`").is_none());
assert!(try_parse_role("ref}`x`").is_none());
assert!(try_parse_role("{math}`a^2").is_none());
}
#[test]
fn domain_qualified_name() {
let r = try_parse_role("{py:func}`open`").unwrap();
assert_eq!(r.name_end, "{py:func}".len());
}
}