loose_liquid/
parser.rs

1use std::fs::File;
2use std::io::prelude::Read;
3use std::path;
4use std::sync;
5
6use liquid_core::error::{Result, ResultLiquidExt, ResultLiquidReplaceExt};
7use liquid_core::parser;
8use liquid_core::runtime;
9
10use super::Template;
11use crate::reflection;
12use liquid_core::partials;
13#[cfg(feature = "stdlib")]
14use liquid_lib::stdlib;
15
16type Partials = partials::EagerCompiler<partials::InMemorySource>;
17
18pub struct ParserBuilder<P = Partials>
19where
20    P: partials::PartialCompiler,
21{
22    blocks: parser::PluginRegistry<Box<dyn parser::ParseBlock>>,
23    tags: parser::PluginRegistry<Box<dyn parser::ParseTag>>,
24    filters: parser::PluginRegistry<Box<dyn parser::ParseFilter>>,
25    partials: Option<P>,
26}
27
28impl ParserBuilder<Partials> {
29    /// Create an empty Liquid parser
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    #[cfg(feature = "stdlib")]
35    pub fn with_stdlib() -> Self {
36        Self::new().stdlib()
37    }
38}
39
40impl<P> ParserBuilder<P>
41where
42    P: partials::PartialCompiler,
43{
44    #[cfg(feature = "stdlib")]
45    /// Create a Liquid parser with built-in Liquid features
46    pub fn stdlib(self) -> Self {
47        self.tag(stdlib::AssignTag)
48            .tag(stdlib::BreakTag)
49            .tag(stdlib::ContinueTag)
50            .tag(stdlib::CycleTag)
51            .tag(stdlib::IncludeTag)
52            .tag(stdlib::IncrementTag)
53            .tag(stdlib::DecrementTag)
54            .tag(stdlib::RenderTag)
55            .block(stdlib::RawBlock)
56            .block(stdlib::IfBlock)
57            .block(stdlib::UnlessBlock)
58            .block(stdlib::IfChangedBlock)
59            .block(stdlib::ForBlock)
60            .block(stdlib::TableRowBlock)
61            .block(stdlib::CommentBlock)
62            .block(stdlib::CaptureBlock)
63            .block(stdlib::CaseBlock)
64            .filter(stdlib::Abs)
65            .filter(stdlib::Append)
66            .filter(stdlib::AtLeast)
67            .filter(stdlib::AtMost)
68            .filter(stdlib::Capitalize)
69            .filter(stdlib::Ceil)
70            .filter(stdlib::Compact)
71            .filter(stdlib::Concat)
72            .filter(stdlib::Date)
73            .filter(stdlib::Default)
74            .filter(stdlib::DividedBy)
75            .filter(stdlib::Downcase)
76            .filter(stdlib::Escape)
77            .filter(stdlib::EscapeOnce)
78            .filter(stdlib::First)
79            .filter(stdlib::Floor)
80            .filter(stdlib::Join)
81            .filter(stdlib::Last)
82            .filter(stdlib::Lstrip)
83            .filter(stdlib::Map)
84            .filter(stdlib::Minus)
85            .filter(stdlib::Modulo)
86            .filter(stdlib::NewlineToBr)
87            .filter(stdlib::Plus)
88            .filter(stdlib::Prepend)
89            .filter(stdlib::Remove)
90            .filter(stdlib::RemoveFirst)
91            .filter(stdlib::Replace)
92            .filter(stdlib::ReplaceFirst)
93            .filter(stdlib::Reverse)
94            .filter(stdlib::Round)
95            .filter(stdlib::Rstrip)
96            .filter(stdlib::Size)
97            .filter(stdlib::Slice)
98            .filter(stdlib::Sort)
99            .filter(stdlib::SortNatural)
100            .filter(stdlib::Split)
101            .filter(stdlib::Strip)
102            .filter(stdlib::StripHtml)
103            .filter(stdlib::StripNewlines)
104            .filter(stdlib::Times)
105            .filter(stdlib::Truncate)
106            .filter(stdlib::TruncateWords)
107            .filter(stdlib::Uniq)
108            .filter(stdlib::Upcase)
109            .filter(stdlib::UrlDecode)
110            .filter(stdlib::UrlEncode)
111            .filter(stdlib::Where)
112    }
113
114    /// Inserts a new custom block into the parser
115    pub fn block<B: Into<Box<dyn parser::ParseBlock>>>(mut self, block: B) -> Self {
116        let block = block.into();
117        self.blocks
118            .register(block.reflection().start_tag().to_owned(), block);
119        self
120    }
121
122    /// Inserts a new custom tag into the parser
123    pub fn tag<T: Into<Box<dyn parser::ParseTag>>>(mut self, tag: T) -> Self {
124        let tag = tag.into();
125        self.tags.register(tag.reflection().tag().to_owned(), tag);
126        self
127    }
128
129    /// Inserts a new custom filter into the parser
130    pub fn filter<F: Into<Box<dyn parser::ParseFilter>>>(mut self, filter: F) -> Self {
131        let filter = filter.into();
132        self.filters
133            .register(filter.reflection().name().to_owned(), filter);
134        self
135    }
136
137    /// Set which partial-templates will be available.
138    pub fn partials<N: partials::PartialCompiler>(self, partials: N) -> ParserBuilder<N> {
139        let Self {
140            blocks,
141            tags,
142            filters,
143            partials: _partials,
144        } = self;
145        ParserBuilder {
146            blocks,
147            tags,
148            filters,
149            partials: Some(partials),
150        }
151    }
152
153    /// Create a parser
154    pub fn build(self) -> Result<Parser> {
155        let Self {
156            blocks,
157            tags,
158            filters,
159            partials,
160        } = self;
161
162        let mut options = parser::Language::empty();
163        options.blocks = blocks;
164        options.tags = tags;
165        options.filters = filters;
166        let options = sync::Arc::new(options);
167        let partials = partials
168            .map(|p| p.compile(options.clone()))
169            .map(|r| r.map(Some))
170            .unwrap_or(Ok(None))?
171            .map(|p| p.into());
172        let p = Parser { options, partials };
173        Ok(p)
174    }
175}
176
177impl<P> Default for ParserBuilder<P>
178where
179    P: partials::PartialCompiler,
180{
181    fn default() -> Self {
182        Self {
183            blocks: Default::default(),
184            tags: Default::default(),
185            filters: Default::default(),
186            partials: Default::default(),
187        }
188    }
189}
190
191impl<P> reflection::ParserReflection for ParserBuilder<P>
192where
193    P: partials::PartialCompiler,
194{
195    fn blocks<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::BlockReflection> + 'r> {
196        Box::new(self.blocks.plugins().map(|p| p.reflection()))
197    }
198
199    fn tags<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::TagReflection> + 'r> {
200        Box::new(self.tags.plugins().map(|p| p.reflection()))
201    }
202
203    fn filters<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::FilterReflection> + 'r> {
204        Box::new(self.filters.plugins().map(|p| p.reflection()))
205    }
206
207    fn partials<'r>(&'r self) -> Box<dyn Iterator<Item = &str> + 'r> {
208        Box::new(
209            self.partials
210                .as_ref()
211                .into_iter()
212                .flat_map(|s| s.source().names()),
213        )
214    }
215}
216
217#[derive(Default, Clone)]
218pub struct Parser {
219    options: sync::Arc<parser::Language>,
220    partials: Option<sync::Arc<dyn runtime::PartialStore + Send + Sync>>,
221}
222
223impl Parser {
224    pub fn new() -> Self {
225        Default::default()
226    }
227
228    /// Parses a liquid template, returning a Template object.
229    /// # Examples
230    ///
231    /// ## Minimal Template
232    ///
233    /// ```
234    /// let template = liquid::ParserBuilder::with_stdlib()
235    ///     .build().unwrap()
236    ///     .parse("Liquid!").unwrap();
237    ///
238    /// let globals = liquid::Object::new();
239    /// let output = template.render(&globals).unwrap();
240    /// assert_eq!(output, "Liquid!".to_string());
241    /// ```
242    ///
243    pub fn parse(&self, text: &str) -> Result<Template> {
244        let template = parser::parse(text, &self.options).map(runtime::Template::new)?;
245        Ok(Template {
246            template,
247            partials: self.partials.clone(),
248        })
249    }
250
251    /// Parse a liquid template from a file, returning a `Result<Template, Error>`.
252    /// # Examples
253    ///
254    /// ## Minimal Template
255    ///
256    /// `template.txt`:
257    ///
258    /// ```text
259    /// "Liquid {{data}}"
260    /// ```
261    ///
262    /// Your rust code:
263    ///
264    /// ```rust,no_run
265    /// let template = liquid::ParserBuilder::with_stdlib()
266    ///     .build().unwrap()
267    ///     .parse_file("path/to/template.txt").unwrap();
268    ///
269    /// let globals = liquid::object!({
270    ///     "data": 4f64,
271    /// });
272    /// let output = template.render(&globals).unwrap();
273    /// assert_eq!(output, "Liquid! 4\n".to_string());
274    /// ```
275    ///
276    pub fn parse_file<P: AsRef<path::Path>>(&self, file: P) -> Result<Template> {
277        self.parse_file_path(file.as_ref())
278    }
279
280    fn parse_file_path(&self, file: &path::Path) -> Result<Template> {
281        let mut f = File::open(file)
282            .replace("Cannot open file")
283            .context_key("path")
284            .value_with(|| file.to_string_lossy().into_owned().into())?;
285        let mut buf = String::new();
286        f.read_to_string(&mut buf)
287            .replace("Cannot read file")
288            .context_key("path")
289            .value_with(|| file.to_string_lossy().into_owned().into())?;
290
291        self.parse(&buf)
292    }
293}
294
295impl reflection::ParserReflection for Parser {
296    fn blocks<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::BlockReflection> + 'r> {
297        Box::new(self.options.blocks.plugins().map(|p| p.reflection()))
298    }
299
300    fn tags<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::TagReflection> + 'r> {
301        Box::new(self.options.tags.plugins().map(|p| p.reflection()))
302    }
303
304    fn filters<'r>(&'r self) -> Box<dyn Iterator<Item = &dyn parser::FilterReflection> + 'r> {
305        Box::new(self.options.filters.plugins().map(|p| p.reflection()))
306    }
307
308    fn partials<'r>(&'r self) -> Box<dyn Iterator<Item = &str> + 'r> {
309        Box::new(self.partials.as_ref().into_iter().flat_map(|s| s.names()))
310    }
311}