papilot 0.1.0 → 0.2.0
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.
- package/README.md +16 -4
- package/README.zh-CN.md +15 -3
- package/bin/doctor.mjs +239 -0
- package/bin/papilot.mjs +96 -87
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-CqhlgnOu.css +1 -0
- package/dist/assets/index-Czf1eCno.js +185 -0
- package/dist/assets/math-DEcVZfaU.css +1 -0
- package/dist/assets/math-Ebyrobf9.js +263 -0
- package/dist/assets/{pdf-view-DJo0X38w.js → pdf-view-BhJZr4qw.js} +18 -18
- package/dist/index.html +11 -2
- package/examples/papilot-paper/preamble.tex +17 -11
- package/examples/papilot-paper/sections/02-using.tex +3 -3
- package/package.json +12 -2
- package/scripts/service.ts +8 -2
- package/skills/check-math.md +12 -0
- package/skills/consistency.md +11 -0
- package/skills/latex-warnings.md +12 -0
- package/skills/polish.md +13 -0
- package/skills/proofread.md +12 -0
- package/skills/reviewer-response.md +11 -0
- package/skills/shorten.md +12 -0
- package/src/conversation.ts +37 -0
- package/src/notes.ts +5 -0
- package/src/server/agents.ts +4 -3
- package/src/server/codex.ts +2 -2
- package/src/server/main.ts +21 -7
- package/src/server/paths.ts +2 -1
- package/src/server/review.ts +1 -1
- package/src/server/service-environment.ts +22 -0
- package/src/server/skills.ts +57 -0
- package/src/server/workspace.ts +43 -19
- package/src/shared.ts +10 -2
- package/vendor/latex-review/dist/web/assets/index-Db7pq4Cj.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-DqkrTw0t.js +19 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/client.ts +10 -3
- package/vendor/latex-review/src/core/build.ts +53 -7
- package/vendor/latex-review/src/core/outline.ts +6 -3
- package/vendor/latex-review/src/core/publish.ts +8 -6
- package/vendor/latex-review/src/runtime.ts +14 -0
- package/vendor/latex-review/src/server.ts +2 -1
- package/vendor/latex-review/src/types.ts +16 -1
- package/vendor/latex-review/src/web/ChangeMap.tsx +21 -9
- package/vendor/latex-review/src/web/PdfStrip.tsx +5 -1
- package/vendor/latex-review/src/web/i18n.ts +48 -0
- package/vendor/latex-review/src/web/model.ts +23 -4
- package/vendor/latex-review/src/web/overview.tsx +25 -20
- package/vendor/latex-review/src/web/review.tsx +387 -143
- package/vendor/latex-review/src/web/style.css +106 -0
- package/vendor/latex-review/src/web/zh-CN.ts +186 -0
- package/dist/assets/index-BhQFCMTx.js +0 -131
- package/dist/assets/index-CAV3-87U.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +0 -19
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
rel="icon"
|
|
10
10
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%232b6e5c'/%3E%3Cpath d='M4 8.5l2.5 2.5L12 5' stroke='white' stroke-width='2' fill='none'/%3E%3C/svg%3E"
|
|
11
11
|
/>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-DqkrTw0t.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Db7pq4Cj.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { ROOT, codeVersion } from "./runtime.ts";
|
|
4
|
+
import { ROOT, codeVersion, environmentVersion } from "./runtime.ts";
|
|
5
5
|
import { Repo } from "./core/git.ts";
|
|
6
6
|
import { readJson, sleep, withLock } from "./core/files.ts";
|
|
7
7
|
export interface Connection {
|
|
@@ -14,6 +14,7 @@ export interface Connection {
|
|
|
14
14
|
export interface ServerInfo {
|
|
15
15
|
root: string;
|
|
16
16
|
version: string;
|
|
17
|
+
environment?: string;
|
|
17
18
|
pid: number;
|
|
18
19
|
busy: boolean;
|
|
19
20
|
csrf: string;
|
|
@@ -153,7 +154,8 @@ export async function connect(
|
|
|
153
154
|
root: string,
|
|
154
155
|
start = true,
|
|
155
156
|
): Promise<Client | null> {
|
|
156
|
-
|
|
157
|
+
// Match the canonical repository path reported by the server (e.g. macOS /var).
|
|
158
|
+
root = fs.realpathSync(root);
|
|
157
159
|
const home = serverHome(root);
|
|
158
160
|
return withLock(path.join(home, "start.lock"), async () => {
|
|
159
161
|
const last = readJson<{ port: number } | null>(
|
|
@@ -167,7 +169,12 @@ export async function connect(
|
|
|
167
169
|
if (!info && last)
|
|
168
170
|
info = { url: `http://127.0.0.1:${last.port}`, port: last.port, pid: 0 };
|
|
169
171
|
let answer = await probe(root, info);
|
|
170
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
answer &&
|
|
174
|
+
(!start ||
|
|
175
|
+
(answer.version === codeVersion() &&
|
|
176
|
+
answer.environment === environmentVersion()))
|
|
177
|
+
)
|
|
171
178
|
return new Client(info!.url, answer.csrf);
|
|
172
179
|
if (!start) return null;
|
|
173
180
|
if (answer && info) {
|
|
@@ -59,21 +59,63 @@ export function affectedTargets(
|
|
|
59
59
|
return [...selected].sort();
|
|
60
60
|
}
|
|
61
61
|
export function errors(text: string) {
|
|
62
|
-
const lines = text.split(
|
|
62
|
+
const lines = text.split(/\r?\n/),
|
|
63
63
|
result: string[] = [];
|
|
64
|
+
const startsError = (line: string) =>
|
|
65
|
+
/^(\S[^\n]*):(\d+): (.+)$/.test(line) || line.startsWith("! ");
|
|
64
66
|
for (const [i, line] of lines.entries())
|
|
65
|
-
if (
|
|
67
|
+
if (startsError(line)) {
|
|
68
|
+
let message = line,
|
|
69
|
+
last = line;
|
|
70
|
+
// TeX hard-wraps even in the middle of words ("P\nackage"). Keep
|
|
71
|
+
// continuations, but stop before help text, source context or a new error.
|
|
72
|
+
let end = i + 1;
|
|
73
|
+
for (; end < lines.length && end < i + 20; end++) {
|
|
74
|
+
const next = lines[end];
|
|
75
|
+
if (
|
|
76
|
+
!next.trim() ||
|
|
77
|
+
startsError(next) ||
|
|
78
|
+
/^(?:See |Type |l\.\d+ |\s*\.\.\.|\s*<)/.test(next)
|
|
79
|
+
)
|
|
80
|
+
break;
|
|
81
|
+
message += (last.length >= 78 ? "" : " ") + next;
|
|
82
|
+
last = next;
|
|
83
|
+
}
|
|
66
84
|
const context = lines
|
|
67
|
-
.slice(
|
|
85
|
+
.slice(end, end + 12)
|
|
68
86
|
.find((l) => /^l\.\d+ /.test(l))
|
|
69
87
|
?.trim();
|
|
70
88
|
result.push(
|
|
71
|
-
|
|
89
|
+
message
|
|
90
|
+
.replace(/^! |^\.\//, "")
|
|
91
|
+
.replace(/\s+/g, " ")
|
|
92
|
+
.trim() + (context ? ` [${context}]` : ""),
|
|
72
93
|
);
|
|
73
94
|
if (result.length === 8) break;
|
|
74
95
|
}
|
|
75
96
|
return result;
|
|
76
97
|
}
|
|
98
|
+
export function compilerDiagnostics(log: string, transcript: string) {
|
|
99
|
+
const details = errors(log);
|
|
100
|
+
// Errors from shell-escape helpers go to latexmk's stderr, not the TeX log.
|
|
101
|
+
for (const traceback of transcript
|
|
102
|
+
.split("Traceback (most recent call last):")
|
|
103
|
+
.slice(1)) {
|
|
104
|
+
const exception = traceback
|
|
105
|
+
.split(/\r?\n/)
|
|
106
|
+
.find((line) => /^[\w.]+(?:Error|Exception): /.test(line));
|
|
107
|
+
if (!exception) continue;
|
|
108
|
+
const minted = traceback
|
|
109
|
+
.slice(0, traceback.indexOf(exception))
|
|
110
|
+
.includes("latexminted");
|
|
111
|
+
details.push((minted ? "latexminted: " : "") + exception);
|
|
112
|
+
if (minted && exception.includes("unexpected keyword argument 'color'"))
|
|
113
|
+
details.push(
|
|
114
|
+
"latexminted is incompatible with this Python version. Update latexminted or select a compatible Python on PATH, then restart the service and retry.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return [...new Set(details)];
|
|
118
|
+
}
|
|
77
119
|
export function warnings(text: string) {
|
|
78
120
|
const patterns: [RegExp, string][] = [
|
|
79
121
|
[/LaTeX Warning: Reference `([^']+)' on page/g, "Undefined reference: "],
|
|
@@ -247,11 +289,15 @@ export class Builder {
|
|
|
247
289
|
log = path.join(out, job + ".log"),
|
|
248
290
|
logText = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : "";
|
|
249
291
|
if (failure || !fs.existsSync(pdf)) {
|
|
250
|
-
const
|
|
292
|
+
const transcriptText = fs.readFileSync(transcript, "utf8"),
|
|
293
|
+
details = compilerDiagnostics(logText, transcriptText),
|
|
251
294
|
kept = path.join(p.state, "logs", key + ".log");
|
|
252
295
|
fs.mkdirSync(path.dirname(kept), { recursive: true });
|
|
253
|
-
fs.
|
|
254
|
-
|
|
296
|
+
fs.writeFileSync(
|
|
297
|
+
kept,
|
|
298
|
+
transcriptText + (logText ? "\n\n--- TeX log ---\n" + logText : ""),
|
|
299
|
+
);
|
|
300
|
+
const tail = transcriptText.split("\n").slice(-25);
|
|
255
301
|
fs.rmSync(folder, { recursive: true, force: true });
|
|
256
302
|
throw new BuildError(
|
|
257
303
|
failure?.message.includes("ENOENT")
|
|
@@ -4,12 +4,13 @@ import type { Heading, Ranges, SectionRef, Span } from "../types.ts";
|
|
|
4
4
|
const LEVELS: Record<string, number> = {
|
|
5
5
|
part: 0,
|
|
6
6
|
chapter: 1,
|
|
7
|
+
addchap: 1,
|
|
7
8
|
section: 2,
|
|
8
9
|
subsection: 3,
|
|
9
10
|
subsubsection: 4,
|
|
10
11
|
};
|
|
11
12
|
const COMMAND =
|
|
12
|
-
/^\s*\\(part|chapter|section|subsection|subsubsection|input|include|subfile|appendix|mainmatter|frontmatter|backmatter)\b(\*)?/;
|
|
13
|
+
/^\s*\\(part|chapter|addchap|section|subsection|subsubsection|input|include|subfile|appendix|mainmatter|frontmatter|backmatter)\b(\*)?/;
|
|
13
14
|
export const stripComments = (text: string) =>
|
|
14
15
|
text.replace(/(?<!\\)%[^\n]*/g, "");
|
|
15
16
|
export function sourceLines(text: string) {
|
|
@@ -190,7 +191,9 @@ export function buildOutline(
|
|
|
190
191
|
":" +
|
|
191
192
|
createHash("sha256").update(`${file}:${i}`).digest("hex").slice(0, 8);
|
|
192
193
|
used.add(id);
|
|
193
|
-
|
|
194
|
+
// KOMA-Script's addchap never advances the chapter counter.
|
|
195
|
+
const isNumbered =
|
|
196
|
+
command !== "addchap" && !match[2] && (numbered || level === 0);
|
|
194
197
|
if (isNumbered) {
|
|
195
198
|
counters[level]++;
|
|
196
199
|
if (level) counters.fill(0, level + 1);
|
|
@@ -198,7 +201,7 @@ export function buildOutline(
|
|
|
198
201
|
const node: Draft = {
|
|
199
202
|
id,
|
|
200
203
|
title: plainTitle(value),
|
|
201
|
-
kind: command,
|
|
204
|
+
kind: command === "addchap" ? "chapter" : command,
|
|
202
205
|
level,
|
|
203
206
|
file,
|
|
204
207
|
line: i + 1,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { ROOT } from "../runtime.ts";
|
|
4
|
+
import { ROOT, codeVersion } from "../runtime.ts";
|
|
5
5
|
import { Builder, BuildError } from "./build.ts";
|
|
6
6
|
import { diffUnits, scopeViolations } from "./changes.ts";
|
|
7
7
|
import { isFigure } from "./config.ts";
|
|
@@ -218,7 +218,8 @@ export class Publisher {
|
|
|
218
218
|
if (
|
|
219
219
|
latest &&
|
|
220
220
|
latest.before === before &&
|
|
221
|
-
app.repo.treeId(latest.after) === app.repo.treeId(after)
|
|
221
|
+
app.repo.treeId(latest.after) === app.repo.treeId(after) &&
|
|
222
|
+
latest.summary.renderer === codeVersion()
|
|
222
223
|
)
|
|
223
224
|
return {
|
|
224
225
|
outcome: "unchanged",
|
|
@@ -242,9 +243,9 @@ export class Publisher {
|
|
|
242
243
|
} catch (error) {
|
|
243
244
|
if (error instanceof BuildError)
|
|
244
245
|
throw new PublishError(
|
|
245
|
-
"The
|
|
246
|
+
"The comparison baseline could not be compiled.",
|
|
246
247
|
error.details,
|
|
247
|
-
"
|
|
248
|
+
"Check the compiler diagnostics, fix the document or build environment, then publish again.",
|
|
248
249
|
error.log,
|
|
249
250
|
);
|
|
250
251
|
throw error;
|
|
@@ -254,9 +255,9 @@ export class Publisher {
|
|
|
254
255
|
} catch (error) {
|
|
255
256
|
if (error instanceof BuildError)
|
|
256
257
|
throw new PublishError(
|
|
257
|
-
"
|
|
258
|
+
"The revised document could not be compiled.",
|
|
258
259
|
error.details,
|
|
259
|
-
"
|
|
260
|
+
"Check the compiler diagnostics, fix the document or build environment, then publish again.",
|
|
260
261
|
error.log,
|
|
261
262
|
);
|
|
262
263
|
throw error;
|
|
@@ -270,6 +271,7 @@ export class Publisher {
|
|
|
270
271
|
output = final + ".pending-" + randomBytes(4).toString("hex");
|
|
271
272
|
fs.mkdirSync(output, { recursive: true });
|
|
272
273
|
const summary = {
|
|
274
|
+
renderer: codeVersion(),
|
|
273
275
|
warnings,
|
|
274
276
|
targets,
|
|
275
277
|
builds: [seed, old, next].filter((b) => b !== null).map((b) => b.key),
|
|
@@ -5,6 +5,20 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { listFiles } from "./core/files.ts";
|
|
6
6
|
export const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
7
7
|
export const WEB = path.join(ROOT, "dist/web");
|
|
8
|
+
export function environmentVersion(env: NodeJS.ProcessEnv = process.env) {
|
|
9
|
+
const keys = [
|
|
10
|
+
"PATH",
|
|
11
|
+
"PYTHONHOME",
|
|
12
|
+
"PYTHONPATH",
|
|
13
|
+
"BIBINPUTS",
|
|
14
|
+
"BSTINPUTS",
|
|
15
|
+
...Object.keys(env).filter((key) => key.startsWith("TEX")),
|
|
16
|
+
].sort();
|
|
17
|
+
return createHash("sha256")
|
|
18
|
+
.update(JSON.stringify(keys.map((key) => [key, env[key] ?? null])))
|
|
19
|
+
.digest("hex")
|
|
20
|
+
.slice(0, 16);
|
|
21
|
+
}
|
|
8
22
|
let version: string | undefined;
|
|
9
23
|
export function codeVersion() {
|
|
10
24
|
if (!version) {
|
|
@@ -4,7 +4,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import staticFiles from "@fastify/static";
|
|
6
6
|
import compress from "@fastify/compress";
|
|
7
|
-
import { WEB, codeVersion } from "./runtime.ts";
|
|
7
|
+
import { WEB, codeVersion, environmentVersion } from "./runtime.ts";
|
|
8
8
|
import { Repo } from "./core/git.ts";
|
|
9
9
|
import { CONFIG_NAME, loadProject, type Project } from "./core/config.ts";
|
|
10
10
|
import { Store } from "./core/store.ts";
|
|
@@ -118,6 +118,7 @@ export function createServer(app: App) {
|
|
|
118
118
|
server.get("/api/info", () => ({
|
|
119
119
|
root: app.root,
|
|
120
120
|
version: codeVersion(),
|
|
121
|
+
environment: environmentVersion(),
|
|
121
122
|
pid: process.pid,
|
|
122
123
|
busy: app.publisher.busy(),
|
|
123
124
|
build_workers: app.project.build_workers,
|
|
@@ -69,7 +69,12 @@ export interface Round {
|
|
|
69
69
|
before: string;
|
|
70
70
|
after: string;
|
|
71
71
|
units: Unit[];
|
|
72
|
-
summary: {
|
|
72
|
+
summary: {
|
|
73
|
+
warnings: string[];
|
|
74
|
+
targets: string[];
|
|
75
|
+
builds: string[];
|
|
76
|
+
renderer?: string;
|
|
77
|
+
};
|
|
73
78
|
created: number;
|
|
74
79
|
}
|
|
75
80
|
export type ActionKind = "accept" | "reject" | "comment";
|
|
@@ -246,6 +251,16 @@ export interface PageState {
|
|
|
246
251
|
/** Optional host-provided reason that file decisions or comments are unavailable. */
|
|
247
252
|
decisionLock?: string | null;
|
|
248
253
|
commentLock?: string | null;
|
|
254
|
+
/**
|
|
255
|
+
* Host activity, undelivered feedback (all updates, and the comments among them),
|
|
256
|
+
* and how many manuscript files this task's accepted changes touched.
|
|
257
|
+
*/
|
|
258
|
+
workspace?: {
|
|
259
|
+
status: string;
|
|
260
|
+
unsent: number;
|
|
261
|
+
comments?: number;
|
|
262
|
+
accepted?: number;
|
|
263
|
+
};
|
|
249
264
|
state: TaskState | null;
|
|
250
265
|
latest: number | null;
|
|
251
266
|
round: number | null;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import type { Decision } from "../types.ts";
|
|
3
|
-
import { stopState, type Stop } from "./model.ts";
|
|
3
|
+
import { STATE, pendingUnits, stopState, type Stop } from "./model.ts";
|
|
4
|
+
import { t } from "./i18n.ts";
|
|
4
5
|
|
|
5
6
|
type Region = { top: number; height: number };
|
|
6
7
|
|
|
7
8
|
export function ChangeMap({
|
|
8
9
|
scrollRoot,
|
|
9
10
|
stops,
|
|
11
|
+
showReviewed = true,
|
|
10
12
|
locations,
|
|
11
13
|
decisions,
|
|
12
14
|
current,
|
|
@@ -15,6 +17,7 @@ export function ChangeMap({
|
|
|
15
17
|
}: {
|
|
16
18
|
scrollRoot: RefObject<HTMLElement | null>;
|
|
17
19
|
stops: Stop[];
|
|
20
|
+
showReviewed?: boolean;
|
|
18
21
|
locations: string[];
|
|
19
22
|
decisions: Record<string, Decision>;
|
|
20
23
|
current: number;
|
|
@@ -25,6 +28,10 @@ export function ChangeMap({
|
|
|
25
28
|
drag = useRef<{ pointer: number; offset: number } | null>(null),
|
|
26
29
|
[regions, setRegions] = useState<Region[]>([]),
|
|
27
30
|
[viewport, setViewport] = useState<Region>({ top: 0, height: 0 });
|
|
31
|
+
const visible = stops
|
|
32
|
+
.map((stop, index) => ({ stop, index }))
|
|
33
|
+
.filter(({ stop }) => showReviewed || pendingUnits(stop, decisions).length)
|
|
34
|
+
.map(({ index }) => index);
|
|
28
35
|
|
|
29
36
|
useEffect(() => {
|
|
30
37
|
const root = scrollRoot.current,
|
|
@@ -125,7 +132,7 @@ export function ChangeMap({
|
|
|
125
132
|
<nav
|
|
126
133
|
ref={rail}
|
|
127
134
|
className="change-map"
|
|
128
|
-
aria-label="Change map"
|
|
135
|
+
aria-label={t("Change map")}
|
|
129
136
|
onPointerDown={(event) => {
|
|
130
137
|
if (
|
|
131
138
|
event.button !== 0 ||
|
|
@@ -165,10 +172,11 @@ export function ChangeMap({
|
|
|
165
172
|
aria-hidden="true"
|
|
166
173
|
/>
|
|
167
174
|
{stops.map((stop, index) => {
|
|
175
|
+
if (!visible.includes(index)) return null;
|
|
168
176
|
const region = regions[index];
|
|
169
177
|
if (!region) return null;
|
|
170
178
|
const state = stopState(stop.units, decisions),
|
|
171
|
-
label =
|
|
179
|
+
label = `${t("Change {n}", { n: index + 1 })} · ${t(STATE[state] ?? state).toLowerCase()} · ${locations[index]}`;
|
|
172
180
|
return (
|
|
173
181
|
<button
|
|
174
182
|
key={stop.key}
|
|
@@ -178,22 +186,26 @@ export function ChangeMap({
|
|
|
178
186
|
data-state={state}
|
|
179
187
|
aria-label={label}
|
|
180
188
|
aria-current={index === current ? "location" : undefined}
|
|
181
|
-
tabIndex={
|
|
189
|
+
tabIndex={
|
|
190
|
+
index === (visible.includes(current) ? current : visible[0])
|
|
191
|
+
? 0
|
|
192
|
+
: -1
|
|
193
|
+
}
|
|
182
194
|
disabled={disabled}
|
|
183
195
|
style={{ top: region.top, height: region.height }}
|
|
184
196
|
onClick={() => onSelect(index)}
|
|
185
197
|
onKeyDown={(event) => {
|
|
186
198
|
const target =
|
|
187
199
|
event.key === "ArrowDown"
|
|
188
|
-
? index + 1
|
|
200
|
+
? (visible[visible.indexOf(index) + 1] ?? index)
|
|
189
201
|
: event.key === "ArrowUp"
|
|
190
|
-
? index - 1
|
|
202
|
+
? (visible[visible.indexOf(index) - 1] ?? index)
|
|
191
203
|
: event.key === "Home"
|
|
192
|
-
? 0
|
|
204
|
+
? visible[0]
|
|
193
205
|
: event.key === "End"
|
|
194
|
-
?
|
|
206
|
+
? visible.at(-1)
|
|
195
207
|
: null;
|
|
196
|
-
if (target
|
|
208
|
+
if (target == null) return;
|
|
197
209
|
event.preventDefault();
|
|
198
210
|
event.stopPropagation();
|
|
199
211
|
rail.current
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useMemo, type CSSProperties } from "react";
|
|
2
2
|
import type { Strip, TextGlyph } from "../types.ts";
|
|
3
|
+
import { t } from "./i18n.ts";
|
|
3
4
|
let context: CanvasRenderingContext2D | null | undefined;
|
|
4
5
|
const widths = new Map<string, number>();
|
|
5
6
|
function width(text: string) {
|
|
@@ -67,7 +68,10 @@ export const PdfStrip = memo(function PdfStrip({
|
|
|
67
68
|
<div
|
|
68
69
|
className="pdf-text"
|
|
69
70
|
role="region"
|
|
70
|
-
aria-label={
|
|
71
|
+
aria-label={t("{label}, PDF page {page}", {
|
|
72
|
+
label,
|
|
73
|
+
page: image.label,
|
|
74
|
+
})}
|
|
71
75
|
>
|
|
72
76
|
{text}
|
|
73
77
|
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { zhCN } from "./zh-CN.ts";
|
|
2
|
+
|
|
3
|
+
// The viewer reads in English unless the page asks for another language with
|
|
4
|
+
// ?lang=zh-CN (Papilot passes its interface language). The English text is the
|
|
5
|
+
// message key, so a string without a translation still reads correctly.
|
|
6
|
+
export type Language = "en" | "zh-CN";
|
|
7
|
+
const messages: Record<Language, Record<string, string>> = {
|
|
8
|
+
en: {},
|
|
9
|
+
"zh-CN": zhCN,
|
|
10
|
+
};
|
|
11
|
+
export function pageLanguage(search = location.search): Language {
|
|
12
|
+
return new URLSearchParams(search).get("lang")?.toLowerCase().startsWith("zh")
|
|
13
|
+
? "zh-CN"
|
|
14
|
+
: "en";
|
|
15
|
+
}
|
|
16
|
+
const current: Language =
|
|
17
|
+
typeof location === "undefined" ? "en" : pageLanguage();
|
|
18
|
+
if (typeof document !== "undefined") document.documentElement.lang = current;
|
|
19
|
+
|
|
20
|
+
export const language = () => current;
|
|
21
|
+
// Dates and numbers follow the interface language.
|
|
22
|
+
export const locale = () => (current === "zh-CN" ? "zh-CN" : "en-US");
|
|
23
|
+
|
|
24
|
+
// Links inside the viewer keep the page's language.
|
|
25
|
+
export function withLanguage(href: string) {
|
|
26
|
+
return current === "en"
|
|
27
|
+
? href
|
|
28
|
+
: `${href}${href.includes("?") ? "&" : "?"}lang=${current}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// t("Round {round}", { round }) fills {placeholders} after translating.
|
|
32
|
+
export function t(text: string, values?: Record<string, string | number>) {
|
|
33
|
+
const template = messages[current][text] ?? text;
|
|
34
|
+
return values
|
|
35
|
+
? template.replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
36
|
+
name in values ? String(values[name]) : match,
|
|
37
|
+
)
|
|
38
|
+
: template;
|
|
39
|
+
}
|
|
40
|
+
// English picks between one and other by count; {n} is the count.
|
|
41
|
+
export function tn(
|
|
42
|
+
count: number,
|
|
43
|
+
one: string,
|
|
44
|
+
other: string,
|
|
45
|
+
values?: Record<string, string | number>,
|
|
46
|
+
) {
|
|
47
|
+
return t(count === 1 ? one : other, { n: count, ...values });
|
|
48
|
+
}
|
|
@@ -38,6 +38,14 @@ export function nextPending(
|
|
|
38
38
|
}
|
|
39
39
|
return -1;
|
|
40
40
|
}
|
|
41
|
+
// Names for a change's state; the viewer translates them where it shows them.
|
|
42
|
+
export const STATE: Record<string, string> = {
|
|
43
|
+
pending: "Pending",
|
|
44
|
+
accepted: "Accepted",
|
|
45
|
+
rejected: "Rejected",
|
|
46
|
+
commented: "Commented",
|
|
47
|
+
mixed: "Mixed",
|
|
48
|
+
};
|
|
41
49
|
export function stopState(
|
|
42
50
|
units: string[],
|
|
43
51
|
decisions: Record<string, Decision>,
|
|
@@ -122,14 +130,24 @@ export function wordDiff(before: string, after: string) {
|
|
|
122
130
|
}
|
|
123
131
|
return result;
|
|
124
132
|
}
|
|
125
|
-
|
|
133
|
+
type Say = (text: string, values?: Record<string, string | number>) => string;
|
|
134
|
+
const english: Say = (text, values = {}) =>
|
|
135
|
+
text.replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
136
|
+
name in values ? String(values[name]) : match,
|
|
137
|
+
);
|
|
138
|
+
// The viewer passes its translator for display; copied references and the
|
|
139
|
+
// default stay in English.
|
|
140
|
+
export function describeLocation(
|
|
141
|
+
units: (Unit & Partial<VisualUnit>)[],
|
|
142
|
+
say: Say = english,
|
|
143
|
+
) {
|
|
126
144
|
const files = new Map<string, string[]>();
|
|
127
145
|
for (const unit of units) {
|
|
128
146
|
const where =
|
|
129
147
|
unit.status === "added"
|
|
130
|
-
? "new file"
|
|
148
|
+
? say("new file")
|
|
131
149
|
: unit.status === "deleted"
|
|
132
|
-
? "deleted"
|
|
150
|
+
? say("deleted")
|
|
133
151
|
: unit.new_start === unit.new_end
|
|
134
152
|
? String(unit.new_start)
|
|
135
153
|
: `${unit.new_start}–${unit.new_end}`;
|
|
@@ -139,7 +157,8 @@ export function describeLocation(units: (Unit & Partial<VisualUnit>)[]) {
|
|
|
139
157
|
return (
|
|
140
158
|
[...files]
|
|
141
159
|
.map(([file, places]) => `${file}:${places.join(", ")}`)
|
|
142
|
-
.join("; ") +
|
|
160
|
+
.join("; ") +
|
|
161
|
+
(pages.length ? ` · ${say("p. {pages}", { pages: pages.join(", ") })}` : "")
|
|
143
162
|
);
|
|
144
163
|
}
|
|
145
164
|
export function copyReference(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import type { Summary } from "../types.ts";
|
|
3
|
+
import { t, tn, withLanguage } from "./i18n.ts";
|
|
3
4
|
export const STATUS: Record<string, string> = {
|
|
4
5
|
editing: "Editing",
|
|
5
6
|
reviewing: "In review",
|
|
@@ -45,34 +46,36 @@ export function Overview() {
|
|
|
45
46
|
return (
|
|
46
47
|
<>
|
|
47
48
|
<header className="bar">
|
|
48
|
-
<a className="brand" href="/">
|
|
49
|
+
<a className="brand" href={withLanguage("/")}>
|
|
49
50
|
LaTeX Review
|
|
50
51
|
</a>
|
|
51
|
-
<span className="status">
|
|
52
|
+
<span className="status">
|
|
53
|
+
{tn(tasks.length, "{n} open review", "{n} open reviews")}
|
|
54
|
+
</span>
|
|
52
55
|
</header>
|
|
53
56
|
<main id="paper" className="index">
|
|
54
57
|
<div className="document">
|
|
55
58
|
<div className="overview-heading">
|
|
56
|
-
<h1>All reviews</h1>
|
|
59
|
+
<h1>{t("All reviews")}</h1>
|
|
57
60
|
</div>
|
|
58
61
|
<div className="task-tools">
|
|
59
62
|
<input
|
|
60
63
|
type="search"
|
|
61
|
-
aria-label="Search reviews"
|
|
62
|
-
placeholder="Search by title or task name"
|
|
64
|
+
aria-label={t("Search reviews")}
|
|
65
|
+
placeholder={t("Search by title or task name")}
|
|
63
66
|
value={search}
|
|
64
67
|
onChange={(e) => setSearch(e.target.value)}
|
|
65
68
|
/>
|
|
66
69
|
<select
|
|
67
|
-
aria-label="Filter reviews"
|
|
70
|
+
aria-label={t("Filter reviews")}
|
|
68
71
|
value={filter}
|
|
69
72
|
onChange={(e) => setFilter(e.target.value)}
|
|
70
73
|
>
|
|
71
|
-
<option value="all">All tasks</option>
|
|
72
|
-
<option value="reviewing">Awaiting your review</option>
|
|
73
|
-
<option value="building">Publishing</option>
|
|
74
|
-
<option value="revise">Awaiting revision</option>
|
|
75
|
-
<option value="done">All decided</option>
|
|
74
|
+
<option value="all">{t("All tasks")}</option>
|
|
75
|
+
<option value="reviewing">{t("Awaiting your review")}</option>
|
|
76
|
+
<option value="building">{t("Publishing")}</option>
|
|
77
|
+
<option value="revise">{t("Awaiting revision")}</option>
|
|
78
|
+
<option value="done">{t("All decided")}</option>
|
|
76
79
|
</select>
|
|
77
80
|
</div>
|
|
78
81
|
{error && <p role="alert">{error}</p>}
|
|
@@ -87,20 +90,20 @@ export function Overview() {
|
|
|
87
90
|
<li key={task.task}>
|
|
88
91
|
<a
|
|
89
92
|
className="task-card"
|
|
90
|
-
href={`/?task=${encodeURIComponent(task.task)}`}
|
|
93
|
+
href={withLanguage(`/?task=${encodeURIComponent(task.task)}`)}
|
|
91
94
|
>
|
|
92
95
|
<div className="task-heading">
|
|
93
96
|
<h2>{task.title}</h2>
|
|
94
97
|
<span className="status" data-state={task.state}>
|
|
95
|
-
{STATUS[task.state]}
|
|
98
|
+
{t(STATUS[task.state])}
|
|
96
99
|
</span>
|
|
97
100
|
</div>
|
|
98
101
|
<div className="task-meta">
|
|
99
102
|
<span className="task-name">{task.task}</span>
|
|
100
103
|
<span>
|
|
101
104
|
{task.round
|
|
102
|
-
?
|
|
103
|
-
: "Not published yet"}
|
|
105
|
+
? t("Round {round}", { round: task.round })
|
|
106
|
+
: t("Not published yet")}
|
|
104
107
|
</span>
|
|
105
108
|
</div>
|
|
106
109
|
{total > 0 && (
|
|
@@ -109,7 +112,7 @@ export function Overview() {
|
|
|
109
112
|
<i style={{ width: `${(100 * decided) / total}%` }} />
|
|
110
113
|
</span>
|
|
111
114
|
<span>
|
|
112
|
-
{decided} of {total} decided
|
|
115
|
+
{t("{decided} of {total} decided", { decided, total })}
|
|
113
116
|
</span>
|
|
114
117
|
</div>
|
|
115
118
|
)}
|
|
@@ -119,12 +122,12 @@ export function Overview() {
|
|
|
119
122
|
.filter(([, n]) => n)
|
|
120
123
|
.map(([kind, n]) => (
|
|
121
124
|
<span key={kind} data-kind={kind}>
|
|
122
|
-
{n} {kind}
|
|
125
|
+
{t(`{n} ${kind}`, { n })}
|
|
123
126
|
</span>
|
|
124
127
|
))}
|
|
125
128
|
</div>
|
|
126
129
|
<span className="task-open">
|
|
127
|
-
{task.counts.pending ? "Review →" : "Open →"}
|
|
130
|
+
{task.counts.pending ? t("Review →") : t("Open →")}
|
|
128
131
|
</span>
|
|
129
132
|
</div>
|
|
130
133
|
</a>
|
|
@@ -135,8 +138,10 @@ export function Overview() {
|
|
|
135
138
|
{!visible.length && (
|
|
136
139
|
<p className="empty">
|
|
137
140
|
{tasks.length
|
|
138
|
-
? "No reviews match this filter."
|
|
139
|
-
:
|
|
141
|
+
? t("No reviews match this filter.")
|
|
142
|
+
: t(
|
|
143
|
+
"No open reviews yet. Start a task from your document repository.",
|
|
144
|
+
)}
|
|
140
145
|
</p>
|
|
141
146
|
)}
|
|
142
147
|
</div>
|