Skip to content

Commit c8f4e06

Browse files
authored
Allow quoted tag parameters (#11)
* Allow quoted parameters * Allow escaping of special characters in text
1 parent fcec8aa commit c8f4e06

File tree

1 file changed

+124
-16
lines changed

1 file changed

+124
-16
lines changed

src/bbcode/parser.rs

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,54 @@ use std::sync::Arc;
22

33
use nom::{
44
branch::alt,
5-
bytes::complete::{tag, tag_no_case, take_while1},
5+
bytes::complete::{is_not, tag, tag_no_case},
66
character::complete::{alpha1, char},
7-
combinator::map,
8-
multi::many0,
7+
combinator::{map, opt, value, verify},
8+
error::ParseError,
9+
multi::{fold_many1, many0},
910
sequence::{delimited, preceded},
10-
IResult,
11+
IResult, Parser,
1112
};
1213

1314
use super::{BbcodeNode, BbcodeTag};
1415

16+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17+
enum StringFragment<'a> {
18+
Literal(&'a str),
19+
EscapedChar(char),
20+
}
21+
1522
pub fn parse_bbcode(input: &str) -> IResult<&str, Vec<Arc<BbcodeNode>>> {
23+
parse_bbcode_internal(input)
24+
}
25+
26+
fn parse_bbcode_internal<'a, E: ParseError<&'a str>>(
27+
input: &'a str,
28+
) -> IResult<&'a str, Vec<Arc<BbcodeNode>>, E> {
1629
many0(map(parse_node, |element| element.into()))(input)
1730
}
1831

19-
fn parse_node(input: &str) -> IResult<&str, BbcodeNode> {
32+
fn parse_node<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, BbcodeNode, E> {
2033
alt((
21-
map(parse_text, |text| BbcodeNode::Text(text.into())),
34+
map(parse_text, BbcodeNode::Text),
2235
map(parse_tag, BbcodeNode::Tag),
2336
))(input)
2437
}
2538

26-
fn parse_tag(input: &str) -> IResult<&str, BbcodeTag> {
39+
fn parse_tag<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, BbcodeTag, E> {
2740
let (input, mut tag) = parse_opening_tag(input)?;
28-
let (input, children) = parse_bbcode(input)?;
41+
let (input, children) = parse_bbcode_internal(input)?;
2942
let (input, _) = parse_closing_tag(input, &tag.name)?;
3043

3144
tag.children = children;
3245

3346
Ok((input, tag))
3447
}
3548

36-
fn parse_opening_tag(input: &str) -> IResult<&str, BbcodeTag> {
49+
fn parse_opening_tag<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, BbcodeTag, E> {
3750
let (mut input, mut tag) = map(preceded(char('['), alpha1), BbcodeTag::new)(input)?;
3851

39-
if let Ok((new_input, simple_param)) = preceded(char('='), parse_param)(input) {
52+
if let Ok((new_input, simple_param)) = preceded(char('='), parse_param::<E>)(input) {
4053
tag.add_simple_param(simple_param);
4154
input = new_input;
4255
}
@@ -46,20 +59,91 @@ fn parse_opening_tag(input: &str) -> IResult<&str, BbcodeTag> {
4659
Ok((input, tag))
4760
}
4861

49-
fn parse_closing_tag<'a>(input: &'a str, tag_name: &str) -> IResult<&'a str, ()> {
62+
fn parse_closing_tag<'a, E: ParseError<&'a str>>(
63+
input: &'a str,
64+
tag_name: &str,
65+
) -> IResult<&'a str, (), E> {
5066
map(
5167
delimited(tag("[/"), tag_no_case(tag_name), char(']')),
5268
|_| (),
5369
)(input)
5470
}
5571

56-
fn parse_text(input: &str) -> IResult<&str, &str> {
57-
take_while1(|ch| !['[', ']'].contains(&ch))(input)
72+
fn parse_text<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, String, E> {
73+
parse_inner_string("[]\\").parse(input)
74+
}
75+
76+
fn parse_param<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, String, E> {
77+
alt((
78+
parse_quoted_string,
79+
map(parse_literal("\"\\[]"), |literal| literal.to_string()),
80+
))
81+
.parse(input)
82+
}
83+
84+
fn parse_quoted_string<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, String, E> {
85+
delimited(
86+
char('"'),
87+
map(opt(parse_inner_string("\"\\")), |string| {
88+
string.unwrap_or_default()
89+
}),
90+
char('"'),
91+
)
92+
.parse(input)
93+
}
94+
95+
fn parse_inner_string<'a, E: ParseError<&'a str>>(
96+
exclude: &'a str,
97+
) -> impl Parser<&'a str, String, E> {
98+
move |input| {
99+
fold_many1(
100+
parse_fragment(exclude),
101+
String::new,
102+
|mut string, fragment| {
103+
match fragment {
104+
StringFragment::Literal(s) => string.push_str(s),
105+
StringFragment::EscapedChar(c) => string.push(c),
106+
}
107+
string
108+
},
109+
)
110+
.parse(input)
111+
}
112+
}
113+
114+
fn parse_fragment<'a, E: ParseError<&'a str>>(
115+
exclude: &'a str,
116+
) -> impl Parser<&'a str, StringFragment<'a>, E> {
117+
move |input| {
118+
alt((
119+
map(parse_literal(exclude), StringFragment::Literal),
120+
map(parse_escaped_char, StringFragment::EscapedChar),
121+
))
122+
.parse(input)
123+
}
124+
}
125+
126+
fn parse_literal<'a, E: ParseError<&'a str>>(exclude: &'a str) -> impl Parser<&'a str, &'a str, E> {
127+
move |input| verify(is_not(exclude), |s: &str| !s.is_empty()).parse(input)
58128
}
59129

60-
fn parse_param(input: &str) -> IResult<&str, &str> {
61-
// TODO: Quote delimited params
62-
take_while1(|ch| !['[', ']', ' ', '='].contains(&ch))(input)
130+
fn parse_escaped_char<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, char, E> {
131+
preceded(
132+
char('\\'),
133+
alt((
134+
value('"', char('"')),
135+
value('/', char('/')),
136+
value('[', char('[')),
137+
value(']', char(']')),
138+
value('\\', char('\\')),
139+
value('\n', char('n')),
140+
value('\r', char('r')),
141+
value('\t', char('t')),
142+
value('\u{08}', char('b')),
143+
value('\u{0C}', char('f')),
144+
)),
145+
)
146+
.parse(input)
63147
}
64148

65149
#[cfg(test)]
@@ -77,6 +161,17 @@ mod tests {
77161
)
78162
}
79163

164+
#[test]
165+
fn test_parse_escaped_text() {
166+
let input = r#"[b]\[\]\\\"\t\n[/b]"#;
167+
let expected_tag = BbcodeTag::new("b").with_text("[]\\\"\t\n");
168+
169+
assert_eq!(
170+
parse_bbcode(input),
171+
Ok(("", vec![BbcodeNode::Tag(expected_tag).into()]))
172+
)
173+
}
174+
80175
#[test]
81176
fn test_parse_simple_param() {
82177
let input = "[c=#ff00ff]test[/c]";
@@ -90,6 +185,19 @@ mod tests {
90185
)
91186
}
92187

188+
#[test]
189+
fn test_parse_quoted_param() {
190+
let input = r#"[c="dark \"blue\" with yellow"]test[/c]"#;
191+
let expected_tag = BbcodeTag::new("c")
192+
.with_simple_param(r#"dark "blue" with yellow"#)
193+
.with_text("test");
194+
195+
assert_eq!(
196+
parse_bbcode(input),
197+
Ok(("", vec![BbcodeNode::Tag(expected_tag).into()]))
198+
)
199+
}
200+
93201
#[test]
94202
fn test_parse_nested() {
95203
let input = "[b]test [i]nested[/i][/b]";

0 commit comments

Comments
 (0)