askama/filters/alloc.rs
1use alloc::string::String;
2use core::fmt::{self, Write};
3
4use super::MAX_LEN;
5use super::escape::HtmlSafeOutput;
6use crate::Result;
7
8/// Return an ephemeral `&str` for `$src: impl fmt::Display`
9///
10/// If `$str` is `&str` or `String`, this macro simply passes on its content.
11/// If it is neither, then the formatted data is collection into `&buffer`.
12///
13/// `return`s with an error if the formatting failed.
14macro_rules! try_to_str {
15 ($src:expr => $buffer:ident) => {
16 match format_args!("{}", $src) {
17 args => {
18 if let Some(s) = args.as_str() {
19 s
20 } else {
21 $buffer = String::new();
22 $buffer.write_fmt(args)?;
23 &$buffer
24 }
25 }
26 }
27 };
28}
29
30/// Formats arguments according to the specified format
31///
32/// The *second* argument to this filter must be a string literal (as in normal
33/// Rust). The two arguments are passed through to the `format!()`
34/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
35/// the Askama code generator, but the order is swapped to support filter
36/// composition.
37///
38/// ```ignore
39/// {{ value|fmt("{:?}") }}
40/// ```
41///
42/// ```
43/// # #[cfg(feature = "code-in-doc")] {
44/// # use askama::Template;
45/// /// ```jinja
46/// /// <div>{{ value|fmt("{:?}") }}</div>
47/// /// ```
48/// #[derive(Template)]
49/// #[template(ext = "html", in_doc = true)]
50/// struct Example {
51/// value: (usize, usize),
52/// }
53///
54/// assert_eq!(
55/// Example { value: (3, 4) }.to_string(),
56/// "<div>(3, 4)</div>"
57/// );
58/// # }
59/// ```
60///
61/// Compare with [format](./fn.format.html).
62pub fn fmt() {}
63
64/// Formats arguments according to the specified format
65///
66/// The first argument to this filter must be a string literal (as in normal
67/// Rust). All arguments are passed through to the `format!()`
68/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
69/// the Askama code generator.
70///
71/// ```ignore
72/// {{ "{:?}{:?}"|format(value, other_value) }}
73/// ```
74///
75/// ```
76/// # #[cfg(feature = "code-in-doc")] {
77/// # use askama::Template;
78/// /// ```jinja
79/// /// <div>{{ "{:?}"|format(value) }}</div>
80/// /// ```
81/// #[derive(Template)]
82/// #[template(ext = "html", in_doc = true)]
83/// struct Example {
84/// value: (usize, usize),
85/// }
86///
87/// assert_eq!(
88/// Example { value: (3, 4) }.to_string(),
89/// "<div>(3, 4)</div>"
90/// );
91/// # }
92/// ```
93///
94/// Compare with [fmt](./fn.fmt.html).
95pub fn format() {}
96
97/// Replaces line breaks in plain text with appropriate HTML
98///
99/// A single newline becomes an HTML line break `<br>` and a new line
100/// followed by a blank line becomes a paragraph break `<p>`.
101///
102/// ```
103/// # #[cfg(feature = "code-in-doc")] {
104/// # use askama::Template;
105/// /// ```jinja
106/// /// <div>{{ example|linebreaks }}</div>
107/// /// ```
108/// #[derive(Template)]
109/// #[template(ext = "html", in_doc = true)]
110/// struct Example<'a> {
111/// example: &'a str,
112/// }
113///
114/// assert_eq!(
115/// Example { example: "Foo\nBar\n\nBaz" }.to_string(),
116/// "<div><p>Foo<br/>Bar</p><p>Baz</p></div>"
117/// );
118/// # }
119/// ```
120#[inline]
121pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
122 fn linebreaks(s: &str) -> String {
123 let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
124 alloc::format!("<p>{linebroken}</p>")
125 }
126
127 let mut buffer;
128 Ok(HtmlSafeOutput(linebreaks(try_to_str!(s => buffer))))
129}
130
131/// Converts all newlines in a piece of plain text to HTML line breaks
132///
133/// ```
134/// # #[cfg(feature = "code-in-doc")] {
135/// # use askama::Template;
136/// /// ```jinja
137/// /// <div>{{ lines|linebreaksbr }}</div>
138/// /// ```
139/// #[derive(Template)]
140/// #[template(ext = "html", in_doc = true)]
141/// struct Example<'a> {
142/// lines: &'a str,
143/// }
144///
145/// assert_eq!(
146/// Example { lines: "a\nb\nc" }.to_string(),
147/// "<div>a<br/>b<br/>c</div>"
148/// );
149/// # }
150/// ```
151#[inline]
152pub fn linebreaksbr(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
153 fn linebreaksbr(s: &str) -> String {
154 s.replace('\n', "<br/>")
155 }
156
157 let mut buffer;
158 Ok(HtmlSafeOutput(linebreaksbr(try_to_str!(s => buffer))))
159}
160
161/// Replaces only paragraph breaks in plain text with appropriate HTML
162///
163/// A new line followed by a blank line becomes a paragraph break `<p>`.
164/// Paragraph tags only wrap content; empty paragraphs are removed.
165/// No `<br/>` tags are added.
166///
167/// ```
168/// # #[cfg(feature = "code-in-doc")] {
169/// # use askama::Template;
170/// /// ```jinja
171/// /// {{ lines|paragraphbreaks }}
172/// /// ```
173/// #[derive(Template)]
174/// #[template(ext = "html", in_doc = true)]
175/// struct Example<'a> {
176/// lines: &'a str,
177/// }
178///
179/// assert_eq!(
180/// Example { lines: "Foo\nBar\n\nBaz" }.to_string(),
181/// "<p>Foo\nBar</p><p>Baz</p>"
182/// );
183/// # }
184/// ```
185#[inline]
186pub fn paragraphbreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::Error> {
187 fn paragraphbreaks(s: &str) -> String {
188 let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
189 alloc::format!("<p>{linebroken}</p>")
190 }
191
192 let mut buffer;
193 Ok(HtmlSafeOutput(paragraphbreaks(try_to_str!(s => buffer))))
194}
195
196/// Converts to lowercase
197///
198/// ```
199/// # #[cfg(feature = "code-in-doc")] {
200/// # use askama::Template;
201/// /// ```jinja
202/// /// <div>{{ word|lower }}</div>
203/// /// ```
204/// #[derive(Template)]
205/// #[template(ext = "html", in_doc = true)]
206/// struct Example<'a> {
207/// word: &'a str,
208/// }
209///
210/// assert_eq!(
211/// Example { word: "FOO" }.to_string(),
212/// "<div>foo</div>"
213/// );
214///
215/// assert_eq!(
216/// Example { word: "FooBar" }.to_string(),
217/// "<div>foobar</div>"
218/// );
219/// # }
220/// ```
221#[inline]
222pub fn lower(s: impl fmt::Display) -> Result<String, fmt::Error> {
223 let mut buffer;
224 Ok(try_to_str!(s => buffer).to_lowercase())
225}
226
227/// Converts to lowercase, alias for the `|lower` filter
228///
229/// ```
230/// # #[cfg(feature = "code-in-doc")] {
231/// # use askama::Template;
232/// /// ```jinja
233/// /// <div>{{ word|lowercase }}</div>
234/// /// ```
235/// #[derive(Template)]
236/// #[template(ext = "html", in_doc = true)]
237/// struct Example<'a> {
238/// word: &'a str,
239/// }
240///
241/// assert_eq!(
242/// Example { word: "FOO" }.to_string(),
243/// "<div>foo</div>"
244/// );
245///
246/// assert_eq!(
247/// Example { word: "FooBar" }.to_string(),
248/// "<div>foobar</div>"
249/// );
250/// # }
251/// ```
252#[inline]
253pub fn lowercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
254 lower(s)
255}
256
257/// Converts to uppercase
258///
259/// ```
260/// # #[cfg(feature = "code-in-doc")] {
261/// # use askama::Template;
262/// /// ```jinja
263/// /// <div>{{ word|upper }}</div>
264/// /// ```
265/// #[derive(Template)]
266/// #[template(ext = "html", in_doc = true)]
267/// struct Example<'a> {
268/// word: &'a str,
269/// }
270///
271/// assert_eq!(
272/// Example { word: "foo" }.to_string(),
273/// "<div>FOO</div>"
274/// );
275///
276/// assert_eq!(
277/// Example { word: "FooBar" }.to_string(),
278/// "<div>FOOBAR</div>"
279/// );
280/// # }
281/// ```
282#[inline]
283pub fn upper(s: impl fmt::Display) -> Result<String, fmt::Error> {
284 let mut buffer;
285 Ok(try_to_str!(s => buffer).to_uppercase())
286}
287
288/// Converts to uppercase, alias for the `|upper` filter
289///
290/// ```
291/// # #[cfg(feature = "code-in-doc")] {
292/// # use askama::Template;
293/// /// ```jinja
294/// /// <div>{{ word|uppercase }}</div>
295/// /// ```
296/// #[derive(Template)]
297/// #[template(ext = "html", in_doc = true)]
298/// struct Example<'a> {
299/// word: &'a str,
300/// }
301///
302/// assert_eq!(
303/// Example { word: "foo" }.to_string(),
304/// "<div>FOO</div>"
305/// );
306///
307/// assert_eq!(
308/// Example { word: "FooBar" }.to_string(),
309/// "<div>FOOBAR</div>"
310/// );
311/// # }
312/// ```
313#[inline]
314pub fn uppercase(s: impl fmt::Display) -> Result<String, fmt::Error> {
315 upper(s)
316}
317
318/// Strip leading and trailing whitespace
319///
320/// ```
321/// # #[cfg(feature = "code-in-doc")] {
322/// # use askama::Template;
323/// /// ```jinja
324/// /// <div>{{ example|trim }}</div>
325/// /// ```
326/// #[derive(Template)]
327/// #[template(ext = "html", in_doc = true)]
328/// struct Example<'a> {
329/// example: &'a str,
330/// }
331///
332/// assert_eq!(
333/// Example { example: " Hello\tworld\t" }.to_string(),
334/// "<div>Hello\tworld</div>"
335/// );
336/// # }
337/// ```
338#[cfg(feature = "alloc")]
339pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
340 struct Collector(String);
341
342 impl fmt::Write for Collector {
343 fn write_str(&mut self, s: &str) -> fmt::Result {
344 match self.0.is_empty() {
345 true => self.0.write_str(s.trim_start()),
346 false => self.0.write_str(s),
347 }
348 }
349 }
350
351 let mut collector = Collector(String::new());
352 write!(collector, "{s}")?;
353 let Collector(mut s) = collector;
354 s.truncate(s.trim_end().len());
355 Ok(s)
356}
357
358/// Indent lines with `width` spaces
359///
360/// ```
361/// # #[cfg(feature = "code-in-doc")] {
362/// # use askama::Template;
363/// /// ```jinja
364/// /// <div>{{ example|indent(4) }}</div>
365/// /// ```
366/// #[derive(Template)]
367/// #[template(ext = "html", in_doc = true)]
368/// struct Example<'a> {
369/// example: &'a str,
370/// }
371///
372/// assert_eq!(
373/// Example { example: "hello\nfoo\nbar" }.to_string(),
374/// "<div>hello\n foo\n bar</div>"
375/// );
376/// # }
377/// ```
378#[inline]
379pub fn indent(s: impl fmt::Display, width: usize) -> Result<String, fmt::Error> {
380 fn indent(args: fmt::Arguments<'_>, width: usize) -> Result<String, fmt::Error> {
381 let mut buffer = String::new();
382 let s = if width >= MAX_LEN {
383 buffer.write_fmt(args)?;
384 return Ok(buffer);
385 } else if let Some(s) = args.as_str() {
386 if s.len() >= MAX_LEN {
387 return Ok(s.into());
388 } else {
389 s
390 }
391 } else {
392 buffer.write_fmt(args)?;
393 if buffer.len() >= MAX_LEN {
394 return Ok(buffer);
395 }
396 buffer.as_str()
397 };
398
399 let mut indented = String::new();
400 for (i, c) in s.char_indices() {
401 indented.push(c);
402
403 if c == '\n' && i < s.len() - 1 {
404 for _ in 0..width {
405 indented.push(' ');
406 }
407 }
408 }
409 Ok(indented)
410 }
411 indent(format_args!("{s}"), width)
412}
413
414/// Capitalize a value. The first character will be uppercase, all others lowercase.
415///
416/// ```
417/// # #[cfg(feature = "code-in-doc")] {
418/// # use askama::Template;
419/// /// ```jinja
420/// /// <div>{{ example|capitalize }}</div>
421/// /// ```
422/// #[derive(Template)]
423/// #[template(ext = "html", in_doc = true)]
424/// struct Example<'a> {
425/// example: &'a str,
426/// }
427///
428/// assert_eq!(
429/// Example { example: "hello" }.to_string(),
430/// "<div>Hello</div>"
431/// );
432///
433/// assert_eq!(
434/// Example { example: "hElLO" }.to_string(),
435/// "<div>Hello</div>"
436/// );
437/// # }
438/// ```
439#[inline]
440pub fn capitalize(s: impl fmt::Display) -> Result<String, fmt::Error> {
441 fn capitalize(s: &str) -> Result<String, fmt::Error> {
442 let mut chars = s.chars();
443 if let Some(c) = chars.next() {
444 let mut replacement = String::with_capacity(s.len());
445 replacement.extend(c.to_uppercase());
446 replacement.push_str(&chars.as_str().to_lowercase());
447 Ok(replacement)
448 } else {
449 Ok(String::new())
450 }
451 }
452
453 let mut buffer;
454 capitalize(try_to_str!(s => buffer))
455}
456
457/// Count the words in that string.
458///
459/// ```
460/// # #[cfg(feature = "code-in-doc")] {
461/// # use askama::Template;
462/// /// ```jinja
463/// /// <div>{{ example|wordcount }}</div>
464/// /// ```
465/// #[derive(Template)]
466/// #[template(ext = "html", in_doc = true)]
467/// struct Example<'a> {
468/// example: &'a str,
469/// }
470///
471/// assert_eq!(
472/// Example { example: "askama is sort of cool" }.to_string(),
473/// "<div>5</div>"
474/// );
475/// # }
476/// ```
477pub fn wordcount(s: impl fmt::Display) -> Result<usize, fmt::Error> {
478 let mut buffer;
479 Ok(try_to_str!(s => buffer).split_whitespace().count())
480}
481
482/// Return a title cased version of the value. Words will start with uppercase letters, all
483/// remaining characters are lowercase.
484///
485/// ```
486/// # #[cfg(feature = "code-in-doc")] {
487/// # use askama::Template;
488/// /// ```jinja
489/// /// <div>{{ example|title }}</div>
490/// /// ```
491/// #[derive(Template)]
492/// #[template(ext = "html", in_doc = true)]
493/// struct Example<'a> {
494/// example: &'a str,
495/// }
496///
497/// assert_eq!(
498/// Example { example: "hello WORLD" }.to_string(),
499/// "<div>Hello World</div>"
500/// );
501/// # }
502/// ```
503pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> {
504 let mut buffer;
505 let s = try_to_str!(s => buffer);
506 let mut need_capitalization = true;
507
508 // Sadly enough, we can't mutate a string when iterating over its chars, likely because it could
509 // change the size of a char, "breaking" the char indices.
510 let mut output = String::with_capacity(s.len());
511 for c in s.chars() {
512 if c.is_whitespace() {
513 output.push(c);
514 need_capitalization = true;
515 } else if need_capitalization {
516 match c.is_uppercase() {
517 true => output.push(c),
518 false => output.extend(c.to_uppercase()),
519 }
520 need_capitalization = false;
521 } else {
522 match c.is_lowercase() {
523 true => output.push(c),
524 false => output.extend(c.to_lowercase()),
525 }
526 }
527 }
528 Ok(output)
529}
530
531#[cfg(test)]
532mod tests {
533 use alloc::string::ToString;
534
535 use super::*;
536
537 #[test]
538 fn test_linebreaks() {
539 assert_eq!(
540 linebreaks("Foo\nBar Baz").unwrap().to_string(),
541 "<p>Foo<br/>Bar Baz</p>"
542 );
543 assert_eq!(
544 linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
545 "<p>Foo<br/>Bar</p><p>Baz</p>"
546 );
547 }
548
549 #[test]
550 fn test_linebreaksbr() {
551 assert_eq!(linebreaksbr("Foo\nBar").unwrap().to_string(), "Foo<br/>Bar");
552 assert_eq!(
553 linebreaksbr("Foo\nBar\n\nBaz").unwrap().to_string(),
554 "Foo<br/>Bar<br/><br/>Baz"
555 );
556 }
557
558 #[test]
559 fn test_paragraphbreaks() {
560 assert_eq!(
561 paragraphbreaks("Foo\nBar Baz").unwrap().to_string(),
562 "<p>Foo\nBar Baz</p>"
563 );
564 assert_eq!(
565 paragraphbreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
566 "<p>Foo\nBar</p><p>Baz</p>"
567 );
568 assert_eq!(
569 paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz")
570 .unwrap()
571 .to_string(),
572 "<p>Foo</p><p>\nBar</p><p>Baz</p>"
573 );
574 }
575
576 #[test]
577 fn test_lower() {
578 assert_eq!(lower("Foo").unwrap().to_string(), "foo");
579 assert_eq!(lower("FOO").unwrap().to_string(), "foo");
580 assert_eq!(lower("FooBar").unwrap().to_string(), "foobar");
581 assert_eq!(lower("foo").unwrap().to_string(), "foo");
582 }
583
584 #[test]
585 fn test_upper() {
586 assert_eq!(upper("Foo").unwrap().to_string(), "FOO");
587 assert_eq!(upper("FOO").unwrap().to_string(), "FOO");
588 assert_eq!(upper("FooBar").unwrap().to_string(), "FOOBAR");
589 assert_eq!(upper("foo").unwrap().to_string(), "FOO");
590 }
591
592 #[test]
593 fn test_trim() {
594 assert_eq!(trim(" Hello\tworld\t").unwrap().to_string(), "Hello\tworld");
595 }
596
597 #[test]
598 fn test_indent() {
599 assert_eq!(indent("hello", 2).unwrap().to_string(), "hello");
600 assert_eq!(indent("hello\n", 2).unwrap().to_string(), "hello\n");
601 assert_eq!(indent("hello\nfoo", 2).unwrap().to_string(), "hello\n foo");
602 assert_eq!(
603 indent("hello\nfoo\n bar", 4).unwrap().to_string(),
604 "hello\n foo\n bar"
605 );
606 assert_eq!(
607 indent("hello", 267_332_238_858).unwrap().to_string(),
608 "hello"
609 );
610 }
611
612 #[test]
613 fn test_capitalize() {
614 assert_eq!(capitalize("foo").unwrap().to_string(), "Foo".to_string());
615 assert_eq!(capitalize("f").unwrap().to_string(), "F".to_string());
616 assert_eq!(capitalize("fO").unwrap().to_string(), "Fo".to_string());
617 assert_eq!(capitalize("").unwrap().to_string(), String::new());
618 assert_eq!(capitalize("FoO").unwrap().to_string(), "Foo".to_string());
619 assert_eq!(
620 capitalize("foO BAR").unwrap().to_string(),
621 "Foo bar".to_string()
622 );
623 assert_eq!(
624 capitalize("äØÄÅÖ").unwrap().to_string(),
625 "Äøäåö".to_string()
626 );
627 assert_eq!(capitalize("ß").unwrap().to_string(), "SS".to_string());
628 assert_eq!(capitalize("ßß").unwrap().to_string(), "SSß".to_string());
629 }
630
631 #[test]
632 fn test_wordcount() {
633 assert_eq!(wordcount("").unwrap(), 0);
634 assert_eq!(wordcount(" \n\t").unwrap(), 0);
635 assert_eq!(wordcount("foo").unwrap(), 1);
636 assert_eq!(wordcount("foo bar").unwrap(), 2);
637 assert_eq!(wordcount("foo bar").unwrap(), 2);
638 }
639
640 #[test]
641 fn test_title() {
642 assert_eq!(&title("").unwrap(), "");
643 assert_eq!(&title(" \n\t").unwrap(), " \n\t");
644 assert_eq!(&title("foo").unwrap(), "Foo");
645 assert_eq!(&title(" foo").unwrap(), " Foo");
646 assert_eq!(&title("foo bar").unwrap(), "Foo Bar");
647 assert_eq!(&title("foo bar ").unwrap(), "Foo Bar ");
648 assert_eq!(&title("fOO").unwrap(), "Foo");
649 assert_eq!(&title("fOo BaR").unwrap(), "Foo Bar");
650 }
651
652 #[test]
653 fn fuzzed_indent_filter() {
654 let s = "hello\nfoo\nbar".to_string().repeat(1024);
655 assert_eq!(indent(s.clone(), 4).unwrap().to_string(), s);
656 }
657}