openscad-parser 2.4.6__tar.gz → 2.4.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_parser
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
5
5
  Keywords: openscad,openscad parser,parser
6
6
  Author: Revar Desmera
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_parser"
7
- version = "2.4.6"
7
+ version = "2.4.8"
8
8
  description = "A PEG parser to read OpenSCAD language source code, with optional AST tree generation."
9
9
  readme = "README.rst"
10
10
  authors = [
@@ -624,7 +624,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
624
624
  arguments = children[0] if len(children) > 0 else []
625
625
  body = children[1] if len(children) > 1 else None
626
626
  if body is None:
627
- raise ValueError("assert_expr should have an Expression body")
627
+ body = UndefinedLiteral(position=self._get_node_position(node))
628
628
  return AssertOp(arguments=arguments, body=body, position=self._get_node_position(node))
629
629
 
630
630
  def visit_echo_expr(self, node, children):
@@ -637,7 +637,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
637
637
  arguments = children[0] if len(children) > 0 else []
638
638
  body = children[1] if len(children) > 1 else None
639
639
  if body is None:
640
- raise ValueError("echo_expr should have an Expression body")
640
+ body = UndefinedLiteral(position=self._get_node_position(node))
641
641
  return EchoOp(arguments=arguments, body=body, position=self._get_node_position(node))
642
642
 
643
643
  def visit_ternary_expr(self, node, children):
@@ -430,7 +430,10 @@ class EchoOp(Expression):
430
430
  body: Expression
431
431
 
432
432
  def __str__(self):
433
- return f"echo({', '.join(str(arg) for arg in self.arguments)}) {self.body}"
433
+ args = ', '.join(str(arg) for arg in self.arguments)
434
+ if isinstance(self.body, UndefinedLiteral):
435
+ return f"echo({args})"
436
+ return f"echo({args}) {self.body}"
434
437
 
435
438
  def build_scope(self, parent_scope: "Scope") -> None:
436
439
  self.scope = parent_scope
@@ -458,7 +461,10 @@ class AssertOp(Expression):
458
461
  body: Expression
459
462
 
460
463
  def __str__(self):
461
- return f"assert({', '.join(str(arg) for arg in self.arguments)}) {self.body}"
464
+ args = ', '.join(str(arg) for arg in self.arguments)
465
+ if isinstance(self.body, UndefinedLiteral):
466
+ return f"assert({args})"
467
+ return f"assert({args}) {self.body}"
462
468
 
463
469
  def build_scope(self, parent_scope: "Scope") -> None:
464
470
  self.scope = parent_scope
@@ -15,11 +15,15 @@ from .nodes import (
15
15
  EchoOp,
16
16
  AssertOp,
17
17
  LetOp,
18
+ UndefinedLiteral,
18
19
  PrimaryCall,
19
20
  ListComprehension,
20
21
  ListCompFor,
21
22
  ListCompCFor,
22
23
  ListCompLet,
24
+ ListCompIf,
25
+ ListCompIfElse,
26
+ ListCompEach,
23
27
  PositionalArgument,
24
28
  NamedArgument,
25
29
  )
@@ -43,7 +47,27 @@ def to_openscad(nodes: list[ASTNode], indent_width: int = 4) -> str:
43
47
  parts.append("")
44
48
  parts.append(_fmt_node(node, 0, indent_width))
45
49
  prev_complex = is_complex
46
- return "\n".join(parts)
50
+ return _coalesce_paren_bracket("\n".join(parts))
51
+
52
+
53
+ def _coalesce_paren_bracket(text: str) -> str:
54
+ """Join consecutive lines where one is a bare ')' and the next starts with '['."""
55
+ lines = text.split("\n")
56
+ result = []
57
+ i = 0
58
+ while i < len(lines):
59
+ if (
60
+ i + 1 < len(lines)
61
+ and lines[i].strip() == ")"
62
+ and lines[i + 1].lstrip().startswith("[")
63
+ ):
64
+ indent = len(lines[i]) - len(lines[i].lstrip())
65
+ result.append(" " * indent + ") " + lines[i + 1].lstrip())
66
+ i += 2
67
+ else:
68
+ result.append(lines[i])
69
+ i += 1
70
+ return "\n".join(result)
47
71
 
48
72
 
49
73
  # Line length beyond which call arguments are formatted one-per-line.
@@ -105,14 +129,29 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
105
129
  return f"let({assigns})\n{pad}{body}"
106
130
  if isinstance(elem, LetOp):
107
131
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
108
- body = str(elem.body)
132
+ body = _fmt_expr(elem.body, indent, w)
109
133
  if len(formatted) > 1 or any("\n" in fa for fa in formatted):
110
134
  assign_lines = (",\n" + inner_pad).join(formatted)
111
135
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
112
136
  assigns = ", ".join(formatted)
113
- return f"let({assigns}) {body}"
137
+ inline = f"let({assigns}) {body}"
138
+ if "\n" in body or len(inline) + indent > _MULTILINE_CHAR_LIMIT:
139
+ return f"let({assigns})\n{pad}{body}"
140
+ return inline
114
141
  if isinstance(elem, ListComprehension):
115
142
  return _fmt_expr(elem, indent, w)
143
+ if isinstance(elem, ListCompIf):
144
+ cond = str(elem.condition)
145
+ body = _fmt_list_elem(elem.true_expr, indent + w, w)
146
+ return f"if ({cond})\n{inner_pad}{body}"
147
+ if isinstance(elem, ListCompIfElse):
148
+ cond = str(elem.condition)
149
+ true_body = _fmt_list_elem(elem.true_expr, indent + w, w)
150
+ false_body = _fmt_list_elem(elem.false_expr, indent + w, w)
151
+ return f"if ({cond})\n{inner_pad}{true_body}\n{pad}else\n{inner_pad}{false_body}"
152
+ if isinstance(elem, ListCompEach):
153
+ body = _fmt_list_elem(elem.body, indent, w)
154
+ return f"each {body}"
116
155
  return str(elem)
117
156
 
118
157
 
@@ -138,10 +177,43 @@ def _fmt_argument(arg, indent: int, w: int) -> str:
138
177
  return str(arg)
139
178
 
140
179
 
180
+ def _fmt_ternary_chain(expr: TernaryOp, indent: int, w: int) -> str:
181
+ """Format a right-chain of ternaries with flat ? / : alignment.
182
+
183
+ All ': ' connectors stay at the same indent column as the conditions;
184
+ each true branch is on its own line at indent+w. The '?' moves to
185
+ the end of the condition line rather than the beginning of the true-branch.
186
+
187
+ cond1?
188
+ true1
189
+ : cond2?
190
+ true2
191
+ : final_else
192
+ """
193
+ pad = " " * indent
194
+ inner_pad = " " * (indent + w)
195
+ parts = []
196
+ node: TernaryOp = expr
197
+ while isinstance(node, TernaryOp):
198
+ parts.append((node.condition, node.true_expr))
199
+ node = node.false_expr
200
+ final = node
201
+ lines = []
202
+ for i, (cond, true_expr) in enumerate(parts):
203
+ true_str = _fmt_expr(true_expr, indent + w, w)
204
+ prefix = "" if i == 0 else f"{pad}: "
205
+ lines.append(f"{prefix}{cond} ?\n{inner_pad}{true_str}")
206
+ lines.append(f"{pad}: {_fmt_expr(final, indent + w, w)}")
207
+ return "\n".join(lines)
208
+
209
+
141
210
  def _fmt_expr(expr, indent: int, w: int) -> str:
142
211
  """Format an expression with indent-aware layout for ternary, assert, and echo."""
143
212
  pad = " " * indent
144
213
  if isinstance(expr, TernaryOp):
214
+ # Right-chain of ternaries → flat cascade format
215
+ if isinstance(expr.false_expr, TernaryOp):
216
+ return _fmt_ternary_chain(expr, indent, w)
145
217
  pad2 = " " * (indent + 2)
146
218
  def _fmt_branch(branch):
147
219
  # Nested ternary: 2 spaces per nesting level
@@ -157,9 +229,13 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
157
229
  )
158
230
  if isinstance(expr, AssertOp):
159
231
  args = ", ".join(str(a) for a in expr.arguments)
232
+ if isinstance(expr.body, UndefinedLiteral):
233
+ return f"assert({args})"
160
234
  return f"assert({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
161
235
  if isinstance(expr, EchoOp):
162
236
  args = ", ".join(str(a) for a in expr.arguments)
237
+ if isinstance(expr.body, UndefinedLiteral):
238
+ return f"echo({args})"
163
239
  return f"echo({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
164
240
  if isinstance(expr, LetOp):
165
241
  inner_pad = " " * (indent + w)
@@ -171,7 +247,7 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
171
247
  f"{pad}{_fmt_expr(expr.body, indent, w)}"
172
248
  )
173
249
  assigns = ", ".join(formatted)
174
- return f"let({assigns})\n{pad}{_fmt_expr(expr.body, indent, w)}"
250
+ return f"let({assigns})\n{inner_pad}{_fmt_expr(expr.body, indent + w, w)}"
175
251
  if isinstance(expr, PrimaryCall):
176
252
  inline = str(expr)
177
253
  if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
@@ -301,12 +377,24 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
301
377
  return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
302
378
 
303
379
  if isinstance(node, ModularEcho):
304
- args = _join_str(node.arguments)
305
- return f"{pad}{prefix}echo({args})" + _fmt_child(node.children, indent, w)
380
+ head = f"{pad}{prefix}echo"
381
+ inline = f"{head}({_join_str(node.arguments)})"
382
+ if len(inline) > _MULTILINE_CHAR_LIMIT:
383
+ call = _fmt_multiline_args(head, node.arguments, indent, w,
384
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w))
385
+ else:
386
+ call = inline
387
+ return call + _fmt_child(node.children, indent, w)
306
388
 
307
389
  if isinstance(node, ModularAssert):
308
- args = _join_str(node.arguments)
309
- return f"{pad}{prefix}assert({args})" + _fmt_child(node.children, indent, w)
390
+ head = f"{pad}{prefix}assert"
391
+ inline = f"{head}({_join_str(node.arguments)})"
392
+ if len(inline) > _MULTILINE_CHAR_LIMIT:
393
+ call = _fmt_multiline_args(head, node.arguments, indent, w,
394
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w))
395
+ else:
396
+ call = inline
397
+ return call + _fmt_child(node.children, indent, w)
310
398
 
311
399
  if isinstance(node, ModularIf):
312
400
  header = f"{pad}{prefix}if ({node.condition})"