1use crate::prelude::HashMap;
19#[cfg(feature = "std")]
20use crate::prelude::Path;
21use crate::prelude::*;
22use crate::{fail, skip};
23use aho_corasick::AhoCorasick;
24use bon::Builder;
25#[cfg(feature = "std")]
26use comfy_table::modifiers::UTF8_ROUND_CORNERS;
27#[cfg(feature = "std")]
28use comfy_table::presets::UTF8_FULL;
29#[cfg(feature = "std")]
30use comfy_table::{ContentArrangement, Table};
31#[cfg(feature = "unicode")]
32use console::Emoji;
33use convert_case::{Case, Casing};
34use core::fmt;
35use core::iter::successors;
36use derive_more::Display;
37use fancy_regex::Regex;
38use fluent_uri::UriRef;
39#[cfg(feature = "std")]
40use nanoid::nanoid;
41use owo_colors::{OwoColorize, Style, Styled};
42#[cfg(feature = "std")]
43use ring::digest::SHA512;
44use rust_embed::Embed;
45use schemars::JsonSchema;
46use serde::{Deserialize, Serialize};
47use serde_json::Value;
48use similar::{
49 ChangeTag::{self, Delete, Equal, Insert},
50 TextDiff,
51};
52use validator::Validate;
53#[cfg(feature = "std")]
54pub mod cmd;
55pub mod constants;
56pub mod macros;
57
58use constants::{CROCKFORD_BASE32_ALPHABET, LINE_SEPARATOR};
59
60pub trait LinkedData {
62 fn with_context(&self) -> Self;
64}
65pub trait Searchable<T> {
67 fn contains(&self, _value: &str) -> bool {
71 false
72 }
73 fn find_by_iso(&self, _value: impl Into<String>) -> Option<T> {
77 None
78 }
79 fn find_by_name(&self, value: impl Into<String>) -> Option<T>;
81}
82pub trait StringConversion {
84 fn file_name_with_parent(&self) -> String;
86 fn to_absolute_string(&self) -> String;
88}
89pub trait StringInterpolation<T>
91where
92 T: AsRef<str> + ToString,
93{
94 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String;
96 fn with_indent(&self, spaces: usize) -> String;
98}
99pub trait ToMarkdown {
101 fn to_markdown(&self) -> String;
103}
104pub trait ToProse {
106 fn to_prose(&self) -> String;
108}
109pub trait ToStrings {
111 fn to_strings(&self) -> Vec<String>;
124 fn to_absolute_strings(&self) -> Vec<String> {
126 vec![]
127 }
128}
129pub trait ToStringChunks<T>
131where
132 T: AsRef<str> + ToString,
133{
134 fn chunk(&self, size: usize) -> Vec<String>;
136}
137pub trait Unstructured {
139 fn content(&self) -> &str;
141}
142#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
146pub enum ChecksumAlgorithm {
147 #[default]
149 #[display("sha256")]
150 #[serde(rename = "SHA256", alias = "sha256")]
151 Sha256,
152 #[display("md2")]
154 #[serde(rename = "MD2", alias = "md2")]
155 Md2,
156 #[display("md4")]
158 #[serde(rename = "MD4", alias = "md4")]
159 Md4,
160 #[display("md5")]
162 #[serde(rename = "MD5", alias = "md5")]
163 Md5,
164 #[display("md6")]
166 #[serde(rename = "MD6", alias = "md6")]
167 Md6,
168 #[display("sha1")]
170 #[serde(rename = "SHA1", alias = "sha1")]
171 Sha1,
172 #[display("sha224")]
174 #[serde(rename = "SHA224", alias = "sha224")]
175 Sha224,
176 #[display("sha384")]
178 #[serde(rename = "SHA384", alias = "sha384")]
179 Sha384,
180 #[display("sha512")]
182 #[serde(rename = "SHA512", alias = "sha512")]
183 Sha512,
184}
185#[derive(Clone, Debug, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
189pub enum License {
190 #[display("AGPL-3.0-only")]
192 #[serde(alias = "AGPL-3.0-only")]
193 Agpl3Only,
194 #[display("Apache-2.0")]
196 #[serde(alias = "Apache-2.0")]
197 Apache2,
198 #[display("BSD-3-Clause")]
200 #[serde(alias = "BSD-3-Clause")]
201 Bsd3Clause,
202 #[display("CC0-1.0")]
204 #[serde(alias = "CC0-1.0", alias = "Creative Commons CC-0")]
205 CreativeCommons,
206 #[display("GPL-2.0-only")]
208 #[serde(alias = "GPL-2.0-only")]
209 Gpl2Only,
210 #[display("GPL-2.0-with-classpath-exception")]
212 #[serde(alias = "GPL-2.0-with-classpath-exception")]
213 Gpl2WithClasspathException,
214 #[display("GPL-3.0-only")]
216 #[serde(alias = "GPL-3.0-only")]
217 Gpl3Only,
218 #[display("GPL-3.0-or-later")]
220 #[serde(alias = "GPL-3.0-or-later")]
221 Gpl3OrLater,
222 #[display("LGPL-2.1-only")]
224 #[serde(alias = "LGPL-2.1-only")]
225 Lgpl21Only,
226 #[display("LPPL-1.3c")]
228 #[serde(alias = "LPPL-1.3c")]
229 Lppl13c,
230 #[display("MIT")]
232 #[serde(alias = "MIT")]
233 Mit,
234 #[display("PostgreSQL")]
236 #[serde(alias = "PostgreSQL")]
237 PostgreSql,
238 #[display("Proprietary")]
240 #[serde(alias = "LicenseRef-Proprietary")]
241 Proprietary,
242 #[display("PSF-based")]
244 #[serde(alias = "PSF-based")]
245 PsfBased,
246 #[display("PSF-2.0")]
248 #[serde(alias = "PSF-2.0")]
249 Psf2,
250 #[display("Public Domain")]
252 #[serde(alias = "Public Domain")]
253 PublicDomain,
254 #[display("Unknown")]
256 Unknown,
257 #[display("Various")]
259 #[serde(alias = "Various")]
260 Various,
261 #[display("W3C")]
263 #[serde(alias = "W3C")]
264 W3C,
265}
266#[derive(Clone, Debug, Display, PartialEq)]
270pub enum MimeType {
271 #[display("application/yaml")]
277 Cff,
278 #[display("text/csv")]
280 Csv,
281 #[display("application/vnd.gguf.model")]
285 Gguf,
286 #[display("application/ld+json")]
290 LdJson,
291 #[display("image/jpeg")]
293 Jpeg,
294 #[display("application/json")]
298 Json,
299 #[display("text/markdown")]
301 Markdown,
302 #[display("application/vnd.ai.modelcard.v1+json")]
304 ModelCard,
305 #[display("application/vnd.onnx.model")]
309 Onnx,
310 #[display("font/otf")]
312 Otf,
313 #[display("application/x-parquet")]
317 Parquet,
318 #[display("application/pdf")]
320 Pdf,
321 #[display("image/png")]
323 Png,
324 #[display("application/vnd.pytorch.model")]
328 Pytorch,
329 #[display("application/vnd.openxmlformats-officedocument.presentationml.presentation")]
333 Powerpoint,
334 #[display("application/vnd.ai.prompt.v1+json")]
338 Prompt,
339 #[display("text/rust")]
341 Rust,
342 #[display("application/vnd.safetensors")]
346 Safetensors,
347 #[display("application/spdx+json")]
351 Sbom,
352 #[display("image/svg+xml")]
354 Svg,
355 #[display("text/plain")]
359 Text,
360 #[display("application/toml")]
364 Toml,
365 #[display("font/ttf")]
367 Ttf,
368 #[display("application/yaml")]
372 Yaml,
373 #[display("application/zip")]
377 Zip,
378 #[display("application/vnd.{}", _0)]
380 Vendor(String),
381 #[display("application/octet-stream")]
383 Unknown(String),
384}
385#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
387pub struct Checksum {
388 pub algorithm: ChecksumAlgorithm,
390 #[serde(rename = "checksumValue")]
392 pub checksum_value: String,
393}
394#[derive(Embed)]
398#[folder = "assets/constants/"]
399pub struct Constant;
400pub struct Label {}
412#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
425#[builder(start_fn = init)]
426#[display("{}.{}.{}", major, minor, patch)]
427pub struct SemanticVersion {
428 #[builder(default = 0)]
430 pub major: u32,
431 #[builder(default = 0)]
433 pub minor: u32,
434 #[builder(default = 0)]
436 pub patch: u32,
437}
438impl fmt::Display for Checksum {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 write!(f, "{}", self.checksum_value)
441 }
442}
443impl From<&str> for ChecksumAlgorithm {
444 fn from(value: &str) -> Self {
445 match value.to_lowercase().as_str() {
446 | "sha256" => ChecksumAlgorithm::Sha256,
447 | "md5" => ChecksumAlgorithm::Md5,
448 | "sha1" => ChecksumAlgorithm::Sha1,
449 | "sha512" => ChecksumAlgorithm::Sha512,
450 | "md2" => ChecksumAlgorithm::Md2,
451 | "md4" => ChecksumAlgorithm::Md4,
452 | "md6" => ChecksumAlgorithm::Md6,
453 | "sha224" => ChecksumAlgorithm::Sha224,
454 | "sha384" => ChecksumAlgorithm::Sha384,
455 | _ => ChecksumAlgorithm::default(),
456 }
457 }
458}
459impl From<String> for ChecksumAlgorithm {
460 fn from(value: String) -> Self {
461 ChecksumAlgorithm::from(value.as_str())
462 }
463}
464#[cfg(feature = "std")]
465impl From<&'static ring::digest::Algorithm> for ChecksumAlgorithm {
466 fn from(algorithm: &'static ring::digest::Algorithm) -> Self {
467 if core::ptr::eq(algorithm, &SHA512) {
468 ChecksumAlgorithm::Sha512
469 } else {
470 ChecksumAlgorithm::Sha256
471 }
472 }
473}
474impl Constant {
475 pub fn from_asset(file_name: &str) -> Option<String> {
477 match Self::get(file_name) {
478 | Some(value) => Some(String::from_utf8_lossy(value.data.as_ref()).into()),
479 | None => None,
480 }
481 }
482 pub fn last_values(file_name: &str) -> impl Iterator<Item = String> {
486 Self::csv(file_name)
487 .into_iter()
488 .filter_map(|x| x.last().cloned())
489 .filter(|x| !x.is_empty())
490 }
491 pub fn nth_values(file_name: &str, column: usize) -> impl Iterator<Item = String> {
495 Self::csv(file_name)
496 .into_iter()
497 .filter_map(move |x| x.get(column).cloned())
498 .filter(|x| !x.is_empty())
499 }
500 pub fn read_lines(file_name: &str) -> Vec<String> {
502 Self::from_asset(file_name)
503 .map(|value| value.lines().map(String::from).collect())
504 .unwrap_or_default()
505 }
506 pub fn csv(file_name: impl AsRef<str>) -> Vec<Vec<String>> {
512 Self::read_lines(format!("{}.csv", file_name.as_ref()).as_str())
513 .into_iter()
514 .map(|x| x.split(",").map(String::from).collect())
515 .collect()
516 }
517 pub fn json<T>(file_name: impl AsRef<str>) -> T
522 where
523 T: Default + serde::de::DeserializeOwned,
524 {
525 Self::from_asset(format!("{}.json", file_name.as_ref()).as_str())
526 .map(|text| serde_json::from_str(&text).unwrap_or_default())
527 .unwrap_or_default()
528 }
529}
530impl Label {
531 #[cfg(not(feature = "unicode"))]
533 pub const CAUTION: &str = "!!! ";
534 #[cfg(feature = "unicode")]
536 pub const CAUTION: Emoji<'_, '_> = Emoji("⚠️ ", "!!! ");
537 #[cfg(not(feature = "unicode"))]
539 pub const CHECKMARK: &str = "✓ ";
540 #[cfg(feature = "unicode")]
542 pub const CHECKMARK: Emoji<'_, '_> = Emoji("✅ ", "☑ ");
543 pub const PROGRESS_BAR_TEMPLATE: &str = " {spinner:.green}{pos:>5} of{len:^5}[{bar:40.green}] {msg}";
547 pub const PROGRESS_SPINNER_TEMPLATE: &str = " {spinner:.green} {msg}";
549 pub const PROGRESS_COUNTER_TEMPLATE: &str = "{pos:>5} of{len:^5} {msg}";
551 pub fn dry_run() -> Styled<&'static &'static str> {
553 let style = Style::new().black().on_yellow();
554 " DRY_RUN ■ ".style(style)
555 }
556 pub fn invalid() -> String {
558 Label::fmt_invalid(" ✗ INVALID")
559 }
560 pub fn fmt_invalid(value: &str) -> String {
562 let style = Style::new().red().on_default_color();
563 value.style(style).to_string()
564 }
565 pub fn valid() -> String {
567 Label::fmt_valid(" ✓ VALID ")
568 }
569 pub fn fmt_valid(value: &str) -> String {
571 let style = Style::new().green().on_default_color();
572 value.style(style).to_string()
573 }
574 pub fn fail() -> String {
576 Label::fmt_fail("FAIL")
577 }
578 pub fn fmt_fail(value: &str) -> String {
580 let style = Style::new().white().on_red();
581 format!(" ✗ {value} ").style(style).to_string()
582 }
583 pub fn found() -> String {
585 Label::fmt_found("FOUND")
586 }
587 pub fn fmt_found(value: &str) -> String {
589 let style = Style::new().green().on_default_color();
590 value.to_string().style(style).to_string()
591 }
592 pub fn not_found() -> String {
594 Label::fmt_not_found("NOT_FOUND")
595 }
596 pub fn fmt_not_found(value: &str) -> String {
598 let style = Style::new().red().on_default_color();
599 value.style(style).to_string()
600 }
601 pub fn output() -> String {
603 Label::fmt_output("OUTPUT")
604 }
605 pub fn fmt_output(value: &str) -> String {
607 let style = Style::new().cyan().dimmed().on_default_color();
608 value.style(style).to_string()
609 }
610 pub fn pass() -> String {
612 Label::fmt_pass("SUCCESS")
613 }
614 pub fn fmt_pass(value: &str) -> String {
616 let style = Style::new().green().bold().on_default_color();
617 format!("{}{}", Label::CHECKMARK, value).style(style).to_string()
618 }
619 pub fn read() -> Styled<&'static &'static str> {
621 let style = Style::new().green().on_default_color();
622 "READ".style(style)
623 }
624 pub fn rejected() -> String {
626 Label::fmt_rejected("REJECTED")
627 }
628 pub fn fmt_rejected(value: &str) -> String {
630 let style = Style::new().red().on_default_color();
631 format!("🛑 {value} ").style(style).to_string()
632 }
633 pub fn run() -> String {
635 Label::fmt_run("RUN")
636 }
637 pub fn fmt_run(value: &str) -> String {
639 let style = Style::new().black().on_yellow();
640 format!(" {value} ▶ ").style(style).to_string()
641 }
642 pub fn skip() -> String {
644 Label::fmt_skip("SKIP")
645 }
646 pub fn fmt_skip(value: &str) -> String {
648 let style = Style::new().yellow().on_default_color();
649 format!("{}{} ", Label::CAUTION, value).style(style).to_string()
650 }
651 pub fn using() -> String {
653 Label::fmt_using("USING")
654 }
655 pub fn fmt_using(value: &str) -> String {
657 let style = Style::new().cyan();
658 value.style(style).to_string()
659 }
660}
661impl<T: AsRef<str>> From<T> for License
662where
663 T: ToString,
664{
665 fn from(value: T) -> Self {
672 match value.as_ref().to_lowercase().as_str() {
673 | "agpl-3.0-only" => License::Agpl3Only,
674 | "apache-2.0" => License::Apache2,
675 | "bsd-2-clause" => License::Bsd3Clause,
676 | "bsd-3-clause" => License::Bsd3Clause,
677 | "cc0-1.0" | "creative commons cc-0" => License::CreativeCommons,
678 | "gpl-1.0-or-later" => License::Gpl2Only,
679 | "gpl-2.0-only" => License::Gpl2Only,
680 | "gpl-2.0-with-classpath-exception" => License::Gpl2WithClasspathException,
681 | "gpl-3.0-only" => License::Gpl3Only,
682 | "gpl-3.0-or-later" => License::Gpl3OrLater,
683 | "lgpl-2.1-only" => License::Lgpl21Only,
684 | "lppl-1.3c" => License::Lppl13c,
685 | "mit" => License::Mit,
686 | "postgresql" => License::PostgreSql,
687 | "proprietary" | "licenseref-proprietary" => License::Proprietary,
688 | "psf-based" => License::PsfBased,
689 | "psf-2.0" => License::Psf2,
690 | "public-domain" | "public domain" => License::PublicDomain,
691 | "various" => License::Various,
692 | "w3c" => License::W3C,
693 | _ => License::Unknown,
694 }
695 }
696}
697impl License {
698 #[allow(dead_code)]
699 fn from_technology(value: &str) -> Option<License> {
700 let data = Constant::csv("technology");
701 let result = data
702 .into_iter()
703 .map(|row| row.into_iter().take(5).collect::<Vec<String>>())
704 .find(|pair| pair.first().map(|s| s.as_str()) == Some(value));
705 match result {
706 | Some(pair) => pair.get(4).map(|s| License::from(s.clone())),
707 | None => None,
708 }
709 }
710 #[allow(dead_code)]
711 fn is_open_source(&self) -> bool {
712 let data = Constant::csv("technology");
713 let result = data
714 .into_iter()
715 .map(|row| row.into_iter().skip(4).take(2).collect::<Vec<String>>())
716 .find(|pair| pair.first().map(|s| s.as_str()) == Some(self.to_string().as_str()));
717 match result {
718 | Some(value) => value.get(1).map(|s| s.as_str()) == Some("true"),
719 | None => false,
720 }
721 }
722}
723impl<T: AsRef<str>> From<T> for MimeType
724where
725 T: ToString,
726{
727 fn from(value: T) -> Self {
754 let name = value.to_string().to_lowercase();
755 match file_extension(name.clone()) {
756 | Some(value) => match value.as_str() {
757 | "cff" => MimeType::Cff,
758 | "csv" => MimeType::Csv,
759 | "gguf" => MimeType::Gguf,
760 | "jpg" | "jpeg" => MimeType::Jpeg,
761 | "json" => MimeType::Json,
762 | "jsonld" | "json-ld" => MimeType::LdJson,
763 | "md" | "markdown" => MimeType::Markdown,
764 | "onnx" => MimeType::Onnx,
765 | "otf" => MimeType::Otf,
766 | "ttf" => MimeType::Ttf,
767 | "parquet" => MimeType::Parquet,
768 | "pdf" => MimeType::Pdf,
769 | "png" => MimeType::Png,
770 | "pt" | "pth" => MimeType::Pytorch,
771 | "pptx" | "ppt" => MimeType::Powerpoint,
772 | "prompt" => MimeType::Prompt,
773 | "rs" => MimeType::Rust,
774 | "safetensors" => MimeType::Safetensors,
775 | "spdx.json" => MimeType::Sbom,
776 | "svg" => MimeType::Svg,
777 | "toml" => MimeType::Toml,
778 | "txt" => MimeType::Text,
779 | "yml" | "yaml" => MimeType::Yaml,
780 | value => MimeType::Vendor(value.to_string()),
781 },
782 | None => MimeType::Unknown(name),
783 }
784 }
785}
786impl MimeType {
787 pub fn file_type(self) -> String {
796 match self {
797 | MimeType::Cff => "cff",
798 | MimeType::Csv => "csv",
799 | MimeType::Gguf => "gguf",
800 | MimeType::Jpeg => "jpeg",
801 | MimeType::Json => "json",
802 | MimeType::LdJson => "jsonld",
803 | MimeType::Markdown => "md",
804 | MimeType::ModelCard => "modelcard",
805 | MimeType::Onnx => "onnx",
806 | MimeType::Otf => "otf",
807 | MimeType::Ttf => "ttf",
808 | MimeType::Parquet => "parquet",
809 | MimeType::Pdf => "pdf",
810 | MimeType::Png => "png",
811 | MimeType::Pytorch => "pt",
812 | MimeType::Powerpoint => "pptx",
813 | MimeType::Prompt => "prompt",
814 | MimeType::Rust => "rs",
815 | MimeType::Safetensors => "safetensors",
816 | MimeType::Sbom => "spdx.json",
817 | MimeType::Svg => "svg",
818 | MimeType::Text => "txt",
819 | MimeType::Toml => "toml",
820 | MimeType::Yaml => "yaml",
821 | MimeType::Zip => "zip",
822 | _ => "unknown-file-type",
823 }
824 .to_string()
825 }
826}
827impl Default for SemanticVersion {
828 fn default() -> Self {
829 SemanticVersion::init().build()
830 }
831}
832impl From<&str> for SemanticVersion {
833 fn from(value: &str) -> Self {
843 let token = value
844 .split(|c: char| !(c.is_ascii_digit() || c == '.'))
845 .find(|x: &&str| x.chars().any(|c: char| c.is_ascii_digit()))
846 .unwrap_or("");
847 let parts = token
848 .split('.')
849 .filter(|x: &&str| !x.is_empty())
850 .map(|x: &str| x.parse::<u32>())
851 .collect::<Vec<_>>();
852 match parts.as_slice() {
853 | [Ok(major), Ok(minor), Ok(patch)] => SemanticVersion::init().major(*major).minor(*minor).patch(*patch).build(),
854 | [Ok(major), Ok(minor)] => SemanticVersion::init().major(*major).minor(*minor).build(),
855 | [Ok(major)] => SemanticVersion::init().major(*major).build(),
856 | _ => SemanticVersion::default(),
857 }
858 }
859}
860impl<T: AsRef<str>> StringInterpolation<T> for T
861where
862 T: ToString,
863{
864 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String {
865 match Regex::new(&format!(r"{{{{\s*{placeholder}\s*}}}}")) {
866 | Ok(re) => re.replace_all(self.as_ref(), value).to_string(),
867 | Err(err) => {
868 fail!("Regex replacement - {}", err);
869 self.to_string()
870 }
871 }
872 }
873 fn with_indent(&self, spaces: usize) -> String {
874 self.to_string()
875 .lines()
876 .map(|line| " ".repeat(spaces) + line.trim_start())
877 .collect::<Vec<_>>()
878 .join(LINE_SEPARATOR)
879 }
880}
881impl<P: AsRef<str>> ToMarkdown for Vec<P> {
882 fn to_markdown(&self) -> String {
883 if self.is_empty() {
884 "[]".to_string()
885 } else {
886 self.iter()
887 .map(|x| format!("{LINE_SEPARATOR}- {}", x.as_ref()))
888 .collect::<Vec<String>>()
889 .join("")
890 }
891 }
892}
893impl<P: AsRef<str> + ToMarkdown> ToMarkdown for Option<Vec<P>> {
894 fn to_markdown(&self) -> String {
895 match &self {
896 | Some(values) => format!("{LINE_SEPARATOR}{}", values.to_markdown()),
897 | None => "[]".to_string(),
898 }
899 }
900}
901impl<T: AsRef<str>> ToStringChunks<T> for T
902where
903 T: ToString,
904{
905 fn chunk(&self, size: usize) -> Vec<String> {
906 self.as_ref()
907 .as_bytes()
908 .chunks(size)
909 .filter_map(|chunk| String::from_utf8(chunk.to_vec()).ok())
910 .collect::<Vec<_>>()
911 }
912}
913pub fn base32_crockford_encode(value: u128) -> String {
925 if value == 0 {
926 "0".to_string()
927 } else {
928 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
929 successors(Some(value), |&n| (n >= MODULUS).then_some(n / MODULUS))
930 .map(|n| char::from(*CROCKFORD_BASE32_ALPHABET.get((n % MODULUS) as usize).unwrap_or(&0)))
931 .collect::<Vec<_>>()
932 .into_iter()
933 .rev()
934 .collect::<String>()
935 .to_ascii_lowercase()
936 }
937}
938pub fn base32_crockford_decode(value: impl AsRef<str>) -> Option<u128> {
953 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
954 value
955 .as_ref()
956 .chars()
957 .filter(|c| !c.is_whitespace() && *c != '-' && *c != '_')
958 .try_fold(0u128, |acc, c| {
959 let digit = crockford_digit(c)?;
960 match acc.checked_mul(MODULUS) {
961 | Some(value) => value.checked_add(digit),
962 | None => None,
963 }
964 })
965}
966pub fn contains_any(patterns: &[&str], haystack: &str) -> bool {
968 match AhoCorasick::new(patterns) {
969 | Ok(matcher) => matcher.is_match(haystack),
970 | Err(_) => false,
971 }
972}
973pub fn contains_any_with_prefix(haystack: &str, prefix: &str, suffixes: &[&str]) -> bool {
975 haystack.contains(prefix) && contains_any(suffixes, haystack)
976}
977fn crockford_digit(value: char) -> Option<u128> {
978 let upper = value.to_ascii_uppercase();
979 let normalized = if upper == 'O' {
980 '0'
981 } else if upper == 'I' || upper == 'L' {
982 '1'
983 } else {
984 upper
985 };
986 let byte = normalized as u8;
987 CROCKFORD_BASE32_ALPHABET.iter().position(|&b| b == byte).map(|index| index as u128)
988}
989pub fn detect_json(text: impl ToString) -> bool {
991 serde_json::from_str::<Value>(&text.to_string()).is_ok()
992}
993pub fn detect_xml(text: impl ToString) -> bool {
995 let content = text.to_string();
996 let trimmed = content.trim();
997 if trimmed.starts_with('<') {
998 let mut reader = quick_xml::Reader::from_str(trimmed);
999 let mut buf = vec![];
1000 loop {
1001 match reader.read_event_into(&mut buf) {
1002 | Ok(quick_xml::events::Event::Eof) => return true,
1003 | Err(_) => return false,
1004 | _ => buf.clear(),
1005 }
1006 }
1007 } else {
1008 false
1009 }
1010}
1011pub fn file_extension<S>(value: S) -> Option<String>
1023where
1024 S: Into<String>,
1025{
1026 let filename = value.into();
1027 let segments = filename.split('.').filter(|x| !x.is_empty()).collect::<Vec<_>>();
1028 if !segments.is_empty() {
1029 let last_segment = segments.last().map(|value| (*value).to_string());
1030 let has_extension = filename.contains(".") && segments.len() > 1;
1031 match last_segment {
1032 | Some(value) => {
1033 let is_filename = !(value.contains("/") || value.is_empty());
1034 if has_extension && is_filename {
1035 Some(value)
1036 } else {
1037 None
1038 }
1039 }
1040 | None => None,
1041 }
1042 } else {
1043 None
1044 }
1045}
1046pub fn find_first(values: Vec<(String, String)>, pattern: &str) -> Option<(String, String)> {
1057 let results = values
1058 .clone()
1059 .into_iter()
1060 .filter(|x| !x.1.is_empty())
1061 .find(|(key, _)| key.starts_with(pattern));
1062 match results {
1063 | Some(value) => Some(value),
1064 | None => None,
1065 }
1066}
1067pub fn format_bytes(bytes: u64) -> String {
1069 let units = ["B", "KB", "MB", "GB", "TB"];
1070 let (size, index) = successors(Some((bytes as f64, 0usize)), |(size, index)| {
1071 if *size >= 1024.0 && *index < units.len().saturating_sub(1) {
1072 Some((size / 1024.0, index.saturating_add(1)))
1073 } else {
1074 None
1075 }
1076 })
1077 .last()
1078 .unwrap_or((bytes as f64, 0));
1079 if index == 0 {
1080 format!("{} {}", bytes, units.get(index).unwrap_or(&""))
1081 } else {
1082 format!("{:.2} {}", size, units.get(index).unwrap_or(&""))
1083 }
1084}
1085pub fn frontmatter_and_body<S>(value: S) -> (Option<String>, String)
1102where
1103 S: AsRef<str>,
1104{
1105 let content = value.as_ref();
1106 let pattern = r"(?s)---\s*(?<frontmatter>.*?)\s*---\s*(?<body>.*)";
1107 let groups = vec!["frontmatter", "body"];
1108 let lookup = regex_capture_lookup(pattern, content, groups);
1109 (
1110 lookup.get("frontmatter").cloned().filter(|s| !s.is_empty()),
1111 lookup.get("body").cloned().unwrap_or_else(|| content.trim().to_string()),
1112 )
1113}
1114pub fn generate_guid() -> String {
1124 #[cfg(feature = "std")]
1125 {
1126 let alphabet = [
1127 '-', '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',
1128 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
1129 ];
1130 let id = nanoid!(10, &alphabet);
1131 id
1132 }
1133 #[cfg(not(feature = "std"))]
1134 {
1135 String::new()
1136 }
1137}
1138pub fn is_uri_or_path(value: &str) -> bool {
1140 value.starts_with('/')
1141 || value.starts_with("./")
1142 || value.starts_with("../")
1143 || {
1144 #[cfg(feature = "std")]
1145 {
1146 Path::new(value).is_absolute()
1147 }
1148 #[cfg(not(feature = "std"))]
1149 {
1150 false
1151 }
1152 }
1153 || (if let Ok(uri) = UriRef::parse(value) {
1154 uri.scheme().is_some()
1155 } else {
1156 false
1157 })
1158}
1159pub fn print_changes(old: &str, new: &str) {
1166 let changes = text_diff_changes(old, new);
1167 let has_no_changes = changes.clone().into_iter().all(|(tag, _)| tag == Equal);
1168 if has_no_changes {
1169 skip!("No format changes");
1170 } else {
1171 #[cfg(feature = "std")]
1172 for change in changes {
1173 print!("{}", change.1);
1174 }
1175 }
1176}
1177pub fn print_values_as_table<T>(headers: Vec<&str>, rows: Vec<Vec<String>>, title: Option<T>)
1184where
1185 T: ToString,
1186{
1187 #[cfg(feature = "std")]
1188 {
1189 let mut table = Table::new();
1190 table
1191 .load_preset(UTF8_FULL)
1192 .apply_modifier(UTF8_ROUND_CORNERS)
1193 .set_content_arrangement(ContentArrangement::Dynamic)
1194 .set_header(headers);
1195 rows.into_iter().for_each(|row| {
1196 table.add_row(row);
1197 });
1198 match title {
1199 | Some(value) => println!("{} \n{table}", value.to_string().bold()),
1200 | None => println!("{table}"),
1201 }
1202 }
1203 #[cfg(not(feature = "std"))]
1204 {
1205 let _ = (headers, rows, title);
1206 }
1207}
1208pub fn regex_capture_lookup<S>(pattern: S, text: S, groups: Vec<S>) -> HashMap<S, String>
1225where
1226 S: Into<String> + AsRef<str> + Clone + core::cmp::Eq + core::hash::Hash,
1227{
1228 #[allow(clippy::unwrap_used)]
1229 let re = Regex::new(pattern.as_ref()).unwrap();
1230 let mut lookup = HashMap::new();
1231 if let Some(capture_matches) = re.captures_iter(text.as_ref()).last() {
1232 match capture_matches {
1233 | Ok(captures) => {
1234 captures.iter().skip(1).enumerate().for_each(|(index, data)| {
1235 if let Some(results) = data {
1236 if let Some(key) = groups.get(index) {
1237 let value = results.as_str().to_string();
1238 lookup.insert(key.clone(), value);
1239 }
1240 }
1241 });
1242 }
1243 | Err(_) => (),
1244 }
1245 };
1246 lookup
1247}
1248pub fn regex_join(patterns: &[String]) -> Option<String> {
1250 let groups = patterns
1251 .iter()
1252 .filter(|pattern| !pattern.is_empty())
1253 .map(|pattern| format!("(?:{pattern})"))
1254 .collect::<Vec<String>>();
1255 match groups.is_empty() {
1256 | true => None,
1257 | false => Some(groups.join("|")),
1258 }
1259}
1260pub fn regex_inverse(pattern: impl AsRef<str>) -> String {
1262 format!("^(?!.*(?:{})).*$", pattern.as_ref())
1263}
1264pub fn snake_case<S>(value: S) -> String
1273where
1274 S: Into<String>,
1275{
1276 value.into().to_case(Case::Snake)
1277}
1278pub fn suffix<T>(value: T) -> String
1289where
1290 T: PartialEq + From<u8>,
1291{
1292 (if value == T::from(1) { "" } else { "s" }).to_string()
1293}
1294pub fn text_diff_changes(old: &str, new: &str) -> Vec<(ChangeTag, String)> {
1311 TextDiff::from_lines(old, new)
1312 .iter_all_changes()
1313 .map(|line| {
1314 let tag = line.tag();
1315 let text = match tag {
1316 | Delete => format!("- {line}").red().to_string(),
1317 | Insert => format!("+ {line}").green().to_string(),
1318 | Equal => format!(" {line}").dimmed().to_string(),
1319 };
1320 (tag, text)
1321 })
1322 .collect::<Vec<_>>()
1323}
1324pub fn to_string(values: Vec<&str>) -> Vec<String> {
1326 values.iter().map(|s| s.to_string()).collect()
1327}
1328
1329#[cfg(test)]
1330mod tests;