use crate::{util::ws, KconfigInput};
use nom::bytes::complete::take_while;
use nom::character::complete::space0;
use nom::combinator::recognize;
use nom::multi::fold_many0;
use nom::sequence::terminated;
use nom::Parser;
use nom::{
branch::alt,
bytes::complete::{tag, take},
character::complete::{line_ending, newline, not_line_ending, space1},
combinator::{eof, map, opt, peek},
multi::many0,
sequence::delimited,
IResult,
};
pub fn weirdo_help(input: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
map(
delimited(
ws(opt(many0(tag("-")))),
ws(tag("help")),
opt(many0(alt((tag("-"), space1)))),
),
|d| d,
)
.parse(input)
}
pub fn parse_help(input: KconfigInput) -> IResult<KconfigInput, String> {
let (input, _) = (
alt((
ws(tag("help")),
weirdo_help,
)),
many0(space1),
newline,
)
.parse(input)?;
let (input, text) = parse_help_text(input)?;
Ok((input, text))
}
fn parse_help_text(input: KconfigInput) -> IResult<KconfigInput, String> {
let (original, initial_indentation_len) = peek_initial_indentation(input)?;
if initial_indentation_len.chars == 0 {
return Ok((original, String::new()));
}
let (remaining, help_text) = fold_many0(
|i| {
let (orig, indent_len) = peek_indentation(i)?;
let peek_line = peek_til_newline(orig)?;
if peek_line.1.fragment().trim() == "" {
parse_newline_only(peek_line.0)
} else if indent_len < initial_indentation_len {
Err(nom::Err::Error(nom::error::Error::new(
peek_line.1,
nom::error::ErrorKind::Fail,
)))
} else {
let (remain, _) =
take(indent_len.chars.min(initial_indentation_len.chars))(peek_line.0)?;
parse_full_help_line(remain)
}
},
String::new,
|mut acc, line| {
acc.push_str(&line);
acc
},
)
.parse(original)?;
Ok((remaining, help_text.trim_end().to_string()))
}
fn parse_line_help(input: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
terminated(not_line_ending, opt(alt((line_ending, eof)))).parse(input)
}
fn parse_full_help_line(input: KconfigInput) -> IResult<KconfigInput, String> {
let (input, raw_text) = parse_line_help(input)?;
let mut parsed_line = raw_text.to_string();
parsed_line.push('\n');
Ok((input, parsed_line))
}
fn parse_newline_only(input: KconfigInput) -> IResult<KconfigInput, String> {
let (input, _newline) = recognize(terminated(space0, line_ending)).parse(input)?;
Ok((input, "\n".to_string()))
}
fn peek_til_newline(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
peek(parse_line_help).parse(s)
}
fn empty_line(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
recognize(terminated(space0, line_ending)).parse(s)
}
fn peek_first_non_empty_line(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
let (original, (_, non_empty_line)) = peek((many0(empty_line), not_line_ending)).parse(s)?;
Ok((original, non_empty_line))
}
fn peek_initial_indentation(s: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
let (original, peeked) = peek_first_non_empty_line.parse(s)?;
let (_, len) = indentation_level(peeked)?;
Ok((original, len))
}
fn peek_indentation(s: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
let (original, peeked) = peek_til_newline(s)?;
let (_, len) = indentation_level(peeked)?;
Ok((original, len))
}
fn indentation_level(input: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
let (input, indent) = take_while(|c: char| c == ' ' || c == '\t')(input)?;
let mut computed = 0;
for c in indent.fragment().chars() {
match c {
'\t' => {
computed = (computed & !7) + 8;
}
' ' => computed += 1,
_ => unreachable!(
"This should never happen because indentation only takes spaces and tabs"
),
}
}
Ok((
input,
IndentationLevel {
chars: indent.fragment().len(),
computed,
},
))
}
#[derive(PartialEq, Debug)]
struct IndentationLevel {
chars: usize,
computed: usize,
}
impl PartialOrd for IndentationLevel {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.computed.partial_cmp(&other.computed)
}
}
#[cfg(test)]
use crate::assert_parsing_eq;
#[test]
fn test_peek_initial_indentation_first_empty_line() {
let input = r#"
This is a general notification"#;
assert_parsing_eq!(
peek_initial_indentation,
input,
Ok((
"\n\t This is a general notification",
IndentationLevel {
chars: 3,
computed: 10
}
))
)
}
#[test]
fn test_peek_initial_indentation() {
let input = r#" first word"#;
assert_parsing_eq!(
peek_initial_indentation,
input,
Ok((
" first word",
IndentationLevel {
chars: 4,
computed: 4
}
))
)
}
#[test]
fn test_indentation_level() {
assert_parsing_eq!(
indentation_level,
"\t",
Ok((
"",
IndentationLevel {
chars: 1,
computed: 8
}
))
);
assert_parsing_eq!(
indentation_level,
" \t",
Ok((
"",
IndentationLevel {
chars: 2,
computed: 8
}
))
);
assert_parsing_eq!(
indentation_level,
" \t",
Ok((
"",
IndentationLevel {
chars: 3,
computed: 8
}
))
);
assert_parsing_eq!(
indentation_level,
" \t",
Ok((
"",
IndentationLevel {
chars: 9,
computed: 16
}
))
);
}
#[test]
fn test_peek_first_non_empty_line() {
let input = " \t\n\n hello";
let (remaining, line) = peek_first_non_empty_line
.parse(KconfigInput::from(input))
.unwrap();
assert_eq!(remaining.fragment(), &input);
assert_eq!(line.fragment(), &" hello");
}
#[test]
fn test_parse_newline_only() {
assert_parsing_eq!(parse_newline_only, " \t\n", Ok(("", "\n".to_string())));
}