Skip to main content

links_notation/
comments.rs

1//! Comments, and the rule that decides where one starts.
2//!
3//! A comment starts at a `#` written where a line or a token starts and runs to
4//! the end of the line. The parser never sees it: [`strip_comments`] replaces
5//! every byte of every comment with a space before the document is parsed, so
6//! the notation itself stays exactly as it was and a position reported by the
7//! parser still points at the same character of the document the caller wrote.
8
9use crate::parser::quoted_reference_end;
10
11/// The character that opens a comment.
12pub const COMMENT: char = '#';
13
14/// The characters a delimited reference can be written between.
15const QUOTES: [u8; 3] = *b"\"'`";
16
17/// What can stand before a delimited reference: the reference is the first
18/// thing on a line, follows a space, opens a group or follows a colon.
19const BEFORE_REFERENCE: [u8; 6] = *b" \t\n\r(:";
20
21/// What can stand before a comment: the comment is the first thing on a line,
22/// or it follows whitespace. A `#` inside a word is part of that word, so
23/// `issue#1047` is a reference and not the start of a comment.
24const BEFORE_COMMENT: [u8; 4] = *b" \t\n\r";
25
26/// Blanks out every comment in `document`, keeping every other byte where it
27/// was.
28///
29/// Comments are replaced rather than removed so that a byte offset in the
30/// result is the same byte offset in `document`: the line and column a parse
31/// error reports are the line and column the writer sees in their file.
32///
33/// A `#` inside a delimited reference is content, so `"# not a comment"` is
34/// still one reference.
35///
36/// # Examples
37/// ```
38/// use links_notation::comments::strip_comments;
39///
40/// assert_eq!(strip_comments("a: b # why\n"), "a: b      \n");
41/// assert_eq!(strip_comments("\"# kept\"\n"), "\"# kept\"\n");
42/// assert_eq!(strip_comments("issue#1047\n"), "issue#1047\n");
43/// ```
44pub fn strip_comments(document: &str) -> String {
45    let mut bytes = document.as_bytes().to_vec();
46    let mut position = 0;
47
48    while position < bytes.len() {
49        let byte = bytes[position];
50
51        if QUOTES.contains(&byte) && follows(&bytes, position, &BEFORE_REFERENCE) {
52            match quoted_reference_end(document, position) {
53                Some(end) => position = end,
54                None => position += 1,
55            }
56            continue;
57        }
58
59        if byte == COMMENT as u8 && follows(&bytes, position, &BEFORE_COMMENT) {
60            while position < bytes.len() && bytes[position] != b'\n' && bytes[position] != b'\r' {
61                bytes[position] = b' ';
62                position += 1;
63            }
64            continue;
65        }
66
67        position += 1;
68    }
69
70    // Only whole comments were replaced, and only by spaces, so what is left is
71    // the document it was read from with some of its bytes blanked.
72    String::from_utf8(bytes).expect("blanking comment bytes keeps the document valid UTF-8")
73}
74
75/// Reports whether the byte before `position` is one of `allowed`, treating the
76/// start of the document as one of them.
77fn follows(bytes: &[u8], position: usize, allowed: &[u8]) -> bool {
78    match position {
79        0 => true,
80        _ => allowed.contains(&bytes[position - 1]),
81    }
82}