glyph-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glyph_cli/__init__.py +7 -0
- glyph_cli/__main__.py +5 -0
- glyph_cli/cli.py +269 -0
- glyph_cli/data/vocab.json +545 -0
- glyph_cli/drawing.py +202 -0
- glyph_cli/export.py +28 -0
- glyph_cli/graph.py +147 -0
- glyph_cli/page.py +113 -0
- glyph_cli/script.py +272 -0
- glyph_cli-0.2.0.dist-info/METADATA +94 -0
- glyph_cli-0.2.0.dist-info/RECORD +14 -0
- glyph_cli-0.2.0.dist-info/WHEEL +4 -0
- glyph_cli-0.2.0.dist-info/entry_points.txt +2 -0
- glyph_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
glyph_cli/__init__.py
ADDED
glyph_cli/__main__.py
ADDED
glyph_cli/cli.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""The ``glyph`` command line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .drawing import decode, draw_words, render, render_svg
|
|
14
|
+
from .export import vocabulary_markdown
|
|
15
|
+
from .graph import format_graph, read_graph, to_dot
|
|
16
|
+
from .page import check_symbol, format_page, parse_page
|
|
17
|
+
from .script import NUMBER, GlyphError, Vocabulary
|
|
18
|
+
|
|
19
|
+
ENV_VOCAB = "GLYPH_VOCAB"
|
|
20
|
+
|
|
21
|
+
DESCRIPTION = """\
|
|
22
|
+
glyph: the dictionary and toolkit for the grid script of Ross 128 b.
|
|
23
|
+
|
|
24
|
+
Words are written KIND.WHICH (BODY.OTHER = "your world"), or KIND alone.
|
|
25
|
+
A number is COUNT.<n> (0-511); COUNT alone is zero. "_" is an empty position or an empty word.
|
|
26
|
+
Pages are text files, one band per line: "SYMBOL: node | relation | node".
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
EPILOG = f"""\
|
|
30
|
+
The vocabulary is the bundled one unless --vocab or ${ENV_VOCAB} names a file.
|
|
31
|
+
Run `glyph init` to make an editable copy. Docs: https://interimm.github.io/glyph-cli/
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------- helpers ----------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_input(path: str) -> str:
|
|
39
|
+
if path == "-":
|
|
40
|
+
return sys.stdin.read()
|
|
41
|
+
try:
|
|
42
|
+
return Path(path).read_text(encoding="utf-8")
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
raise GlyphError(f"file not found: {path}") from None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_output(text: str, path: str | None) -> None:
|
|
48
|
+
if path:
|
|
49
|
+
Path(path).write_text(text, encoding="utf-8")
|
|
50
|
+
print(f"wrote {path}", file=sys.stderr)
|
|
51
|
+
else:
|
|
52
|
+
sys.stdout.write(text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def emit_json(data: object) -> None:
|
|
56
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------- commands ----------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cmd_parts(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
63
|
+
if a.json:
|
|
64
|
+
emit_json({"parts": v.to_json()["parts"], "markers": v.markers})
|
|
65
|
+
return 0
|
|
66
|
+
for p in v.parts.values():
|
|
67
|
+
print(f"{p.name:<7} {p.shape[0]} {p.thing:<22} as relation: {p.relation or '—'}")
|
|
68
|
+
print(f"{'':<7} {p.shape[1]}")
|
|
69
|
+
print(f"{'':<7} {p.shape[2]}")
|
|
70
|
+
print("\nRelation markers (the which of a relation word):")
|
|
71
|
+
for m, g in v.markers.items():
|
|
72
|
+
print(f" {m:<7} {g}")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_list(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
77
|
+
entries = [e for e in v.entries if not a.domain or a.domain.lower() in e.domain.lower()]
|
|
78
|
+
if a.json:
|
|
79
|
+
emit_json([dict(e.to_json(), word=str(e.word)) for e in entries])
|
|
80
|
+
return 0
|
|
81
|
+
if not entries:
|
|
82
|
+
print(f'no domain matches "{a.domain}"; domains: {", ".join(v.domains())}', file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
for domain in v.domains():
|
|
85
|
+
rows = [e for e in entries if e.domain == domain]
|
|
86
|
+
if not rows:
|
|
87
|
+
continue
|
|
88
|
+
print(f"\n{domain}")
|
|
89
|
+
for e in rows:
|
|
90
|
+
note = f" ({e.note})" if e.note else ""
|
|
91
|
+
print(f" {str(e.word):<14} {e.gloss}{note}")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_check(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
96
|
+
word = v.parse(a.word)
|
|
97
|
+
e = v.lookup(word)
|
|
98
|
+
if e:
|
|
99
|
+
print(f'yes: {word} = "{e.gloss}" ({e.domain})')
|
|
100
|
+
return 0
|
|
101
|
+
if word.kind == NUMBER:
|
|
102
|
+
print(f"yes: {word} = the number {word.which or 0} (numbers are built in)")
|
|
103
|
+
return 0
|
|
104
|
+
print(f"no: {word} is not in the vocabulary. Literal reading: {v.gloss(word)}")
|
|
105
|
+
same_kind = [x for x in v.entries if x.word.kind == word.kind]
|
|
106
|
+
if same_kind:
|
|
107
|
+
print(" other words of this kind: " + ", ".join(f"{x.word} {x.gloss}" for x in same_kind))
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_find(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
112
|
+
words, parts = v.search(a.text)
|
|
113
|
+
for e in words:
|
|
114
|
+
print(f"{str(e.word):<14} {e.gloss} [{e.domain}]")
|
|
115
|
+
for p in parts:
|
|
116
|
+
print(f"{p.name:<14} part: {p.thing}; as relation: {p.relation or '—'}")
|
|
117
|
+
if not words and not parts:
|
|
118
|
+
print(f'nothing matches "{a.text}"')
|
|
119
|
+
return 1
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_show(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
124
|
+
words = [v.parse(s) for s in a.words]
|
|
125
|
+
print("\n".join(draw_words(v, words, check_symbol(a.symbol))))
|
|
126
|
+
for w in words:
|
|
127
|
+
print(f" {w}: {v.gloss(w)}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_add(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
132
|
+
e = v.add(v.parse(a.word), a.gloss, a.domain, a.note, a.force)
|
|
133
|
+
path = v.save()
|
|
134
|
+
print(f'added {e.word} = "{e.gloss}" to {path}')
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_remove(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
139
|
+
e = v.remove(v.parse(a.word))
|
|
140
|
+
path = v.save()
|
|
141
|
+
print(f'removed {e.word} ("{e.gloss}") from {path}')
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_validate(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
146
|
+
errors = v.validate()
|
|
147
|
+
for x in errors:
|
|
148
|
+
print("error:", x)
|
|
149
|
+
print(f"{len(v.parts)} parts, {len(v.entries)} words, {len(errors)} problems")
|
|
150
|
+
return 1 if errors else 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def cmd_render(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
154
|
+
page = parse_page(read_input(a.file), v)
|
|
155
|
+
if a.svg:
|
|
156
|
+
write_output(render_svg(v, page, cell=a.cell, grid=not a.no_grid), a.output)
|
|
157
|
+
else:
|
|
158
|
+
write_output("\n".join(render(v, page)) + "\n", a.output)
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_graph(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
163
|
+
g = read_graph(v, parse_page(read_input(a.file), v))
|
|
164
|
+
if a.json:
|
|
165
|
+
emit_json(g.to_json())
|
|
166
|
+
elif a.dot:
|
|
167
|
+
sys.stdout.write(to_dot(g))
|
|
168
|
+
else:
|
|
169
|
+
print("\n".join(format_graph(v, g)))
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_decode(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
174
|
+
sys.stdout.write(format_page(decode(v, read_input(a.file))))
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_export(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
179
|
+
write_output(vocabulary_markdown(v, a.title), a.output)
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_init(v: Vocabulary, a: argparse.Namespace) -> int:
|
|
184
|
+
target = Path(a.path)
|
|
185
|
+
if target.exists() and not a.force:
|
|
186
|
+
raise GlyphError(f"{target} already exists; use --force to overwrite")
|
|
187
|
+
v.save(target)
|
|
188
|
+
print(f"wrote {target} ({len(v.entries)} words). Use it with:")
|
|
189
|
+
print(f" export {ENV_VOCAB}={target.resolve()}")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------- parser ----------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
197
|
+
ap = argparse.ArgumentParser(
|
|
198
|
+
prog="glyph", description=DESCRIPTION, epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
199
|
+
)
|
|
200
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
201
|
+
ap.add_argument("--vocab", metavar="PATH", help=f"vocabulary file (default: ${ENV_VOCAB}, else the bundled one)")
|
|
202
|
+
sub = ap.add_subparsers(dest="cmd", required=True, metavar="COMMAND")
|
|
203
|
+
|
|
204
|
+
def add(name: str, func: Callable, help: str) -> argparse.ArgumentParser:
|
|
205
|
+
p = sub.add_parser(name, help=help, description=help)
|
|
206
|
+
p.set_defaults(func=func)
|
|
207
|
+
return p
|
|
208
|
+
|
|
209
|
+
p = add("parts", cmd_parts, "list the parts and the relation markers")
|
|
210
|
+
p.add_argument("--json", action="store_true", help="print JSON")
|
|
211
|
+
p = add("list", cmd_list, "list the vocabulary")
|
|
212
|
+
p.add_argument("--domain", help="only domains containing this text")
|
|
213
|
+
p.add_argument("--json", action="store_true", help="print JSON")
|
|
214
|
+
p = add("check", cmd_check, "is a word in the vocabulary? (exit 1 if not)")
|
|
215
|
+
p.add_argument("word")
|
|
216
|
+
p = add("find", cmd_find, "search meanings and notes")
|
|
217
|
+
p.add_argument("text")
|
|
218
|
+
p = add("show", cmd_show, "draw words")
|
|
219
|
+
p.add_argument("words", nargs="+", metavar="WORD")
|
|
220
|
+
p.add_argument("--symbol", default="+", help="the party's symbol (default +)")
|
|
221
|
+
p = add("add", cmd_add, "add a word to an editable vocabulary")
|
|
222
|
+
p.add_argument("word")
|
|
223
|
+
p.add_argument("gloss", help="its meaning")
|
|
224
|
+
p.add_argument("--domain", default="Unsorted")
|
|
225
|
+
p.add_argument("--note", default="")
|
|
226
|
+
p.add_argument("--force", action="store_true", help="add even if the meaning is already used")
|
|
227
|
+
p = add("remove", cmd_remove, "remove a word from an editable vocabulary")
|
|
228
|
+
p.add_argument("word")
|
|
229
|
+
add("validate", cmd_validate, "check the vocabulary (exit 1 on problems)")
|
|
230
|
+
p = add("render", cmd_render, "draw a page from page source")
|
|
231
|
+
p.add_argument("file", help="page source file, or - for stdin")
|
|
232
|
+
p.add_argument("--svg", action="store_true", help="draw SVG pixels instead of text")
|
|
233
|
+
p.add_argument("--cell", type=int, default=12, help="SVG cell size in pixels (default 12)")
|
|
234
|
+
p.add_argument("--no-grid", action="store_true", help="SVG: leave empty cells undrawn")
|
|
235
|
+
p.add_argument("-o", "--output", help="write to a file instead of stdout")
|
|
236
|
+
p = add("graph", cmd_graph, "read a page as a knowledge graph")
|
|
237
|
+
p.add_argument("file", help="page source file, or - for stdin")
|
|
238
|
+
fmt = p.add_mutually_exclusive_group()
|
|
239
|
+
fmt.add_argument("--json", action="store_true", help="print JSON")
|
|
240
|
+
fmt.add_argument("--dot", action="store_true", help="print Graphviz DOT")
|
|
241
|
+
p = add("decode", cmd_decode, "read a drawing back into page source")
|
|
242
|
+
p.add_argument("file", help="drawing file, or - for stdin")
|
|
243
|
+
p = add("export-md", cmd_export, "print the vocabulary as Markdown")
|
|
244
|
+
p.add_argument("--title", help="add a top-level heading")
|
|
245
|
+
p.add_argument("-o", "--output", help="write to a file instead of stdout")
|
|
246
|
+
p = add("init", cmd_init, "copy the vocabulary to a file you can edit")
|
|
247
|
+
p.add_argument("path", nargs="?", default="vocab.json")
|
|
248
|
+
p.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
249
|
+
return ap
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main(argv: list[str] | None = None) -> int:
|
|
253
|
+
for stream in (sys.stdout, sys.stderr):
|
|
254
|
+
if hasattr(stream, "reconfigure"):
|
|
255
|
+
stream.reconfigure(errors="replace")
|
|
256
|
+
ap = build_parser()
|
|
257
|
+
a = ap.parse_args(argv)
|
|
258
|
+
try:
|
|
259
|
+
vocab = Vocabulary.load(a.vocab or os.environ.get(ENV_VOCAB) or None)
|
|
260
|
+
return a.func(vocab, a)
|
|
261
|
+
except GlyphError as exc:
|
|
262
|
+
print(f"glyph: error: {exc}", file=sys.stderr)
|
|
263
|
+
return 2
|
|
264
|
+
except BrokenPipeError:
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
sys.exit(main())
|