orgize/src/syntax/keyword.rs
2023-11-14 14:12:05 +08:00

214 lines
5 KiB
Rust

use nom::{
bytes::complete::take_till,
character::complete::space0,
combinator::{cond, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
blank_lines, colon_token, debug_assert_lossless, hash_plus_token, l_bracket_token,
r_bracket_token, trim_line_end, GreenElement, NodeBuilder,
},
input::Input,
SyntaxKind,
};
pub fn keyword_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(keyword_node_base)(input)
}
fn keyword_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (ws, hash_plus, key)) = tuple((
space0,
hash_plus_token,
take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '['),
))(input)?;
let is_babel_call = key.s.eq_ignore_ascii_case("CALL");
let (input, optional) = cond(
!is_babel_call,
opt(tuple((
l_bracket_token,
take_till(|c| c == ']' || c == '\n'),
r_bracket_token,
))),
)(input)?;
let (input, (colon, (value, ws_, nl), post_blank)) =
tuple((colon_token, trim_line_end, blank_lines))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.push(hash_plus);
b.text(key);
if let Some(Some((l_bracket, optional, r_bracket))) = optional {
b.children
.extend([l_bracket, optional.text_token(), r_bracket]);
}
b.push(colon);
b.ws(ws_);
b.text(value);
b.nl(nl);
b.children.extend(post_blank);
Ok((
input,
b.finish(if is_babel_call {
SyntaxKind::BABEL_CALL
} else {
SyntaxKind::KEYWORD
}),
))
}
pub fn affiliated_keyword_nodes(input: Input) -> IResult<Input, Vec<GreenElement>, ()> {
use rowan::NodeOrToken;
let mut children = vec![];
let mut i = input;
while !i.is_empty() {
let Ok((input, keyword)) = keyword_node(i) else {
break;
};
i = input;
let Some(node) = keyword.as_node() else {
return Err(nom::Err::Error(()));
};
// find the first text token in children
let Some(NodeOrToken::Token(token)) = node
.children()
.find(|t| t.kind() == SyntaxKind::TEXT.into())
else {
return Err(nom::Err::Error(()));
};
let text = token.text();
if input.c.affiliated_keywords.iter().all(|w| w != text) && !text.starts_with("ATTR_") {
return Err(nom::Err::Error(()));
}
children.push(keyword);
}
Ok((i, children))
}
#[test]
fn parse() {
use crate::{
ast::{BabelCall, Keyword},
tests::to_ast,
ParseConfig,
};
let to_keyword = to_ast::<Keyword>(keyword_node);
let to_babel_call = to_ast::<BabelCall>(keyword_node);
insta::assert_debug_snapshot!(
to_keyword("#+KEY:").syntax,
@r###"
KEYWORD@0..6
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..6 ""
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+KEY: VALUE").syntax,
@r###"
KEYWORD@0..12
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..12 " VALUE"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+K_E_Y: VALUE").syntax,
@r###"
KEYWORD@0..14
HASH_PLUS@0..2 "#+"
TEXT@2..7 "K_E_Y"
COLON@7..8 ":"
TEXT@8..14 " VALUE"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+KEY:VALUE\n").syntax,
@r###"
KEYWORD@0..12
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..11 "VALUE"
NEW_LINE@11..12 "\n"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+RESULTS:").syntax,
@r###"
KEYWORD@0..10
HASH_PLUS@0..2 "#+"
TEXT@2..9 "RESULTS"
COLON@9..10 ":"
TEXT@10..10 ""
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+ATTR_LATEX: :width 5cm\n").syntax,
@r###"
KEYWORD@0..25
HASH_PLUS@0..2 "#+"
TEXT@2..12 "ATTR_LATEX"
COLON@12..13 ":"
TEXT@13..24 " :width 5cm"
NEW_LINE@24..25 "\n"
"###
);
insta::assert_debug_snapshot!(
to_babel_call("#+CALL: double(n=4)").syntax,
@r###"
BABEL_CALL@0..19
HASH_PLUS@0..2 "#+"
TEXT@2..6 "CALL"
COLON@6..7 ":"
TEXT@7..19 " double(n=4)"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+CAPTION[Short caption]: Longer caption.").syntax,
@r###"
KEYWORD@0..41
HASH_PLUS@0..2 "#+"
TEXT@2..9 "CAPTION"
L_BRACKET@9..10 "["
TEXT@10..23 "Short caption"
R_BRACKET@23..24 "]"
COLON@24..25 ":"
TEXT@25..41 " Longer caption."
"###
);
let config = &ParseConfig::default();
assert!(keyword_node(("#+KE Y: VALUE", config).into()).is_err());
assert!(keyword_node(("#+CALL[option]: VALUE", config).into()).is_err());
assert!(keyword_node(("#+ KEY: VALUE", config).into()).is_err());
}