use crate::parse::{
alignment, attributes::attributes, from_num, list::list, opt_either_order,
phrase::phrase, table::table,
};
use crate::structs::{
Align, Attributes, BlockHeader, BlockKind, BlockTag, Indent,
};
use nom::{
branch::alt,
bytes::complete::{escaped_transform, tag},
character::complete::{char, digit1, line_ending, none_of},
combinator::{complete, eof, fail, map_res, opt, value},
multi::many0_count,
sequence::{preceded, tuple},
IResult,
};
pub fn block(input: &str) -> IResult<&str, BlockTag> {
if input.is_empty() {
return fail(input);
}
if let Ok((rest, list)) = list(input) {
return Ok((rest, list));
} else if let Ok((rest, table)) = table(input) {
return Ok((rest, table));
}
let (rest, opt_header) = opt(block_header)(input)?;
let (kind, attributes, opt_indent_align, extended) =
opt_header.unwrap_or((BlockKind::Paragraph, None, None, false));
let (indent, align) = opt_indent_align.unwrap_or((None, None));
let mut i = 0;
loop {
match if extended {
alt((eof, preceded(end_of_block, value("", block_header))))(
&rest[i..],
)
} else {
end_of_block(&rest[i..])
} {
Ok(_) => {
break;
}
_ => {
i += 1;
while !rest.is_char_boundary(i) {
i += 1;
}
}
}
}
let body = &rest[..i];
let (rest, _) = alt((eof, end_of_block))(&rest[i..])?;
match kind {
BlockKind::Paragraph
| BlockKind::Header(_)
| BlockKind::Footnote(_)
| BlockKind::BlockQuote => {
let (_, content) = complete(phrase)(body)?;
Ok((
rest,
BlockTag::Basic {
kind,
header: BlockHeader {
indent,
align,
attributes,
},
content,
},
))
}
BlockKind::BlockCode | BlockKind::Preformatted => {
let mut content = String::from(body);
if let Ok((_, stripped)) = complete(strip_flatiron_extended)(body) {
content = stripped;
}
Ok((
rest,
BlockTag::Preformatted {
kind,
header: BlockHeader {
indent,
align,
attributes,
},
content,
},
))
}
BlockKind::NoTextile => {
let mut content = String::from(body);
if let Ok((_, stripped)) = complete(strip_flatiron_extended)(body) {
content = stripped;
}
Ok((rest, BlockTag::NoTextile(content)))
}
}
}
fn block_header(
input: &str,
) -> IResult<
&str,
(
BlockKind,
Option<Attributes>,
Option<(Option<Indent>, Option<Align>)>,
bool,
),
> {
tuple((
block_kind,
opt(attributes),
opt(indent_align),
alt((value(true, tag(".. ")), value(false, tag(". ")))), ))(input)
}
fn block_kind(input: &str) -> IResult<&str, BlockKind> {
alt((
value(BlockKind::Preformatted, tag("pre")),
value(BlockKind::BlockCode, tag("bc")),
value(BlockKind::BlockQuote, tag("bq")),
value(BlockKind::Paragraph, tag("p")),
value(BlockKind::NoTextile, tag("notextile")),
header_modifier,
footnote_modifier,
))(input)
}
fn header_modifier(input: &str) -> IResult<&str, BlockKind> {
let (rest, n) = preceded(
char('h'),
alt((
value(1, char('1')),
value(2, char('2')),
value(3, char('3')),
value(4, char('4')),
value(5, char('5')),
value(6, char('6')),
)),
)(input)?;
Ok((rest, BlockKind::Header(n)))
}
fn footnote_modifier(input: &str) -> IResult<&str, BlockKind> {
let (rest, n) = preceded(tag("fn"), map_res(digit1, from_num))(input)?;
Ok((rest, BlockKind::Footnote(n)))
}
pub fn indent_align(
input: &str,
) -> IResult<&str, (Option<Indent>, Option<Align>)> {
opt_either_order(indent, alignment)(input)
}
fn indent(input: &str) -> IResult<&str, Indent> {
let (rest, (left, right)) =
tuple((many0_count(char('(')), many0_count(char(')'))))(input)?;
if left + right > 0 {
Ok((rest, Indent { left, right }))
} else {
fail(input)
}
}
pub fn end_of_block(input: &str) -> IResult<&str, &str> {
alt((
eof,
value("", tuple((line_ending, line_ending))),
value("", tuple((line_ending, eof))),
))(input)
}
pub fn strip_flatiron_extended(input: &str) -> IResult<&str, String> {
escaped_transform(none_of("\n"), '\n', value("\n", tag("|")))(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::structs::InlineTag;
#[test]
fn block_basic() {
let input = "Untagged paragraph";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: None,
align: None,
attributes: None,
},
content: InlineTag::Plaintext(String::from(
"Untagged paragraph"
))
}
))
);
}
#[test]
fn explicit_paragraph() {
let input = "p. Tagged paragraph";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: None,
align: None,
attributes: None,
},
content: InlineTag::Plaintext(String::from(
"Tagged paragraph"
))
}
))
);
}
#[test]
fn block_pre() {
let input = "pre. Preformatted _paragraph_";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Preformatted {
kind: BlockKind::Preformatted,
header: BlockHeader {
indent: None,
align: None,
attributes: None,
},
content: String::from("Preformatted _paragraph_")
}
))
);
}
#[test]
fn block_notextile() {
let input = "notextile. **Notextile** paragraph";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::NoTextile(String::from("**Notextile** paragraph"))
))
);
}
#[test]
fn extended_block() {
let input =
"p.. Extended paragraph\n\nWhere will she go?\n\np. Hell if I know.";
let result = block(input);
assert_eq!(
result,
Ok((
"p. Hell if I know.",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: None,
align: None,
attributes: None,
},
content: InlineTag::Phrase {
kind: None,
attributes: None,
content: vec![
InlineTag::Plaintext(String::from(
"Extended paragraph"
)),
InlineTag::LineBreak,
InlineTag::LineBreak,
InlineTag::Plaintext(String::from(
"Where will she go?"
))
]
}
}
))
);
}
#[test]
fn block_with_attributes() {
let input = "h2(class){color:green}. This is a title";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Header(2),
header: BlockHeader {
indent: None,
align: None,
attributes: Some(Attributes {
class: Some(String::from("class")),
id: None,
style: Some(String::from("color:green")),
language: None,
}),
},
content: InlineTag::Plaintext(String::from(
"This is a title"
))
}
))
);
}
#[test]
fn centered_block() {
let input = "h2=. This is a title";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Header(2),
header: BlockHeader {
indent: None,
align: Some(Align::Center),
attributes: None,
},
content: InlineTag::Plaintext(String::from(
"This is a title"
))
}
))
);
}
#[test]
fn indented_block() {
let input = "h2(). This is a title";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Header(2),
header: BlockHeader {
indent: Some(Indent { left: 1, right: 1 }),
align: None,
attributes: None,
},
content: InlineTag::Plaintext(String::from(
"This is a title"
))
}
))
);
}
#[test]
fn indented_aligned_block() {
let input = "p))>. I am a fish!";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: Some(Indent { left: 0, right: 2 }),
align: Some(Align::Right),
attributes: None,
},
content: InlineTag::Plaintext(String::from("I am a fish!"))
}
))
);
}
#[test]
fn aligned_indented_block() {
let input = "p>)). I am a transmitter!";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: Some(Indent { left: 0, right: 2 }),
align: Some(Align::Right),
attributes: None,
},
content: InlineTag::Plaintext(String::from(
"I am a transmitter!"
))
}
))
);
}
#[test]
fn block_with_attributes_indent_align() {
let input = "p(greeting){color:green}[fr]()>. Salut!";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Basic {
kind: BlockKind::Paragraph,
header: BlockHeader {
indent: Some(Indent { left: 1, right: 1 }),
align: Some(Align::Right),
attributes: Some(Attributes {
class: Some(String::from("greeting")),
id: None,
style: Some(String::from("color:green")),
language: Some(String::from("fr")),
}),
},
content: InlineTag::Plaintext(String::from("Salut!"))
}
))
);
}
#[test]
fn flatiron_blockcode() {
let input = "bc. This is supposed to be a block of textile code.
|
|bq. Textile should really have a closing delimiter for blocks.
|--Autumn
|
|Here's some textile code within the textile code:
|
|bc.
||bq. This is getting out of hand... now there are two of them!
||--Nute Gunray
|
|Now some more text within the code block";
let result = block(input);
assert_eq!(
result,
Ok((
"",
BlockTag::Preformatted {
kind: BlockKind::BlockCode,
header: BlockHeader {
indent: None,
align: None,
attributes: None,
},
content: String::from(
"This is supposed to be a block of textile code.
bq. Textile should really have a closing delimiter for blocks.
--Autumn
Here's some textile code within the textile code:
bc.
|bq. This is getting out of hand... now there are two of them!
|--Nute Gunray
Now some more text within the code block"
)
}
))
);
}
}