]> git.proxmox.com Git - rustc.git/blob - vendor/askama_shared/src/filters/mod.rs
New upstream version 1.69.0+dfsg1
[rustc.git] / vendor / askama_shared / src / filters / mod.rs
1 //! Module for built-in filter functions
2 //!
3 //! Contains all the built-in filter functions for use in templates.
4 //! You can define your own filters, as well.
5 //! For more information, read the [book](https://djc.github.io/askama/filters.html).
6 #![allow(clippy::trivially_copy_pass_by_ref)]
7
8 use std::fmt;
9
10 #[cfg(feature = "serde_json")]
11 mod json;
12 #[cfg(feature = "serde_json")]
13 pub use self::json::json;
14
15 #[cfg(feature = "serde_yaml")]
16 mod yaml;
17 #[cfg(feature = "serde_yaml")]
18 pub use self::yaml::yaml;
19
20 #[allow(unused_imports)]
21 use crate::error::Error::Fmt;
22 use askama_escape::{Escaper, MarkupDisplay};
23 #[cfg(feature = "humansize")]
24 use humansize::{file_size_opts, FileSize};
25 #[cfg(feature = "num-traits")]
26 use num_traits::{cast::NumCast, Signed};
27 #[cfg(feature = "percent-encoding")]
28 use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
29
30 use super::Result;
31
32 #[cfg(feature = "percent-encoding")]
33 // Urlencode char encoding set. Only the characters in the unreserved set don't
34 // have any special purpose in any part of a URI and can be safely left
35 // unencoded as specified in https://tools.ietf.org/html/rfc3986.html#section-2.3
36 const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC
37 .remove(b'_')
38 .remove(b'.')
39 .remove(b'-')
40 .remove(b'~');
41
42 #[cfg(feature = "percent-encoding")]
43 // Same as URLENCODE_STRICT_SET, but preserves forward slashes for encoding paths
44 const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
45
46 // This is used by the code generator to decide whether a named filter is part of
47 // Askama or should refer to a local `filters` module. It should contain all the
48 // filters shipped with Askama, even the optional ones (since optional inclusion
49 // in the const vector based on features seems impossible right now).
50 pub const BUILT_IN_FILTERS: [&str; 27] = [
51 "abs",
52 "capitalize",
53 "center",
54 "e",
55 "escape",
56 "filesizeformat",
57 "fmt",
58 "format",
59 "indent",
60 "into_f64",
61 "into_isize",
62 "join",
63 "linebreaks",
64 "linebreaksbr",
65 "paragraphbreaks",
66 "lower",
67 "lowercase",
68 "safe",
69 "trim",
70 "truncate",
71 "upper",
72 "uppercase",
73 "urlencode",
74 "urlencode_strict",
75 "wordcount",
76 "json", // Optional feature; reserve the name anyway
77 "yaml", // Optional feature; reserve the name anyway
78 ];
79
80 /// Marks a string (or other `Display` type) as safe
81 ///
82 /// Use this is you want to allow markup in an expression, or if you know
83 /// that the expression's contents don't need to be escaped.
84 ///
85 /// Askama will automatically insert the first (`Escaper`) argument,
86 /// so this filter only takes a single argument of any type that implements
87 /// `Display`.
88 pub fn safe<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
89 where
90 E: Escaper,
91 T: fmt::Display,
92 {
93 Ok(MarkupDisplay::new_safe(v, e))
94 }
95
96 /// Escapes `&`, `<` and `>` in strings
97 ///
98 /// Askama will automatically insert the first (`Escaper`) argument,
99 /// so this filter only takes a single argument of any type that implements
100 /// `Display`.
101 pub fn escape<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
102 where
103 E: Escaper,
104 T: fmt::Display,
105 {
106 Ok(MarkupDisplay::new_unsafe(v, e))
107 }
108
109 #[cfg(feature = "humansize")]
110 /// Returns adequate string representation (in KB, ..) of number of bytes
111 pub fn filesizeformat<B: FileSize>(b: &B) -> Result<String> {
112 b.file_size(file_size_opts::DECIMAL)
113 .map_err(|_| Fmt(fmt::Error))
114 }
115
116 #[cfg(feature = "percent-encoding")]
117 /// Percent-encodes the argument for safe use in URI; does not encode `/`.
118 ///
119 /// This should be safe for all parts of URI (paths segments, query keys, query
120 /// values). In the rare case that the server can't deal with forward slashes in
121 /// the query string, use [`urlencode_strict`], which encodes them as well.
122 ///
123 /// Encodes all characters except ASCII letters, digits, and `_.-~/`. In other
124 /// words, encodes all characters which are not in the unreserved set,
125 /// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3),
126 /// with the exception of `/`.
127 ///
128 /// ```none,ignore
129 /// <a href="/metro{{ "/stations/Château d'Eau"|urlencode }}">Station</a>
130 /// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode }}">Page</a>
131 /// ```
132 ///
133 /// To encode `/` as well, see [`urlencode_strict`](./fn.urlencode_strict.html).
134 ///
135 /// [`urlencode_strict`]: ./fn.urlencode_strict.html
136 pub fn urlencode<T: fmt::Display>(s: T) -> Result<String> {
137 let s = s.to_string();
138 Ok(utf8_percent_encode(&s, URLENCODE_SET).to_string())
139 }
140
141 #[cfg(feature = "percent-encoding")]
142 /// Percent-encodes the argument for safe use in URI; encodes `/`.
143 ///
144 /// Use this filter for encoding query keys and values in the rare case that
145 /// the server can't process them unencoded.
146 ///
147 /// Encodes all characters except ASCII letters, digits, and `_.-~`. In other
148 /// words, encodes all characters which are not in the unreserved set,
149 /// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3).
150 ///
151 /// ```none,ignore
152 /// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode_strict }}">Page</a>
153 /// ```
154 ///
155 /// If you want to preserve `/`, see [`urlencode`](./fn.urlencode.html).
156 pub fn urlencode_strict<T: fmt::Display>(s: T) -> Result<String> {
157 let s = s.to_string();
158 Ok(utf8_percent_encode(&s, URLENCODE_STRICT_SET).to_string())
159 }
160
161 /// Formats arguments according to the specified format
162 ///
163 /// The *second* argument to this filter must be a string literal (as in normal
164 /// Rust). The two arguments are passed through to the `format!()`
165 /// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
166 /// the Askama code generator, but the order is swapped to support filter
167 /// composition.
168 ///
169 /// ```ignore
170 /// {{ value | fmt("{:?}") }}
171 /// ```
172 ///
173 /// Compare with [format](./fn.format.html).
174 pub fn fmt() {}
175
176 /// Formats arguments according to the specified format
177 ///
178 /// The first argument to this filter must be a string literal (as in normal
179 /// Rust). All arguments are passed through to the `format!()`
180 /// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
181 /// the Askama code generator.
182 ///
183 /// ```ignore
184 /// {{ "{:?}{:?}" | format(value, other_value) }}
185 /// ```
186 ///
187 /// Compare with [fmt](./fn.fmt.html).
188 pub fn format() {}
189
190 /// Replaces line breaks in plain text with appropriate HTML
191 ///
192 /// A single newline becomes an HTML line break `<br>` and a new line
193 /// followed by a blank line becomes a paragraph break `<p>`.
194 pub fn linebreaks<T: fmt::Display>(s: T) -> Result<String> {
195 let s = s.to_string();
196 let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
197
198 Ok(format!("<p>{}</p>", linebroken))
199 }
200
201 /// Converts all newlines in a piece of plain text to HTML line breaks
202 pub fn linebreaksbr<T: fmt::Display>(s: T) -> Result<String> {
203 let s = s.to_string();
204 Ok(s.replace('\n', "<br/>"))
205 }
206
207 /// Replaces only paragraph breaks in plain text with appropriate HTML
208 ///
209 /// A new line followed by a blank line becomes a paragraph break `<p>`.
210 /// Paragraph tags only wrap content; empty paragraphs are removed.
211 /// No `<br/>` tags are added.
212 pub fn paragraphbreaks<T: fmt::Display>(s: T) -> Result<String> {
213 let s = s.to_string();
214 let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
215
216 Ok(format!("<p>{}</p>", linebroken))
217 }
218
219 /// Converts to lowercase
220 pub fn lower<T: fmt::Display>(s: T) -> Result<String> {
221 let s = s.to_string();
222 Ok(s.to_lowercase())
223 }
224
225 /// Alias for the `lower()` filter
226 pub fn lowercase<T: fmt::Display>(s: T) -> Result<String> {
227 lower(s)
228 }
229
230 /// Converts to uppercase
231 pub fn upper<T: fmt::Display>(s: T) -> Result<String> {
232 let s = s.to_string();
233 Ok(s.to_uppercase())
234 }
235
236 /// Alias for the `upper()` filter
237 pub fn uppercase<T: fmt::Display>(s: T) -> Result<String> {
238 upper(s)
239 }
240
241 /// Strip leading and trailing whitespace
242 pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
243 let s = s.to_string();
244 Ok(s.trim().to_owned())
245 }
246
247 /// Limit string length, appends '...' if truncated
248 pub fn truncate<T: fmt::Display>(s: T, len: usize) -> Result<String> {
249 let mut s = s.to_string();
250 if s.len() > len {
251 let mut real_len = len;
252 while !s.is_char_boundary(real_len) {
253 real_len += 1;
254 }
255 s.truncate(real_len);
256 s.push_str("...");
257 }
258 Ok(s)
259 }
260
261 /// Indent lines with `width` spaces
262 pub fn indent<T: fmt::Display>(s: T, width: usize) -> Result<String> {
263 let s = s.to_string();
264
265 let mut indented = String::new();
266
267 for (i, c) in s.char_indices() {
268 indented.push(c);
269
270 if c == '\n' && i < s.len() - 1 {
271 for _ in 0..width {
272 indented.push(' ');
273 }
274 }
275 }
276
277 Ok(indented)
278 }
279
280 #[cfg(feature = "num-traits")]
281 /// Casts number to f64
282 pub fn into_f64<T>(number: T) -> Result<f64>
283 where
284 T: NumCast,
285 {
286 number.to_f64().ok_or(Fmt(fmt::Error))
287 }
288
289 #[cfg(feature = "num-traits")]
290 /// Casts number to isize
291 pub fn into_isize<T>(number: T) -> Result<isize>
292 where
293 T: NumCast,
294 {
295 number.to_isize().ok_or(Fmt(fmt::Error))
296 }
297
298 /// Joins iterable into a string separated by provided argument
299 pub fn join<T, I, S>(input: I, separator: S) -> Result<String>
300 where
301 T: fmt::Display,
302 I: Iterator<Item = T>,
303 S: AsRef<str>,
304 {
305 let separator: &str = separator.as_ref();
306
307 let mut rv = String::new();
308
309 for (num, item) in input.enumerate() {
310 if num > 0 {
311 rv.push_str(separator);
312 }
313
314 rv.push_str(&format!("{}", item));
315 }
316
317 Ok(rv)
318 }
319
320 #[cfg(feature = "num-traits")]
321 /// Absolute value
322 pub fn abs<T>(number: T) -> Result<T>
323 where
324 T: Signed,
325 {
326 Ok(number.abs())
327 }
328
329 /// Capitalize a value. The first character will be uppercase, all others lowercase.
330 pub fn capitalize<T: fmt::Display>(s: T) -> Result<String> {
331 let mut s = s.to_string();
332
333 match s.get_mut(0..1).map(|s| {
334 s.make_ascii_uppercase();
335 &*s
336 }) {
337 None => Ok(s),
338 _ => {
339 s.get_mut(1..).map(|s| {
340 s.make_ascii_lowercase();
341 &*s
342 });
343 Ok(s)
344 }
345 }
346 }
347
348 /// Centers the value in a field of a given width
349 pub fn center(src: &dyn fmt::Display, dst_len: usize) -> Result<String> {
350 let src = src.to_string();
351 let len = src.len();
352
353 if dst_len <= len {
354 Ok(src)
355 } else {
356 let diff = dst_len - len;
357 let mid = diff / 2;
358 let r = diff % 2;
359 let mut buf = String::with_capacity(dst_len);
360
361 for _ in 0..mid {
362 buf.push(' ');
363 }
364
365 buf.push_str(&src);
366
367 for _ in 0..mid + r {
368 buf.push(' ');
369 }
370
371 Ok(buf)
372 }
373 }
374
375 /// Count the words in that string
376 pub fn wordcount<T: fmt::Display>(s: T) -> Result<usize> {
377 let s = s.to_string();
378
379 Ok(s.split_whitespace().count())
380 }
381
382 #[cfg(test)]
383 mod tests {
384 use super::*;
385 #[cfg(feature = "num-traits")]
386 use std::f64::INFINITY;
387
388 #[cfg(feature = "humansize")]
389 #[test]
390 fn test_filesizeformat() {
391 assert_eq!(filesizeformat(&0).unwrap(), "0 B");
392 assert_eq!(filesizeformat(&999u64).unwrap(), "999 B");
393 assert_eq!(filesizeformat(&1000i32).unwrap(), "1 KB");
394 assert_eq!(filesizeformat(&1023).unwrap(), "1.02 KB");
395 assert_eq!(filesizeformat(&1024usize).unwrap(), "1.02 KB");
396 }
397
398 #[cfg(feature = "percent-encoding")]
399 #[test]
400 fn test_urlencoding() {
401 // Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3)
402 // alpha / digit
403 assert_eq!(urlencode(&"AZaz09").unwrap(), "AZaz09");
404 assert_eq!(urlencode_strict(&"AZaz09").unwrap(), "AZaz09");
405 // other
406 assert_eq!(urlencode(&"_.-~").unwrap(), "_.-~");
407 assert_eq!(urlencode_strict(&"_.-~").unwrap(), "_.-~");
408
409 // Reserved (https://tools.ietf.org/html/rfc3986.html#section-2.2)
410 // gen-delims
411 assert_eq!(urlencode(&":/?#[]@").unwrap(), "%3A/%3F%23%5B%5D%40");
412 assert_eq!(
413 urlencode_strict(&":/?#[]@").unwrap(),
414 "%3A%2F%3F%23%5B%5D%40"
415 );
416 // sub-delims
417 assert_eq!(
418 urlencode(&"!$&'()*+,;=").unwrap(),
419 "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
420 );
421 assert_eq!(
422 urlencode_strict(&"!$&'()*+,;=").unwrap(),
423 "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
424 );
425
426 // Other
427 assert_eq!(
428 urlencode(&"žŠďŤňĚáÉóŮ").unwrap(),
429 "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
430 );
431 assert_eq!(
432 urlencode_strict(&"žŠďŤňĚáÉóŮ").unwrap(),
433 "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
434 );
435
436 // Ferris
437 assert_eq!(urlencode(&"🦀").unwrap(), "%F0%9F%A6%80");
438 assert_eq!(urlencode_strict(&"🦀").unwrap(), "%F0%9F%A6%80");
439 }
440
441 #[test]
442 fn test_linebreaks() {
443 assert_eq!(
444 linebreaks(&"Foo\nBar Baz").unwrap(),
445 "<p>Foo<br/>Bar Baz</p>"
446 );
447 assert_eq!(
448 linebreaks(&"Foo\nBar\n\nBaz").unwrap(),
449 "<p>Foo<br/>Bar</p><p>Baz</p>"
450 );
451 }
452
453 #[test]
454 fn test_linebreaksbr() {
455 assert_eq!(linebreaksbr(&"Foo\nBar").unwrap(), "Foo<br/>Bar");
456 assert_eq!(
457 linebreaksbr(&"Foo\nBar\n\nBaz").unwrap(),
458 "Foo<br/>Bar<br/><br/>Baz"
459 );
460 }
461
462 #[test]
463 fn test_paragraphbreaks() {
464 assert_eq!(
465 paragraphbreaks(&"Foo\nBar Baz").unwrap(),
466 "<p>Foo\nBar Baz</p>"
467 );
468 assert_eq!(
469 paragraphbreaks(&"Foo\nBar\n\nBaz").unwrap(),
470 "<p>Foo\nBar</p><p>Baz</p>"
471 );
472 assert_eq!(
473 paragraphbreaks(&"Foo\n\n\n\n\nBar\n\nBaz").unwrap(),
474 "<p>Foo</p><p>\nBar</p><p>Baz</p>"
475 );
476 }
477
478 #[test]
479 fn test_lower() {
480 assert_eq!(lower(&"Foo").unwrap(), "foo");
481 assert_eq!(lower(&"FOO").unwrap(), "foo");
482 assert_eq!(lower(&"FooBar").unwrap(), "foobar");
483 assert_eq!(lower(&"foo").unwrap(), "foo");
484 }
485
486 #[test]
487 fn test_upper() {
488 assert_eq!(upper(&"Foo").unwrap(), "FOO");
489 assert_eq!(upper(&"FOO").unwrap(), "FOO");
490 assert_eq!(upper(&"FooBar").unwrap(), "FOOBAR");
491 assert_eq!(upper(&"foo").unwrap(), "FOO");
492 }
493
494 #[test]
495 fn test_trim() {
496 assert_eq!(trim(&" Hello\tworld\t").unwrap(), "Hello\tworld");
497 }
498
499 #[test]
500 fn test_truncate() {
501 assert_eq!(truncate(&"hello", 2).unwrap(), "he...");
502 let a = String::from("您好");
503 assert_eq!(a.len(), 6);
504 assert_eq!(String::from("您").len(), 3);
505 assert_eq!(truncate(&"您好", 1).unwrap(), "您...");
506 assert_eq!(truncate(&"您好", 2).unwrap(), "您...");
507 assert_eq!(truncate(&"您好", 3).unwrap(), "您...");
508 assert_eq!(truncate(&"您好", 4).unwrap(), "您好...");
509 assert_eq!(truncate(&"您好", 6).unwrap(), "您好");
510 assert_eq!(truncate(&"您好", 7).unwrap(), "您好");
511 let s = String::from("🤚a🤚");
512 assert_eq!(s.len(), 9);
513 assert_eq!(String::from("🤚").len(), 4);
514 assert_eq!(truncate(&"🤚a🤚", 1).unwrap(), "🤚...");
515 assert_eq!(truncate(&"🤚a🤚", 2).unwrap(), "🤚...");
516 assert_eq!(truncate(&"🤚a🤚", 3).unwrap(), "🤚...");
517 assert_eq!(truncate(&"🤚a🤚", 4).unwrap(), "🤚...");
518 assert_eq!(truncate(&"🤚a🤚", 5).unwrap(), "🤚a...");
519 assert_eq!(truncate(&"🤚a🤚", 6).unwrap(), "🤚a🤚...");
520 assert_eq!(truncate(&"🤚a🤚", 9).unwrap(), "🤚a🤚");
521 assert_eq!(truncate(&"🤚a🤚", 10).unwrap(), "🤚a🤚");
522 }
523
524 #[test]
525 fn test_indent() {
526 assert_eq!(indent(&"hello", 2).unwrap(), "hello");
527 assert_eq!(indent(&"hello\n", 2).unwrap(), "hello\n");
528 assert_eq!(indent(&"hello\nfoo", 2).unwrap(), "hello\n foo");
529 assert_eq!(
530 indent(&"hello\nfoo\n bar", 4).unwrap(),
531 "hello\n foo\n bar"
532 );
533 }
534
535 #[cfg(feature = "num-traits")]
536 #[test]
537 #[allow(clippy::float_cmp)]
538 fn test_into_f64() {
539 assert_eq!(into_f64(1).unwrap(), 1.0_f64);
540 assert_eq!(into_f64(1.9).unwrap(), 1.9_f64);
541 assert_eq!(into_f64(-1.9).unwrap(), -1.9_f64);
542 assert_eq!(into_f64(INFINITY as f32).unwrap(), INFINITY);
543 assert_eq!(into_f64(-INFINITY as f32).unwrap(), -INFINITY);
544 }
545
546 #[cfg(feature = "num-traits")]
547 #[test]
548 fn test_into_isize() {
549 assert_eq!(into_isize(1).unwrap(), 1_isize);
550 assert_eq!(into_isize(1.9).unwrap(), 1_isize);
551 assert_eq!(into_isize(-1.9).unwrap(), -1_isize);
552 assert_eq!(into_isize(1.5_f64).unwrap(), 1_isize);
553 assert_eq!(into_isize(-1.5_f64).unwrap(), -1_isize);
554 match into_isize(INFINITY) {
555 Err(Fmt(fmt::Error)) => {}
556 _ => panic!("Should return error of type Err(Fmt(fmt::Error))"),
557 };
558 }
559
560 #[allow(clippy::needless_borrow)]
561 #[test]
562 fn test_join() {
563 assert_eq!(
564 join((&["hello", "world"]).iter(), ", ").unwrap(),
565 "hello, world"
566 );
567 assert_eq!(join((&["hello"]).iter(), ", ").unwrap(), "hello");
568
569 let empty: &[&str] = &[];
570 assert_eq!(join(empty.iter(), ", ").unwrap(), "");
571
572 let input: Vec<String> = vec!["foo".into(), "bar".into(), "bazz".into()];
573 assert_eq!(
574 join((&input).iter(), ":".to_string()).unwrap(),
575 "foo:bar:bazz"
576 );
577 assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar:bazz");
578 assert_eq!(join(input.iter(), ":".to_string()).unwrap(), "foo:bar:bazz");
579
580 let input: &[String] = &["foo".into(), "bar".into()];
581 assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar");
582 assert_eq!(join(input.iter(), ":".to_string()).unwrap(), "foo:bar");
583
584 let real: String = "blah".into();
585 let input: Vec<&str> = vec![&real];
586 assert_eq!(join(input.iter(), ";").unwrap(), "blah");
587
588 assert_eq!(
589 join((&&&&&["foo", "bar"]).iter(), ", ").unwrap(),
590 "foo, bar"
591 );
592 }
593
594 #[cfg(feature = "num-traits")]
595 #[test]
596 #[allow(clippy::float_cmp)]
597 fn test_abs() {
598 assert_eq!(abs(1).unwrap(), 1);
599 assert_eq!(abs(-1).unwrap(), 1);
600 assert_eq!(abs(1.0).unwrap(), 1.0);
601 assert_eq!(abs(-1.0).unwrap(), 1.0);
602 assert_eq!(abs(1.0_f64).unwrap(), 1.0_f64);
603 assert_eq!(abs(-1.0_f64).unwrap(), 1.0_f64);
604 }
605
606 #[test]
607 fn test_capitalize() {
608 assert_eq!(capitalize(&"foo").unwrap(), "Foo".to_string());
609 assert_eq!(capitalize(&"f").unwrap(), "F".to_string());
610 assert_eq!(capitalize(&"fO").unwrap(), "Fo".to_string());
611 assert_eq!(capitalize(&"").unwrap(), "".to_string());
612 assert_eq!(capitalize(&"FoO").unwrap(), "Foo".to_string());
613 assert_eq!(capitalize(&"foO BAR").unwrap(), "Foo bar".to_string());
614 }
615
616 #[test]
617 fn test_center() {
618 assert_eq!(center(&"f", 3).unwrap(), " f ".to_string());
619 assert_eq!(center(&"f", 4).unwrap(), " f ".to_string());
620 assert_eq!(center(&"foo", 1).unwrap(), "foo".to_string());
621 assert_eq!(center(&"foo bar", 8).unwrap(), "foo bar ".to_string());
622 }
623
624 #[test]
625 fn test_wordcount() {
626 assert_eq!(wordcount(&"").unwrap(), 0);
627 assert_eq!(wordcount(&" \n\t").unwrap(), 0);
628 assert_eq!(wordcount(&"foo").unwrap(), 1);
629 assert_eq!(wordcount(&"foo bar").unwrap(), 2);
630 }
631 }