nom_kconfig/attribute/
help.rs

1use crate::{util::ws, KconfigInput};
2use nom::bytes::complete::take_while;
3use nom::character::complete::space0;
4use nom::combinator::recognize;
5use nom::multi::fold_many0;
6use nom::sequence::terminated;
7use nom::Parser;
8use nom::{
9    branch::alt,
10    bytes::complete::{tag, take},
11    character::complete::{line_ending, newline, not_line_ending, space1},
12    combinator::{eof, map, opt, peek},
13    multi::many0,
14    sequence::delimited,
15    IResult,
16};
17
18pub fn weirdo_help(input: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
19    // TODO linux v-3.2, in file /drivers/net/ethernet/stmicro/stmmac/Kconfig
20    // TODO 3.4.110/drivers/net/ethernet/sfc/Kconfig
21    map(
22        delimited(
23            ws(opt(many0(tag("-")))),
24            ws(tag("help")),
25            opt(many0(alt((tag("-"), space1)))),
26        ),
27        |d| d,
28    )
29    .parse(input)
30}
31
32/// This parses a help text. The end of the help text is determined by the indentation level, this means it ends at the first line which has a smaller indentation than the first line of the help text.
33///
34/// # Example
35/// ```
36/// use nom_kconfig::{
37///     assert_parsing_eq,
38///     attribute::parse_help,
39/// };
40///
41/// assert_parsing_eq!(parse_help, "help\n   hello world", Ok(("", "hello world".to_string())))
42/// ```
43pub fn parse_help(input: KconfigInput) -> IResult<KconfigInput, String> {
44    // parse out help tag
45    let (input, _) = (
46        alt((
47            ws(tag("help")),
48            // TODO linux v-3.2, in file /drivers/net/ethernet/stmicro/stmmac/Kconfig
49            weirdo_help,
50        )),
51        many0(space1),
52        newline,
53    )
54        .parse(input)?;
55
56    // parse the raw text
57    let (input, text) = parse_help_text(input)?;
58    Ok((input, text))
59}
60
61//fn parse_help_text_2(input: KconfigInput) -> IResult<KconfigInput, String> {
62//    let (original, initial_indentation_len) = peek_initial_indentation(input)?;
63//    if initial_indentation_len.chars == 0 {
64//        return Ok((original, String::new()));
65//    }
66//
67//    // parse the help text
68//    // if the line contains only newline, we should accept it as well
69//    // if the indentation is the same or larger, we should continue parsing, whitespaces should not be trimmed when the current indentation is larger than the initial one
70//    // if the indentation decreases, we should stop parsing
71//    // newlines counts
72//
73//    let mut remaining = original;
74//    let mut help_text = String::new();
75//    loop {
76//        let (next_input, current_indentation_len) = peek_indentation(remaining)?;
77//        let peek_line = peek_til_newline(next_input)?.1;
78//        if current_indentation_len < initial_indentation_len && peek_line.fragment().trim() != "" {
79//            break;
80//        }
81//
82//
83//        // parse either a full help line or a newline only
84//        let parse_result = alt((parse_full_help_line, parse_newline_only)).parse(remaining);
85//        match parse_result {
86//            Ok((next_input, parsed_line)) => {
87//                help_text.push_str(&parsed_line);
88//                remaining = next_input;
89//            }
90//            Err(_) => {
91//                break;
92//            }
93//        }
94//    }
95//
96//    Ok((remaining, help_text.trim().to_string()))
97//}
98
99fn parse_help_text(input: KconfigInput) -> IResult<KconfigInput, String> {
100    let (original, initial_indentation_len) = peek_initial_indentation(input)?;
101    if initial_indentation_len.chars == 0 {
102        return Ok((original, String::new()));
103    }
104
105    // Parse subsequent lines while maintaining indentation
106    let (remaining, help_text) = fold_many0(
107        |i| {
108            let (orig, indent_len) = peek_indentation(i)?;
109            let peek_line = peek_til_newline(orig)?;
110
111            if peek_line.1.fragment().trim() == "" {
112                // allow empty lines
113                parse_newline_only(peek_line.0)
114            } else if indent_len < initial_indentation_len {
115                // Stop parsing when indentation decreases
116                Err(nom::Err::Error(nom::error::Error::new(
117                    peek_line.1,
118                    nom::error::ErrorKind::Fail,
119                )))
120            } else {
121                // Consume the same base indentation.
122                let (remain, _) =
123                    take(indent_len.chars.min(initial_indentation_len.chars))(peek_line.0)?;
124                // Parse the raw help line text.
125                parse_full_help_line(remain)
126            }
127        },
128        //            if (indent_len.chars != 0) && (indent_len < initial_indentation_len) {
129        //                return Err(nom::Err::Error(nom::error::Error::new(
130        //                    orig,
131        //                    nom::error::ErrorKind::Fail, // Stop parsing when indentation decreases
132        //                )));
133        //            } else if indent_len.chars == 0 {
134        //                // handle newlines between paragraph's
135        //                parse_newline_only(orig)
136        //            } else {
137        //                // Consume the same base indentation.
138        //                let (remain, _) = take(initial_indentation_len.chars.min(indent_len.chars))(orig)?;
139        //                // Parse the raw help line text.
140        //                parse_full_help_line(remain)
141        //            }
142        //        },
143        String::new,
144        |mut acc, line| {
145            acc.push_str(&line);
146            acc
147        },
148    )
149    .parse(original)?;
150
151    Ok((remaining, help_text.trim_end().to_string()))
152}
153
154fn parse_line_help(input: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
155    //pair(not_line_ending, alt((line_ending, eof))).parse(input)
156    terminated(not_line_ending, opt(alt((line_ending, eof)))).parse(input)
157}
158
159fn parse_full_help_line(input: KconfigInput) -> IResult<KconfigInput, String> {
160    let (input, raw_text) = parse_line_help(input)?;
161    let mut parsed_line = raw_text.to_string();
162    parsed_line.push('\n');
163    Ok((input, parsed_line))
164}
165
166fn parse_newline_only(input: KconfigInput) -> IResult<KconfigInput, String> {
167    let (input, _newline) = recognize(terminated(space0, line_ending)).parse(input)?;
168    Ok((input, "\n".to_string()))
169}
170fn peek_til_newline(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
171    peek(parse_line_help).parse(s)
172}
173
174// Parser that consumes empty lines (lines with only whitespace or nothing)
175fn empty_line(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
176    recognize(terminated(space0, line_ending)).parse(s)
177}
178
179// Peek at the first non-empty line without consuming it
180fn peek_first_non_empty_line(s: KconfigInput) -> IResult<KconfigInput, KconfigInput> {
181    let (original, (_, non_empty_line)) = peek((many0(empty_line), not_line_ending)).parse(s)?;
182    Ok((original, non_empty_line))
183    //let (input, first_none_empty_line) = peek(many0(empty_line)).parse(s)?;
184    //peek(not_line_ending).parse(input)
185}
186
187/// find the indentation level
188fn peek_initial_indentation(s: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
189    let (original, peeked) = peek_first_non_empty_line.parse(s)?;
190    let (_, len) = indentation_level(peeked)?;
191    Ok((original, len))
192}
193
194fn peek_indentation(s: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
195    let (original, peeked) = peek_til_newline(s)?;
196    let (_, len) = indentation_level(peeked)?;
197    Ok((original, len))
198}
199
200/// Inspired from <https://github.com/movidius/kconfig-frontends/blob/44b2a3287ebd5be5b49e51feaafb9c54c9f0fe41/libs/parser/lconf.l#L204-L250>
201fn indentation_level(input: KconfigInput) -> IResult<KconfigInput, IndentationLevel> {
202    // Assumes that tab and space usage is consistent across multi line text.
203    // E.g We don't know how much spaces a tab is.
204    //
205    // Example:
206    // help\n
207    //     Lorem Ipsum\n
208    // \t\tLorem Ipsum\n
209    //
210    // The above would consider the second text line to have less indentation( 4 white spaces vs 2 tabs as we only count chars) and thous only include the first line.
211    let (input, indent) = take_while(|c: char| c == ' ' || c == '\t')(input)?;
212
213    let mut computed = 0;
214    for c in indent.fragment().chars() {
215        match c {
216            '\t' => {
217                computed = (computed & !7) + 8;
218            }
219            ' ' => computed += 1,
220            _ => unreachable!(
221                "This should never happen because indentation only takes spaces and tabs"
222            ),
223        }
224    }
225
226    Ok((
227        input,
228        IndentationLevel {
229            chars: indent.fragment().len(),
230            computed,
231        },
232    ))
233}
234
235#[derive(PartialEq, Debug)]
236struct IndentationLevel {
237    chars: usize,
238    computed: usize,
239}
240
241// need to use the operator < on IndentationLevel
242impl PartialOrd for IndentationLevel {
243    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
244        self.computed.partial_cmp(&other.computed)
245    }
246}
247
248#[cfg(test)]
249use crate::assert_parsing_eq;
250#[test]
251fn test_peek_initial_indentation_first_empty_line() {
252    let input = r#"
253	  This is a general notification"#;
254
255    assert_parsing_eq!(
256        peek_initial_indentation,
257        input,
258        Ok((
259            "\n\t  This is a general notification",
260            IndentationLevel {
261                chars: 3,
262                computed: 10
263            }
264        ))
265    )
266}
267
268#[test]
269fn test_peek_initial_indentation() {
270    let input = r#"    first word"#;
271    assert_parsing_eq!(
272        peek_initial_indentation,
273        input,
274        Ok((
275            "    first word",
276            IndentationLevel {
277                chars: 4,
278                computed: 4
279            }
280        ))
281    )
282}
283
284#[test]
285fn test_indentation_level() {
286    assert_parsing_eq!(
287        indentation_level,
288        "\t",
289        Ok((
290            "",
291            IndentationLevel {
292                chars: 1,
293                computed: 8
294            }
295        ))
296    );
297    assert_parsing_eq!(
298        indentation_level,
299        " \t",
300        Ok((
301            "",
302            IndentationLevel {
303                chars: 2,
304                computed: 8
305            }
306        ))
307    );
308    assert_parsing_eq!(
309        indentation_level,
310        "  \t",
311        Ok((
312            "",
313            IndentationLevel {
314                chars: 3,
315                computed: 8
316            }
317        ))
318    );
319    assert_parsing_eq!(
320        indentation_level,
321        "        \t",
322        Ok((
323            "",
324            IndentationLevel {
325                chars: 9,
326                computed: 16
327            }
328        ))
329    );
330}
331
332#[test]
333fn test_peek_first_non_empty_line() {
334    let input = "        \t\n\n  hello";
335    let (remaining, line) = peek_first_non_empty_line
336        .parse(KconfigInput::from(input))
337        .unwrap();
338    assert_eq!(remaining.fragment(), &input);
339    assert_eq!(line.fragment(), &"  hello");
340}
341
342#[test]
343fn test_parse_newline_only() {
344    assert_parsing_eq!(parse_newline_only, "    \t\n", Ok(("", "\n".to_string())));
345}