use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case},
character::complete::{alpha1, line_ending, space0},
combinator::eof,
sequence::tuple,
IResult, InputTake,
};
use super::{
combinator::{
blank_lines, line_starts_iter, node, token, trim_line_end, GreenElement, NodeBuilder,
},
element::element_nodes,
input::Input,
SyntaxKind::*,
};
fn block_node_base(input: Input) -> IResult {
let (input, (block_begin, name)) = block_begin_node(input)?;
let (input, pre_blank) = blank_lines(input)?;
let (kind, is_greater_block) = match name {
s if s.eq_ignore_ascii_case("COMMENT") => (COMMENT_BLOCK, false),
s if s.eq_ignore_ascii_case("EXAMPLE") => (EXAMPLE_BLOCK, false),
s if s.eq_ignore_ascii_case("EXPORT") => (EXPORT_BLOCK, false),
s if s.eq_ignore_ascii_case("SRC") => (SOURCE_BLOCK, false),
s if s.eq_ignore_ascii_case("CENTER") => (CENTER_BLOCK, true),
s if s.eq_ignore_ascii_case("QUOTE") => (QUOTE_BLOCK, true),
s if s.eq_ignore_ascii_case("VERSE") => (VERSE_BLOCK, true),
_ => (SPECIAL_BLOCK, true),
};
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
if let Ok((input, block_end)) = block_end_node(input, name) {
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![block_begin];
children.extend(pre_blank);
if is_greater_block {
children.push(node(BLOCK_CONTENT, element_nodes(contents)?));
} else {
children.push(node(BLOCK_CONTENT, comma_quoted_text_nodes(contents)));
}
children.push(block_end);
children.extend(post_blank);
return Ok((input, node(kind, children)));
}
}
Err(nom::Err::Error(()))
}
fn block_begin_node(input: Input) -> IResult {
let (input, (ws, start, name, (argument, ws_, nl))) =
tuple((space0, tag_no_case("#+BEGIN_"), alpha1, trim_line_end))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(start);
b.text(name);
b.text(argument);
b.ws(ws_);
b.nl(nl);
Ok((input, (b.finish(BLOCK_BEGIN), name.as_str())))
}
fn block_end_node<'a>(input: Input<'a>, name: &str) -> IResult, GreenElement, ()> {
let (input, (ws, end, name, ws_, nl)) = tuple((
space0,
tag_no_case("#+END_"),
tag(name),
space0,
alt((line_ending, eof)),
))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(end);
b.text(name);
b.ws(ws_);
b.nl(nl);
Ok((input, b.finish(BLOCK_END)))
}
fn comma_quoted_text_nodes(input: Input) -> Vec {
let mut nodes = vec![];
let s = input.as_str();
let mut start = 0;
for i in line_starts_iter(s) {
// line must start with either ",*" or ",#+"
if s.get(i..i + 2) != Some(",*") && s.get(i..i + 3) != Some(",#+") {
continue;
}
let text = &s[start..i];
if !text.is_empty() {
nodes.push(token(TEXT, text));
}
nodes.push(token(COMMA, ","));
start = i + 1;
}
if !s[start..].is_empty() {
nodes.push(token(TEXT, &s[start..]));
}
nodes
}
#[tracing::instrument(level = "debug", skip(input), fields(input = input.s))]
pub fn block_node(input: Input) -> IResult {
crate::lossless_parser!(block_node_base, input)
}
#[test]
fn test_parse() {
use crate::ast::{ExampleBlock, SourceBlock};
use crate::tests::to_ast;
let to_src_block = to_ast::(block_node);
let to_example_block = to_ast::(block_node);
insta::assert_debug_snapshot!(
to_example_block(
r#"#+BEGIN_EXAMPLE
,* headline
,#+block
text
#+END_EXAMPLE"#
).syntax,
@r###"
EXAMPLE_BLOCK@0..59
BLOCK_BEGIN@0..16
TEXT@0..8 "#+BEGIN_"
TEXT@8..15 "EXAMPLE"
TEXT@15..15 ""
NEW_LINE@15..16 "\n"
BLOCK_CONTENT@16..42
COMMA@16..17 ","
TEXT@17..28 "* headline\n"
COMMA@28..29 ","
TEXT@29..42 "#+block\ntext\n"
BLOCK_END@42..59
WHITESPACE@42..46 " "
TEXT@46..52 "#+END_"
TEXT@52..59 "EXAMPLE"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+BEGIN_SRC
#+END_SRC"#
).syntax,
@r###"
SOURCE_BLOCK@0..27
BLOCK_BEGIN@0..12
TEXT@0..8 "#+BEGIN_"
TEXT@8..11 "SRC"
TEXT@11..11 ""
NEW_LINE@11..12 "\n"
BLANK_LINE@12..13 "\n"
BLANK_LINE@13..14 "\n"
BLOCK_CONTENT@14..14
BLOCK_END@14..27
WHITESPACE@14..18 " "
TEXT@18..24 "#+END_"
TEXT@24..27 "SRC"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+begin_src
#+end_src"#
).syntax,
@r###"
SOURCE_BLOCK@0..25
BLOCK_BEGIN@0..12
TEXT@0..8 "#+begin_"
TEXT@8..11 "src"
TEXT@11..11 ""
NEW_LINE@11..12 "\n"
BLOCK_CONTENT@12..12
BLOCK_END@12..25
WHITESPACE@12..16 " "
TEXT@16..22 "#+end_"
TEXT@22..25 "src"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+BEGIN_SRC javascript
alert('Hello World!');
#+END_SRC
"#).syntax,
@r###"
SOURCE_BLOCK@0..69
BLOCK_BEGIN@0..27
TEXT@0..8 "#+BEGIN_"
TEXT@8..11 "SRC"
TEXT@11..22 " javascript"
WHITESPACE@22..26 " "
NEW_LINE@26..27 "\n"
BLOCK_CONTENT@27..50
TEXT@27..50 "alert('Hello World!');\n"
BLOCK_END@50..64
WHITESPACE@50..54 " "
TEXT@54..60 "#+END_"
TEXT@60..63 "SRC"
NEW_LINE@63..64 "\n"
BLANK_LINE@64..65 "\n"
BLANK_LINE@65..69 " "
"###
);
// TODO: more testing
}