Skip to content

Commit 85e0fca

Browse files
committed
Prettify sexpr to be as close to original KiCad format
Inspired by: mvnmgrx#123
1 parent d9c15ba commit 85e0fca

9 files changed

Lines changed: 208 additions & 8 deletions

File tree

src/kiutils/board.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from kiutils.items.dimensions import Dimension
2626
from kiutils.utils.strings import dequote
2727
from kiutils.utils import sexpr
28+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2829
from kiutils.footprint import Footprint
2930
from kiutils.misc.config import *
3031

@@ -249,7 +250,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
249250
filepath = self.filePath
250251

251252
with open(filepath, 'w', encoding=encoding) as outfile:
252-
outfile.write(self.to_sexpr())
253+
pre_formatted_sexpr = self.to_sexpr()
254+
outfile.write(prettify(pre_formatted_sexpr))
253255

254256
def to_sexpr(self, indent=0, newline=True) -> str:
255257
"""Generate the S-Expression representing this object

src/kiutils/dru.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from os import path
2121

2222
from kiutils.utils import sexpr
23+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2324
from kiutils.utils.strings import dequote
2425

2526
@dataclass
@@ -290,7 +291,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
290291
filepath = self.filePath
291292

292293
with open(filepath, 'w', encoding=encoding) as outfile:
293-
outfile.write(self.to_sexpr())
294+
pre_formatted_sexpr = self.to_sexpr()
295+
outfile.write(prettify(pre_formatted_sexpr))
294296

295297
def to_sexpr(self, indent=0, newline=False):
296298
"""Generate the S-Expression representing this object

src/kiutils/footprint.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from kiutils.items.fpitems import *
2727
from kiutils.items.gritems import *
2828
from kiutils.utils import sexpr
29+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2930
from kiutils.utils.strings import dequote, remove_prefix
3031
from kiutils.misc.config import *
3132

@@ -1028,7 +1029,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
10281029
filepath = self.filePath
10291030

10301031
with open(filepath, 'w', encoding=encoding) as outfile:
1031-
outfile.write(self.to_sexpr())
1032+
pre_formatted_sexpr = self.to_sexpr()
1033+
outfile.write(prettify(pre_formatted_sexpr))
10321034

10331035
def to_sexpr(self, indent=0, newline=True, layerInFirstLine=False) -> str:
10341036
"""Generate the S-Expression representing this object

src/kiutils/libraries.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from kiutils.utils.strings import dequote
2020
from kiutils.utils import sexpr
21+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2122

2223
@dataclass
2324
class Library():
@@ -194,7 +195,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
194195
filepath = self.filePath
195196

196197
with open(filepath, 'w', encoding=encoding) as outfile:
197-
outfile.write(self.to_sexpr())
198+
pre_formatted_sexpr = self.to_sexpr()
199+
outfile.write(prettify(pre_formatted_sexpr))
198200

199201
def to_sexpr(self, indent=0, newline=True) -> str:
200202
"""Generate the S-Expression representing this object

src/kiutils/schematic.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from kiutils.items.schitems import *
2323
from kiutils.symbol import Symbol
2424
from kiutils.utils import sexpr
25+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2526
from kiutils.misc.config import *
2627

2728
@dataclass
@@ -240,7 +241,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
240241
filepath = self.filePath
241242

242243
with open(filepath, 'w', encoding=encoding) as outfile:
243-
outfile.write(self.to_sexpr())
244+
pre_formatted_sexpr = self.to_sexpr()
245+
outfile.write(prettify(pre_formatted_sexpr))
244246

245247
def to_sexpr(self, indent=0, newline=True) -> str:
246248
"""Generate the S-Expression representing this object

src/kiutils/symbol.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from kiutils.items.common import Property, Font
2121
from kiutils.items.syitems import *
2222
from kiutils.utils import sexpr
23+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2324
from kiutils.utils.strings import dequote
2425
from kiutils.misc.config import *
2526

@@ -589,7 +590,8 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
589590
filepath = self.filePath
590591

591592
with open(filepath, 'w', encoding=encoding) as outfile:
592-
outfile.write(self.to_sexpr())
593+
pre_formatted_sexpr = self.to_sexpr()
594+
outfile.write(prettify(pre_formatted_sexpr))
593595

594596
def to_sexpr(self, indent: int = 0, newline: bool = True) -> str:
595597
"""Generate the S-Expression representing this object

src/kiutils/utils/sexp_prettify.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""
2+
Extracted from: https://github.com/mofosyne/sexp_prettify
3+
"""
4+
5+
def sexp_prettify(source,
6+
compact_save=False,
7+
indent_char='\t',
8+
indent_size=1,
9+
consecutive_token_wrap_threshold=72,
10+
compact_list_prefixes={"pts"},
11+
compact_list_column_limit=99,
12+
shortform_prefixes={"font", "stroke", "fill", "offset", "rotate", "scale", "teardrop"}):
13+
"""
14+
Reformats KiCad-like S-expressions to match a specific formatting style.
15+
16+
Args:
17+
source (str): The source S-expression string.
18+
compact_save (bool): If True, enables compact mode formatting.
19+
20+
Returns:
21+
str: The formatted S-expression.
22+
"""
23+
24+
# State tracking
25+
formatted = []
26+
list_depth = 0
27+
column = 0
28+
previous_non_space_output = ''
29+
in_quote = False
30+
escape_next_char = False
31+
singular_element = False
32+
space_pending = False
33+
wrapped_list = False
34+
scanning_for_prefix = False
35+
prefix_token = ''
36+
compact_list_mode = False
37+
compact_list_indent = 0
38+
shortform_mode = False
39+
shortform_indent = 0
40+
41+
for c in source:
42+
43+
# Parse quoted strings
44+
if c == '"' or in_quote:
45+
if space_pending:
46+
formatted.append(' ')
47+
column += 1
48+
space_pending = False
49+
50+
if escape_next_char:
51+
escape_next_char = False
52+
elif c == '\\':
53+
escape_next_char = True
54+
elif c == '"':
55+
in_quote = not in_quote
56+
57+
formatted.append(c)
58+
column += 1
59+
previous_non_space_output = c
60+
continue
61+
62+
# Parse spaces and newlines
63+
if c.isspace():
64+
space_pending = True
65+
66+
if scanning_for_prefix:
67+
if prefix_token in compact_list_prefixes:
68+
compact_list_mode = True
69+
compact_list_indent = list_depth
70+
elif compact_save and prefix_token in shortform_prefixes:
71+
shortform_mode = True
72+
shortform_indent = list_depth
73+
74+
scanning_for_prefix = False
75+
continue
76+
77+
# Parse opening parentheses
78+
if c == '(':
79+
space_pending = False
80+
if compact_list_mode:
81+
# In fixed listDepth, visually compact mode
82+
if column < compact_list_column_limit and previous_non_space_output == ')' or compact_list_column_limit == 0:
83+
# Is a consecutive list and still within column limit (or column limit disabled)
84+
formatted.append(' ')
85+
column += 1
86+
else:
87+
# List is either beyond column limit or not after another list move this list to the next line
88+
formatted.append('\n')
89+
formatted.append(indent_char * compact_list_indent * indent_size)
90+
column = compact_list_indent * indent_size
91+
elif shortform_mode:
92+
# In one liner mode
93+
formatted.append(' ')
94+
column += 1
95+
else:
96+
# Start scanning for prefix for special list handling
97+
scanning_for_prefix = True
98+
prefix_token = ''
99+
if list_depth > 0:
100+
# Print next line depth
101+
formatted.append('\n')
102+
formatted.append(indent_char * list_depth * indent_size)
103+
column = list_depth * indent_size
104+
105+
singular_element = True
106+
list_depth += 1
107+
108+
formatted.append('(')
109+
column += 1
110+
111+
previous_non_space_output = '('
112+
continue
113+
114+
# Parse closing parentheses
115+
if c == ')':
116+
current_shortform_mode = shortform_mode
117+
space_pending = False
118+
scanning_for_prefix = False
119+
120+
if list_depth > 0:
121+
list_depth -= 1
122+
123+
if compact_list_mode and list_depth < compact_list_indent:
124+
compact_list_mode = False
125+
if shortform_mode and list_depth < shortform_indent:
126+
shortform_mode = False
127+
128+
if wrapped_list:
129+
# This was a list with wrapped tokens so is already indented
130+
formatted.append('\n')
131+
formatted.append(indent_char * list_depth * indent_size)
132+
column = list_depth * indent_size
133+
if singular_element:
134+
singular_element = False
135+
wrapped_list = False
136+
else:
137+
# Normal List
138+
if singular_element:
139+
singular_element = False
140+
elif not current_shortform_mode:
141+
formatted.append('\n')
142+
formatted.append(indent_char * list_depth * indent_size)
143+
column = list_depth * indent_size
144+
145+
formatted.append(')')
146+
column += 1
147+
148+
if list_depth <= 0:
149+
formatted.append('\n')
150+
column = 0
151+
152+
previous_non_space_output = ')'
153+
continue
154+
155+
# Parse characters
156+
if c != '\0':
157+
if previous_non_space_output == ')' and not shortform_mode:
158+
# Is Bare token after a list that should be on next line
159+
# Dev Note: In KiCAD this may indicate a flag bug
160+
formatted.append('\n')
161+
formatted.append(indent_char * list_depth * indent_size)
162+
column = list_depth * indent_size
163+
space_pending = False
164+
elif space_pending and not shortform_mode and not compact_list_mode and column >= consecutive_token_wrap_threshold:
165+
# Token is above wrap threshold. Move token to next line (If token wrap threshold is zero then this feature is disabled)
166+
wrapped_list = True
167+
formatted.append('\n')
168+
formatted.append(indent_char * list_depth * indent_size)
169+
column = list_depth * indent_size
170+
space_pending = False
171+
elif space_pending and previous_non_space_output != '(':
172+
formatted.append(' ')
173+
column += 1
174+
space_pending = False
175+
176+
# Add to prefix scanning buffer if scanning for special list handling detection
177+
if scanning_for_prefix:
178+
prefix_token += c
179+
180+
# Add character to list
181+
formatted.append(c)
182+
column += 1
183+
previous_non_space_output = c
184+
continue
185+
186+
return ''.join(formatted)

src/kiutils/utils/sexpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ def parse_sexp(sexp):
4040
else:
4141
raise NotImplementedError("Error: (%r, %r)" % (term, value))
4242
assert not stack, "Trouble with nesting of brackets"
43-
return out[0]
43+
return out[0]

src/kiutils/wks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from kiutils.items.common import Justify
2323
from kiutils.utils.strings import dequote
2424
from kiutils.utils import sexpr
25+
from kiutils.utils.sexp_prettify import sexp_prettify as prettify
2526
from kiutils.misc.config import KIUTILS_CREATE_NEW_GENERATOR_STR, KIUTILS_CREATE_NEW_VERSION_STR
2627

2728
@dataclass
@@ -938,7 +939,8 @@ def to_file(self, filepath = None):
938939
filepath = self.filePath
939940

940941
with open(filepath, 'w') as outfile:
941-
outfile.write(self.to_sexpr())
942+
pre_formatted_sexpr = self.to_sexpr()
943+
outfile.write(prettify(pre_formatted_sexpr))
942944

943945
def to_sexpr(self, indent=0, newline=True):
944946
"""Generate the S-Expression representing this object

0 commit comments

Comments
 (0)