Render json files in a more pretty way
This commit is contained in:
parent
953ba11036
commit
3e7d1703b3
3 changed files with 104 additions and 41 deletions
|
|
@ -29,6 +29,34 @@ class JsonHandler(DictLikeHandler):
|
|||
# As before: ignore original_text and rebuild structurally
|
||||
return self._generate_json_template(role_prefix, parsed)
|
||||
|
||||
JSON_INDENT = 2
|
||||
|
||||
def _leading_indent(self, s: str, idx: int) -> int:
|
||||
"""Return the number of leading spaces on the line containing idx."""
|
||||
line_start = s.rfind("\n", 0, idx) + 1
|
||||
indent = 0
|
||||
while line_start + indent < len(s) and s[line_start + indent] == " ":
|
||||
indent += 1
|
||||
return indent
|
||||
|
||||
def _replace_marker_with_pretty_loop(
|
||||
self,
|
||||
s: str,
|
||||
marker: str,
|
||||
replacement_builder,
|
||||
) -> str:
|
||||
"""
|
||||
Replace a quoted marker with an indentation-aware multiline snippet.
|
||||
`marker` must include the surrounding JSON quotes, e.g. '"__LOOP_SCALAR__...__"'.
|
||||
"""
|
||||
marker_re = re.compile(re.escape(marker))
|
||||
|
||||
def _repl(m: re.Match[str]) -> str:
|
||||
base_indent = self._leading_indent(m.string, m.start())
|
||||
return replacement_builder(base_indent)
|
||||
|
||||
return marker_re.sub(_repl, s)
|
||||
|
||||
def generate_jinja2_template_with_loops(
|
||||
self,
|
||||
parsed: Any,
|
||||
|
|
@ -127,65 +155,94 @@ class JsonHandler(DictLikeHandler):
|
|||
r'"__SCALAR__([a-zA-Z_][a-zA-Z0-9_]*)__"', r"{{ \1 | tojson }}", json_str
|
||||
)
|
||||
|
||||
# Post-process to replace loop markers with actual Jinja loops
|
||||
# Post-process to replace loop markers with actual Jinja loops (indent-aware)
|
||||
for candidate in loop_candidates:
|
||||
collection_var = self.make_var_name(role_prefix, candidate.path)
|
||||
item_var = candidate.loop_var
|
||||
|
||||
if candidate.item_schema == "scalar":
|
||||
# Replace scalar loop marker with Jinja for loop
|
||||
marker = f'"__LOOP_SCALAR__{collection_var}__{item_var}__"'
|
||||
replacement = self._generate_json_scalar_loop(
|
||||
collection_var, item_var, candidate
|
||||
json_str = self._replace_marker_with_pretty_loop(
|
||||
json_str,
|
||||
marker,
|
||||
lambda base, cv=collection_var, iv=item_var, c=candidate: self._generate_json_scalar_loop(
|
||||
cv, iv, c, base
|
||||
),
|
||||
)
|
||||
json_str = json_str.replace(marker, replacement)
|
||||
|
||||
elif candidate.item_schema in ("simple_dict", "nested"):
|
||||
# Replace dict loop marker with Jinja for loop
|
||||
marker = f'"__LOOP_DICT__{collection_var}__{item_var}__"'
|
||||
replacement = self._generate_json_dict_loop(
|
||||
collection_var, item_var, candidate
|
||||
json_str = self._replace_marker_with_pretty_loop(
|
||||
json_str,
|
||||
marker,
|
||||
lambda base, cv=collection_var, iv=item_var, c=candidate: self._generate_json_dict_loop(
|
||||
cv, iv, c, base
|
||||
),
|
||||
)
|
||||
json_str = json_str.replace(marker, replacement)
|
||||
|
||||
return json_str + "\n"
|
||||
|
||||
def _generate_json_scalar_loop(
|
||||
self, collection_var: str, item_var: str, candidate: LoopCandidate
|
||||
self,
|
||||
collection_var: str,
|
||||
item_var: str,
|
||||
candidate: LoopCandidate,
|
||||
base_indent: int,
|
||||
) -> str:
|
||||
"""Generate a Jinja for loop for a scalar list in JSON."""
|
||||
# Use tojson filter to properly handle strings (quotes them) and other types
|
||||
# Include array brackets around the loop
|
||||
return (
|
||||
f"[{{% for {item_var} in {collection_var} %}}"
|
||||
f"{{{{ {item_var} | tojson }}}}"
|
||||
f"{{% if not loop.last %}}, {{% endif %}}"
|
||||
f"{{% endfor %}}]"
|
||||
)
|
||||
|
||||
def _generate_json_dict_loop(
|
||||
self, collection_var: str, item_var: str, candidate: LoopCandidate
|
||||
) -> str:
|
||||
"""Generate a Jinja for loop for a dict list in JSON."""
|
||||
"""Generate an indentation-preserving Jinja for-loop for a scalar JSON list."""
|
||||
if not candidate.items:
|
||||
return "[]"
|
||||
|
||||
# Get first item as template
|
||||
sample_item = candidate.items[0]
|
||||
|
||||
# Build the dict template - use tojson for all values to handle types correctly
|
||||
fields = []
|
||||
for key, value in sample_item.items():
|
||||
if key == "_key":
|
||||
continue
|
||||
# Use tojson filter to properly serialize all types (strings, numbers, booleans)
|
||||
fields.append(f'"{key}": {{{{ {item_var}.{key} | tojson }}}}')
|
||||
|
||||
dict_template = "{" + ", ".join(fields) + "}"
|
||||
inner_indent = base_indent + self.JSON_INDENT
|
||||
inner = " " * inner_indent
|
||||
base = " " * base_indent
|
||||
|
||||
# Put the `{% for %}` at the start of the first item line so we don't emit
|
||||
# a blank line between iterations under default Jinja whitespace settings.
|
||||
return (
|
||||
f"{{% for {item_var} in {collection_var} %}}"
|
||||
f"{dict_template}"
|
||||
f"{{% if not loop.last %}}, {{% endif %}}"
|
||||
f"{{% endfor %}}"
|
||||
f"[\n"
|
||||
f"{{% for {item_var} in {collection_var} %}}{inner}{{{{ {item_var} | tojson }}}}"
|
||||
f"{{% if not loop.last %}},{{% endif %}}\n"
|
||||
f"{{% endfor %}}{base}]"
|
||||
)
|
||||
|
||||
def _generate_json_dict_loop(
|
||||
self,
|
||||
collection_var: str,
|
||||
item_var: str,
|
||||
candidate: LoopCandidate,
|
||||
base_indent: int,
|
||||
) -> str:
|
||||
"""Generate an indentation-preserving Jinja for-loop for a list of dicts in JSON."""
|
||||
if not candidate.items:
|
||||
return "[]"
|
||||
|
||||
# Get first item as template (preserve key order from the sample)
|
||||
sample_item = candidate.items[0]
|
||||
keys = [k for k in sample_item.keys() if k != "_key"]
|
||||
|
||||
inner_indent = base_indent + self.JSON_INDENT # list item indent
|
||||
field_indent = inner_indent + self.JSON_INDENT # dict field indent
|
||||
inner = " " * inner_indent
|
||||
field = " " * field_indent
|
||||
base = " " * base_indent
|
||||
|
||||
# Build a pretty dict body that matches json.dumps(indent=2) style.
|
||||
dict_lines: list[str] = [
|
||||
"{"
|
||||
] # first line has no indent; we prepend `inner` when emitting
|
||||
for i, key in enumerate(keys):
|
||||
comma = "," if i < len(keys) - 1 else ""
|
||||
dict_lines.append(
|
||||
f'{field}"{key}": {{{{ {item_var}.{key} | tojson }}}}{comma}'
|
||||
)
|
||||
# Comma between *items* goes after the closing brace.
|
||||
dict_lines.append(f"{inner}}}{{% if not loop.last %}},{{% endif %}}")
|
||||
dict_body = "\n".join(dict_lines)
|
||||
|
||||
# Put the `{% for %}` at the start of the first item line to avoid blank lines.
|
||||
return (
|
||||
f"[\n"
|
||||
f"{{% for {item_var} in {collection_var} %}}{inner}{dict_body}\n"
|
||||
f"{{% endfor %}}{base}]"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue