Skip to main content

kagami/
normalize_version_words.rs

1use crate::normalize_version::{bound_every_version, interval_to_requirement};
2
3// Turns the English of hand-written ranges into operators. A `through` or `to` range becomes a
4// hyphen range, which `normalize_version` handles afterwards.
5//   `1.20.1 and earlier`                      ->  `<= 1.20.1`
6//   `7.2.10 and below, 8.0.5 and below`       ->  `<= 7.2.10, <= 8.0.5`
7//   `6.2 or newer`                            ->  `>= 6.2`
8//   `2.0.0+`                                  ->  `>= 2.0.0`
9//   `All versions prior to 1.7.14 and 1.8.7`  ->  `< 1.7.14, < 1.8.7`
10//   `2.2.0 <= V <= 2.10.1`                    ->  `>= 2.2.0, <= 2.10.1`
11//   `1.2.0 through 1.8.7`                     ->  `1.2.0 - 1.8.7`
12//   `from 3.5.0 to 4.3.10`                    ->  `3.5.0 - 4.3.10`
13//   `>= 1.0 and < 2.0`                        ->  `>= 1.0, < 2.0`
14//   `>= 1.0.0, < 1.2.6`                       ->  `>= 1.0.0, < 1.2.6`  (nothing to do)
15pub fn words_to_operators(range: &str) -> String {
16    let range = strip_prefix_ignore_case(range, "all versions").unwrap_or(range);
17    let range = strip_prefix_ignore_case(range, "from ").unwrap_or(range);
18
19    let upper_bound_prefixes = [
20        ("up to and including", "<="),
21        ("up to", "<="),
22        ("prior to", "<"),
23        ("earlier than", "<"),
24        ("before", "<"),
25        ("until", "<"),
26    ];
27    for (phrase, operator) in upper_bound_prefixes {
28        if let Some(versions) = strip_prefix_ignore_case(range, phrase) {
29            return bound_every_version(versions, operator, "");
30        }
31    }
32
33    let bound_suffixes = [
34        ("and newer", ">="),
35        ("or newer", ">="),
36        ("and later", ">="),
37        ("or later", ">="),
38        ("and above", ">="),
39        ("or above", ">="),
40        ("and earlier", "<="),
41        ("or earlier", "<="),
42        ("and before", "<="),
43        ("or before", "<="),
44        ("and below", "<="),
45        ("or below", "<="),
46        ("and lower", "<="),
47        ("or lower", "<="),
48        ("and older", "<="),
49        ("or older", "<="),
50        ("and prior", "<="),
51    ];
52    for (phrase, operator) in bound_suffixes {
53        if let Some(versions) = strip_suffix_ignore_case(range, phrase) {
54            return bound_every_version(versions, operator, phrase);
55        }
56    }
57
58    if let Some(version) = range.strip_suffix('+')
59        && !version.contains([' ', ','])
60    {
61        return format!(">= {version}");
62    }
63
64    if let Some(requirement) = interval_to_requirement(range) {
65        return requirement;
66    }
67
68    let range = replace_ignore_case(range, " up to and including ", " - ");
69    let range = replace_ignore_case(&range, " through and including ", " - ");
70    let range = replace_ignore_case(&range, " through ", " - ");
71    let range = replace_ignore_case(&range, " until ", " - ");
72    let range = replace_ignore_case(&range, " to ", " - ");
73    replace_ignore_case(&range, " and ", ", ")
74}
75
76// ----- text helpers -----
77
78// `str::strip_prefix` that ignores ASCII case and the whitespace after the prefix.
79//   (`All versions prior to 1.7.14`, `all versions`)  ->  Some(`prior to 1.7.14`)
80//   (`>= 1.0.0`, `all versions`)                      ->  None
81fn strip_prefix_ignore_case<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
82    let candidate = text.get(..prefix.len())?;
83    if candidate.eq_ignore_ascii_case(prefix) {
84        Some(text[prefix.len()..].trim_start())
85    } else {
86        None
87    }
88}
89
90// `str::strip_suffix` that ignores ASCII case and the whitespace before the suffix.
91//   (`1.20.1 and Earlier`, `and earlier`)  ->  Some(`1.20.1`)
92//   (`1.20.1`, `and earlier`)              ->  None
93fn strip_suffix_ignore_case<'a>(text: &'a str, suffix: &str) -> Option<&'a str> {
94    let start = text.len().checked_sub(suffix.len())?;
95    let candidate = text.get(start..)?;
96    if candidate.eq_ignore_ascii_case(suffix) {
97        Some(text[..start].trim_end())
98    } else {
99        None
100    }
101}
102
103// `str::replace` that ignores ASCII case when searching.
104//   (`1.2.0 Through 1.8.7`, ` through `, ` - `)  ->  `1.2.0 - 1.8.7`
105//   (`a and b AND c`, ` and `, `, `)             ->  `a, b, c`
106pub fn replace_ignore_case(text: &str, from: &str, to: &str) -> String {
107    let lowercase_text = text.to_ascii_lowercase();
108    let lowercase_from = from.to_ascii_lowercase();
109    let mut result = String::new();
110    let mut rest_starts_at = 0;
111    for (index, _) in lowercase_text.match_indices(&lowercase_from) {
112        result.push_str(&text[rest_starts_at..index]);
113        result.push_str(to);
114        rest_starts_at = index + from.len();
115    }
116    result.push_str(&text[rest_starts_at..]);
117    result
118}
119
120// Replaces the characters people type instead of the semver ones. Markdown escapes, backticks and
121// a trailing period are dropped.
122//   `≤ 5.59.0`                 ->  `<= 5.59.0`
123//   `=< 0.17.1`                ->  `<= 0.17.1`
124//   `=>9.0.0, <= 9.0.2`        ->  `>=9.0.0, <= 9.0.2`
125//   `>= 7.0.0-beta < =7.2.3`   ->  `>= 7.0.0-beta <=7.2.3`
126//   `== 2.8.0`                 ->  `= 2.8.0`
127//   `>= 6.19.4 && < 6.21.1`    ->  `>= 6.19.4 , < 6.21.1`
128//   `<=7.1.0; <=6.9.11`        ->  `<=7.1.0, <=6.9.11`
129//   `v0.10.0–v1.3.1`           ->  `v0.10.0 - v1.3.1`
130//   `\>= 1.11.0, \<= 1.16.2`   ->  `>= 1.11.0, <= 1.16.2`
131//   `<= v1.18.2` in backticks  ->  `<= v1.18.2`
132//   `from 3.5.0 to 4.3.10.`    ->  `from 3.5.0 to 4.3.10`
133pub fn replace_unusual_characters(range: &str) -> String {
134    range
135        .trim()
136        .trim_end_matches('.')
137        .replace('≤', "<=")
138        .replace('≥', ">=")
139        .replace("=<", "<=")
140        .replace("=>", ">=")
141        .replace("< =", "<=")
142        .replace("> =", ">=")
143        .replace("==", "=")
144        .replace("&&", ",")
145        .replace([';', ','], ",")
146        .replace(['–', '—'], " - ")
147        .replace(['\\', '`'], "")
148}