diff --git a/src/py/bbctrl/AuxPreprocessor.py b/src/py/bbctrl/AuxPreprocessor.py index c4084be..13318a1 100644 --- a/src/py/bbctrl/AuxPreprocessor.py +++ b/src/py/bbctrl/AuxPreprocessor.py @@ -46,8 +46,42 @@ import tempfile # Strip line comments so we don't get fooled by "(M100 not really)". +# Note this is a simple regex and doesn't handle nested parentheses +# - which actually occur in real macro headers like +# `(Composed from atoms: M102 = RELEASE (V1 on), M103 = CLAMP)`. +# Use _strip_comments() below for a parser that does handle them. _PAREN_COMMENT_RE = re.compile(r'\([^)]*\)') + +def _strip_comments(line): + """Return `line` with paren comments and the trailing semicolon + comment removed. Handles arbitrarily nested parentheses (RS274 + technically forbids them but real-world gcode comments often + contain prose with parens, e.g. `(M102 = RELEASE (V1 on))`). + + Returns just the executable code, with the original whitespace + preserved between tokens.""" + out = [] + depth = 0 + i = 0 + n = len(line) + while i < n: + c = line[i] + if c == ';' and depth == 0: + break + if c == '(': + depth += 1 + i += 1 + continue + if c == ')': + if depth > 0: depth -= 1 + i += 1 + continue + if depth == 0: + out.append(c) + i += 1 + return ''.join(out) + # ATC pneumatics M-codes mapped onto hook events. M101 is # deliberately unassigned (see header). _ATC_M_CODES = { @@ -135,8 +169,7 @@ class AuxPreprocessor(object): try: with open(path, 'r', encoding='utf-8', errors='replace') as f: for line in f: - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] + code = _strip_comments(line) if _ATC_M_RE.search(code): return True if couple_active: @@ -327,8 +360,7 @@ class AuxPreprocessor(object): line = raw.rstrip('\n') # Comment-only or blank lines pass through verbatim. - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] + code = _strip_comments(line) if not code.strip(): fout.write(raw) continue @@ -347,10 +379,14 @@ class AuxPreprocessor(object): if self._maybe_inject_a_down(code, fout): rewrote_any = True - # ATC M-codes (M100/M102/M103). Each ATC M-code on the line - # is replaced with its (MSG,HOOK::) line and - # stripped from the residual. - atc_matches = list(_ATC_M_RE.finditer(line)) + # ATC M-codes (M100/M102/M103). Match against the + # comment-stripped `code` so prose mentions like + # `(M102 = RELEASE)` inside a comment don't spuriously + # fire hooks. Each match emits a (MSG,HOOK::) + # line; the M-code is stripped from the executable + # residual but the original line's comments are kept + # for log readability. + atc_matches = list(_ATC_M_RE.finditer(code)) if atc_matches: rewrote_any = True for m in atc_matches: @@ -359,18 +395,13 @@ class AuxPreprocessor(object): event = _ATC_M_CODES.get(num) if event: fout.write('(MSG,HOOK:%s:)\n' % event) - line = _ATC_M_RE.sub('', line) - code = _PAREN_COMMENT_RE.sub('', line) - code = code.split(';', 1)[0] - if not code.strip(): - # Nothing meaningful left; preserve any trailing - # comment text but skip empty lines. - rest = line.rstrip() - if rest: - fout.write(rest + '\n') - continue - # Other gcode remains on the line - emit it. - fout.write(line + '\n') + code_stripped = _ATC_M_RE.sub('', code).strip() + if code_stripped: + # Mixed line: keep the residual executable + # gcode. Drop the comments to keep the + # rewritten file tidy (the original line's + # text already appears once as the input). + fout.write(code_stripped + '\n') continue # No rewrite needed.