mirror of
https://github.com/github/spec-kit.git
synced 2026-07-10 01:33:05 +08:00
* fix(toml): escape control characters so generated command files parse both toml renderers (TomlIntegration._render_toml_string for gemini/tabnine and CommandRegistrar.render_toml_command for extension/preset commands) wrote control characters raw into a multiline or basic string. toml forbids literal control chars (U+0000-U+001F except tab/newline, and U+007F) in every string form, and a bare CR that is not part of a CRLF pair, so a description or body containing one produced a .toml file that fails to parse. route any value with such a character to a fully-escaped basic string that emits the leftover control chars as \uXXXX. added regression tests that round-trip through tomllib. * refactor(toml): centralize control-char escaping in one shared helper the control-char detection and basic-string escaping added for both toml renderers were copy-pasted into agents.py and integrations/base.py. move the two functions into specify_cli/_toml_string.py and have both renderers delegate to it, so the escaping rules can't drift apart later. no behavior change; both renderers now reference the same implementation.
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""Shared TOML string-escaping helpers.
|
||
|
||
Both TOML command renderers — ``TomlIntegration`` (gemini, tabnine) in
|
||
``specify_cli.integrations.base`` and ``CommandRegistrar.render_toml_command``
|
||
(extension/preset commands) in ``specify_cli.agents`` — need the same rules for
|
||
detecting characters TOML forbids literally and for emitting a fully-escaped
|
||
basic string. Keeping one implementation here avoids the two drifting apart if
|
||
the escaping rules change again.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
|
||
def has_illegal_toml_control(value: str) -> bool:
|
||
"""True when *value* contains a character TOML forbids literally.
|
||
|
||
TOML basic/literal strings (single- or multi-line) allow tab and, in the
|
||
multiline forms, newlines — but every other control character
|
||
(``U+0000``–``U+001F`` and ``U+007F``) must be ``\\u``-escaped, which only a
|
||
basic string can do. A bare carriage return counts too: a multiline basic
|
||
string treats ``\\r`` as a newline only when paired into ``\\r\\n``; a lone
|
||
``\\r`` is an illegal control character.
|
||
"""
|
||
length = len(value)
|
||
for i, ch in enumerate(value):
|
||
code = ord(ch)
|
||
if ch == "\r":
|
||
# Only a CR that is part of a CRLF newline is allowed literally.
|
||
if i + 1 < length and value[i + 1] == "\n":
|
||
continue
|
||
return True
|
||
if (code < 0x20 and ch not in ("\t", "\n")) or code == 0x7F:
|
||
return True
|
||
return False
|
||
|
||
|
||
def escape_toml_basic(value: str) -> str:
|
||
"""Render *value* as a single-line basic string, escaping everything.
|
||
|
||
Always valid TOML: backslash/quote are escaped, the common control chars
|
||
use their short escapes, and any remaining control character is emitted as
|
||
a ``\\uXXXX`` sequence.
|
||
"""
|
||
out: list[str] = []
|
||
for ch in value:
|
||
code = ord(ch)
|
||
if ch == "\\":
|
||
out.append("\\\\")
|
||
elif ch == '"':
|
||
out.append('\\"')
|
||
elif ch == "\n":
|
||
out.append("\\n")
|
||
elif ch == "\r":
|
||
out.append("\\r")
|
||
elif ch == "\t":
|
||
out.append("\\t")
|
||
elif code < 0x20 or code == 0x7F:
|
||
out.append(f"\\u{code:04x}")
|
||
else:
|
||
out.append(ch)
|
||
return '"' + "".join(out) + '"'
|