|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
import argparse
|
|
|
|
|
import enum
|
|
|
|
|
import re
|
|
|
|
|
import pathlib
|
|
|
|
|
from typing import NamedTuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def Main():
|
|
|
|
|
root = pathlib.Path(__file__).resolve().parent.parent
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"files",
|
|
|
|
|
nargs="*",
|
|
|
|
|
default=[
|
|
|
|
|
root.joinpath("Source/itemdat.cpp"),
|
|
|
|
|
root.joinpath("Source/misdat.cpp"),
|
|
|
|
|
root.joinpath("Source/monstdat.cpp"),
|
|
|
|
|
root.joinpath("Source/spelldat.cpp"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
for file in args.files:
|
|
|
|
|
Process(file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LineState(enum.Enum):
|
|
|
|
|
NONE = 1
|
|
|
|
|
IN_TABLE = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ColumnAlign(enum.Enum):
|
|
|
|
|
LEFT = 1
|
|
|
|
|
RIGHT = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ColumnsState:
|
|
|
|
|
widths: list[int]
|
|
|
|
|
aligns: list[ColumnAlign]
|
|
|
|
|
has_header: bool
|
|
|
|
|
first_row: list[str]
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.widths = []
|
|
|
|
|
self.aligns = []
|
|
|
|
|
self.has_header = False
|
|
|
|
|
self.first_row = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def Process(path: str):
|
|
|
|
|
with open(path, "r", encoding="utf-8", newline="\r\n") as f:
|
|
|
|
|
input = f.read().splitlines()
|
|
|
|
|
|
|
|
|
|
columns_state = ColumnsState()
|
|
|
|
|
output_lines = []
|
|
|
|
|
state = LineState.NONE
|
|
|
|
|
begin = 0
|
|
|
|
|
for i in range(len(input)):
|
|
|
|
|
prev_state = state
|
|
|
|
|
state = ProcessLine(input[i], state, columns_state)
|
|
|
|
|
if prev_state != state:
|
|
|
|
|
for j in range(begin, i):
|
|
|
|
|
output_line = FormatLine(input[j], prev_state, columns_state)
|
|
|
|
|
output_lines.append(output_line)
|
|
|
|
|
columns_state = ColumnsState()
|
|
|
|
|
begin = i
|
|
|
|
|
for j in range(begin, len(input)):
|
|
|
|
|
output_line = FormatLine(input[j], state, columns_state)
|
|
|
|
|
output_lines.append(output_line)
|
|
|
|
|
|
|
|
|
|
with open(path, "w", encoding="utf-8", newline='') as f:
|
|
|
|
|
f.writelines(f"{line}\r\n" for line in output_lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CharState:
|
|
|
|
|
parentheses: list[str]
|
|
|
|
|
quotes: list[str]
|
|
|
|
|
backslash_escape: bool
|
|
|
|
|
pushed_paren: str
|
|
|
|
|
prev_char: str
|
|
|
|
|
in_comment: bool
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.parentheses = []
|
|
|
|
|
self.quotes = []
|
|
|
|
|
self.backslash_escape = False
|
|
|
|
|
self.pushed_paren = ""
|
|
|
|
|
self.prev_char = ""
|
|
|
|
|
self.in_comment = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_PARENTHESES_MAP = {")": "(", "}": "{", "]": "["}
|
|
|
|
|
_OPEN_PARENTHESES = _PARENTHESES_MAP.values()
|
|
|
|
|
_CLOSE_PARENTHESES = _PARENTHESES_MAP.keys()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def UpdateCharState(c: str, state: CharState):
|
|
|
|
|
prev_char = state.prev_char
|
|
|
|
|
state.prev_char = c
|
|
|
|
|
state.pushed_paren = ""
|
|
|
|
|
if state.in_comment:
|
|
|
|
|
if prev_char == "*" and c == "/":
|
|
|
|
|
state.in_comment = False
|
|
|
|
|
return
|
|
|
|
|
if prev_char == "/" and c == "*":
|
|
|
|
|
state.in_comment = True
|
|
|
|
|
return
|
|
|
|
|
if state.backslash_escape:
|
|
|
|
|
state.backslash_escape = False
|
|
|
|
|
return
|
|
|
|
|
if c == "\\":
|
|
|
|
|
state.backslash_escape = True
|
|
|
|
|
elif c == '"' or c == "'":
|
|
|
|
|
if not state.quotes:
|
|
|
|
|
state.quotes.append(c)
|
|
|
|
|
elif state.quotes[-1] == c:
|
|
|
|
|
state.quotes.pop()
|
|
|
|
|
elif not state.quotes:
|
|
|
|
|
if c in _OPEN_PARENTHESES:
|
|
|
|
|
state.parentheses.append(c)
|
|
|
|
|
state.pushed_paren = c
|
|
|
|
|
elif c in _CLOSE_PARENTHESES:
|
|
|
|
|
if state.parentheses and state.parentheses[-1] == _PARENTHESES_MAP.get(c):
|
|
|
|
|
state.parentheses.pop()
|
|
|
|
|
else:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"Mismatched parenthesis. Stack: {state.parentheses}. Value: '{c}'"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SKIP_LINE_RE = re.compile(r"^\s*(//|\})")
|
|
|
|
|
_HEADER_COMMENT_RE = re.compile(r"^\s*//(?! TRANSLATORS)")
|
|
|
|
|
_HEADER_CONTENTS_RE = re.compile(r"^\s*//\s*(.*)$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Row(NamedTuple):
|
|
|
|
|
header: bool
|
|
|
|
|
leading_comment: bool
|
|
|
|
|
columns: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ParseHeader(line: str) -> list[str]:
|
|
|
|
|
parens = []
|
|
|
|
|
columns = []
|
|
|
|
|
begin = end = 0
|
|
|
|
|
leading_spaces = True
|
|
|
|
|
for i in range(len(line)):
|
|
|
|
|
c = line[i]
|
|
|
|
|
if c == "{":
|
|
|
|
|
if not parens:
|
|
|
|
|
if end > begin:
|
|
|
|
|
columns.append(line[begin:end])
|
|
|
|
|
begin = end = i
|
|
|
|
|
parens.append(c)
|
|
|
|
|
continue
|
|
|
|
|
elif c == "}":
|
|
|
|
|
if not parens or parens[-1] != "{":
|
|
|
|
|
raise RuntimeError("Mismatched paretheses")
|
|
|
|
|
parens.pop()
|
|
|
|
|
if not parens:
|
|
|
|
|
if i >= begin:
|
|
|
|
|
columns.append(line[begin : i + 1])
|
|
|
|
|
begin = end = i
|
|
|
|
|
elif parens:
|
|
|
|
|
end = i + 1
|
|
|
|
|
else:
|
|
|
|
|
if c == " ":
|
|
|
|
|
if not leading_spaces:
|
|
|
|
|
if end > begin:
|
|
|
|
|
columns.append(line[begin:end])
|
|
|
|
|
leading_spaces = True
|
|
|
|
|
begin = end = i
|
|
|
|
|
else:
|
|
|
|
|
if leading_spaces:
|
|
|
|
|
begin = i
|
|
|
|
|
leading_spaces = False
|
|
|
|
|
else:
|
|
|
|
|
end = i + 1
|
|
|
|
|
if end > begin:
|
|
|
|
|
columns.append(line[begin:end])
|
|
|
|
|
return columns
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ParseRow(line: str, column_state: ColumnsState) -> Row:
|
|
|
|
|
if line.endswith("// clang-format off"):
|
|
|
|
|
return Row(header=False, leading_comment=False, columns=[])
|
|
|
|
|
if not column_state.has_header and _HEADER_COMMENT_RE.match(line):
|
|
|
|
|
header_columns = ParseHeader(_HEADER_CONTENTS_RE.match(line).group(1))
|
|
|
|
|
if len(header_columns) > 1:
|
|
|
|
|
return Row(header=True, leading_comment=False, columns=header_columns)
|
|
|
|
|
|
|
|
|
|
if _SKIP_LINE_RE.match(line):
|
|
|
|
|
return Row(header=False, leading_comment=False, columns=[])
|
|
|
|
|
|
|
|
|
|
state = CharState()
|
|
|
|
|
leading_comment = False
|
|
|
|
|
column_begin = 0
|
|
|
|
|
column_end = 0
|
|
|
|
|
leading_spaces = True
|
|
|
|
|
columns = []
|
|
|
|
|
for i in range(len(line)):
|
|
|
|
|
c = line[i]
|
|
|
|
|
try:
|
|
|
|
|
UpdateCharState(c, state)
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
raise RuntimeError(f" in:\n{line}") from e
|
|
|
|
|
if (state.parentheses and state.parentheses != ["{"]) or state.quotes:
|
|
|
|
|
if leading_spaces:
|
|
|
|
|
leading_spaces = False
|
|
|
|
|
column_begin = column_end = i
|
|
|
|
|
else:
|
|
|
|
|
column_end = i
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Top-level "{":
|
|
|
|
|
if state.pushed_paren == "{" and state.parentheses == ["{"]:
|
|
|
|
|
column = line[column_begin:column_end]
|
|
|
|
|
if column:
|
|
|
|
|
if column.startswith("/*"):
|
|
|
|
|
leading_comment = True
|
|
|
|
|
columns.append(column)
|
|
|
|
|
column_begin = column_end + 2
|
|
|
|
|
column_end = column_begin
|
|
|
|
|
leading_spaces = True
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Top-level "}":
|
|
|
|
|
if (
|
|
|
|
|
c == "}"
|
|
|
|
|
and not state.in_comment
|
|
|
|
|
and not state.quotes
|
|
|
|
|
and not state.parentheses
|
|
|
|
|
):
|
|
|
|
|
columns.append(line[column_begin:column_end])
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if state.in_comment:
|
|
|
|
|
if leading_spaces:
|
|
|
|
|
leading_spaces = False
|
|
|
|
|
column_begin = i
|
|
|
|
|
column_end = i + 1
|
|
|
|
|
elif c == " " or c == "\t":
|
|
|
|
|
if leading_spaces:
|
|
|
|
|
column_begin += 1
|
|
|
|
|
elif c == ",":
|
|
|
|
|
columns.append(line[column_begin:column_end] + c)
|
|
|
|
|
column_begin = column_end + 1
|
|
|
|
|
column_end = column_begin
|
|
|
|
|
leading_spaces = True
|
|
|
|
|
elif leading_spaces:
|
|
|
|
|
leading_spaces = False
|
|
|
|
|
column_begin = i
|
|
|
|
|
column_end = i + 1
|
|
|
|
|
else:
|
|
|
|
|
column_end = i + 1
|
|
|
|
|
|
|
|
|
|
return Row(header=False, leading_comment=leading_comment, columns=columns)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_RIGHT_ALIGN_RE = re.compile(r"^-?\d")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def CompareRows(a: list[str], b: list[str]):
|
|
|
|
|
a_width = max(len(x) for x in a) + 2
|
|
|
|
|
b_width = max(len(x) for x in b) + 2
|
|
|
|
|
shared_len = min(len(a), len(b))
|
|
|
|
|
result = []
|
|
|
|
|
for i in range(shared_len):
|
|
|
|
|
result.append(f"{f'[{a[i]}]'.ljust(a_width)} | {f'[{b[i]}]'.ljust(b_width)}")
|
|
|
|
|
if len(a) > len(b):
|
|
|
|
|
for i in range(shared_len, len(a)):
|
|
|
|
|
result.append(f"{f'[{a[i]}]'.ljust(a_width)} |")
|
|
|
|
|
else:
|
|
|
|
|
for i in range(shared_len, len(b)):
|
|
|
|
|
result.append(f"{''.ljust(a_width)} | {f'[{b[i]}]'.ljust(b_width)}")
|
|
|
|
|
return "\n".join(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ProcessLine(line: str, line_state: LineState, state: ColumnsState) -> LineState:
|
|
|
|
|
if line_state == LineState.IN_TABLE:
|
|
|
|
|
if line.endswith("// clang-format on"):
|
|
|
|
|
return LineState.NONE
|
|
|
|
|
row = ParseRow(line, state)
|
|
|
|
|
if len(row.columns) < 2:
|
|
|
|
|
return line_state
|
|
|
|
|
if not state.widths:
|
|
|
|
|
state.first_row = list(row.columns)
|
|
|
|
|
for column in row.columns:
|
|
|
|
|
state.widths.append(len(column) + 1)
|
|
|
|
|
state.aligns.append(ColumnAlign.RIGHT)
|
|
|
|
|
return line_state
|
|
|
|
|
if len(row.columns) != len(state.widths):
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"Expected {len(state.widths)} columns, got {len(row.columns)}.\n"
|
|
|
|
|
+ CompareRows(state.first_row, row.columns)
|
|
|
|
|
)
|
|
|
|
|
for i in range(len(row.columns)):
|
|
|
|
|
column = row.columns[i]
|
|
|
|
|
state.widths[i] = max(len(column), state.widths[i])
|
|
|
|
|
if column and not _RIGHT_ALIGN_RE.match(column):
|
|
|
|
|
state.aligns[i] = ColumnAlign.LEFT
|
|
|
|
|
elif line.endswith("// clang-format off"):
|
|
|
|
|
return LineState.IN_TABLE
|
|
|
|
|
return line_state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def FormatColumn(column: str, align: ColumnAlign, width: int):
|
|
|
|
|
return column.ljust(width) if align == ColumnAlign.LEFT else column.rjust(width)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def FormatLine(line: str, line_state: LineState, state: ColumnsState) -> str:
|
|
|
|
|
if line_state == LineState.NONE:
|
|
|
|
|
return line
|
|
|
|
|
row = ParseRow(line, state)
|
|
|
|
|
if len(row.columns) < 2:
|
|
|
|
|
return line
|
|
|
|
|
|
|
|
|
|
if row.header:
|
|
|
|
|
return "// " + " ".join(
|
|
|
|
|
FormatColumn(column.rstrip(), align, width)
|
|
|
|
|
for column, width, align in zip(
|
|
|
|
|
row.columns, [state.widths[0] - 1, *state.widths[1:]], state.aligns
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
if row.leading_comment:
|
|
|
|
|
result.append(FormatColumn(row.columns[0], state.aligns[0], state.widths[0]))
|
|
|
|
|
result.append("{")
|
|
|
|
|
for column, width, align in zip(
|
|
|
|
|
row.columns[1:], state.widths[1:], state.aligns[1:]
|
|
|
|
|
):
|
|
|
|
|
result.append(FormatColumn(column, align, width))
|
|
|
|
|
result.append("},")
|
|
|
|
|
return " ".join(result)
|
|
|
|
|
|
|
|
|
|
result.append("{")
|
|
|
|
|
for column, width, align in zip(row.columns, state.widths, state.aligns):
|
|
|
|
|
result.append(FormatColumn(column, align, width))
|
|
|
|
|
result.append("},")
|
|
|
|
|
return " ".join(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Main()
|