use crate::*;
#[derive(Debug)]
pub struct LineParser<'s> {
src: &'s str,
idx: usize, pub(crate) code: bool,
pub(crate) italic: bool,
pub(crate) bold: bool,
pub(crate) strikeout: bool,
}
impl<'s> LineParser<'s> {
pub fn from(src: &'s str) -> LineParser<'s> {
LineParser {
src,
idx: 0,
bold: false,
italic: false,
code: false,
strikeout: false,
}
}
fn close_compound(
&mut self,
end: usize,
tag_length: usize,
compounds: &mut Vec<Compound<'s>>,
) {
if end > self.idx {
compounds.push(Compound::new(
self.src,
self.idx,
end,
self.bold,
self.italic,
self.code,
self.strikeout,
));
}
self.idx = end + tag_length;
}
fn code_block_compound_from_idx(
&self,
idx: usize,
) -> Compound<'s> {
Compound::new(self.src, idx, self.src.len(), false, false, false, false)
}
fn parse_compounds(
&mut self,
stop_on_pipe: bool,
) -> Vec<Compound<'s>> {
let mut compounds = Vec::new();
let mut after_first_star = false;
let mut after_first_tilde = false;
let mut after_antislash = false;
let chars_to_skip = self.src[..self.idx].chars().count();
for (idx, char) in self.src.char_indices().skip(chars_to_skip) {
if self.code {
if char == '`' {
self.close_compound(idx, 1, &mut compounds);
self.code = false;
}
after_antislash = false;
after_first_star = false;
continue;
}
#[cfg(feature = "escaping")]
if after_antislash {
after_antislash = false;
match char {
'*' | '~' | '|' | '`' => {
self.close_compound(idx - 1, 1, &mut compounds);
continue;
}
'\\' => {
self.close_compound(idx, 1, &mut compounds);
continue;
}
_ => {} }
} else if char == '\\' {
after_antislash = true;
continue;
}
if after_first_star {
match char {
'*' => {
self.close_compound(idx - 1, 2, &mut compounds);
self.bold ^= true;
}
'~' => {
after_first_tilde = true;
self.close_compound(idx - 1, 2, &mut compounds);
self.italic ^= true;
}
'|' if stop_on_pipe => {
self.close_compound(idx - 1, 1, &mut compounds);
return compounds;
}
'`' => {
self.close_compound(idx - 1, 2, &mut compounds);
self.italic ^= true;
self.code = true;
}
_ => {
self.close_compound(idx - 1, 1, &mut compounds);
self.italic ^= true;
}
}
after_first_star = false;
} else if after_first_tilde {
match char {
'*' => {
after_first_star = true;
}
'~' => {
self.close_compound(idx - 1, 2, &mut compounds);
self.strikeout ^= true;
}
'|' if stop_on_pipe => {
self.close_compound(idx - 1, 1, &mut compounds);
return compounds;
}
_ => {
}
}
after_first_tilde = false;
} else {
match char {
'*' => {
after_first_star = true;
}
'~' => {
after_first_tilde = true;
}
'|' if stop_on_pipe => {
self.close_compound(idx, 0, &mut compounds);
return compounds;
}
'`' => {
self.close_compound(idx, 1, &mut compounds);
self.code = true;
}
_ => {}
}
}
}
let mut idx = self.src.len();
if after_first_star && self.italic {
idx -= 1;
}
if after_first_tilde && self.strikeout {
idx -= 1;
}
self.close_compound(idx, 0, &mut compounds);
compounds
}
fn parse_cells(&mut self) -> Vec<Composite<'s>> {
let mut cells = Vec::new();
while self.idx < self.src.len() {
self.idx += 1;
let style = if self.src[self.idx..].starts_with("* ") {
self.idx += 2;
CompositeStyle::ListItem(0)
} else if self.src[self.idx..].starts_with(" * ") {
self.idx += 3;
CompositeStyle::ListItem(1)
} else if self.src[self.idx..].starts_with(" * ") {
self.idx += 4;
CompositeStyle::ListItem(2)
} else if self.src[self.idx..].starts_with(" * ") {
self.idx += 5;
CompositeStyle::ListItem(3)
} else if self.src[self.idx..].starts_with("> ") {
self.idx += 2;
CompositeStyle::Quote
} else {
CompositeStyle::Paragraph
};
self.bold = false;
self.italic = false;
self.code = false;
self.strikeout = false;
let compounds = self.parse_compounds(true);
let mut composite = Composite { style, compounds };
composite.trim_spaces();
cells.push(composite);
}
if !cells.is_empty() && cells[cells.len() - 1].compounds.is_empty() {
cells.pop();
}
cells
}
pub fn inline(mut self) -> Composite<'s> {
Composite {
style: CompositeStyle::Paragraph,
compounds: self.parse_compounds(false),
}
}
pub fn as_code(mut self) -> Line<'s> {
if self.src.starts_with("```") {
self.idx = 3;
Line::new_code_fence(self.parse_compounds(false))
} else {
Line::new_code(self.code_block_compound_from_idx(0))
}
}
pub fn line(mut self) -> Line<'s> {
self.parse_line()
}
pub(crate) fn parse_line(&mut self) -> Line<'s> {
if self.src.starts_with('|') {
let tr = TableRow {
cells: self.parse_cells(),
};
return match tr.as_table_alignments() {
Some(aligns) => Line::TableRule(aligns),
None => Line::TableRow(tr),
};
}
if self.src.starts_with(" ") {
return Line::new_code(self.code_block_compound_from_idx(4));
}
if self.src.starts_with('\t') {
return Line::new_code(self.code_block_compound_from_idx(1));
}
if self.src.starts_with("* ") {
self.idx = 2;
return Line::new_list_item(0, self.parse_compounds(false));
}
if self.src.starts_with(" * ") {
self.idx = 3;
return Line::new_list_item(1, self.parse_compounds(false));
}
if self.src.starts_with(" * ") {
self.idx = 4;
return Line::new_list_item(2, self.parse_compounds(false));
}
if self.src.starts_with(" * ") {
self.idx = 5;
return Line::new_list_item(3, self.parse_compounds(false));
}
if self.src == ">" {
return Line::new_quote(Vec::new());
}
if self.src.starts_with("> ") {
self.idx = 2;
return Line::new_quote(self.parse_compounds(false));
}
if self.src.starts_with("```") {
self.idx = 3;
return Line::new_code_fence(self.parse_compounds(false));
}
let header_level = header_level(self.src);
if header_level > 0 {
self.idx = header_level + 1;
return Line::new_header(header_level as u8, self.parse_compounds(false));
}
let compounds = self.parse_compounds(false);
if compounds_are_rule(&compounds) {
Line::HorizontalRule
} else {
Line::new_paragraph(compounds)
}
}
}
const DASH: u8 = 45;
fn compounds_are_rule(compounds: &[Compound<'_>]) -> bool {
if compounds.len() != 1 {
return false;
}
let s = compounds[0].as_str();
if s.len() < 3 {
return false;
}
for c in s.as_bytes() {
if *c != DASH {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn simple_line_parsing() {
assert_eq!(
Line::from("Hello ~~wolrd~~ **World**. *Code*: `sqrt(π/2)`"),
Line::new_paragraph(vec![
Compound::raw_str("Hello "),
Compound::raw_str("wolrd").strikeout(),
Compound::raw_str(" "),
Compound::raw_str("World").bold(),
Compound::raw_str(". "),
Compound::raw_str("Code").italic(),
Compound::raw_str(": "),
Compound::raw_str("sqrt(π/2)").code(),
])
);
}
#[test]
fn nested_styles_parsing() {
assert_eq!(
Line::from("*Italic then **bold and italic `and some *code*`** and italic*"),
Line::new_paragraph(vec![
Compound::raw_str("Italic then ").italic(),
Compound::raw_str("bold and italic ").bold().italic(),
Compound::raw_str("and some *code*").bold().italic().code(),
Compound::raw_str(" and italic").italic(),
])
);
}
#[test]
fn quote() {
assert_eq!(
Line::from("> Veni, vidi, *vici*!"),
Line::new_quote(vec![
Compound::raw_str("Veni, vidi, "),
Compound::raw_str("vici").italic(),
Compound::raw_str("!"),
])
);
}
#[test]
fn code_after_italic() {
assert_eq!(
Line::from("*name=*`code`"),
Line::new_paragraph(vec![
Compound::raw_str("name=").italic(),
Compound::raw_str("code").code(),
])
);
}
#[test]
fn single_star() {
assert_eq!(
Line::from("*"),
Line::new_paragraph(vec![Compound::raw_str("*"),])
);
}
#[test]
fn single_tilde() {
assert_eq!(
Line::from("~"),
Line::new_paragraph(vec![Compound::raw_str("~"),])
);
}
#[test]
fn striked_after_italic() {
assert_eq!(
Line::from("*italic*~~striked~~"),
Line::new_paragraph(vec![
Compound::raw_str("italic").italic(),
Compound::raw_str("striked").strikeout(),
])
);
}
#[test]
fn tight_sequence() {
assert_eq!(
Line::from(
"*italic*`code`**bold**`code`*italic**italic+bold***`code`*I*~~striked~~*I*"
),
Line::new_paragraph(vec![
Compound::raw_str("italic").italic(),
Compound::raw_str("code").code(),
Compound::raw_str("bold").bold(),
Compound::raw_str("code").code(),
Compound::raw_str("italic").italic(),
Compound::raw_str("italic+bold").italic().bold(),
Compound::raw_str("code").code(),
Compound::raw_str("I").italic(),
Compound::raw_str("striked").strikeout(),
Compound::raw_str("I").italic(),
])
);
}
#[cfg(feature = "escaping")]
#[test]
fn escapes() {
assert_eq!(
Line::from("no \\*italic\\* here"),
Line::new_paragraph(vec![
Compound::raw_str("no "),
Compound::raw_str("*italic"),
Compound::raw_str("* here"),
])
);
assert_eq!(
Line::from("a\\bc\\"),
Line::new_paragraph(vec![Compound::raw_str("a\\bc\\"),])
);
assert_eq!(
Line::from("*italic\\*and\\*still\\*italic*"),
Line::new_paragraph(vec![
Compound::raw_str("italic").italic(),
Compound::raw_str("*and").italic(),
Compound::raw_str("*still").italic(),
Compound::raw_str("*italic").italic(),
])
);
assert_eq!(
Line::from(
"\\**Italic then **bold\\\\ and \\`italic `and some *code*`** and italic*\\*"
),
Line::new_paragraph(vec![
Compound::raw_str("*"),
Compound::raw_str("Italic then ").italic(),
Compound::raw_str("bold\\").bold().italic(),
Compound::raw_str(" and ").bold().italic(),
Compound::raw_str("`italic ").bold().italic(),
Compound::raw_str("and some *code*").bold().italic().code(),
Compound::raw_str(" and italic*").italic(),
])
);
}
#[test]
fn code_fence() {
assert_eq!(Line::from("```"), Line::new_code_fence(vec![]));
assert_eq!(
Line::from("```rust"),
Line::new_code_fence(vec![Compound::raw_str("rust"),]),
);
}
#[test]
fn line_of_code() {
assert_eq!(
Line::from(" let r = Math.sin(π/2) * 7"),
Line::new_code(Compound::raw_str("let r = Math.sin(π/2) * 7"))
);
}
#[test]
fn standard_header() {
assert_eq!(
Line::from("### just a title"),
Line::new_header(3, vec![Compound::raw_str("just a title"),])
);
}
#[test]
fn list_item() {
assert_eq!(
Line::from("* *list* item"),
Line::new_list_item(
0,
vec![
Compound::raw_str("list").italic(),
Compound::raw_str(" item"),
]
)
);
}
#[test]
fn deep_list_items() {
assert_eq!(
Line::from(" * *list* item"),
Line::new_list_item(
1,
vec![
Compound::raw_str("list").italic(),
Compound::raw_str(" item"),
]
)
);
assert_eq!(
Line::from(" * deeper"),
Line::new_list_item(2, vec![Compound::raw_str("deeper"),])
);
assert_eq!(
Line::from(" * even **deeper**"),
Line::new_list_item(
3,
vec![
Compound::raw_str("even "),
Compound::raw_str("deeper").bold(),
]
)
);
assert_eq!(
Line::from(" * but not this one..."),
Line::new_code(Compound::raw_str("* but not this one...")),
);
}
#[test]
fn horizontal_rule() {
assert_eq!(Line::from("----------"), Line::HorizontalRule,);
}
#[test]
fn styled_header() {
assert_eq!(
Line::from("## a header with some **bold**!"),
Line::new_header(
2,
vec![
Compound::raw_str("a header with some "),
Compound::raw_str("bold").bold(),
Compound::raw_str("!"),
]
)
);
}
#[test]
fn table_row() {
assert_eq!(
Line::from("| bla |*italic*|hi!|> some quote"),
Line::new_table_row(vec![
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("bla"),],
},
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("italic").italic(),],
},
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("hi!"),],
},
Composite {
style: CompositeStyle::Quote,
compounds: vec![Compound::raw_str("some quote"),],
}
])
);
}
#[test]
fn table_row_issue_4() {
assert_eq!(
Line::from("| 安 | 安 | 安 |"),
Line::new_table_row(vec![
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("安"),],
},
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("安"),],
},
Composite {
style: CompositeStyle::Paragraph,
compounds: vec![Compound::raw_str("安"),],
},
])
);
}
#[test]
fn table_alignments() {
assert_eq!(
Line::from("|-----|:--|:-:|----:"),
Line::new_table_alignments(vec![
Alignment::Unspecified,
Alignment::Left,
Alignment::Center,
Alignment::Right,
])
);
}
}