use crate::syntax::SyntaxKind;
use rowan::GreenNodeBuilder;
use super::core::parse_inline_text;
use crate::options::ParserOptions;
pub(crate) fn try_parse_inline_footnote(text: &str) -> Option<(usize, &str)> {
let bytes = text.as_bytes();
if bytes.len() < 3 || bytes[0] != b'^' || bytes[1] != b'[' {
return None;
}
let mut pos = 2;
let mut bracket_depth = 1;
while pos < bytes.len() {
match bytes[pos] {
b'\\' => {
pos += 2;
continue;
}
b'[' => {
bracket_depth += 1;
pos += 1;
}
b']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
let content = &text[2..pos];
return Some((pos + 1, content));
}
pos += 1;
}
_ => {
pos += 1;
}
}
}
None
}
pub(crate) fn emit_inline_footnote(
builder: &mut GreenNodeBuilder,
content: &str,
config: &ParserOptions,
) {
builder.start_node(SyntaxKind::INLINE_FOOTNOTE.into());
builder.token(SyntaxKind::INLINE_FOOTNOTE_START.into(), "^[");
parse_inline_text(builder, content, config, false);
builder.token(SyntaxKind::INLINE_FOOTNOTE_END.into(), "]");
builder.finish_node();
}
pub(crate) fn try_parse_footnote_reference(text: &str) -> Option<(usize, String)> {
let bytes = text.as_bytes();
if bytes.len() < 4 || bytes[0] != b'[' || bytes[1] != b'^' {
return None;
}
let mut pos = 2;
while pos < bytes.len() && bytes[pos] != b']' && bytes[pos] != b'\n' && bytes[pos] != b'\r' {
pos += 1;
}
if pos >= bytes.len() || bytes[pos] != b']' {
return None;
}
let id = &text[2..pos];
if id.is_empty() {
return None;
}
Some((pos + 1, id.to_string()))
}
pub(crate) fn emit_footnote_reference(builder: &mut GreenNodeBuilder, id: &str) {
builder.start_node(SyntaxKind::FOOTNOTE_REFERENCE.into());
builder.token(SyntaxKind::FOOTNOTE_LABEL_START.into(), "[^");
builder.token(SyntaxKind::FOOTNOTE_LABEL_ID.into(), id);
builder.token(SyntaxKind::FOOTNOTE_LABEL_END.into(), "]");
builder.finish_node();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_inline_footnote() {
let result = try_parse_inline_footnote("^[This is a note]");
assert_eq!(result, Some((17, "This is a note")));
}
#[test]
fn test_parse_inline_footnote_with_trailing_text() {
let result = try_parse_inline_footnote("^[Note text] and more");
assert_eq!(result, Some((12, "Note text")));
}
#[test]
fn test_parse_inline_footnote_with_brackets_inside() {
let result = try_parse_inline_footnote("^[Text with [nested] brackets]");
assert_eq!(result, Some((30, "Text with [nested] brackets")));
}
#[test]
fn test_parse_inline_footnote_with_escaped_bracket() {
let result = try_parse_inline_footnote("^[Text with \\] escaped]");
assert_eq!(result, Some((23, "Text with \\] escaped")));
}
#[test]
fn test_not_inline_footnote_no_opening() {
let result = try_parse_inline_footnote("[Not a footnote]");
assert_eq!(result, None);
}
#[test]
fn test_not_inline_footnote_no_closing() {
let result = try_parse_inline_footnote("^[No closing bracket");
assert_eq!(result, None);
}
#[test]
fn test_not_inline_footnote_just_caret() {
let result = try_parse_inline_footnote("^Not a footnote");
assert_eq!(result, None);
}
#[test]
fn test_empty_inline_footnote() {
let result = try_parse_inline_footnote("^[]");
assert_eq!(result, Some((3, "")));
}
#[test]
fn test_inline_footnote_multiline() {
let result = try_parse_inline_footnote("^[This is\na multiline\nnote]");
assert_eq!(result, Some((27, "This is\na multiline\nnote")));
}
#[test]
fn test_inline_footnote_with_code() {
let result = try_parse_inline_footnote("^[Contains `code` inside]");
assert_eq!(result, Some((25, "Contains `code` inside")));
}
#[test]
fn test_footnote_reference_with_crlf() {
let input = "[^foo\r\nbar]";
let result = try_parse_footnote_reference(input);
assert_eq!(
result, None,
"Should not parse footnote reference with CRLF in ID"
);
}
#[test]
fn test_footnote_reference_with_lf() {
let input = "[^foo\nbar]";
let result = try_parse_footnote_reference(input);
assert_eq!(
result, None,
"Should not parse footnote reference with LF in ID"
);
}
}