pub mod nodes;
pub mod tokens;
pub use nodes::{Begin, Command, End, Environment, Group, NameGroup, Optional};
pub use tokens::ControlWord;
use rowan::{NodeOrToken, TextRange};
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
pub trait AstNode {
fn can_cast(kind: SyntaxKind) -> bool
where
Self: Sized;
fn cast(syntax: SyntaxNode) -> Option<Self>
where
Self: Sized;
fn syntax(&self) -> &SyntaxNode;
}
pub trait AstToken {
fn can_cast(kind: SyntaxKind) -> bool
where
Self: Sized;
fn cast(syntax: SyntaxToken) -> Option<Self>
where
Self: Sized;
fn syntax(&self) -> &SyntaxToken;
fn text(&self) -> &str {
self.syntax().text()
}
}
pub fn child<N: AstNode>(parent: &SyntaxNode) -> Option<N> {
parent.children().find_map(N::cast)
}
pub fn children<N: AstNode>(parent: &SyntaxNode) -> impl Iterator<Item = N> {
parent.children().filter_map(N::cast)
}
pub fn child_token<T: AstToken>(parent: &SyntaxNode) -> Option<T> {
parent
.children_with_tokens()
.filter_map(NodeOrToken::into_token)
.find_map(T::cast)
}
pub fn command_name(command: &SyntaxNode) -> Option<String> {
child_token::<ControlWord>(command).map(|cw| cw.name())
}
pub fn control_word_range(command: &SyntaxNode) -> Option<TextRange> {
child_token::<ControlWord>(command).map(|cw| cw.range())
}
pub fn nth_group_text(command: &SyntaxNode, n: usize) -> Option<String> {
children::<Group>(command)
.nth(n)
.and_then(|g| g.inner_text())
}
pub fn nth_group_inner(command: &SyntaxNode, n: usize) -> Option<(TextRange, String)> {
children::<Group>(command).nth(n).and_then(|g| g.inner())
}
pub fn nth_group(command: &SyntaxNode, n: usize) -> Option<SyntaxNode> {
children::<Group>(command)
.nth(n)
.map(|g| g.syntax().clone())
}
pub fn first_group_range(command: &SyntaxNode) -> TextRange {
match children::<Group>(command).next() {
Some(group) => TextRange::new(
command.text_range().start(),
group.syntax().text_range().end(),
),
None => command.text_range(),
}
}
pub fn group_command_name(group: &SyntaxNode) -> Option<String> {
child::<Command>(group).and_then(|c| c.name())
}
pub fn group_inner_source(group: &SyntaxNode) -> String {
nodes::inner_source_of(group)
}
pub fn environment_name(begin_or_end: &SyntaxNode) -> Option<String> {
child::<NameGroup>(begin_or_end).and_then(|g| g.text())
}
pub fn environment_name_range(begin_or_end: &SyntaxNode) -> Option<TextRange> {
child::<NameGroup>(begin_or_end).and_then(|g| g.range())
}
#[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")
}
fn node(src: &str, kind: SyntaxKind) -> SyntaxNode {
SyntaxNode::new_root(parse(src).green)
.descendants()
.find(|n| n.kind() == kind)
.expect("a matching 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);
}
#[test]
fn environment_name_range_spans_only_the_name() {
let src = "\\begin{equation}\nx\n\\end{equation}\n";
let begin = node(src, SyntaxKind::BEGIN);
let range = environment_name_range(&begin).expect("a name span");
assert_eq!(&src[range], "equation");
let end = node(src, SyntaxKind::END);
let range = environment_name_range(&end).expect("a name span");
assert_eq!(&src[range], "equation");
}
#[test]
fn environment_name_range_none_for_empty_name() {
assert_eq!(
environment_name_range(&node("\\begin{}\n\\end{}\n", SyntaxKind::BEGIN)),
None
);
}
#[test]
fn cast_is_kind_exact() {
let cmd = command("\\section{Hi}\n");
assert!(Command::cast(cmd.clone()).is_some());
assert!(Group::cast(cmd.clone()).is_none());
let group = nth_group(&cmd, 0).unwrap();
assert!(Group::cast(group.clone()).is_some());
assert!(Command::cast(group).is_none());
}
#[test]
fn typed_nth_group_is_a_group_node() {
let cmd = Command::cast(command("\\label{k}\n")).unwrap();
let group = cmd.nth_group(0).unwrap();
assert_eq!(group.syntax().kind(), SyntaxKind::GROUP);
}
#[test]
fn optionals_do_not_shift_group_indexing() {
let cmd = Command::cast(command("\\cmd[o]{a}\n")).unwrap();
assert_eq!(cmd.nth_group_text(0).as_deref(), Some("a"));
assert_eq!(cmd.optionals().count(), 1);
}
#[test]
fn first_group_range_stops_at_first_group() {
let src = "\\label{a}\n{b}\n";
let cmd = Command::cast(command(src)).unwrap();
assert_eq!(&src[cmd.first_group_range()], "\\label{a}");
assert_eq!(cmd.nth_group_text(1).as_deref(), Some("b"));
}
#[test]
fn free_fn_shims_stay_kind_agnostic() {
let begin = node(
"\\begin{macro}{\\foo}\ncode\n\\end{macro}\n",
SyntaxKind::BEGIN,
);
assert_eq!(
nth_group(&begin, 0).map(|g| g.kind()),
Some(SyntaxKind::GROUP)
);
assert_eq!(
group_command_name(&nth_group(&begin, 0).unwrap()).as_deref(),
Some("foo")
);
}
#[test]
fn environment_wrapper_reaches_begin_and_end() {
let env = Environment::cast(node(
"\\begin{equation}\nx\n\\end{equation}\n",
SyntaxKind::ENVIRONMENT,
))
.unwrap();
assert_eq!(
env.begin().and_then(|b| b.name()).as_deref(),
Some("equation")
);
assert_eq!(
env.end().and_then(|e| e.name()).as_deref(),
Some("equation")
);
assert_eq!(env.name().as_deref(), Some("equation"));
}
}