fetchit-engine 0.1.2__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.2
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.2"
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
@@ -247,17 +301,17 @@ def _clean_cells(text, disabled=frozenset()):
247
301
  cells = _build_cells(text, disabled)
248
302
  current = "".join(c.text for c in cells)
249
303
  # Run the dash/space passes to a FIXED POINT, not once. A single pass is
250
- # not idempotent: removing an em dash can manufacture the spacing that arms
251
- # the spaced-en-dash rule ("X—– Y" -> "X – Y", and only a second clean
252
- # reached "X Y"), which broke the documented clean(clean(x)) == clean(x)
253
- # contract. Each enabled dash pass strictly reduces the dash count, so this
254
- # terminates; the equality check breaks when the dash rule is disabled.
255
- # Mirrors cleanCells() in the JS engine exactly.
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.
256
310
  for _guard in range(8):
257
311
  if not _EM_DASH.search(current):
258
312
  break
259
- if _DASH_RULE[0] not in disabled:
260
- cells, _ = _regex_pass(cells, _EM_DASH, " ", _DASH_RULE)
313
+ if _DASH_RULE_ID not in disabled:
314
+ cells, _ = _regex_pass(cells, _EM_DASH, _dash_match_replacement, _dash_rule_for)
261
315
  if _COLLAPSE_RULE[0] not in disabled:
262
316
  cells, _ = _regex_pass(cells, _MULTI_SPACE, " ", _COLLAPSE_RULE)
263
317
  if _SPACE_BEFORE_RULE[0] not in disabled:
@@ -281,12 +335,12 @@ def rebuild_text(text):
281
335
 
282
336
 
283
337
  def remove_em_dashes(text):
284
- """Replace spaced dashes with a single space. Returns (new_text, count).
285
- 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."""
286
340
  count = len(_EM_DASH.findall(text))
287
341
  if not count:
288
342
  return text, 0
289
- new = _EM_DASH.sub(" ", text)
343
+ new = _EM_DASH.sub(_dash_match_replacement, text)
290
344
  new = _MULTI_SPACE.sub(" ", new)
291
345
  new = _SPACE_BEFORE_PUNCT.sub(r"\1", new)
292
346
  return new, count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchit-engine
3
- Version: 0.1.2
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.2"
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