calkit-python 0.48.1__py3-none-any.whl → 0.48.2__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.
- calkit/cli/latex.py +454 -0
- calkit/git.py +4 -2
- calkit/latex.py +260 -10
- calkit/markdown.py +156 -5
- calkit/models/pipeline.py +104 -1
- calkit/pipeline.py +4 -1
- calkit/resources/devcontainer/devcontainer.json +1 -1
- calkit/resources/vscode/settings.json +1 -1
- calkit/tests/cli/main/test_core.py +15 -0
- calkit/tests/cli/test_latex.py +239 -0
- calkit/tests/test_docx.py +37 -3
- calkit/tests/test_git.py +29 -0
- calkit/tests/test_markdown.py +88 -0
- {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/METADATA +2 -1
- {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/RECORD +31 -31
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/WHEEL +0 -0
- {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/licenses/LICENSE +0 -0
calkit/cli/latex.py
CHANGED
|
@@ -10,6 +10,7 @@ import shutil
|
|
|
10
10
|
import string
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from copy import deepcopy
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
|
|
@@ -189,6 +190,186 @@ def from_questions(
|
|
|
189
190
|
json2latex.dump(command_name, values, f)
|
|
190
191
|
|
|
191
192
|
|
|
193
|
+
@latex_app.command(name="from-markdown")
|
|
194
|
+
def from_markdown(
|
|
195
|
+
md_path: Annotated[
|
|
196
|
+
str, typer.Argument(help="The Markdown file to build.")
|
|
197
|
+
],
|
|
198
|
+
outputs: Annotated[
|
|
199
|
+
list[str],
|
|
200
|
+
typer.Option(
|
|
201
|
+
"--output",
|
|
202
|
+
"-o",
|
|
203
|
+
help="Where to write the PDF, or the LaTeX with a .tex "
|
|
204
|
+
"extension; can be given more than once.",
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
environment: Annotated[
|
|
208
|
+
str | None,
|
|
209
|
+
typer.Option(
|
|
210
|
+
"--environment",
|
|
211
|
+
"-e",
|
|
212
|
+
help="Environment to run pandoc and LaTeX in, e.g., one using "
|
|
213
|
+
"Calkit's LaTeX image, which includes both.",
|
|
214
|
+
),
|
|
215
|
+
] = None,
|
|
216
|
+
template_path: Annotated[
|
|
217
|
+
str | None, typer.Option("--template", help="Pandoc template.")
|
|
218
|
+
] = None,
|
|
219
|
+
filters: Annotated[
|
|
220
|
+
list[str] | None,
|
|
221
|
+
typer.Option(
|
|
222
|
+
"--filter", help="Pandoc Lua filter; can be given more than once."
|
|
223
|
+
),
|
|
224
|
+
] = None,
|
|
225
|
+
pandoc_args: Annotated[
|
|
226
|
+
list[str] | None,
|
|
227
|
+
typer.Option(
|
|
228
|
+
"--pandoc-arg",
|
|
229
|
+
help="Another argument for pandoc; can be given more than once.",
|
|
230
|
+
),
|
|
231
|
+
] = None,
|
|
232
|
+
no_check: Annotated[
|
|
233
|
+
bool,
|
|
234
|
+
typer.Option("--no-check", help="Don't check the environment first."),
|
|
235
|
+
] = False,
|
|
236
|
+
verbose: Annotated[
|
|
237
|
+
bool, typer.Option("--verbose", "-v", help="Print commands.")
|
|
238
|
+
] = False,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Build a Markdown file into a PDF with pandoc and LaTeX.
|
|
241
|
+
|
|
242
|
+
Value markers are filled from their results files, which keeps the
|
|
243
|
+
Markdown itself current too, and each value links to the question on
|
|
244
|
+
the project's hub whose evidence cites it. The template gets the Calkit
|
|
245
|
+
version and the project's URL as the ``calkit-version``,
|
|
246
|
+
``project-url``, and ``project`` variables.
|
|
247
|
+
"""
|
|
248
|
+
import calkit.docx
|
|
249
|
+
import calkit.markdown
|
|
250
|
+
|
|
251
|
+
def question_url(value: calkit.markdown.MarkdownValue) -> str | None:
|
|
252
|
+
# The question whose evidence cites this value, or failing that,
|
|
253
|
+
# the first to cite its file
|
|
254
|
+
if project_url is None:
|
|
255
|
+
return None
|
|
256
|
+
first = None
|
|
257
|
+
for n, q in enumerate(ck_info.get("questions") or [], start=1):
|
|
258
|
+
evidence = q.get("evidence") if isinstance(q, dict) else None
|
|
259
|
+
for ev in evidence or []:
|
|
260
|
+
if not isinstance(ev, dict) or ev.get("path") != value.path:
|
|
261
|
+
continue
|
|
262
|
+
keys = [ev.get("key"), *(ev.get("values") or {}).values()]
|
|
263
|
+
if value.key in keys:
|
|
264
|
+
return f"{project_url}/questions/{n}"
|
|
265
|
+
first = first or n
|
|
266
|
+
return f"{project_url}/questions/{first}" if first else None
|
|
267
|
+
|
|
268
|
+
def to_latex(tex_path: str) -> None:
|
|
269
|
+
# Images are pointed at from where the LaTeX is, by a filter that
|
|
270
|
+
# runs before the others, so they see the paths LaTeX will
|
|
271
|
+
build_dir = os.path.dirname(tex_path) or "."
|
|
272
|
+
os.makedirs(build_dir, exist_ok=True)
|
|
273
|
+
stem = Path(tex_path).stem
|
|
274
|
+
src_path = f"{build_dir}/{stem}.pandoc.md"
|
|
275
|
+
with open(src_path, "w", encoding="utf-8", newline="\n") as f:
|
|
276
|
+
f.write(
|
|
277
|
+
calkit.markdown.prepare_for_pdf(
|
|
278
|
+
text, md_path, link=question_url
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
prefix = Path(os.path.relpath(md_dir, build_dir)).as_posix()
|
|
282
|
+
repoint = f"{build_dir}/{stem}.images.lua"
|
|
283
|
+
with open(repoint, "w", encoding="utf-8", newline="\n") as f:
|
|
284
|
+
f.write(calkit.markdown.image_prefix_filter(prefix))
|
|
285
|
+
args = [src_path, "-o", tex_path, "--standalone"]
|
|
286
|
+
if template_path is not None:
|
|
287
|
+
args += ["--template", template_path]
|
|
288
|
+
for path in [repoint, *(filters or [])]:
|
|
289
|
+
args += ["--lua-filter", path]
|
|
290
|
+
args += [f"--variable=calkit-version:{version}"]
|
|
291
|
+
if project_url is not None:
|
|
292
|
+
args += [
|
|
293
|
+
f"--variable=project-url:{project_url}",
|
|
294
|
+
f"--variable=project:{project_url.split('://', 1)[1]}",
|
|
295
|
+
]
|
|
296
|
+
args += pandoc_args or []
|
|
297
|
+
if environment is not None:
|
|
298
|
+
cmd = _tex_cmd(
|
|
299
|
+
["pandoc", *args], environment, no_check, verbose, dep="pandoc"
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
pandoc = calkit.docx.find_pandoc()
|
|
303
|
+
if pandoc is None:
|
|
304
|
+
raise_error(
|
|
305
|
+
"Pandoc is needed; name an environment that has it, "
|
|
306
|
+
"e.g., one using Calkit's LaTeX image, or install it"
|
|
307
|
+
)
|
|
308
|
+
cmd = [pandoc, *args]
|
|
309
|
+
try:
|
|
310
|
+
subprocess.check_call(cmd)
|
|
311
|
+
except subprocess.CalledProcessError:
|
|
312
|
+
raise_error(f"Pandoc failed to convert {md_path}")
|
|
313
|
+
|
|
314
|
+
for output in outputs:
|
|
315
|
+
if not output.endswith((".pdf", ".tex")):
|
|
316
|
+
raise_error("Outputs must be .pdf or .tex files")
|
|
317
|
+
# Paths go into the build directory's, so they have to be the
|
|
318
|
+
# project's own
|
|
319
|
+
rel = os.path.relpath(md_path)
|
|
320
|
+
if rel == ".." or rel.startswith(".." + os.sep) or os.path.isabs(rel):
|
|
321
|
+
raise_error(f"{md_path} isn't in this project")
|
|
322
|
+
md_path = Path(rel).as_posix()
|
|
323
|
+
md_dir = os.path.dirname(md_path) or "."
|
|
324
|
+
ck_info = calkit.load_calkit_info()
|
|
325
|
+
project_url = None
|
|
326
|
+
if ck_info.get("owner") and ck_info.get("name"):
|
|
327
|
+
hub = str(ck_info.get("hub") or "calkit.io").rstrip("/")
|
|
328
|
+
if "://" not in hub:
|
|
329
|
+
hub = "https://" + hub
|
|
330
|
+
project_url = f"{hub}/{ck_info['owner']}/{ck_info['name']}"
|
|
331
|
+
# A local version's commit is the project's own, which isn't Calkit's
|
|
332
|
+
version = calkit.__version__.split("+")[0]
|
|
333
|
+
with open(md_path, encoding="utf-8") as f:
|
|
334
|
+
text = f.read()
|
|
335
|
+
try:
|
|
336
|
+
text, changed = calkit.markdown.set_values(text, md_path)
|
|
337
|
+
except calkit.markdown.MarkdownParseError as e:
|
|
338
|
+
raise_error(str(e))
|
|
339
|
+
if changed:
|
|
340
|
+
with open(md_path, "w", encoding="utf-8", newline="\n") as f:
|
|
341
|
+
f.write(text)
|
|
342
|
+
for output in outputs:
|
|
343
|
+
if output.endswith(".tex"):
|
|
344
|
+
to_latex(output)
|
|
345
|
+
if not any(o.endswith(".pdf") for o in outputs):
|
|
346
|
+
return
|
|
347
|
+
# A PDF is built where Calkit keeps what it derives from the file, and
|
|
348
|
+
# dated by the file's last commit rather than the build directory's,
|
|
349
|
+
# which has none of its own
|
|
350
|
+
build_dir = Path(".calkit", "markdown", md_path, "pdf").as_posix()
|
|
351
|
+
tex_path = f"{build_dir}/{Path(md_path).stem}.tex"
|
|
352
|
+
to_latex(tex_path)
|
|
353
|
+
cmd = [sys.executable, "-m", "calkit", "latex", "build"]
|
|
354
|
+
if environment is not None:
|
|
355
|
+
cmd += ["-e", environment]
|
|
356
|
+
if no_check:
|
|
357
|
+
cmd += ["--no-check"]
|
|
358
|
+
env = os.environ.copy()
|
|
359
|
+
epoch = calkit.latex.get_source_date_epoch(md_path)
|
|
360
|
+
if epoch is not None:
|
|
361
|
+
env["SOURCE_DATE_EPOCH"] = epoch
|
|
362
|
+
try:
|
|
363
|
+
subprocess.check_call(cmd + [tex_path], env=env)
|
|
364
|
+
except subprocess.CalledProcessError:
|
|
365
|
+
raise_error(f"LaTeX failed to build {tex_path}")
|
|
366
|
+
for output in outputs:
|
|
367
|
+
if output.endswith(".pdf"):
|
|
368
|
+
if os.path.dirname(output):
|
|
369
|
+
os.makedirs(os.path.dirname(output), exist_ok=True)
|
|
370
|
+
shutil.copyfile(Path(tex_path).with_suffix(".pdf"), output)
|
|
371
|
+
|
|
372
|
+
|
|
192
373
|
def _tex_cmd(
|
|
193
374
|
tex_cmd: list[str],
|
|
194
375
|
environment: str | None,
|
|
@@ -2773,6 +2954,12 @@ def merge_docx(
|
|
|
2773
2954
|
and existing.resolved == tc.resolved
|
|
2774
2955
|
):
|
|
2775
2956
|
continue
|
|
2957
|
+
# Word can't carry these, so they come from the source
|
|
2958
|
+
tc.id, tc.issue, tc.attrs = (
|
|
2959
|
+
existing.id,
|
|
2960
|
+
existing.issue,
|
|
2961
|
+
existing.attrs,
|
|
2962
|
+
)
|
|
2776
2963
|
# Word knows neither emails nor what the source already
|
|
2777
2964
|
# recorded, so carry those over for unchanged messages
|
|
2778
2965
|
known = {(e.author, e.text): e for e in existing.entries}
|
|
@@ -2850,3 +3037,270 @@ def merge_docx(
|
|
|
2850
3037
|
f"already there, {counts['pending']} pending, {counts['unplaced']} "
|
|
2851
3038
|
f"unplaced); {added} comments added, {updated} updated"
|
|
2852
3039
|
)
|
|
3040
|
+
|
|
3041
|
+
|
|
3042
|
+
comments_app = typer.Typer(no_args_is_help=True)
|
|
3043
|
+
latex_app.add_typer(
|
|
3044
|
+
comments_app,
|
|
3045
|
+
name="comments",
|
|
3046
|
+
help="Work with review comments in LaTeX source.",
|
|
3047
|
+
)
|
|
3048
|
+
|
|
3049
|
+
_ID_OPTION = typer.Option("--id", help="ID of the thread.")
|
|
3050
|
+
_LINE_OPTION = typer.Option(
|
|
3051
|
+
"--line", "-l", help="Number of any line in the thread."
|
|
3052
|
+
)
|
|
3053
|
+
|
|
3054
|
+
|
|
3055
|
+
def _new_entry(
|
|
3056
|
+
text: str, author: str | None, email: str | None
|
|
3057
|
+
) -> calkit.latex.Entry:
|
|
3058
|
+
import datetime
|
|
3059
|
+
|
|
3060
|
+
import git
|
|
3061
|
+
|
|
3062
|
+
if author is None:
|
|
3063
|
+
try:
|
|
3064
|
+
author = git.Git().config("--get", "user.name").strip()
|
|
3065
|
+
email = email or git.Git().config("--get", "user.email").strip()
|
|
3066
|
+
except git.GitCommandError:
|
|
3067
|
+
raise_error("Set --author, or Git's user.name")
|
|
3068
|
+
date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
3069
|
+
return calkit.latex.Entry(author, text, email or None, date)
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
def _edit_comment(
|
|
3073
|
+
tex_path: str,
|
|
3074
|
+
id: str | None,
|
|
3075
|
+
line: int | None,
|
|
3076
|
+
edit: Callable[[calkit.latex.TexComment], None],
|
|
3077
|
+
) -> calkit.latex.TexComment:
|
|
3078
|
+
"""Apply an edit to one thread and write the file back, giving the
|
|
3079
|
+
thread an ID if it had none."""
|
|
3080
|
+
if (id is None) == (line is None):
|
|
3081
|
+
raise_error("Specify one of --id or --line")
|
|
3082
|
+
if not os.path.isfile(tex_path):
|
|
3083
|
+
raise_error(f"{tex_path} does not exist")
|
|
3084
|
+
lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
|
|
3085
|
+
tc = calkit.latex.find_comment(lines, id=id, lineno=line)
|
|
3086
|
+
if tc is None:
|
|
3087
|
+
raise_error(f"No comment thread in {tex_path} at {id or line}")
|
|
3088
|
+
edit(tc)
|
|
3089
|
+
tc.id = tc.id or calkit.latex.new_comment_id()
|
|
3090
|
+
calkit.latex.write_comment(lines, tc)
|
|
3091
|
+
Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
|
|
3092
|
+
return tc
|
|
3093
|
+
|
|
3094
|
+
|
|
3095
|
+
@comments_app.command(name="list")
|
|
3096
|
+
def list_comments(
|
|
3097
|
+
tex_path: Annotated[
|
|
3098
|
+
str,
|
|
3099
|
+
typer.Argument(
|
|
3100
|
+
help="Document to list comments from, including files it inputs."
|
|
3101
|
+
),
|
|
3102
|
+
],
|
|
3103
|
+
unresolved: Annotated[
|
|
3104
|
+
bool,
|
|
3105
|
+
typer.Option("--unresolved", "-u", help="Only list open threads."),
|
|
3106
|
+
] = False,
|
|
3107
|
+
as_json: Annotated[
|
|
3108
|
+
bool, typer.Option("--json", help="Output result as JSON.")
|
|
3109
|
+
] = False,
|
|
3110
|
+
) -> None:
|
|
3111
|
+
"""List the comment threads in a LaTeX document."""
|
|
3112
|
+
if not os.path.isfile(tex_path):
|
|
3113
|
+
raise_error(f"{tex_path} does not exist")
|
|
3114
|
+
out = [
|
|
3115
|
+
c
|
|
3116
|
+
for c in calkit.latex.list_comments(tex_path)
|
|
3117
|
+
if not (unresolved and c["resolved"])
|
|
3118
|
+
]
|
|
3119
|
+
if as_json:
|
|
3120
|
+
typer.echo(json.dumps(out, indent=2))
|
|
3121
|
+
return
|
|
3122
|
+
for c in out:
|
|
3123
|
+
status = " (resolved)" if c["resolved"] else ""
|
|
3124
|
+
typer.echo(f"{c['path']}:{c['line']} [{c['id'] or '-'}]{status}")
|
|
3125
|
+
if c["highlight"]:
|
|
3126
|
+
typer.echo(f' On "{c["highlight"]["text"]}"')
|
|
3127
|
+
for m in c["messages"]:
|
|
3128
|
+
typer.echo(f" {m['author']}: {m['text']}")
|
|
3129
|
+
|
|
3130
|
+
|
|
3131
|
+
@comments_app.command(name="locate")
|
|
3132
|
+
def locate_paragraph(
|
|
3133
|
+
tex_path: Annotated[
|
|
3134
|
+
str,
|
|
3135
|
+
typer.Argument(help="Document to search, including files it inputs."),
|
|
3136
|
+
],
|
|
3137
|
+
text: Annotated[
|
|
3138
|
+
str | None,
|
|
3139
|
+
typer.Option(
|
|
3140
|
+
"--text", help="Rendered text, e.g., a line copied from the PDF."
|
|
3141
|
+
),
|
|
3142
|
+
] = None,
|
|
3143
|
+
path: Annotated[
|
|
3144
|
+
str | None,
|
|
3145
|
+
typer.Option("--path", help="File with the line to look up."),
|
|
3146
|
+
] = None,
|
|
3147
|
+
line: Annotated[
|
|
3148
|
+
int | None,
|
|
3149
|
+
typer.Option("--line", "-l", help="Line to look up in --path."),
|
|
3150
|
+
] = None,
|
|
3151
|
+
as_json: Annotated[
|
|
3152
|
+
bool, typer.Option("--json", help="Output result as JSON.")
|
|
3153
|
+
] = False,
|
|
3154
|
+
) -> None:
|
|
3155
|
+
"""Find the paragraph some rendered text is in, or a source line is in.
|
|
3156
|
+
|
|
3157
|
+
This is how a place in a PDF built without SyncTeX, or by someone else,
|
|
3158
|
+
can be matched to the source.
|
|
3159
|
+
"""
|
|
3160
|
+
if (text is None) == (line is None):
|
|
3161
|
+
raise_error("Specify one of --text or --path and --line")
|
|
3162
|
+
if not os.path.isfile(tex_path):
|
|
3163
|
+
raise_error(f"{tex_path} does not exist")
|
|
3164
|
+
found = calkit.latex.locate_block(tex_path, text, path, line)
|
|
3165
|
+
if found is None:
|
|
3166
|
+
raise_error("No matching paragraph found")
|
|
3167
|
+
out = {
|
|
3168
|
+
"path": found.path,
|
|
3169
|
+
"line": found.lineno,
|
|
3170
|
+
"end_line": found.lines[-1].lineno,
|
|
3171
|
+
"text": found.text or calkit.latex.display_text(found),
|
|
3172
|
+
}
|
|
3173
|
+
if as_json:
|
|
3174
|
+
typer.echo(json.dumps(out, indent=2))
|
|
3175
|
+
else:
|
|
3176
|
+
typer.echo(f"{out['path']}:{out['line']}")
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
@comments_app.command(name="add")
|
|
3180
|
+
def add_comment(
|
|
3181
|
+
tex_path: Annotated[str, typer.Argument(help="File to comment in.")],
|
|
3182
|
+
line: Annotated[
|
|
3183
|
+
int,
|
|
3184
|
+
typer.Option(
|
|
3185
|
+
"--line",
|
|
3186
|
+
"-l",
|
|
3187
|
+
help="A line in, or just above, the paragraph to comment on.",
|
|
3188
|
+
),
|
|
3189
|
+
],
|
|
3190
|
+
text: Annotated[str, typer.Option("--text", "-m", help="The comment.")],
|
|
3191
|
+
highlight: Annotated[
|
|
3192
|
+
str | None,
|
|
3193
|
+
typer.Option(
|
|
3194
|
+
"--highlight", help="Rendered text in the paragraph it's about."
|
|
3195
|
+
),
|
|
3196
|
+
] = None,
|
|
3197
|
+
occ: Annotated[
|
|
3198
|
+
int,
|
|
3199
|
+
typer.Option(
|
|
3200
|
+
"--occ", help="Which occurrence of the highlight text, from 0."
|
|
3201
|
+
),
|
|
3202
|
+
] = 0,
|
|
3203
|
+
author: Annotated[
|
|
3204
|
+
str | None,
|
|
3205
|
+
typer.Option("--author", help="Defaults to Git's user.name."),
|
|
3206
|
+
] = None,
|
|
3207
|
+
email: Annotated[
|
|
3208
|
+
str | None,
|
|
3209
|
+
typer.Option("--email", help="Defaults to Git's user.email."),
|
|
3210
|
+
] = None,
|
|
3211
|
+
as_json: Annotated[
|
|
3212
|
+
bool, typer.Option("--json", help="Output the thread as JSON.")
|
|
3213
|
+
] = False,
|
|
3214
|
+
) -> None:
|
|
3215
|
+
"""Start a comment thread above a paragraph, printing its ID."""
|
|
3216
|
+
if not os.path.isfile(tex_path):
|
|
3217
|
+
raise_error(f"{tex_path} does not exist")
|
|
3218
|
+
lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
|
|
3219
|
+
tc = calkit.latex.TexComment(
|
|
3220
|
+
[_new_entry(text, author, email)],
|
|
3221
|
+
highlight=highlight or None,
|
|
3222
|
+
highlight_occ=occ,
|
|
3223
|
+
id=calkit.latex.new_comment_id(),
|
|
3224
|
+
)
|
|
3225
|
+
try:
|
|
3226
|
+
calkit.latex.add_comment(lines, line, tc)
|
|
3227
|
+
except ValueError as e:
|
|
3228
|
+
raise_error(str(e))
|
|
3229
|
+
Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
|
|
3230
|
+
if as_json:
|
|
3231
|
+
blks = calkit.latex.file_blocks(tex_path, lines)
|
|
3232
|
+
typer.echo(
|
|
3233
|
+
json.dumps(
|
|
3234
|
+
calkit.latex.comment_to_dict(tex_path, tc, blks), indent=2
|
|
3235
|
+
)
|
|
3236
|
+
)
|
|
3237
|
+
else:
|
|
3238
|
+
typer.echo(tc.id)
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
@comments_app.command(name="reply")
|
|
3242
|
+
def reply_to_comment(
|
|
3243
|
+
tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
|
|
3244
|
+
text: Annotated[str, typer.Option("--text", "-m", help="The reply.")],
|
|
3245
|
+
id: Annotated[str | None, _ID_OPTION] = None,
|
|
3246
|
+
line: Annotated[int | None, _LINE_OPTION] = None,
|
|
3247
|
+
author: Annotated[
|
|
3248
|
+
str | None,
|
|
3249
|
+
typer.Option("--author", help="Defaults to Git's user.name."),
|
|
3250
|
+
] = None,
|
|
3251
|
+
email: Annotated[
|
|
3252
|
+
str | None,
|
|
3253
|
+
typer.Option("--email", help="Defaults to Git's user.email."),
|
|
3254
|
+
] = None,
|
|
3255
|
+
) -> None:
|
|
3256
|
+
"""Reply to a comment thread."""
|
|
3257
|
+
entry = _new_entry(text, author, email)
|
|
3258
|
+
tc = _edit_comment(tex_path, id, line, lambda tc: tc.entries.append(entry))
|
|
3259
|
+
typer.echo(tc.id)
|
|
3260
|
+
|
|
3261
|
+
|
|
3262
|
+
@comments_app.command(name="resolve")
|
|
3263
|
+
def resolve_comment(
|
|
3264
|
+
tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
|
|
3265
|
+
id: Annotated[str | None, _ID_OPTION] = None,
|
|
3266
|
+
line: Annotated[int | None, _LINE_OPTION] = None,
|
|
3267
|
+
) -> None:
|
|
3268
|
+
"""Mark a comment thread resolved."""
|
|
3269
|
+
|
|
3270
|
+
def edit(tc: calkit.latex.TexComment) -> None:
|
|
3271
|
+
tc.resolved = True
|
|
3272
|
+
|
|
3273
|
+
typer.echo(_edit_comment(tex_path, id, line, edit).id)
|
|
3274
|
+
|
|
3275
|
+
|
|
3276
|
+
@comments_app.command(name="reopen")
|
|
3277
|
+
def reopen_comment(
|
|
3278
|
+
tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
|
|
3279
|
+
id: Annotated[str | None, _ID_OPTION] = None,
|
|
3280
|
+
line: Annotated[int | None, _LINE_OPTION] = None,
|
|
3281
|
+
) -> None:
|
|
3282
|
+
"""Mark a resolved comment thread open again."""
|
|
3283
|
+
|
|
3284
|
+
def edit(tc: calkit.latex.TexComment) -> None:
|
|
3285
|
+
tc.resolved = False
|
|
3286
|
+
|
|
3287
|
+
typer.echo(_edit_comment(tex_path, id, line, edit).id)
|
|
3288
|
+
|
|
3289
|
+
|
|
3290
|
+
@comments_app.command(name="delete")
|
|
3291
|
+
def delete_comment(
|
|
3292
|
+
tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
|
|
3293
|
+
id: Annotated[str | None, _ID_OPTION] = None,
|
|
3294
|
+
line: Annotated[int | None, _LINE_OPTION] = None,
|
|
3295
|
+
) -> None:
|
|
3296
|
+
"""Delete a comment thread and its replies."""
|
|
3297
|
+
if (id is None) == (line is None):
|
|
3298
|
+
raise_error("Specify one of --id or --line")
|
|
3299
|
+
if not os.path.isfile(tex_path):
|
|
3300
|
+
raise_error(f"{tex_path} does not exist")
|
|
3301
|
+
lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
|
|
3302
|
+
tc = calkit.latex.find_comment(lines, id=id, lineno=line)
|
|
3303
|
+
if tc is None:
|
|
3304
|
+
raise_error(f"No comment thread in {tex_path} at {id or line}")
|
|
3305
|
+
del lines[tc.lineno - 1 : tc.lineno - 1 + tc.nlines]
|
|
3306
|
+
Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
|
calkit/git.py
CHANGED
|
@@ -131,7 +131,8 @@ def get_staged_files(
|
|
|
131
131
|
repo = get_repo(path)
|
|
132
132
|
cmd = ["--staged", "--name-only"]
|
|
133
133
|
if path is not None:
|
|
134
|
-
|
|
134
|
+
# A staged deletion's path is gone, so Git would read it as a ref
|
|
135
|
+
cmd += ["--", path]
|
|
135
136
|
diff = repo.git.diff(cmd)
|
|
136
137
|
paths = diff.split("\n")
|
|
137
138
|
return [p for p in paths if p]
|
|
@@ -166,7 +167,8 @@ def get_staged_files_with_status(
|
|
|
166
167
|
repo = get_repo(path)
|
|
167
168
|
cmd = ["--staged", "--name-status"]
|
|
168
169
|
if path is not None:
|
|
169
|
-
|
|
170
|
+
# A staged deletion's path is gone, so Git would read it as a ref
|
|
171
|
+
cmd += ["--", path]
|
|
170
172
|
diff = repo.git.diff(cmd)
|
|
171
173
|
paths = diff.split("\n")
|
|
172
174
|
res = []
|