vicary 0.2.15 → 0.2.17
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/README.md +13 -0
- data/lib/vicary/candidates.rb +67 -6
- data/lib/vicary/conformance.rb +17 -0
- data/lib/vicary/placeholders.rb +196 -0
- data/lib/vicary/version.rb +1 -1
- data/lib/vicary.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 381ed16c17b2aa427a72b173af92825ad08db621b13e562fdc7977d41d760a26
|
|
4
|
+
data.tar.gz: 9e7a68508af7f405888c9a0e5ebc9b7993b3f0e7de6149c91156cf7c23b9fe29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87ffdbf4edb505a3c1d5faca88c817a5a1ad44b093fbfc329f99958c05a528b078c3fdcc1d97c9e6ffca2d255be617cb3912d6d6e2a9735f02861600aa0f0ce6
|
|
7
|
+
data.tar.gz: ec871699f441d07106e5f32981eb9a34f35ce2935f75a7f78163fc0ae277dc1afbe8fdd042e3dbf9676b8a4493dc9fb8078c36a0e3ba9cb65804cad79b17c93b
|
data/README.md
CHANGED
|
@@ -45,6 +45,19 @@ replaced. A map that is absent, or that covers only some of the placeholders in
|
|
|
45
45
|
the text, yields **no** spans and translates as the identity — a caller can
|
|
46
46
|
handle "no spans" and cannot detect a wrong offset.
|
|
47
47
|
|
|
48
|
+
Before a reader sees it, check that no placeholder the host did not restore
|
|
49
|
+
survived — echoed by a model, or paraphrased as `[NAME_1]`, `{Name_1}` or a bare
|
|
50
|
+
`NAME_1`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
show(Vicary.assert_no_placeholders(feedback)) # raises PlaceholderLeakError
|
|
54
|
+
show(Vicary.scrub_placeholders(feedback).text) # or drop each sentence carrying one
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The error names the kinds and the count, never the text.
|
|
58
|
+
`Vicary.find_placeholders` and `Vicary.has_placeholders?` answer without raising;
|
|
59
|
+
`scrub_placeholders(text, quote: true)` elides instead, for a verbatim quote.
|
|
60
|
+
|
|
48
61
|
## Checking it
|
|
49
62
|
|
|
50
63
|
Three layers, because each catches what the one above it cannot.
|
data/lib/vicary/candidates.rb
CHANGED
|
@@ -300,6 +300,39 @@ module Vicary
|
|
|
300
300
|
most much many few several enough
|
|
301
301
|
]).freeze
|
|
302
302
|
|
|
303
|
+
# Phonetic spellings of English stop words, each with the stop word it
|
|
304
|
+
# spells. Young writers, and young Spanish-L1 writers most of all,
|
|
305
|
+
# spell the closed-class words by ear — "dey sai ai smel", "so ai dont get
|
|
306
|
+
# mai clos dirtier" — and several of those spellings are given names the
|
|
307
|
+
# tier knows, so the lowercase route masked "sai ai" and "mai clos" and the
|
|
308
|
+
# sentence-initial rule let the tier vouch for "Ai", "Mi" and "De". A
|
|
309
|
+
# spelling of a stop word is the same word, so in a document that writes
|
|
310
|
+
# this way it is a stop word too. Only the spellings are read; the pairing
|
|
311
|
+
# is documentation. See `phonetic_function_words` for when a document
|
|
312
|
+
# qualifies.
|
|
313
|
+
PHONETIC_FUNCTION_WORDS = {
|
|
314
|
+
"ai" => "i", "becuz" => "because", "bicuz" => "because", "bot" => "but",
|
|
315
|
+
"cud" => "could", "da" => "the", "dan" => "than", "dat" => "that",
|
|
316
|
+
"de" => "the", "deir" => "their", "dem" => "them", "den" => "then",
|
|
317
|
+
"dere" => "there", "dese" => "these", "dey" => "they", "dis" => "this",
|
|
318
|
+
"dose" => "those", "frum" => "from", "hiz" => "his", "iz" => "is",
|
|
319
|
+
"mai" => "my", "mi" => "me", "sai" => "say", "sed" => "said",
|
|
320
|
+
"shud" => "should", "sum" => "some", "tu" => "to", "wat" => "what",
|
|
321
|
+
"wen" => "when", "wud" => "would", "wuz" => "was", "yu" => "you",
|
|
322
|
+
"yur" => "your",
|
|
323
|
+
}.freeze
|
|
324
|
+
|
|
325
|
+
# Distinct phonetic spellings a document must write in lower case before
|
|
326
|
+
# any of them is read as a word rather than a name. One is a coincidence —
|
|
327
|
+
# a friend called Mai — two different ones is a writer spelling by ear.
|
|
328
|
+
PHONETIC_REGISTER_MIN = 2
|
|
329
|
+
|
|
330
|
+
PHONETIC_TOKEN = Regexp.new(
|
|
331
|
+
"(?<![A-Za-z'’-])(?:" +
|
|
332
|
+
PHONETIC_FUNCTION_WORDS.keys.sort_by { |w| [-w.length, w] }.join("|") +
|
|
333
|
+
")(?![A-Za-z'’-])"
|
|
334
|
+
)
|
|
335
|
+
|
|
303
336
|
HONORIFIC_SET = Set.new(HONORIFICS.map(&:downcase)).freeze
|
|
304
337
|
|
|
305
338
|
# One capitalised word, hyphens and apostrophes included so
|
|
@@ -742,6 +775,20 @@ module Vicary
|
|
|
742
775
|
strip(without_clitic(strip(token.downcase, ".,")), "'’")
|
|
743
776
|
end
|
|
744
777
|
|
|
778
|
+
# The phonetic spellings this document uses as words, to be read as
|
|
779
|
+
# stops. Empty unless the document writes at least PHONETIC_REGISTER_MIN
|
|
780
|
+
# distinct spellings in lower case. When it does, the spellings it writes
|
|
781
|
+
# in lower case — the writer's own testimony that the token is a word to
|
|
782
|
+
# them — less any it also capitalises mid-sentence, which is the opposite
|
|
783
|
+
# testimony and wins.
|
|
784
|
+
def phonetic_function_words(text, written_as_a_capital = Set.new)
|
|
785
|
+
seen = Set.new
|
|
786
|
+
each_match(text, PHONETIC_TOKEN) { |m| seen << m[0] }
|
|
787
|
+
return Set.new if seen.size < PHONETIC_REGISTER_MIN
|
|
788
|
+
|
|
789
|
+
Set.new(seen.reject { |w| written_as_a_capital.include?(w) })
|
|
790
|
+
end
|
|
791
|
+
|
|
745
792
|
# Whether a capital on `token` mid-sentence is a mistake rather than a
|
|
746
793
|
# construction — the narrow half of the stoplist.
|
|
747
794
|
#
|
|
@@ -846,7 +893,11 @@ module Vicary
|
|
|
846
893
|
# the stoplist so that a bare "Mrs." cannot become a candidate on its own,
|
|
847
894
|
# but "Mrs. Okonkwo" has to stay whole: masking only the surname leaves the
|
|
848
895
|
# relationship and the surname's position in the text.
|
|
849
|
-
|
|
896
|
+
#
|
|
897
|
+
# `phonetic` is `phonetic_function_words` for the document: spellings read
|
|
898
|
+
# as stop words here. Except a particle *inside* a run — "Maria de Cruz"
|
|
899
|
+
# keeps its `de` however the rest of the document spells "the".
|
|
900
|
+
def trim(tokens, phonetic = Set.new)
|
|
850
901
|
runs = []
|
|
851
902
|
current = []
|
|
852
903
|
tokens.each_with_index do |token, index|
|
|
@@ -854,7 +905,10 @@ module Vicary
|
|
|
854
905
|
HONORIFIC_SET.include?(strip(token.downcase, ".,")) &&
|
|
855
906
|
index + 1 < tokens.length &&
|
|
856
907
|
!stop?(tokens[index + 1])
|
|
857
|
-
|
|
908
|
+
spelled_stop = !phonetic.empty? && phonetic.include?(fold(token)) &&
|
|
909
|
+
!(index.positive? && index < tokens.length - 1 &&
|
|
910
|
+
PARTICLES.include?(token))
|
|
911
|
+
if (stop?(token) || spelled_stop) && !introduces_a_name
|
|
858
912
|
unless current.empty?
|
|
859
913
|
runs << current
|
|
860
914
|
current = []
|
|
@@ -1849,7 +1903,7 @@ module Vicary
|
|
|
1849
1903
|
# capitalisation signal, and the seed stands on the given-name tier
|
|
1850
1904
|
# alone.
|
|
1851
1905
|
def find_lowercase_candidates(text, is_given, protected_span, corroborate = nil,
|
|
1852
|
-
settlement = nil)
|
|
1906
|
+
settlement = nil, phonetic = Set.new)
|
|
1853
1907
|
tokens = each_match(text, LOWER_TOKEN).map do |m|
|
|
1854
1908
|
[m[0], m.begin(0), m.begin(0) + m[0].length]
|
|
1855
1909
|
end
|
|
@@ -1857,7 +1911,10 @@ module Vicary
|
|
|
1857
1911
|
index = 0
|
|
1858
1912
|
while index < tokens.length
|
|
1859
1913
|
word, start, = tokens[index]
|
|
1860
|
-
|
|
1914
|
+
# A phonetic spelling may not SEED a span ("sai ai smel"), but may
|
|
1915
|
+
# still be the token a real seed reaches ("jeni sai") — see the
|
|
1916
|
+
# reference.
|
|
1917
|
+
if stop?(word) || phonetic.include?(word) || !is_given.call(word)
|
|
1861
1918
|
index += 1
|
|
1862
1919
|
next
|
|
1863
1920
|
end
|
|
@@ -1996,6 +2053,10 @@ module Vicary
|
|
|
1996
2053
|
)
|
|
1997
2054
|
written_as_a_capital = casing.written_as_a_capital
|
|
1998
2055
|
lower_cased = casing.written_in_lower_case
|
|
2056
|
+
# Read off the same casing pass, so a spelling the writer capitalises
|
|
2057
|
+
# mid-sentence is never one of them; and only where a given-name oracle
|
|
2058
|
+
# exists, because it only withdraws what the tier proposed or vouched for.
|
|
2059
|
+
phonetic = given_name.nil? ? Set.new : phonetic_function_words(text, written_as_a_capital)
|
|
1999
2060
|
stray_capitals = mid_sentence_corroboration && !given_name.nil? &&
|
|
2000
2061
|
capitalises_ordinary_words?(text, headings)
|
|
2001
2062
|
|
|
@@ -2007,7 +2068,7 @@ module Vicary
|
|
|
2007
2068
|
tokens = words(span)
|
|
2008
2069
|
# A long all-caps run means the capitalisation told us nothing, so the
|
|
2009
2070
|
# stoplist is carrying the whole decision.
|
|
2010
|
-
trim(tokens).each do |run|
|
|
2071
|
+
trim(tokens, phonetic).each do |run|
|
|
2011
2072
|
next if run.empty?
|
|
2012
2073
|
|
|
2013
2074
|
joined = run.join(" ")
|
|
@@ -2078,7 +2139,7 @@ module Vicary
|
|
|
2078
2139
|
# evidence to offer and is better served by it.
|
|
2079
2140
|
find_lowercase_candidates(
|
|
2080
2141
|
text, given_name, is_protected,
|
|
2081
|
-
habit == LOWERCASE ? nil : written_as_a_capital, settlement
|
|
2142
|
+
habit == LOWERCASE ? nil : written_as_a_capital, settlement, phonetic
|
|
2082
2143
|
).each do |candidate|
|
|
2083
2144
|
next if claimed.any? { |s, e| candidate.start < e && candidate.end > s }
|
|
2084
2145
|
|
data/lib/vicary/conformance.rb
CHANGED
|
@@ -181,6 +181,23 @@ module Vicary
|
|
|
181
181
|
raw
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
# The placeholder guard's contract: the pattern source, the cases and the
|
|
185
|
+
# reference's answers. Absent is an error, as for `spans.json`: a guard
|
|
186
|
+
# checked against nothing reports the same green as one that agrees.
|
|
187
|
+
def load_placeholders(dir = nil)
|
|
188
|
+
dir = Pathname.new(dir || directory)
|
|
189
|
+
path = dir.join("placeholders.json")
|
|
190
|
+
unless path.file?
|
|
191
|
+
raise SpecError,
|
|
192
|
+
"no placeholders.json at #{path}. The ports would check their " \
|
|
193
|
+
"placeholder guard against nothing."
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
raw = JSON.parse(path.read)
|
|
197
|
+
require_version(raw["document_version"], "placeholders.json")
|
|
198
|
+
raw
|
|
199
|
+
end
|
|
200
|
+
|
|
184
201
|
# Score an implementation against every frame.
|
|
185
202
|
#
|
|
186
203
|
# The block receives (sentence, identity) — the same input every Python arm
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vicary
|
|
4
|
+
# Text bound for a reader still carries a redaction placeholder.
|
|
5
|
+
#
|
|
6
|
+
# Names the kinds and the count, never the text around them: the message is
|
|
7
|
+
# going to a log, and the text is exactly what was not supposed to leave.
|
|
8
|
+
class PlaceholderLeakError < ArgumentError
|
|
9
|
+
attr_reader :kinds, :count
|
|
10
|
+
|
|
11
|
+
def initialize(kinds, count)
|
|
12
|
+
@kinds = kinds.freeze
|
|
13
|
+
@count = count
|
|
14
|
+
noun = count == 1 ? "placeholder" : "placeholders"
|
|
15
|
+
super("#{count} redaction #{noun} would reach a reader " \
|
|
16
|
+
"(kinds: #{kinds.join(', ')})")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The host boundary: assert that no placeholder reaches a reader — the Ruby
|
|
21
|
+
# port of `python/src/vicary/placeholders.py`.
|
|
22
|
+
#
|
|
23
|
+
# vicary masks what it redacts as typed placeholders (`{NAME_1}`,
|
|
24
|
+
# `{LOCATION_2}`, `{EMAIL}`) and a host restores the ones a reader may see.
|
|
25
|
+
# Everything else must never be displayed, and two ways one gets out anyway
|
|
26
|
+
# are invisible to the masker: a model echoes a placeholder the host's restore
|
|
27
|
+
# map does not cover, or paraphrases one — `[NAME_1]`, `{Name_1}`,
|
|
28
|
+
# `{ NAME_1 }`, a bare `NAME_1`.
|
|
29
|
+
#
|
|
30
|
+
# {assert_no_placeholders} raises before the text is shown;
|
|
31
|
+
# {scrub_placeholders} removes them (a prose sentence carrying one is dropped,
|
|
32
|
+
# a quote elides it with `…`). Nothing is rewritten back into a word here.
|
|
33
|
+
#
|
|
34
|
+
# The pattern source is byte-identical in all three front doors, written in
|
|
35
|
+
# the dialect the engines share — ASCII classes rather than `\b`/`\w`/`\s`,
|
|
36
|
+
# and `\A`/`\z` rather than `^`/`$`, which anchor per line in Ruby.
|
|
37
|
+
# `test/placeholders_test.rb` compares it with `conformance/placeholders.json`.
|
|
38
|
+
module Placeholders
|
|
39
|
+
KINDS = %w[
|
|
40
|
+
ADDRESS AGE AWS_ACCESS_KEY AWS_SECRET_KEY
|
|
41
|
+
CREDIT_DEBIT_CARD_CVV CREDIT_DEBIT_CARD_EXPIRY
|
|
42
|
+
CREDIT_DEBIT_CARD_NUMBER DATE_OF_BIRTH DRIVER_ID EMAIL
|
|
43
|
+
IP_ADDRESS LICENSE_PLATE LOCATION MAC_ADDRESS NAME
|
|
44
|
+
ORGANIZATION PASSWORD PERSON PHONE PIN SCHOOL URL
|
|
45
|
+
US_BANK_ACCOUNT_NUMBER US_BANK_ROUTING_NUMBER US_PASSPORT_NUMBER
|
|
46
|
+
US_SOCIAL_SECURITY_NUMBER USERNAME ZIP_CODE
|
|
47
|
+
].freeze
|
|
48
|
+
private_constant :KINDS
|
|
49
|
+
|
|
50
|
+
# Every placeholder kind vicary or its Guardrail arm can mint, plus
|
|
51
|
+
# `PERSON`, which is what a model writes when it paraphrases `NAME`.
|
|
52
|
+
# Longest first, so a match reports the whole kind.
|
|
53
|
+
PLACEHOLDER_KINDS = KINDS.sort_by { |kind| [-kind.length, kind] }.freeze
|
|
54
|
+
|
|
55
|
+
# The kind reported for a `{UPPER_CASE}` token that names no known kind.
|
|
56
|
+
OTHER_KIND = "OTHER"
|
|
57
|
+
|
|
58
|
+
WORD = "[A-Za-z0-9_]"
|
|
59
|
+
private_constant :WORD
|
|
60
|
+
|
|
61
|
+
# `NAME` -> `[Nn][Aa][Mm][Ee]`: case-insensitive in every dialect.
|
|
62
|
+
def self.fold(kind)
|
|
63
|
+
kind.chars.map { |c| c.match?(/[A-Z]/) ? "[#{c}#{c.downcase}]" : c }.join
|
|
64
|
+
end
|
|
65
|
+
private_class_method :fold
|
|
66
|
+
|
|
67
|
+
# Anything placeholder-shaped: vicary's own `{KIND}` / `{KIND_n}` for any
|
|
68
|
+
# upper-case kind; a known kind in any case inside `{}`, `[]` or `<>` with
|
|
69
|
+
# stray spaces or tabs; or a bare upper-case `KIND_n` as a whole word.
|
|
70
|
+
PLACEHOLDER_LIKE = Regexp.new(
|
|
71
|
+
"\\{[A-Z][A-Z_]*(?:_[0-9]+)?\\}" \
|
|
72
|
+
"|[{\\[<][ \\t]*(?:#{PLACEHOLDER_KINDS.map { |k| fold(k) }.join('|')})" \
|
|
73
|
+
"(?:_[0-9]+)?[ \\t]*[}\\]>]" \
|
|
74
|
+
"|(?<!#{WORD})(?:#{PLACEHOLDER_KINDS.join('|')})_[0-9]+(?!#{WORD})"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# A sentence ends at `.`, `!` or `?` followed by whitespace — but not after
|
|
78
|
+
# an honorific, an initial, `e.g.`, `i.e.` or `vs.`.
|
|
79
|
+
SENTENCE_END = Regexp.new(
|
|
80
|
+
"(?<![A-Z][a-z]\\.)" \
|
|
81
|
+
"(?<!(?<!#{WORD})[A-Z][a-z][a-z]\\.)" \
|
|
82
|
+
"(?<!(?<!#{WORD})[A-Z]\\.)" \
|
|
83
|
+
"(?<!(?<!#{WORD})e\\.g\\.)" \
|
|
84
|
+
"(?<!(?<!#{WORD})i\\.e\\.)" \
|
|
85
|
+
"(?<!(?<!#{WORD})vs\\.)" \
|
|
86
|
+
"(?<=[.!?])[ \\t\\n\\r\\f\\v]+"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# What stands in for a placeholder when every sentence of a prose field
|
|
90
|
+
# carries one. Any other kind reads "this".
|
|
91
|
+
GENERIC_STAND_INS = {
|
|
92
|
+
"NAME" => "someone",
|
|
93
|
+
"PERSON" => "someone",
|
|
94
|
+
"LOCATION" => "a place",
|
|
95
|
+
"SCHOOL" => "the school",
|
|
96
|
+
"ORGANIZATION" => "an organization"
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
TRIM = /\A[ \t\n\r\f\v]+|[ \t\n\r\f\v]+\z/
|
|
100
|
+
TRIM_TOKEN = /\A[{}\[\]<> \t\n\r\f\v]+|[{}\[\]<> \t\n\r\f\v]+\z/
|
|
101
|
+
ELISION = /[ \t\n\r\f\v]*…[ \t\n\r\f\v]*/
|
|
102
|
+
ELISION_BEFORE_PUNCT = / … ([.,;:!?])/
|
|
103
|
+
NUMBER_SUFFIX = /_[0-9]+\z/
|
|
104
|
+
private_constant :TRIM, :TRIM_TOKEN, :ELISION, :ELISION_BEFORE_PUNCT,
|
|
105
|
+
:NUMBER_SUFFIX
|
|
106
|
+
|
|
107
|
+
# One placeholder found in text. `start`/`end` count characters.
|
|
108
|
+
Match = Struct.new(:text, :kind, :start, :end) do
|
|
109
|
+
def to_h
|
|
110
|
+
{ "text" => text, "kind" => kind, "start" => start, "end" => self.end }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Text with every placeholder removed, and how many were.
|
|
115
|
+
ScrubResult = Struct.new(:text, :removed)
|
|
116
|
+
|
|
117
|
+
class << self
|
|
118
|
+
# The kind a placeholder token names: `[ Name_2 ]` -> `NAME`; else
|
|
119
|
+
# {OTHER_KIND}.
|
|
120
|
+
def kind_of(token)
|
|
121
|
+
kind = token.gsub(TRIM_TOKEN, "").upcase.sub(NUMBER_SUFFIX, "")
|
|
122
|
+
PLACEHOLDER_KINDS.include?(kind) ? kind : OTHER_KIND
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Every placeholder in `text`, in order.
|
|
126
|
+
def find(text)
|
|
127
|
+
return [] if text.nil? || text.empty?
|
|
128
|
+
|
|
129
|
+
text.to_enum(:scan, PLACEHOLDER_LIKE).map do
|
|
130
|
+
m = Regexp.last_match
|
|
131
|
+
Match.new(m[0], kind_of(m[0]), m.begin(0), m.end(0))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Whether `text` carries any placeholder.
|
|
136
|
+
def any?(text)
|
|
137
|
+
!text.nil? && !text.empty? && PLACEHOLDER_LIKE.match?(text)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Return `text` unchanged, or raise {Vicary::PlaceholderLeakError}. Call
|
|
141
|
+
# it on the last string a host builds before a reader sees it.
|
|
142
|
+
def assert_none(text)
|
|
143
|
+
found = find(text)
|
|
144
|
+
unless found.empty?
|
|
145
|
+
raise PlaceholderLeakError.new(found.map(&:kind).uniq.sort, found.length)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
text
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Remove every placeholder from `text`. Prose (the default) drops each
|
|
152
|
+
# sentence carrying one, or — when every sentence does — replaces each
|
|
153
|
+
# with its {GENERIC_STAND_INS} entry; `quote: true` elides each as `…`.
|
|
154
|
+
def scrub(text, quote: false)
|
|
155
|
+
return ScrubResult.new(text, 0) unless any?(text)
|
|
156
|
+
|
|
157
|
+
removed = text.scan(PLACEHOLDER_LIKE).length
|
|
158
|
+
if quote
|
|
159
|
+
out = text.gsub(PLACEHOLDER_LIKE, "…").gsub(ELISION, " … ")
|
|
160
|
+
.gsub(ELISION_BEFORE_PUNCT, ' …\1').gsub(TRIM, "")
|
|
161
|
+
return ScrubResult.new(out, removed)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
kept = text.gsub(TRIM, "").split(SENTENCE_END).reject { |s| any?(s) }
|
|
165
|
+
return ScrubResult.new(kept.join(" "), removed) unless kept.empty?
|
|
166
|
+
|
|
167
|
+
out = text.gsub(PLACEHOLDER_LIKE) do |token|
|
|
168
|
+
GENERIC_STAND_INS.fetch(kind_of(token), "this")
|
|
169
|
+
end
|
|
170
|
+
ScrubResult.new(out, removed)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class << self
|
|
176
|
+
# Every placeholder in `text` — see {Vicary::Placeholders}.
|
|
177
|
+
def find_placeholders(text)
|
|
178
|
+
Placeholders.find(text)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Whether `text` carries any placeholder.
|
|
182
|
+
def has_placeholders?(text)
|
|
183
|
+
Placeholders.any?(text)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Return `text`, or raise {Vicary::PlaceholderLeakError}.
|
|
187
|
+
def assert_no_placeholders(text)
|
|
188
|
+
Placeholders.assert_none(text)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Remove every placeholder from `text`.
|
|
192
|
+
def scrub_placeholders(text, quote: false)
|
|
193
|
+
Placeholders.scrub(text, quote: quote)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
data/lib/vicary/version.rb
CHANGED
|
@@ -6,5 +6,5 @@ module Vicary
|
|
|
6
6
|
# Shared across all three front doors on purpose: one detector, one number. A
|
|
7
7
|
# gem 0.3.0 that corresponds to nothing on PyPI cannot be reasoned about, and
|
|
8
8
|
# the parity claim is between *versions*, not between package names.
|
|
9
|
-
VERSION = "0.2.
|
|
9
|
+
VERSION = "0.2.17"
|
|
10
10
|
end
|
data/lib/vicary.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vicary
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.17
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blake Thomas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Finds the names a student writes about — classmates, teachers, relatives — and
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/vicary/latency_baseline.rb
|
|
39
39
|
- lib/vicary/lexicon.rb
|
|
40
40
|
- lib/vicary/minter.rb
|
|
41
|
+
- lib/vicary/placeholders.rb
|
|
41
42
|
- lib/vicary/redact.rb
|
|
42
43
|
- lib/vicary/spans.rb
|
|
43
44
|
- lib/vicary/structured.rb
|