fluent_syntax/parser/
pattern.rs

1use super::errors::{ErrorKind, ParserError};
2use super::{core::Parser, core::Result, slice::Slice};
3use crate::ast;
4
5#[derive(Debug, PartialEq)]
6enum TextElementTermination {
7    LineFeed,
8    Crlf,
9    PlaceableStart,
10    Eof,
11}
12
13// This enum tracks the placement of the text element in the pattern, which is needed for
14// dedentation logic.
15#[derive(Debug, PartialEq)]
16enum TextElementPosition {
17    InitialLineStart,
18    LineStart,
19    Continuation,
20}
21
22// This enum allows us to mark pointers in the source which will later become text elements
23// but without slicing them out of the source string. This makes the indentation adjustments
24// cheaper since they'll happen on the pointers, rather than extracted slices.
25#[derive(Debug)]
26enum PatternElementPlaceholders<S> {
27    Placeable(ast::Expression<S>),
28    // (start, end, indent, position)
29    TextElement(usize, usize, usize, TextElementPosition),
30}
31
32// This enum tracks whether the text element is blank or not.
33// This is important to identify text elements which should not be taken into account
34// when calculating common indent.
35#[derive(Debug, PartialEq)]
36enum TextElementType {
37    Blank,
38    NonBlank,
39}
40
41impl<'s, S> Parser<S>
42where
43    S: Slice<'s>,
44{
45    pub(super) fn get_pattern(&mut self) -> Result<Option<ast::Pattern<S>>> {
46        let mut elements = vec![];
47        let mut last_non_blank = None;
48        let mut common_indent = None;
49
50        self.skip_blank_inline();
51
52        let mut text_element_role = if self.skip_eol() {
53            self.skip_blank_block();
54            TextElementPosition::LineStart
55        } else {
56            TextElementPosition::InitialLineStart
57        };
58
59        while self.ptr < self.length {
60            if self.take_byte_if(b'{') {
61                if text_element_role == TextElementPosition::LineStart {
62                    common_indent = Some(0);
63                }
64                let exp = self.get_placeable()?;
65                last_non_blank = Some(elements.len());
66                elements.push(PatternElementPlaceholders::Placeable(exp));
67                text_element_role = TextElementPosition::Continuation;
68            } else {
69                let slice_start = self.ptr;
70                let mut indent = 0;
71                if text_element_role == TextElementPosition::LineStart {
72                    indent = self.skip_blank_inline();
73                    if let Some(b) = get_current_byte!(self) {
74                        if indent == 0 {
75                            if b != &b'\r' && b != &b'\n' {
76                                break;
77                            }
78                        } else if !Self::is_byte_pattern_continuation(*b) {
79                            self.ptr = slice_start;
80                            break;
81                        }
82                    } else {
83                        break;
84                    }
85                }
86                let (start, end, text_element_type, termination_reason) = self.get_text_slice()?;
87                if start != end {
88                    if text_element_role == TextElementPosition::LineStart
89                        && text_element_type == TextElementType::NonBlank
90                    {
91                        if let Some(common) = common_indent {
92                            if indent < common {
93                                common_indent = Some(indent);
94                            }
95                        } else {
96                            common_indent = Some(indent);
97                        }
98                    }
99                    if text_element_role != TextElementPosition::LineStart
100                        || text_element_type == TextElementType::NonBlank
101                        || termination_reason == TextElementTermination::LineFeed
102                    {
103                        if text_element_type == TextElementType::NonBlank {
104                            last_non_blank = Some(elements.len());
105                        }
106                        elements.push(PatternElementPlaceholders::TextElement(
107                            slice_start,
108                            end,
109                            indent,
110                            text_element_role,
111                        ));
112                    }
113                }
114
115                text_element_role = match termination_reason {
116                    TextElementTermination::LineFeed => TextElementPosition::LineStart,
117                    TextElementTermination::Crlf => TextElementPosition::LineStart,
118                    TextElementTermination::PlaceableStart => TextElementPosition::Continuation,
119                    TextElementTermination::Eof => TextElementPosition::Continuation,
120                };
121            }
122        }
123
124        if let Some(last_non_blank) = last_non_blank {
125            let elements = elements
126                .into_iter()
127                .take(last_non_blank + 1)
128                .enumerate()
129                .map(|(i, elem)| match elem {
130                    PatternElementPlaceholders::Placeable(expression) => {
131                        ast::PatternElement::Placeable { expression }
132                    }
133                    PatternElementPlaceholders::TextElement(start, end, indent, role) => {
134                        let start = if role == TextElementPosition::LineStart {
135                            common_indent.map_or_else(
136                                || start + indent,
137                                |common_indent| start + std::cmp::min(indent, common_indent),
138                            )
139                        } else {
140                            start
141                        };
142                        let mut value = self.source.slice(start..end);
143                        if last_non_blank == i {
144                            value.trim();
145                        }
146                        ast::PatternElement::TextElement { value }
147                    }
148                })
149                .collect();
150            return Ok(Some(ast::Pattern { elements }));
151        }
152
153        Ok(None)
154    }
155
156    fn get_text_slice(
157        &mut self,
158    ) -> Result<(usize, usize, TextElementType, TextElementTermination)> {
159        let start_pos = self.ptr;
160        let Some(rest) = get_remaining_bytes!(self) else {
161            return Ok((
162                start_pos,
163                self.ptr,
164                TextElementType::Blank,
165                TextElementTermination::Eof,
166            ));
167        };
168        let end = memchr::memchr3(b'\n', b'{', b'}', rest);
169        let element_type = |text: &[u8]| {
170            if text.iter().any(|&c| c != b' ') {
171                TextElementType::NonBlank
172            } else {
173                TextElementType::Blank
174            }
175        };
176        match end.map(|p| &rest[..=p]) {
177            Some([text @ .., b'}']) => {
178                self.ptr += text.len();
179                error!(ErrorKind::UnbalancedClosingBrace, self.ptr)
180            }
181            Some([text @ .., b'\r', b'\n']) => {
182                self.ptr += text.len() + 1;
183                Ok((
184                    start_pos,
185                    self.ptr - 1,
186                    element_type(text),
187                    TextElementTermination::Crlf,
188                ))
189            }
190            Some([text @ .., b'\n']) => {
191                self.ptr += text.len() + 1;
192                Ok((
193                    start_pos,
194                    self.ptr,
195                    element_type(text),
196                    TextElementTermination::LineFeed,
197                ))
198            }
199            Some([text @ .., b'{']) => {
200                self.ptr += text.len();
201                Ok((
202                    start_pos,
203                    self.ptr,
204                    element_type(text),
205                    TextElementTermination::PlaceableStart,
206                ))
207            }
208            None => {
209                self.ptr += rest.len();
210                Ok((
211                    start_pos,
212                    self.ptr,
213                    element_type(rest),
214                    TextElementTermination::Eof,
215                ))
216            }
217            _ => unreachable!(),
218        }
219    }
220}