Skip to content

Commit 7dc03bd

Browse files
authored
feat: title_case word_separator and separate separator stage ops (#7)
title_case gains the word_separator kwarg (parser, converter, engine). Ruby parity: upcase the first character of every line, then of every character following the separator; an empty separator leaves only the per-line casing. separate becomes a real engine op (join every codepoint with the separator, default space) instead of an unsupported construct. Fixes an infinite parse loop the kwargs path exposed: an unrecognized token on the last line skipped to EOF, where _skip_line no longer advances — the stage loop then spun. The kwarg peek now skips inline whitespace first.
1 parent f5e4bbd commit 7dc03bd

4 files changed

Lines changed: 79 additions & 2 deletions

File tree

‎src/interscript/engine.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,25 @@ def _run_op(self, op: dict, text: str) -> str:
164164
if self._loader is None:
165165
raise ExecutionError(f"run {op['map']!r}: no map loader configured")
166166
return self._loader(target).transliterate(text)
167+
if kind == "separate":
168+
separator = op.get("separator", " ")
169+
return separator.join(text)
170+
if kind == "title_case":
171+
# Ruby parity: upcase the first character of every line and
172+
# of every segment following the word separator.
173+
sep = op.get("word_separator", " ")
174+
out = "\n".join(
175+
line[:1].upper() + line[1:] for line in text.split("\n")
176+
)
177+
if sep != "":
178+
out = re.sub(
179+
re.escape(sep) + "(.)",
180+
lambda m: m.group(1).upper(),
181+
out,
182+
)
183+
return out
184+
if kind == "titlecase":
185+
return text.title()
167186
if kind == "downcase":
168187
return text.lower()
169188
if kind == "upcase":

‎src/interscript/isc.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,15 @@ def _stage(self) -> dict:
318318
body.append({"kind": "funcall", "name": "rababa", "kwargs": kwargs})
319319
elif keyword in _FUNCTIONS:
320320
self._consume(keyword)
321-
body.append({"kind": "string_case", "op": keyword})
321+
item = {"kind": "string_case", "op": keyword}
322+
self._skip_inline_ws()
323+
if keyword == "title_case" and self._peek_word() == "word_separator":
324+
self._consume("word_separator")
325+
self._skip_ws()
326+
self._expect(":")
327+
self._skip_ws()
328+
item["word_separator"] = self._item_atom()["value"]
329+
body.append(item)
322330
elif keyword == "sub":
323331
body.append({"kind": "bare_rule", "rule": self._rule()})
324332
else:
@@ -811,7 +819,16 @@ def _stage_tree(
811819
if kind in ("compose", "decompose"):
812820
return {"kind": kind}
813821
if kind == "string_case":
814-
return {"kind": body_item["op"]}
822+
op = {"kind": body_item["op"]}
823+
if "word_separator" in body_item:
824+
op["word_separator"] = body_item["word_separator"]
825+
return op
826+
if kind == "separate":
827+
sep = {"kind": "separate"}
828+
separator = body_item.get("separator")
829+
if separator and separator.get("type") == "string":
830+
sep["separator"] = separator["value"]
831+
return sep
815832
raise UnsupportedConstruct(f"stage item {kind}")
816833

817834

‎tests/test_engine.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,29 @@ def test_real_greek_map_dependency_runs():
8989
cases = engine.tree["tests"]
9090
passed = sum(1 for src, want in cases if engine.transliterate(src) == want)
9191
assert passed >= 200, f"regression: {passed}/{len(cases)}"
92+
93+
94+
def test_engine_separate_and_title_case():
95+
import tempfile, os, sys
96+
from interscript import add_load_path, transliterate
97+
98+
d = tempfile.mkdtemp()
99+
open(os.path.join(d, "ops.isc"), "w").write(
100+
'system "ops" {\n'
101+
' stage main {\n'
102+
" separate separator \"|\"\n"
103+
" }\n"
104+
"}\n"
105+
)
106+
add_load_path(d)
107+
assert transliterate("ops", "こんいちは") == "こ|ん|い|ち|は"
108+
109+
open(os.path.join(d, "tc.isc"), "w").write(
110+
'system "tc" {\n'
111+
' stage main {\n'
112+
" title_case word_separator: \"\"\n"
113+
" }\n"
114+
"}\n"
115+
)
116+
assert transliterate("tc", "hello world") == "Hello world"
117+
assert transliterate("tc", "hello world\nhello hello") == "Hello world\nHello hello"

‎tests/test_isc.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,18 @@ def test_imported_stage_run_resolves_to_dependency():
177177
"""
178178
tree = isc_to_tree(src)
179179
assert tree["stages"][0]["children"][0] == {"kind": "run", "map": "imp"}
180+
181+
182+
def test_separate_stage_op():
183+
src = 'system "x" { stage main { separate separator "|" } }'
184+
tree = isc_to_tree(src)
185+
assert tree["stages"][0]["children"][0] == {"kind": "separate", "separator": "|"}
186+
187+
188+
def test_title_case_word_separator():
189+
src = 'system "x" { stage main { title_case word_separator: "" } }'
190+
tree = isc_to_tree(src)
191+
assert tree["stages"][0]["children"][0] == {
192+
"kind": "title_case",
193+
"word_separator": "",
194+
}

0 commit comments

Comments
 (0)