From 09ec6aaeb57628ea24dfea32fef59334dda5cee1 Mon Sep 17 00:00:00 2001 From: Matthew Runo <74583+inktomi@users.noreply.github.com> Date: Fri, 1 May 2026 10:03:49 -0700 Subject: [PATCH] fix: escape newlines in WireManager.add_text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The KiCad s-expression parser rejects raw newline and carriage-return characters inside quoted string literals — a multi-line text annotation written through `add_text` produced a `.kicad_sch` file that eeschema silently tolerated but `kicad-cli sch ...` refused with "Failed to load schematic." The escape pass only handled backslashes and double quotes. Add `\\n` → `\\\\n` and `\\r` → `\\\\r` to the same escape chain. Order matters: backslashes are escaped first so we don't double-escape our own escapes. A new regression test (`test_escapes_newlines_in_multiline_text`) checks both that the resulting quoted string literal contains no raw newline characters and that the file round-trips cleanly through the sexpdata parser. End-to-end smoke: a 4-line annotation written through the patched add_text now passes `kicad-cli sch erc` (exit 0) where the previous behaviour failed parse. Note: the same escape gap exists in `_make_hierarchical_label_text` and `_make_sheet_pin_text` for unescaped quotes/newlines in the user- supplied text. Not fixed here to keep this PR scoped to the documented add_text bug; happy to fold it in if a reviewer prefers. --- python/commands/wire_manager.py | 10 +++++++++- tests/test_add_schematic_text.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/python/commands/wire_manager.py b/python/commands/wire_manager.py index 47d7a75..c847fd0 100644 --- a/python/commands/wire_manager.py +++ b/python/commands/wire_manager.py @@ -1045,7 +1045,15 @@ class WireManager: ) -> bool: """Add a free-form text annotation (SCH_TEXT) to a KiCad schematic.""" try: - text_escaped = text.replace("\\", "\\\\").replace('"', '\\"') + # KiCad's parser rejects raw newlines inside quoted string literals, + # so escape them along with backslashes and quotes. Order matters: + # backslashes first, otherwise we double-escape our own escapes. + text_escaped = ( + text.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\r", "\\r") + ) uid = str(uuid.uuid4()) font_attrs = f"\n\t\t\t\t(size {font_size} {font_size})" if bold: diff --git a/tests/test_add_schematic_text.py b/tests/test_add_schematic_text.py index 9370716..1ab68b2 100644 --- a/tests/test_add_schematic_text.py +++ b/tests/test_add_schematic_text.py @@ -136,6 +136,39 @@ class TestWireManagerAddText: content = sch.read_text(encoding="utf-8") assert r"He said \"hello\"" in content + def test_escapes_newlines_in_multiline_text(self, tmp_path): + """Raw newlines in quoted string literals break kicad-cli's parser + (silently in eeschema, but kicad-cli sch reports 'Failed to load + schematic'). They must be escaped to the two-character \\n sequence.""" + from commands.wire_manager import WireManager + + sch = tmp_path / "test.kicad_sch" + sch.write_text(_MINIMAL_SCH, encoding="utf-8") + + WireManager.add_text(sch, "line one\nline two\r\nline three", [10.0, 10.0]) + + content = sch.read_text(encoding="utf-8") + # The text S-expression's quoted argument must not contain a literal + # newline. Find the (text "...") and check its first quoted arg. + text_start = content.index('(text "') + len('(text "') + # Find matching close-quote, respecting backslash escapes. + i = text_start + while i < len(content): + if content[i] == "\\": + i += 2 + continue + if content[i] == '"': + break + i += 1 + quoted = content[text_start:i] + assert ( + "\n" not in quoted and "\r" not in quoted + ), f"Quoted text still contains a raw newline: {quoted!r}" + assert "\\n" in quoted, "Newline should be escaped to \\n" + + # And the file must round-trip through the s-expression parser cleanly. + sexpdata.loads(content) + def test_result_is_valid_sexp(self, tmp_path): from commands.wire_manager import WireManager