Skip to content

Commit 705a5eb

Browse files
Format template expressions and prepare for LSP (#5)
* Add formatting for expressions within template and prepare for being used LSP * Rename tests files for clarity and split formatter into template * Update `__init__` calls in Components to new `init` hook * Fix encoding
1 parent 5d87e8b commit 705a5eb

13 files changed

Lines changed: 1546 additions & 297 deletions

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ruff-cgx"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
description = "Ruff linter and formatter for collagraph single-file components"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -39,6 +39,7 @@ select = [
3939
"T10", # flake8-debugger
4040
"T20", # flake8-print
4141
"RUF", # ruff
42+
"ARG", # flake8-unused-arguments
4243
]
4344
unfixable = [
4445
"F401", # Unused import

ruff_cgx/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
from .formatter import format_file
2-
from .linter import lint_file
3-
4-
__all__ = (
1+
from .formatter import (
2+
format_cgx_content,
53
format_file,
4+
)
5+
from .linter import (
6+
lint_cgx_content,
67
lint_file,
78
)
9+
10+
__all__ = (
11+
"format_cgx_content",
12+
"format_file",
13+
"lint_cgx_content",
14+
"lint_file",
15+
)

ruff_cgx/formatter.py

Lines changed: 128 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,55 @@
11
import logging
2-
import os
3-
import subprocess
4-
import tempfile
52
from pathlib import Path
63

7-
from collagraph.sfc.parser import CGXParser
8-
94
from .template_formatter import format_template
5+
from .utils import get_script_range, parse_cgx_file, run_ruff_format
106

117
logger = logging.getLogger(__name__)
128

139

14-
def format_script(path, script_node, source_lines, check):
15-
"""
16-
Returns formatted source and the original location of the node
10+
def format_script(script_node, source_lines):
1711
"""
18-
start, end = script_node.location[0], script_node.end[0] - 1
12+
Format script section (CLI version with printing).
1913
14+
Returns formatted source and the original location of the node.
15+
"""
16+
start, end = get_script_range(script_node)
2017
source = "".join(source_lines[start:end])
2118

22-
ruff_command = ["ruff", "format"]
23-
if check:
24-
ruff_command.append("--check")
25-
26-
# Write source to a temp file
27-
with tempfile.TemporaryDirectory() as directory:
28-
target_file = Path(directory) / "source.py"
29-
target_file.write_text(source)
30-
ruff_command.append(str(target_file))
31-
32-
# Enable color output for ruff
33-
# TODO: check whether we want to force color or not?
34-
env = os.environ.copy()
35-
env["CLICOLOR_FORCE"] = "1"
36-
# Then run ruff to format the temp file
37-
output = subprocess.run(ruff_command, capture_output=True, text=True, env=env)
38-
with target_file.open(mode="r", encoding="utf-8") as fh:
39-
formatted_source = fh.readlines()
40-
41-
stdout = output.stdout.replace(str(target_file), str(path))
42-
43-
print(stdout, end="") # noqa: T201
44-
if check:
45-
if output.returncode:
46-
exit(output.returncode)
19+
# Format using ruff
20+
formatted_source = run_ruff_format(source, check=False)
4721

48-
# Then return the contents
49-
return formatted_source, (start, end)
22+
# Convert back to lines for consistency
23+
formatted_lines = formatted_source.splitlines(keepends=True)
24+
25+
return formatted_lines, (start, end)
5026

5127

5228
def format_file(path, check=False, write=True):
5329
"""
5430
Format CGX files (the contents of the script tag) with ruff.
5531
56-
Returns 0 if everything succeeded, or nothing changed.
57-
Returns 1 when running check and something would change.
58-
Returns list of lines when write is set to False instead
59-
of an error code (used for the test-suite).
32+
Args:
33+
content: The CGX file content as a string
34+
uri: Optional URI for logging purposes
35+
36+
Returns:
37+
0 if everything succeeded, or nothing changed.
38+
1 when running check and something would change.
39+
list of lines when write is set to False instead
40+
of an error code (used for the test-suite).
6041
"""
6142
path = Path(path)
6243
if path.suffix != ".cgx":
6344
return
6445

65-
parser = CGXParser()
66-
parser.feed(path.read_text())
67-
68-
with path.open(mode="r") as fh:
69-
lines = fh.readlines()
46+
content = path.read_text(encoding="utf-8")
47+
parsed = parse_cgx_file(content)
7048

71-
script_node = parser.root.child_with_tag("script")
72-
template_nodes = [
73-
node
74-
for node in parser.root.children
75-
if not hasattr(node, "tag") or node.tag != "script"
76-
]
49+
lines = content.splitlines(keepends=True)
7750

78-
script_content, script_location = format_script(path, script_node, lines, check)
79-
formatted_template_nodes = [
80-
format_template(node, lines, parser=parser) for node in template_nodes
81-
]
51+
script_content, script_location = format_script(parsed.script_node, lines)
52+
formatted_template_nodes = [format_template(node) for node in parsed.template_nodes]
8253

8354
changed_script = lines[script_location[0] : script_location[1]] != script_content
8455
changed_template = any(
@@ -91,8 +62,9 @@ def format_file(path, check=False, write=True):
9162
changed = changed_script or changed_template or needs_newline_at_end_of_file
9263
if check:
9364
if changed:
94-
logger.warning(f"Would change: {path}")
65+
print(f"Would reformat: {path}") # noqa: T201
9566
return 1
67+
print("1 file already formatted") # noqa: T201
9668
return 0
9769

9870
formatted_parts = reversed(
@@ -111,10 +83,109 @@ def format_file(path, check=False, write=True):
11183
if needs_newline_at_end_of_file:
11284
lines.append("\n")
11385

86+
# Print status message based on changes
87+
if changed:
88+
print("1 file reformatted") # noqa: T201
89+
else:
90+
print("1 file left unchanged") # noqa: T201
91+
11492
if not write:
93+
# For testing, return the lines instead of writing
11594
return lines
11695

11796
if changed:
118-
with path.open(mode="w") as fh:
97+
with path.open(mode="w", encoding="utf-8") as fh:
11998
fh.writelines(lines)
99+
120100
return 0
101+
102+
103+
def format_cgx_content(content: str, uri: str = "") -> str:
104+
"""
105+
Format CGX file content (for LSP use).
106+
107+
Args:
108+
content: The CGX file content as a string
109+
uri: Optional URI for logging purposes
110+
111+
Returns:
112+
Formatted content as a string
113+
"""
114+
try:
115+
# Parse the CGX file
116+
parsed = parse_cgx_file(content)
117+
118+
# Split content into lines
119+
lines = content.splitlines(keepends=True)
120+
121+
# Check for script node
122+
if not parsed.script_node:
123+
logger.warning(f"Missing script node in {uri}")
124+
return content
125+
126+
# Format script section (using updated signature for LSP)
127+
script_content, script_location = format_script_content(
128+
parsed.script_node, lines
129+
)
130+
131+
# Format all template nodes
132+
formatted_template_nodes = [
133+
format_template(node) for node in parsed.template_nodes
134+
]
135+
136+
# Check if newline is needed at end of file
137+
needs_newline_at_end_of_file = lines and not lines[-1].endswith("\n")
138+
139+
# Sort all formatted parts by their starting location
140+
# (in reverse order for replacement)
141+
formatted_parts = reversed(
142+
sorted(
143+
[
144+
(script_content, script_location),
145+
*formatted_template_nodes,
146+
],
147+
key=lambda x: x[1][0],
148+
)
149+
)
150+
151+
# Replace content in reverse order to maintain correct line indices
152+
for formatted_content, (start, end) in formatted_parts:
153+
lines[start:end] = formatted_content
154+
155+
# Add newline at end if needed
156+
if needs_newline_at_end_of_file:
157+
lines.append("\n")
158+
159+
return "".join(lines)
160+
161+
except Exception as e:
162+
logger.error(f"Error formatting {uri}: {e}", exc_info=True)
163+
# Return original content on error
164+
return content
165+
166+
167+
def format_script_content(script_node, source_lines):
168+
"""
169+
Format the script section of a CGX file using ruff (for LSP use).
170+
171+
Args:
172+
script_node: script node from collagraph's parser
173+
source_lines: contents of source file as list of lines
174+
175+
Returns:
176+
Formatted source and the original location of the node.
177+
"""
178+
if script_node.end is None:
179+
raise RuntimeError("Invalid script node: no end")
180+
181+
start, end = get_script_range(script_node)
182+
source = "".join(source_lines[start:end])
183+
184+
# Format using ruff
185+
formatted_source = run_ruff_format(source)
186+
187+
# Convert to lines for consistency
188+
formatted_lines = formatted_source.splitlines(keepends=True)
189+
190+
# Return the formatted content and its location
191+
return formatted_lines, (start, end)

0 commit comments

Comments
 (0)