R tests/sample.textile => samples/textism.textile +0 -0
M src/lib.rs => src/lib.rs +5 -2
@@ 1,2 1,5 @@
-pub mod parse;
-pub mod render;
+#![allow(dead_code)]
+
+mod parse;
+mod render;
+mod structs;
D src/parse.rs => src/parse.rs +0 -1037
@@ 1,1037 0,0 @@
-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, map_res, opt, value},
- multi::{
- fold_many0, fold_many1, fold_many_m_n, many0, many0_count, many1_count,
- many_till,
- },
- sequence::{delimited, preceded, terminated, tuple},
- IResult,
-};
-
-#[derive(Debug, PartialEq)]
-pub struct Textile(pub Vec<BlockTag>);
-
-#[derive(Debug, PartialEq)]
-pub enum BlockTag {
- Basic {
- kind: BlockKind,
- indent: Option<Indent>,
- align: Option<Align>,
- attributes: Option<Attributes>,
- content: InlineTag,
- },
- Preformatted {
- kind: BlockKind,
- indent: Option<Indent>,
- align: Option<Align>,
- attributes: Option<Attributes>,
- content: String,
- },
- List {
- indent: Option<Indent>,
- align: Option<Align>,
- attributes: Option<Attributes>,
- content: List,
- },
- NoTextile(String),
-}
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-pub enum BlockKind {
- Paragraph,
- NoTextile,
- BlockQuote,
- BlockCode,
- Preformatted,
- Header(usize),
- Footnote(usize),
-}
-
-#[derive(Debug, PartialEq)]
-pub struct List {
- pub kind: ListKind,
- pub items: Vec<ListItem>,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct ListItem {
- pub content: InlineTag,
- pub sublist: Option<List>,
-}
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-pub enum ListKind {
- Numeric,
- Bulleted,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct Indent {
- pub left: usize,
- pub right: usize,
-}
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-pub enum Align {
- Left,
- Right,
- Center,
- Justify,
-}
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-pub enum VerticalAlign {
- Top,
- Middle,
- Bottom,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct Attributes {
- pub class: Option<String>,
- pub id: Option<String>,
- pub style: Option<String>,
- pub language: Option<String>,
-}
-
-#[derive(Debug, PartialEq)]
-pub enum InlineTag {
- Plaintext(String),
- NoTextile(String),
- Code(String),
- FootnoteRef(usize),
- LineBreak,
- Image {
- attributes: Option<Attributes>,
- align: Option<Align>,
- url: String,
- alt: Option<String>,
- },
- Acronym {
- title: Option<String>,
- content: String,
- },
- Link {
- attributes: Option<Attributes>,
- title: Option<String>,
- url: String,
- content: Box<InlineTag>,
- },
- Phrase {
- kind: Option<PhraseKind>,
- attributes: Option<Attributes>,
- content: Vec<InlineTag>,
- },
-}
-
-impl InlineTag {
- fn parsers() -> &'static [for<'r> fn(
- &'r str,
- ) -> Result<
- (&'r str, InlineTag),
- nom::Err<nom::error::Error<&'r str>>,
- >] {
- &[line_break, no_textile, code, footnote_ref, link, image]
- }
-}
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-pub enum PhraseKind {
- Italic,
- Bold,
- Emphasis,
- Strong,
- Citation,
- Deleted,
- Inserted,
- Superscript,
- Subscript,
- Span,
-}
-
-impl PhraseKind {
- fn list() -> &'static [PhraseKind] {
- use PhraseKind::*;
- &[
- Italic,
- Bold,
- Emphasis,
- Strong,
- Citation,
- Deleted,
- Inserted,
- Superscript,
- Subscript,
- Span,
- ]
- }
-
- fn delimiter(self, input: &str) -> IResult<&str, &str> {
- use PhraseKind::*;
- match self {
- Italic => tag("__")(input),
- Bold => tag("**")(input),
- Emphasis => tag("_")(input),
- Strong => tag("*")(input),
- Citation => tag("??")(input),
- Deleted => tag("#")(input),
- Inserted => tag("+")(input),
- Superscript => tag("^")(input),
- Subscript => tag("~")(input),
- Span => tag("%")(input),
- }
- }
-}
-
-pub fn textile(input: &str) -> IResult<&str, Textile> {
- let (rest, blocks) = preceded(
- many0(line_ending),
- many0(terminated(block, many0(line_ending))),
- )(input)?;
- Ok((rest, Textile(blocks)))
-}
-
-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));
- let (indent, align) = opt_indent_align.unwrap_or((None, None));
-
- // get body of block to be parsed later.
- 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;
- }
- }
- }
-
- let body = &rest[..i];
- // consume interstitial newlines if they are there
- 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,
- 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,
- 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 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<
- &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(". ")))), // is extended
- ))(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(take_while1(|c: char| c.is_ascii_digit()), from_num),
- )(input)?;
- Ok((rest, BlockKind::Footnote(n)))
-}
-
-fn from_num(input: &str) -> Result<usize, std::num::ParseIntError> {
- usize::from_str_radix(input, 10)
-}
-
-fn indent_align(
- mut input: &str,
-) -> IResult<&str, (Option<Indent>, Option<Align>)> {
- let (mut opt_indent, mut opt_align) = (None, None);
- loop {
- if let Ok((rest, i)) = indent(input) {
- if opt_indent.is_none() {
- opt_indent = Some(i);
- input = rest;
- } else {
- return Ok((input, (opt_indent, opt_align)));
- }
- } else if let Ok((rest, a)) = alignment(input) {
- if opt_align.is_none() {
- opt_align = Some(a);
- input = rest;
- } else {
- return Ok((input, (opt_indent, opt_align)));
- }
- } else {
- break;
- }
- }
-
- if opt_indent.is_some() || opt_align.is_some() {
- Ok((input, (opt_indent, opt_align)))
- } else {
- fail(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)
- }
-}
-
-fn alignment(input: &str) -> IResult<&str, Align> {
- alt((
- value(Align::Justify, tag("<>")),
- value(Align::Left, tag("<")),
- value(Align::Right, tag(">")),
- value(Align::Center, tag("=")),
- ))(input)
-}
-
-pub fn attributes(mut input: &str) -> IResult<&str, Attributes> {
- let mut attributes = Attributes {
- class: None,
- id: None,
- style: None,
- language: None,
- };
- loop {
- if let Ok((rest, (class, id))) = class_and_id(input) {
- if attributes.class.is_none() {
- attributes.class = class;
- } else if class.is_some() {
- return Ok((input, attributes));
- }
- if attributes.id.is_none() {
- attributes.id = id;
- } else if id.is_some() {
- return Ok((input, attributes));
- }
- input = rest;
- } else if let Ok((rest, style)) = style(input) {
- if attributes.style.is_none() {
- attributes.style = Some(style);
- } else {
- return Ok((input, attributes));
- }
- input = rest;
- } else if let Ok((rest, language)) = language(input) {
- if attributes.language.is_none() {
- attributes.language = Some(language);
- } else {
- return Ok((input, attributes));
- }
- input = rest;
- } else {
- break;
- }
- }
- if attributes.class.is_some()
- || attributes.id.is_some()
- || attributes.style.is_some()
- || attributes.language.is_some()
- {
- Ok((input, attributes))
- } else {
- fail(input)
- }
-}
-
-fn class_and_id(
- input: &str,
-) -> IResult<&str, (Option<String>, Option<String>)> {
- let (rest, (opt_class, opt_id)) = delimited(
- char('('),
- tuple((opt(class_name), opt(preceded(char('#'), class_name)))),
- char(')'),
- )(input)?;
- if opt_class.is_some() || opt_id.is_some() {
- Ok((rest, (opt_class, opt_id)))
- } else {
- fail(rest)
- }
-}
-
-fn class_name(input: &str) -> IResult<&str, String> {
- escaped_transform(
- none_of("\\#()"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- value("#", tag("#")),
- )),
- )(input)
-}
-
-fn style(input: &str) -> IResult<&str, String> {
- delimited(
- char('{'),
- escaped_transform(
- none_of("\\{}"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("{", tag("{")),
- value("}", tag("}")),
- )),
- ),
- char('}'),
- )(input)
-}
-
-fn language(input: &str) -> IResult<&str, String> {
- delimited(
- char('['),
- escaped_transform(
- none_of("\\[]"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("[", tag("[")),
- value("]", tag("]")),
- )),
- ),
- char(']'),
- )(input)
-}
-
-fn end_of_block(input: &str) -> IResult<&str, &str> {
- alt((
- eof,
- value("", tuple((line_ending, line_ending))),
- value("", tuple((line_ending, eof))),
- ))(input)
-}
-
-fn strip_flatiron_extended(input: &str) -> IResult<&str, String> {
- escaped_transform(none_of("\n"), '\n', value("\n", tag("|")))(input)
-}
-
-pub fn phrase(input: &str) -> IResult<&str, InlineTag> {
- let mut input_string = input.to_string();
- if let Ok((_, stripped)) = complete(strip_flatiron_extended)(input) {
- input_string = stripped;
- }
- let mut input = input_string.as_str();
-
- let mut content: Vec<InlineTag> = Vec::new();
-
- let mut i = 0;
- let mut preceding_space = true;
- loop {
- // check if end of phrase has been reached
- if &input[i..] == "" {
- if i > 0 {
- content.push(InlineTag::Plaintext(String::from(&input[..i])));
- }
- match content.len() {
- 0 => {
- // return empty plaintext rather than phrase
- return Ok(("", InlineTag::Plaintext(String::from(""))));
- }
- 1 => {
- // return the only tag
- return Ok(("", content.remove(0)));
- }
- _ => (),
- }
- return Ok((
- "",
- InlineTag::Phrase {
- attributes: None,
- kind: None,
- content,
- },
- ));
- }
-
- let mut matched = None;
- if preceding_space {
- // only match acronym if preceded by a space
- if let Ok((rest, tag)) = acronym(&input[i..]) {
- matched = Some((rest, tag))
- }
- }
- if matched.is_none() {
- for parser in InlineTag::parsers() {
- if let Ok((rest, tag)) = parser(&input[i..]) {
- matched = Some((rest, tag));
- break;
- }
- }
- }
- if matched.is_none() {
- for phrase_kind in PhraseKind::list() {
- if let Ok((rest, tag)) =
- tagged_phrase(*phrase_kind)(&input[i..])
- {
- matched = Some((rest, tag));
- break;
- }
- }
- }
-
- if let Some((rest, tag)) = matched {
- if i > 0 {
- content.push(InlineTag::Plaintext(String::from(&input[..i])));
- }
- input = rest;
- i = 0;
- content.push(tag);
- preceding_space = true;
- } else {
- let c = input.chars().nth(i).unwrap();
- preceding_space = !c.is_alphanumeric();
- i += 1;
- }
- }
-}
-
-fn tagged_phrase(
- kind: PhraseKind,
-) -> impl FnMut(&str) -> IResult<&str, InlineTag> {
- move |mut input: &str| {
- // match opening delimiter
- let (rest, _delimiter) = kind.delimiter(input)?;
- input = rest;
- // only match attributes if phrase is tagged
- let (rest, attributes) = opt(attributes)(input)?;
- input = rest;
-
- let mut content: Vec<InlineTag> = Vec::new();
-
- let mut i = 0;
- let mut preceding_space = true;
- loop {
- if i >= input.len() {
- return fail(input);
- }
- // check if end of phrase has been reached
- if let Ok((rest, _delimiter)) = kind.delimiter(&input[i..]) {
- if i > 0 {
- content
- .push(InlineTag::Plaintext(String::from(&input[..i])));
- }
- return Ok((
- rest,
- InlineTag::Phrase {
- attributes,
- kind: Some(kind),
- content,
- },
- ));
- }
-
- let mut matched = None;
- if preceding_space {
- // only match acronym if preceded by a space
- if let Ok((rest, tag)) = acronym(&input[i..]) {
- matched = Some((rest, tag))
- }
- }
- if matched.is_none() {
- for parser in InlineTag::parsers() {
- if let Ok((rest, tag)) = parser(&input[i..]) {
- matched = Some((rest, tag));
- break;
- }
- }
- }
- if matched.is_none() {
- for phrase_kind in PhraseKind::list() {
- if let Ok((rest, tag)) =
- tagged_phrase(*phrase_kind)(&input[i..])
- {
- matched = Some((rest, tag));
- break;
- }
- }
- }
- if let Some((rest, tag)) = matched {
- if i > 0 {
- content
- .push(InlineTag::Plaintext(String::from(&input[..i])));
- }
- input = rest;
- i = 0;
- content.push(tag);
- preceding_space = true;
- } else {
- let c = input.chars().nth(i).unwrap();
- preceding_space = !c.is_alphanumeric();
- i += 1;
- }
- }
- }
-}
-
-fn line_break(input: &str) -> IResult<&str, InlineTag> {
- let (rest, _) = line_ending(input)?;
- Ok((rest, InlineTag::LineBreak))
-}
-
-pub fn no_textile(input: &str) -> IResult<&str, InlineTag> {
- let (rest, content) = delimited(
- tag("=="),
- fold_many0(
- alt((
- value("\\", tag("\\\\")),
- value("==", tag("\\==")),
- no_textile_content,
- )),
- String::new,
- |mut acc, s| {
- acc.push_str(s);
- acc
- },
- ),
- tag("=="),
- )(input)?;
- Ok((rest, InlineTag::NoTextile(content)))
-}
-
-fn no_textile_content(input: &str) -> IResult<&str, &str> {
- let res: IResult<&str, &str> = alt((tag("=="), tag("\\")))(input);
- if let Err(_) = res {
- if input != "" {
- return Ok((&input[1..], &input[0..1]));
- }
- }
- fail(input)
-}
-
-fn code(input: &str) -> IResult<&str, InlineTag> {
- let (rest, content) = delimited(
- char('@'),
- escaped_transform(
- none_of("\\@"),
- '\\',
- alt((value("\\", char('\\')), value("@", char('@')))),
- ),
- char('@'),
- )(input)?;
- Ok((rest, InlineTag::Code(content)))
-}
-
-fn footnote_ref(input: &str) -> IResult<&str, InlineTag> {
- let (rest, n) = delimited(
- char('['),
- map_res(take_while1(|c: char| c.is_ascii_digit()), from_num),
- char(']'),
- )(input)?;
- Ok((rest, InlineTag::FootnoteRef(n)))
-}
-
-pub fn link(input: &str) -> IResult<&str, InlineTag> {
- let (rest, (attributes, (content_vec, (title, url)))) = preceded(
- char('"'),
- tuple((
- opt(attributes),
- many_till(
- alt((
- value('\\', tag("\\\\")),
- value('"', tag("\\\"")),
- value('(', tag("\\(")),
- value(')', tag("\\)")),
- none_of("\\\"()"),
- )),
- tuple((
- opt(delimited(
- tag(" ("),
- escaped_transform(
- none_of("\\()"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- )),
- ),
- char(')'),
- )),
- preceded(char('"'), link_suffix),
- )),
- ),
- )),
- )(input)?;
- let content_string = content_vec.into_iter().collect::<String>();
- let content = match complete(phrase)(content_string.as_str()) {
- Ok((_, content)) => content,
- Err(_) => return fail(input),
- };
- Ok((
- rest,
- InlineTag::Link {
- attributes,
- title,
- url,
- content: Box::new(content),
- },
- ))
-}
-
-fn link_suffix(input: &str) -> IResult<&str, String> {
- preceded(
- char(':'),
- alt((
- delimited(char('('), link_url_parenthesized, char(')')),
- link_url_standard,
- )),
- )(input)
-}
-
-fn link_url_parenthesized(input: &str) -> IResult<&str, String> {
- let (rest, url) = escaped_transform(
- none_of("\\() \r\n\t"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- )),
- )(input)?;
- Ok((rest, String::from(url)))
-}
-
-fn link_url_standard(input: &str) -> IResult<&str, String> {
- fold_many1(none_of(" \r\n\t"), String::new, |mut acc, c| {
- acc.push(c);
- acc
- })(input)
-}
-
-pub fn image(input: &str) -> IResult<&str, InlineTag> {
- let (rest, ((align, attributes, url, alt), opt_link)) = tuple((
- delimited(
- char('!'),
- tuple((
- opt(image_alignment),
- opt(attributes),
- image_url,
- opt(delimited(char('('), image_alt, char(')'))),
- )),
- char('!'),
- ),
- opt(link_suffix),
- ))(input)?;
- let img = InlineTag::Image {
- attributes,
- align,
- url,
- alt,
- };
- Ok((
- rest,
- match opt_link {
- Some(link) => InlineTag::Link {
- attributes: None,
- title: None,
- url: link,
- content: Box::new(img),
- },
- None => img,
- },
- ))
-}
-
-fn image_alignment(input: &str) -> IResult<&str, Align> {
- alt((
- value(Align::Left, tag("<")),
- value(Align::Right, tag(">")),
- value(Align::Center, tag("=")),
- ))(input)
-}
-
-fn image_url(input: &str) -> IResult<&str, String> {
- let (rest, url) = escaped_transform(
- none_of("\\()! \r\n\t"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- value("!", tag("!")),
- )),
- )(input)?;
- Ok((rest, String::from(url)))
-}
-
-fn image_alt(input: &str) -> IResult<&str, String> {
- let (rest, alt) = escaped_transform(
- none_of("\\()!"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- value("!", tag("!")),
- )),
- )(input)?;
- Ok((rest, String::from(alt)))
-}
-
-pub fn acronym(input: &str) -> IResult<&str, InlineTag> {
- let (rest, (caps, opt_title)) = tuple((
- capitals,
- opt(delimited(char('('), acronym_title, char(')'))),
- ))(input)?;
- alt((eof, take_while_m_n(1, 1, |c: char| !c.is_alphanumeric())))(rest)?;
- Ok((
- rest,
- InlineTag::Acronym {
- title: opt_title,
- content: caps,
- },
- ))
-}
-
-fn acronym_title(input: &str) -> IResult<&str, String> {
- let (rest, title) = escaped_transform(
- none_of("\\()"),
- '\\',
- alt((
- value("\\", tag("\\")),
- value("(", tag("(")),
- value(")", tag(")")),
- )),
- )(input)?;
- Ok((rest, String::from(title)))
-}
-
-fn capitals(input: &str) -> IResult<&str, String> {
- let (rest, caps) = alt((
- // acronyms of the form ABC
- fold_many_m_n(
- 2,
- usize::MAX,
- single_capital,
- String::new,
- |mut acc, c| {
- acc.push(c);
- acc
- },
- ),
- // acronyms of the form A.B.C.
- fold_many_m_n(
- 2,
- usize::MAX,
- capital_with_dot,
- String::new,
- |mut acc, (c, dot)| {
- acc.push(c);
- acc.push(dot);
- acc
- },
- ),
- ))(input)?;
- Ok((rest, String::from(caps)))
-}
-
-fn capital_with_dot(input: &str) -> IResult<&str, (char, char)> {
- tuple((single_capital, char('.')))(input)
-}
-
-fn single_capital(input: &str) -> IResult<&str, char> {
- satisfy(|c| c.is_ascii_uppercase())(input)
-}
-
-#[allow(dead_code)]
-fn vertical_alignment(input: &str) -> IResult<&str, VerticalAlign> {
- alt((
- value(VerticalAlign::Top, tag("^")),
- value(VerticalAlign::Middle, tag("-")),
- value(VerticalAlign::Bottom, tag("~")),
- ))(input)
-}
R tests/acronym.rs => src/parse/acronym.rs +143 -64
@@ 1,74 1,153 @@
-use flatiron::parse::*;
+use crate::structs::InlineTag;
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, tag, take_while_m_n},
+ character::complete::{char, none_of, satisfy},
+ combinator::{eof, opt, value},
+ multi::fold_many_m_n,
+ sequence::{delimited, tuple},
+ IResult,
+};
-#[test]
-fn acronym_basic() {
- let input = "ACLU(American Civil Liberties Union)";
- let result = acronym(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Acronym {
- title: Some(String::from("American Civil Liberties Union")),
- content: String::from("ACLU")
- }
- ))
- );
+pub fn acronym(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, (caps, opt_title)) = tuple((
+ capitals,
+ opt(delimited(char('('), acronym_title, char(')'))),
+ ))(input)?;
+ alt((eof, take_while_m_n(1, 1, |c: char| !c.is_alphanumeric())))(rest)?;
+ Ok((
+ rest,
+ InlineTag::Acronym {
+ title: opt_title,
+ content: caps,
+ },
+ ))
}
-#[test]
-fn acronym_without_title() {
- let input = "ACLU";
- let result = acronym(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Acronym {
- title: None,
- content: String::from("ACLU")
- }
- ))
- );
+fn acronym_title(input: &str) -> IResult<&str, String> {
+ let (rest, title) = escaped_transform(
+ none_of("\\()"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ )),
+ )(input)?;
+ Ok((rest, String::from(title)))
}
-#[test]
-fn acronym_with_parentheses() {
- let input = "ACLU(American Civil Liberties Union \\(a union\\))";
- let result = acronym(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Acronym {
- title: Some(String::from(
- "American Civil Liberties Union (a union)"
- )),
- content: String::from("ACLU")
- }
- ))
- );
+fn capitals(input: &str) -> IResult<&str, String> {
+ let (rest, caps) = alt((
+ // acronyms of the form ABC
+ fold_many_m_n(
+ 2,
+ usize::MAX,
+ single_capital,
+ String::new,
+ |mut acc, c| {
+ acc.push(c);
+ acc
+ },
+ ),
+ // acronyms of the form A.B.C.
+ fold_many_m_n(
+ 2,
+ usize::MAX,
+ capital_with_dot,
+ String::new,
+ |mut acc, (c, dot)| {
+ acc.push(c);
+ acc.push(dot);
+ acc
+ },
+ ),
+ ))(input)?;
+ Ok((rest, String::from(caps)))
}
-#[test]
-fn acronym_with_dots() {
- let input = "A.C.L.U.(American Civil Liberties Union)";
- let result = acronym(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Acronym {
- title: Some(String::from("American Civil Liberties Union")),
- content: String::from("A.C.L.U.")
- }
- ))
- );
+fn capital_with_dot(input: &str) -> IResult<&str, (char, char)> {
+ tuple((single_capital, char('.')))(input)
}
-#[test]
-#[should_panic]
-fn malformed_acronym_with_dots() {
- let input = "A.CL.U.(American CivilLiberties Union)";
- acronym(input).unwrap();
+fn single_capital(input: &str) -> IResult<&str, char> {
+ satisfy(|c| c.is_ascii_uppercase())(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn acronym_basic() {
+ let input = "ACLU(American Civil Liberties Union)";
+ let result = acronym(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Acronym {
+ title: Some(String::from("American Civil Liberties Union")),
+ content: String::from("ACLU")
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn acronym_without_title() {
+ let input = "ACLU";
+ let result = acronym(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Acronym {
+ title: None,
+ content: String::from("ACLU")
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn acronym_with_parentheses() {
+ let input = "ACLU(American Civil Liberties Union \\(a union\\))";
+ let result = acronym(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Acronym {
+ title: Some(String::from(
+ "American Civil Liberties Union (a union)"
+ )),
+ content: String::from("ACLU")
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn acronym_with_dots() {
+ let input = "A.C.L.U.(American Civil Liberties Union)";
+ let result = acronym(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Acronym {
+ title: Some(String::from("American Civil Liberties Union")),
+ content: String::from("A.C.L.U.")
+ }
+ ))
+ );
+ }
+
+ #[test]
+ #[should_panic]
+ fn malformed_acronym_with_dots() {
+ let input = "A.CL.U.(American CivilLiberties Union)";
+ acronym(input).unwrap();
+ }
}
A src/parse/attributes.rs => src/parse/attributes.rs +195 -0
@@ 0,0 1,195 @@
+use crate::structs::Attributes;
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, tag},
+ character::complete::{char, none_of},
+ combinator::{fail, opt, value},
+ sequence::{delimited, preceded, tuple},
+ IResult,
+};
+
+pub fn attributes(mut input: &str) -> IResult<&str, Attributes> {
+ let mut attributes = Attributes {
+ class: None,
+ id: None,
+ style: None,
+ language: None,
+ };
+ loop {
+ if let Ok((rest, (class, id))) = class_and_id(input) {
+ if attributes.class.is_none() {
+ attributes.class = class;
+ } else if class.is_some() {
+ return Ok((input, attributes));
+ }
+ if attributes.id.is_none() {
+ attributes.id = id;
+ } else if id.is_some() {
+ return Ok((input, attributes));
+ }
+ input = rest;
+ } else if let Ok((rest, style)) = style(input) {
+ if attributes.style.is_none() {
+ attributes.style = Some(style);
+ } else {
+ return Ok((input, attributes));
+ }
+ input = rest;
+ } else if let Ok((rest, language)) = language(input) {
+ if attributes.language.is_none() {
+ attributes.language = Some(language);
+ } else {
+ return Ok((input, attributes));
+ }
+ input = rest;
+ } else {
+ break;
+ }
+ }
+ if attributes.class.is_some()
+ || attributes.id.is_some()
+ || attributes.style.is_some()
+ || attributes.language.is_some()
+ {
+ Ok((input, attributes))
+ } else {
+ fail(input)
+ }
+}
+
+fn class_and_id(
+ input: &str,
+) -> IResult<&str, (Option<String>, Option<String>)> {
+ let (rest, (opt_class, opt_id)) = delimited(
+ char('('),
+ tuple((opt(class_name), opt(preceded(char('#'), class_name)))),
+ char(')'),
+ )(input)?;
+ if opt_class.is_some() || opt_id.is_some() {
+ Ok((rest, (opt_class, opt_id)))
+ } else {
+ fail(rest)
+ }
+}
+
+fn class_name(input: &str) -> IResult<&str, String> {
+ escaped_transform(
+ none_of("\\#()"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ value("#", tag("#")),
+ )),
+ )(input)
+}
+
+fn style(input: &str) -> IResult<&str, String> {
+ delimited(
+ char('{'),
+ escaped_transform(
+ none_of("\\{}"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("{", tag("{")),
+ value("}", tag("}")),
+ )),
+ ),
+ char('}'),
+ )(input)
+}
+
+fn language(input: &str) -> IResult<&str, String> {
+ delimited(
+ char('['),
+ escaped_transform(
+ none_of("\\[]"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("[", tag("[")),
+ value("]", tag("]")),
+ )),
+ ),
+ char(']'),
+ )(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn attributes_csl() {
+ let input = "(class#id){style}[language]";
+ let result = attributes(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Attributes {
+ class: Some(String::from("class")),
+ id: Some(String::from("id")),
+ style: Some(String::from("style")),
+ language: Some(String::from("language")),
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn attributes_scl() {
+ let input = "{style}(class#id)[language]";
+ let result = attributes(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Attributes {
+ class: Some(String::from("class")),
+ id: Some(String::from("id")),
+ style: Some(String::from("style")),
+ language: Some(String::from("language")),
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn attributes_lcs() {
+ let input = "[language]{style}(class#id)";
+ let result = attributes(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Attributes {
+ class: Some(String::from("class")),
+ id: Some(String::from("id")),
+ style: Some(String::from("style")),
+ language: Some(String::from("language")),
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn attributes_class() {
+ let input = "(class)";
+ let result = attributes(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Attributes {
+ class: Some(String::from("class")),
+ id: None,
+ style: None,
+ language: None,
+ }
+ ))
+ );
+ }
+}
A src/parse/block.rs => src/parse/block.rs +497 -0
@@ 0,0 1,497 @@
+use crate::parse::{
+ attributes::attributes, from_num, list::list, phrase::phrase,
+};
+use crate::structs::{Align, Attributes, BlockKind, BlockTag, Indent};
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, tag, take_while1},
+ character::complete::{char, 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));
+ }
+
+ 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));
+
+ // get body of block to be parsed later.
+ 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;
+ }
+ }
+ }
+
+ let body = &rest[..i];
+ // consume interstitial newlines if they are there
+ 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,
+ 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,
+ 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(". ")))), // is extended
+ ))(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(take_while1(|c: char| c.is_ascii_digit()), from_num),
+ )(input)?;
+ Ok((rest, BlockKind::Footnote(n)))
+}
+
+pub fn indent_align(
+ mut input: &str,
+) -> IResult<&str, (Option<Indent>, Option<Align>)> {
+ let (mut opt_indent, mut opt_align) = (None, None);
+ loop {
+ if let Ok((rest, i)) = indent(input) {
+ if opt_indent.is_none() {
+ opt_indent = Some(i);
+ input = rest;
+ } else {
+ return Ok((input, (opt_indent, opt_align)));
+ }
+ } else if let Ok((rest, a)) = alignment(input) {
+ if opt_align.is_none() {
+ opt_align = Some(a);
+ input = rest;
+ } else {
+ return Ok((input, (opt_indent, opt_align)));
+ }
+ } else {
+ break;
+ }
+ }
+
+ if opt_indent.is_some() || opt_align.is_some() {
+ Ok((input, (opt_indent, opt_align)))
+ } else {
+ fail(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)
+ }
+}
+
+fn alignment(input: &str) -> IResult<&str, Align> {
+ alt((
+ value(Align::Justify, tag("<>")),
+ value(Align::Left, tag("<")),
+ value(Align::Right, tag(">")),
+ value(Align::Center, tag("=")),
+ ))(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,
+ 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,
+ 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,
+ 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,
+ 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),
+ 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),
+ 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),
+ 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,
+ 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,
+ 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,
+ 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,
+ 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"
+ )
+ }
+ ))
+ );
+ }
+}
R tests/image.rs => src/parse/image.rs +204 -120
@@ 1,138 1,222 @@
-use flatiron::parse::*;
+use crate::parse::{attributes, link::link_suffix};
+use crate::structs::{Align, InlineTag};
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, tag},
+ character::complete::{char, none_of},
+ combinator::{opt, value},
+ sequence::{delimited, tuple},
+ IResult,
+};
-#[test]
-fn image_basic() {
- let input =
- "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Image {
+pub fn image(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, ((align, attributes, url, alt), opt_link)) = tuple((
+ delimited(
+ char('!'),
+ tuple((
+ opt(image_alignment),
+ opt(attributes::attributes),
+ image_url,
+ opt(delimited(char('('), image_alt, char(')'))),
+ )),
+ char('!'),
+ ),
+ opt(link_suffix),
+ ))(input)?;
+ let img = InlineTag::Image {
+ attributes,
+ align,
+ url,
+ alt,
+ };
+ Ok((
+ rest,
+ match opt_link {
+ Some(link) => InlineTag::Link {
attributes: None,
- align: None,
- url: String::from(
- "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
- ),
- alt: None
- }
- ))
- );
+ title: None,
+ url: link,
+ content: Box::new(img),
+ },
+ None => img,
+ },
+ ))
}
-#[test]
-fn image_with_alt() {
- let input =
- "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png(Flatiron logo)!";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Image {
- attributes: None,
- align: None,
- url: String::from(
- "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
- ),
- alt: Some(String::from("Flatiron logo")),
- }
- ))
- );
+fn image_alignment(input: &str) -> IResult<&str, Align> {
+ alt((
+ value(Align::Left, tag("<")),
+ value(Align::Right, tag(">")),
+ value(Align::Center, tag("=")),
+ ))(input)
}
-#[test]
-fn image_with_parentheses() {
- let input =
- "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png\\(parentheses\\)!";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Image {
- attributes: None,
- align: None,
- url: String::from(
- "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png(parentheses)",
- ),
- alt: None
- }
- ))
- );
+fn image_url(input: &str) -> IResult<&str, String> {
+ let (rest, url) = escaped_transform(
+ none_of("\\()! \r\n\t"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ value("!", tag("!")),
+ )),
+ )(input)?;
+ Ok((rest, String::from(url)))
}
-#[test]
-fn image_with_exclamation() {
- let input =
- "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png\\!\\!!";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Image {
- attributes: None,
- align: None,
- url: String::from(
- "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!!",
- ),
- alt: None,
- }
- ))
- );
+fn image_alt(input: &str) -> IResult<&str, String> {
+ let (rest, alt) = escaped_transform(
+ none_of("\\()!"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ value("!", tag("!")),
+ )),
+ )(input)?;
+ Ok((rest, String::from(alt)))
}
-#[test]
-#[should_panic]
-fn not_an_image() {
- let input = "Stop! This is not an image, leave me alone!";
- image(input).unwrap();
-}
+#[cfg(test)]
+mod tests {
+ use super::*;
-#[test]
-fn image_link() {
- let input = "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!:https://github.com/autumnull/flatiron";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: None,
- title: None,
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Image {
+ #[test]
+ fn image_basic() {
+ let input =
+ "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Image {
attributes: None,
align: None,
- url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
- alt: None,
- })
- }
- ))
- );
-}
+ url: String::from(
+ "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
+ ),
+ alt: None
+ }
+ ))
+ );
+ }
-#[test]
-fn image_link_parenthesized() {
- let input = "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!:(https://github.com/autumnull/flatiron)";
- let result = image(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: None,
- title: None,
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Image {
+ #[test]
+ fn image_with_alt() {
+ let input =
+ "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png(Flatiron logo)!";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Image {
attributes: None,
align: None,
- url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
+ url: String::from(
+ "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png",
+ ),
+ alt: Some(String::from("Flatiron logo")),
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn image_with_parentheses() {
+ let input =
+ "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png\\(parentheses\\)!";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from(
+ "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png(parentheses)",
+ ),
+ alt: None
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn image_with_exclamation() {
+ let input =
+ "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png\\!\\!!";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from(
+ "https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!!",
+ ),
alt: None,
- })
- }
- ))
- );
+ }
+ ))
+ );
+ }
+
+ #[test]
+ #[should_panic]
+ fn not_an_image() {
+ let input = "Stop! This is not an image, leave me alone!";
+ image(input).unwrap();
+ }
+
+ #[test]
+ fn image_link() {
+ let input = "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!:https://github.com/autumnull/flatiron";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: None,
+ title: None,
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
+ alt: None,
+ })
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn image_link_parenthesized() {
+ let input = "!https://github.com/autumnull/flatiron/raw/main/images/flatiron.png!:(https://github.com/autumnull/flatiron)";
+ let result = image(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: None,
+ title: None,
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from("https://github.com/autumnull/flatiron/raw/main/images/flatiron.png"),
+ alt: None,
+ })
+ }
+ ))
+ );
+ }
}
A src/parse/link.rs => src/parse/link.rs +209 -0
@@ 0,0 1,209 @@
+use crate::parse::{attributes::attributes, phrase::phrase};
+use crate::structs::InlineTag;
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, tag},
+ character::complete::{char, none_of},
+ combinator::{complete, fail, opt, value},
+ multi::{fold_many1, many_till},
+ sequence::{delimited, preceded, tuple},
+ IResult,
+};
+
+pub fn link(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, (attributes, (content_vec, (title, url)))) = preceded(
+ char('"'),
+ tuple((
+ opt(attributes),
+ many_till(
+ alt((
+ value('\\', tag("\\\\")),
+ value('"', tag("\\\"")),
+ value('(', tag("\\(")),
+ value(')', tag("\\)")),
+ none_of("\\\"()"),
+ )),
+ tuple((
+ opt(delimited(
+ tag(" ("),
+ escaped_transform(
+ none_of("\\()"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ )),
+ ),
+ char(')'),
+ )),
+ preceded(char('"'), link_suffix),
+ )),
+ ),
+ )),
+ )(input)?;
+ let content_string = content_vec.into_iter().collect::<String>();
+ let content = match complete(phrase)(content_string.as_str()) {
+ Ok((_, content)) => content,
+ Err(_) => return fail(input),
+ };
+ Ok((
+ rest,
+ InlineTag::Link {
+ attributes,
+ title,
+ url,
+ content: Box::new(content),
+ },
+ ))
+}
+
+pub fn link_suffix(input: &str) -> IResult<&str, String> {
+ preceded(
+ char(':'),
+ alt((
+ delimited(char('('), link_url_parenthesized, char(')')),
+ link_url_standard,
+ )),
+ )(input)
+}
+
+fn link_url_parenthesized(input: &str) -> IResult<&str, String> {
+ let (rest, url) = escaped_transform(
+ none_of("\\() \r\n\t"),
+ '\\',
+ alt((
+ value("\\", tag("\\")),
+ value("(", tag("(")),
+ value(")", tag(")")),
+ )),
+ )(input)?;
+ Ok((rest, String::from(url)))
+}
+
+fn link_url_standard(input: &str) -> IResult<&str, String> {
+ fold_many1(none_of(" \r\n\t"), String::new, |mut acc, c| {
+ acc.push(c);
+ acc
+ })(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::structs::Attributes;
+
+ #[test]
+ fn link_basic() {
+ let input = "\"a link\":https://github.com/autumnull/flatiron";
+ let result = link(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: None,
+ title: None,
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Plaintext(String::from(
+ "a link"
+ )))
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn link_attributes() {
+ let input = "\"(class)a link\":https://github.com/autumnull/flatiron";
+ let result = link(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: Some(Attributes {
+ class: Some(String::from("class")),
+ id: None,
+ style: None,
+ language: None,
+ }),
+ title: None,
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Plaintext(String::from(
+ "a link"
+ )))
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn link_attributes_title() {
+ let input =
+ "\"(class)a link (title)\":https://github.com/autumnull/flatiron";
+ let result = link(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: Some(Attributes {
+ class: Some(String::from("class")),
+ id: None,
+ style: None,
+ language: None,
+ }),
+ title: Some(String::from("title")),
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Plaintext(String::from(
+ "a link"
+ )))
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn link_escaped() {
+ let input =
+ "\"Earvin \\\"Magic\\\" Johnson \\(Basketball Player\\)\":https://en.wikipedia.org/wiki/Magic_Johnson";
+ let result = link(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Link {
+ attributes: None,
+ title: None,
+ url: String::from(
+ "https://en.wikipedia.org/wiki/Magic_Johnson"
+ ),
+ content: Box::new(InlineTag::Plaintext(String::from(
+ "Earvin \"Magic\" Johnson (Basketball Player)"
+ )))
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn link_parenthesized() {
+ let input = "\"this\":(https://github.com/autumnull/flatiron)?";
+ let result = link(input);
+ assert_eq!(
+ result,
+ Ok((
+ "?",
+ InlineTag::Link {
+ attributes: None,
+ title: None,
+ url: String::from("https://github.com/autumnull/flatiron"),
+ content: Box::new(InlineTag::Plaintext(String::from(
+ "this"
+ )))
+ }
+ ))
+ );
+ }
+}
A src/parse/list.rs => src/parse/list.rs +106 -0
@@ 0,0 1,106 @@
+use crate::parse::{attributes, block, phrase};
+use crate::structs::{BlockTag, InlineTag, List, ListItem, ListKind};
+use nom::{
+ branch::alt,
+ character::complete::{char, line_ending},
+ combinator::{fail, map, opt, value},
+ multi::many1_count,
+ sequence::{delimited, terminated, tuple},
+ IResult,
+};
+
+pub 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::attributes),
+ opt(block::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(_) = block::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::phrase(&input[..i])?;
+ Ok((&input[i..], content))
+}
+
+fn end_list_item(input: &str) -> IResult<&str, &str> {
+ alt((value("", list_item_head), block::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 })
+}
R tests/textile.rs => src/parse/mod.rs +556 -469
@@ 1,484 1,571 @@
-use flatiron::parse::*;
+use crate::structs::{InlineTag, PhraseKind, Textile};
+use nom::{
+ bytes::complete::tag,
+ character::complete::line_ending,
+ multi::many0,
+ sequence::{preceded, terminated},
+ IResult,
+};
-#[test]
-fn textile_basic() {
- let input = "h2{color:green}. This is a title
+mod acronym;
+mod attributes;
+mod block;
+mod image;
+mod link;
+mod list;
+mod no_textile;
+mod phrase;
+mod table;
+
+impl PhraseKind {
+ fn list() -> &'static [PhraseKind] {
+ use PhraseKind::*;
+ &[
+ Italic,
+ Bold,
+ Emphasis,
+ Strong,
+ Citation,
+ Deleted,
+ Inserted,
+ Superscript,
+ Subscript,
+ Span,
+ ]
+ }
+
+ fn delimiter(self, input: &str) -> IResult<&str, &str> {
+ use PhraseKind::*;
+ match self {
+ Italic => tag("__")(input),
+ Bold => tag("**")(input),
+ Emphasis => tag("_")(input),
+ Strong => tag("*")(input),
+ Citation => tag("??")(input),
+ Deleted => tag("#")(input),
+ Inserted => tag("+")(input),
+ Superscript => tag("^")(input),
+ Subscript => tag("~")(input),
+ Span => tag("%")(input),
+ }
+ }
+}
+
+impl InlineTag {
+ fn parsers() -> &'static [for<'r> fn(
+ &'r str,
+ ) -> Result<
+ (&'r str, InlineTag),
+ nom::Err<nom::error::Error<&'r str>>,
+ >] {
+ &[
+ phrase::line_break,
+ no_textile::no_textile,
+ phrase::code,
+ phrase::footnote_ref,
+ link::link,
+ image::image,
+ ]
+ }
+}
+
+pub fn textile(input: &str) -> IResult<&str, Textile> {
+ let (rest, blocks) = preceded(
+ many0(line_ending),
+ many0(terminated(block::block, many0(line_ending))),
+ )(input)?;
+ Ok((rest, Textile(blocks)))
+}
+
+fn from_num(input: &str) -> Result<usize, std::num::ParseIntError> {
+ usize::from_str_radix(input, 10)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::structs::{
+ Attributes, BlockKind, BlockTag, List, ListItem, ListKind,
+ };
+
+ #[test]
+ fn textile_basic() {
+ let input = "h2{color:green}. This is a title
This is a paragraph with some _emphasized text_.";
- let result = textile(input);
- assert_eq!(
- result,
- Ok((
- "",
- Textile(vec![
- BlockTag::Basic {
- kind: BlockKind::Header(2),
- indent: None,
- align: None,
- attributes: Some(Attributes {
- class: None,
- id: None,
- style: Some(String::from("color:green")),
- language: None,
- }),
- content: InlineTag::Plaintext(String::from(
- "This is a title"
- ))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Phrase {
- kind: None,
+ let result = textile(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Textile(vec![
+ BlockTag::Basic {
+ kind: BlockKind::Header(2),
+ indent: None,
+ align: None,
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: Some(String::from("color:green")),
+ language: None,
+ }),
+ content: InlineTag::Plaintext(String::from(
+ "This is a title"
+ ))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from(
- "This is a paragraph with some "
- )),
- InlineTag::Phrase {
- kind: Some(PhraseKind::Emphasis),
- attributes: None,
- content: vec![InlineTag::Plaintext(
- String::from("emphasized text")
- )]
- },
- InlineTag::Plaintext(String::from(".")),
- ]
+ content: InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from(
+ "This is a paragraph with some "
+ )),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Emphasis),
+ attributes: None,
+ content: vec![InlineTag::Plaintext(
+ String::from("emphasized text")
+ )]
+ },
+ InlineTag::Plaintext(String::from(".")),
+ ]
+ }
}
- }
- ])
- ))
- );
-}
+ ])
+ ))
+ );
+ }
-/*
-#[test]
-fn textile_sample() {
- let input = include_str!("sample.textile");
- let result = textile(input);
- assert_eq!(
- result,
- Ok((
- "",
- Textile(vec![
- BlockTag::Basic {
- kind: BlockKind::Header(2),
- indent: None,
- align: None,
- attributes: Some(Attributes {
- class: None,
- id: None,
- style: Some(String::from("color:green")),
- language: None
- }),
- content: InlineTag::Plaintext(String::from("This is a title"))
- },
- BlockTag::Basic {
- kind: BlockKind::Header(3),
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("This is a subhead"))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: Some(Attributes {
- class: None,
- id: None,
- style: Some(String::from("color:red")),
- language: None
- }),
- content: InlineTag::Plaintext(String::from("This is some text of dubious character. Isn't the use of \"quotes\" just lazy writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote."))
- },
- BlockTag::Basic {
- kind: BlockKind::BlockQuote,
- indent: None,
- align: None,
- attributes: Some(Attributes {
- class: None,
- id: None,
- style: None,
- language: Some(String::from("fr"))
- }),
- content: InlineTag::Plaintext(String::from("This is a block quote. I'll admit it's not the most exciting block quote ever devised."))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("Simple list:"))
- },
- BlockTag::List{
- indent: None,
- align: None,
- attributes: Some(Attributes {
- class: None,
- id: None,
- style: Some(String::from("color:blue")),
- language: None
- }),
- content: List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("one")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("two")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("three")),
- sublist: None,
- },
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("Multi-level list:"))
- },
- BlockTag::List{
- indent: None,
- align: None,
- attributes: None,
- content: List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("one")),
- sublist: Some(List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("aye")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("bee")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("see")),
- sublist: None,
- }
- ],
- }),
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("two")),
- sublist: Some(List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("x")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("y")),
- sublist: None,
- },
- ],
- }),
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("three")),
- sublist: None,
- },
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("Mixed list:"))
- },
- BlockTag::List{
- indent: None,
- align: None,
- attributes: None,
- content: List {
- kind: ListKind::Bulleted,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("Point one")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Point two")),
- sublist: Some(List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("Step 1")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Step 2")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Step 3")),
- sublist: None,
- }
- ],
- }),
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Point three")),
- sublist: Some(List {
- kind: ListKind::Bulleted,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("Sub point 1")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Sub point 2")),
- sublist: None,
- },
- ],
- }),
- },
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Phrase {
- kind: None,
+ #[test]
+ fn textile_sample() {
+ let input = include_str!("../../samples/textism.textile");
+ let result = textile(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ Textile(vec![
+ BlockTag::Basic {
+ kind: BlockKind::Header(2),
+ indent: None,
+ align: None,
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: Some(String::from("color:green")),
+ language: None
+ }),
+ content: InlineTag::Plaintext(String::from("This is a title"))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Header(3),
+ indent: None,
+ align: None,
attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from("Well, that went well. How about we insert an ")),
- InlineTag::NoTextile(String::from("<a href=\"http://www.textism.com/\" title=\"watch out\">old-fashioned hypertext link</a>")),
- InlineTag::Plaintext(String::from("? Will the quote marks in the tags get messed up? No!")),
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Link {
+ content: InlineTag::Plaintext(String::from("This is a subhead"))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: Some(String::from("color:red")),
+ language: None
+ }),
+ content: InlineTag::Plaintext(String::from("This is some text of dubious character. Isn't the use of \"quotes\" just lazy writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote."))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::BlockQuote,
+ indent: None,
+ align: None,
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: None,
+ language: Some(String::from("fr"))
+ }),
+ content: InlineTag::Plaintext(String::from("This is a block quote. I'll admit it's not the most exciting block quote ever devised."))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
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,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Phrase {
- kind: None,
+ content: InlineTag::Plaintext(String::from("Simple list:"))
+ },
+ BlockTag::List{
+ indent: None,
+ align: None,
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: Some(String::from("color:blue")),
+ language: None
+ }),
+ content: List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("one")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("two")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("three")),
+ sublist: None,
+ },
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from("table{border:1px solid black}.")),
- InlineTag::LineBreak,
- InlineTag::Plaintext(String::from("|")),
- InlineTag::Phrase {
- kind: Some(PhraseKind::Emphasis),
- attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from(". this|"))
- ]
- },
- InlineTag::Plaintext(String::from(". is|")),
- InlineTag::Phrase {
- kind: Some(PhraseKind::Emphasis),
- attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from(". 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|")),
- InlineTag::LineBreak,
- InlineTag::Plaintext(String::from("|this|<>{padding:10px}. is|"))
- ]
- },
- InlineTag::Plaintext(String::from(". another|(bob#bob). row|"))
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("An image:"))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Image {
+ content: InlineTag::Plaintext(String::from("Multi-level list:"))
+ },
+ BlockTag::List{
+ indent: None,
+ align: None,
attributes: None,
+ content: List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("one")),
+ sublist: Some(List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("aye")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("bee")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("see")),
+ sublist: None,
+ }
+ ],
+ }),
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("two")),
+ sublist: Some(List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("x")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("y")),
+ sublist: None,
+ },
+ ],
+ }),
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("three")),
+ sublist: None,
+ },
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
align: None,
- url: String::from("/textist.gif"),
- alt: Some(String::from("optional alt text"))
- }
- },
- BlockTag::List{
- indent: None,
- align: None,
- attributes: None,
- content: List {
- kind: ListKind::Numeric,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("Librarians rule")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("Yes they do")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("But you knew that")),
- sublist: None,
- },
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Phrase {
- kind: None,
attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from("Some more text of dubious character. Here is a noisome string of ")),
- InlineTag::Acronym {
- title: None,
- content: String::from("CAPITAL")
- },
- InlineTag::Plaintext(String::from(" letters. Here is something we want to ")),
- InlineTag::Phrase {
- kind: Some(PhraseKind::Emphasis),
- attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from("emphasize"))
- ]
- },
- 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,
- content: vec![
- InlineTag::Plaintext(String::from("strength"))
- ]
- },
- InlineTag::Plaintext(String::from(". Of course I could use <em>my own ")),
- InlineTag::Acronym {
- title: None,
- content: String::from("HTML")
- },
- InlineTag::Plaintext(String::from(" tags</em> if I <strong>felt</strong> like it."))
- ]
- }
- },
- BlockTag::Basic {
- kind: BlockKind::Header(3),
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("Coding"))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Phrase {
- kind: None,
+ content: InlineTag::Plaintext(String::from("Mixed list:"))
+ },
+ BlockTag::List{
+ indent: None,
+ align: None,
attributes: None,
- content: vec![
- InlineTag::Plaintext(String::from("This ")),
- InlineTag::Code(String::from("is some code, \"isn't it\"")),
- InlineTag::Plaintext(String::from(". Watch those quote marks! Now for some preformatted text:"))
- ]
- }
- },
- BlockTag::Preformatted {
- kind: BlockKind::BlockCode,
- indent: None,
- align: None,
- attributes: None,
- content: String::from("$text = str_replace(\"<p>%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%\",\"\",$text);")
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("This isn't code."))
- },
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- indent: None,
- align: None,
- attributes: None,
- content: InlineTag::Plaintext(String::from("So you see, my friends:"))
- },
- BlockTag::List {
- indent: None,
- align: None,
- attributes: None,
- content: List {
- kind: ListKind::Bulleted,
- items: vec![
- ListItem {
- content: InlineTag::Plaintext(String::from("The time is now")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("The time is not later")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("The time is not yesterday")),
- sublist: None,
- },
- ListItem {
- content: InlineTag::Plaintext(String::from("We must act")),
- sublist: None,
- },
- ]
+ content: List {
+ kind: ListKind::Bulleted,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Point one")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Point two")),
+ sublist: Some(List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Step 1")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Step 2")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Step 3")),
+ sublist: None,
+ }
+ ],
+ }),
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Point three")),
+ sublist: Some(List {
+ kind: ListKind::Bulleted,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Sub point 1")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Sub point 2")),
+ sublist: None,
+ },
+ ],
+ }),
+ },
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("Well, that went well. How about we insert an ")),
+ InlineTag::NoTextile(String::from("<a href=\"http://www.textism.com/\" title=\"watch out\">old-fashioned hypertext link</a>")),
+ InlineTag::Plaintext(String::from("? Will the quote marks in the tags get messed up? No!")),
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ 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,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("table{border:1px solid black}.")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("|")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Emphasis),
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from(". this|"))
+ ]
+ },
+ InlineTag::Plaintext(String::from(". is|")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Emphasis),
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from(". 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|")),
+ InlineTag::LineBreak,
+ InlineTag::Plaintext(String::from("|this|<>{padding:10px}. is|"))
+ ]
+ },
+ InlineTag::Plaintext(String::from(". another|(bob#bob). row|"))
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Plaintext(String::from("An image:"))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from("/textist.gif"),
+ alt: Some(String::from("optional alt text"))
+ }
+ },
+ BlockTag::List{
+ indent: None,
+ align: None,
+ attributes: None,
+ content: List {
+ kind: ListKind::Numeric,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Librarians rule")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("Yes they do")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("But you knew that")),
+ sublist: None,
+ },
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("Some more text of dubious character. Here is a noisome string of ")),
+ InlineTag::Acronym {
+ title: None,
+ content: String::from("CAPITAL")
+ },
+ InlineTag::Plaintext(String::from(" letters. Here is something we want to ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Emphasis),
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("emphasize"))
+ ]
+ },
+ 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,
+ content: vec![
+ InlineTag::Plaintext(String::from("strength"))
+ ]
+ },
+ InlineTag::Plaintext(String::from(". Of course I could use <em>my own ")),
+ InlineTag::Acronym {
+ title: None,
+ content: String::from("HTML")
+ },
+ InlineTag::Plaintext(String::from(" tags</em> if I <strong>felt</strong> like it."))
+ ]
+ }
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Header(3),
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Plaintext(String::from("Coding"))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This ")),
+ InlineTag::Code(String::from("is some code, \"isn't it\"")),
+ InlineTag::Plaintext(String::from(". Watch those quote marks! Now for some preformatted text:"))
+ ]
+ }
+ },
+ BlockTag::Preformatted {
+ kind: BlockKind::BlockCode,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: String::from("$text = str_replace(\"<p>%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%</p>\",\"\",$text);\n$text = str_replace(\"%::%\",\"\",$text);")
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Plaintext(String::from("This isn't code."))
+ },
+ BlockTag::Basic {
+ kind: BlockKind::Paragraph,
+ indent: None,
+ align: None,
+ attributes: None,
+ content: InlineTag::Plaintext(String::from("So you see, my friends:"))
+ },
+ BlockTag::List {
+ indent: None,
+ align: None,
+ attributes: None,
+ content: List {
+ kind: ListKind::Bulleted,
+ items: vec![
+ ListItem {
+ content: InlineTag::Plaintext(String::from("The time is now")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("The time is not later")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("The time is not yesterday")),
+ sublist: None,
+ },
+ ListItem {
+ content: InlineTag::Plaintext(String::from("We must act")),
+ sublist: None,
+ },
+ ]
+ }
}
- }
- ])
- ))
- );
+ ])
+ ))
+ );
+ }
}
-*/
A src/parse/no_textile.rs => src/parse/no_textile.rs +70 -0
@@ 0,0 1,70 @@
+use crate::structs::InlineTag;
+use nom::{
+ branch::alt,
+ bytes::complete::tag,
+ combinator::{fail, value},
+ multi::fold_many0,
+ sequence::delimited,
+ IResult,
+};
+
+pub fn no_textile(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, content) = delimited(
+ tag("=="),
+ fold_many0(
+ alt((
+ value("\\", tag("\\\\")),
+ value("==", tag("\\==")),
+ no_textile_content,
+ )),
+ String::new,
+ |mut acc, s| {
+ acc.push_str(s);
+ acc
+ },
+ ),
+ tag("=="),
+ )(input)?;
+ Ok((rest, InlineTag::NoTextile(content)))
+}
+
+fn no_textile_content(input: &str) -> IResult<&str, &str> {
+ let res: IResult<&str, &str> = alt((tag("=="), tag("\\")))(input);
+ if let Err(_) = res {
+ if input != "" {
+ return Ok((&input[1..], &input[0..1]));
+ }
+ }
+ fail(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn no_textile_basic() {
+ let input = "==hello *world!*==";
+ let result = no_textile(input);
+ assert_eq!(
+ result,
+ Ok(("", InlineTag::NoTextile(String::from("hello *world!*",))))
+ );
+ }
+
+ #[test]
+ fn no_textile_with_equals() {
+ let input =
+ "==This is two equals signs: \\== here are two more: \\====";
+ let result = no_textile(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::NoTextile(String::from(
+ "This is two equals signs: == here are two more: ==",
+ ))
+ ))
+ );
+ }
+}
A src/parse/phrase.rs => src/parse/phrase.rs +510 -0
@@ 0,0 1,510 @@
+use crate::parse::block::strip_flatiron_extended;
+use crate::parse::{acronym, attributes, from_num};
+use crate::structs::{InlineTag, PhraseKind};
+use nom::{
+ branch::alt,
+ bytes::complete::{escaped_transform, take_while1},
+ character::complete::{char, line_ending, none_of},
+ combinator::{complete, fail, map_res, opt, value},
+ sequence::delimited,
+ IResult,
+};
+
+pub fn phrase(input: &str) -> IResult<&str, InlineTag> {
+ let mut input_string = input.to_string();
+ if let Ok((_, stripped)) = complete(strip_flatiron_extended)(input) {
+ input_string = stripped;
+ }
+ let mut input = input_string.as_str();
+
+ let mut content: Vec<InlineTag> = Vec::new();
+
+ let mut i = 0;
+ let mut preceding_space = true;
+ loop {
+ // check if end of phrase has been reached
+ if &input[i..] == "" {
+ if i > 0 {
+ content.push(InlineTag::Plaintext(String::from(&input[..i])));
+ }
+ match content.len() {
+ 0 => {
+ // return empty plaintext rather than phrase
+ return Ok(("", InlineTag::Plaintext(String::from(""))));
+ }
+ 1 => {
+ // return the only tag
+ return Ok(("", content.remove(0)));
+ }
+ _ => (),
+ }
+ return Ok((
+ "",
+ InlineTag::Phrase {
+ attributes: None,
+ kind: None,
+ content,
+ },
+ ));
+ }
+
+ let mut matched = None;
+ if preceding_space {
+ // only match acronym if preceded by a space
+ if let Ok((rest, tag)) = acronym::acronym(&input[i..]) {
+ matched = Some((rest, tag))
+ }
+ }
+ if matched.is_none() {
+ for parser in InlineTag::parsers() {
+ if let Ok((rest, tag)) = parser(&input[i..]) {
+ matched = Some((rest, tag));
+ break;
+ }
+ }
+ }
+ if matched.is_none() {
+ for phrase_kind in PhraseKind::list() {
+ if let Ok((rest, tag)) =
+ tagged_phrase(*phrase_kind)(&input[i..])
+ {
+ matched = Some((rest, tag));
+ break;
+ }
+ }
+ }
+
+ if let Some((rest, tag)) = matched {
+ if i > 0 {
+ content.push(InlineTag::Plaintext(String::from(&input[..i])));
+ }
+ input = rest;
+ i = 0;
+ content.push(tag);
+ preceding_space = true;
+ } else {
+ let c = input.chars().nth(i).unwrap();
+ preceding_space = !c.is_alphanumeric();
+ i += 1;
+ }
+ }
+}
+
+fn tagged_phrase(
+ kind: PhraseKind,
+) -> impl FnMut(&str) -> IResult<&str, InlineTag> {
+ move |mut input: &str| {
+ // match opening delimiter
+ let (rest, _delimiter) = kind.delimiter(input)?;
+ input = rest;
+ // only match attributes if phrase is tagged
+ let (rest, attributes) = opt(attributes::attributes)(input)?;
+ input = rest;
+
+ let mut content: Vec<InlineTag> = Vec::new();
+
+ let mut i = 0;
+ let mut preceding_space = true;
+ loop {
+ if i >= input.len() {
+ return fail(input);
+ }
+ // check if end of phrase has been reached
+ if let Ok((rest, _delimiter)) = kind.delimiter(&input[i..]) {
+ if i > 0 {
+ content
+ .push(InlineTag::Plaintext(String::from(&input[..i])));
+ }
+ return Ok((
+ rest,
+ InlineTag::Phrase {
+ attributes,
+ kind: Some(kind),
+ content,
+ },
+ ));
+ }
+
+ let mut matched = None;
+ if preceding_space {
+ // only match acronym if preceded by a space
+ if let Ok((rest, tag)) = acronym::acronym(&input[i..]) {
+ matched = Some((rest, tag))
+ }
+ }
+ if matched.is_none() {
+ for parser in InlineTag::parsers() {
+ if let Ok((rest, tag)) = parser(&input[i..]) {
+ matched = Some((rest, tag));
+ break;
+ }
+ }
+ }
+ if matched.is_none() {
+ for phrase_kind in PhraseKind::list() {
+ if let Ok((rest, tag)) =
+ tagged_phrase(*phrase_kind)(&input[i..])
+ {
+ matched = Some((rest, tag));
+ break;
+ }
+ }
+ }
+ if let Some((rest, tag)) = matched {
+ if i > 0 {
+ content
+ .push(InlineTag::Plaintext(String::from(&input[..i])));
+ }
+ input = rest;
+ i = 0;
+ content.push(tag);
+ preceding_space = true;
+ } else {
+ let c = input.chars().nth(i).unwrap();
+ preceding_space = !c.is_alphanumeric();
+ i += 1;
+ }
+ }
+ }
+}
+
+pub fn line_break(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, _) = line_ending(input)?;
+ Ok((rest, InlineTag::LineBreak))
+}
+
+pub fn code(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, content) = delimited(
+ char('@'),
+ escaped_transform(
+ none_of("\\@"),
+ '\\',
+ alt((value("\\", char('\\')), value("@", char('@')))),
+ ),
+ char('@'),
+ )(input)?;
+ Ok((rest, InlineTag::Code(content)))
+}
+
+pub fn footnote_ref(input: &str) -> IResult<&str, InlineTag> {
+ let (rest, n) = delimited(
+ char('['),
+ map_res(take_while1(|c: char| c.is_ascii_digit()), from_num),
+ char(']'),
+ )(input)?;
+ Ok((rest, InlineTag::FootnoteRef(n)))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::structs::Attributes;
+
+ #[test]
+ fn empty_phrase() {
+ let input = "";
+ let result = phrase(input);
+ assert_eq!(result, Ok(("", InlineTag::Plaintext(String::from("")))));
+ }
+
+ #[test]
+ fn plaintext_phrase() {
+ let input = "I am french!!";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok(("", InlineTag::Plaintext(String::from("I am french!!"))))
+ );
+ }
+
+ #[test]
+ fn phrase_with_acronym() {
+ let input = "I am in the ACLU(American Civil Liberties Union), I know how this works";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("I am in the ")),
+ InlineTag::Acronym {
+ title: Some(String::from(
+ "American Civil Liberties Union"
+ )),
+ content: String::from("ACLU")
+ },
+ InlineTag::Plaintext(String::from(
+ ", I know how this works"
+ ))
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_image() {
+ let input = "Textist: !/common/textist.gif(Textist)!";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("Textist: ")),
+ InlineTag::Image {
+ attributes: None,
+ align: None,
+ url: String::from("/common/textist.gif"),
+ alt: Some(String::from("Textist")),
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_notextile() {
+ let input = "Here is ==no **textile**==";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("Here is ")),
+ InlineTag::NoTextile(String::from("no **textile**"))
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_italic() {
+ let input = "RIP__scrip__";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Acronym {
+ title: None,
+ content: String::from("RIP")
+ },
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Italic),
+ attributes: None,
+ content: vec![InlineTag::Plaintext(String::from(
+ "scrip"
+ ))]
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_bold() {
+ let input = "This is **bold**";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This is ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Bold),
+ attributes: None,
+ content: vec![InlineTag::Plaintext(String::from(
+ "bold"
+ ))]
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_bold_and_italic() {
+ let input = "This is **bold. RIP__scrip__**";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This is ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Bold),
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("bold. ")),
+ InlineTag::Acronym {
+ title: None,
+ content: String::from("RIP")
+ },
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Italic),
+ attributes: None,
+ content: vec![InlineTag::Plaintext(
+ String::from("scrip")
+ )]
+ }
+ ]
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_bold_and_notextile() {
+ let input = "This is **bold. ==no **textile**!!==**";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This is ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Bold),
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("bold. ")),
+ InlineTag::NoTextile(String::from(
+ "no **textile**!!"
+ ))
+ ]
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_secret_strong() {
+ let input = "This is **bold. or is it?";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This is ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Strong),
+ attributes: None,
+ content: vec![]
+ },
+ InlineTag::Plaintext(String::from("bold. or is it?"))
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_hyphens() {
+ let input = "You yellow-bellied hot-blooded rattlesnake! Who do you think you are?? I spit at you!!";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Plaintext(String::from(
+ "You yellow-bellied hot-blooded rattlesnake! Who do you think you are?? I spit at you!!"
+ ))
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_broken_nesting() {
+ let input = "This is **bold. No _you hang up first**";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("This is ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Bold),
+ attributes: None,
+ content: vec![InlineTag::Plaintext(String::from(
+ "bold. No _you hang up first"
+ )),]
+ }
+ ]
+ }
+ ))
+ );
+ }
+
+ #[test]
+ fn phrase_with_attributes() {
+ let input = "Look, it's %{color:red}red%";
+ let result = phrase(input);
+ assert_eq!(
+ result,
+ Ok((
+ "",
+ InlineTag::Phrase {
+ kind: None,
+ attributes: None,
+ content: vec![
+ InlineTag::Plaintext(String::from("Look, it's ")),
+ InlineTag::Phrase {
+ kind: Some(PhraseKind::Span),
+ attributes: Some(Attributes {
+ class: None,
+ id: None,
+ style: Some(String::from("color:red")),
+ language: None,
+ }),
+ content: vec![InlineTag::Plaintext(String::from(
+ "red"
+ )),]
+ }
+ ]
+ }
+ ))
+ );
+ }
+}
A src/parse/table.rs => src/parse/table.rs +10 -0
@@ 0,0 1,10 @@
+use crate::structs::VerticalAlign;
+use nom::{branch::alt, bytes::complete::tag, combinator::value, IResult};
+
+fn vertical_alignment(input: &str) -> IResult<&str, VerticalAlign> {
+ alt((
+ value(VerticalAlign::Top, tag("^")),
+ value(VerticalAlign::Middle, tag("-")),
+ value(VerticalAlign::Bottom, tag("~")),
+ ))(input)
+}
A src/structs.rs => src/structs.rs +129 -0
@@ 0,0 1,129 @@
+#[derive(Debug, PartialEq)]
+pub struct Textile(pub Vec<BlockTag>);
+
+#[derive(Debug, PartialEq)]
+pub enum BlockTag {
+ Basic {
+ kind: BlockKind,
+ indent: Option<Indent>,
+ align: Option<Align>,
+ attributes: Option<Attributes>,
+ content: InlineTag,
+ },
+ Preformatted {
+ kind: BlockKind,
+ indent: Option<Indent>,
+ align: Option<Align>,
+ attributes: Option<Attributes>,
+ content: String,
+ },
+ List {
+ indent: Option<Indent>,
+ align: Option<Align>,
+ attributes: Option<Attributes>,
+ content: List,
+ },
+ NoTextile(String),
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum BlockKind {
+ Paragraph,
+ NoTextile,
+ BlockQuote,
+ BlockCode,
+ Preformatted,
+ Header(usize),
+ Footnote(usize),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct List {
+ pub kind: ListKind,
+ pub items: Vec<ListItem>,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ListItem {
+ pub content: InlineTag,
+ pub sublist: Option<List>,
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum ListKind {
+ Numeric,
+ Bulleted,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Indent {
+ pub left: usize,
+ pub right: usize,
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum Align {
+ Left,
+ Right,
+ Center,
+ Justify,
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum VerticalAlign {
+ Top,
+ Middle,
+ Bottom,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Attributes {
+ pub class: Option<String>,
+ pub id: Option<String>,
+ pub style: Option<String>,
+ pub language: Option<String>,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum InlineTag {
+ Plaintext(String),
+ NoTextile(String),
+ Code(String),
+ FootnoteRef(usize),
+ LineBreak,
+ Image {
+ attributes: Option<Attributes>,
+ align: Option<Align>,
+ url: String,
+ alt: Option<String>,
+ },
+ Acronym {
+ title: Option<String>,
+ content: String,
+ },
+ Link {
+ attributes: Option<Attributes>,
+ title: Option<String>,
+ url: String,
+ content: Box<InlineTag>,
+ },
+ Phrase {
+ kind: Option<PhraseKind>,
+ attributes: Option<Attributes>,
+ content: Vec<InlineTag>,
+ },
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum PhraseKind {
+ Italic,
+ Bold,
+ Emphasis,
+ Strong,
+ Citation,
+ Deleted,
+ Inserted,
+ Superscript,
+ Subscript,
+ Span,
+}
D tests/attributes.rs => tests/attributes.rs +0 -73
@@ 1,73 0,0 @@
-use flatiron::parse::*;
-
-#[test]
-fn attributes_csl() {
- let input = "(class#id){style}[language]";
- let result = attributes(input);
- assert_eq!(
- result,
- Ok((
- "",
- Attributes {
- class: Some(String::from("class")),
- id: Some(String::from("id")),
- style: Some(String::from("style")),
- language: Some(String::from("language")),
- }
- ))
- );
-}
-
-#[test]
-fn attributes_scl() {
- let input = "{style}(class#id)[language]";
- let result = attributes(input);
- assert_eq!(
- result,
- Ok((
- "",
- Attributes {
- class: Some(String::from("class")),
- id: Some(String::from("id")),
- style: Some(String::from("style")),
- language: Some(String::from("language")),
- }
- ))
- );
-}
-
-#[test]
-fn attributes_lcs() {
- let input = "[language]{style}(class#id)";
- let result = attributes(input);
- assert_eq!(
- result,
- Ok((
- "",
- Attributes {
- class: Some(String::from("class")),
- id: Some(String::from("id")),
- style: Some(String::from("style")),
- language: Some(String::from("language")),
- }
- ))
- );
-}
-
-#[test]
-fn attributes_class() {
- let input = "(class)";
- let result = attributes(input);
- assert_eq!(
- result,
- Ok((
- "",
- Attributes {
- class: Some(String::from("class")),
- id: None,
- style: None,
- language: None,
- }
- ))
- );
-}
D tests/block.rs => tests/block.rs +0 -275
@@ 1,275 0,0 @@
-use flatiron::parse::*;
-
-#[test]
-fn block_basic() {
- let input = "Untagged paragraph";
- let result = block(input);
- assert_eq!(
- result,
- Ok((
- "",
- BlockTag::Basic {
- kind: BlockKind::Paragraph,
- 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,
- 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,
- 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,
- 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),
- 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),
- 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),
- 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,
- 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,
- 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,
- 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,
- 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"
- )
- }
- ))
- );
-}
D tests/link.rs => tests/link.rs +0 -107
@@ 1,107 0,0 @@
-use flatiron::parse::*;
-
-#[test]
-fn link_basic() {
- let input = "\"a link\":https://github.com/autumnull/flatiron";
- let result = link(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: None,
- title: None,
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Plaintext(String::from("a link")))
- }
- ))
- );
-}
-
-#[test]
-fn link_attributes() {
- let input = "\"(class)a link\":https://github.com/autumnull/flatiron";
- let result = link(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: Some(Attributes {
- class: Some(String::from("class")),
- id: None,
- style: None,
- language: None,
- }),
- title: None,
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Plaintext(String::from("a link")))
- }
- ))
- );
-}
-
-#[test]
-fn link_attributes_title() {
- let input =
- "\"(class)a link (title)\":https://github.com/autumnull/flatiron";
- let result = link(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: Some(Attributes {
- class: Some(String::from("class")),
- id: None,
- style: None,
- language: None,
- }),
- title: Some(String::from("title")),
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Plaintext(String::from("a link")))
- }
- ))
- );
-}
-
-#[test]
-fn link_escaped() {
- let input =
- "\"Earvin \\\"Magic\\\" Johnson \\(Basketball Player\\)\":https://en.wikipedia.org/wiki/Magic_Johnson";
- let result = link(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::Link {
- attributes: None,
- title: None,
- url: String::from(
- "https://en.wikipedia.org/wiki/Magic_Johnson"
- ),
- content: Box::new(InlineTag::Plaintext(String::from(
- "Earvin \"Magic\" Johnson (Basketball Player)"
- )))
- }
- ))
- );
-}
-
-#[test]
-fn link_parenthesized() {
- let input = "\"this\":(https://github.com/autumnull/flatiron)?";
- let result = link(input);
- assert_eq!(
- result,
- Ok((
- "?",
- InlineTag::Link {
- attributes: None,
- title: None,
- url: String::from("https://github.com/autumnull/flatiron"),
- content: Box::new(InlineTag::Plaintext(String::from("this")))
- }
- ))
- );
-}
D tests/notextile.rs => tests/notextile.rs +0 -26
@@ 1,26 0,0 @@
-use flatiron::parse::*;
-
-#[test]
-fn no_textile_basic() {
- let input = "==hello *world!*==";
- let result = no_textile(input);
- assert_eq!(
- result,
- Ok(("", InlineTag::NoTextile(String::from("hello *world!*",))))
- );
-}
-
-#[test]
-fn no_textile_with_equals() {
- let input = "==This is two equals signs: \\== here are two more: \\====";
- let result = no_textile(input);
- assert_eq!(
- result,
- Ok((
- "",
- InlineTag::NoTextile(String::from(
- "This is two equals signs: == here are two more: ==",
- ))
- ))
- );
-}
D tests/phrase.rs => tests/phrase.rs +0 -309
@@ 1,309 0,0 @@
-use flatiron::parse::*;
-
-#[test]
-fn empty_phrase() {
- let input = "";
- let result = phrase(input);
- assert_eq!(result, Ok(("", InlineTag::Plaintext(String::from("")))));
-}
-