@@ 2,8 2,10 @@ use nom::{
branch::alt,
bytes::complete::{escaped_transform, tag, take_while1, take_while_m_n},
character::complete::{char, line_ending, none_of, satisfy},
- combinator::{complete, eof, fail, map_res, opt, value},
- multi::{fold_many1, fold_many_m_n, many0, many0_count, many_till},
+ combinator::{complete, eof, fail, map, map_res, opt, value},
+ multi::{
+ fold_many1, fold_many_m_n, many0, many0_count, many1_count, many_till,
+ },
sequence::{delimited, preceded, terminated, tuple},
IResult,
};
@@ 59,7 61,7 @@ pub struct ListItem {
pub sublist: Option<List>,
}
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ListKind {
Numeric,
Bulleted,
@@ 71,7 73,7 @@ pub struct Indent {
pub right: usize,
}
-#[derive(Debug, PartialEq, Clone)]
+#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Align {
Left,
Right,
@@ 79,7 81,7 @@ pub enum Align {
Justify,
}
-#[derive(Debug, PartialEq, Clone)]
+#[derive(Debug, PartialEq, Clone, Copy)]
pub enum VerticalAlign {
Top,
Middle,
@@ 195,6 197,11 @@ 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));
+ }
+
let (rest, opt_header) = opt(block_header)(input)?;
let (kind, attributes, opt_indent_align, extended) =
opt_header.unwrap_or((BlockKind::Paragraph, None, None, false));
@@ 266,6 273,102 @@ pub fn block(input: &str) -> IResult<&str, BlockTag> {
}
}
+fn list(input: &str) -> IResult<&str, BlockTag> {
+ let (rest, (kind, attributes, opt_indent_align)) = terminated(
+ tuple((
+ alt((
+ value(ListKind::Numeric, char('#')),
+ value(ListKind::Bulleted, char('*')),
+ )),
+ opt(attributes),
+ opt(indent_align),
+ )),
+ char(' '),
+ )(input)?;
+ let (indent, align) = opt_indent_align.unwrap_or((None, None));
+
+ let mut all_items: Vec<(ListKind, usize, InlineTag)> = Vec::new();
+ // get first item
+ let (rest, first_content) = list_item_content(rest)?;
+ all_items.push((kind, 1, first_content));
+
+ // collect all items with depth information
+ let mut input = rest;
+ while let Err(_) = end_of_block(input) {
+ let (rest, (item_kind, depth)) = list_item_head(input)?;
+ let (rest, content) = list_item_content(rest)?;
+ all_items.push((item_kind, depth, content));
+ input = rest;
+ }
+
+ // convert flat items list to nested list
+ match list_items_to_nested_list(&mut all_items) {
+ Ok(l) => Ok((
+ input,
+ BlockTag::List {
+ indent,
+ align,
+ attributes,
+ content: l,
+ },
+ )),
+ Err(_) => fail(input),
+ }
+}
+
+fn list_item_head(input: &str) -> IResult<&str, (ListKind, usize)> {
+ delimited(
+ line_ending,
+ alt((
+ map(many1_count(char('#')), |n| (ListKind::Numeric, n)),
+ map(many1_count(char('*')), |n| (ListKind::Bulleted, n)),
+ )),
+ char(' '),
+ )(input)
+}
+
+fn list_item_content(input: &str) -> IResult<&str, InlineTag> {
+ let mut i = 0;
+ while let Err(_) = end_list_item(&input[i..]) {
+ i += 1
+ }
+ let (_, content) = phrase(&input[..i])?;
+ Ok((&input[i..], content))
+}
+
+fn end_list_item(input: &str) -> IResult<&str, &str> {
+ alt((value("", list_item_head), end_of_block))(input)
+}
+
+fn list_items_to_nested_list(
+ list: &mut Vec<(ListKind, usize, InlineTag)>,
+) -> Result<List, String> {
+ let mut items: Vec<ListItem> = Vec::new();
+ let kind = list[0].0;
+ let depth = list[0].1;
+ while !list.is_empty() {
+ if list[0].1 > depth {
+ // unwrap should be safe because depth can only be greater after 1st item
+ items.last_mut().unwrap().sublist =
+ Some(list_items_to_nested_list(list)?);
+ } else if list[0].1 < depth {
+ break;
+ } else if list[0].0 != kind {
+ return Err(format!(
+ "List uses mixed markers on same level: {:?}",
+ list[0].2
+ ));
+ } else {
+ let (_, _, content) = list.remove(0);
+ items.push(ListItem {
+ content,
+ sublist: None,
+ })
+ }
+ }
+ Ok(List { kind, items })
+}
+
fn block_header(
input: &str,
) -> IResult<
@@ 347,6 450,7 @@ fn indent_align(
break;
}
}
+
if opt_indent.is_some() || opt_align.is_some() {
Ok((input, (opt_indent, opt_align)))
} else {
@@ 483,7 587,11 @@ fn language(input: &str) -> IResult<&str, String> {
}
fn end_of_block(input: &str) -> IResult<&str, &str> {
- alt((eof, value("", tuple((line_ending, line_ending)))))(input)
+ alt((
+ eof,
+ value("", tuple((line_ending, line_ending))),
+ value("", tuple((line_ending, eof))),
+ ))(input)
}
fn strip_flatiron_extended(input: &str) -> IResult<&str, String> {
@@ 492,11 600,10 @@ fn strip_flatiron_extended(input: &str) -> IResult<&str, String> {
pub fn phrase(input: &str) -> IResult<&str, InlineTag> {
let mut input_string = input.to_string();
- if !(input.contains("\n") && !input.contains("\n|")) {
- input_string = input.replace("\n|", "\n");
+ if let Ok((_, stripped)) = complete(strip_flatiron_extended)(input) {
+ input_string = stripped;
}
let mut input = input_string.as_str();
- println!("{}", input);
let mut content: Vec<InlineTag> = Vec::new();
@@ 274,7 274,14 @@ fn textile_sample() {
indent: None,
align: None,
attributes: None,
- content: InlineTag::Plaintext(String::from("\"This is a link (optional title)\":http://www.textism.com"))
+ content: InlineTag::Link {
+ attributes: None,
+ title: Some(String::from("optional title")),
+ url: String::from("http://www.textism.com"),
+ content: Box::new(
+ InlineTag::Plaintext(String::from("This is a link"))
+ )
+ }
},
BlockTag::Basic {
kind: BlockKind::Paragraph,
@@ 285,7 292,9 @@ fn textile_sample() {
kind: None,
attributes: None,
content: vec![
- InlineTag::Plaintext(String::from("table{border:1px solid black}.\n|")),
+ InlineTag::Plaintext(String::from("table{border:1px solid black}.")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("|")),
InlineTag::Phrase {
kind: Some(PhraseKind::Emphasis),
attributes: None,
@@ 301,12 310,16 @@ fn textile_sample() {
InlineTag::Plaintext(String::from(". a|"))
]
},
- InlineTag::Plaintext(String::from(". header|\n<{background:gray}. |\\2. this is|{background:red;width:200px}. a|")),
+ InlineTag::Plaintext(String::from(". header|")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("<{background:gray}. |\\2. this is|{background:red;width:200px}. a|")),
InlineTag::Phrase {
kind: Some(PhraseKind::Superscript),
attributes: None,
content: vec![
- InlineTag::Plaintext(String::from("<>{height:200px}. row|\n|this|<>{padding:10px}. is|"))
+ InlineTag::Plaintext(String::from("<>{height:200px}. row|")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("|this|<>{padding:10px}. is|"))
]
},
InlineTag::Plaintext(String::from(". another|(bob#bob). row|"))
@@ 325,17 338,11 @@ fn textile_sample() {
indent: None,
align: None,
attributes: None,
- content: InlineTag::Phrase {
- kind: None,
+ content: InlineTag::Image {
attributes: None,
- content: vec![
- InlineTag::Image {
- attributes: None,
- align: None,
- url: String::from("/textist.gif"),
- alt: Some(String::from("optional alt text"))
- }
- ]
+ align: None,
+ url: String::from("/textist.gif"),
+ alt: Some(String::from("optional alt text"))
}
},
BlockTag::List{
@@ 382,7 389,9 @@ fn textile_sample() {
InlineTag::Plaintext(String::from("emphasize"))
]
},
- InlineTag::Plaintext(String::from(".\nThat was a linebreak. And something to indicate ")),
+ InlineTag::Plaintext(String::from(".")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("That was a linebreak. And something to indicate ")),
InlineTag::Phrase {
kind: Some(PhraseKind::Strong),
attributes: None,