1#![allow(clippy::trivially_copy_pass_by_ref)]
7
8use std::fmt::{self, Write};
9
10#[cfg(feature = "serde-json")]
11mod json;
12#[cfg(feature = "serde-json")]
13pub use self::json::json;
14
15#[cfg(feature = "serde-yaml")]
16mod yaml;
17#[cfg(feature = "serde-yaml")]
18pub use self::yaml::yaml;
19
20#[allow(unused_imports)]
21use crate::error::Error::Fmt;
22use askama_escape::{Escaper, MarkupDisplay};
23#[cfg(feature = "humansize")]
24use dep_humansize::{format_size_i, ToF64, DECIMAL};
25#[cfg(feature = "num-traits")]
26use dep_num_traits::{cast::NumCast, Signed};
27#[cfg(feature = "percent-encoding")]
28use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
29
30use super::Result;
31
32#[cfg(feature = "percent-encoding")]
33const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC
37 .remove(b'_')
38 .remove(b'.')
39 .remove(b'-')
40 .remove(b'~');
41
42#[cfg(feature = "percent-encoding")]
43const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
45
46pub fn safe<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
55where
56 E: Escaper,
57 T: fmt::Display,
58{
59 Ok(MarkupDisplay::new_safe(v, e))
60}
61
62pub fn escape<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
71where
72 E: Escaper,
73 T: fmt::Display,
74{
75 Ok(MarkupDisplay::new_unsafe(v, e))
76}
77
78#[cfg(feature = "humansize")]
79pub fn filesizeformat(b: &(impl ToF64 + Copy)) -> Result<String> {
81 Ok(format_size_i(*b, DECIMAL))
82}
83
84#[cfg(feature = "percent-encoding")]
85pub fn urlencode<T: fmt::Display>(s: T) -> Result<String> {
105 let s = s.to_string();
106 Ok(utf8_percent_encode(&s, URLENCODE_SET).to_string())
107}
108
109#[cfg(feature = "percent-encoding")]
110pub fn urlencode_strict<T: fmt::Display>(s: T) -> Result<String> {
125 let s = s.to_string();
126 Ok(utf8_percent_encode(&s, URLENCODE_STRICT_SET).to_string())
127}
128
129pub fn fmt() {}
143
144pub fn format() {}
157
158pub fn linebreaks<T: fmt::Display>(s: T) -> Result<String> {
163 let s = s.to_string();
164 let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
165
166 Ok(format!("<p>{linebroken}</p>"))
167}
168
169pub fn linebreaksbr<T: fmt::Display>(s: T) -> Result<String> {
171 let s = s.to_string();
172 Ok(s.replace('\n', "<br/>"))
173}
174
175pub fn paragraphbreaks<T: fmt::Display>(s: T) -> Result<String> {
181 let s = s.to_string();
182 let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
183
184 Ok(format!("<p>{linebroken}</p>"))
185}
186
187pub fn lower<T: fmt::Display>(s: T) -> Result<String> {
189 let s = s.to_string();
190 Ok(s.to_lowercase())
191}
192
193pub fn lowercase<T: fmt::Display>(s: T) -> Result<String> {
195 lower(s)
196}
197
198pub fn upper<T: fmt::Display>(s: T) -> Result<String> {
200 let s = s.to_string();
201 Ok(s.to_uppercase())
202}
203
204pub fn uppercase<T: fmt::Display>(s: T) -> Result<String> {
206 upper(s)
207}
208
209pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
211 let s = s.to_string();
212 Ok(s.trim().to_owned())
213}
214
215pub fn truncate<T: fmt::Display>(s: T, len: usize) -> Result<String> {
217 let mut s = s.to_string();
218 if s.len() > len {
219 let mut real_len = len;
220 while !s.is_char_boundary(real_len) {
221 real_len += 1;
222 }
223 s.truncate(real_len);
224 s.push_str("...");
225 }
226 Ok(s)
227}
228
229pub fn indent<T: fmt::Display>(s: T, width: usize) -> Result<String> {
231 let s = s.to_string();
232
233 let mut indented = String::new();
234
235 for (i, c) in s.char_indices() {
236 indented.push(c);
237
238 if c == '\n' && i < s.len() - 1 {
239 for _ in 0..width {
240 indented.push(' ');
241 }
242 }
243 }
244
245 Ok(indented)
246}
247
248#[cfg(feature = "num-traits")]
249pub fn into_f64<T>(number: T) -> Result<f64>
251where
252 T: NumCast,
253{
254 number.to_f64().ok_or(Fmt(fmt::Error))
255}
256
257#[cfg(feature = "num-traits")]
258pub fn into_isize<T>(number: T) -> Result<isize>
260where
261 T: NumCast,
262{
263 number.to_isize().ok_or(Fmt(fmt::Error))
264}
265
266pub fn join<T, I, S>(input: I, separator: S) -> Result<String>
268where
269 T: fmt::Display,
270 I: Iterator<Item = T>,
271 S: AsRef<str>,
272{
273 let separator: &str = separator.as_ref();
274
275 let mut rv = String::new();
276
277 for (num, item) in input.enumerate() {
278 if num > 0 {
279 rv.push_str(separator);
280 }
281
282 write!(rv, "{item}")?;
283 }
284
285 Ok(rv)
286}
287
288#[cfg(feature = "num-traits")]
289pub fn abs<T>(number: T) -> Result<T>
291where
292 T: Signed,
293{
294 Ok(number.abs())
295}
296
297pub fn capitalize<T: fmt::Display>(s: T) -> Result<String> {
299 let s = s.to_string();
300 match s.chars().next() {
301 Some(c) => {
302 let mut replacement: String = c.to_uppercase().collect();
303 replacement.push_str(&s[c.len_utf8()..].to_lowercase());
304 Ok(replacement)
305 }
306 _ => Ok(s),
307 }
308}
309
310pub fn center(src: &dyn fmt::Display, dst_len: usize) -> Result<String> {
312 let src = src.to_string();
313 let len = src.len();
314
315 if dst_len <= len {
316 Ok(src)
317 } else {
318 let diff = dst_len - len;
319 let mid = diff / 2;
320 let r = diff % 2;
321 let mut buf = String::with_capacity(dst_len);
322
323 for _ in 0..mid {
324 buf.push(' ');
325 }
326
327 buf.push_str(&src);
328
329 for _ in 0..mid + r {
330 buf.push(' ');
331 }
332
333 Ok(buf)
334 }
335}
336
337pub fn wordcount<T: fmt::Display>(s: T) -> Result<usize> {
339 let s = s.to_string();
340
341 Ok(s.split_whitespace().count())
342}
343
344#[cfg(feature = "markdown")]
345pub fn markdown<E, S>(
346 e: E,
347 s: S,
348 options: Option<&comrak::ComrakOptions>,
349) -> Result<MarkupDisplay<E, String>>
350where
351 E: Escaper,
352 S: AsRef<str>,
353{
354 use comrak::{
355 markdown_to_html, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions,
356 ComrakRenderOptions, ListStyleType,
357 };
358
359 const DEFAULT_OPTIONS: ComrakOptions = ComrakOptions {
360 extension: ComrakExtensionOptions {
361 strikethrough: true,
362 tagfilter: true,
363 table: true,
364 autolink: true,
365 tasklist: false,
367 superscript: false,
368 header_ids: None,
369 footnotes: false,
370 description_lists: false,
371 front_matter_delimiter: None,
372 },
373 parse: ComrakParseOptions {
374 smart: false,
376 default_info_string: None,
377 relaxed_tasklist_matching: false,
378 },
379 render: ComrakRenderOptions {
380 escape: true,
381 hardbreaks: false,
383 github_pre_lang: false,
384 full_info_string: false,
385 width: 0,
386 unsafe_: false,
387 list_style: ListStyleType::Dash,
388 sourcepos: false,
389 },
390 };
391
392 let s = markdown_to_html(s.as_ref(), options.unwrap_or(&DEFAULT_OPTIONS));
393 Ok(MarkupDisplay::new_safe(s, e))
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 #[cfg(feature = "num-traits")]
400 use std::f64::INFINITY;
401
402 #[cfg(feature = "humansize")]
403 #[test]
404 fn test_filesizeformat() {
405 assert_eq!(filesizeformat(&0).unwrap(), "0 B");
406 assert_eq!(filesizeformat(&999u64).unwrap(), "999 B");
407 assert_eq!(filesizeformat(&1000i32).unwrap(), "1 kB");
408 assert_eq!(filesizeformat(&1023).unwrap(), "1.02 kB");
409 assert_eq!(filesizeformat(&1024usize).unwrap(), "1.02 kB");
410 }
411
412 #[cfg(feature = "percent-encoding")]
413 #[test]
414 fn test_urlencoding() {
415 assert_eq!(urlencode("AZaz09").unwrap(), "AZaz09");
418 assert_eq!(urlencode_strict("AZaz09").unwrap(), "AZaz09");
419 assert_eq!(urlencode("_.-~").unwrap(), "_.-~");
421 assert_eq!(urlencode_strict("_.-~").unwrap(), "_.-~");
422
423 assert_eq!(urlencode(":/?#[]@").unwrap(), "%3A/%3F%23%5B%5D%40");
426 assert_eq!(
427 urlencode_strict(":/?#[]@").unwrap(),
428 "%3A%2F%3F%23%5B%5D%40"
429 );
430 assert_eq!(
432 urlencode("!$&'()*+,;=").unwrap(),
433 "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
434 );
435 assert_eq!(
436 urlencode_strict("!$&'()*+,;=").unwrap(),
437 "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
438 );
439
440 assert_eq!(
442 urlencode("žŠďŤňĚáÉóŮ").unwrap(),
443 "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
444 );
445 assert_eq!(
446 urlencode_strict("žŠďŤňĚáÉóŮ").unwrap(),
447 "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
448 );
449
450 assert_eq!(urlencode("🦀").unwrap(), "%F0%9F%A6%80");
452 assert_eq!(urlencode_strict("🦀").unwrap(), "%F0%9F%A6%80");
453 }
454
455 #[test]
456 fn test_linebreaks() {
457 assert_eq!(
458 linebreaks("Foo\nBar Baz").unwrap(),
459 "<p>Foo<br/>Bar Baz</p>"
460 );
461 assert_eq!(
462 linebreaks("Foo\nBar\n\nBaz").unwrap(),
463 "<p>Foo<br/>Bar</p><p>Baz</p>"
464 );
465 }
466
467 #[test]
468 fn test_linebreaksbr() {
469 assert_eq!(linebreaksbr("Foo\nBar").unwrap(), "Foo<br/>Bar");
470 assert_eq!(
471 linebreaksbr("Foo\nBar\n\nBaz").unwrap(),
472 "Foo<br/>Bar<br/><br/>Baz"
473 );
474 }
475
476 #[test]
477 fn test_paragraphbreaks() {
478 assert_eq!(
479 paragraphbreaks("Foo\nBar Baz").unwrap(),
480 "<p>Foo\nBar Baz</p>"
481 );
482 assert_eq!(
483 paragraphbreaks("Foo\nBar\n\nBaz").unwrap(),
484 "<p>Foo\nBar</p><p>Baz</p>"
485 );
486 assert_eq!(
487 paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz").unwrap(),
488 "<p>Foo</p><p>\nBar</p><p>Baz</p>"
489 );
490 }
491
492 #[test]
493 fn test_lower() {
494 assert_eq!(lower("Foo").unwrap(), "foo");
495 assert_eq!(lower("FOO").unwrap(), "foo");
496 assert_eq!(lower("FooBar").unwrap(), "foobar");
497 assert_eq!(lower("foo").unwrap(), "foo");
498 }
499
500 #[test]
501 fn test_upper() {
502 assert_eq!(upper("Foo").unwrap(), "FOO");
503 assert_eq!(upper("FOO").unwrap(), "FOO");
504 assert_eq!(upper("FooBar").unwrap(), "FOOBAR");
505 assert_eq!(upper("foo").unwrap(), "FOO");
506 }
507
508 #[test]
509 fn test_trim() {
510 assert_eq!(trim(" Hello\tworld\t").unwrap(), "Hello\tworld");
511 }
512
513 #[test]
514 fn test_truncate() {
515 assert_eq!(truncate("hello", 2).unwrap(), "he...");
516 let a = String::from("您好");
517 assert_eq!(a.len(), 6);
518 assert_eq!(String::from("您").len(), 3);
519 assert_eq!(truncate("您好", 1).unwrap(), "您...");
520 assert_eq!(truncate("您好", 2).unwrap(), "您...");
521 assert_eq!(truncate("您好", 3).unwrap(), "您...");
522 assert_eq!(truncate("您好", 4).unwrap(), "您好...");
523 assert_eq!(truncate("您好", 6).unwrap(), "您好");
524 assert_eq!(truncate("您好", 7).unwrap(), "您好");
525 let s = String::from("🤚a🤚");
526 assert_eq!(s.len(), 9);
527 assert_eq!(String::from("🤚").len(), 4);
528 assert_eq!(truncate("🤚a🤚", 1).unwrap(), "🤚...");
529 assert_eq!(truncate("🤚a🤚", 2).unwrap(), "🤚...");
530 assert_eq!(truncate("🤚a🤚", 3).unwrap(), "🤚...");
531 assert_eq!(truncate("🤚a🤚", 4).unwrap(), "🤚...");
532 assert_eq!(truncate("🤚a🤚", 5).unwrap(), "🤚a...");
533 assert_eq!(truncate("🤚a🤚", 6).unwrap(), "🤚a🤚...");
534 assert_eq!(truncate("🤚a🤚", 9).unwrap(), "🤚a🤚");
535 assert_eq!(truncate("🤚a🤚", 10).unwrap(), "🤚a🤚");
536 }
537
538 #[test]
539 fn test_indent() {
540 assert_eq!(indent("hello", 2).unwrap(), "hello");
541 assert_eq!(indent("hello\n", 2).unwrap(), "hello\n");
542 assert_eq!(indent("hello\nfoo", 2).unwrap(), "hello\n foo");
543 assert_eq!(
544 indent("hello\nfoo\n bar", 4).unwrap(),
545 "hello\n foo\n bar"
546 );
547 }
548
549 #[cfg(feature = "num-traits")]
550 #[test]
551 #[allow(clippy::float_cmp)]
552 fn test_into_f64() {
553 assert_eq!(into_f64(1).unwrap(), 1.0_f64);
554 assert_eq!(into_f64(1.9).unwrap(), 1.9_f64);
555 assert_eq!(into_f64(-1.9).unwrap(), -1.9_f64);
556 assert_eq!(into_f64(INFINITY as f32).unwrap(), INFINITY);
557 assert_eq!(into_f64(-INFINITY as f32).unwrap(), -INFINITY);
558 }
559
560 #[cfg(feature = "num-traits")]
561 #[test]
562 fn test_into_isize() {
563 assert_eq!(into_isize(1).unwrap(), 1_isize);
564 assert_eq!(into_isize(1.9).unwrap(), 1_isize);
565 assert_eq!(into_isize(-1.9).unwrap(), -1_isize);
566 assert_eq!(into_isize(1.5_f64).unwrap(), 1_isize);
567 assert_eq!(into_isize(-1.5_f64).unwrap(), -1_isize);
568 match into_isize(INFINITY) {
569 Err(Fmt(fmt::Error)) => {}
570 _ => panic!("Should return error of type Err(Fmt(fmt::Error))"),
571 };
572 }
573
574 #[allow(clippy::needless_borrow)]
575 #[test]
576 fn test_join() {
577 assert_eq!(
578 join((&["hello", "world"]).iter(), ", ").unwrap(),
579 "hello, world"
580 );
581 assert_eq!(join((&["hello"]).iter(), ", ").unwrap(), "hello");
582
583 let empty: &[&str] = &[];
584 assert_eq!(join(empty.iter(), ", ").unwrap(), "");
585
586 let input: Vec<String> = vec!["foo".into(), "bar".into(), "bazz".into()];
587 assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar:bazz");
588
589 let input: &[String] = &["foo".into(), "bar".into()];
590 assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar");
591
592 let real: String = "blah".into();
593 let input: Vec<&str> = vec![&real];
594 assert_eq!(join(input.iter(), ";").unwrap(), "blah");
595
596 assert_eq!(
597 join((&&&&&["foo", "bar"]).iter(), ", ").unwrap(),
598 "foo, bar"
599 );
600 }
601
602 #[cfg(feature = "num-traits")]
603 #[test]
604 #[allow(clippy::float_cmp)]
605 fn test_abs() {
606 assert_eq!(abs(1).unwrap(), 1);
607 assert_eq!(abs(-1).unwrap(), 1);
608 assert_eq!(abs(1.0).unwrap(), 1.0);
609 assert_eq!(abs(-1.0).unwrap(), 1.0);
610 assert_eq!(abs(1.0_f64).unwrap(), 1.0_f64);
611 assert_eq!(abs(-1.0_f64).unwrap(), 1.0_f64);
612 }
613
614 #[test]
615 fn test_capitalize() {
616 assert_eq!(capitalize("foo").unwrap(), "Foo".to_string());
617 assert_eq!(capitalize("f").unwrap(), "F".to_string());
618 assert_eq!(capitalize("fO").unwrap(), "Fo".to_string());
619 assert_eq!(capitalize("").unwrap(), "".to_string());
620 assert_eq!(capitalize("FoO").unwrap(), "Foo".to_string());
621 assert_eq!(capitalize("foO BAR").unwrap(), "Foo bar".to_string());
622 assert_eq!(capitalize("äØÄÅÖ").unwrap(), "Äøäåö".to_string());
623 assert_eq!(capitalize("ß").unwrap(), "SS".to_string());
624 assert_eq!(capitalize("ßß").unwrap(), "SSß".to_string());
625 }
626
627 #[test]
628 fn test_center() {
629 assert_eq!(center(&"f", 3).unwrap(), " f ".to_string());
630 assert_eq!(center(&"f", 4).unwrap(), " f ".to_string());
631 assert_eq!(center(&"foo", 1).unwrap(), "foo".to_string());
632 assert_eq!(center(&"foo bar", 8).unwrap(), "foo bar ".to_string());
633 }
634
635 #[test]
636 fn test_wordcount() {
637 assert_eq!(wordcount("").unwrap(), 0);
638 assert_eq!(wordcount(" \n\t").unwrap(), 0);
639 assert_eq!(wordcount("foo").unwrap(), 1);
640 assert_eq!(wordcount("foo bar").unwrap(), 2);
641 }
642}