Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prettify_sexpr: Added s-expression formatter #123

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/kiutils/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> Board:
raise Exception("Given path is not a file!")

with open(filepath, 'r', encoding=encoding) as infile:
item = cls.from_sexpr(sexpr.parse_sexp(infile.read()))
item = cls.from_sexpr(sexpr.parse_sexpr(infile.read()))
item.filePath = filepath
return item

Expand Down Expand Up @@ -238,7 +238,13 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)


def to_sexpr(self, indent=0, newline=True) -> str:
"""Generate the S-Expression representing this object
Expand Down
9 changes: 7 additions & 2 deletions src/kiutils/dru.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> DesignRules
# This dirty fix adds opening and closing brackets `(..)` to the read input to enable
# the S-Expression parser to work for the DRU-format as well.
data = f'({infile.read()})'
item = cls.from_sexpr(sexpr.parse_sexp(data))
item = cls.from_sexpr(sexpr.parse_sexpr(data))
item.filePath = filepath
return item

Expand Down Expand Up @@ -290,7 +290,12 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)

def to_sexpr(self, indent=0, newline=False):
"""Generate the S-Expression representing this object
Expand Down
11 changes: 9 additions & 2 deletions src/kiutils/footprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> Footprint:
with open(filepath, 'r', encoding=encoding) as infile:
rawFootprint = infile.read()

fpData = sexpr.parse_sexp(rawFootprint)
fpData = sexpr.parse_sexpr(rawFootprint)
return cls.from_sexpr(fpData)

@classmethod
Expand Down Expand Up @@ -1004,7 +1004,14 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
compact_element_settings.append({"prefix":"pts", "elements per line": 4})
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)


def to_sexpr(self, indent=0, newline=True, layerInFirstLine=False) -> str:
"""Generate the S-Expression representing this object
Expand Down
10 changes: 8 additions & 2 deletions src/kiutils/libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> LibTable:
raise Exception("Given path is not a file!")

with open(filepath, 'r', encoding=encoding) as infile:
item = cls.from_sexpr(sexpr.parse_sexp(infile.read()))
item = cls.from_sexpr(sexpr.parse_sexpr(infile.read()))
item.filePath = filepath
return item

Expand Down Expand Up @@ -194,7 +194,13 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
compact_element_settings.append({"prefix":"pts", "elements per line": 6})
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)

def to_sexpr(self, indent=0, newline=True) -> str:
"""Generate the S-Expression representing this object
Expand Down
11 changes: 9 additions & 2 deletions src/kiutils/schematic.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> Schematic:
raise Exception("Given path is not a file!")

with open(filepath, 'r', encoding=encoding) as infile:
item = cls.from_sexpr(sexpr.parse_sexp(infile.read()))
item = cls.from_sexpr(sexpr.parse_sexpr(infile.read()))
item.filePath = filepath
return item

Expand Down Expand Up @@ -227,7 +227,14 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
compact_element_settings.append({"prefix":"pts", "elements per line": 6})
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)


def to_sexpr(self, indent=0, newline=True) -> str:
"""Generate the S-Expression representing this object
Expand Down
11 changes: 9 additions & 2 deletions src/kiutils/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> SymbolLib:
raise Exception("Given path is not a file!")

with open(filepath, 'r', encoding=encoding) as infile:
item = cls.from_sexpr(sexpr.parse_sexp(infile.read()))
item = cls.from_sexpr(sexpr.parse_sexpr(infile.read()))
item.filePath = filepath
return item

Expand Down Expand Up @@ -570,7 +570,14 @@ def to_file(self, filepath = None, encoding: Optional[str] = None):
filepath = self.filePath

with open(filepath, 'w', encoding=encoding) as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
compact_element_settings.append({"prefix":"pts", "elements per line": 6})
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)


def to_sexpr(self, indent: int = 0, newline: bool = True) -> str:
"""Generate the S-Expression representing this object
Expand Down
149 changes: 147 additions & 2 deletions src/kiutils/utils/sexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
(?P<s>[^(^)\s]+)
)'''

def parse_sexp(sexp):
def parse_sexpr(sexp):
stack = []
out = []
if dbg: print("%-6s %-14s %-44s %-s" % tuple("term value out stack".split()))
Expand All @@ -40,4 +40,149 @@ def parse_sexp(sexp):
else:
raise NotImplementedError("Error: %r" % (term, value))
assert not stack, "Trouble with nesting of brackets"
return out[0]
return out[0]

def prettify_sexpr(sexpr_str, compact_element_settings=[]):
"""
Prettifies KiCad-like S-expressions according to a KiCADv8-style formatting.

Args:
sexpr_str (str): The input S-expression string to be formatted.
compact_element_settings (list of dict): A list of dictionaries containing element-specific settings.
Each dictionary should contain:
- "prefix" (str): The prefix of elements that should be handled specially.
- "elements per line" (int): The number of elements per line in compact mode (optional).

Returns:
str: The prettified S-expression string.

Example:
# Input S-expression string
sexpr = "(module (fp_text \"example\"))"

# Settings for compact element handling
compact_settings = [{"prefix": "pts", "elements per line": 4}]

# Prettify the S-expression
formatted_sexpr = prettify_sexpr(sexpr, compact_settings)
print(formatted_sexpr)
"""

indent = 0
result = []

in_quote = False
escape_next_char = False
singular_element = False

in_prefix_scan = False
prefix_keyword_buffer = ""
prefix_stack = []

element_count_stack = [0]

# Iterate through the s-expression and apply formatting
for char in sexpr_str:

if char == '"' or in_quote:
# Handle quoted strings, preserving their internal formatting
result.append(char)
if escape_next_char:
escape_next_char = False
elif char == '\\':
escape_next_char = True
elif char == '"':
in_quote = not in_quote

elif char == '(':
# Check for compact element handling
in_compact_mode = False
elements_per_line = 0

if compact_element_settings:
parent_prefix = prefix_stack[-1] if (len(prefix_stack) > 0) else None
for setting in compact_element_settings:
if setting.get("prefix") in prefix_stack:
in_compact_mode = True
if setting.get("prefix") == parent_prefix:
elements_per_line = setting.get("elements per line", 0)

if in_compact_mode:
if elements_per_line > 0:
parent_element_count = element_count_stack[-1]
if parent_element_count != 0 and ((parent_element_count % elements_per_line) == 0):
result.append('\n' + '\t' * indent)

result.append('(')

else:
# New line and add an opening parenthesis with the current indentation
result.append('\n' + '\t' * indent + '(')

# Start Prefix Keyword Scanning
in_prefix_scan = True
prefix_keyword_buffer = ""

# Start tracking if element is singular
singular_element = True

# Element Count Tracking
element_count_stack[-1] += 1
element_count_stack.append(0)

indent += 1

elif char == ')':
# Handle closing elements
indent -= 1
element_count_stack.pop()

if singular_element:
result.append(')')
singular_element = False
else:
result.append('\n' + '\t' * indent + ')')

if in_prefix_scan:
prefix_stack.append(prefix_keyword_buffer)
in_prefix_scan = False

prefix_stack.pop()

elif char.isspace():
# Handling spaces
if result and not result[-1].isspace() and result[-1] != '(':
result.append(' ')

if in_prefix_scan:
# Capture Prefix Keyword
prefix_stack.append(prefix_keyword_buffer)

# Handle special compact elements
if compact_element_settings:
for setting in compact_element_settings:
if setting.get("prefix") == prefix_keyword_buffer:
result.append('\n' + '\t' * indent)
break

in_prefix_scan = False

else:
# Handle any other characters
result.append(char)

# Capture Prefix Keyword
if in_prefix_scan:
prefix_keyword_buffer += char

# Dev Note: In my opinion, this shouldn't be here... but is here so that we can match KiCADv8's behavior when a ')' is following a non ')'
singular_element = True

# Join results list and strip out any spaces in the beginning and end of the document
formatted_sexp = ''.join(result).strip()

# Strip out any extra space on the right hand side of each line
formatted_sexp = '\n'.join(line.rstrip() for line in formatted_sexp.split('\n')) + '\n'

# Formatting of s-expression completed
return formatted_sexp
10 changes: 8 additions & 2 deletions src/kiutils/wks.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ def from_file(cls, filepath: str, encoding: Optional[str] = None) -> WorkSheet:
raise Exception("Given path is not a file!")

with open(filepath, 'r', encoding=encoding) as infile:
item = cls.from_sexpr(sexpr.parse_sexp(infile.read()))
item = cls.from_sexpr(sexpr.parse_sexpr(infile.read()))
item.filePath = filepath
return item

Expand Down Expand Up @@ -938,7 +938,13 @@ def to_file(self, filepath = None):
filepath = self.filePath

with open(filepath, 'w') as outfile:
outfile.write(self.to_sexpr())
pre_formatted_sexpr = self.to_sexpr()

compact_element_settings = []
post_formatted_sexpr = sexpr.prettify_sexpr(pre_formatted_sexpr, compact_element_settings)

outfile.write(post_formatted_sexpr)


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