Skip to content

Instantly share code, notes, and snippets.

@mofosyne
Last active September 22, 2024 15:49
Show Gist options
  • Select an option

  • Save mofosyne/93ad8d64fb40ef80f6af3b09dc755414 to your computer and use it in GitHub Desktop.

Select an option

Save mofosyne/93ad8d64fb40ef80f6af3b09dc755414 to your computer and use it in GitHub Desktop.

Revisions

  1. mofosyne renamed this gist Sep 22, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. mofosyne revised this gist Sep 22, 2024. 1 changed file with 119 additions and 72 deletions.
    191 changes: 119 additions & 72 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,144 +1,191 @@
    #!/usr/bin/env python3
    # KiCADv8 Style Prettify S-Expression Formatter (sexp formatter)
    # By Brian Khuu 2024
    # By Brian Khuu, 2024
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.
    # Dev Note: one_liner_prefix is added to support KiCAD specific handling of certain s-expression elements
    # for better readability e.g. `PCB_PLUGIN::formatPolyPts` in KiCAD source code.
    # Note: This script modifies formatting only; it does not perform linting or validation.
    # Context: Compact element settings are added to support KiCAD-specific handling for readability, e.g., PCB_PLUGIN::formatPolyPts.

    import os
    import argparse
    from pathlib import Path

    def prettify_sexpr(sexpr_str, one_liner_prefix=["pts"]):
    # KiCADv8 Style Prettify S-Expression Formatter (sexp formatter)
    # Function to prettify KiCad-like S-expressions
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.
    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
    in_next_char_escaped = False
    in_singular_element = False
    escape_next_char = False
    singular_element = False

    in_prefix_scan = False
    prefix_keyword_buffer = ""
    prefix_stack = []

    in_prefix = False
    prefix_keyword_scan_buffer = None
    prefix_indent = []
    element_count_stack = [0]

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

    if char == '"' or in_quote:
    # Inside a quoted string, just add characters as-is
    # Handle quoted strings, preserving their internal formatting
    result.append(char)

    if in_next_char_escaped:
    # Add the escaped character and reset escape state
    in_next_char_escaped = False
    if escape_next_char:
    escape_next_char = False
    elif char == '\\':
    # Set flag to escape the next character
    in_next_char_escaped = True
    escape_next_char = True
    elif char == '"':
    # Quote Start/Stop Control character
    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)

    # Check if we are in one liner mode for readability mode
    one_liner_mode = False
    for prefix_entry in one_liner_prefix:
    if prefix_entry in prefix_indent:
    one_liner_mode = True

    if one_liner_mode:
    # Certain prefixes elements do not do indents for readability reasons
    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 = True
    prefix_keyword_scan_buffer = ""
    in_prefix_scan = True
    prefix_keyword_buffer = ""

    # Start tracking if element is singular
    in_singular_element = True
    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 in_singular_element:
    # Closing immediately after an opening means it's a singular element, keep it inline
    if singular_element:
    result.append(')')
    in_singular_element = False
    singular_element = False
    else:
    # Close parenthesis at the current indentation level
    result.append('\n' + '\t' * indent + ')')

    # Check if still scanning for prefix keyword
    if in_prefix:
    prefix_indent.append(prefix_keyword_scan_buffer)
    in_prefix = False
    if in_prefix_scan:
    prefix_stack.append(prefix_keyword_buffer)
    in_prefix_scan = False

    # Pop element tracking stack
    prefix_indent.pop()
    prefix_stack.pop()

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

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

    # Check if prefix keyword is special one liner structure
    for prefix_entry in one_liner_prefix:
    if prefix_entry == prefix_keyword_scan_buffer:
    result.append('\n' + '\t' * indent)
    break
    # 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

    in_prefix = False

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

    # Capture Prefix Keyword
    if in_prefix:
    prefix_keyword_scan_buffer += char
    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 ')'
    in_singular_element = True
    # 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'
    formatted_sexp = '\n'.join(line.rstrip() for line in formatted_sexp.split('\n')) + '\n'

    # Formatting of s-expression completed
    return formatted_sexp

    # Arg Parsing
    parser = argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    parser.add_argument("--dst", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    dst_file = p.dst.resolve()
    # Argument Parsing
    parser = argparse.ArgumentParser(description="Prettify S-expression files")
    parser.add_argument("src", type=Path, help="Source file path")
    parser.add_argument("--dst", type=Path, help="Destination file path")
    args = parser.parse_args()

    src_file = args.src.resolve()
    dst_file = args.dst.resolve() if args.dst else None

    # Open source file
    with open(src_file, "r") as file:
    sexp_data = file.read()

    # Format s-expression source
    pretty_sexpr = prettify_sexpr(sexp_data)
    # Compact element settings for special handling
    compact_element_settings = []
    src_basename = os.path.basename(src_file)

    if src_basename.endswith(".kicad_sym"):
    compact_element_settings.append({"prefix":"pts", "elements per line": 6})
    elif src_basename.endswith(".kicad_mod"):
    pass
    elif src_basename.endswith(".kicad_sch"):
    compact_element_settings.append({"prefix":"pts", "elements per line": 6})
    elif src_basename.endswith(".kicad_pcb"):
    compact_element_settings.append({"prefix":"pts", "elements per line": 4})

    # Format the S-expression
    pretty_sexpr = prettify_sexpr(sexp_data, compact_element_settings)

    # Save formatted output
    # Save or output the result
    if dst_file:
    with open(dst_file, "w") as file:
    file.write(pretty_sexpr)
    else:
    print(pretty_sexpr)
    print(pretty_sexpr)
  3. mofosyne revised this gist Sep 20, 2024. 1 changed file with 85 additions and 53 deletions.
    138 changes: 85 additions & 53 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,112 +1,144 @@
    #!/usr/bin/env python3
    # KiCADv8 Style Prettyfy S-Expression Formatter (sexp formatter)
    # KiCADv8 Style Prettify S-Expression Formatter (sexp formatter)
    # By Brian Khuu 2024
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.

    from os import path
    # Dev Note: one_liner_prefix is added to support KiCAD specific handling of certain s-expression elements
    # for better readability e.g. `PCB_PLUGIN::formatPolyPts` in KiCAD source code.
    import argparse
    from pathlib import Path

    def prettify_sexpr(sexpr_str):
    # Prettyfy S-Expression
    def prettify_sexpr(sexpr_str, one_liner_prefix=["pts"]):
    # KiCADv8 Style Prettify S-Expression Formatter (sexp formatter)
    # Function to prettify KiCad-like S-expressions
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.
    # Warning: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.
    indent = 0
    result = []
    i = 0

    in_quote = False
    escape_next = False
    in_element = False
    in_next_char_escaped = False
    in_singular_element = False

    while i < len(sexpr_str):
    char = sexpr_str[i]
    in_prefix = False
    prefix_keyword_scan_buffer = None
    prefix_indent = []

    if escape_next:
    # Add the escaped character and reset escape state
    result.append(char)
    escape_next = False

    elif char == '\\':
    # Set flag to escape the next character
    result.append(char)
    escape_next = True

    elif char == '"':
    if escape_next:
    # If the quote is escaped, treat it as a regular character
    result.append(char)
    escape_next = False
    else:
    # Toggle the in_quote state
    result.append(char)
    in_quote = not in_quote
    in_element = True
    for char in sexpr_str:

    elif in_quote:
    if char == '"' or in_quote:
    # Inside a quoted string, just add characters as-is
    result.append(char)

    if in_next_char_escaped:
    # Add the escaped character and reset escape state
    in_next_char_escaped = False
    elif char == '\\':
    # Set flag to escape the next character
    in_next_char_escaped = True
    elif char == '"':
    # Quote Start/Stop Control character
    in_quote = not in_quote

    elif char == '(':
    if in_element:
    # If we are in an element, continue without a newline
    result.append(' (')

    # Check if we are in one liner mode for readability mode
    one_liner_mode = False
    for prefix_entry in one_liner_prefix:
    if prefix_entry in prefix_indent:
    one_liner_mode = True

    if one_liner_mode:
    # Certain prefixes elements do not do indents for readability reasons
    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 = True
    prefix_keyword_scan_buffer = ""

    # Start tracking if element is singular
    in_singular_element = True

    indent += 1
    in_element = False


    elif char == ')':
    indent -= 1

    if in_element:
    # Closing immediately after an opening means an empty list, keep it inline
    if in_singular_element:
    # Closing immediately after an opening means it's a singular element, keep it inline
    result.append(')')
    in_singular_element = False
    else:
    # Close parenthesis at the current indentation level
    result.append('\n' + '\t' * indent + ')')

    in_element = False

    # Check if still scanning for prefix keyword
    if in_prefix:
    prefix_indent.append(prefix_keyword_scan_buffer)
    in_prefix = False

    # Pop element tracking stack
    prefix_indent.pop()

    elif char.isspace():
    if result and not result[-1].isspace() and result[-1] != '(':
    result.append(' ')
    in_element = False

    # Capture Prefix Keyword
    if in_prefix:
    prefix_indent.append(prefix_keyword_scan_buffer)

    # Check if prefix keyword is special one liner structure
    for prefix_entry in one_liner_prefix:
    if prefix_entry == prefix_keyword_scan_buffer:
    result.append('\n' + '\t' * indent)
    break

    in_prefix = False

    else:
    if not in_quote:
    result.append(char)
    in_element = True
    else:
    result.append(char)
    result.append(char)

    # Capture Prefix Keyword
    if in_prefix:
    prefix_keyword_scan_buffer += char

    i += 1
    # 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 ')'
    in_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')])
    formatted_sexp = '\n'.join([line.rstrip() for line in formatted_sexp.split('\n')]) + '\n'

    # Formatting of s-expression completed
    return formatted_sexp

    # Arg Parsing
    parser = argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    parser.add_argument("--dst", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    dst_file = p.dst.resolve()

    # Open and process the file
    # Open source file
    with open(src_file, "r") as file:
    sexp_data = file.read()

    # Format s-expression source
    pretty_sexpr = prettify_sexpr(sexp_data)
    print(pretty_sexpr)

    # Save formatted output
    if dst_file:
    with open(dst_file, "w") as file:
    file.write(pretty_sexpr)
    else:
    print(pretty_sexpr)
  4. mofosyne revised this gist Sep 20, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    #!/usr/bin/env python3
    # KiCADv8 Style Prettyfy S-Expression Formatter
    # KiCADv8 Style Prettyfy S-Expression Formatter (sexp formatter)
    # By Brian Khuu 2024
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
  5. mofosyne revised this gist Sep 20, 2024. No changes.
  6. mofosyne revised this gist Sep 20, 2024. No changes.
  7. mofosyne revised this gist Sep 20, 2024. 1 changed file with 12 additions and 8 deletions.
    20 changes: 12 additions & 8 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,23 @@
    #!/usr/bin/env python3
    # KiCADv8 Style Prettyfy S-Expression Linter
    # KiCADv8 Style Prettyfy S-Expression Formatter
    # By Brian Khuu 2024
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.

    from os import path
    import argparse
    from pathlib import Path

    # Prettyfy S-Expression
    # Function to prettify KiCad-like S-expressions
    # Design Goal: To match KiCADv8 formatting style as close as possible.
    # This script formats S-expressions and is designed to be standalone without external modules.
    # Note: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.
    def prettify_sexpr(sexpr_str):
    # Prettyfy S-Expression
    # Function to prettify KiCad-like S-expressions
    # This script reformats KiCad-like S-expressions to match a specific formatting style.
    # Design Goal: To format S-expressions according to a style similar to KiCADv8.
    # Note: This script modifies the formatting of S-expressions and does not perform linting or validation.
    # Warning: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.
    indent = 0
    result = []
    i = 0
  8. mofosyne revised this gist Sep 20, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    #!/usr/bin/env python3
    # KiCADv8 style sexp linter
    # KiCADv8 Style Prettyfy S-Expression Linter
    # By Brian Khuu 2024

    from os import path
  9. mofosyne revised this gist Sep 20, 2024. No changes.
  10. mofosyne revised this gist Sep 20, 2024. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -31,8 +31,8 @@ def prettify_sexpr(sexpr_str):

    elif char == '\\':
    # Set flag to escape the next character
    escape_next = True
    result.append(char)
    escape_next = True

    elif char == '"':
    if escape_next:
    @@ -41,8 +41,8 @@ def prettify_sexpr(sexpr_str):
    escape_next = False
    else:
    # Toggle the in_quote state
    in_quote = not in_quote
    result.append(char)
    in_quote = not in_quote
    in_element = True

    elif in_quote:
  11. mofosyne revised this gist Sep 20, 2024. 1 changed file with 7 additions and 8 deletions.
    15 changes: 7 additions & 8 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,18 @@
    #!/usr/bin/env python3
    # KiCADv8 style s-exp linter
    # KiCADv8 style sexp linter
    # By Brian Khuu 2024

    # Design Goal: To match KiCADv8 formatting style as close as possible.
    # This script formats S-expressions and is designed to be standalone without external modules.

    # Note: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.

    from os import path
    import argparse
    from pathlib import Path

    # Prettyfy S-Expression
    # Function to prettify KiCad-like S-expressions
    # Design Goal: To match KiCADv8 formatting style as close as possible.
    # This script formats S-expressions and is designed to be standalone without external modules.
    # Note: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.
    def prettify_sexpr(sexpr_str):
    indent = 0
    result = []
  12. mofosyne revised this gist Sep 20, 2024. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,14 @@
    #!/usr/bin/env python3
    # KiCADv8 style s-exp linter
    # By Brian Khuu 2024
    # Design Goal: To match KiCADv8 formatting style as close as possible

    # Design Goal: To match KiCADv8 formatting style as close as possible.
    # This script formats S-expressions and is designed to be standalone without external modules.

    # Note: This diverges from KiCADv8's output style in that the 'xy' element inside 'pts' is also indented on a new line.
    # This is because KiCADv8's styling rule is not consistent specifically regarding the 'pts' element.
    # To help keep complexity low with this script, we do not support such special handling.

    from os import path
    import argparse
    from pathlib import Path
  13. mofosyne revised this gist Sep 20, 2024. No changes.
  14. mofosyne revised this gist Sep 20, 2024. 1 changed file with 23 additions and 21 deletions.
    44 changes: 23 additions & 21 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -1,27 +1,25 @@
    #!/usr/bin/env python3
    # KiCADv8 style s-exp linter
    # By Brian Khuu 2024
    # Design Goal: To match KiCADv8 formatting style as close as possible (not perfectly however as that's a bit too much work)
    # This will allow for easier comparison between file format versions and to investigate kicad bugs via diffing
    # Design Goal: To match KiCADv8 formatting style as close as possible
    # This script formats S-expressions and is designed to be standalone without external modules.

    from os import path

    import argparse
    from pathlib import Path


    # Function to prettify KiCad-like S-expressions
    def prettify_sexpr(sexpr_str):
    indent = 0
    result = []
    i = 0
    in_quote = False
    escape_next = False
    in_element = False
    in_quote = False # Track if we're inside a quoted string
    escape_next = False # Track if the next character is escaped


    while i < len(sexpr_str):
    char = sexpr_str[i]

    if escape_next:
    # Add the escaped character and reset escape state
    result.append(char)
    @@ -30,7 +28,7 @@ def prettify_sexpr(sexpr_str):
    elif char == '\\':
    # Set flag to escape the next character
    escape_next = True
    result.append(char) # Add the backslash as part of the string
    result.append(char)

    elif char == '"':
    if escape_next:
    @@ -40,8 +38,8 @@ def prettify_sexpr(sexpr_str):
    else:
    # Toggle the in_quote state
    in_quote = not in_quote
    result.append(char) # Always add the quote character
    in_element = True # We're still in an element (quoted content)
    result.append(char)
    in_element = True

    elif in_quote:
    # Inside a quoted string, just add characters as-is
    @@ -70,33 +68,37 @@ def prettify_sexpr(sexpr_str):
    in_element = False

    elif char.isspace():
    # Skip redundant spaces, but add space if it's part of an element
    if result and not result[-1].endswith('(') and not result[-1].isspace():
    if result and not result[-1].isspace() and result[-1] != '(':
    result.append(' ')
    in_element = False

    else:
    # Handle any other character (non-whitespace, non-parenthesis)
    result.append(char)
    in_element = True
    if not in_quote:
    result.append(char)
    in_element = True
    else:
    result.append(char)

    i += 1

    # Strip out any extra space on the right hand side of each line
    # 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')])

    # Formatting of s-expression completed
    return formatted_sexp

    # Arg Parsing
    parser=argparse.ArgumentParser(description="Format s-expression")
    parser = argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    base_path = path.dirname(path.realpath(__file__))

    # Open and process the file
    sexp_data = open(src_file, "r").read()
    with open(src_file, "r") as file:
    sexp_data = file.read()

    pretty_sexpr = prettify_sexpr(sexp_data)
    print(pretty_sexpr)
    print(pretty_sexpr)
  15. mofosyne revised this gist Sep 20, 2024. 1 changed file with 9 additions and 9 deletions.
    18 changes: 9 additions & 9 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -9,14 +9,6 @@
    import argparse
    from pathlib import Path

    # Arg Parsing
    parser=argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    base_path = path.dirname(path.realpath(__file__))

    sexp_data = open(src_file, "r").read()

    # Function to prettify KiCad-like S-expressions
    def prettify_sexpr(sexpr_str):
    @@ -97,6 +89,14 @@ def prettify_sexpr(sexpr_str):
    # Formatting of s-expression completed
    return formatted_sexp

    # Call the prettify function and print the result
    # Arg Parsing
    parser=argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    base_path = path.dirname(path.realpath(__file__))

    # Open and process the file
    sexp_data = open(src_file, "r").read()
    pretty_sexpr = prettify_sexpr(sexp_data)
    print(pretty_sexpr)
  16. mofosyne revised this gist Sep 20, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -90,10 +90,11 @@ def prettify_sexpr(sexpr_str):

    i += 1

    # Strip out any extra space on the right hand side of each line
    formatted_sexp = ''.join(result).strip()

    formatted_sexp = '\n'.join([line.rstrip() for line in formatted_sexp.split('\n')])

    # Formatting of s-expression completed
    return formatted_sexp

    # Call the prettify function and print the result
  17. mofosyne revised this gist Sep 20, 2024. 1 changed file with 1 addition and 4 deletions.
    5 changes: 1 addition & 4 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -85,10 +85,7 @@ def prettify_sexpr(sexpr_str):

    else:
    # Handle any other character (non-whitespace, non-parenthesis)
    if result and result[-1].endswith('('):
    result.append(char)
    else:
    result.append(char)
    result.append(char)
    in_element = True

    i += 1
  18. mofosyne created this gist Sep 20, 2024.
    104 changes: 104 additions & 0 deletions sexp_linter_kicadv8_style.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,104 @@
    #!/usr/bin/env python3
    # KiCADv8 style s-exp linter
    # By Brian Khuu 2024
    # Design Goal: To match KiCADv8 formatting style as close as possible (not perfectly however as that's a bit too much work)
    # This will allow for easier comparison between file format versions and to investigate kicad bugs via diffing

    from os import path

    import argparse
    from pathlib import Path

    # Arg Parsing
    parser=argparse.ArgumentParser(description="Format s-expression")
    parser.add_argument("src", type=Path)
    p = parser.parse_args()
    src_file = p.src.resolve()
    base_path = path.dirname(path.realpath(__file__))

    sexp_data = open(src_file, "r").read()

    # Function to prettify KiCad-like S-expressions
    def prettify_sexpr(sexpr_str):
    indent = 0
    result = []
    i = 0
    in_element = False
    in_quote = False # Track if we're inside a quoted string
    escape_next = False # Track if the next character is escaped

    while i < len(sexpr_str):
    char = sexpr_str[i]

    if escape_next:
    # Add the escaped character and reset escape state
    result.append(char)
    escape_next = False

    elif char == '\\':
    # Set flag to escape the next character
    escape_next = True
    result.append(char) # Add the backslash as part of the string

    elif char == '"':
    if escape_next:
    # If the quote is escaped, treat it as a regular character
    result.append(char)
    escape_next = False
    else:
    # Toggle the in_quote state
    in_quote = not in_quote
    result.append(char) # Always add the quote character
    in_element = True # We're still in an element (quoted content)

    elif in_quote:
    # Inside a quoted string, just add characters as-is
    result.append(char)

    elif char == '(':
    if in_element:
    # If we are in an element, continue without a newline
    result.append(' (')
    else:
    # New line and add an opening parenthesis with the current indentation
    result.append('\n' + '\t' * indent + '(')
    indent += 1
    in_element = False

    elif char == ')':
    indent -= 1

    if in_element:
    # Closing immediately after an opening means an empty list, keep it inline
    result.append(')')
    else:
    # Close parenthesis at the current indentation level
    result.append('\n' + '\t' * indent + ')')

    in_element = False

    elif char.isspace():
    # Skip redundant spaces, but add space if it's part of an element
    if result and not result[-1].endswith('(') and not result[-1].isspace():
    result.append(' ')
    in_element = False

    else:
    # Handle any other character (non-whitespace, non-parenthesis)
    if result and result[-1].endswith('('):
    result.append(char)
    else:
    result.append(char)
    in_element = True

    i += 1

    formatted_sexp = ''.join(result).strip()

    formatted_sexp = '\n'.join([line.rstrip() for line in formatted_sexp.split('\n')])

    return formatted_sexp

    # Call the prettify function and print the result
    pretty_sexpr = prettify_sexpr(sexp_data)
    print(pretty_sexpr)