disarm 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/disarm/Cargo.toml +1 -1
- data/ext/disarm/src/lib.rs +110 -18
- data/lib/disarm/version.rb +1 -1
- data/lib/disarm.rb +97 -13
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b0ec44307d78ca53394c2b8aa21522f3e2e9a486c83bd536abdb8afd2b0ea7e
|
|
4
|
+
data.tar.gz: e4e5edead5532bc809a8b2c3e4389b02177246d0d5c783bf5b24b641e32bb576
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65a1b31c791a89e407a697af6d89b8f65e772803705e0cb12d3a8b458d4bae093c1dfc558dfa9749db10857dcafe521b221d28979e22f1a37d66326cd2d86fe6
|
|
7
|
+
data.tar.gz: a26f6cdfca9f10d678d5b8b56d243be22ac91a1d582d92a5fb6aee7d0f28b8abbe154da24e2bad1e924c1c5121f6c26baf937cba8f5caebc06cc954810d13458
|
data/ext/disarm/Cargo.toml
CHANGED
|
@@ -30,7 +30,7 @@ crate-type = ["cdylib"]
|
|
|
30
30
|
# dep — not a path — so the gem is self-contained and the rb-sys-dock cross-gem
|
|
31
31
|
# build (which mounts only this gem dir) can fetch it. Imported as `disarm_core`
|
|
32
32
|
# because the package name `disarm` would otherwise clash with this crate.
|
|
33
|
-
disarm_core = { package = "disarm", version = "0.
|
|
33
|
+
disarm_core = { package = "disarm", version = "0.16", default-features = false }
|
|
34
34
|
# magnus: ergonomic, safe Ruby<->Rust bindings over rb-sys. 0.8 supports Ruby >= 3.0
|
|
35
35
|
# (the gem's required_ruby_version); rb-sys handles the platform glue.
|
|
36
36
|
magnus = "0.8"
|
data/ext/disarm/src/lib.rs
CHANGED
|
@@ -215,6 +215,16 @@ fn normalize_confusables(
|
|
|
215
215
|
Ok(api::normalize_confusables_with(&text, target, digit_policy).into_owned())
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/// The `digit_policy` token every key builder takes (#896): parsed once, at the boundary.
|
|
219
|
+
fn parse_policy(digit_policy: &str) -> Result<api::DigitPolicy, Error> {
|
|
220
|
+
digit_policy.parse().map_err(|e| map_err(&e))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// `Disarm._is_canonical?(text, preset)` — already its own canonical form (#730).
|
|
224
|
+
fn is_canonical(text: Wtf8Text, preset: String) -> Result<bool, Error> {
|
|
225
|
+
api::is_canonical(&text, &preset).map_err(|e| map_err(&e))
|
|
226
|
+
}
|
|
227
|
+
|
|
218
228
|
/// `Disarm._confusable?(text, "latin" | "cyrillic" | "arabic" | "hebrew")`.
|
|
219
229
|
fn is_confusable(text: Wtf8Text, target: String) -> Result<bool, Error> {
|
|
220
230
|
let target: api::TargetScript = target.parse().map_err(|e| map_err(&e))?;
|
|
@@ -321,6 +331,11 @@ fn slugify(
|
|
|
321
331
|
api::slugify(&text, &config)
|
|
322
332
|
}
|
|
323
333
|
|
|
334
|
+
/// `Disarm._replace_emoji(text, replacement)` — every emoji replaced, verbatim (#972).
|
|
335
|
+
fn replace_emoji(text: Wtf8Text, replacement: String) -> String {
|
|
336
|
+
api::replace_emoji(&text, &replacement)
|
|
337
|
+
}
|
|
338
|
+
|
|
324
339
|
/// `Disarm._demojize(text, strip_modifiers)`.
|
|
325
340
|
fn demojize(text: Wtf8Text, strip_modifiers: bool) -> String {
|
|
326
341
|
api::demojize(&text, strip_modifiers)
|
|
@@ -340,8 +355,8 @@ fn demojize(text: Wtf8Text, strip_modifiers: bool) -> String {
|
|
|
340
355
|
///
|
|
341
356
|
/// Unlike `canonicalize` it does NOT fold confusables, so non-Latin text keeps its script
|
|
342
357
|
/// — the point of the preset.
|
|
343
|
-
fn canonicalize_strict(text: Wtf8Text) -> Result<String, Error> {
|
|
344
|
-
api::
|
|
358
|
+
fn canonicalize_strict(text: Wtf8Text, digit_policy: String) -> Result<String, Error> {
|
|
359
|
+
api::canonicalize_strict_with(&text, parse_policy(&digit_policy)?)
|
|
345
360
|
.map(std::borrow::Cow::into_owned)
|
|
346
361
|
.map_err(|e| map_err(&e))
|
|
347
362
|
}
|
|
@@ -350,42 +365,72 @@ fn strip_format(text: Wtf8Text) -> String {
|
|
|
350
365
|
api::strip_format(&text).into_owned()
|
|
351
366
|
}
|
|
352
367
|
|
|
353
|
-
fn strip_obfuscation(text: Wtf8Text) -> Result<String, Error> {
|
|
354
|
-
api::
|
|
368
|
+
fn strip_obfuscation(text: Wtf8Text, digit_policy: String) -> Result<String, Error> {
|
|
369
|
+
api::strip_obfuscation_with(&text, parse_policy(&digit_policy)?)
|
|
355
370
|
.map(std::borrow::Cow::into_owned)
|
|
356
371
|
.map_err(|e| map_err(&e))
|
|
357
372
|
}
|
|
358
373
|
|
|
359
|
-
fn canonicalize(text: Wtf8Text) -> Result<String, Error> {
|
|
360
|
-
api::
|
|
374
|
+
fn canonicalize(text: Wtf8Text, digit_policy: String) -> Result<String, Error> {
|
|
375
|
+
api::canonicalize_with(&text, parse_policy(&digit_policy)?)
|
|
361
376
|
.map(std::borrow::Cow::into_owned)
|
|
362
377
|
.map_err(|e| map_err(&e))
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
/// `Disarm._search_key(text, lang)` — case/accent/script-insensitive lookup key.
|
|
366
381
|
/// `lang` is `nil` (no profile) or a code like `"ru"`. Fails on an unknown `lang`.
|
|
367
|
-
fn search_key(text: Wtf8Text, lang: Option<String
|
|
368
|
-
api::
|
|
382
|
+
fn search_key(text: Wtf8Text, lang: Option<String>, digit_policy: String) -> Result<String, Error> {
|
|
383
|
+
api::search_key_with(&text, lang.as_deref(), parse_policy(&digit_policy)?)
|
|
369
384
|
.map(std::borrow::Cow::into_owned)
|
|
370
385
|
.map_err(|e| map_err(&e))
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
/// `Disarm._sort_key(text, lang)` — collation sort key (preserves base accented
|
|
374
389
|
/// characters for correct ordering). Fails on an unknown `lang`.
|
|
375
|
-
fn sort_key(text: Wtf8Text, lang: Option<String
|
|
376
|
-
api::
|
|
390
|
+
fn sort_key(text: Wtf8Text, lang: Option<String>, digit_policy: String) -> Result<String, Error> {
|
|
391
|
+
api::sort_key_with(&text, lang.as_deref(), parse_policy(&digit_policy)?)
|
|
377
392
|
.map(std::borrow::Cow::into_owned)
|
|
378
393
|
.map_err(|e| map_err(&e))
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
/// `Disarm._catalog_key(text, lang, strict_iso9)` — catalog deduplication key.
|
|
382
397
|
/// `strict_iso9` selects the ISO 9:1995 Cyrillic scheme. Fails on an unknown `lang`.
|
|
383
|
-
fn catalog_key(
|
|
384
|
-
|
|
398
|
+
fn catalog_key(
|
|
399
|
+
text: Wtf8Text,
|
|
400
|
+
lang: Option<String>,
|
|
401
|
+
strict_iso9: bool,
|
|
402
|
+
digit_policy: String,
|
|
403
|
+
) -> Result<String, Error> {
|
|
404
|
+
api::catalog_key_with(&text, lang.as_deref(), strict_iso9, parse_policy(&digit_policy)?)
|
|
405
|
+
.map(std::borrow::Cow::into_owned)
|
|
406
|
+
.map_err(|e| map_err(&e))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// `Disarm._skeleton_key(text, digit_policy)` — the TR39 identifier skeleton plus the two
|
|
410
|
+
/// prototype classes disarm's table keeps apart (#650). A spoof key: never for display.
|
|
411
|
+
fn skeleton_key(text: Wtf8Text, digit_policy: String) -> Result<String, Error> {
|
|
412
|
+
api::skeleton_key(&text, parse_policy(&digit_policy)?)
|
|
385
413
|
.map(std::borrow::Cow::into_owned)
|
|
386
414
|
.map_err(|e| map_err(&e))
|
|
387
415
|
}
|
|
388
416
|
|
|
417
|
+
/// `Disarm._edit_distance(a, b)` — Levenshtein distance in characters (#894).
|
|
418
|
+
fn edit_distance(a: Wtf8Text, b: Wtf8Text) -> usize {
|
|
419
|
+
api::edit_distance(&a, &b)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// `Disarm._nearest_match(value, candidates, max_distance)` — the closest candidate and
|
|
423
|
+
/// its distance as a `(value, distance)` tuple, or `nil` beyond `max_distance` (#894).
|
|
424
|
+
/// The Ruby layer maps the tuple to a `{ value:, distance: }` hash.
|
|
425
|
+
fn nearest_match(
|
|
426
|
+
value: Wtf8Text,
|
|
427
|
+
candidates: Vec<String>,
|
|
428
|
+
max_distance: usize,
|
|
429
|
+
) -> Option<(String, usize)> {
|
|
430
|
+
api::nearest_match(&value, candidates.iter().map(String::as_str), max_distance)
|
|
431
|
+
.map(|m| (m.value, m.distance))
|
|
432
|
+
}
|
|
433
|
+
|
|
389
434
|
/// `Disarm._suspicious_hostname?(host)` — flags mixed-script / confusable IDN
|
|
390
435
|
/// spoofs. A false result asserts nothing was *found*, not that the host is safe.
|
|
391
436
|
fn suspicious_hostname(host: Wtf8Text) -> bool {
|
|
@@ -670,6 +715,22 @@ fn lang_info(code: String) -> Result<RHash, Error> {
|
|
|
670
715
|
Ok(hash)
|
|
671
716
|
}
|
|
672
717
|
|
|
718
|
+
/// `Disarm._confusable_coverage(script)` — TR39 sources whose prototype is in `script`
|
|
719
|
+
/// and how many the bundled tables fold (#963), as a Ruby Hash with symbol keys
|
|
720
|
+
/// (`{ script:, sources:, folded: }`). The denominator `unmapped_confusables` does not
|
|
721
|
+
/// have. Fails (ArgumentError → Disarm::InvalidArgument) on an unknown script.
|
|
722
|
+
fn confusable_coverage(script: String) -> Result<RHash, Error> {
|
|
723
|
+
let row = api::confusable_coverage(&script).map_err(|e| map_err(&e))?;
|
|
724
|
+
// GVL invariant: a Ruby method callback always holds the GVL. Justified.
|
|
725
|
+
#[allow(clippy::expect_used)]
|
|
726
|
+
let ruby = Ruby::get().expect("a Ruby method callback always holds the GVL");
|
|
727
|
+
let hash = ruby.hash_new();
|
|
728
|
+
hash.aset(ruby.to_symbol("script"), row.script)?;
|
|
729
|
+
hash.aset(ruby.to_symbol("sources"), row.sources)?;
|
|
730
|
+
hash.aset(ruby.to_symbol("folded"), row.folded)?;
|
|
731
|
+
Ok(hash)
|
|
732
|
+
}
|
|
733
|
+
|
|
673
734
|
/// `Disarm._script_info(name)` — curated metadata for one script as a Ruby Hash
|
|
674
735
|
/// with symbol keys (`{ name:, default_lang:, example:, context_aware: }`);
|
|
675
736
|
/// `default_lang` is `nil` when the core has none. Fails (ArgumentError →
|
|
@@ -823,6 +884,26 @@ fn pipeline_process(rb_self: &Pipeline, text: Wtf8Text) -> Result<String, Error>
|
|
|
823
884
|
rb_self.inner.process(&text).map_err(|e| map_err(&e))
|
|
824
885
|
}
|
|
825
886
|
|
|
887
|
+
/// `Disarm::Pipeline#purpose` — what the named profile is for, or `nil` (#860).
|
|
888
|
+
fn pipeline_purpose(rb_self: &Pipeline) -> Option<String> {
|
|
889
|
+
rb_self.inner.purpose().map(str::to_owned)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/// `Disarm::Pipeline#_with_digit_policy(policy)` — a copy of this pipeline whose confusable
|
|
893
|
+
/// passes fold under `policy` (#646). Fails when the profile has no confusables step and the
|
|
894
|
+
/// policy is not the default: a setting that would never run is refused rather than kept.
|
|
895
|
+
/// The Ruby layer wraps it as `#with_digit_policy` so the error arrives as
|
|
896
|
+
/// `Disarm::InvalidArgument`, the way every module-level call does.
|
|
897
|
+
fn pipeline_with_digit_policy(rb_self: &Pipeline, digit_policy: String) -> Result<Pipeline, Error> {
|
|
898
|
+
Ok(Pipeline {
|
|
899
|
+
inner: rb_self
|
|
900
|
+
.inner
|
|
901
|
+
.clone()
|
|
902
|
+
.with_digit_policy(parse_policy(&digit_policy)?)
|
|
903
|
+
.map_err(|e| map_err(&e))?,
|
|
904
|
+
})
|
|
905
|
+
}
|
|
906
|
+
|
|
826
907
|
/// `Disarm._get_pipeline(profile)` — build a reusable `Disarm::Pipeline` for a
|
|
827
908
|
/// named policy profile. Fails (Disarm::InvalidArgument) on an unknown profile.
|
|
828
909
|
fn get_pipeline(profile: String) -> Result<Pipeline, Error> {
|
|
@@ -847,15 +928,20 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
847
928
|
module.define_singleton_method("_confusable?", function!(is_confusable, 2))?;
|
|
848
929
|
module.define_singleton_method("_slugify", function!(slugify, 13))?;
|
|
849
930
|
module.define_singleton_method("_demojize", function!(demojize, 2))?;
|
|
850
|
-
module.define_singleton_method("
|
|
931
|
+
module.define_singleton_method("_replace_emoji", function!(replace_emoji, 2))?;
|
|
932
|
+
module.define_singleton_method("_canonicalize_strict", function!(canonicalize_strict, 2))?;
|
|
851
933
|
module.define_singleton_method("_strip_format", function!(strip_format, 1))?;
|
|
852
|
-
module.define_singleton_method("_strip_obfuscation", function!(strip_obfuscation,
|
|
853
|
-
module.define_singleton_method("_canonicalize", function!(canonicalize,
|
|
934
|
+
module.define_singleton_method("_strip_obfuscation", function!(strip_obfuscation, 2))?;
|
|
935
|
+
module.define_singleton_method("_canonicalize", function!(canonicalize, 2))?;
|
|
936
|
+
module.define_singleton_method("_is_canonical?", function!(is_canonical, 2))?;
|
|
854
937
|
|
|
855
938
|
// Key-derivation presets (#404 Group A parity backfill).
|
|
856
|
-
module.define_singleton_method("_search_key", function!(search_key,
|
|
857
|
-
module.define_singleton_method("_sort_key", function!(sort_key,
|
|
858
|
-
module.define_singleton_method("_catalog_key", function!(catalog_key,
|
|
939
|
+
module.define_singleton_method("_search_key", function!(search_key, 3))?;
|
|
940
|
+
module.define_singleton_method("_sort_key", function!(sort_key, 3))?;
|
|
941
|
+
module.define_singleton_method("_catalog_key", function!(catalog_key, 4))?;
|
|
942
|
+
module.define_singleton_method("_skeleton_key", function!(skeleton_key, 2))?;
|
|
943
|
+
module.define_singleton_method("_edit_distance", function!(edit_distance, 2))?;
|
|
944
|
+
module.define_singleton_method("_nearest_match", function!(nearest_match, 3))?;
|
|
859
945
|
|
|
860
946
|
// No options / no symbols, but still wrapped by the Ruby layer so a wrong-type
|
|
861
947
|
// argument surfaces as Disarm::InvalidArgument rather than a raw TypeError —
|
|
@@ -922,6 +1008,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
922
1008
|
// Metadata introspection (#404 phase 3 parity backfill).
|
|
923
1009
|
module.define_singleton_method("_lang_info", function!(lang_info, 1))?;
|
|
924
1010
|
module.define_singleton_method("_script_info", function!(script_info, 1))?;
|
|
1011
|
+
module.define_singleton_method(
|
|
1012
|
+
"_confusable_coverage",
|
|
1013
|
+
function!(confusable_coverage, 1),
|
|
1014
|
+
)?;
|
|
925
1015
|
module.define_singleton_method(
|
|
926
1016
|
"_confusables_version",
|
|
927
1017
|
function!(confusables_version, 0),
|
|
@@ -954,6 +1044,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
954
1044
|
// instance method on the wrapped handle. Mirrors `Disarm::Lexicon` above.
|
|
955
1045
|
let pipeline = module.define_class("Pipeline", ruby.class_object())?;
|
|
956
1046
|
pipeline.define_method("process", method!(pipeline_process, 1))?;
|
|
1047
|
+
pipeline.define_method("purpose", method!(pipeline_purpose, 0))?;
|
|
1048
|
+
pipeline.define_method("_with_digit_policy", method!(pipeline_with_digit_policy, 1))?;
|
|
957
1049
|
module.define_singleton_method("_get_pipeline", function!(get_pipeline, 1))?;
|
|
958
1050
|
Ok(())
|
|
959
1051
|
}
|
data/lib/disarm/version.rb
CHANGED
data/lib/disarm.rb
CHANGED
|
@@ -116,11 +116,26 @@ module Disarm
|
|
|
116
116
|
translate_errors { _demojize(text, strip_modifiers) }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Replace every emoji with +replacement+, verbatim.
|
|
120
|
+
#
|
|
121
|
+
# The counterpart to +demojize+, and a different question of a different table.
|
|
122
|
+
# +demojize+ asks what CLDR calls a character, so its domain is the name table, which
|
|
123
|
+
# is wider than the emoji: <tt>demojize("x\u2122y")</tt> is "x trade mark y". This
|
|
124
|
+
# asks whether the UCD calls it an emoji — Emoji_Presentation=Yes, an Emoji=Yes base
|
|
125
|
+
# carrying U+FE0F, and the ZWJ, modifier, keycap and flag sequences on those. Nothing
|
|
126
|
+
# else moves.
|
|
127
|
+
#
|
|
128
|
+
# +replacement+ is inserted exactly as given: "" closes an intra-word split and " "
|
|
129
|
+
# keeps two words apart, and no rule serves both.
|
|
130
|
+
def replace_emoji(text, replacement = "")
|
|
131
|
+
translate_errors { _replace_emoji(text, replacement) }
|
|
132
|
+
end
|
|
133
|
+
|
|
119
134
|
# Canonicalize, but raise rather than silently normalize a structural difference
|
|
120
135
|
# away — the half of the pair that lets a caller reject input instead of comparing
|
|
121
136
|
# a value the sender never wrote.
|
|
122
|
-
def canonicalize_strict(text)
|
|
123
|
-
translate_errors { _canonicalize_strict(text) }
|
|
137
|
+
def canonicalize_strict(text, digit_policy: :numeric)
|
|
138
|
+
translate_errors { _canonicalize_strict(text, digit_policy.to_s) }
|
|
124
139
|
end
|
|
125
140
|
|
|
126
141
|
# Strip the non-interchange and invisible classes while KEEPING the script.
|
|
@@ -136,8 +151,8 @@ module Disarm
|
|
|
136
151
|
|
|
137
152
|
# Remove obfuscation (zero-width, bidi, combining-mark abuse) while keeping
|
|
138
153
|
# legible content.
|
|
139
|
-
def strip_obfuscation(text)
|
|
140
|
-
translate_errors { _strip_obfuscation(text) }
|
|
154
|
+
def strip_obfuscation(text, digit_policy: :numeric)
|
|
155
|
+
translate_errors { _strip_obfuscation(text, digit_policy.to_s) }
|
|
141
156
|
end
|
|
142
157
|
|
|
143
158
|
# Canonicalize text for security-sensitive comparison: strip obfuscation,
|
|
@@ -151,8 +166,8 @@ module Disarm
|
|
|
151
166
|
# delimiter can leave here carrying one. #inspect_anomalies reports it as :confusable
|
|
152
167
|
# WHEN the word also carries an ASCII letter, which is the gate that keeps ordinary
|
|
153
168
|
# non-Latin text from firing; a delimiter-only string is not reported.
|
|
154
|
-
def canonicalize(text)
|
|
155
|
-
translate_errors { _canonicalize(text) }
|
|
169
|
+
def canonicalize(text, digit_policy: :numeric)
|
|
170
|
+
translate_errors { _canonicalize(text, digit_policy.to_s) }
|
|
156
171
|
end
|
|
157
172
|
|
|
158
173
|
# @deprecated Renamed to {#canonicalize} in 0.11 (the +_clean+ name
|
|
@@ -165,22 +180,60 @@ module Disarm
|
|
|
165
180
|
# Case/accent/script-insensitive search lookup key. `lang:` applies a
|
|
166
181
|
# language profile for transliteration (e.g. "ru", "uk"); nil means none.
|
|
167
182
|
# Raises Disarm::InvalidArgument on an unknown lang.
|
|
168
|
-
def search_key(text, lang: nil)
|
|
169
|
-
translate_errors { _search_key(text, lang&.to_s) }
|
|
183
|
+
def search_key(text, lang: nil, digit_policy: :numeric)
|
|
184
|
+
translate_errors { _search_key(text, lang&.to_s, digit_policy.to_s) }
|
|
170
185
|
end
|
|
171
186
|
|
|
172
187
|
# Collation sort key (like #search_key, but keeps base accented characters
|
|
173
188
|
# for correct ordering). `lang:` applies a language profile; nil means none.
|
|
174
189
|
# Raises Disarm::InvalidArgument on an unknown lang.
|
|
175
|
-
def sort_key(text, lang: nil)
|
|
176
|
-
translate_errors { _sort_key(text, lang&.to_s) }
|
|
190
|
+
def sort_key(text, lang: nil, digit_policy: :numeric)
|
|
191
|
+
translate_errors { _sort_key(text, lang&.to_s, digit_policy.to_s) }
|
|
177
192
|
end
|
|
178
193
|
|
|
179
194
|
# Library catalog deduplication key (search_key plus confusable folding).
|
|
180
195
|
# `lang:` applies a language profile; `strict_iso9:` selects the ISO 9:1995
|
|
181
196
|
# Cyrillic scheme. Raises Disarm::InvalidArgument on an unknown lang.
|
|
182
|
-
def catalog_key(text, lang: nil, strict_iso9: false)
|
|
183
|
-
translate_errors { _catalog_key(text, lang&.to_s, strict_iso9) }
|
|
197
|
+
def catalog_key(text, lang: nil, strict_iso9: false, digit_policy: :numeric)
|
|
198
|
+
translate_errors { _catalog_key(text, lang&.to_s, strict_iso9, digit_policy.to_s) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The TR39 identifier skeleton plus the two prototype classes disarm keeps apart
|
|
202
|
+
# (#650). A spoof key: its only job is to make confusable identifiers collide, and its
|
|
203
|
+
# output is never for display. `digit_policy:` is `:numeric` (the letter half only),
|
|
204
|
+
# `:tr39` (adds `1 ≡ l` and `0 ≡ O`) or `:preserve` (a non-Latin numeral keeps its
|
|
205
|
+
# script).
|
|
206
|
+
def skeleton_key(text, digit_policy: :numeric)
|
|
207
|
+
translate_errors { _skeleton_key(text, digit_policy.to_s) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Levenshtein edit distance between `a` and `b`, in characters (#894). The one class
|
|
211
|
+
# of registry spoofing the confusable tables deliberately do not model — `paypa1`,
|
|
212
|
+
# `adm1n`. Canonicalize both sides first when composed and decomposed spellings
|
|
213
|
+
# should compare equal.
|
|
214
|
+
def edit_distance(left, right)
|
|
215
|
+
translate_errors { _edit_distance(left, right) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The candidate closest to `value`, as `{ value:, distance: }`, or nil beyond
|
|
219
|
+
# `max_distance:` (#894). Reports; it does not decide. An exact match is reported with
|
|
220
|
+
# distance 0, and ties go to the first candidate at the lowest distance.
|
|
221
|
+
def nearest_match(value, candidates, max_distance: 1)
|
|
222
|
+
hit = translate_errors { _nearest_match(value, candidates.map(&:to_s), max_distance) }
|
|
223
|
+
hit && { value: hit[0], distance: hit[1] }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Whether `text` is already its own canonical form under `preset:` (#730).
|
|
227
|
+
#
|
|
228
|
+
# The verification-path counterpart to the presets: text in, normalized text out is
|
|
229
|
+
# the generation path, and this is the question a caller asks about bytes that arrive
|
|
230
|
+
# already bound. `has_anomalies?` is not this predicate — 142,760 assigned code points are
|
|
231
|
+
# reported clean by the detector and are not their own canonical form.
|
|
232
|
+
#
|
|
233
|
+
# `preset:` is any name in the preset registry or any profile. Raises
|
|
234
|
+
# Disarm::InvalidArgument on an unknown one.
|
|
235
|
+
def canonical?(text, preset: "canonicalize")
|
|
236
|
+
translate_errors { _is_canonical?(text, preset.to_s) }
|
|
184
237
|
end
|
|
185
238
|
|
|
186
239
|
# Strip diacritics ("café" → "cafe").
|
|
@@ -486,6 +539,22 @@ module Disarm
|
|
|
486
539
|
translate_errors { _script_info(name.to_s) }
|
|
487
540
|
end
|
|
488
541
|
|
|
542
|
+
# TR39 confusable sources whose prototype is in +script+, and how many of those
|
|
543
|
+
# disarm's bundled tables fold. Returns a Hash with +:script+, +:sources+ and
|
|
544
|
+
# +:folded+ keys.
|
|
545
|
+
#
|
|
546
|
+
# The denominator +unmapped_confusables+ does not have: that measures one bundled
|
|
547
|
+
# table against the whole 6,565-source population, so a script disarm ships no table
|
|
548
|
+
# for reports a number determined by that absence rather than by its coverage.
|
|
549
|
+
#
|
|
550
|
+
# +:folded+ counts sources any bundled table reaches, not sources folded *toward*
|
|
551
|
+
# this script — Greek is 71 of 159, because the Latin table folds Greek letters that
|
|
552
|
+
# look Latin. A script disarm knows that TR39 never uses as a prototype returns 0 of
|
|
553
|
+
# 0. Raises Disarm::InvalidArgument on an unknown script.
|
|
554
|
+
def confusable_coverage(script)
|
|
555
|
+
translate_errors { _confusable_coverage(script.to_s) }
|
|
556
|
+
end
|
|
557
|
+
|
|
489
558
|
# The Unicode `confusables.txt` release the bundled confusable tables were folded
|
|
490
559
|
# from, e.g. "17.0.0". Not a Unicode version for the library as a whole — the
|
|
491
560
|
# case-folding and width tables track different releases (see docs/provenance.md).
|
|
@@ -574,7 +643,9 @@ module Disarm
|
|
|
574
643
|
# pipe.process("Café") # => "cafe"
|
|
575
644
|
# pipe.process("Köln") # reuse the same handle
|
|
576
645
|
#
|
|
577
|
-
# Disarm::Pipeline#process is the Rust-defined instance method on the handle
|
|
646
|
+
# Disarm::Pipeline#process is the Rust-defined instance method on the handle, and
|
|
647
|
+
# Disarm::Pipeline#with_digit_policy(policy) returns a copy whose confusable passes
|
|
648
|
+
# fold under `policy` (#646); a profile with no confusables step refuses one.
|
|
578
649
|
def get_pipeline(profile)
|
|
579
650
|
translate_errors { _get_pipeline(profile.to_s) }
|
|
580
651
|
end
|
|
@@ -615,4 +686,17 @@ module Disarm
|
|
|
615
686
|
raise Error, e.message, e.backtrace
|
|
616
687
|
end
|
|
617
688
|
end
|
|
689
|
+
|
|
690
|
+
# The reusable handle `Disarm.get_pipeline` returns. `#process` is Rust-defined; this
|
|
691
|
+
# reopens the class for the one method that can fail, so its error arrives as
|
|
692
|
+
# `Disarm::InvalidArgument` the way every module-level call's does.
|
|
693
|
+
class Pipeline
|
|
694
|
+
# A copy of this pipeline whose confusable passes fold under `policy` (#646):
|
|
695
|
+
# `:numeric`, `:tr39` or `:preserve`. Raises Disarm::InvalidArgument when the profile
|
|
696
|
+
# has no confusables step and the policy is not the default — a setting that would
|
|
697
|
+
# never run is refused rather than kept.
|
|
698
|
+
def with_digit_policy(policy)
|
|
699
|
+
Disarm.send(:translate_errors) { _with_digit_policy(policy.to_s) }
|
|
700
|
+
end
|
|
701
|
+
end
|
|
618
702
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: disarm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Richard Quinn
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|