use rowan::{TextRange, TextSize};
use crate::syntax::{SyntaxKind, SyntaxNode};
pub fn command_name(command: &SyntaxNode) -> Option<String> {
command
.children_with_tokens()
.filter_map(|element| element.into_token())
.find(|token| token.kind() == SyntaxKind::CONTROL_WORD)
.map(|token| token.text().trim_start_matches('\\').to_string())
}
pub fn nth_group_text(command: &SyntaxNode, n: usize) -> Option<String> {
let group = command
.children()
.filter(|child| child.kind() == SyntaxKind::GROUP)
.nth(n)?;
let mut text = String::new();
for element in group.children_with_tokens() {
match element {
rowan::NodeOrToken::Token(token) => match token.kind() {
SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
_ => text.push_str(token.text()),
},
rowan::NodeOrToken::Node(_) => return None,
}
}
Some(text)
}
pub fn nth_group_inner(command: &SyntaxNode, n: usize) -> Option<(TextRange, String)> {
let group = command
.children()
.filter(|child| child.kind() == SyntaxKind::GROUP)
.nth(n)?;
let mut text = String::new();
let mut start: Option<TextSize> = None;
let mut end: Option<TextSize> = None;
let mut after_l_brace = group.text_range().start();
for element in group.children_with_tokens() {
match element {
rowan::NodeOrToken::Token(token) => match token.kind() {
SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
SyntaxKind::R_BRACE => {}
_ => {
let range = token.text_range();
start.get_or_insert(range.start());
end = Some(range.end());
text.push_str(token.text());
}
},
rowan::NodeOrToken::Node(_) => return None,
}
}
let range = match (start, end) {
(Some(start), Some(end)) => TextRange::new(start, end),
_ => TextRange::empty(after_l_brace),
};
Some((range, text))
}
pub fn nth_group(command: &SyntaxNode, n: usize) -> Option<SyntaxNode> {
command
.children()
.filter(|child| child.kind() == SyntaxKind::GROUP)
.nth(n)
}
pub fn first_group_range(command: &SyntaxNode) -> TextRange {
match nth_group(command, 0) {
Some(group) => TextRange::new(command.text_range().start(), group.text_range().end()),
None => command.text_range(),
}
}
pub fn group_command_name(group: &SyntaxNode) -> Option<String> {
let command = group
.children()
.find(|child| child.kind() == SyntaxKind::COMMAND)?;
command_name(&command)
}
pub fn group_inner_source(group: &SyntaxNode) -> String {
let mut text = String::new();
for element in group.descendants_with_tokens() {
if let rowan::NodeOrToken::Token(token) = element {
text.push_str(token.text());
}
}
let inner = text.strip_prefix('{').unwrap_or(&text);
inner.strip_suffix('}').unwrap_or(inner).to_string()
}
pub fn environment_name(begin_or_end: &SyntaxNode) -> Option<String> {
let group = begin_or_end
.children()
.find(|child| child.kind() == SyntaxKind::NAME_GROUP)?;
let mut text = String::new();
for element in group.children_with_tokens() {
match element {
rowan::NodeOrToken::Token(token) => match token.kind() {
SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
_ => text.push_str(token.text()),
},
rowan::NodeOrToken::Node(_) => return None,
}
}
Some(text)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn command(src: &str) -> SyntaxNode {
SyntaxNode::new_root(parse(src).green)
.descendants()
.find(|node| node.kind() == SyntaxKind::COMMAND)
.expect("a COMMAND node")
}
#[test]
fn command_name_strips_backslash() {
assert_eq!(
command_name(&command("\\section{Hi}\n")).as_deref(),
Some("section")
);
}
#[test]
fn nth_group_text_reassembles_inner_tokens() {
assert_eq!(
nth_group_text(&command("\\label{sec:intro}\n"), 0).as_deref(),
Some("sec:intro")
);
}
#[test]
fn nth_group_inner_spans_only_the_key() {
let src = "\\label{sec:intro}\n";
let cmd = command(src);
let (range, text) = nth_group_inner(&cmd, 0).expect("an inner span");
assert_eq!(text, "sec:intro");
assert_eq!(&src[range], "sec:intro");
}
#[test]
fn nth_group_inner_empty_group_is_zero_width_after_brace() {
let cmd = command("\\label{}\n");
let (range, text) = nth_group_inner(&cmd, 0).expect("an inner span");
assert!(text.is_empty());
assert!(range.is_empty());
}
#[test]
fn nth_group_inner_none_for_nested_command() {
assert_eq!(nth_group_inner(&command("\\input{\\jobname}\n"), 0), None);
}
#[test]
fn nth_group_text_none_for_nested_command() {
assert_eq!(nth_group_text(&command("\\input{\\jobname}\n"), 0), None);
}
#[test]
fn nth_group_text_none_when_group_absent() {
assert_eq!(nth_group_text(&command("\\input\n"), 0), None);
}
#[test]
fn group_command_name_reads_braced_control_word() {
let cmd = command("\\newcommand{\\foo}{x}\n");
let name = nth_group(&cmd, 0).and_then(|g| group_command_name(&g));
assert_eq!(name.as_deref(), Some("foo"));
}
#[test]
fn group_command_name_none_for_plain_text() {
let cmd = command("\\newenvironment{thm}{a}{b}\n");
let name = nth_group(&cmd, 0).and_then(|g| group_command_name(&g));
assert_eq!(name, None);
}
#[test]
fn group_inner_source_keeps_nested_braces() {
let cmd = command("\\NewDocumentCommand{\\foo}{m O{d} m}{x}\n");
let spec = nth_group(&cmd, 1).map(|g| group_inner_source(&g));
assert_eq!(spec.as_deref(), Some("m O{d} m"));
assert_eq!(nth_group_text(&cmd, 1), None);
}
}