disarm 0.15.0-aarch64-linux → 0.17.0-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: affa6ae8c002469f386f0cc010093af423746f17b2b23f33cb8e5baeef7d460a
4
- data.tar.gz: 8c04e95811cc9608f473a44c85815c2e37332c5c71d489963a236b83e1a405e4
3
+ metadata.gz: 0f8710ef6fd0f70a94b8cef89b07ba9e3ab600baf666dcfeb78eafb931c1a846
4
+ data.tar.gz: 20388fac65395ab997fc2ab9e8bdc00685aa12202fea170886be626fdea96e27
5
5
  SHA512:
6
- metadata.gz: 2102a383ea426986f32d6bbc2c1aa52970226e9e7ed135b5d6f0bd34102c060b5076dd12071985e695ccf3638499930ee4d2ad8b414f0fbc5d4871de38d82bbd
7
- data.tar.gz: 70976043387c5e0d0cf23c5ced76e0bb58af9cae186f03937963ff08f98e225f90443e4077f93d4cb56e7a27d29812ca37bea3bc4a21fb9ac2f97ca2ec849cab
6
+ metadata.gz: 996597cb752b703dd8e83f7fe476fc3d0622b93911335eca50674a6ea2afffcdab4d048a1f97d48b83f7ee269f7b69a20185ff447cf78d44b109b5667744f462
7
+ data.tar.gz: f8b0788a1900b1cee4840ac387d6917aff961ca17e75f2d9b28902a000ea1fcfc97eb3c7a7eaa97b87b769dfe27e2bcb42481b876d2f5da115c6457415b45ccc
data/README.md CHANGED
@@ -19,10 +19,10 @@ gem "disarm"
19
19
  gem install disarm
20
20
  ```
21
21
 
22
- Requires Ruby >= 3.1. Precompiled platform gems ship for Ruby 3.1 through 4.0
22
+ Requires Ruby >= 3.3. Precompiled platform gems ship for Ruby 3.3 through 4.0
23
23
  (Linux x86_64/aarch64, macOS x86_64/arm64, Windows). On a supported Ruby with no
24
24
  matching platform gem, the source gem installs and compiles locally, which needs a
25
- Rust toolchain. Below 3.1 the gem does not install at all.
25
+ Rust toolchain. Below 3.3 the gem does not install at all.
26
26
 
27
27
  ## Usage
28
28
 
Binary file
Binary file
Binary file
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Disarm
4
4
  # Kept in lockstep with the Rust crate / Python package version.
5
- VERSION = "0.15.0"
5
+ VERSION = "0.17.0"
6
6
  end
data/lib/disarm.rb CHANGED
@@ -24,6 +24,13 @@ end
24
24
  # keyword arguments with the core's defaults, symbol tokens (:latin, :default, …),
25
25
  # a single transliterate(text, scheme:) entrypoint, and a Disarm::Error hierarchy.
26
26
  # Each method is still a thin wrapper over the pure-Rust `disarm` core.
27
+ #
28
+ # Text arguments are read by the encoding the String declares. UTF-8 and US-ASCII are
29
+ # read as they are, and ASCII-8BIT (BINARY) is read as UTF-8, the way the C ABI reads
30
+ # its bytes; any other encoding (ISO-8859-1, Windows-1251, UTF-16LE, ...) is transcoded
31
+ # to UTF-8 first. A malformed or unmappable sequence becomes one U+FFFD, and so does a
32
+ # lone surrogate, the contract every binding shares (#469). An encoding Ruby cannot
33
+ # convert from at all raises Disarm::InvalidArgument.
27
34
  module Disarm
28
35
  # Base class for every error disarm raises, so consumers can `rescue
29
36
  # Disarm::Error`. The native shim raises Ruby's built-in ArgumentError /
@@ -38,7 +45,8 @@ module Disarm
38
45
  # Transliterate Unicode text to ASCII. `scheme:` selects the standard:
39
46
  # :default (the general-purpose scheme), :strict_iso9, or :gost7034. `lang:`
40
47
  # applies a language profile on top of the scheme (e.g. "uk" → Київ → "Kyiv",
41
- # "de" → ü → "ue"); nil means no profile. Both accept a String or Symbol.
48
+ # "de" → ü → "ue"); nil means no profile. Both accept a String or Symbol. Raises
49
+ # Disarm::InvalidArgument on an unknown lang, as every binding does.
42
50
  def transliterate(text, scheme: :default, lang: nil)
43
51
  scheme = scheme.to_s
44
52
  lang = lang&.to_s
@@ -53,7 +61,8 @@ module Disarm
53
61
  end
54
62
  end
55
63
 
56
- # Fold cross-script confusables toward `target:` (:latin or :cyrillic).
64
+ # Fold cross-script confusables toward `target:` (:latin, :cyrillic, :arabic or
65
+ # :hebrew).
57
66
  #
58
67
  # `digit_policy:` selects how non-Latin DIGITS fold (#561).
59
68
  #
@@ -61,9 +70,9 @@ module Disarm
61
70
  # right for prose, where a Devanagari zero really is a zero.
62
71
  #
63
72
  # `:tr39` uses upstream's targets, which send most of them to a Latin letter
64
- # (`०` → `o`; three of the 45 rows fold to `.` or to the two characters `rn`
73
+ # (`०` → `o`; three of the 47 rows fold to `.` or to the two characters `rn`
65
74
  # instead). That is what an identifier *skeleton* wants, since its only job is to
66
- # make two confusable identifiers collide. The two differ on 45 rows and agree
75
+ # make two confusable identifiers collide. The two differ on 47 rows and agree
67
76
  # everywhere else. Scoped to `target: :latin` — the override rows are generated from
68
77
  # the Latin table and carry TR39's Latin-script targets, so with `target: :cyrillic`
69
78
  # it is a no-op.
@@ -75,15 +84,16 @@ module Disarm
75
84
  translate_errors { _normalize_confusables(text, target.to_s, digit_policy.to_s) }
76
85
  end
77
86
 
78
- # Whether `text` contains a character confusable with `target:` (:latin or
79
- # :cyrillic).
87
+ # Whether `text` contains a character confusable with `target:` (:latin,
88
+ # :cyrillic, :arabic or :hebrew).
80
89
  def confusable?(text, target: :latin)
81
90
  translate_errors { _confusable?(text, target.to_s) }
82
91
  end
83
92
 
84
93
  # Generate a URL-safe slug. Mirrors the core's `SlugConfig` defaults; every
85
94
  # option past `text` is keyword-only. (`regex_pattern`/`replacements` are not
86
- # surfaced yet — see ext/disarm/src/lib.rs.)
95
+ # surfaced yet — see ext/disarm/src/lib.rs.) Raises Disarm::InvalidArgument on an
96
+ # unknown `lang:`.
87
97
  def slugify(
88
98
  text,
89
99
  separator: "-",
@@ -111,16 +121,34 @@ module Disarm
111
121
  end
112
122
 
113
123
  # Replace emoji with their plain names (e.g. "👍" → "thumbs up").
114
- # `strip_modifiers:` drops skin-tone / variation modifiers before naming.
124
+ # `strip_modifiers:` drops skin-tone / variation modifiers before naming. An emoji
125
+ # CLDR cannot name (a regional indicator or a Plane 14 tag character standing
126
+ # alone) becomes "[?]", the sentinel #transliterate writes, as in every binding.
115
127
  def demojize(text, strip_modifiers: false)
116
128
  translate_errors { _demojize(text, strip_modifiers) }
117
129
  end
118
130
 
131
+ # Replace every emoji with +replacement+, verbatim.
132
+ #
133
+ # The counterpart to +demojize+, and a different question of a different table.
134
+ # +demojize+ asks what CLDR calls a character, so its domain is the name table, which
135
+ # is wider than the emoji: <tt>demojize("x\u2122y")</tt> is "x trade mark y". This
136
+ # asks whether the UCD calls it an emoji — Emoji_Presentation=Yes, an Emoji=Yes base
137
+ # carrying U+FE0F (not an Extended_Pictographic one, so a black star with a selector
138
+ # stays), and the ZWJ, modifier, keycap and flag sequences on those. Nothing
139
+ # else moves.
140
+ #
141
+ # +replacement+ is inserted exactly as given: "" closes an intra-word split and " "
142
+ # keeps two words apart, and no rule serves both.
143
+ def replace_emoji(text, replacement = "")
144
+ translate_errors { _replace_emoji(text, replacement) }
145
+ end
146
+
119
147
  # Canonicalize, but raise rather than silently normalize a structural difference
120
148
  # away — the half of the pair that lets a caller reject input instead of comparing
121
149
  # a value the sender never wrote.
122
- def canonicalize_strict(text)
123
- translate_errors { _canonicalize_strict(text) }
150
+ def canonicalize_strict(text, digit_policy: :numeric)
151
+ translate_errors { _canonicalize_strict(text, digit_policy.to_s) }
124
152
  end
125
153
 
126
154
  # Strip the non-interchange and invisible classes while KEEPING the script.
@@ -131,13 +159,13 @@ module Disarm
131
159
  # keeps the VS15/VS16 presentation selectors after a base, which the naive chain
132
160
  # deletes, and it collapses TAB/LF to a space, which the primitives leave alone.
133
161
  def strip_format(text)
134
- _strip_format(text)
162
+ translate_errors { _strip_format(text) }
135
163
  end
136
164
 
137
165
  # Remove obfuscation (zero-width, bidi, combining-mark abuse) while keeping
138
166
  # legible content.
139
- def strip_obfuscation(text)
140
- translate_errors { _strip_obfuscation(text) }
167
+ def strip_obfuscation(text, digit_policy: :numeric)
168
+ translate_errors { _strip_obfuscation(text, digit_policy.to_s) }
141
169
  end
142
170
 
143
171
  # Canonicalize text for security-sensitive comparison: strip obfuscation,
@@ -151,8 +179,8 @@ module Disarm
151
179
  # delimiter can leave here carrying one. #inspect_anomalies reports it as :confusable
152
180
  # WHEN the word also carries an ASCII letter, which is the gate that keeps ordinary
153
181
  # non-Latin text from firing; a delimiter-only string is not reported.
154
- def canonicalize(text)
155
- translate_errors { _canonicalize(text) }
182
+ def canonicalize(text, digit_policy: :numeric)
183
+ translate_errors { _canonicalize(text, digit_policy.to_s) }
156
184
  end
157
185
 
158
186
  # @deprecated Renamed to {#canonicalize} in 0.11 (the +_clean+ name
@@ -165,22 +193,65 @@ module Disarm
165
193
  # Case/accent/script-insensitive search lookup key. `lang:` applies a
166
194
  # language profile for transliteration (e.g. "ru", "uk"); nil means none.
167
195
  # Raises Disarm::InvalidArgument on an unknown lang.
168
- def search_key(text, lang: nil)
169
- translate_errors { _search_key(text, lang&.to_s) }
196
+ def search_key(text, lang: nil, digit_policy: :numeric)
197
+ translate_errors { _search_key(text, lang&.to_s, digit_policy.to_s) }
170
198
  end
171
199
 
172
200
  # Collation sort key (like #search_key, but keeps base accented characters
173
201
  # for correct ordering). `lang:` applies a language profile; nil means none.
174
202
  # Raises Disarm::InvalidArgument on an unknown lang.
175
- def sort_key(text, lang: nil)
176
- translate_errors { _sort_key(text, lang&.to_s) }
203
+ def sort_key(text, lang: nil, digit_policy: :numeric)
204
+ translate_errors { _sort_key(text, lang&.to_s, digit_policy.to_s) }
177
205
  end
178
206
 
179
207
  # Library catalog deduplication key (search_key plus confusable folding).
180
208
  # `lang:` applies a language profile; `strict_iso9:` selects the ISO 9:1995
181
209
  # 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) }
210
+ def catalog_key(text, lang: nil, strict_iso9: false, digit_policy: :numeric)
211
+ translate_errors { _catalog_key(text, lang&.to_s, strict_iso9, digit_policy.to_s) }
212
+ end
213
+
214
+ # The TR39 identifier skeleton plus the two prototype classes disarm keeps apart
215
+ # (#650). A spoof key: its only job is to make confusable identifiers collide, and its
216
+ # output is never for display. `digit_policy:` is `:numeric` (the letter half only),
217
+ # `:tr39` (adds `1 ≡ l` and `0 ≡ O`) or `:preserve` (a non-Latin numeral keeps its
218
+ # script).
219
+ def skeleton_key(text, digit_policy: :numeric)
220
+ translate_errors { _skeleton_key(text, digit_policy.to_s) }
221
+ end
222
+
223
+ # Levenshtein edit distance between `a` and `b`, in characters (#894). The one class
224
+ # of registry spoofing the confusable tables deliberately do not model — `paypa1`,
225
+ # `adm1n`. Canonicalize both sides first when composed and decomposed spellings
226
+ # should compare equal.
227
+ def edit_distance(left, right)
228
+ translate_errors { _edit_distance(left, right) }
229
+ end
230
+
231
+ # The candidate closest to `value`, as `{ value:, distance: }`, or nil beyond
232
+ # `max_distance:` (#894). Reports; it does not decide. An exact match is reported with
233
+ # distance 0, and ties go to the first candidate at the lowest distance.
234
+ def nearest_match(value, candidates, max_distance: 1)
235
+ hit = translate_errors do
236
+ # A non-collection is a wrong-typed argument, not a NoMethodError (N2).
237
+ raise ::TypeError, "candidates must be an Array or Enumerable" unless candidates.respond_to?(:map)
238
+
239
+ _nearest_match(value, candidates.map(&:to_s), max_distance)
240
+ end
241
+ hit && { value: hit[0], distance: hit[1] }
242
+ end
243
+
244
+ # Whether `text` is already its own canonical form under `preset:` (#730).
245
+ #
246
+ # The verification-path counterpart to the presets: text in, normalized text out is
247
+ # the generation path, and this is the question a caller asks about bytes that arrive
248
+ # already bound. `has_anomalies?` is not this predicate — 142,760 assigned code points are
249
+ # reported clean by the detector and are not their own canonical form.
250
+ #
251
+ # `preset:` is any name in the preset registry or any profile. Raises
252
+ # Disarm::InvalidArgument on an unknown one.
253
+ def canonical?(text, preset: "canonicalize")
254
+ translate_errors { _is_canonical?(text, preset.to_s) }
184
255
  end
185
256
 
186
257
  # Strip diacritics ("café" → "cafe").
@@ -325,15 +396,18 @@ module Disarm
325
396
  translate_errors { _strip_pua(text) }
326
397
  end
327
398
 
328
- # Strip "zalgo" combining-mark stacking, keeping at most `max_marks:` (2)
329
- # combining marks per base character.
330
- def strip_zalgo(text, max_marks: 2)
399
+ # Strip "zalgo" combining-mark stacking, keeping at most `max_marks:` marks of each
400
+ # combining class on one base character. The default is the core's,
401
+ # DEFAULT_ZALGO_MAX_MARKS (3), equal to #zalgo?'s threshold (#788), so this never
402
+ # strips from text #zalgo? declines to flag.
403
+ def strip_zalgo(text, max_marks: DEFAULT_ZALGO_MAX_MARKS)
331
404
  translate_errors { _strip_zalgo(text, max_marks) }
332
405
  end
333
406
 
334
407
  # Whether `text` looks like zalgo: any base character carries more than
335
- # `threshold:` (3) combining marks.
336
- def zalgo?(text, threshold: 3)
408
+ # `threshold:` marks of one combining class. The default is the core's,
409
+ # DEFAULT_ZALGO_THRESHOLD (3).
410
+ def zalgo?(text, threshold: DEFAULT_ZALGO_THRESHOLD)
337
411
  translate_errors { _zalgo?(text, threshold) }
338
412
  end
339
413
 
@@ -369,13 +443,15 @@ module Disarm
369
443
  # Turn arbitrary text into a safe filename. `platform:` is :universal
370
444
  # (default), :windows, or :posix; `preserve_extension:` keeps the final
371
445
  # extension when truncating to `max_length:`. Raises Disarm::InvalidArgument
372
- # on an unknown platform.
446
+ # on an unknown platform, or on a `separator:` that is not printable, non-space
447
+ # ASCII, contains a character illegal on the platform, or contains a path
448
+ # separator ("/" or "\\"). An empty separator is allowed.
373
449
  #
374
450
  # A safe *filename*, not a safe URL path segment. "%" is legal in a filename, so one
375
451
  # the caller typed is kept — sanitize_filename("..%2Fetc") returns "%2Fetc" — and a
376
452
  # consumer that percent-decodes the result must validate AFTER decoding. What this
377
- # will not do is manufacture one: "%" never appears in the output unless it appeared
378
- # in the input (#721).
453
+ # will not do is manufacture one: every "%" in the output is one the input
454
+ # contained, or part of the separator (#721).
379
455
  def sanitize_filename(text, separator: "_", max_length: 255, platform: :universal,
380
456
  lang: nil, preserve_extension: true)
381
457
  translate_errors do
@@ -422,8 +498,9 @@ module Disarm
422
498
  end
423
499
  end
424
500
 
425
- # ML/NLP normalization: NFKC → emoji→text → transliterate → strip accents →
426
- # [case fold] → strip control → strip zero-width → collapse whitespace.
501
+ # ML/NLP normalization: resolve deletions → NFKC → emoji→text → transliterate →
502
+ # strip accents → emoji→text → [case fold] → strip control → strip zero-width →
503
+ # collapse whitespace → NFC.
427
504
  #
428
505
  # `fold_case:` defaults to true. Pass false in front of a CASED model — folding is
429
506
  # destructive, cannot be undone downstream, and an uncased evaluation harness cannot
@@ -460,7 +537,7 @@ module Disarm
460
537
  # anomaly detector's `bidi` kind reports nine of the twelve, holding back LRM, RLM
461
538
  # and ALM because a lone directional mark is ordinary in right-to-left text.
462
539
  def bidi_control?(text)
463
- _has_bidi_control?(text)
540
+ translate_errors { _has_bidi_control?(text) }
464
541
  end
465
542
 
466
543
  # Explain how `lang: "auto"` detection resolves `text`: a hash with
@@ -486,6 +563,22 @@ module Disarm
486
563
  translate_errors { _script_info(name.to_s) }
487
564
  end
488
565
 
566
+ # TR39 confusable sources whose prototype is in +script+, and how many of those
567
+ # disarm's bundled tables fold. Returns a Hash with +:script+, +:sources+ and
568
+ # +:folded+ keys.
569
+ #
570
+ # The denominator +unmapped_confusables+ does not have: that measures one bundled
571
+ # table against the whole 6,565-source population, so a script disarm ships no table
572
+ # for reports a number determined by that absence rather than by its coverage.
573
+ #
574
+ # +:folded+ counts sources any bundled table reaches, not sources folded *toward*
575
+ # this script — Greek is 71 of 159, because the Latin table folds Greek letters that
576
+ # look Latin. A script disarm knows that TR39 never uses as a prototype returns 0 of
577
+ # 0. Raises Disarm::InvalidArgument on an unknown script.
578
+ def confusable_coverage(script)
579
+ translate_errors { _confusable_coverage(script.to_s) }
580
+ end
581
+
489
582
  # The Unicode `confusables.txt` release the bundled confusable tables were folded
490
583
  # from, e.g. "17.0.0". Not a Unicode version for the library as a whole — the
491
584
  # case-folding and width tables track different releases (see docs/provenance.md).
@@ -574,7 +667,9 @@ module Disarm
574
667
  # pipe.process("Café") # => "cafe"
575
668
  # pipe.process("Köln") # reuse the same handle
576
669
  #
577
- # Disarm::Pipeline#process is the Rust-defined instance method on the handle.
670
+ # Disarm::Pipeline#process is the Rust-defined instance method on the handle, and
671
+ # Disarm::Pipeline#with_digit_policy(policy) returns a copy whose confusable passes
672
+ # fold under `policy` (#646); a profile with no confusables step refuses one.
578
673
  def get_pipeline(profile)
579
674
  translate_errors { _get_pipeline(profile.to_s) }
580
675
  end
@@ -603,16 +698,30 @@ module Disarm
603
698
  # whole surface. The original backtrace is preserved (passed as the third
604
699
  # `raise` argument) so the failing native call site stays visible. A bad
605
700
  # argument from the native layer can arrive as ArgumentError (an invalid
606
- # scheme/target), TypeError (a non-String argument), or RangeError (e.g. a
607
- # negative max_length) — all map to Disarm::InvalidArgument.
701
+ # scheme/target), TypeError (a non-String argument), RangeError (e.g. a
702
+ # negative max_length) or EncodingError (a String in an encoding Ruby cannot
703
+ # transcode to UTF-8) — all map to Disarm::InvalidArgument.
608
704
  def translate_errors
609
705
  yield
610
706
  rescue Error
611
707
  raise # already in our hierarchy — don't re-wrap
612
- rescue ::ArgumentError, ::TypeError, ::RangeError => e
708
+ rescue ::ArgumentError, ::TypeError, ::RangeError, ::EncodingError => e
613
709
  raise InvalidArgument, e.message, e.backtrace
614
710
  rescue ::RuntimeError => e
615
711
  raise Error, e.message, e.backtrace
616
712
  end
617
713
  end
714
+
715
+ # The reusable handle `Disarm.get_pipeline` returns. `#process` is Rust-defined; this
716
+ # reopens the class for the one method that can fail, so its error arrives as
717
+ # `Disarm::InvalidArgument` the way every module-level call's does.
718
+ class Pipeline
719
+ # A copy of this pipeline whose confusable passes fold under `policy` (#646):
720
+ # `:numeric`, `:tr39` or `:preserve`. Raises Disarm::InvalidArgument when the profile
721
+ # has no confusables step and the policy is not the default — a setting that would
722
+ # never run is refused rather than kept.
723
+ def with_digit_policy(policy)
724
+ Disarm.send(:translate_errors) { _with_digit_policy(policy.to_s) }
725
+ end
726
+ end
618
727
  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.15.0
4
+ version: 0.17.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Richard Quinn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-01 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -95,8 +95,6 @@ files:
95
95
  - LICENSE
96
96
  - README.md
97
97
  - lib/disarm.rb
98
- - lib/disarm/3.1/disarm.so
99
- - lib/disarm/3.2/disarm.so
100
98
  - lib/disarm/3.3/disarm.so
101
99
  - lib/disarm/3.4/disarm.so
102
100
  - lib/disarm/4.0/disarm.so
@@ -117,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
115
  requirements:
118
116
  - - ">="
119
117
  - !ruby/object:Gem::Version
120
- version: '3.1'
118
+ version: '3.3'
121
119
  - - "<"
122
120
  - !ruby/object:Gem::Version
123
121
  version: 4.1.dev
Binary file
Binary file