DSN reader: only the declared string_quote character opens a string

KiCad declares " and writes a keycap part number as a bare ', which the
tokenizer took for an unterminated string (PCBench f.60_keyboard).
This commit is contained in:
2026-09-28 09:41:27 -06:00
parent 9de8329027
commit bb75dc5067
2 changed files with 40 additions and 1 deletions
+17 -1
View File
@@ -9,6 +9,8 @@ tolerant ``skip_scope`` behaviour.
from __future__ import annotations
import re
from .model import (
ClearanceRule,
ComponentPlace,
@@ -481,12 +483,26 @@ def _read_network(node: SExp, board: DsnBoard) -> None:
# --- top level ---------------------------------------------------------------
_STRING_QUOTE = re.compile(r"\(\s*string_quote\s+(\S)\s*\)")
def _declared_quote(text: str) -> str:
"""The quote character the file declares in ``(string_quote X)``.
Only that character opens a string. Without a declaration both kinds are
accepted, as before: KiCad declares ``"`` and then writes a bare ``'``
where a value is a single quote mark, e.g. a keycap part number.
"""
found = _STRING_QUOTE.search(text[:4096])
return found.group(1) if found else "\"'"
def parse_dsn(text: str) -> DsnBoard:
"""Parse DSN ``text`` into a :class:`DsnBoard`.
Raises :class:`DsnParseError` if the top-level scope is not ``(pcb ...)``.
"""
top = parse(text)
top = parse(text, _declared_quote(text))
if top.head != "pcb":
raise DsnParseError(
f"not a Specctra DSN file: expected top-level '(pcb ...)', got '({top.head} ...)'"
+23
View File
@@ -156,3 +156,26 @@ def test_unplaced_component_has_no_coords():
place = board.placements[0].places[0]
assert place.name == "U9"
assert place.x is None and place.y is None
# --- string_quote ----------------------------------------------------------
_QUOTED = """(pcb q.dsn
(parser (string_quote {q}) (space_in_quoted_tokens on))
(resolution um 10) (unit um)
(structure (layer F.Cu (type signal)) (boundary (path pcb 0 0 0 1000 0 1000 1000 0 1000 0 0)))
(placement (component "SW1" (place SW1 100 100 front 0 (PN {pn}))))
(library)
(network)
)"""
def test_a_bare_apostrophe_is_an_atom_when_the_file_quotes_with_double_quotes():
# KiCad declares " and writes a keycap's part number as a bare '
board = parse_dsn(_QUOTED.format(q='"', pn="'"))
assert board.parser.string_quote == '"'
def test_the_declared_quote_character_is_the_one_that_opens_strings():
board = parse_dsn(_QUOTED.format(q="'", pn="'10k 1%'").replace('"SW1"', "'SW1'"))
assert board.parser.string_quote == "'"