source_map/
to_string.rs

1use crate::{
2    count_characters_on_last_line, FileSystem, SourceMap, SourceMapBuilder, SpanWithSource,
3};
4
5/// A trait for defining behavior of adding content to a buffer. As well as register markers for source maps
6pub trait ToString {
7    /// Append character
8    fn push(&mut self, chr: char);
9
10    /// Append a new line character
11    fn push_new_line(&mut self);
12
13    /// Use [ToString::push_str_contains_new_line] if `string` could contain new lines
14    fn push_str(&mut self, string: &str);
15
16    /// Used to push strings that may contain new lines
17    fn push_str_contains_new_line(&mut self, string: &str);
18
19    /// Adds a mapping of the from a original position in the source to the position in the current buffer
20    ///
21    /// **Should be called before adding new content**
22    fn add_mapping(&mut self, source_span: &SpanWithSource);
23
24    /// Some implementors might not ToString the whole input. This signals for users to end early as further usage
25    /// of this trait has no effect
26    fn should_halt(&self) -> bool {
27        false
28    }
29
30    fn characters_on_current_line(&self) -> u32;
31
32    fn is_counting(&self) -> bool {
33        false
34    }
35}
36
37// TODO clarify calls
38impl ToString for String {
39    fn push(&mut self, chr: char) {
40        self.push(chr);
41    }
42
43    fn push_new_line(&mut self) {
44        self.push('\n');
45    }
46
47    fn push_str(&mut self, string: &str) {
48        self.push_str(string)
49    }
50
51    fn push_str_contains_new_line(&mut self, string: &str) {
52        self.push_str(string)
53    }
54
55    fn add_mapping(&mut self, _source_span: &SpanWithSource) {}
56
57    fn characters_on_current_line(&self) -> u32 {
58        count_characters_on_last_line(self)
59    }
60}
61
62pub struct Writable<T: std::io::Write> {
63    pub writable: T,
64    pub length: u32,
65    pub since_new_line: u32,
66    pub source_map: Option<SourceMapBuilder>,
67}
68
69impl<T: std::io::Write> ToString for Writable<T> {
70    fn push(&mut self, chr: char) {
71        let mut buf = [0u8; 4]; // A char can be at most 4 bytes in UTF-8
72        let buf = chr.encode_utf8(&mut buf).as_bytes();
73        let char_size = chr.len_utf8();
74        self.length += char_size as u32;
75        self.since_new_line += char_size as u32;
76        self.writable.write_all(buf).unwrap();
77    }
78
79    fn push_new_line(&mut self) {
80        self.length += 1;
81        self.writable.write_all(&[b'\n']).unwrap();
82    }
83
84    fn push_str(&mut self, string: &str) {
85        self.length += string.len() as u32;
86        self.since_new_line += string.len() as u32;
87        self.writable.write_all(string.as_bytes()).unwrap();
88    }
89
90    fn push_str_contains_new_line(&mut self, slice: &str) {
91        self.length += slice.len() as u32;
92        self.writable.write_all(slice.as_bytes()).unwrap();
93        if let Some(ref mut sm) = self.source_map {
94            slice
95                .chars()
96                .filter(|chr| *chr == '\n')
97                .for_each(|_| sm.add_new_line());
98        }
99        self.since_new_line = count_characters_on_last_line(slice);
100    }
101
102    fn add_mapping(&mut self, source_span: &SpanWithSource) {
103        if let Some(ref mut sm) = self.source_map {
104            sm.add_mapping(source_span, self.since_new_line);
105        }
106    }
107
108    fn characters_on_current_line(&self) -> u32 {
109        self.since_new_line
110    }
111}
112
113/// Building a source along with its source map
114///
115/// Really for debug builds
116#[derive(Default)]
117pub struct StringWithOptionalSourceMap {
118    pub source: String,
119    pub source_map: Option<SourceMapBuilder>,
120    pub quit_after: Option<usize>,
121    pub since_new_line: u32,
122}
123
124impl StringWithOptionalSourceMap {
125    pub fn new(with_source_map: bool) -> Self {
126        Self {
127            source: String::new(),
128            source_map: with_source_map.then(SourceMapBuilder::new),
129            quit_after: None,
130            since_new_line: 0,
131        }
132    }
133
134    /// Returns output and the source map
135    pub fn build(self, filesystem: &impl FileSystem) -> (String, Option<SourceMap>) {
136        (self.source, self.source_map.map(|sm| sm.build(filesystem)))
137    }
138
139    #[cfg(feature = "inline-source-map")]
140    /// Build the output and append the source map in base 64
141    pub fn build_with_inline_source_map(self, filesystem: &impl FileSystem) -> String {
142        use base64::Engine;
143
144        let Self {
145            mut source,
146            source_map,
147            quit_after: _,
148            since_new_line: _,
149        } = self;
150        let built_source_map = source_map.unwrap().build(filesystem);
151        // Inline URL:
152        source.push_str("\n//# sourceMappingURL=data:application/json;base64,");
153        source.push_str(
154            &base64::prelude::BASE64_STANDARD.encode(built_source_map.to_json(filesystem)),
155        );
156        source
157    }
158}
159
160impl ToString for StringWithOptionalSourceMap {
161    fn push(&mut self, chr: char) {
162        self.source.push(chr);
163        if let Some(ref mut sm) = self.source_map {
164            sm.add_to_column(chr.len_utf16());
165        }
166        self.since_new_line += chr.len_utf8() as u32;
167    }
168
169    fn push_new_line(&mut self) {
170        self.source.push('\n');
171        if let Some(ref mut sm) = self.source_map {
172            sm.add_new_line();
173        }
174        self.since_new_line = 0;
175    }
176
177    fn push_str(&mut self, slice: &str) {
178        self.source.push_str(slice);
179        if let Some(ref mut sm) = self.source_map {
180            sm.add_to_column(slice.chars().count());
181        }
182        self.since_new_line += slice.len() as u32;
183    }
184
185    fn push_str_contains_new_line(&mut self, slice: &str) {
186        self.source.push_str(slice);
187        if let Some(ref mut sm) = self.source_map {
188            slice
189                .chars()
190                .filter(|chr| *chr == '\n')
191                .for_each(|_| sm.add_new_line());
192        }
193        self.since_new_line = count_characters_on_last_line(slice);
194    }
195
196    fn add_mapping(&mut self, source_span: &SpanWithSource) {
197        if let Some(ref mut sm) = self.source_map {
198            sm.add_mapping(source_span, self.since_new_line);
199        }
200    }
201
202    fn should_halt(&self) -> bool {
203        self.quit_after
204            .map_or(false, |quit_after| self.source.len() > quit_after)
205    }
206
207    fn characters_on_current_line(&self) -> u32 {
208        self.since_new_line
209    }
210
211    fn is_counting(&self) -> bool {
212        self.quit_after.is_some()
213    }
214}
215
216/// Counts text until a limit. Used for telling whether the text is greater than some threshold
217pub struct Counter {
218    acc: usize,
219    max: usize,
220}
221
222impl Counter {
223    pub fn new(max: usize) -> Self {
224        Self { acc: 0, max }
225    }
226
227    pub fn get_count(&self) -> usize {
228        self.acc
229    }
230}
231
232impl ToString for Counter {
233    fn push(&mut self, chr: char) {
234        self.acc += chr.len_utf8();
235    }
236
237    fn push_new_line(&mut self) {
238        self.push('\n');
239    }
240
241    fn push_str(&mut self, string: &str) {
242        self.acc += string.len();
243    }
244
245    fn push_str_contains_new_line(&mut self, string: &str) {
246        self.acc += string.len();
247    }
248
249    fn add_mapping(&mut self, _source_span: &SpanWithSource) {}
250
251    fn should_halt(&self) -> bool {
252        self.acc > self.max
253    }
254
255    fn characters_on_current_line(&self) -> u32 {
256        // TODO?
257        0
258    }
259    fn is_counting(&self) -> bool {
260        true
261    }
262}
263
264#[cfg(test)]
265mod to_string_tests {
266    use super::*;
267
268    fn serializer<T: ToString>(t: &mut T) {
269        t.push_str("Hello");
270        t.push(' ');
271        t.push_str("World");
272    }
273
274    #[test]
275    fn string_concatenation() {
276        let mut s = String::new();
277        serializer(&mut s);
278        assert_eq!(&s, "Hello World");
279    }
280
281    #[test]
282    fn counting() {
283        let mut s = Counter::new(usize::MAX);
284        serializer(&mut s);
285        assert_eq!(s.get_count(), "Hello World".chars().count());
286    }
287
288    #[test]
289    fn max_counter() {
290        let mut s = Counter::new(14);
291        serializer(&mut s);
292        assert!(!s.should_halt());
293        serializer(&mut s);
294        assert!(s.should_halt());
295    }
296}