forked from KA-Advocates/KATranslationCheck
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Rules.py
executable file
·606 lines (552 loc) · 25.1 KB
/
Rules.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
#!/usr/bin/env python3
# coding: utf-8
import re
import cffi_re2
import os
import sys
import fnmatch
from collections import defaultdict
from enum import IntEnum
import importlib
from ansicolor import black, red, blue
from ImageAliases import downloadGDocsCSV
from io import StringIO
import sre_constants
import csv
from Perseus import *
class Severity(IntEnum):
# Notice should be used for rules where a significant number of unfixable false-positives are expected
notice = 1
# Info should be used for rules that have less impact and more false positives than standard rules.
info = 2
# Standard values should have impact on the readability that does not lead to misunderstandins.
standard = 3
# Warning values should have clearly visible impact on the readability.
warning = 4
# Dangerous values should harm the readability of the text significantly and have virtually no false-positives
dangerous = 5
if sys.version_info[0] < 3:
print("This script requires Python version 3.x")
sys.exit(1)
class CompatibilityRegexCompiler(object):
"""
Many regexes, especially those with lookahead/lookbehind constructs,
are not supported by re2 (and therefore also by cffi_re2).
However, cffi_re2 is expected to be significantly faster on complex regexes
This class transparently compiles regexes as either RE2 (preferred) or
standard re. A RE2 fail is assumed if it throws an exception.
Additionally, all regexes are wrapped in parentheses so for findall() there is always
the entire group available.
"""
def __init__(self):
self.numRegex = 0
self.numCompatRegex = 0
def compile(self, rgx, flags=0):
rgx = "({0})".format(rgx)
self.numRegex += 1
try:
return cffi_re2.compile(rgx, flags)
except ValueError:
# Enable this for debugging
# print("Regex in compatibility mode: {0}".format(rgx))
self.numCompatRegex += 1
return re.compile(rgx, flags)
reCompiler = CompatibilityRegexCompiler()
__cleanupRegex = reCompiler.compile(r'<(a|span|div|table)\s*([a-z-]+=("[^"]+"|\'[^\']+\')\s*)*>(.+?)</(a|span|div|table)>\s*', re.MULTILINE)
__cleanupDetectRegex = reCompiler.compile(r"<(a|span|div|table)")
def cleanupTranslatedString(s):
"""
Minor but fast cleanup of the msgstr in order to avoid hits in
invisible parts like HTML.
"""
if not __cleanupDetectRegex.search(s):
return s
return __cleanupRegex.sub(r"\1", s)
def importRulesForLanguage(lang, basedir="."):
"""Import ruleset from the language-specific python file"""
moduleName = "rules.{}".format(lang)
print(black("Reading rules from {}".format(moduleName), bold=True))
langModule = importlib.import_module(moduleName)
print(black("Found {} rules for language {} ({} in compatibility mode)".format(len(langModule.rules), lang, reCompiler.numCompatRegex), bold=True))
return langModule.rules, langModule.rule_errors
_extractImgRegex = reCompiler.compile(r"(https?://ka-perseus-graphie\.s3\.amazonaws\.com/[0-9a-f]{40,40}\.(png|svg))")
class Rule(object):
"""
A baseclass for rules.
Remember to implement __call__(self, msgstr, msgid),
which must return the hit or None if no hit is found.
"""
def __init__(self, name, severity=Severity.standard):
self.name = name
# If you need to save some state, you can do it here.
# This MUST NOT be filled by subclasses.
self.custom_info = {}
self.severity = severity
@property
def machine_name(self):
"""Get a machine-readable name from a rule name"""
name = self.name.lower().replace("'", "").replace("\"", "")
name = name.replace("(", "").replace(")", "").replace("{", "")
name = name.replace("}", "").replace("\\", "").replace(",", "")
name = name.replace("*", "").replace("/", "-").replace("%", "")
name = name.replace("<", "").replace(">", "").replace("/", "")
name = name.replace(">", "").replace("<", "").replace(":","")
name = re.sub(r"(\s+|-+)", "-", name)
name = re.sub(r"-+", "-", name)
name = re.sub(r"^-", "", name)
return name
def getBootstrapColor(self):
"""Get a bootstrap color class (text-...) depending on the severity"""
if self.severity == Severity.notice: return "text-muted"
elif self.severity == Severity.info: return "text-success"
elif self.severity == Severity.standard: return "text-primary"
elif self.severity == Severity.warning: return "text-warning"
elif self.severity == Severity.dangerous: return "text-danger"
else: return "text-info"
@property
def meta_dict(self):
"""Return a dictionary with meta-information about this rule"""
return {
"name": self.name,
"machine_name": self.machine_name,
"severity": self.severity,
"description": self.description,
"color": self.getBootstrapColor(),
}
def __lt__(self, other):
if self.severity != other.severity:
return self.severity < other.severity
return self.name < other.name
def apply_to_xliff_entry(self, entry, filename, ignore_untranslated=True):
"""
Apply to a dictionary of parsed PO files.
Yields tuples entry, hit, filename
"""
if ignore_untranslated and entry.is_untranslated:
return
# Translated string cleanup
translated = cleanupTranslatedString(entry.translated)
# Apply the rule
for hit in self(translated, entry.english, entry.note or "", filename=filename):
#Find images in both original and new string
origImages = [h[0] for h in _extractImgRegex.findall(entry.english)]
translatedImages = [h[0] for h in _extractImgRegex.findall(translated)]
yield (entry, hit, filename, origImages, translatedImages)
def apply_to_po(self, po, filename="[unknown file]", ignore_untranslated=True):
"""
Apply to a dictionary of parsed PO files.
Yields tuples entry, hit, filename
"""
for entry in po:
# Ignore empty translations (-> untranslated)
if not entry.msgstr:
continue
# Ignore strings which are the same orig/msgid
# This accounts for the fact that we don't know how
if ignore_untranslated and entry.msgstr == entry.msgid:
continue
# Translated string cleanup
msgstr = cleanupTranslatedString(entry.msgstr)
# Apply the rule
for hit in self(msgstr, entry.msgid, entry.tcomment, filename=filename):
#Find images in both original and new string
origImages = [h[0] for h in _extractImgRegex.findall(entry.msgid)]
translatedImages = [h[0] for h in _extractImgRegex.findall(entry.msgstr)]
yield (entry, hit, filename, origImages, translatedImages)
class SimpleRegexRule(Rule):
"""
A simple rule type that matches a regex to the translated string.
Partial matches (via re.search) are considered hits.
"""
def __init__(self, name, regex, severity=Severity.standard, flags=re.UNICODE):
super().__init__(name, severity)
self.re = reCompiler.compile(regex, flags)
self.regex_str = regex
@property
def description(self):
return "Matches regular expression '%s'" % self.regex_str
def __call__(self, msgstr, msgid, tcomment="", filename=None):
for hit in self.re.findall(msgstr):
if isinstance(hit, tuple): # Regex has groups
yield hit[0]
else:
yield hit
class SimpleSubstringRule(Rule):
"""
A simple rule type that hits when a given substring is found in the msgstr.
"""
def __init__(self, name, substr, severity=Severity.standard, case_insensitive=False):
super().__init__(name, severity)
self.substr = substr
self.ci = case_insensitive
if self.ci:
self.substr = self.substr.lower()
@property
def description(self):
return "Matches substring '%s'" % self.substr
def __call__(self, msgstr, msgid, tcomment="", filename=None):
# Case-insensitive preprocessing
if self.ci:
msgstr = msgstr.lower()
if msgstr.find(self.substr) != -1:
yield self.substr
class TranslationConstraintRule(Rule):
"""
Enforces that a certain regex in the original string will
be translated a certain way
i.e. the rule hits when regexOrig has >= 1 match in the msgid
while regexTranslated has 0 machte
"""
def __init__(self, name, regexOrig, regexTranslated, severity=Severity.standard, flags=re.UNICODE):
super().__init__(name, severity)
self.reOrig = reCompiler.compile(regexOrig, flags)
self.reTranslated = reCompiler.compile(regexTranslated, flags)
self.regex_orig_str = regexOrig
self.regex_translated_str = regexTranslated
@property
def description(self):
return "Matches '%s' if translated as '%s'" % (self.regex_orig_str, self.regex_translated_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if self.reOrig.search(msgid) and not self.reTranslated.search(msgstr):
yield "[failed constraint]"
class NegativeTranslationConstraintRule(Rule):
"""
Enforces that a certain regex in the original string will
NOT be translated a certain way,
i.e. the rule hits when regexOrig has >= 1 match in the msgid
while regexTranslated has a match.
"""
def __init__(self, name, regexOrig, regexTranslated, severity=Severity.standard, flags=re.UNICODE):
super().__init__(name, severity)
self.reOrig = reCompiler.compile(regexOrig, flags)
self.reTranslated = reCompiler.compile(regexTranslated, flags)
self.regex_orig_str = regexOrig
self.regex_translated_str = regexTranslated
@property
def description(self):
return "Matches '%s' if NOT translated as '%s'" % (self.regex_orig_str, self.regex_translated_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if self.reOrig.search(msgid) and self.reTranslated.search(msgstr):
yield "[failed constraint]"
class DynamicTranslationIdentityRule(Rule):
"""
Enforces that a match to the given regex does is contained in the translated string as-is.
This rule can also be used as a negative rule to enforce the match is not present
in the translated string.
"""
def __init__(self, name, regex, negative=False, group=None, severity=Severity.standard, flags=re.UNICODE):
super().__init__(name, severity)
self.regex_str = regex
self.regex = reCompiler.compile(regex, flags)
self.negative = negative
self.group = group
@property
def description(self):
return "Matches a match for '%s' if %spresent in the translated string" % (self.regex_str, "NOT " if self.negative else "")
def __call__(self, msgstr, msgid, tcomment="", filename=None):
matches = self.regex.findall(msgid)
if not matches: return
# Apply group filter if enabled
if self.group is not None:
matches = [m[self.group] for m in matches]
# Check individual matches
if self.negative:
for match in matches:
if match in msgstr:
yield match
else: # Positive rule
for match in matches:
if match not in msgstr:
yield match
def SimpleGlobRule(name, glob):
"""Rule wrapper that translates a glob-ish rule to a regex rule"""
return SimpleRegexRule(name, fnmatch.translate(glob))
_whitespaceRegex = reCompiler.compile(r"\s+")
class ExactCopyRule(Rule):
"""
Requires that when a list of regex matches is present in the orignal text,
the exact same list of matches is also present in the same order.
This can be used, for example, to ensure GUI elements, numbers or URLs are the same in
both the translated text and the original.
"""
def __init__(self, name, regex, severity=Severity.standard, aliases=defaultdict(str), ignore_whitespace=False, group=None):
super().__init__(name, severity)
self.regex = reCompiler.compile(regex)
self.regex_str = regex
self.aliases = aliases
self.group = group
self.ignore_whitespace = ignore_whitespace
@property
def description(self):
return "Matches if all instances of '%s' are the same (with %d aliases)" % (self.regex_str, len(self.aliases))
def __call__(self, msgstr, msgid, tcomment="", filename=None):
origMatches = self.regex.findall(msgid)
translatedMatches = self.regex.findall(msgstr)
# Apply aliases
origMatches = [self.aliases[x] or x for x in origMatches]
translatedMatches = [self.aliases[x] or x for x in translatedMatches]
# Check length
if len(origMatches) > len(translatedMatches):
yield "{0} english matches but only {1} translated matches".format(
len(origMatches), len(translatedMatches))
if len(translatedMatches) > len(origMatches):
yield "{0} translated matches but only {1} english matches".format(
len(translatedMatches), len(origMatches))
# Apply group if
if self.group is not None: # None - Use entire string. No groups must be present in regex
origMatches = [m[self.group] for m in origMatches]
translatedMatches = [m[self.group] for m in translatedMatches]
# Apply whitespace filtering
if self.ignore_whitespace:
origMatches = [_whitespaceRegex.sub("", x) or x for x in origMatches]
translatedMatches = [_whitespaceRegex.sub("", x) or x for x in translatedMatches]
# Find index of first mismatch
try:
idx = next(idx for idx, (x, y) in
enumerate(zip(origMatches, translatedMatches)) if x != y)
#Yield meaningful hit msg
engl = origMatches[idx][0] if isinstance(origMatches[idx], tuple) else origMatches[idx]
trans = translatedMatches[idx][0] if isinstance(translatedMatches[idx], tuple) else translatedMatches[idx]
engl = engl.replace(" ", "⸱").replace("\t", "→").replace("\n", "\\n")
trans = trans.replace(" ", "⸱").replace("\t", "→").replace("\n", "\\n")
yield "#{0}: '{1}' (engl) vs '{2}' (translated)".format(idx + 1, engl, trans)
except StopIteration: # No mismatch
pass
class IgnoreByFilenameRegexWrapper(Rule):
"""
Ignore a rule (i.e. force zero hits) for a set of filenames defined by a regex.
If you want to ignore a rule for all filenames starting with "learn.", you'd use:
"""
def __init__(self, filename_regex, child, invert=False):
"""
Keyword arguments:
invert: Set this to true to invert this regex, i.e. mismatches of the regex lead to a ignored entry
"""
super().__init__(child.name)
self.child = child
self.invert = invert
self.filename_regex = reCompiler.compile(filename_regex)
self.filename_regex_str = filename_regex
self.severity = child.severity
@property
def description(self):
if self.invert:
return "%s (only applied to filenames matching '%s')" % (self.child.description, self.filename_regex_str)
else:
return "%s (ignored for filenames matching '%s')" % (self.child.description, self.filename_regex_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if bool(self.filename_regex.match(filename)) != self.invert:
return None
yield from self.child(msgstr, msgid, tcomment, filename)
class IgnoreByFilenameListWrapper(Rule):
"""
Ignore a rule (i.e. force zero hits) for a set of filenames defined by a list of exact hits.
"""
def __init__(self, filenames, child):
super().__init__(child.name)
self.child = child
self.filenames = frozenset(filenames)
self.severity = child.severity
@property
def description(self):
return "%s (ignored for files %s)" % (self.child.description, str(list(self.filenames)))
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if filename in self.filenames:
return None
yield from self.child(msgstr, msgid, tcomment, filename)
class IgnoreByMsgidRegexWrapper(Rule):
"""
Ignore a rule if a regex search in the msgid returns a certain value.
This can be useful to ignore special cases of translation which
are distinguishable by the untranslated (english) text, e.g.
"Green's theorem" as a special case of untranslated "green".
Note that if a single regex hit is found, the entire string is ignore
"""
def __init__(self, msgid_regex, child):
super().__init__(child.name)
self.child = child
self.msgid_regex = reCompiler.compile(msgid_regex)
self.msgid_regex_str = msgid_regex
self.severity = child.severity
@property
def description(self):
return "%s (ignored for msgids matching '%s')" % (self.child.description, self.msgid_regex_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if self.msgid_regex.search(msgid):
return None
yield from self.child(msgstr, msgid, tcomment, filename)
class IgnoreByMsgstrRegexWrapper(Rule):
"""
Ignore a rule if a regex search in the msgstr returns a certain value.
This can be useful to ignore special cases of translation which
are distinguishable by the untranslated (english) text, e.g.
"Green's theorem" as a special case of untranslated "green".
Note that if a single regex hit is found, the entire string is ignore
"""
def __init__(self, msgstr_regex, child):
super().__init__(child.name)
self.child = child
self.msgstr_regex = reCompiler.compile(msgstr_regex)
self.msgid_regex_str = msgstr_regex
self.severity = child.severity
@property
def description(self):
return "%s (ignored for msgids matching '%s')" % (self.child.description, self.msgid_regex_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if self.msgstr_regex.search(msgstr):
return None
yield from self.child(msgstr, msgid, tcomment, filename)
class IgnoreByTcommentRegexWrapper(Rule):
"""
Ignore a rule if a regex search in the tcomment returns a certain value.
This can be useful to ignore special cases of translation which
are distinguishable by the untranslated (english) text, e.g.
"Green's theorem" as a special case of untranslated "green".
Note that if a single regex hit is found, the entire string is ignore
"""
def __init__(self, tcommentRegex, child):
super().__init__(child.name)
self.child = child
self.tcommentRegex = reCompiler.compile(tcommentRegex)
self.tcomment_regex_str = tcommentRegex
self.severity = child.severity
@property
def description(self):
return "%s (ignored for tcomments matching '%s')" % (self.child.description, self.tcomment_regex_str)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if self.tcommentRegex.search(tcomment):
return None
yield from self.child(msgstr, msgid, tcomment, filename)
class TextListRule(Rule):
"""
A rule that excepts a text list of words (e.g. typos), each of which will generate a
rule hit. The file is expected to contain one string per line.
If the file does not exist, this method prints a red bold error message and does not
generate any rule hits
"""
def __init__(self, name, filename, severity=Severity.standard, flags=re.UNICODE):
super().__init__(name, severity)
self.filename = filename
regexes = set()
self.valid = False
# Check if file exists
if os.path.isfile(filename):
with open(filename) as infile:
for line in infile:
rgx = line.strip().replace(" ", r"\s+")
# Don't match in the middle of a word
rgx = r"\b{0}\b".format(rgx)
regexes.add(rgx)
# Build large regex from all sub.regexes
self.regex = reCompiler.compile("|".join(regexes), flags=flags)
self.valid = True
else: # File does not exist
print(red("Unable to find text list file %s" % filename, bold=True))
@property
def description(self):
return "Matches one of the strings in file %s" % self.filename
def __call__(self, msgstr, msgid, tcomment="", filename=None):
if not self.valid:
return
for hit in self.regex.findall(msgstr):
if isinstance(hit, tuple): # Regex has groups
yield hit[0]
else:
yield hit
def findRule(rules, name):
"Find a rule by name"
try:
next(rule for rule in rules if rule.name == name)
except StopIteration:
return None
def AutoUntranslatedRule(name, s, severity=Severity.standard):
"""
Automatic translation constraint rule that creates a regex from a simple string.
Use case-insensitive first-character and matches only whole words
"""
buildRGX = lambda s: r"[{0}{1}]{2}".format(s[0].upper(), s[0].lower(), s[1:]) if s[0].isalpha() else s
rgxParts = [buildRGX(p.strip()) for p in s.split(",")]
rgx = r"\b({0})\b".format("|".join(rgxParts))
return SimpleRegexRule(name, rgx, severity=severity)
def AutoTranslationConstraintRule(name, sa, sb, severity=Severity.standard, flags=re.UNICODE | re.IGNORECASE):
"""
Automatic translation constraint rule that creates a regex from a plain string.
Use case-insensitive first-character and matches only whole words
"""
rgxa = r"\b" + sa.replace(" ", r"\s+") + r"\b"
rgxb = r"\b" + sb.replace(" ", r"\s+") + r"\b"
return TranslationConstraintRule(name, rgxa, rgxb, severity=severity, flags=flags)
def readRulesFromGoogleDocs(gdid, rules=[]):
rule_errors = []
for rule in readRulesFromGDocs(gdid):
if isinstance(rule, RuleError):
rule_errors.append(rule)
print(red(rule.msg))
else:
rules.append(rule)
return rules, rule_errors
class RuleError(object):
def __init__(self, msg):
self.msg = msg
class IgnorePerseusCommandsRuleWrapper(Rule):
"""
Ignore perseus commands in the msgstr
"""
def __init__(self, child):
super().__init__(child.name)
self.child = child
self.perseusList = getCachedKAPerseusCommands()
@property
def description(self):
return "%s (ignored for Perseus commands)" % (self.child.description)
def __call__(self, msgstr, msgid, tcomment="", filename=None):
for cmd in self.perseusList:
msgstr = msgstr.replace("\\{0}".format(cmd), "")
yield from self.child(msgstr, msgid, tcomment, filename)
def readRulesFromGDocs(ssid):
"Read a set of rules from a Google Docs spreadsheet"
text = StringIO(downloadGDocsCSV(ssid))
reader = csv.reader(text, delimiter=',')
next(reader) # Skip header
aliases = defaultdict(str)
for row in reader:
try:
enabled, name, ruletype, rgx1, rgx2, severityStr, ignorePerseus, group, tcommentIgnore = row
except:
yield RuleError("Unparseable row: " + str(row))
continue
# Parse enabled field
if enabled != "Y":
continue
# Parse severity
try:
severity = Severity[severityStr.lower()]
except KeyError:
yield RuleError("Rule {0}: severity {1} is invalid".format(name, severityStr))
continue
try:
rule = None
if ruletype == "AutoUntranslatedRule":
rule = AutoUntranslatedRule(name, rgx1, severity=severity)
elif ruletype == "AutoTranslationConstraintRule":
rule = AutoTranslationConstraintRule(name, rgx1, rgx2, severity=severity)
elif ruletype == "SimpleRegexRule":
rule = SimpleRegexRule(name, rgx1, severity=severity)
elif ruletype == "NegativeTranslationConstraintRule":
rule = NegativeTranslationConstraintRule(name, rgx1, rgx2, severity=severity)
elif ruletype == "TranslationConstraintRule":
rule = TranslationConstraintRule(name, rgx1, rgx2, severity=severity)
elif ruletype == "ExactCopyRule":
rule = ExactCopyRule(name, rgx1, severity=severity, group=group or None)
else:
yield RuleError("Rule {0}: ruletype {1} is invalid".format(name, ruletype))
continue
# Wrap rule
if ignorePerseus == "Y":
rule = IgnorePerseusCommandsRuleWrapper(rule)
if tcommentIgnore:
rule = IgnoreByMsgstrRegexWrapper(tcommentIgnore, rule)
yield rule
except Exception as e:
yield RuleError("Rule {0}: Error while parsing rule: {1}".format(name, e))
continue