2023-03-25 14:10:22 -04:00
use nom ::branch ::alt ;
2023-03-25 14:23:52 -04:00
use nom ::bytes ::complete ::tag ;
use nom ::character ::complete ::digit1 ;
2023-04-03 16:29:47 -04:00
use nom ::character ::complete ::line_ending ;
2023-03-25 14:23:52 -04:00
use nom ::character ::complete ::one_of ;
2023-03-25 14:10:22 -04:00
use nom ::character ::complete ::space0 ;
2023-04-03 16:29:47 -04:00
use nom ::character ::complete ::space1 ;
2023-03-25 14:10:22 -04:00
use nom ::combinator ::eof ;
2023-04-14 19:24:05 -04:00
use nom ::combinator ::peek ;
2023-03-25 14:10:22 -04:00
use nom ::combinator ::recognize ;
use nom ::combinator ::verify ;
2023-04-12 13:09:44 -04:00
use nom ::multi ::many1 ;
2023-03-27 12:52:49 -04:00
use nom ::multi ::many_till ;
2023-04-14 23:56:55 -04:00
use nom ::sequence ::preceded ;
use nom ::sequence ::terminated ;
2023-03-25 14:10:22 -04:00
use nom ::sequence ::tuple ;
2023-04-22 21:45:18 -04:00
use super ::greater_element ::PlainList ;
use super ::greater_element ::PlainListItem ;
use super ::parser_with_context ::parser_with_context ;
use super ::util ::non_whitespace_character ;
use super ::Context ;
use crate ::error ::CustomError ;
use crate ::error ::MyError ;
use crate ::error ::Res ;
use crate ::parser ::element_parser ::element ;
use crate ::parser ::exiting ::ExitClass ;
use crate ::parser ::parser_context ::ContextElement ;
use crate ::parser ::parser_context ::ExitMatcherNode ;
use crate ::parser ::util ::blank_line ;
use crate ::parser ::util ::exit_matcher_parser ;
use crate ::parser ::util ::get_consumed ;
use crate ::parser ::util ::start_of_line ;
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 14:28:48 -04:00
pub fn plain_list < ' r , ' s > ( context : Context < ' r , ' s > , input : & ' s str ) -> Res < & ' s str , PlainList < ' s > > {
2023-04-18 22:10:44 -04:00
let parser_context = context
. with_additional_node ( ContextElement ::Context ( " plain list " ) )
. with_additional_node ( ContextElement ::ExitMatcherNode ( ExitMatcherNode {
2023-04-18 20:33:01 -04:00
class : ExitClass ::Beta ,
exit_matcher : & plain_list_end ,
2023-04-12 13:09:44 -04:00
} ) ) ;
2023-04-03 19:30:17 -04:00
let mut children = Vec ::new ( ) ;
2023-04-14 19:24:05 -04:00
let mut first_item_indentation : Option < usize > = None ;
let mut remaining = input ;
2023-04-03 19:30:17 -04:00
loop {
2023-08-15 01:28:19 -04:00
let maybe_exit = parser_with_context! ( exit_matcher_parser ) ( & parser_context ) ( remaining ) ;
if maybe_exit . is_ok ( ) {
break ;
2023-04-14 19:56:54 -04:00
}
2023-08-15 01:28:19 -04:00
let list_item = parser_with_context! ( plain_list_item ) ( & parser_context ) ( remaining ) ;
match list_item {
Ok ( ( remain , item ) )
if item . indentation = = * first_item_indentation . get_or_insert ( item . indentation ) = >
{
remaining = remain ;
children . push ( item ) ;
continue ;
}
Ok ( _ ) | Err ( _ ) = > {
break ;
}
} ;
2023-04-03 19:30:17 -04:00
}
2023-04-10 11:50:43 -04:00
2023-04-14 19:24:05 -04:00
if children . is_empty ( ) {
return Err ( nom ::Err ::Error ( CustomError ::MyError ( MyError (
" Plain lists require at least one element. " ,
) ) ) ) ;
}
2023-08-15 01:28:19 -04:00
// TODO: trailing whitespace
2023-03-27 12:52:49 -04:00
let source = get_consumed ( input , remaining ) ;
Ok ( ( remaining , PlainList { source , children } ) )
2023-03-25 14:28:48 -04:00
}
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 13:25:20 -04:00
pub fn plain_list_item < ' r , ' s > (
context : Context < ' r , ' s > ,
input : & ' s str ,
2023-03-25 14:23:52 -04:00
) -> Res < & ' s str , PlainListItem < ' s > > {
2023-03-25 14:10:22 -04:00
start_of_line ( context , input ) ? ;
let ( remaining , leading_whitespace ) = space0 ( input ) ? ;
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09)
let indent_level = leading_whitespace . len ( ) ;
2023-04-03 16:38:26 -04:00
let ( remaining , bull ) =
verify ( bullet , | bull : & str | bull ! = " * " | | indent_level > 0 ) ( remaining ) ? ;
2023-08-15 02:02:15 -04:00
2023-04-03 16:29:47 -04:00
let maybe_contentless_item : Res < & str , & str > = alt ( ( eof , line_ending ) ) ( remaining ) ;
match maybe_contentless_item {
Ok ( ( rem , _ws ) ) = > {
let source = get_consumed ( input , rem ) ;
return Ok ( (
rem ,
PlainListItem {
source ,
indentation : indent_level ,
bullet : bull ,
2023-04-03 17:33:01 -04:00
children : Vec ::new ( ) ,
2023-04-03 16:29:47 -04:00
} ,
) ) ;
}
2023-08-15 02:02:15 -04:00
Err ( _ ) = > { }
2023-04-03 16:29:47 -04:00
} ;
2023-08-15 02:02:15 -04:00
let ( remaining , _ws ) = space1 ( remaining ) ? ;
let parser_context = context
. with_additional_node ( ContextElement ::ListItem ( indent_level ) )
. with_additional_node ( ContextElement ::ExitMatcherNode ( ExitMatcherNode {
class : ExitClass ::Beta ,
exit_matcher : & plain_list_item_end ,
} ) ) ;
let ( remaining , ( children , _exit_contents ) ) = verify (
many_till (
parser_with_context! ( element ( true ) ) ( & parser_context ) ,
parser_with_context! ( exit_matcher_parser ) ( & parser_context ) ,
) ,
| ( children , _exit_contents ) | ! children . is_empty ( ) ,
) ( remaining ) ? ;
let source = get_consumed ( input , remaining ) ;
return Ok ( (
remaining ,
PlainListItem {
source ,
indentation : indent_level ,
bullet : bull ,
children ,
} ,
) ) ;
2023-03-25 14:23:52 -04:00
}
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 14:23:52 -04:00
fn bullet < ' s > ( i : & ' s str ) -> Res < & ' s str , & ' s str > {
alt ( (
tag ( " * " ) ,
tag ( " - " ) ,
tag ( " + " ) ,
recognize ( tuple ( ( counter , alt ( ( tag ( " . " ) , tag ( " ) " ) ) ) ) ) ) ,
) ) ( i )
}
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 14:23:52 -04:00
fn counter < ' s > ( i : & ' s str ) -> Res < & ' s str , & ' s str > {
alt ( ( recognize ( one_of ( " abcdefghijklmnopqrstuvwxyz " ) ) , digit1 ) ) ( i )
2023-03-25 13:25:20 -04:00
}
2023-03-25 14:10:22 -04:00
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-04-12 13:09:44 -04:00
fn plain_list_end < ' r , ' s > ( context : Context < ' r , ' s > , input : & ' s str ) -> Res < & ' s str , & ' s str > {
let start_of_line_matcher = parser_with_context! ( start_of_line ) ( context ) ;
recognize ( tuple ( (
start_of_line_matcher ,
verify ( many1 ( blank_line ) , | lines : & Vec < & str > | lines . len ( ) > = 2 ) ,
) ) ) ( input )
}
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 14:10:22 -04:00
fn plain_list_item_end < ' r , ' s > ( context : Context < ' r , ' s > , input : & ' s str ) -> Res < & ' s str , & ' s str > {
2023-08-16 16:05:16 -04:00
recognize ( parser_with_context! ( line_indented_lte ) ( context ) ) ( input )
2023-03-25 14:10:22 -04:00
}
2023-08-10 20:04:59 -04:00
#[ cfg_attr(feature = " tracing " , tracing::instrument(ret, level = " debug " )) ]
2023-03-25 14:10:22 -04:00
fn line_indented_lte < ' r , ' s > ( context : Context < ' r , ' s > , input : & ' s str ) -> Res < & ' s str , & ' s str > {
let current_item_indent_level : & usize =
get_context_item_indent ( context ) . ok_or ( nom ::Err ::Error ( CustomError ::MyError ( MyError (
" Not inside a plain list item " ,
) ) ) ) ? ;
start_of_line ( context , input ) ? ;
let matched = recognize ( verify (
tuple ( ( space0 ::< & str , _ > , non_whitespace_character ) ) ,
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09)
| ( _space0 , _anychar ) | _space0 . len ( ) < = * current_item_indent_level ,
) ) ( input ) ? ;
Ok ( matched )
}
fn get_context_item_indent < ' r , ' s > ( context : Context < ' r , ' s > ) -> Option < & ' r usize > {
for thing in context . iter ( ) {
match thing . get_data ( ) {
ContextElement ::ListItem ( depth ) = > return Some ( depth ) ,
_ = > { }
} ;
}
None
}
2023-03-25 14:28:48 -04:00
#[ cfg(test) ]
mod tests {
2023-04-22 21:45:18 -04:00
use super ::* ;
2023-03-25 14:28:48 -04:00
use crate ::parser ::parser_context ::ContextElement ;
use crate ::parser ::parser_context ::ContextTree ;
use crate ::parser ::parser_with_context ::parser_with_context ;
2023-04-12 13:09:44 -04:00
use crate ::parser ::Source ;
2023-03-25 14:28:48 -04:00
#[ test ]
fn plain_list_item_empty ( ) {
let input = " 1. " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_item_matcher = parser_with_context! ( plain_list_item ) ( & document_context ) ;
let ( remaining , result ) = plain_list_item_matcher ( input ) . unwrap ( ) ;
assert_eq! ( remaining , " " ) ;
assert_eq! ( result . source , " 1. " ) ;
}
2023-03-25 14:45:35 -04:00
#[ test ]
fn plain_list_item_simple ( ) {
let input = " 1. foo " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_item_matcher = parser_with_context! ( plain_list_item ) ( & document_context ) ;
let ( remaining , result ) = plain_list_item_matcher ( input ) . unwrap ( ) ;
assert_eq! ( remaining , " " ) ;
assert_eq! ( result . source , " 1. foo " ) ;
}
2023-03-27 12:52:49 -04:00
#[ test ]
fn plain_list_empty ( ) {
let input = " 1. " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_matcher = parser_with_context! ( plain_list ) ( & document_context ) ;
let ( remaining , result ) = plain_list_matcher ( input ) . unwrap ( ) ;
assert_eq! ( remaining , " " ) ;
assert_eq! ( result . source , " 1. " ) ;
}
#[ test ]
fn plain_list_simple ( ) {
let input = " 1. foo " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_matcher = parser_with_context! ( plain_list ) ( & document_context ) ;
let ( remaining , result ) = plain_list_matcher ( input ) . unwrap ( ) ;
assert_eq! ( remaining , " " ) ;
assert_eq! ( result . source , " 1. foo " ) ;
}
2023-04-03 16:38:26 -04:00
#[ test ]
fn plain_list_cant_start_line_with_asterisk ( ) {
// Plain lists with an asterisk bullet must be indented or else they would be a headline
let input = " * foo " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_matcher = parser_with_context! ( plain_list ) ( & document_context ) ;
let result = plain_list_matcher ( input ) ;
assert! ( result . is_err ( ) ) ;
}
#[ test ]
fn indented_can_start_line_with_asterisk ( ) {
// Plain lists with an asterisk bullet must be indented or else they would be a headline
let input = " * foo " ;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
let plain_list_matcher = parser_with_context! ( plain_list ) ( & document_context ) ;
let result = plain_list_matcher ( input ) ;
assert! ( result . is_ok ( ) ) ;
}
2023-04-12 13:09:44 -04:00
#[ test ]
fn two_blank_lines_ends_list ( ) {
let input = r #" 1. foo
2. bar
baz
3. lorem
ipsum
" #;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
2023-04-22 01:45:38 -04:00
let plain_list_matcher = parser_with_context! ( element ( true ) ) ( & document_context ) ;
2023-04-12 13:09:44 -04:00
let ( remaining , result ) =
plain_list_matcher ( input ) . expect ( " Should parse the plain list successfully. " ) ;
assert_eq! ( remaining , " ipsum \n " ) ;
assert_eq! (
result . get_source ( ) ,
r #" 1. foo
2. bar
baz
3. lorem
2023-04-12 13:21:31 -04:00
" #
) ;
}
#[ test ]
fn two_blank_lines_ends_nested_list ( ) {
let input = r #" 1. foo
1. bar
baz " #;
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
2023-04-22 01:45:38 -04:00
let plain_list_matcher = parser_with_context! ( element ( true ) ) ( & document_context ) ;
2023-04-12 13:21:31 -04:00
let ( remaining , result ) =
plain_list_matcher ( input ) . expect ( " Should parse the plain list successfully. " ) ;
assert_eq! ( remaining , " baz " ) ;
assert_eq! (
result . get_source ( ) ,
r #" 1. foo
1. bar
2023-04-12 15:24:18 -04:00
" #
) ;
}
#[ test ]
fn interior_trailing_whitespace ( ) {
let input = r #" 1. foo
bar
1. baz
lorem
2023-04-14 19:24:05 -04:00
ipsum
2023-04-12 15:24:18 -04:00
2023-04-14 19:24:05 -04:00
dolar " #;
2023-04-12 15:24:18 -04:00
let initial_context : ContextTree < '_ , '_ > = ContextTree ::new ( ) ;
let document_context =
initial_context . with_additional_node ( ContextElement ::DocumentRoot ( input ) ) ;
2023-04-22 01:45:38 -04:00
let plain_list_matcher = parser_with_context! ( element ( true ) ) ( & document_context ) ;
2023-04-12 15:24:18 -04:00
let ( remaining , result ) =
plain_list_matcher ( input ) . expect ( " Should parse the plain list successfully. " ) ;
2023-04-14 19:24:05 -04:00
assert_eq! ( remaining , " dolar " ) ;
2023-04-12 15:24:18 -04:00
assert_eq! (
result . get_source ( ) ,
r #" 1. foo
bar
1. baz
lorem
2023-04-14 19:24:05 -04:00
ipsum
2023-04-12 15:24:18 -04:00
2023-04-12 13:09:44 -04:00
" #
) ;
}
2023-03-25 14:28:48 -04:00
}