asciidoc_parser/parser/
source_map.rs

1use std::fmt;
2
3/// Generated by preprocessor: Sparse map of result ([`Span`]) line number to a
4/// tuple of file name and line number. This allows us to work backwards from
5/// the pre-processed and unified [`Span`] to the various inputs that were used
6/// to construct it. Line numbers here are 1-based.
7///
8/// [`Span`]: crate::Span
9#[derive(Clone, Default, Eq, PartialEq)]
10pub struct SourceMap(pub Vec<(usize, SourceLine)>);
11
12/// A `SourceLine` represents the original file and line number where a line of
13/// Asciidoc text was found before [include file] and [conditional]
14/// pre-processing occurred.
15///
16/// The first member is the file name as specified on the [include file]
17/// directive. The second member is the 1-based line number.
18///
19/// [include file]: https://docs.asciidoctor.org/asciidoc/latest/directives/include/
20/// [conditional]: https://docs.asciidoctor.org/asciidoc/latest/directives/conditionals/
21#[derive(Clone, Debug, Default, Eq, PartialEq)]
22pub struct SourceLine(pub Option<String>, pub usize);
23
24impl SourceMap {
25    pub(crate) fn append(&mut self, preprocessed_line: usize, source_line: SourceLine) {
26        // IMPORTANT: These _should_ be added in increasing order of
27        // `preprocessed_line`, but this is not enforced.
28        self.0.push((preprocessed_line, source_line));
29    }
30
31    /// Given a 1-based line number in the preprocessed source file, translate
32    /// that to a file name and line number as original inputs to the parsing
33    /// process.
34    pub fn original_file_and_line(&self, key: usize) -> Option<SourceLine> {
35        match self.0.binary_search_by_key(&key, |(k, _)| *k) {
36            Ok(i) => self.0.get(i).map(|(_k, v)| v.clone()),
37            Err(0) => Some(SourceLine(None, key)),
38            Err(i) => self
39                .0
40                .get(i - 1)
41                .map(|(k, v)| SourceLine(v.0.clone(), v.1 + key - k)),
42        }
43    }
44}
45
46impl fmt::Debug for SourceMap {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str("SourceMap(&")?;
49        f.debug_list()
50            .entries(self.0.iter().map(|(k, v)| (k, v)))
51            .finish()?;
52        f.write_str(")")
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    #![allow(clippy::unwrap_used)]
59
60    use crate::parser::{SourceLine, SourceMap};
61
62    #[test]
63    fn empty() {
64        let sm = SourceMap::default();
65        assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
66    }
67
68    #[test]
69    fn one_entry() {
70        let mut sm = SourceMap::default();
71        sm.append(1, SourceLine(None, 1));
72
73        assert_eq!(sm.original_file_and_line(0), Some(SourceLine(None, 0)));
74        assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
75        assert_eq!(sm.original_file_and_line(4), Some(SourceLine(None, 4)));
76
77        assert_eq!(
78            sm.original_file_and_line(4000),
79            Some(SourceLine(None, 4000))
80        );
81    }
82
83    #[test]
84    fn multiple_entries() {
85        let mut sm = SourceMap::default();
86        sm.append(1, SourceLine(None, 1));
87        sm.append(10, SourceLine(Some("foo.adoc".to_owned()), 1));
88        sm.append(20, SourceLine(Some("bar.adoc".to_owned()), 18));
89        sm.append(30, SourceLine(None, 11));
90
91        assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
92        assert_eq!(sm.original_file_and_line(4), Some(SourceLine(None, 4)));
93
94        assert_eq!(
95            sm.original_file_and_line(10),
96            Some(SourceLine(Some("foo.adoc".to_owned()), 1))
97        );
98        assert_eq!(
99            sm.original_file_and_line(19),
100            Some(SourceLine(Some("foo.adoc".to_owned()), 10))
101        );
102
103        assert_eq!(
104            sm.original_file_and_line(20),
105            Some(SourceLine(Some("bar.adoc".to_owned()), 18))
106        );
107        assert_eq!(
108            sm.original_file_and_line(21),
109            Some(SourceLine(Some("bar.adoc".to_owned()), 19))
110        );
111        assert_eq!(
112            sm.original_file_and_line(29),
113            Some(SourceLine(Some("bar.adoc".to_owned()), 27))
114        );
115
116        assert_eq!(sm.original_file_and_line(30), Some(SourceLine(None, 11)));
117        assert_eq!(sm.original_file_and_line(40), Some(SourceLine(None, 21)));
118    }
119
120    #[test]
121    fn impl_debug() {
122        let mut sm = SourceMap::default();
123        sm.append(1, SourceLine(None, 1));
124
125        assert_eq!(
126            format!("{sm:#?}"),
127            "SourceMap(&[\n    (\n        1,\n        SourceLine(\n            None,\n            1,\n        ),\n    ),\n])"
128        );
129    }
130}