openscad-parser 2.3.3__tar.gz → 2.4.0__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,3 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: openscad_parser
3
+ Version: 2.4.0
4
+ Summary: A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
5
+ Keywords: openscad,openscad parser,parser
6
+ Author: Revar Desmera
7
+ Author-email: Revar Desmera <revarbat@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Manufacturing
13
+ Classifier: Operating System :: MacOS :: MacOS X
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Artistic Software
21
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
22
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Dist: arpeggio>=2.0.3
26
+ Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
28
+ Requires-Dist: pyyaml>=6.0 ; extra == 'dev'
29
+ Requires-Dist: genbadge[coverage]>=1.1.0 ; extra == 'dev'
30
+ Requires-Dist: pyyaml>=6.0 ; extra == 'yaml'
31
+ Maintainer: Revar Desmera
32
+ Maintainer-email: Revar Desmera <revarbat@gmail.com>
33
+ Requires-Python: >=3.11
34
+ Project-URL: Bug Tracker, https://github.com/belfryscad/openscad_parser/issues
35
+ Project-URL: Documentation, https://github.com/belfryscad/openscad_parser/WRITING_DOCS.md
36
+ Project-URL: Homepage, https://github.com/belfryscad/openscad_parser
37
+ Project-URL: Releases, https://github.com/belfryscad/openscad_parser/releases
38
+ Project-URL: Repository, https://github.com/belfryscad/openscad_parser
39
+ Project-URL: Usage, https://github.com/belfryscad/openscad_parser/README.rst
40
+ Provides-Extra: dev
41
+ Provides-Extra: yaml
42
+ Description-Content-Type: text/x-rst
43
+
1
44
  OpenSCAD Parser
2
45
  ===============
3
46
 
@@ -22,6 +65,8 @@ Features
22
65
  - AST tree can contain comment nodes (single-line and multi-line)
23
66
  - AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
24
67
  - JSON and YAML serialization/deserialization of AST trees
68
+ - Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``)
69
+ - Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
25
70
 
26
71
  Installation
27
72
  ------------
@@ -512,6 +557,13 @@ Main Functions
512
557
  - ``scope.lookup_module(name)`` — search this scope and its parents
513
558
  - ``scope.parent`` — the enclosing scope (``None`` for root)
514
559
 
560
+ ``to_openscad(nodes: list[ASTNode], indent_width: int = 4)``
561
+ Convert a list of AST nodes to formatted OpenSCAD source code.
562
+
563
+ :param nodes: Top-level AST nodes as returned by the ``getAST*`` functions.
564
+ :param indent_width: Spaces per indentation level (default: 4).
565
+ :returns: Formatted OpenSCAD source as a string.
566
+
515
567
  Serialization Functions
516
568
  ~~~~~~~~~~~~~~~~~~~~~~~
517
569
 
@@ -719,6 +771,100 @@ They are also available from ``openscad_parser.ast.serialization``::
719
771
  ast_from_yaml,
720
772
  )
721
773
 
774
+ Pretty-Printing
775
+ ---------------
776
+
777
+ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source code::
778
+
779
+ from openscad_parser.ast import getASTfromString, to_openscad
780
+
781
+ code = "module box(w,h){cube([w,h,1]);}"
782
+ ast = getASTfromString(code)
783
+
784
+ formatted = to_openscad(ast)
785
+ # module box(w, h) {
786
+ # cube([w, h, 1.0]);
787
+ # }
788
+ print(formatted)
789
+
790
+ The pretty-printer normalises whitespace and indentation while preserving the logical
791
+ structure of the code. It supports all AST node types including modules, functions,
792
+ control structures, modifiers, list comprehensions, and comments.
793
+
794
+ ``to_openscad(nodes, indent_width=4)``
795
+ Convert a list of AST nodes to formatted OpenSCAD source.
796
+
797
+ :param nodes: Top-level AST nodes (as returned by ``getAST*`` functions).
798
+ :param indent_width: Spaces per indentation level (default: 4).
799
+ :returns: Formatted OpenSCAD source code as a string.
800
+
801
+ - Blank lines are inserted before and after module/function declarations.
802
+ - Single-child module instantiations are formatted inline; multiple children use a block.
803
+ - Comments are preserved when the AST was parsed with ``include_comments=True``.
804
+
805
+ Controlling indentation::
806
+
807
+ from openscad_parser.ast import getASTfromString, to_openscad
808
+
809
+ ast = getASTfromString("module m() { cube(1); }")
810
+ print(to_openscad(ast, indent_width=2))
811
+ # module m() {
812
+ # cube(1.0);
813
+ # }
814
+
815
+ Command-Line Interface
816
+ -----------------------
817
+
818
+ The ``openscad-parser`` CLI is installed alongside the package::
819
+
820
+ pip install openscad-parser
821
+
822
+ Usage::
823
+
824
+ openscad-parser [OPTIONS] [FILE]
825
+
826
+ Read from a file or ``-`` for stdin. Default output is JSON.
827
+
828
+ **Options:**
829
+
830
+ ``--json``
831
+ Output AST as JSON (default).
832
+
833
+ ``--yaml``
834
+ Output AST as YAML (requires ``pip install openscad_parser[yaml]``).
835
+
836
+ ``--format``
837
+ Output reformatted OpenSCAD source code.
838
+
839
+ ``--indent N``
840
+ Indentation width in spaces (default: 4). Applies to ``--format`` and ``--json``.
841
+
842
+ ``--include-comments``
843
+ Include comment nodes in the output.
844
+
845
+ ``--no-includes``
846
+ Do not expand ``include <...>`` statements; keep ``IncludeStatement`` nodes instead.
847
+
848
+ **Examples:**
849
+
850
+ Dump AST as JSON::
851
+
852
+ openscad-parser model.scad
853
+ openscad-parser - < model.scad # stdin
854
+
855
+ Reformat OpenSCAD source::
856
+
857
+ openscad-parser --format model.scad
858
+ openscad-parser --format --indent 2 model.scad
859
+
860
+ Output YAML::
861
+
862
+ openscad-parser --yaml model.scad
863
+
864
+ Include comments in the AST::
865
+
866
+ openscad-parser --include-comments --json model.scad
867
+
722
868
  Error Handling
723
869
  --------------
724
870
 
@@ -1,43 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: openscad_parser
3
- Version: 2.3.3
4
- Summary: A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
5
- Keywords: openscad,openscad parser,parser
6
- Author: Revar Desmera
7
- Author-email: Revar Desmera <revarbat@gmail.com>
8
- License-Expression: MIT
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Intended Audience :: Manufacturing
13
- Classifier: Operating System :: MacOS :: MacOS X
14
- Classifier: Operating System :: Microsoft :: Windows
15
- Classifier: Operating System :: POSIX
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Topic :: Artistic Software
18
- Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
19
- Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
20
- Classifier: Topic :: Software Development :: Libraries
21
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Requires-Dist: arpeggio>=2.0.3
23
- Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
24
- Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
25
- Requires-Dist: pyyaml>=6.0 ; extra == 'dev'
26
- Requires-Dist: coverage-badge>=1.1.0 ; extra == 'dev'
27
- Requires-Dist: pyyaml>=6.0 ; extra == 'yaml'
28
- Maintainer: Revar Desmera
29
- Maintainer-email: Revar Desmera <revarbat@gmail.com>
30
- Requires-Python: >=3.11
31
- Project-URL: Bug Tracker, https://github.com/belfryscad/openscad_parser/issues
32
- Project-URL: Documentation, https://github.com/belfryscad/openscad_parser/WRITING_DOCS.md
33
- Project-URL: Homepage, https://github.com/belfryscad/openscad_parser
34
- Project-URL: Releases, https://github.com/belfryscad/openscad_parser/releases
35
- Project-URL: Repository, https://github.com/belfryscad/openscad_parser
36
- Project-URL: Usage, https://github.com/belfryscad/openscad_parser/README.rst
37
- Provides-Extra: dev
38
- Provides-Extra: yaml
39
- Description-Content-Type: text/x-rst
40
-
41
1
  OpenSCAD Parser
42
2
  ===============
43
3
 
@@ -62,6 +22,8 @@ Features
62
22
  - AST tree can contain comment nodes (single-line and multi-line)
63
23
  - AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
64
24
  - JSON and YAML serialization/deserialization of AST trees
25
+ - Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``)
26
+ - Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
65
27
 
66
28
  Installation
67
29
  ------------
@@ -552,6 +514,13 @@ Main Functions
552
514
  - ``scope.lookup_module(name)`` — search this scope and its parents
553
515
  - ``scope.parent`` — the enclosing scope (``None`` for root)
554
516
 
517
+ ``to_openscad(nodes: list[ASTNode], indent_width: int = 4)``
518
+ Convert a list of AST nodes to formatted OpenSCAD source code.
519
+
520
+ :param nodes: Top-level AST nodes as returned by the ``getAST*`` functions.
521
+ :param indent_width: Spaces per indentation level (default: 4).
522
+ :returns: Formatted OpenSCAD source as a string.
523
+
555
524
  Serialization Functions
556
525
  ~~~~~~~~~~~~~~~~~~~~~~~
557
526
 
@@ -759,6 +728,100 @@ They are also available from ``openscad_parser.ast.serialization``::
759
728
  ast_from_yaml,
760
729
  )
761
730
 
731
+ Pretty-Printing
732
+ ---------------
733
+
734
+ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source code::
735
+
736
+ from openscad_parser.ast import getASTfromString, to_openscad
737
+
738
+ code = "module box(w,h){cube([w,h,1]);}"
739
+ ast = getASTfromString(code)
740
+
741
+ formatted = to_openscad(ast)
742
+ # module box(w, h) {
743
+ # cube([w, h, 1.0]);
744
+ # }
745
+ print(formatted)
746
+
747
+ The pretty-printer normalises whitespace and indentation while preserving the logical
748
+ structure of the code. It supports all AST node types including modules, functions,
749
+ control structures, modifiers, list comprehensions, and comments.
750
+
751
+ ``to_openscad(nodes, indent_width=4)``
752
+ Convert a list of AST nodes to formatted OpenSCAD source.
753
+
754
+ :param nodes: Top-level AST nodes (as returned by ``getAST*`` functions).
755
+ :param indent_width: Spaces per indentation level (default: 4).
756
+ :returns: Formatted OpenSCAD source code as a string.
757
+
758
+ - Blank lines are inserted before and after module/function declarations.
759
+ - Single-child module instantiations are formatted inline; multiple children use a block.
760
+ - Comments are preserved when the AST was parsed with ``include_comments=True``.
761
+
762
+ Controlling indentation::
763
+
764
+ from openscad_parser.ast import getASTfromString, to_openscad
765
+
766
+ ast = getASTfromString("module m() { cube(1); }")
767
+ print(to_openscad(ast, indent_width=2))
768
+ # module m() {
769
+ # cube(1.0);
770
+ # }
771
+
772
+ Command-Line Interface
773
+ -----------------------
774
+
775
+ The ``openscad-parser`` CLI is installed alongside the package::
776
+
777
+ pip install openscad-parser
778
+
779
+ Usage::
780
+
781
+ openscad-parser [OPTIONS] [FILE]
782
+
783
+ Read from a file or ``-`` for stdin. Default output is JSON.
784
+
785
+ **Options:**
786
+
787
+ ``--json``
788
+ Output AST as JSON (default).
789
+
790
+ ``--yaml``
791
+ Output AST as YAML (requires ``pip install openscad_parser[yaml]``).
792
+
793
+ ``--format``
794
+ Output reformatted OpenSCAD source code.
795
+
796
+ ``--indent N``
797
+ Indentation width in spaces (default: 4). Applies to ``--format`` and ``--json``.
798
+
799
+ ``--include-comments``
800
+ Include comment nodes in the output.
801
+
802
+ ``--no-includes``
803
+ Do not expand ``include <...>`` statements; keep ``IncludeStatement`` nodes instead.
804
+
805
+ **Examples:**
806
+
807
+ Dump AST as JSON::
808
+
809
+ openscad-parser model.scad
810
+ openscad-parser - < model.scad # stdin
811
+
812
+ Reformat OpenSCAD source::
813
+
814
+ openscad-parser --format model.scad
815
+ openscad-parser --format --indent 2 model.scad
816
+
817
+ Output YAML::
818
+
819
+ openscad-parser --yaml model.scad
820
+
821
+ Include comments in the AST::
822
+
823
+ openscad-parser --include-comments --json model.scad
824
+
762
825
  Error Handling
763
826
  --------------
764
827
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_parser"
7
- version = "2.3.3"
7
+ version = "2.4.0"
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 = [
@@ -24,6 +24,9 @@ classifiers = [
24
24
  "Operating System :: Microsoft :: Windows",
25
25
  "Operating System :: POSIX",
26
26
  "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
27
30
  "Topic :: Artistic Software",
28
31
  "Topic :: Multimedia :: Graphics :: 3D Modeling",
29
32
  "Topic :: Multimedia :: Graphics :: 3D Rendering",
@@ -35,12 +38,15 @@ dependencies = [
35
38
  "arpeggio>=2.0.3",
36
39
  ]
37
40
 
41
+ [project.scripts]
42
+ openscad-parser = "openscad_parser.cli:main"
43
+
38
44
  [project.optional-dependencies]
39
45
  dev = [
40
46
  "pytest>=7.0.0",
41
47
  "pytest-cov>=7.1.0",
42
48
  "PyYAML>=6.0",
43
- "coverage-badge>=1.1.0",
49
+ "genbadge[coverage]>=1.1.0",
44
50
  ]
45
51
  yaml = [
46
52
  "PyYAML>=6.0",
@@ -87,6 +87,9 @@ from .builder import ASTBuilderVisitor, Position
87
87
  # Import scope classes
88
88
  from .scope import Scope, build_scopes
89
89
 
90
+ # Import pretty-printer
91
+ from .pretty_print import to_openscad
92
+
90
93
  # Import serialization functions
91
94
  from .serialization import (
92
95
  ast_to_dict,
@@ -333,7 +333,7 @@ class RangeLiteral(Primary):
333
333
  step: Expression
334
334
 
335
335
  def __str__(self):
336
- return f"{self.start}:{self.end}:{self.step}"
336
+ return f"[{self.start}:{self.end}:{self.step}]"
337
337
 
338
338
  def build_scope(self, parent_scope: "Scope") -> None:
339
339
  self.scope = parent_scope
@@ -1208,7 +1208,7 @@ class ListCompLet(VectorElement):
1208
1208
  body: Expression
1209
1209
 
1210
1210
  def __str__(self):
1211
- return f"let ({self.assignments}) {self.body}"
1211
+ return f"let ({', '.join(str(a) for a in self.assignments)}) {self.body}"
1212
1212
 
1213
1213
  def build_scope(self, parent_scope: "Scope") -> None:
1214
1214
  self.scope = parent_scope
@@ -1267,7 +1267,7 @@ class ListCompFor(VectorElement):
1267
1267
  body: VectorElement
1268
1268
 
1269
1269
  def __str__(self):
1270
- return f"for ({self.assignments}) {self.body}"
1270
+ return f"for ({', '.join(str(a) for a in self.assignments)}) {self.body}"
1271
1271
 
1272
1272
  def build_scope(self, parent_scope: "Scope") -> None:
1273
1273
  self.scope = parent_scope
@@ -1308,7 +1308,7 @@ class ListCompCFor(VectorElement):
1308
1308
  body: VectorElement
1309
1309
 
1310
1310
  def __str__(self):
1311
- return f"for ({self.initial}; {self.condition}; {self.increment}) {self.body}"
1311
+ return f"for ({', '.join(str(a) for a in self.initial)}; {self.condition}; {', '.join(str(a) for a in self.increment)}) {self.body}"
1312
1312
 
1313
1313
  def build_scope(self, parent_scope: "Scope") -> None:
1314
1314
  self.scope = parent_scope
@@ -0,0 +1,169 @@
1
+ """Pretty-printer: convert an OpenSCAD AST back to formatted source code."""
2
+ from __future__ import annotations
3
+ from .nodes import (
4
+ ASTNode, Assignment, FunctionDeclaration, ModuleDeclaration,
5
+ UseStatement, IncludeStatement,
6
+ ModuleInstantiation,
7
+ ModularCall, ModularFor, ModularCFor,
8
+ ModularIntersectionFor, ModularIntersectionCFor,
9
+ ModularLet, ModularEcho, ModularAssert,
10
+ ModularIf, ModularIfElse,
11
+ ModularModifierShowOnly, ModularModifierHighlight,
12
+ ModularModifierBackground, ModularModifierDisable,
13
+ CommentLine, CommentSpan,
14
+ )
15
+
16
+
17
+ def to_openscad(nodes: list[ASTNode], indent_width: int = 4) -> str:
18
+ """Convert a list of AST nodes to formatted OpenSCAD source code.
19
+
20
+ Args:
21
+ nodes: The AST nodes to format (top-level statements).
22
+ indent_width: Number of spaces per indentation level (default: 4).
23
+
24
+ Returns:
25
+ Formatted OpenSCAD source code as a string.
26
+ """
27
+ parts = []
28
+ prev_complex = False
29
+ for node in nodes:
30
+ is_complex = isinstance(node, (ModuleDeclaration, FunctionDeclaration))
31
+ if parts and (is_complex or prev_complex):
32
+ parts.append("")
33
+ parts.append(_fmt_node(node, 0, indent_width))
34
+ prev_complex = is_complex
35
+ return "\n".join(parts)
36
+
37
+
38
+ # --- helpers ---
39
+
40
+ def _as_list(val) -> list:
41
+ if isinstance(val, list):
42
+ return val
43
+ if val is None:
44
+ return []
45
+ return [val]
46
+
47
+
48
+ def _join_str(items) -> str:
49
+ return ", ".join(str(i) for i in items)
50
+
51
+
52
+ def _fmt_node(node: ASTNode, indent: int, w: int) -> str:
53
+ """Format any top-level or block-body node."""
54
+ pad = " " * indent
55
+
56
+ if isinstance(node, CommentLine):
57
+ return f"{pad}//{node.text}"
58
+ if isinstance(node, CommentSpan):
59
+ return f"{pad}/*{node.text}*/"
60
+ if isinstance(node, UseStatement):
61
+ return f"{pad}use <{node.filepath.val}>"
62
+ if isinstance(node, IncludeStatement):
63
+ return f"{pad}include <{node.filepath.val}>"
64
+ if isinstance(node, Assignment):
65
+ return f"{pad}{node.name} = {node.expr};"
66
+ if isinstance(node, FunctionDeclaration):
67
+ params = _join_str(node.parameters)
68
+ return f"{pad}function {node.name}({params}) = {node.expr};"
69
+ if isinstance(node, ModuleDeclaration):
70
+ params = _join_str(node.parameters)
71
+ block = _fmt_block(node.children, indent, w)
72
+ return f"{pad}module {node.name}({params}) {block}"
73
+ if isinstance(node, ModuleInstantiation):
74
+ return _fmt_inst(node, indent, w)
75
+ return f"{pad}{node}"
76
+
77
+
78
+ def _fmt_block(nodes: list, indent: int, w: int) -> str:
79
+ """Format a list of nodes as a braced block."""
80
+ pad = " " * indent
81
+ if not nodes:
82
+ return "{}"
83
+ inner = "\n".join(_fmt_node(n, indent + w, w) for n in nodes)
84
+ return "{\n" + inner + "\n" + pad + "}"
85
+
86
+
87
+ def _fmt_child(body, indent: int, w: int) -> str:
88
+ """Format the child body of a module instantiation.
89
+
90
+ Returns the tail string appended after the header:
91
+ - ``";"`` when there are no children
92
+ - ``"\\n child;"`` for a single inline child
93
+ - ``" {\\n ...\\n}"`` for a block of multiple children
94
+ """
95
+ nodes = _as_list(body)
96
+ pad = " " * indent
97
+
98
+ if not nodes:
99
+ return ";"
100
+ if len(nodes) == 1:
101
+ return "\n" + _fmt_inst(nodes[0], indent + w, w)
102
+ inner = "\n".join(_fmt_inst(n, indent + w, w) for n in nodes)
103
+ return " {\n" + inner + "\n" + pad + "}"
104
+
105
+
106
+ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "") -> str:
107
+ """Format a ModuleInstantiation node.
108
+
109
+ ``prefix`` accumulates modifier characters (``!``, ``#``, ``%``, ``*``)
110
+ so nested modifiers produce e.g. ``!#cube(10);``.
111
+ """
112
+ pad = " " * indent
113
+
114
+ # Modifiers: push prefix down to the wrapped node
115
+ if isinstance(node, ModularModifierShowOnly):
116
+ return _fmt_inst(node.child, indent, w, "!" + prefix)
117
+ if isinstance(node, ModularModifierHighlight):
118
+ return _fmt_inst(node.child, indent, w, "#" + prefix)
119
+ if isinstance(node, ModularModifierBackground):
120
+ return _fmt_inst(node.child, indent, w, "%" + prefix)
121
+ if isinstance(node, ModularModifierDisable):
122
+ return _fmt_inst(node.child, indent, w, "*" + prefix)
123
+
124
+ if isinstance(node, ModularCall):
125
+ args = _join_str(node.arguments)
126
+ return f"{pad}{prefix}{node.name}({args})" + _fmt_child(node.children, indent, w)
127
+
128
+ if isinstance(node, ModularFor):
129
+ assigns = _join_str(_as_list(node.assignments))
130
+ return f"{pad}{prefix}for ({assigns})" + _fmt_child(node.body, indent, w)
131
+
132
+ if isinstance(node, ModularCFor):
133
+ init = _join_str(_as_list(node.initial))
134
+ inc = _join_str(_as_list(node.increment))
135
+ return f"{pad}{prefix}for ({init}; {node.condition}; {inc})" + _fmt_child(node.body, indent, w)
136
+
137
+ if isinstance(node, ModularIntersectionFor):
138
+ assigns = _join_str(_as_list(node.assignments))
139
+ return f"{pad}{prefix}intersection_for ({assigns})" + _fmt_child(node.body, indent, w)
140
+
141
+ if isinstance(node, ModularIntersectionCFor):
142
+ init = _join_str(_as_list(node.initial))
143
+ inc = _join_str(_as_list(node.increment))
144
+ return f"{pad}{prefix}intersection_for ({init}; {node.condition}; {inc})" + _fmt_child(node.body, indent, w)
145
+
146
+ if isinstance(node, ModularLet):
147
+ assigns = _join_str(_as_list(node.assignments))
148
+ return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
149
+
150
+ if isinstance(node, ModularEcho):
151
+ args = _join_str(node.arguments)
152
+ return f"{pad}{prefix}echo({args})" + _fmt_child(node.children, indent, w)
153
+
154
+ if isinstance(node, ModularAssert):
155
+ args = _join_str(node.arguments)
156
+ return f"{pad}{prefix}assert({args})" + _fmt_child(node.children, indent, w)
157
+
158
+ if isinstance(node, ModularIf):
159
+ header = f"{pad}{prefix}if ({node.condition})"
160
+ return header + _fmt_child(node.true_branch, indent, w)
161
+
162
+ if isinstance(node, ModularIfElse):
163
+ header = f"{pad}{prefix}if ({node.condition})"
164
+ true_tail = _fmt_child(node.true_branch, indent, w)
165
+ false_tail = _fmt_child(node.false_branch, indent, w)
166
+ connector = " else" if true_tail.startswith(" {") else f"\n{pad}else"
167
+ return header + true_tail + connector + false_tail
168
+
169
+ return f"{pad}{prefix}{node};"
@@ -0,0 +1,84 @@
1
+ """Command-line interface for openscad_parser."""
2
+ import sys
3
+ import argparse
4
+ from openscad_parser.ast import getASTfromString, getASTfromFile, ast_to_json
5
+ from openscad_parser.ast.pretty_print import to_openscad
6
+
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(
10
+ prog="openscad-parser",
11
+ description=(
12
+ "Parse an OpenSCAD file and dump its AST as JSON (default), "
13
+ "YAML, or reformatted OpenSCAD source."
14
+ ),
15
+ )
16
+ ap.add_argument(
17
+ "file",
18
+ nargs="?",
19
+ metavar="FILE",
20
+ help="OpenSCAD source file to parse. Omit or use '-' to read from stdin.",
21
+ )
22
+ output = ap.add_mutually_exclusive_group()
23
+ output.add_argument(
24
+ "--json", action="store_true", default=True,
25
+ help="Output AST as JSON (default).",
26
+ )
27
+ output.add_argument(
28
+ "--yaml", action="store_true",
29
+ help="Output AST as YAML (requires PyYAML).",
30
+ )
31
+ output.add_argument(
32
+ "--format", action="store_true",
33
+ help="Output reformatted OpenSCAD source code.",
34
+ )
35
+ ap.add_argument(
36
+ "--include-comments", action="store_true",
37
+ help="Include comment nodes in the AST.",
38
+ )
39
+ ap.add_argument(
40
+ "--no-includes", action="store_true",
41
+ help="Do not expand include <...> statements (keeps IncludeStatement nodes).",
42
+ )
43
+ ap.add_argument(
44
+ "--indent", type=int, default=4, metavar="N",
45
+ help="Indentation width in spaces (default: 4). Applies to --format and --json.",
46
+ )
47
+ args = ap.parse_args()
48
+
49
+ # --yaml and --format clear the --json default
50
+ if args.yaml or args.format:
51
+ args.json = False
52
+
53
+ try:
54
+ if args.file is None or args.file == "-":
55
+ code = sys.stdin.read()
56
+ ast = getASTfromString(code, include_comments=args.include_comments)
57
+ else:
58
+ ast = getASTfromFile(
59
+ args.file,
60
+ include_comments=args.include_comments,
61
+ process_includes=not args.no_includes,
62
+ )
63
+ except FileNotFoundError as e:
64
+ print(f"openscad-parser: {e}", file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+ if ast is None:
68
+ sys.exit(1)
69
+
70
+ if args.format:
71
+ print(to_openscad(ast, indent_width=args.indent))
72
+ elif args.yaml:
73
+ try:
74
+ from openscad_parser.ast import ast_to_yaml
75
+ except ImportError:
76
+ print("openscad-parser: --yaml requires PyYAML (pip install openscad_parser[yaml])", file=sys.stderr)
77
+ sys.exit(1)
78
+ print(ast_to_yaml(ast))
79
+ else:
80
+ print(ast_to_json(ast, indent=args.indent))
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()