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
6
debian/changelog
vendored
6
debian/changelog
vendored
|
|
@ -1,3 +1,9 @@
|
||||||
|
jinjaturtle (0.3.4) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Render json files in a more pretty way
|
||||||
|
|
||||||
|
-- Miguel Jacq <mig@mig5.net> Sun, 21 Dec 2025 11:20:00 +1100
|
||||||
|
|
||||||
jinjaturtle (0.3.3) unstable; urgency=medium
|
jinjaturtle (0.3.3) unstable; urgency=medium
|
||||||
|
|
||||||
* Fixes for tomli on Ubuntu 22
|
* Fixes for tomli on Ubuntu 22
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jinjaturtle"
|
name = "jinjaturtle"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
description = "Convert config files into Ansible defaults and Jinja2 templates."
|
description = "Convert config files into Ansible defaults and Jinja2 templates."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,34 @@ class JsonHandler(DictLikeHandler):
|
||||||
# As before: ignore original_text and rebuild structurally
|
# As before: ignore original_text and rebuild structurally
|
||||||
return self._generate_json_template(role_prefix, parsed)
|
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(
|
def generate_jinja2_template_with_loops(
|
||||||
self,
|
self,
|
||||||
parsed: Any,
|
parsed: Any,
|
||||||
|
|
@ -127,65 +155,94 @@ class JsonHandler(DictLikeHandler):
|
||||||
r'"__SCALAR__([a-zA-Z_][a-zA-Z0-9_]*)__"', r"{{ \1 | tojson }}", json_str
|
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:
|
for candidate in loop_candidates:
|
||||||
collection_var = self.make_var_name(role_prefix, candidate.path)
|
collection_var = self.make_var_name(role_prefix, candidate.path)
|
||||||
item_var = candidate.loop_var
|
item_var = candidate.loop_var
|
||||||
|
|
||||||
if candidate.item_schema == "scalar":
|
if candidate.item_schema == "scalar":
|
||||||
# Replace scalar loop marker with Jinja for loop
|
|
||||||
marker = f'"__LOOP_SCALAR__{collection_var}__{item_var}__"'
|
marker = f'"__LOOP_SCALAR__{collection_var}__{item_var}__"'
|
||||||
replacement = self._generate_json_scalar_loop(
|
json_str = self._replace_marker_with_pretty_loop(
|
||||||
collection_var, item_var, candidate
|
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"):
|
elif candidate.item_schema in ("simple_dict", "nested"):
|
||||||
# Replace dict loop marker with Jinja for loop
|
|
||||||
marker = f'"__LOOP_DICT__{collection_var}__{item_var}__"'
|
marker = f'"__LOOP_DICT__{collection_var}__{item_var}__"'
|
||||||
replacement = self._generate_json_dict_loop(
|
json_str = self._replace_marker_with_pretty_loop(
|
||||||
collection_var, item_var, candidate
|
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"
|
return json_str + "\n"
|
||||||
|
|
||||||
def _generate_json_scalar_loop(
|
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:
|
) -> str:
|
||||||
"""Generate a Jinja for loop for a scalar list in JSON."""
|
"""Generate an indentation-preserving Jinja for-loop for a scalar JSON list."""
|
||||||
# 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."""
|
|
||||||
if not candidate.items:
|
if not candidate.items:
|
||||||
return "[]"
|
return "[]"
|
||||||
|
|
||||||
# Get first item as template
|
inner_indent = base_indent + self.JSON_INDENT
|
||||||
sample_item = candidate.items[0]
|
inner = " " * inner_indent
|
||||||
|
base = " " * base_indent
|
||||||
# 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) + "}"
|
|
||||||
|
|
||||||
|
# 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 (
|
return (
|
||||||
f"{{% for {item_var} in {collection_var} %}}"
|
f"[\n"
|
||||||
f"{dict_template}"
|
f"{{% for {item_var} in {collection_var} %}}{inner}{{{{ {item_var} | tojson }}}}"
|
||||||
f"{{% if not loop.last %}}, {{% endif %}}"
|
f"{{% if not loop.last %}},{{% endif %}}\n"
|
||||||
f"{{% endfor %}}"
|
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