aimx/aim/
writer.rs

1//! Buffered writer for serializing AIM/AIMX data structures.
2//!
3//! Provides an internal `Writer` used by parsers and evaluators to render
4//! `Literal`, `Value`, `Reference` and related structures into text with
5//! minimal allocation. Supports plain, escaped and quoted+escaped output
6//! via [`PrintMode`] and list-style prefixes via [`Prefix`].
7//!
8//! Only use this module directly when implementing serialization
9//! or debugging output; external consumers should prefer the high-level
10//! API where available.
11
12use jiff::civil::DateTime;
13
14pub trait WriterLike {
15    /// Serialize this expression with the given [`Writer`].
16    fn write(&self, writer: &mut Writer);
17
18    /// Return a string representation (raw unsafe output).
19    fn to_stringized(&self) -> String {
20        let mut writer = Writer::stringizer();
21        self.write(&mut writer);
22        writer.finish()
23    }
24
25    /// Return a sanitized string representation (escaped for safe output).
26    fn to_sanitized(&self) -> String {
27        let mut writer = Writer::sanitizer();
28        self.write(&mut writer);
29        writer.finish()
30    }
31
32    /// Return a sanitized string representation (escaped for safe output).
33    fn to_expressionized(&self) -> String {
34        let mut writer = Writer::expressionizer();
35        self.write(&mut writer);
36        writer.finish()
37    }
38}
39
40/// Prefix style for list items.
41#[derive(Debug, PartialEq, Clone, Copy)]
42pub enum Prefix {
43    /// No prefix
44    None,
45    /// Unordered list prefix (e.g., "- ")
46    Unordered,
47    /// Ordered list prefix (e.g., "1. ")
48    Ordered,
49}
50
51/// Output formatting mode for the writer.
52#[derive(Debug, PartialEq, Clone, Copy)]
53pub enum PrintMode {
54    /// No special formatting
55    None,
56    /// Escape special characters
57    Escape,
58    /// Quote and escape special characters
59    QuoteEscape,
60    /// Double quote and escape special characters
61    DoubleQuoteEscape,
62}
63
64/// Internal buffered writer for AIM/AIMX structures.
65///
66/// Used by parsing and evaluation code to render `Literal`, `Value`,
67/// `Reference` and related types with configurable [`PrintMode`] and
68/// [`Prefix`]. External callers normally use higher-level APIs.
69pub struct Writer {
70    buffer: String,
71    int: itoa::Buffer,
72    float: ryu::Buffer,
73    mode: PrintMode,
74    prefix: Prefix,
75}
76
77impl Writer {
78    pub fn new(format: PrintMode, prefix: Prefix) -> Self {
79        Self {
80            buffer: String::new(),
81            int: itoa::Buffer::new(),
82            float: ryu::Buffer::new(),
83            mode: format,
84            prefix,
85        }
86    }
87
88    pub fn stringizer() -> Self {
89        Self {
90            buffer: String::new(),
91            int: itoa::Buffer::new(),
92            float: ryu::Buffer::new(),
93            mode: PrintMode::None,
94            prefix: Prefix::Unordered,
95        }
96    }
97
98    pub fn is_stringizer(&self) -> bool {
99        matches!(self.mode, PrintMode::None)
100    }
101
102    pub fn sanitizer() -> Self {
103        Self {
104            buffer: String::new(),
105            int: itoa::Buffer::new(),
106            float: ryu::Buffer::new(),
107            mode: PrintMode::Escape,
108            prefix: Prefix::Unordered,
109        }
110    }
111
112    pub fn is_sanitizer(&self) -> bool {
113        matches!(self.mode, PrintMode::Escape)
114    }
115
116    pub fn formulizer() -> Self {
117        Self {
118            buffer: String::new(),
119            int: itoa::Buffer::new(),
120            float: ryu::Buffer::new(),
121            mode: PrintMode::QuoteEscape,
122            prefix: Prefix::None,
123        }
124    }
125
126    pub fn is_formulizer(&self) -> bool {
127        matches!(self.mode, PrintMode::QuoteEscape)
128    }
129
130    pub fn expressionizer() -> Self {
131        Self {
132            buffer: String::new(),
133            int: itoa::Buffer::new(),
134            float: ryu::Buffer::new(),
135            mode: PrintMode::DoubleQuoteEscape,
136            prefix: Prefix::None,
137        }
138    }
139
140    pub fn is_expressionizer(&self) -> bool {
141        matches!(self.mode, PrintMode::DoubleQuoteEscape)
142    }
143
144    pub fn prefix(&self) -> Prefix {
145        self.prefix
146    }
147
148    pub fn print_mode(&self) -> PrintMode {
149        self.mode
150    }
151
152    pub fn write_str(&mut self, s: &str) {
153        self.buffer.push_str(s);
154    }
155
156    pub fn write_char(&mut self, c: char) {
157        self.buffer.push(c);
158    }
159
160    pub fn write_eol(&mut self) {
161        if self.is_stringizer() {
162            self.buffer.push('\r');
163        }
164        self.buffer.push('\n');
165    }
166
167    pub fn write_line(&mut self, s: &str) {
168        self.buffer.push_str(s);
169        if self.is_stringizer() {
170            self.buffer.push('\r');
171        }
172        self.buffer.push('\n');
173    }
174
175    pub fn write_f64(&mut self, value: f64) {
176        if value.is_normal() {
177            let mut string = self.float.format(value);
178            if let Some(integer) = string.strip_suffix(".0") {
179                string = integer;
180            }
181            self.buffer.push_str(string);
182        } else if value.is_infinite() {
183            if value.is_sign_positive() {
184                self.buffer.push_str("+Inf");
185            } else {
186                self.buffer.push_str("-Inf");
187            }
188        } else if value.is_nan() {
189            self.buffer.push_str("NaN");
190        } else {
191            self.buffer.push('0');
192        }
193    }
194
195    pub fn write_unsigned(&mut self, value: u32) {
196        self.buffer.push_str(self.int.format(value));
197    }
198
199    pub fn write_u64(&mut self, value: u64) {
200        self.buffer.push_str(self.int.format(value));
201    }
202
203    pub fn print(&mut self, text: &str) {
204        match self.mode {
205            PrintMode::None => self.write_str(text),
206            PrintMode::Escape => self.escape(text),
207            PrintMode::QuoteEscape => self.quote_escape(text),
208            PrintMode::DoubleQuoteEscape => self.double_quote_escape(text),
209        }
210    }
211
212    pub fn escape(&mut self, text: &str) {
213        for c in text.chars() {
214            match c {
215                '"' => self.buffer.push_str("\\\""),
216                '\\' => self.buffer.push_str("\\\\"),
217                '\u{08}' => self.buffer.push_str("\\b"),
218                '\u{0C}' => self.buffer.push_str("\\f"),
219                '\n' => self.buffer.push_str("\\n"),
220                '\r' => self.buffer.push_str("\\r"),
221                '\t' => self.buffer.push_str("\\t"),
222                c if c < '\u{20}' => {}
223                _ => self.buffer.push(c),
224            }
225        }
226    }
227
228    pub fn quote_escape(&mut self, text: &str) {
229        self.buffer.push('\'');
230        for c in text.chars() {
231            match c {
232                '"' => self.buffer.push_str("\\\""),
233                '\'' => self.buffer.push_str("\\\'"),
234                '\\' => self.buffer.push_str("\\\\"),
235                '\u{08}' => self.buffer.push_str("\\b"),
236                '\u{0C}' => self.buffer.push_str("\\f"),
237                '\n' => self.buffer.push_str("\\n"),
238                '\r' => self.buffer.push_str("\\r"),
239                '\t' => self.buffer.push_str("\\t"),
240                c if c < '\u{20}' => {}
241                _ => self.buffer.push(c),
242            }
243        }
244        self.buffer.push('\'');
245    }
246
247    pub fn double_quote_escape(&mut self, text: &str) {
248        self.buffer.push('"');
249        self.escape(text);
250        self.buffer.push('"');
251    }
252
253    pub fn write_date(&mut self, date: &DateTime) {
254        self.buffer.push_str(self.int.format(date.year()));
255        self.buffer.push('-');
256        let month = date.month();
257        if month < 10 {
258            self.buffer.push('0');
259        }
260        self.buffer.push_str(self.int.format(month));
261        self.buffer.push('-');
262        let day = date.day();
263        if day < 10 {
264            self.buffer.push('0');
265        }
266        self.buffer.push_str(self.int.format(day));
267        self.buffer.push('T');
268        let hour = date.hour();
269        if hour < 10 {
270            self.buffer.push('0');
271        }
272        self.buffer.push_str(self.int.format(hour));
273        self.buffer.push(':');
274        let minute = date.minute();
275        if minute < 10 {
276            self.buffer.push('0');
277        }
278        self.buffer.push_str(self.int.format(minute));
279        self.buffer.push(':');
280        let second = date.second();
281        if second < 10 {
282            self.buffer.push('0');
283        }
284        self.buffer.push_str(self.int.format(second));
285        let millisecond = date.subsec_nanosecond() / 1_000_000;
286        if millisecond > 0 {
287            self.buffer.push('.');
288            if millisecond < 100 {
289                self.buffer.push('0');
290            }
291            if millisecond < 10 {
292                self.buffer.push('0');
293            }
294            self.buffer.push_str(self.int.format(millisecond));
295        }
296    }
297
298    pub fn write_bool(&mut self, state: bool) {
299        if state {
300            self.buffer.push_str("true");
301        } else {
302            self.buffer.push_str("false");
303        }
304    }
305
306    pub fn write_prefix(&mut self, indent: isize, prefix: &Prefix, index: usize) {
307        match prefix {
308            Prefix::Ordered => {
309                for _ in 0..indent {
310                    self.buffer.push('\t');
311                }
312                self.write_unsigned((index + 1) as u32);
313                self.buffer.push_str(". ");
314            }
315            Prefix::Unordered => {
316                for _ in 0..indent {
317                    self.buffer.push('\t');
318                }
319                self.buffer.push_str("- ");
320            }
321            Prefix::None => {}
322        }
323    }
324
325    pub fn write_unary_op(&mut self, op: &str, unary: &dyn WriterLike) {
326        self.write_str(op);
327        unary.write(self);
328    }
329
330    pub fn write_binary_op(
331        &mut self,
332        left: &dyn WriterLike,
333        op: &str,
334        right: &dyn WriterLike,
335    ) {
336        left.write(self);
337        self.write_str(op);
338        right.write(self);
339    }
340
341    pub fn finish(self) -> String {
342        self.buffer
343    }
344
345    pub fn finish_and_reuse(&mut self) -> String {
346        let result = self.buffer.clone();
347        self.buffer.clear();
348        result
349    }
350}