1use crate::fail;
19use crate::prelude::HashMap;
20#[cfg(feature = "std")]
21use crate::prelude::Path;
22use crate::prelude::*;
23use aho_corasick::AhoCorasick;
24use alloc::collections::BTreeSet;
25use bon::Builder;
26use core::fmt;
27use core::iter::successors;
28use derive_more::Display;
29use fancy_regex::Regex;
30use fluent_uri::UriRef;
31use jiff::{tz::Offset, Timestamp};
32use jsonc_parser::{parse_to_serde_value, ParseOptions};
33#[cfg(feature = "std")]
34use nanoid::nanoid;
35use schemars::JsonSchema;
36use serde::de::DeserializeOwned;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39use strum::EnumIs;
40use validator::Validate;
41pub mod assets;
42#[cfg(feature = "std")]
43pub mod cmd;
44pub mod constants;
45pub mod macros;
46pub mod portable;
47pub mod terminal;
48pub use assets::Constant;
49use constants::{CROCKFORD_BASE32_ALPHABET, LINE_SEPARATOR};
50pub use terminal::Label;
51#[cfg(feature = "std")]
52pub use terminal::{
53 print_changes, print_changes_with_color, print_values_as_table, text_diff_changes, text_diff_changes_with_color, values_as_table,
54};
55pub trait LinkedData {
57 fn with_context(&self) -> Self;
59}
60pub trait MarkdownSupport {
62 fn to_markdown(&self) -> String;
64 fn from_markdown(content: &str) -> Result<Self, String>
66 where
67 Self: Sized,
68 {
69 let _ = content;
70 Err("Markdown parsing is not implemented for this type".to_string())
71 }
72 fn from_front_matter(value: &str) -> Result<Self, String>
74 where
75 Self: DeserializeOwned + Sized,
76 {
77 serde_norway::from_str::<Value>(value)
78 .map_err(|why| why.to_string())
79 .and_then(|value| serde_json::from_value(value).map_err(|why| why.to_string()))
80 }
81 fn to_front_matter(&self) -> Result<String, String>
83 where
84 Self: Serialize,
85 {
86 serde_json::to_value(self)
87 .map_err(|why| why.to_string())
88 .and_then(|value| serde_norway::to_string(&value).map_err(|why| why.to_string()))
89 }
90 fn to_markdown_text(&self) -> String
92 where
93 Self: AsRef<str>,
94 {
95 self.as_ref().replace('&', "&").replace('\r', " ").replace('\n', " ")
96 }
97 fn decode_markdown_text(&self) -> String
99 where
100 Self: AsRef<str>,
101 {
102 self.as_ref().replace(" ", "\r").replace(" ", "\n").replace("&", "&")
103 }
104 fn normalize(&self) -> String
106 where
107 Self: AsRef<str>,
108 {
109 self.as_ref()
110 .chars()
111 .filter(|character| !matches!(character, '-' | '_' | '.' | ','))
112 .collect::<String>()
113 .replace('&', "and")
114 .trim()
115 .to_string()
116 }
117}
118pub trait Searchable<T> {
120 fn contains(&self, _value: &str) -> bool {
124 false
125 }
126 fn find_by_iso(&self, _value: impl Into<String>) -> Option<T> {
130 None
131 }
132 fn find_by_name(&self, value: impl Into<String>) -> Option<T>;
134}
135pub trait StringConversion {
137 fn normalized(&self) -> String;
139 fn to_cross_platform_path(&self) -> String;
141 fn file_name_with_parent(&self) -> String;
143 fn to_absolute_path(&self) -> String;
145}
146pub trait StringInterpolation<T>
148where
149 T: AsRef<str> + ToString,
150{
151 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String;
153 fn with_indent(&self, spaces: usize) -> String;
155 fn with_additional_indent(&self, spaces: usize) -> String;
157}
158pub trait ToProse {
160 fn to_prose(&self) -> String;
162}
163pub trait ToStrings {
165 fn to_strings(&self) -> Vec<String>;
178 fn to_absolute_strings(&self) -> Vec<String> {
180 vec![]
181 }
182}
183pub trait ToStringChunks<T>
185where
186 T: AsRef<str> + ToString,
187{
188 fn chunk(&self, size: usize) -> Vec<String>;
190}
191pub trait Unstructured {
193 fn content(&self) -> &str;
195}
196#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
200pub enum ChecksumAlgorithm {
201 #[default]
203 #[display("sha256")]
204 #[serde(rename = "SHA256", alias = "sha256")]
205 Sha256,
206 #[display("md2")]
208 #[serde(rename = "MD2", alias = "md2")]
209 Md2,
210 #[display("md4")]
212 #[serde(rename = "MD4", alias = "md4")]
213 Md4,
214 #[display("md5")]
216 #[serde(rename = "MD5", alias = "md5")]
217 Md5,
218 #[display("md6")]
220 #[serde(rename = "MD6", alias = "md6")]
221 Md6,
222 #[display("sha1")]
224 #[serde(rename = "SHA1", alias = "sha1")]
225 Sha1,
226 #[display("sha224")]
228 #[serde(rename = "SHA224", alias = "sha224")]
229 Sha224,
230 #[display("sha384")]
232 #[serde(rename = "SHA384", alias = "sha384")]
233 Sha384,
234 #[display("sha512")]
236 #[serde(rename = "SHA512", alias = "sha512")]
237 Sha512,
238}
239#[derive(Clone, Debug, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
243pub enum License {
244 #[display("AGPL-3.0-only")]
246 #[serde(alias = "AGPL-3.0-only")]
247 Agpl3Only,
248 #[display("Apache-2.0")]
250 #[serde(alias = "Apache-2.0")]
251 Apache2,
252 #[display("BSD-3-Clause")]
254 #[serde(alias = "BSD-3-Clause")]
255 Bsd3Clause,
256 #[display("CC0-1.0")]
258 #[serde(alias = "CC0-1.0", alias = "Creative Commons CC-0")]
259 CreativeCommons,
260 #[display("GPL-2.0-only")]
262 #[serde(alias = "GPL-2.0-only")]
263 Gpl2Only,
264 #[display("GPL-2.0-with-classpath-exception")]
266 #[serde(alias = "GPL-2.0-with-classpath-exception")]
267 Gpl2WithClasspathException,
268 #[display("GPL-3.0-only")]
270 #[serde(alias = "GPL-3.0-only")]
271 Gpl3Only,
272 #[display("GPL-3.0-or-later")]
274 #[serde(alias = "GPL-3.0-or-later")]
275 Gpl3OrLater,
276 #[display("LGPL-2.1-only")]
278 #[serde(alias = "LGPL-2.1-only")]
279 Lgpl21Only,
280 #[display("LPPL-1.3c")]
282 #[serde(alias = "LPPL-1.3c")]
283 Lppl13c,
284 #[display("MIT")]
286 #[serde(alias = "MIT")]
287 Mit,
288 #[display("PostgreSQL")]
290 #[serde(alias = "PostgreSQL")]
291 PostgreSql,
292 #[display("Proprietary")]
294 #[serde(alias = "LicenseRef-Proprietary")]
295 Proprietary,
296 #[display("PSF-based")]
298 #[serde(alias = "PSF-based")]
299 PsfBased,
300 #[display("PSF-2.0")]
302 #[serde(alias = "PSF-2.0")]
303 Psf2,
304 #[display("Public Domain")]
306 #[serde(alias = "Public Domain")]
307 PublicDomain,
308 #[display("Unknown")]
310 Unknown,
311 #[display("Various")]
313 #[serde(alias = "Various")]
314 Various,
315 #[display("W3C")]
317 #[serde(alias = "W3C")]
318 W3C,
319}
320#[derive(Clone, Debug, Display, EnumIs, PartialEq)]
324pub enum MimeType {
325 #[display("application/yaml")]
331 Cff,
332 #[display("text/csv")]
334 Csv,
335 #[display("application/msword")]
337 Doc,
338 #[display("application/vnd.openxmlformats-officedocument.wordprocessingml.document")]
340 Docx,
341 #[display("application/epub+zip")]
343 Epub,
344 #[display("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
346 Excel,
347 #[display("application/vnd.gguf.model")]
351 Gguf,
352 #[display("application/ld+json")]
356 LdJson,
357 #[display("image/jpeg")]
359 Jpeg,
360 #[display("application/json")]
364 Json,
365 #[display("application/jsonc")]
369 Jsonc,
370 #[display("text/markdown")]
372 Markdown,
373 #[display("application/vnd.ai.modelcard.v1+json")]
375 ModelCard,
376 #[display("application/vnd.onnx.model")]
380 Onnx,
381 #[display("application/vnd.oasis.opendocument.presentation")]
383 Odp,
384 #[display("application/vnd.oasis.opendocument.spreadsheet")]
386 Ods,
387 #[display("application/vnd.oasis.opendocument.text")]
389 Odt,
390 #[display("font/otf")]
392 Otf,
393 #[display("application/x-parquet")]
397 Parquet,
398 #[display("application/pdf")]
400 Pdf,
401 #[display("image/png")]
403 Png,
404 #[display("application/vnd.ms-powerpoint")]
406 Ppt,
407 #[display("application/vnd.pytorch.model")]
411 Pytorch,
412 #[display("application/vnd.openxmlformats-officedocument.presentationml.presentation")]
416 Powerpoint,
417 #[display("application/vnd.ai.prompt.v1+json")]
421 Prompt,
422 #[display("application/rtf")]
424 Rtf,
425 #[display("text/rust")]
427 Rust,
428 #[display("application/vnd.safetensors")]
432 Safetensors,
433 #[display("application/spdx+json")]
437 Sbom,
438 #[display("image/svg+xml")]
440 Svg,
441 #[display("text/plain")]
445 Text,
446 #[display("application/toml")]
450 Toml,
451 #[display("font/ttf")]
453 Ttf,
454 #[display("application/yaml")]
458 Yaml,
459 #[display("application/zip")]
463 Zip,
464 #[display("application/vnd.{}", _0)]
466 Vendor(String),
467 #[display("application/octet-stream")]
469 Unknown(String),
470}
471#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
473pub struct Checksum {
474 pub algorithm: ChecksumAlgorithm,
476 #[serde(rename = "checksumValue")]
478 pub checksum_value: String,
479}
480#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
493#[builder(start_fn = init)]
494#[display("{}.{}.{}", major, minor, patch)]
495pub struct SemanticVersion {
496 #[builder(default = 0)]
498 pub major: u32,
499 #[builder(default = 0)]
501 pub minor: u32,
502 #[builder(default = 0)]
504 pub patch: u32,
505}
506impl fmt::Display for Checksum {
507 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508 write!(f, "{}", self.checksum_value)
509 }
510}
511impl From<&str> for ChecksumAlgorithm {
512 fn from(value: &str) -> Self {
513 match value.to_lowercase().as_str() {
514 | "sha256" => ChecksumAlgorithm::Sha256,
515 | "md5" => ChecksumAlgorithm::Md5,
516 | "sha1" => ChecksumAlgorithm::Sha1,
517 | "sha512" => ChecksumAlgorithm::Sha512,
518 | "md2" => ChecksumAlgorithm::Md2,
519 | "md4" => ChecksumAlgorithm::Md4,
520 | "md6" => ChecksumAlgorithm::Md6,
521 | "sha224" => ChecksumAlgorithm::Sha224,
522 | "sha384" => ChecksumAlgorithm::Sha384,
523 | _ => ChecksumAlgorithm::default(),
524 }
525 }
526}
527impl From<String> for ChecksumAlgorithm {
528 fn from(value: String) -> Self {
529 ChecksumAlgorithm::from(value.as_str())
530 }
531}
532impl<T: AsRef<str>> From<T> for License
533where
534 T: ToString,
535{
536 fn from(value: T) -> Self {
543 match value.as_ref().to_lowercase().as_str() {
544 | "agpl-3.0-only" => License::Agpl3Only,
545 | "apache-2.0" => License::Apache2,
546 | "bsd-2-clause" | "bsd-3-clause" => License::Bsd3Clause,
547 | "cc0-1.0" | "creative commons cc-0" => License::CreativeCommons,
548 | "gpl-1.0-or-later" | "gpl-2.0-only" => License::Gpl2Only,
549 | "gpl-2.0-with-classpath-exception" => License::Gpl2WithClasspathException,
550 | "gpl-3.0-only" => License::Gpl3Only,
551 | "gpl-3.0-or-later" => License::Gpl3OrLater,
552 | "lgpl-2.1-only" => License::Lgpl21Only,
553 | "lppl-1.3c" => License::Lppl13c,
554 | "mit" => License::Mit,
555 | "postgresql" => License::PostgreSql,
556 | "proprietary" | "licenseref-proprietary" => License::Proprietary,
557 | "psf-based" => License::PsfBased,
558 | "psf-2.0" => License::Psf2,
559 | "public-domain" | "public domain" => License::PublicDomain,
560 | "various" => License::Various,
561 | "w3c" => License::W3C,
562 | _ => License::Unknown,
563 }
564 }
565}
566impl License {
567 #[allow(dead_code)]
568 fn from_technology(value: &str) -> Option<License> {
569 let data = Constant::csv("technology");
570 let result = data
571 .into_iter()
572 .map(|row| row.into_iter().take(5).collect::<Vec<String>>())
573 .find(|pair| pair.first().map(|s| s.as_str()) == Some(value));
574 match result {
575 | Some(pair) => pair.get(4).map(|s| License::from(s.clone())),
576 | None => None,
577 }
578 }
579 #[allow(dead_code)]
580 fn is_open_source(&self) -> bool {
581 let data = Constant::csv("technology");
582 let result = data
583 .into_iter()
584 .map(|row| row.into_iter().skip(4).take(2).collect::<Vec<String>>())
585 .find(|pair| pair.first().map(|s| s.as_str()) == Some(self.to_string().as_str()));
586 match result {
587 | Some(value) => value.get(1).map(|s| s.as_str()) == Some("true"),
588 | None => false,
589 }
590 }
591}
592impl From<&str> for MimeType {
593 fn from(value: &str) -> Self {
621 let name = value.to_lowercase();
622 match file_extension(name.clone()) {
623 | Some(value) => match value.as_str() {
624 | "cff" => MimeType::Cff,
625 | "csv" => MimeType::Csv,
626 | "doc" => MimeType::Doc,
627 | "docx" | "docm" => MimeType::Docx,
628 | "epub" => MimeType::Epub,
629 | "gguf" => MimeType::Gguf,
630 | "jpg" | "jpeg" => MimeType::Jpeg,
631 | "json" => MimeType::Json,
632 | "jsonc" => MimeType::Jsonc,
633 | "jsonld" | "json-ld" => MimeType::LdJson,
634 | "md" | "markdown" => MimeType::Markdown,
635 | "onnx" => MimeType::Onnx,
636 | "odp" => MimeType::Odp,
637 | "ods" => MimeType::Ods,
638 | "odt" => MimeType::Odt,
639 | "otf" => MimeType::Otf,
640 | "ttf" => MimeType::Ttf,
641 | "parquet" => MimeType::Parquet,
642 | "pdf" => MimeType::Pdf,
643 | "png" => MimeType::Png,
644 | "pt" | "pth" => MimeType::Pytorch,
645 | "ppt" | "pps" | "pot" => MimeType::Ppt,
646 | "pptx" | "pptm" | "ppsx" | "ppsm" => MimeType::Powerpoint,
647 | "prompt" => MimeType::Prompt,
648 | "rtf" => MimeType::Rtf,
649 | "rs" => MimeType::Rust,
650 | "safetensors" => MimeType::Safetensors,
651 | "spdx.json" => MimeType::Sbom,
652 | "svg" => MimeType::Svg,
653 | "toml" => MimeType::Toml,
654 | "txt" => MimeType::Text,
655 | "xls" | "xlsb" | "xlsm" | "xlsx" => MimeType::Excel,
656 | "yml" | "yaml" => MimeType::Yaml,
657 | value => MimeType::Vendor(value.to_string()),
658 },
659 | None => MimeType::Unknown(name),
660 }
661 }
662}
663impl From<&String> for MimeType {
664 fn from(value: &String) -> Self {
665 Self::from(value.as_str())
666 }
667}
668impl From<String> for MimeType {
669 fn from(value: String) -> Self {
670 Self::from(value.as_str())
671 }
672}
673impl MimeType {
674 pub fn file_type(self) -> String {
683 match self {
684 | MimeType::Cff => "cff",
685 | MimeType::Csv => "csv",
686 | MimeType::Doc => "doc",
687 | MimeType::Docx => "docx",
688 | MimeType::Epub => "epub",
689 | MimeType::Excel => "xlsx",
690 | MimeType::Gguf => "gguf",
691 | MimeType::Jpeg => "jpeg",
692 | MimeType::Json => "json",
693 | MimeType::Jsonc => "jsonc",
694 | MimeType::LdJson => "jsonld",
695 | MimeType::Markdown => "md",
696 | MimeType::ModelCard => "modelcard",
697 | MimeType::Onnx => "onnx",
698 | MimeType::Odp => "odp",
699 | MimeType::Ods => "ods",
700 | MimeType::Odt => "odt",
701 | MimeType::Otf => "otf",
702 | MimeType::Ttf => "ttf",
703 | MimeType::Parquet => "parquet",
704 | MimeType::Pdf => "pdf",
705 | MimeType::Png => "png",
706 | MimeType::Ppt => "ppt",
707 | MimeType::Pytorch => "pt",
708 | MimeType::Powerpoint => "pptx",
709 | MimeType::Prompt => "prompt",
710 | MimeType::Rtf => "rtf",
711 | MimeType::Rust => "rs",
712 | MimeType::Safetensors => "safetensors",
713 | MimeType::Sbom => "spdx.json",
714 | MimeType::Svg => "svg",
715 | MimeType::Text => "txt",
716 | MimeType::Toml => "toml",
717 | MimeType::Yaml => "yaml",
718 | MimeType::Zip => "zip",
719 | _ => "unknown-file-type",
720 }
721 .to_string()
722 }
723}
724impl Default for SemanticVersion {
725 fn default() -> Self {
726 SemanticVersion::init().build()
727 }
728}
729impl From<&str> for SemanticVersion {
730 fn from(value: &str) -> Self {
740 let token = value
741 .split(|c: char| !(c.is_ascii_digit() || c == '.'))
742 .find(|x: &&str| x.chars().any(|c: char| c.is_ascii_digit()))
743 .unwrap_or("");
744 let parts = token
745 .split('.')
746 .filter(|x: &&str| !x.is_empty())
747 .map(|x: &str| x.parse::<u32>())
748 .collect::<Vec<_>>();
749 match parts.as_slice() {
750 | [Ok(major), Ok(minor), Ok(patch)] => SemanticVersion::init().major(*major).minor(*minor).patch(*patch).build(),
751 | [Ok(major), Ok(minor)] => SemanticVersion::init().major(*major).minor(*minor).build(),
752 | [Ok(major)] => SemanticVersion::init().major(*major).build(),
753 | _ => SemanticVersion::default(),
754 }
755 }
756}
757impl SemanticVersion {
758 pub fn from_string(value: impl AsRef<str>) -> Self {
760 Self::from(value.as_ref())
761 }
762}
763impl<T: AsRef<str>> StringInterpolation<T> for T
764where
765 T: ToString,
766{
767 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String {
768 match Regex::new(&format!(r"{{{{\s*{placeholder}\s*}}}}")) {
769 | Ok(re) => re.replace_all(self.as_ref(), value).to_string(),
770 | Err(err) => {
771 fail!("Regex replacement - {}", err);
772 self.to_string()
773 }
774 }
775 }
776 fn with_indent(&self, spaces: usize) -> String {
777 self.to_string()
778 .lines()
779 .map(|line| " ".repeat(spaces) + line.trim_start())
780 .collect::<Vec<_>>()
781 .join(LINE_SEPARATOR)
782 }
783 fn with_additional_indent(&self, spaces: usize) -> String {
784 let prefix = " ".repeat(spaces);
785 self.to_string()
786 .lines()
787 .map(|line| prefix.clone() + line)
788 .collect::<Vec<_>>()
789 .join(LINE_SEPARATOR)
790 }
791}
792impl MarkdownSupport for str {
793 fn to_markdown(&self) -> String {
794 self.to_string()
795 }
796}
797impl MarkdownSupport for String {
798 fn to_markdown(&self) -> String {
799 self.clone()
800 }
801}
802impl<P: AsRef<str>> MarkdownSupport for Vec<P> {
803 fn to_markdown(&self) -> String {
804 if self.is_empty() {
805 "[]".to_string()
806 } else {
807 self.iter()
808 .map(|x| format!("{LINE_SEPARATOR}- {}", x.as_ref()))
809 .collect::<Vec<String>>()
810 .join("")
811 }
812 }
813}
814impl<P: AsRef<str>> MarkdownSupport for Option<Vec<P>> {
815 fn to_markdown(&self) -> String {
816 match &self {
817 | Some(values) => values.to_markdown(),
818 | None => "[]".to_string(),
819 }
820 }
821}
822impl<T: AsRef<str>> ToStringChunks<T> for T
823where
824 T: ToString,
825{
826 fn chunk(&self, size: usize) -> Vec<String> {
827 self.as_ref()
828 .as_bytes()
829 .chunks(size)
830 .filter_map(|chunk| String::from_utf8(chunk.to_vec()).ok())
831 .collect::<Vec<_>>()
832 }
833}
834pub fn base32_crockford_encode(value: u128) -> String {
846 if value == 0 {
847 "0".to_string()
848 } else {
849 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
850 successors(Some(value), |&n| (n >= MODULUS).then_some(n / MODULUS))
851 .map(|n| char::from(*CROCKFORD_BASE32_ALPHABET.get((n % MODULUS) as usize).unwrap_or(&0)))
852 .collect::<Vec<_>>()
853 .into_iter()
854 .rev()
855 .collect::<String>()
856 .to_ascii_lowercase()
857 }
858}
859pub fn base32_crockford_decode(value: impl AsRef<str>) -> Option<u128> {
874 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
875 value
876 .as_ref()
877 .chars()
878 .filter(|c| !c.is_whitespace() && *c != '-' && *c != '_')
879 .try_fold(0u128, |acc, c| {
880 let digit = crockford_digit(c)?;
881 match acc.checked_mul(MODULUS) {
882 | Some(value) => value.checked_add(digit),
883 | None => None,
884 }
885 })
886}
887pub fn constant_time_eq<A: AsRef<[u8]>, B: AsRef<[u8]>>(a: A, b: B) -> bool {
890 let (a, b) = (a.as_ref(), b.as_ref());
891 a.len() == b.len() && a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
892}
893pub fn contains_any(patterns: &[&str], haystack: &str) -> bool {
895 match AhoCorasick::new(patterns) {
896 | Ok(matcher) => matcher.is_match(haystack),
897 | Err(_) => false,
898 }
899}
900pub fn contains_any_with_prefix(haystack: &str, prefix: &str, suffixes: &[&str]) -> bool {
902 haystack.contains(prefix) && contains_any(suffixes, haystack)
903}
904fn crockford_digit(value: char) -> Option<u128> {
905 let upper = value.to_ascii_uppercase();
906 let normalized = if upper == 'O' {
907 '0'
908 } else if upper == 'I' || upper == 'L' {
909 '1'
910 } else {
911 upper
912 };
913 let byte = normalized as u8;
914 CROCKFORD_BASE32_ALPHABET.iter().position(|&b| b == byte).map(|index| index as u128)
915}
916pub fn detect_json(text: impl ToString) -> bool {
918 let text = text.to_string();
919 let options = ParseOptions {
920 allow_comments: true,
921 allow_trailing_commas: true,
922 allow_loose_object_property_names: false,
923 allow_missing_commas: false,
924 allow_single_quoted_strings: false,
925 allow_hexadecimal_numbers: false,
926 allow_unary_plus_numbers: false,
927 };
928 serde_json::from_str::<Value>(&text).is_ok() || parse_to_serde_value::<Value>(&text, &options).is_ok()
929}
930pub fn detect_xml(text: impl ToString) -> bool {
932 let content = text.to_string();
933 let trimmed = content.trim();
934 if trimmed.starts_with('<') {
935 let mut reader = quick_xml::Reader::from_str(trimmed);
936 let mut buf = vec![];
937 loop {
938 match reader.read_event_into(&mut buf) {
939 | Ok(quick_xml::events::Event::Eof) => return true,
940 | Err(_) => return false,
941 | _ => buf.clear(),
942 }
943 }
944 } else {
945 false
946 }
947}
948pub fn file_extension<S>(value: S) -> Option<String>
960where
961 S: Into<String>,
962{
963 let filename = value.into();
964 let segments = filename.split('.').filter(|x| !x.is_empty()).collect::<Vec<_>>();
965 if !segments.is_empty() {
966 let last_segment = segments.last().map(|value| (*value).to_string());
967 let has_extension = filename.contains(".") && segments.len() > 1;
968 match last_segment {
969 | Some(value) => {
970 let is_filename = !(value.contains("/") || value.is_empty());
971 if has_extension && is_filename {
972 Some(value)
973 } else {
974 None
975 }
976 }
977 | None => None,
978 }
979 } else {
980 None
981 }
982}
983pub fn find_first(values: Vec<(String, String)>, pattern: &str) -> Option<(String, String)> {
994 let results = values
995 .clone()
996 .into_iter()
997 .filter(|x| !x.1.is_empty())
998 .find(|(key, _)| key.starts_with(pattern));
999 match results {
1000 | Some(value) => Some(value),
1001 | None => None,
1002 }
1003}
1004pub fn format_bytes(bytes: u64) -> String {
1006 let units = ["B", "KB", "MB", "GB", "TB"];
1007 let (size, index) = successors(Some((bytes as f64, 0usize)), |(size, index)| {
1008 if *size >= 1024.0 && *index < units.len().saturating_sub(1) {
1009 Some((size / 1024.0, index.saturating_add(1)))
1010 } else {
1011 None
1012 }
1013 })
1014 .last()
1015 .unwrap_or((bytes as f64, 0));
1016 if index == 0 {
1017 format!("{} {}", bytes, units.get(index).unwrap_or(&""))
1018 } else {
1019 format!("{:.2} {}", size, units.get(index).unwrap_or(&""))
1020 }
1021}
1022pub fn frontmatter_and_body<S>(value: S) -> (Option<String>, String)
1039where
1040 S: AsRef<str>,
1041{
1042 let content = value.as_ref();
1043 let pattern = r"(?s)---\s*(?<frontmatter>.*?)\s*---\s*(?<body>.*)";
1044 let groups = vec!["frontmatter", "body"];
1045 let lookup = regex_capture_lookup(pattern, content, groups);
1046 (
1047 lookup.get("frontmatter").cloned().filter(|s| !s.is_empty()),
1048 lookup.get("body").cloned().unwrap_or_else(|| content.trim().to_string()),
1049 )
1050}
1051pub fn generate_guid() -> String {
1053 #[cfg(feature = "std")]
1054 {
1055 let alphabet = [
1056 '-', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', 'a', 'b', 'c',
1057 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
1058 ];
1059 nanoid!(10, &alphabet)
1060 }
1061 #[cfg(not(feature = "std"))]
1062 String::new()
1063}
1064pub fn glob_matches(path: &str, pattern: &str) -> bool {
1066 fn go(path: &[u8], pattern: &[u8]) -> bool {
1067 match pattern.split_first() {
1068 | None => path.is_empty(),
1069 | Some((b'*', rest)) => go(path, rest) || path.split_first().is_some_and(|(_, tail)| go(tail, pattern)),
1070 | Some((b'?', rest)) => path.split_first().is_some_and(|(_, tail)| go(tail, rest)),
1071 | Some((p, rest)) => path.split_first().is_some_and(|(q, tail)| p == q && go(tail, rest)),
1072 }
1073 }
1074 go(path.as_bytes(), pattern.as_bytes())
1075}
1076#[cfg(feature = "std")]
1080pub fn is_filetype<I, S, P>(extensions: I) -> impl Fn(&P) -> bool
1081where
1082 I: IntoIterator<Item = S>,
1083 S: AsRef<str>,
1084 P: AsRef<Path> + ?Sized,
1085{
1086 let extensions = extensions
1087 .into_iter()
1088 .map(|extension| extension.as_ref().trim_start_matches('.').to_ascii_lowercase())
1089 .collect::<Vec<_>>();
1090 move |path| {
1091 let path = path.as_ref();
1092 let filename = path.file_name().and_then(|value| value.to_str()).unwrap_or_default();
1093 let extension = path.extension().and_then(|value| value.to_str()).unwrap_or_default();
1094 extensions
1095 .iter()
1096 .any(|allowed| filename.eq_ignore_ascii_case(allowed) || extension.eq_ignore_ascii_case(allowed))
1097 }
1098}
1099pub fn is_uri_or_path(value: &str) -> bool {
1101 value.starts_with('/')
1102 || value.starts_with("./")
1103 || value.starts_with("../")
1104 || {
1105 #[cfg(feature = "std")]
1106 {
1107 Path::new(value).is_absolute()
1108 }
1109 #[cfg(not(feature = "std"))]
1110 {
1111 false
1112 }
1113 }
1114 || (if let Ok(uri) = UriRef::parse(value) {
1115 uri.scheme().is_some()
1116 } else {
1117 false
1118 })
1119}
1120pub fn merge<A, B, S, T>(a: A, b: Option<B>) -> Vec<String>
1124where
1125 A: IntoIterator<Item = S>,
1126 B: IntoIterator<Item = T>,
1127 S: AsRef<str>,
1128 T: AsRef<str>,
1129{
1130 let mut seen = HashSet::new();
1131 a.into_iter()
1132 .map(|value| value.as_ref().trim().to_string())
1133 .chain(b.into_iter().flatten().map(|value| value.as_ref().trim().to_string()))
1134 .filter(|value| !value.is_empty() && seen.insert(value.clone()))
1135 .collect()
1136}
1137pub fn merge_unique<T: Ord>(left: impl IntoIterator<Item = T>, right: impl IntoIterator<Item = T>) -> Vec<T> {
1139 left.into_iter().chain(right).collect::<BTreeSet<_>>().into_iter().collect()
1140}
1141pub fn regex_capture_lookup<S>(pattern: S, text: S, groups: Vec<S>) -> HashMap<S, String>
1158where
1159 S: Into<String> + AsRef<str> + Clone + core::cmp::Eq + core::hash::Hash,
1160{
1161 match Regex::new(pattern.as_ref()) {
1162 | Ok(re) => re
1163 .captures_iter(text.as_ref())
1164 .last()
1165 .and_then(Result::ok)
1166 .map(|captures| {
1167 captures
1168 .iter()
1169 .skip(1)
1170 .enumerate()
1171 .filter_map(|(index, data)| data.and_then(|results| groups.get(index).cloned().map(|key| (key, results.as_str().to_string()))))
1172 .collect::<HashMap<S, String>>()
1173 })
1174 .unwrap_or_default(),
1175 | Err(_) => HashMap::new(),
1176 }
1177}
1178pub fn regex_join(patterns: &[String]) -> Option<String> {
1180 let groups = patterns
1181 .iter()
1182 .filter(|pattern| !pattern.is_empty())
1183 .map(|pattern| format!("(?:{pattern})"))
1184 .collect::<Vec<String>>();
1185 match groups.is_empty() {
1186 | true => None,
1187 | false => Some(groups.join("|")),
1188 }
1189}
1190pub fn regex_inverse(pattern: impl AsRef<str>) -> String {
1192 format!("^(?!.*(?:{})).*$", pattern.as_ref())
1193}
1194pub fn regex_to_glob(pattern: impl AsRef<str>) -> Option<String> {
1204 let mut bytes = pattern.as_ref().as_bytes().iter().peekable();
1205 let mut glob = String::new();
1206 let mut ok = true;
1207 let mut anchored_start = false;
1208 let mut anchored_end = false;
1209 if bytes.peek() == Some(&&b'^') {
1210 anchored_start = true;
1211 bytes.next();
1212 }
1213 while let Some(&byte) = bytes.next() {
1214 match byte {
1215 | b'\\' => match bytes.next() {
1216 | Some(&b'.') => glob.push('.'),
1217 | Some(b'd' | b'D' | b'w' | b'W' | b's' | b'S' | b'b' | b'B') | None => ok = false,
1218 | Some(&other) => glob.push(other as char),
1219 },
1220 | b'.' if bytes.peek() == Some(&&b'*') => {
1221 bytes.next();
1222 glob.push('*');
1223 }
1224 | b'.' if bytes.peek() == Some(&&b'+') => {
1225 bytes.next();
1226 glob.push('?');
1227 glob.push('*');
1228 }
1229 | b'.' | b'?' | b'+' | b'[' | b'(' | b'|' | b'{' => ok = false,
1230 | b'$' if bytes.peek().is_some() => ok = false,
1231 | b'$' => anchored_end = true,
1232 | other => glob.push(other as char),
1233 }
1234 }
1235 if ok && !glob.is_empty() {
1236 if !anchored_start {
1237 glob.insert(0, '*');
1238 }
1239 if !anchored_end {
1240 glob.push('*');
1241 }
1242 Some(glob)
1243 } else {
1244 None
1245 }
1246}
1247pub fn strip_suffixes<'a>(values: &[&str], name: &'a str) -> &'a str {
1249 values.iter().find_map(|value| name.strip_suffix(value)).unwrap_or(name)
1250}
1251pub fn suffix<T>(value: T) -> String
1262where
1263 T: PartialEq + From<u8>,
1264{
1265 (if value == T::from(1) { "" } else { "s" }).to_string()
1266}
1267pub fn to_ascii_alphanumeric(value: &str) -> String {
1269 value
1270 .chars()
1271 .filter(|character| character.is_ascii_alphanumeric())
1272 .collect::<String>()
1273 .to_ascii_lowercase()
1274}
1275pub fn to_rfc3339(value: Timestamp) -> String {
1277 value.display_with_offset(Offset::UTC).to_string()
1278}
1279pub fn to_string(values: Vec<&str>) -> Vec<String> {
1281 values.iter().map(|s| s.to_string()).collect()
1282}
1283pub fn trim_unmatched_trailing_parentheses(value: &str) -> &str {
1285 let opening = value.chars().filter(|character| *character == '(').count();
1286 let closing = value.chars().filter(|character| *character == ')').count();
1287 let trailing = value.chars().rev().take_while(|character| *character == ')').count();
1288 let trim = closing.saturating_sub(opening).min(trailing);
1289 value.get(..value.len().saturating_sub(trim)).unwrap_or(value)
1290}
1291
1292#[cfg(test)]
1293mod tests;