use crate::options::Extensions;
use crate::parser::utils::helpers::{strip_leading_spaces, strip_newline};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DirectiveOpen {
pub fence_char: u8,
pub fence_count: usize,
pub indent_len: usize,
pub name_len: usize,
pub is_verbatim: bool,
}
fn is_verbatim_directive(name: &str) -> bool {
matches!(
name,
"code" | "code-block" | "code-cell" | "math" | "toctree"
)
}
fn is_name_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '_' | '-' | '+' | ':' | '.')
}
pub(crate) fn try_parse_directive_open(content: &str, ext: &Extensions) -> Option<DirectiveOpen> {
if !ext.myst_directives {
return None;
}
let (line, _newline) = strip_newline(content);
let indent_len = line.bytes().take_while(|&b| b == b' ').count();
if indent_len > 3 {
return None;
}
let rest = &line[indent_len..];
let fence_char = *rest.as_bytes().first()?;
if !matches!(fence_char, b'`' | b'~' | b':') {
return None;
}
if fence_char == b':' && !ext.myst_colon_fence {
return None;
}
let fence_count = rest.bytes().take_while(|&b| b == fence_char).count();
if fence_count < 3 {
return None;
}
let after_fence = &rest[fence_count..];
if !after_fence.starts_with('{') {
return None;
}
let close_brace = after_fence.find('}')?;
let name_inner = &after_fence[1..close_brace];
if name_inner.is_empty() || !name_inner.chars().all(is_name_char) {
return None;
}
Some(DirectiveOpen {
fence_char,
fence_count,
indent_len,
name_len: close_brace + 1,
is_verbatim: is_verbatim_directive(name_inner),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DirectiveOption {
pub indent_len: usize,
pub name_len: usize,
}
fn is_option_name_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '_' | '-')
}
pub(crate) fn try_parse_directive_option(content: &str) -> Option<DirectiveOption> {
let (line, _newline) = strip_newline(content);
let indent_len = line.bytes().take_while(|&b| b == b' ').count();
if indent_len > 3 {
return None;
}
let rest = &line[indent_len..];
if !rest.starts_with(':') {
return None;
}
let after_colon = &rest[1..];
let name_len = after_colon
.chars()
.take_while(|&c| is_option_name_char(c))
.map(char::len_utf8)
.sum();
if name_len == 0 || !after_colon[name_len..].starts_with(':') {
return None;
}
Some(DirectiveOption {
indent_len,
name_len,
})
}
pub(crate) fn is_directive_closing_fence(content: &str, fence_char: u8, open_count: usize) -> bool {
let trimmed = strip_leading_spaces(content);
let count = trimmed.bytes().take_while(|&b| b == fence_char).count();
if count < open_count {
return false;
}
trimmed[count..].trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
fn ext_backtick() -> Extensions {
Extensions {
myst_directives: true,
..Extensions::for_flavor(crate::options::Flavor::Myst)
}
}
fn ext_colon() -> Extensions {
Extensions {
myst_directives: true,
myst_colon_fence: true,
..Extensions::for_flavor(crate::options::Flavor::Myst)
}
}
#[test]
fn basic_backtick_directive() {
let d = try_parse_directive_open("```{note}\n", &ext_backtick()).unwrap();
assert_eq!(d.fence_char, b'`');
assert_eq!(d.fence_count, 3);
assert_eq!(d.indent_len, 0);
assert_eq!(d.name_len, "{note}".len());
}
#[test]
fn verbatim_directive_names() {
for name in ["code", "code-block", "code-cell", "math"] {
let line = format!("```{{{name}}}\n");
let d = try_parse_directive_open(&line, &ext_backtick()).unwrap();
assert!(d.is_verbatim, "{name} should have a verbatim body");
}
let d = try_parse_directive_open("```{code-block} python\n", &ext_backtick()).unwrap();
assert!(d.is_verbatim);
}
#[test]
fn toctree_body_is_verbatim() {
let d = try_parse_directive_open("```{toctree}\n", &ext_backtick()).unwrap();
assert!(d.is_verbatim, "toctree body is a line-oriented entry list");
}
#[test]
fn prose_directive_names_are_not_verbatim() {
for name in ["note", "figure", "warning", "admonition"] {
let line = format!("```{{{name}}}\n");
let d = try_parse_directive_open(&line, &ext_backtick()).unwrap();
assert!(!d.is_verbatim, "{name} body is markdown, not verbatim");
}
}
#[test]
fn domain_qualified_name() {
let d = try_parse_directive_open("````{py:function}\n", &ext_backtick()).unwrap();
assert_eq!(d.fence_count, 4);
assert_eq!(d.name_len, "{py:function}".len());
}
#[test]
fn plain_code_fence_is_not_a_directive() {
assert!(try_parse_directive_open("```python\n", &ext_backtick()).is_none());
assert!(try_parse_directive_open("```\n", &ext_backtick()).is_none());
assert!(try_parse_directive_open("```{=html}\n", &ext_backtick()).is_none());
assert!(try_parse_directive_open("```{}\n", &ext_backtick()).is_none());
}
#[test]
fn colon_fence_gated_on_extension() {
assert!(try_parse_directive_open(":::{note}\n", &ext_backtick()).is_none());
let d = try_parse_directive_open(":::{note}\n", &ext_colon()).unwrap();
assert_eq!(d.fence_char, b':');
assert_eq!(d.fence_count, 3);
}
#[test]
fn gated_on_directives_extension() {
let off = Extensions::for_flavor(crate::options::Flavor::CommonMark);
assert!(try_parse_directive_open("```{note}\n", &off).is_none());
}
#[test]
fn indented_four_spaces_is_not_a_directive() {
assert!(try_parse_directive_open(" ```{note}\n", &ext_backtick()).is_none());
let d = try_parse_directive_open(" ```{note}\n", &ext_backtick()).unwrap();
assert_eq!(d.indent_len, 3);
}
#[test]
fn basic_option() {
let o = try_parse_directive_option(":alt: An image\n").unwrap();
assert_eq!(o.indent_len, 0);
assert_eq!(o.name_len, "alt".len());
}
#[test]
fn valueless_option() {
let o = try_parse_directive_option(":nofigs:\n").unwrap();
assert_eq!(o.name_len, "nofigs".len());
}
#[test]
fn hyphenated_option_key() {
let o = try_parse_directive_option(":number-lines: 1\n").unwrap();
assert_eq!(o.name_len, "number-lines".len());
}
#[test]
fn indented_option() {
let o = try_parse_directive_option(" :width: 200px\n").unwrap();
assert_eq!(o.indent_len, 2);
assert_eq!(o.name_len, "width".len());
assert!(try_parse_directive_option(" :width: 200px\n").is_none());
}
#[test]
fn not_an_option_no_closing_colon() {
assert!(try_parse_directive_option(":not an option just text\n").is_none());
assert!(try_parse_directive_option("def five(): return 5\n").is_none());
}
#[test]
fn colon_fence_is_not_option() {
assert!(try_parse_directive_option(":::\n").is_none());
assert!(try_parse_directive_option(":::{note}\n").is_none());
}
#[test]
fn closing_fence_matches_char_and_count() {
assert!(is_directive_closing_fence("```\n", b'`', 3));
assert!(is_directive_closing_fence("````\n", b'`', 3));
assert!(is_directive_closing_fence(" ``` \n", b'`', 3));
assert!(!is_directive_closing_fence("```\n", b'`', 4));
assert!(!is_directive_closing_fence("```python\n", b'`', 3));
assert!(!is_directive_closing_fence(":::\n", b'`', 3));
assert!(is_directive_closing_fence(":::\n", b':', 3));
}
}