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::quotes::{DelimitedReferences, QUOTES};
10use crate::reference_literal::reference_literal_end;
11
12/// The character that opens a comment.
13pub const COMMENT: char = '#';
14
15/// What ends a bare reference. A token starts after one of these, at the start
16/// of the document, and right after a delimited reference or a versioned
17/// literal: the places the parser reads a reference from.
18const SEPARATORS: [u8; 7] = *b" \t\n\r(:)";
19
20/// What can stand before a comment: the comment is the first thing on a line,
21/// or it follows whitespace. A `#` inside a word is part of that word, so
22/// `issue#1047` is a reference and not the start of a comment.
23const BEFORE_COMMENT: [u8; 4] = *b" \t\n\r";
24
25/// Blanks out every comment in `document`, keeping every other byte where it
26/// was.
27///
28/// Comments are replaced rather than removed so that a byte offset in the
29/// result is the same byte offset in `document`: the line and column a parse
30/// error reports are the line and column the writer sees in their file.
31///
32/// A `#` inside a delimited reference is content, so `"# not a comment"` is
33/// still one reference. The document is read token by token, the way the
34/// parser reads it, so this holds as well for a reference that starts right
35/// after a group, another reference or a literal, and references are read in
36/// the document as written, as the parser reads them (issue #377).
37///
38/// # Examples
39/// ```
40/// use links_notation::comments::strip_comments;
41///
42/// assert_eq!(strip_comments("a: b # why\n"), "a: b      \n");
43/// assert_eq!(strip_comments("\"# kept\"\n"), "\"# kept\"\n");
44/// assert_eq!(strip_comments("issue#1047\n"), "issue#1047\n");
45/// assert_eq!(strip_comments("(a)\" #kept\"\n"), "(a)\" #kept\"\n");
46/// ```
47pub fn strip_comments(document: &str) -> String {
48    let mut bytes = document.as_bytes().to_vec();
49    let references = DelimitedReferences::new(document);
50    let mut position = 0;
51    // Whether a token may start at `position`.
52    let mut token_start = true;
53
54    while position < bytes.len() {
55        let byte = bytes[position];
56
57        if byte == COMMENT as u8 && follows(&bytes, position, &BEFORE_COMMENT) {
58            while position < bytes.len() && bytes[position] != b'\n' && bytes[position] != b'\r' {
59                bytes[position] = b' ';
60                position += 1;
61            }
62            continue;
63        }
64
65        if SEPARATORS.contains(&byte) {
66            token_start = true;
67            position += 1;
68            continue;
69        }
70
71        if token_start {
72            if let Some(end) = delimited_token_end(document, position, &references) {
73                position = end;
74                continue;
75            }
76        }
77
78        // Anything else is part of a bare reference, which runs to a separator.
79        token_start = false;
80        position += 1;
81    }
82
83    // Only whole comments were replaced, and only by spaces, so what is left is
84    // the document it was read from with some of its bytes blanked.
85    String::from_utf8(bytes).expect("blanking comment bytes keeps the document valid UTF-8")
86}
87
88/// The offset just past the versioned literal or delimited reference that
89/// starts the token at `start`, or `None` when the token there is a bare
90/// reference: a delimiter run that never closes is read as one, as the parser
91/// reads it.
92fn delimited_token_end(
93    document: &str,
94    start: usize,
95    references: &DelimitedReferences,
96) -> Option<usize> {
97    if let Some(end) = reference_literal_end(document, start) {
98        return Some(end);
99    }
100    if QUOTES.contains(&document.as_bytes()[start]) {
101        return references.end_at(document, start);
102    }
103    None
104}
105
106/// Reports whether the byte before `position` is one of `allowed`, treating the
107/// start of the document as one of them.
108fn follows(bytes: &[u8], position: usize, allowed: &[u8]) -> bool {
109    match position {
110        0 => true,
111        _ => allowed.contains(&bytes[position - 1]),
112    }
113}