fetchit-engine 0.1.1__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchit-engine
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Deterministic text cleanup and AI-writing heuristics. Runs entirely in your process; text never leaves it.
5
5
  Author: Outta Work Solutions
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ import json
15
15
  import os
16
16
  import re
17
17
 
18
- ENGINE_VERSION = "0.1.1"
18
+ ENGINE_VERSION = "0.2.0"
19
19
 
20
20
  # --- ruleset (single source of truth, shared with the JS package) -----------
21
21
  _RULESET_PATH = os.path.join(
@@ -43,10 +43,12 @@ _LEVEL_MODERATE = _T["levelModerate"]
43
43
  _LEVEL_HIGH = _T["levelHigh"]
44
44
 
45
45
  # --- regexes that are logic, not data (kept in code; may contain dashes) -----
46
- # Em dash (U+2014) / horizontal bar (U+2015) in any spacing; en dash (U+2013)
47
- # only when spaced on both sides. Only spaces/tabs are consumed so line breaks
48
- # survive. Mirrors _EM_DASH in text_tools.py.
49
- _EM_DASH = re.compile(r"[ \t]*[—―][ \t]*|[ \t]+–[ \t]+")
46
+ # A RUN of em dashes (U+2014) / horizontal bars (U+2015), optionally
47
+ # space-separated, is one match, so "a——b" produces one comma, not two edits;
48
+ # an en dash (U+2013) matches only when spaced on both sides, so numeric
49
+ # ranges like 3–5 survive. Only spaces/tabs are consumed so line breaks
50
+ # survive. Mirrors EM_DASH_RE in the JS engine.
51
+ _EM_DASH = re.compile(r"[ \t]*[—―](?:[ \t]*[—―])*[ \t]*|[ \t]+–[ \t]+")
50
52
  _MULTI_SPACE = re.compile(r"[ \t]{2,}")
51
53
  _SPACE_BEFORE_PUNCT = re.compile(r"[ \t]+([,.;:!?])")
52
54
 
@@ -85,10 +87,54 @@ def _split_ws(s):
85
87
  return [w for w in re.split(r"[ \t\n\r\f\v]+", s) if w]
86
88
 
87
89
  # Rule metadata for edits produced by the dash/space normalization passes.
88
- _DASH_RULE = ("dash.spaced", "dash", "Replaced a spaced dash with a space")
90
+ _DASH_RULE_ID = "dash.spaced"
91
+ _DASH_COMMA_RULE = (_DASH_RULE_ID, "dash", "Replaced a dash with a comma")
92
+ _DASH_SPACE_RULE = (_DASH_RULE_ID, "dash", "Replaced a spaced dash with a space")
93
+
94
+
95
+ def _dash_rule_for(rep):
96
+ return _DASH_COMMA_RULE if rep[:1] == "," else _DASH_SPACE_RULE
97
+
98
+
89
99
  _COLLAPSE_RULE = ("space.collapse", "space", "Collapsed repeated spaces")
90
100
  _SPACE_BEFORE_RULE = ("punct.space-before", "space", "Removed a space before punctuation")
91
101
 
102
+ # What a matched dash run becomes. A clause dash reads as a pause, so the
103
+ # default replacement is a comma. The comma is withheld (single space instead,
104
+ # the old behavior, tidied by the collapse and space-before-punct passes) when
105
+ # a comma cannot sit there: at a text or line boundary, next to punctuation or
106
+ # a bracket it would double up against, next to a dash the match could not
107
+ # consume, or between digits, where the dash is a range rather than a pause.
108
+ # When the next character is whitespace the comma takes no trailing space, so
109
+ # it hugs the word before a line break. Mirrors dashReplacement in the JS
110
+ # engine.
111
+ _DASH_CP = frozenset((0x2014, 0x2015, 0x2013))
112
+ _NO_COMMA_BEFORE = frozenset(ord(c) for c in ",.;:!?([{")
113
+ _NO_COMMA_AFTER = frozenset(ord(c) for c in ",.;:!?)]}")
114
+
115
+
116
+ def _cp_before(s, i):
117
+ return ord(s[i - 1]) if i > 0 else -1
118
+
119
+
120
+ def _cp_after(s, i):
121
+ return ord(s[i]) if i < len(s) else -1
122
+
123
+
124
+ def _dash_replacement(before_cp, after_cp):
125
+ blocked_before = (before_cp < 0 or before_cp in _WS
126
+ or before_cp in _NO_COMMA_BEFORE or before_cp in _DASH_CP)
127
+ blocked_after = after_cp < 0 or after_cp in _NO_COMMA_AFTER or after_cp in _DASH_CP
128
+ if blocked_before or blocked_after:
129
+ return " "
130
+ if 0x30 <= before_cp <= 0x39 and 0x30 <= after_cp <= 0x39:
131
+ return " "
132
+ return "," if after_cp in _WS else ", "
133
+
134
+
135
+ def _dash_match_replacement(m):
136
+ return _dash_replacement(_cp_before(m.string, m.start()), _cp_after(m.string, m.end()))
137
+
92
138
  # Priority when several passes touch the same characters and their cells merge.
93
139
  # Higher wins the label. Dash beats space cleanup beats invisible.
94
140
  _RULE_PRIORITY = {
@@ -181,14 +227,22 @@ def _regex_pass(cells, regex, replacement, rule):
181
227
  merged = cells[ci:cj]
182
228
  src0 = merged[0].src0
183
229
  src1 = merged[-1].src1
230
+ if callable(replacement):
231
+ rep = replacement(m)
232
+ elif "\\" in replacement:
233
+ rep = m.expand(replacement)
234
+ else:
235
+ rep = replacement
236
+ # `rule` may depend on the replacement chosen (the dash pass labels
237
+ # comma and space outcomes differently while keeping one ruleId).
238
+ base = rule(rep) if callable(rule) else rule
184
239
  # if any merged cell already carried a higher-priority rule, keep it
185
- best = rule
186
- best_pri = _RULE_PRIORITY.get(rule[0], 0)
240
+ best = base
241
+ best_pri = _RULE_PRIORITY.get(base[0], 0)
187
242
  for c in merged:
188
243
  if c.rule and _RULE_PRIORITY.get(c.rule[0], 0) > best_pri:
189
244
  best = c.rule
190
245
  best_pri = _RULE_PRIORITY.get(c.rule[0], 0)
191
- rep = m.expand(replacement) if "\\" in replacement else replacement
192
246
  out.append(_Cell(rep, src0, src1, best))
193
247
  last = cj
194
248
  changed = True
@@ -245,16 +299,28 @@ def _clean_cells(text, disabled=frozenset()):
245
299
  dash pass plus multi-space collapse and space-before-punct tidy. A disabled
246
300
  rule id skips its pass entirely, so cleaned text and the edit list agree."""
247
301
  cells = _build_cells(text, disabled)
248
- rebuilt = "".join(c.text for c in cells)
249
- if _EM_DASH.search(rebuilt):
250
- if _DASH_RULE[0] not in disabled:
251
- cells, _ = _regex_pass(cells, _EM_DASH, " ", _DASH_RULE)
302
+ current = "".join(c.text for c in cells)
303
+ # Run the dash/space passes to a FIXED POINT, not once. A single pass is
304
+ # not idempotent: replacing an em dash can manufacture the spacing that
305
+ # arms the spaced-en-dash rule ("X—– Y" -> "X – Y", and only the next
306
+ # iteration reaches "X, Y"), which would break the documented
307
+ # clean(clean(x)) == clean(x) contract. Each enabled dash pass strictly
308
+ # reduces the dash count, so this terminates; the equality check breaks
309
+ # when the dash rule is disabled. Mirrors cleanCells() in the JS engine.
310
+ for _guard in range(8):
311
+ if not _EM_DASH.search(current):
312
+ break
313
+ if _DASH_RULE_ID not in disabled:
314
+ cells, _ = _regex_pass(cells, _EM_DASH, _dash_match_replacement, _dash_rule_for)
252
315
  if _COLLAPSE_RULE[0] not in disabled:
253
316
  cells, _ = _regex_pass(cells, _MULTI_SPACE, " ", _COLLAPSE_RULE)
254
317
  if _SPACE_BEFORE_RULE[0] not in disabled:
255
318
  cells, _ = _regex_pass(cells, _SPACE_BEFORE_PUNCT, "\\1", _SPACE_BEFORE_RULE)
256
- cleaned = "".join(c.text for c in cells)
257
- return cleaned, cells
319
+ nxt = "".join(c.text for c in cells)
320
+ if nxt == current:
321
+ break
322
+ current = nxt
323
+ return current, cells
258
324
 
259
325
 
260
326
  # --- public building blocks --------------------------------------------------
@@ -269,12 +335,12 @@ def rebuild_text(text):
269
335
 
270
336
 
271
337
  def remove_em_dashes(text):
272
- """Replace spaced dashes with a single space. Returns (new_text, count).
273
- Back-compatible with text_tools.remove_em_dashes."""
338
+ """Replace clause dashes with a comma where one fits, else a space.
339
+ Returns (new_text, count); count is the number of matched dash groups."""
274
340
  count = len(_EM_DASH.findall(text))
275
341
  if not count:
276
342
  return text, 0
277
- new = _EM_DASH.sub(" ", text)
343
+ new = _EM_DASH.sub(_dash_match_replacement, text)
278
344
  new = _MULTI_SPACE.sub(" ", new)
279
345
  new = _SPACE_BEFORE_PUNCT.sub(r"\1", new)
280
346
  return new, count
@@ -485,7 +551,9 @@ def clean(text, options=None):
485
551
 
486
552
  invisible_n = _count_consumed(lambda cp: _invisible_rule(cp) is not None)
487
553
  oddspace_n = _count_consumed(_is_odd_space)
488
- dashes_n = sum(1 for e in edits if e["ruleId"] == "dash.spaced")
554
+ # Dashes count by consumed CHARACTER too: with the fixed-point dash pass, a
555
+ # chain like "—– " merges into one edit that removed two dashes.
556
+ dashes_n = _count_consumed(lambda cp: cp in (0x2014, 0x2015, 0x2013))
489
557
 
490
558
  return {
491
559
  "engineVersion": ENGINE_VERSION,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchit-engine
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Deterministic text cleanup and AI-writing heuristics. Runs entirely in your process; text never leaves it.
5
5
  Author: Outta Work Solutions
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fetchit-engine"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Deterministic text cleanup and AI-writing heuristics. Runs entirely in your process; text never leaves it."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes
File without changes
File without changes