vondb 3.0.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.
- von/__init__.py +3 -0
- von/__main__.py +15 -0
- von/api.py +67 -0
- von/clipboard.py +21 -0
- von/controller/__init__.py +46 -0
- von/controller/add.py +224 -0
- von/controller/asy.py +46 -0
- von/controller/br.py +22 -0
- von/controller/cd.py +18 -0
- von/controller/clear.py +7 -0
- von/controller/edit.py +45 -0
- von/controller/index.py +9 -0
- von/controller/link.py +22 -0
- von/controller/markup.py +91 -0
- von/controller/nuke.py +18 -0
- von/controller/paths.py +10 -0
- von/controller/po.py +289 -0
- von/controller/preview.py +20 -0
- von/controller/search.py +120 -0
- von/controller/show.py +71 -0
- von/controller/solve.py +76 -0
- von/controller/status.py +9 -0
- von/fzf.py +33 -0
- von/model.py +489 -0
- von/puid.py +250 -0
- von/rc.py +131 -0
- von/strparse.py +104 -0
- von/term.py +117 -0
- von/termcolors.py +28 -0
- von/view.py +243 -0
- vondb-3.0.0.dist-info/METADATA +230 -0
- vondb-3.0.0.dist-info/RECORD +35 -0
- vondb-3.0.0.dist-info/WHEEL +4 -0
- vondb-3.0.0.dist-info/entry_points.txt +2 -0
- vondb-3.0.0.dist-info/licenses/LICENSE.txt +373 -0
von/__init__.py
ADDED
von/__main__.py
ADDED
von/api.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# For importing von externally
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from . import model
|
|
5
|
+
from .puid import inferPUID
|
|
6
|
+
|
|
7
|
+
sys.modules["model"] = model
|
|
8
|
+
|
|
9
|
+
index = model.VonIndex().store # get the underlying dict
|
|
10
|
+
|
|
11
|
+
source_to_puid_lookup = {inferPUID(source): source for source in index}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def has(source: str) -> bool:
|
|
15
|
+
"""Checks whether a given source exists in database"""
|
|
16
|
+
return source in index
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def has_solution(source: str) -> bool:
|
|
20
|
+
"""Checks whether a given source exists in database AND has a solution"""
|
|
21
|
+
if not has(source):
|
|
22
|
+
return False
|
|
23
|
+
entry = index[source]
|
|
24
|
+
return len(entry.full.bodies) > 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_index(source: str, brave=False) -> model.PickleMappingEntry | None:
|
|
28
|
+
"""Returns the index entry for a given source, or None"""
|
|
29
|
+
entry = index.get(source)
|
|
30
|
+
|
|
31
|
+
if entry is None:
|
|
32
|
+
puid = source.upper()
|
|
33
|
+
for index_source, index_entry in index.items():
|
|
34
|
+
if puid == inferPUID(index_source):
|
|
35
|
+
entry = index_entry
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
if entry is not None and entry.secret:
|
|
39
|
+
assert brave, f"{entry} requires security clearance"
|
|
40
|
+
return entry
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get(source: str, brave=False) -> model.Problem:
|
|
44
|
+
"""Returns the full data for a given source"""
|
|
45
|
+
entry = get_index(source, brave)
|
|
46
|
+
assert entry is not None, f"{source} is None"
|
|
47
|
+
return entry.full
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_statement(source: str, brave=False) -> str:
|
|
51
|
+
"""Returns just the problem statement for a given source"""
|
|
52
|
+
return get(source, brave).bodies[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_solution(source: str, brave=False) -> str:
|
|
56
|
+
"""Returns just the solution for a given source (asserts existence)"""
|
|
57
|
+
bodies = get(source, brave).bodies
|
|
58
|
+
assert len(bodies) > 1, f"{source} has no solution"
|
|
59
|
+
return bodies[1]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_puid(source: str) -> str:
|
|
63
|
+
return inferPUID(source)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_source(puid: str) -> str | None:
|
|
67
|
+
return source_to_puid_lookup.get(puid)
|
von/clipboard.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import pyperclip
|
|
3
|
+
except ModuleNotFoundError:
|
|
4
|
+
PYPERCLIP_AVAILABLE = False
|
|
5
|
+
pyperclip = None
|
|
6
|
+
else:
|
|
7
|
+
PYPERCLIP_AVAILABLE = True
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_clipboard() -> str:
|
|
11
|
+
if PYPERCLIP_AVAILABLE:
|
|
12
|
+
assert pyperclip is not None
|
|
13
|
+
return pyperclip.paste().strip()
|
|
14
|
+
else:
|
|
15
|
+
return ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_clipboard(s: str):
|
|
19
|
+
if PYPERCLIP_AVAILABLE:
|
|
20
|
+
assert pyperclip is not None
|
|
21
|
+
pyperclip.copy(s)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from . import ( # NOQA
|
|
2
|
+
add,
|
|
3
|
+
asy,
|
|
4
|
+
br,
|
|
5
|
+
cd,
|
|
6
|
+
clear,
|
|
7
|
+
edit,
|
|
8
|
+
index,
|
|
9
|
+
link,
|
|
10
|
+
markup,
|
|
11
|
+
nuke,
|
|
12
|
+
paths,
|
|
13
|
+
po,
|
|
14
|
+
search,
|
|
15
|
+
show,
|
|
16
|
+
solve,
|
|
17
|
+
status,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VonController:
|
|
22
|
+
def do_ls(self, argv: list[str]):
|
|
23
|
+
self.do_cd(["."] + argv)
|
|
24
|
+
|
|
25
|
+
do_add = add.main
|
|
26
|
+
do_asy = asy.main
|
|
27
|
+
do_br = br.main
|
|
28
|
+
do_cd = cd.main
|
|
29
|
+
do_clear = clear.main
|
|
30
|
+
do_cs = do_cd
|
|
31
|
+
do_edit = edit.main
|
|
32
|
+
do_f = show.main # alias find
|
|
33
|
+
do_link = link.main
|
|
34
|
+
do_m = markup.main
|
|
35
|
+
do_markup = markup.main
|
|
36
|
+
do_index = index.main
|
|
37
|
+
do_nuke = nuke.main
|
|
38
|
+
do_paths = paths.main
|
|
39
|
+
do_po = po.main
|
|
40
|
+
do_s = search.main
|
|
41
|
+
do_search = search.main
|
|
42
|
+
do_show = show.main
|
|
43
|
+
do_solve = solve.main
|
|
44
|
+
do_solve = solve.main
|
|
45
|
+
do_ss = status.main
|
|
46
|
+
do_status = status.main
|
von/controller/add.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import traceback
|
|
8
|
+
from argparse import Namespace
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from yaml.scanner import ScannerError
|
|
13
|
+
|
|
14
|
+
from .. import model, view
|
|
15
|
+
from ..clipboard import get_clipboard
|
|
16
|
+
from ..puid import inferPUID
|
|
17
|
+
from ..rc import EDITOR, NSEPARATOR, SEPARATOR, TAG_HINT_TEXT
|
|
18
|
+
from . import preview
|
|
19
|
+
|
|
20
|
+
# https://urlregex.com/
|
|
21
|
+
RE_URL = re.compile(
|
|
22
|
+
r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def user_file_input(
|
|
27
|
+
initial="",
|
|
28
|
+
extension=".tmp",
|
|
29
|
+
pre_hook: Callable[[str], None] = None,
|
|
30
|
+
delete: bool = False,
|
|
31
|
+
):
|
|
32
|
+
"""Opens in $EDITOR a file with content 'initial'
|
|
33
|
+
and 'extension', and returns edited file.
|
|
34
|
+
If pre_hook is not None, runs pre_hook(tf.name) before opening EDITOR.
|
|
35
|
+
If delete is True, delete the file afterwards.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as tf:
|
|
39
|
+
tf.write(initial.encode())
|
|
40
|
+
filename = tf.name
|
|
41
|
+
if pre_hook is not None:
|
|
42
|
+
pre_hook(filename)
|
|
43
|
+
subprocess.run([EDITOR, filename])
|
|
44
|
+
|
|
45
|
+
with open(filename, "r") as tf:
|
|
46
|
+
edited_message = "".join(_ for _ in tf.readlines())
|
|
47
|
+
if delete:
|
|
48
|
+
os.unlink(filename)
|
|
49
|
+
return edited_message
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def alert_error_tryagain(message=""):
|
|
53
|
+
"""Prints an error message and waits for user to confirm."""
|
|
54
|
+
logging.error(message)
|
|
55
|
+
return input("** Press enter to continue: ")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
PS_INSTRUCT = r"""% Input your problem and solution below.
|
|
59
|
+
% Three dashes on a newline indicate the breaking points.
|
|
60
|
+
% URL detected: {url}
|
|
61
|
+
% TEX root = {preview_path}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def solicit_user_for_content(raw_text: str, url: str, opts: Namespace):
|
|
66
|
+
del opts
|
|
67
|
+
initial = (
|
|
68
|
+
PS_INSTRUCT.format(
|
|
69
|
+
url=url if url != "<++>" else "None",
|
|
70
|
+
preview_path=preview.VON_PREVIEW_PATH,
|
|
71
|
+
)
|
|
72
|
+
+ NSEPARATOR
|
|
73
|
+
+ raw_text
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def pre_hook(tempfile_name: str):
|
|
77
|
+
preview.make_preview(tempfile_name)
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
# TODO maybe give user instructions
|
|
81
|
+
raw_ps = user_file_input(
|
|
82
|
+
initial=initial, extension="von.tex", pre_hook=pre_hook
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if raw_ps.count(SEPARATOR) >= 1:
|
|
86
|
+
bodies = [_.strip() for _ in raw_ps.split(SEPARATOR)[1:]]
|
|
87
|
+
if bodies[0] == "":
|
|
88
|
+
return None
|
|
89
|
+
return bodies
|
|
90
|
+
elif raw_ps.strip() == "":
|
|
91
|
+
return None
|
|
92
|
+
else:
|
|
93
|
+
alert_error_tryagain("Bad format: can't find separator. Try again.")
|
|
94
|
+
initial = raw_ps
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
DEFAULT_PATH = model.getcwd()
|
|
98
|
+
YAML_DATA_FILE = """# Input your problem metadata here
|
|
99
|
+
|
|
100
|
+
source: {source} # e.g. USAMO 2000/6. This must be unique
|
|
101
|
+
desc: <++> # e.g. Fiendish inequality
|
|
102
|
+
path: {path}<++>
|
|
103
|
+
tags: [{now.year}-{now.month:02d}, <++>]
|
|
104
|
+
url: {url}
|
|
105
|
+
|
|
106
|
+
{hint}"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def solicit_user_for_yaml(opts: Namespace, url: str) -> None | tuple[str, Any]:
|
|
110
|
+
if clipboard_text := get_clipboard():
|
|
111
|
+
if RE_URL.fullmatch(clipboard_text) is not None:
|
|
112
|
+
url = clipboard_text
|
|
113
|
+
|
|
114
|
+
initial = YAML_DATA_FILE.format(
|
|
115
|
+
path=model.completePath(DEFAULT_PATH),
|
|
116
|
+
now=datetime.datetime.now(),
|
|
117
|
+
source="<++>" if opts.source is None else opts.source,
|
|
118
|
+
hint=TAG_HINT_TEXT,
|
|
119
|
+
url=url,
|
|
120
|
+
)
|
|
121
|
+
while True:
|
|
122
|
+
raw_yaml = user_file_input(initial=initial, extension="von.yaml", delete=True)
|
|
123
|
+
try:
|
|
124
|
+
d = yaml.safe_load(raw_yaml)
|
|
125
|
+
if d is None:
|
|
126
|
+
return None
|
|
127
|
+
assert "path" in d, "Path is mandatory"
|
|
128
|
+
assert "source" in d, "Source is mandatory"
|
|
129
|
+
if d["path"][-1] != "/":
|
|
130
|
+
d["path"] += "/"
|
|
131
|
+
assert os.path.isdir(d["path"]), d["path"] + " directory non-existent"
|
|
132
|
+
target = d["path"] + inferPUID(d["source"]) + ".tex"
|
|
133
|
+
assert not os.path.isfile(target), target + " already taken"
|
|
134
|
+
assert model.getEntryByKey(d["source"]) is None, (
|
|
135
|
+
d["source"] + " is already an existing problem source"
|
|
136
|
+
)
|
|
137
|
+
except AssertionError:
|
|
138
|
+
traceback.print_exc()
|
|
139
|
+
alert_error_tryagain("Assertions failed, please try again.")
|
|
140
|
+
initial = raw_yaml
|
|
141
|
+
except ScannerError:
|
|
142
|
+
traceback.print_exc()
|
|
143
|
+
alert_error_tryagain("Could not parse YAML, please try again.")
|
|
144
|
+
initial = raw_yaml
|
|
145
|
+
else:
|
|
146
|
+
del d["path"]
|
|
147
|
+
# darn PyYAML used to do this fine -_-
|
|
148
|
+
tags = d.pop("tags")
|
|
149
|
+
output = (
|
|
150
|
+
yaml.dump(d, default_flow_style=False).strip()
|
|
151
|
+
+ "\n"
|
|
152
|
+
+ "tags: ["
|
|
153
|
+
+ ", ".join(tags)
|
|
154
|
+
+ "]"
|
|
155
|
+
)
|
|
156
|
+
return (target, output)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def do_add_problem(raw_text: str, url: str, opts: Namespace):
|
|
160
|
+
"""Core procedure. Opens two instances of editors to solicit user input
|
|
161
|
+
on problem and produce a problem instance."""
|
|
162
|
+
|
|
163
|
+
# Get problem and solution
|
|
164
|
+
bodies = solicit_user_for_content(raw_text, url, opts)
|
|
165
|
+
if bodies is None:
|
|
166
|
+
logging.warning("Aborting due to empty input...")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
yaml_info = solicit_user_for_yaml(opts, url)
|
|
170
|
+
if yaml_info is None:
|
|
171
|
+
logging.warning("Aborting due to empty input...")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
target, out_yaml = yaml_info
|
|
175
|
+
out_text = NSEPARATOR.join([out_yaml] + bodies)
|
|
176
|
+
p = model.addProblemByFileContents(target, out_text)
|
|
177
|
+
assert p is not None
|
|
178
|
+
model.augmentCache(p.entry)
|
|
179
|
+
view.printEntry(p.entry)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
parser = view.Parser(prog="add", description="Adds a problem to VON.")
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"source",
|
|
185
|
+
default=None,
|
|
186
|
+
nargs="?",
|
|
187
|
+
help="If specified, sets the source for the new problem.",
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"-f",
|
|
191
|
+
"--file",
|
|
192
|
+
dest="filename",
|
|
193
|
+
default=None,
|
|
194
|
+
help="If specified, uses contents of file as body",
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"-u",
|
|
198
|
+
"--url",
|
|
199
|
+
default=None,
|
|
200
|
+
help="Specify a URL on the command line",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(self: object, argv: list[str]):
|
|
205
|
+
del self
|
|
206
|
+
opts = parser.process(argv)
|
|
207
|
+
opts.verbose = True
|
|
208
|
+
url = opts.url if opts.url is not None else "<++>"
|
|
209
|
+
if opts.filename is not None:
|
|
210
|
+
if not os.path.isfile(opts.filename):
|
|
211
|
+
logging.error("The file " + opts.filename + " doesn't exist")
|
|
212
|
+
return
|
|
213
|
+
with open(opts.filename) as f:
|
|
214
|
+
initial_text = "".join(f.readlines())
|
|
215
|
+
else:
|
|
216
|
+
if clipboard_text := get_clipboard():
|
|
217
|
+
if RE_URL.fullmatch(clipboard_text) is not None and opts.url is not None:
|
|
218
|
+
initial_text = "<++>"
|
|
219
|
+
url = clipboard_text
|
|
220
|
+
else:
|
|
221
|
+
initial_text = clipboard_text
|
|
222
|
+
else:
|
|
223
|
+
initial_text = "<++>"
|
|
224
|
+
do_add_problem(initial_text, url, opts)
|
von/controller/asy.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from .. import model, view
|
|
5
|
+
|
|
6
|
+
parser = view.Parser(
|
|
7
|
+
prog="asy", description="Prints the diagram from the solution to a problem"
|
|
8
|
+
)
|
|
9
|
+
parser.add_argument("key", help="The key of the problem to open")
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-n",
|
|
12
|
+
"--number",
|
|
13
|
+
default=1,
|
|
14
|
+
type=int,
|
|
15
|
+
help="Takes the n-th diagram, one-indexed. Defaults to 1.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-b",
|
|
19
|
+
"--body",
|
|
20
|
+
default=1,
|
|
21
|
+
type=int,
|
|
22
|
+
help="Takes the diagram from the b'th body. Defaults to 1.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-c",
|
|
26
|
+
"--comments",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="Decides whether or not commented lines should be displayed",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(self: object, argv: list[str]):
|
|
33
|
+
opts = parser.process(argv)
|
|
34
|
+
entry = model.getEntryByKey(opts.key)
|
|
35
|
+
if entry is None:
|
|
36
|
+
logging.error(opts.key + " not found")
|
|
37
|
+
else:
|
|
38
|
+
problem = entry.full
|
|
39
|
+
soln = problem.bodies[opts.body]
|
|
40
|
+
asys = re.findall(r"\\begin\{asy\}(.+?)\\end\{asy\}", soln, flags=re.DOTALL)
|
|
41
|
+
assert opts.number >= 1
|
|
42
|
+
diagram = asys[opts.number - 1]
|
|
43
|
+
|
|
44
|
+
if not opts.comments:
|
|
45
|
+
diagram = re.sub(r"/\*.*?\*/", "", diagram, flags=re.DOTALL)
|
|
46
|
+
print("\n".join(line.strip() for line in diagram.strip().split("\n")))
|
von/controller/br.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
from .. import model, view
|
|
6
|
+
|
|
7
|
+
parser = view.Parser(prog="br", description="Browse a problem on AoPS")
|
|
8
|
+
parser.add_argument("key", help="The key of the problem to open")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(self: object, argv: list[str]):
|
|
12
|
+
opts = parser.process(argv)
|
|
13
|
+
entry = model.getEntryByKey(opts.key)
|
|
14
|
+
if entry is None:
|
|
15
|
+
logging.error(opts.key + " not found")
|
|
16
|
+
else:
|
|
17
|
+
url = entry.url
|
|
18
|
+
if url is None:
|
|
19
|
+
print("No URL is provided for this problem")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
print(url)
|
|
22
|
+
webbrowser.open(url)
|
von/controller/cd.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .. import model, view
|
|
4
|
+
|
|
5
|
+
parser = view.Parser(prog="cd", description="Changes the working directory.")
|
|
6
|
+
parser.add_argument("path", help="The path to change to")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(self: object, argv: list[str]):
|
|
10
|
+
opts = parser.process(argv)
|
|
11
|
+
os.chdir(opts.path)
|
|
12
|
+
|
|
13
|
+
if not opts.quiet:
|
|
14
|
+
entries, dirs = model.viewDirectory(model.getCompleteCwd())
|
|
15
|
+
for d in dirs:
|
|
16
|
+
view.printDir(d)
|
|
17
|
+
for e in entries:
|
|
18
|
+
view.printEntry(e)
|
von/controller/clear.py
ADDED
von/controller/edit.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from .. import model, view
|
|
5
|
+
from ..fzf import fzf_choose
|
|
6
|
+
from ..rc import EDITOR
|
|
7
|
+
from . import preview
|
|
8
|
+
|
|
9
|
+
parser = view.Parser(prog="edit", description="Opens problem(s) by source name.")
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-a",
|
|
12
|
+
"--all",
|
|
13
|
+
action="store_true",
|
|
14
|
+
dest="all",
|
|
15
|
+
help="Edit all problems in cache",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"keys",
|
|
19
|
+
nargs="*",
|
|
20
|
+
help="The key of the problem to open (either source or cache index).",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(self: object, argv: list[str]):
|
|
25
|
+
opts = parser.process(argv)
|
|
26
|
+
opts.verbose = True
|
|
27
|
+
|
|
28
|
+
if opts.all is True:
|
|
29
|
+
entries = model.readCache()
|
|
30
|
+
elif len(opts.keys) == 0:
|
|
31
|
+
entries = [model.getEntryByKey(fzf_choose())]
|
|
32
|
+
else:
|
|
33
|
+
entries = [model.getEntryByKey(key) for key in opts.keys]
|
|
34
|
+
for entry in entries:
|
|
35
|
+
if entry is None:
|
|
36
|
+
logging.error("Not found")
|
|
37
|
+
else:
|
|
38
|
+
full_path = model.completePath(entry.path)
|
|
39
|
+
preview.make_preview(full_path)
|
|
40
|
+
subprocess.call([EDITOR, full_path]) # do editing
|
|
41
|
+
|
|
42
|
+
problem = model.makeProblemFromPath(entry.path)
|
|
43
|
+
new_entry = model.updateEntryByProblem(old_entry=entry, new_problem=problem)
|
|
44
|
+
# update cache after editing problem
|
|
45
|
+
view.printEntry(new_entry)
|
von/controller/index.py
ADDED
von/controller/link.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .. import model, view
|
|
5
|
+
from ..clipboard import set_clipboard
|
|
6
|
+
|
|
7
|
+
parser = view.Parser(prog="link", description="Pastes the URL to clipboard")
|
|
8
|
+
parser.add_argument("key", help="The key of the problem to open")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(self: object, argv: list[str]):
|
|
12
|
+
opts = parser.process(argv)
|
|
13
|
+
entry = model.getEntryByKey(opts.key)
|
|
14
|
+
if entry is None:
|
|
15
|
+
logging.error(opts.key + " not found")
|
|
16
|
+
else:
|
|
17
|
+
url = entry.url
|
|
18
|
+
if url is None:
|
|
19
|
+
print("No URL is provided for this problem")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
print(url)
|
|
22
|
+
set_clipboard(url)
|
von/controller/markup.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import model, strparse, view
|
|
4
|
+
from ..fzf import fzf_choose
|
|
5
|
+
|
|
6
|
+
parser = view.Parser(
|
|
7
|
+
prog="markup",
|
|
8
|
+
description="Gives a Markdown version of problem statement for copy-pasting in email etc.",
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"keys",
|
|
12
|
+
nargs="*",
|
|
13
|
+
help="The key(S) of the problem to open.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"-p",
|
|
17
|
+
"--preserve",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Suppress macro expansion from body.",
|
|
20
|
+
)
|
|
21
|
+
parser_display_modes = parser.add_mutually_exclusive_group()
|
|
22
|
+
|
|
23
|
+
parser_display_modes.add_argument(
|
|
24
|
+
"-a",
|
|
25
|
+
"--aops",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Returns in AoPS mode.",
|
|
28
|
+
)
|
|
29
|
+
parser_display_modes.add_argument(
|
|
30
|
+
"-d",
|
|
31
|
+
"--discord",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="Returns in Discord mode.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(self: object, argv: list[str]):
|
|
38
|
+
opts = parser.process(argv)
|
|
39
|
+
keys = opts.keys
|
|
40
|
+
if len(keys) == 0:
|
|
41
|
+
keys = [fzf_choose()]
|
|
42
|
+
|
|
43
|
+
for i, key in enumerate(keys):
|
|
44
|
+
entry = model.getEntryByKey(key)
|
|
45
|
+
|
|
46
|
+
if entry is None:
|
|
47
|
+
logging.error(opts.key + " not found")
|
|
48
|
+
return
|
|
49
|
+
elif entry.secret and not opts.brave:
|
|
50
|
+
logging.error("Problem can't be shown without --brave option")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
problem = entry.full
|
|
54
|
+
statement = problem.bodies[0]
|
|
55
|
+
|
|
56
|
+
if opts.aops is True:
|
|
57
|
+
header = "[b]"
|
|
58
|
+
if problem.url is not None:
|
|
59
|
+
header += f"[url={problem.url}]{entry.source}[/url]"
|
|
60
|
+
else:
|
|
61
|
+
header += entry.source
|
|
62
|
+
if problem.author is not None:
|
|
63
|
+
header += f", proposed by {problem.author}.[/b]"
|
|
64
|
+
else:
|
|
65
|
+
header += ".[/b]"
|
|
66
|
+
view.out(header)
|
|
67
|
+
view.out(strparse.toAOPS(statement))
|
|
68
|
+
if i != len(keys) - 1:
|
|
69
|
+
view.out("\n--------\n")
|
|
70
|
+
else:
|
|
71
|
+
header = "## "
|
|
72
|
+
if problem.url is not None:
|
|
73
|
+
header += f"[{problem.source}]({problem.url})"
|
|
74
|
+
else:
|
|
75
|
+
header += entry.source
|
|
76
|
+
if problem.author is not None:
|
|
77
|
+
header += f", proposed by {problem.author}."
|
|
78
|
+
else:
|
|
79
|
+
header += "."
|
|
80
|
+
view.out(header)
|
|
81
|
+
if opts.discord is True:
|
|
82
|
+
view.out(r"```latex")
|
|
83
|
+
else:
|
|
84
|
+
view.out("")
|
|
85
|
+
view.out(
|
|
86
|
+
statement if opts.preserve is True else strparse.demacro(statement)
|
|
87
|
+
)
|
|
88
|
+
if opts.discord is True:
|
|
89
|
+
view.out(r"```")
|
|
90
|
+
elif i != len(keys) - 1:
|
|
91
|
+
view.out("\n")
|
von/controller/nuke.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .. import model, view
|
|
4
|
+
from ..puid import inferPUID
|
|
5
|
+
from ..rc import VON_BASE_PATH
|
|
6
|
+
|
|
7
|
+
parser = view.Parser(prog="nuke", description="Fixes all the filenames to match PUID")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(self: object, argv: list[str]):
|
|
11
|
+
parser.process(argv)
|
|
12
|
+
for p in model.getAllProblems():
|
|
13
|
+
puid = inferPUID(p.source)
|
|
14
|
+
src = VON_BASE_PATH / Path(p.path)
|
|
15
|
+
target = VON_BASE_PATH / src.parent / f"{puid}.tex"
|
|
16
|
+
if not target.exists():
|
|
17
|
+
src.rename(target)
|
|
18
|
+
model.rebuildIndex()
|