papilot 0.8.1 → 0.8.3

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.
Files changed (112) hide show
  1. package/README.md +4 -2
  2. package/bin/doctor.mjs +1 -1
  3. package/dist/assets/{_virtual_papilot-licenses-CMfCtEYQ.js → _virtual_papilot-licenses-YJbh9c1G.js} +1 -1
  4. package/dist/assets/index-0siHHtfz.js +267 -0
  5. package/dist/assets/index-CxdYm81a.css +1 -0
  6. package/dist/assets/{math-Bu3xEyrC.js → math-ibBpTslk.js} +1 -1
  7. package/dist/assets/{pdf-view-PbkCuTZ-.js → pdf-view-CD1kNKYH.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/engine/dist/web/assets/FullPaper-DUgLb01O.js +54 -0
  10. package/engine/dist/web/assets/index-D3_ZFdT8.css +1 -0
  11. package/engine/dist/web/assets/index-DJywe0wU.js +19 -0
  12. package/engine/dist/web/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
  13. package/{vendor/latex-review → engine}/dist/web/index.html +2 -2
  14. package/engine/package-lock.json +3390 -0
  15. package/engine/package.json +50 -0
  16. package/{vendor/latex-review → engine}/src/core/build.ts +198 -99
  17. package/{vendor/latex-review → engine}/src/core/git.ts +133 -28
  18. package/{vendor/latex-review → engine}/src/core/outline.ts +0 -9
  19. package/engine/src/core/progress.ts +67 -0
  20. package/{vendor/latex-review → engine}/src/core/review.ts +32 -0
  21. package/engine/src/locales.ts +47 -0
  22. package/{vendor/latex-review → engine}/src/render/inline.ts +1 -0
  23. package/{vendor/latex-review → engine}/src/render/locate.ts +26 -16
  24. package/engine/src/render/positions.ts +34 -0
  25. package/engine/src/render/synctex.ts +188 -0
  26. package/engine/src/render/synctex.wasm +0 -0
  27. package/engine/src/render/synctex.wasm.LICENSE +50 -0
  28. package/{vendor/latex-review → engine}/src/server.ts +5 -3
  29. package/{vendor/latex-review → engine}/src/types.ts +13 -0
  30. package/engine/src/web/FullPaper.tsx +318 -0
  31. package/engine/src/web/i18n.ts +36 -0
  32. package/engine/src/web/locales/de.json +253 -0
  33. package/engine/src/web/locales/es.json +253 -0
  34. package/engine/src/web/locales/fr.json +253 -0
  35. package/engine/src/web/locales/index.ts +16 -0
  36. package/engine/src/web/locales/it.json +253 -0
  37. package/engine/src/web/locales/ja.json +253 -0
  38. package/engine/src/web/locales/ko.json +253 -0
  39. package/engine/src/web/locales/pt.json +253 -0
  40. package/engine/src/web/locales/sv.json +253 -0
  41. package/engine/src/web/locales/zh-Hant.json +253 -0
  42. package/engine/src/web/paper.ts +13 -0
  43. package/{vendor/latex-review → engine}/src/web/references.tsx +1 -1
  44. package/{vendor/latex-review → engine}/src/web/review.tsx +99 -2
  45. package/{vendor/latex-review → engine}/src/web/style.css +71 -11
  46. package/engine/src/web/view-preferences.ts +13 -0
  47. package/{vendor/latex-review → engine}/src/web/zh-CN.ts +3 -0
  48. package/package.json +18 -9
  49. package/src/conversation.ts +1 -1
  50. package/src/server/acp.ts +9 -5
  51. package/src/server/agents.ts +4 -2
  52. package/src/server/app.ts +23 -0
  53. package/src/server/changes.ts +12 -3
  54. package/src/server/imports.ts +69 -25
  55. package/src/server/main.ts +3 -2
  56. package/src/server/manuscript.ts +31 -12
  57. package/src/server/paths.ts +1 -1
  58. package/src/server/start.ts +3 -3
  59. package/src/server/store.ts +0 -8
  60. package/src/server/update-install.ts +6 -3
  61. package/src/server/updates.ts +55 -23
  62. package/src/server/workspace.ts +24 -2
  63. package/src/shared.ts +7 -1
  64. package/src/skill-metadata.ts +1 -1
  65. package/vendor/latex-review/package-lock.json +4 -3106
  66. package/vendor/latex-review/package.json +3 -46
  67. package/README.zh-CN.md +0 -100
  68. package/dist/assets/index-BlLyIqT9.js +0 -265
  69. package/dist/assets/index-DVZtyZvu.css +0 -1
  70. package/vendor/latex-review/dist/web/assets/index-CXdnUjct.js +0 -19
  71. package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +0 -1
  72. package/vendor/latex-review/src/web/i18n.ts +0 -48
  73. /package/{vendor/latex-review → engine}/LICENSE +0 -0
  74. /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
  75. /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
  76. /package/{vendor/latex-review → engine}/src/cli.ts +0 -0
  77. /package/{vendor/latex-review → engine}/src/client.ts +0 -0
  78. /package/{vendor/latex-review → engine}/src/core/bib.ts +0 -0
  79. /package/{vendor/latex-review → engine}/src/core/changes.ts +0 -0
  80. /package/{vendor/latex-review → engine}/src/core/config.ts +0 -0
  81. /package/{vendor/latex-review → engine}/src/core/current-commit.ts +0 -0
  82. /package/{vendor/latex-review → engine}/src/core/delivery.ts +0 -0
  83. /package/{vendor/latex-review → engine}/src/core/diff.ts +0 -0
  84. /package/{vendor/latex-review → engine}/src/core/files.ts +0 -0
  85. /package/{vendor/latex-review → engine}/src/core/process.ts +0 -0
  86. /package/{vendor/latex-review → engine}/src/core/publish.ts +0 -0
  87. /package/{vendor/latex-review → engine}/src/core/reconcile.ts +0 -0
  88. /package/{vendor/latex-review → engine}/src/core/store.ts +0 -0
  89. /package/{vendor/latex-review → engine}/src/core/tasks.ts +0 -0
  90. /package/{vendor/latex-review → engine}/src/core/verify.ts +0 -0
  91. /package/{vendor/latex-review → engine}/src/core/view.ts +0 -0
  92. /package/{vendor/latex-review → engine}/src/render/effects.ts +0 -0
  93. /package/{vendor/latex-review → engine}/src/render/pdf.ts +0 -0
  94. /package/{vendor/latex-review → engine}/src/render/pdfdiff.ts +0 -0
  95. /package/{vendor/latex-review → engine}/src/render/round.ts +0 -0
  96. /package/{vendor/latex-review → engine}/src/render/worker.ts +0 -0
  97. /package/{vendor/latex-review → engine}/src/runtime.ts +0 -0
  98. /package/{vendor/latex-review → engine}/src/web/ChangeMap.tsx +0 -0
  99. /package/{vendor/latex-review → engine}/src/web/PdfStrip.tsx +0 -0
  100. /package/{vendor/latex-review → engine}/src/web/effect-preferences.ts +0 -0
  101. /package/{vendor/latex-review → engine}/src/web/fonts/GUST-FONT-LICENSE.txt +0 -0
  102. /package/{vendor/latex-review → engine}/src/web/fonts/LPPL.txt +0 -0
  103. /package/{vendor/latex-review → engine}/src/web/fonts/MANIFEST-newpx.txt +0 -0
  104. /package/{vendor/latex-review → engine}/src/web/fonts/README-newpx.txt +0 -0
  105. /package/{vendor/latex-review → engine}/src/web/fonts/README.md +0 -0
  106. /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
  107. /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
  108. /package/{vendor/latex-review → engine}/src/web/index.html +0 -0
  109. /package/{vendor/latex-review → engine}/src/web/main.tsx +0 -0
  110. /package/{vendor/latex-review → engine}/src/web/model.ts +0 -0
  111. /package/{vendor/latex-review → engine}/src/web/overview.tsx +0 -0
  112. /package/{vendor/latex-review → engine}/src/web/shortcuts.ts +0 -0
@@ -8,6 +8,7 @@ import type { App } from "../server.ts";
8
8
  import type {
9
9
  Action,
10
10
  ActionDetail,
11
+ BuildInfo,
11
12
  ActionKind,
12
13
  Decision,
13
14
  Feedback,
@@ -54,6 +55,8 @@ export function safePath(root: string, file: string) {
54
55
  return target;
55
56
  }
56
57
  export class Review {
58
+ // Strip positions recovered for earlier rounds, by round and strip image.
59
+ private tops = new Map<string, Map<string, number>>();
57
60
  constructor(public app: App) {}
58
61
  latestRound(name: string) {
59
62
  return this.app.store.one<Round>(
@@ -223,6 +226,35 @@ export class Review {
223
226
  : null,
224
227
  };
225
228
  }
229
+ // The full paper places each change by where its strips start on the page.
230
+ // Rounds rendered before strips recorded that recover it from the round's
231
+ // new PDF the first time they are shown.
232
+ async positioned(detail: RoundDetail): Promise<RoundDetail> {
233
+ const strips = [...detail.sections.flatMap((s) => s.blocks), ...detail.loose]
234
+ .flatMap((block) => (block.kind === "change" ? block.after : []))
235
+ .filter((strip) => strip.top === undefined);
236
+ if (!strips.length) return detail;
237
+ const key = `${detail.task}:${detail.round}`;
238
+ let tops = this.tops.get(key);
239
+ if (!tops) {
240
+ tops = new Map();
241
+ const build = this.round(detail.task, detail.round).summary.builds?.at(-1),
242
+ info = build
243
+ ? readJson<BuildInfo | null>(path.join(this.app.state, "cache/builds", build, "result.json"), null)
244
+ : null;
245
+ if (info && fs.existsSync(info.pdf)) {
246
+ const { stripTops } = await import("../render/positions.ts");
247
+ stripTops(info.pdf, strips);
248
+ for (const strip of strips) if (strip.top !== undefined) tops.set(strip.src, strip.top);
249
+ }
250
+ this.tops.set(key, tops);
251
+ }
252
+ for (const strip of strips) {
253
+ const top = tops.get(strip.src);
254
+ if (top !== undefined) strip.top = top;
255
+ }
256
+ return detail;
257
+ }
226
258
  roundDetail(name: string, number: number): RoundDetail {
227
259
  const round = this.round(name, number),
228
260
  render = readJson<RenderedRound>(
@@ -0,0 +1,47 @@
1
+ // Shared by the workspace and the standalone review viewer.
2
+ export const LANGUAGES = [
3
+ { id: 'en', name: 'English' },
4
+ { id: 'zh-Hans', name: '简体中文' },
5
+ { id: 'zh-Hant', name: '繁體中文' },
6
+ { id: 'ja', name: '日本語' },
7
+ { id: 'ko', name: '한국어' },
8
+ { id: 'de', name: 'Deutsch' },
9
+ { id: 'fr', name: 'Français' },
10
+ { id: 'es', name: 'Español' },
11
+ { id: 'pt', name: 'Português' },
12
+ { id: 'it', name: 'Italiano' },
13
+ { id: 'sv', name: 'Svenska' },
14
+ ] as const;
15
+
16
+ export type Language = typeof LANGUAGES[number]['id'];
17
+
18
+ // Accept older saved choices and regional browser preferences, but expose only
19
+ // the canonical language/script choices in the interface.
20
+ export function supportedLanguage(tag: string | null | undefined): Language | null {
21
+ if (!tag) return null;
22
+ try {
23
+ const locale = new Intl.Locale(tag);
24
+ if (locale.language === 'zh') return locale.maximize().script === 'Hant' ? 'zh-Hant' : 'zh-Hans';
25
+ return LANGUAGES.find(item => item.id === locale.language)?.id || null;
26
+ } catch { return null; }
27
+ }
28
+
29
+ const rules = new Map<Language, Intl.PluralRules>();
30
+ export function singularCount(language: Language, count: number) {
31
+ let rule = rules.get(language);
32
+ if (!rule) { rule = new Intl.PluralRules(language); rules.set(language, rule); }
33
+ return rule.select(count) === 'one';
34
+ }
35
+
36
+ const numberFormats = new Map<Language, Intl.NumberFormat>();
37
+ export function interpolateMessage(text: string, language: Language, values?: Record<string, string | number>) {
38
+ if (!values) return text;
39
+ return text.replace(/\{(\w+)\}/g, (match, name: string) => {
40
+ if (!Object.hasOwn(values, name)) return match;
41
+ const value = values[name];
42
+ if (typeof value !== 'number') return value;
43
+ let format = numberFormats.get(language);
44
+ if (!format) { format = new Intl.NumberFormat(language, { maximumFractionDigits: 20 }); numberFormats.set(language, format); }
45
+ return format.format(value);
46
+ });
47
+ }
@@ -626,6 +626,7 @@ export class Strips {
626
626
  clean,
627
627
  width: Math.round(page.width * 100) / 100,
628
628
  height: Math.round((bottom - top) * 100) / 100,
629
+ top: Math.round(top * 100) / 100,
629
630
  page: number + 1,
630
631
  label: page.label,
631
632
  text,
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { run } from "../core/process.ts";
4
3
  import { argument, buildOutline } from "../core/outline.ts";
4
+ import { SynctexIndex } from "./synctex.ts";
5
5
  import { listFiles } from "../core/files.ts";
6
6
  import { PdfDocument, type PdfPage, type OutlineEntry } from "./pdf.ts";
7
7
  import type {
@@ -47,6 +47,7 @@ export class Build {
47
47
  }
48
48
  close() {
49
49
  this.document.close();
50
+ this.locator.close();
50
51
  }
51
52
  }
52
53
  export function sourceRange(
@@ -135,29 +136,34 @@ export class Margins {
135
136
  : [(this.header ?? 36) + 4, (this.footer ?? page.height - 36) - 4];
136
137
  }
137
138
  }
139
+ // The CLI prints six decimals with printf, which rounds an exact half to
140
+ // even where toFixed rounds it up; matching it keeps every crop the same as
141
+ // with `synctex view`.
142
+ export function printed(value: number) {
143
+ const exact = Math.abs(value).toFixed(30),
144
+ cut = exact.indexOf(".") + 7,
145
+ kept = exact.slice(0, cut);
146
+ const magnitude =
147
+ /^50*$/.test(exact.slice(cut)) && Number(kept.at(-1)) % 2 === 0
148
+ ? Number(kept)
149
+ : Number(Math.abs(value).toFixed(6));
150
+ return value < 0 ? -magnitude : magnitude;
151
+ }
138
152
  export class Locator {
139
153
  cache = new Map<string, Box[]>();
154
+ private index: SynctexIndex | null = null;
140
155
  constructor(public build: Build) {}
141
156
  lookup(file: string, line: number): Box[] {
142
157
  const key = `${file}:${line}`;
143
158
  if (this.cache.has(key)) return this.cache.get(key)!;
144
- const result = run(
145
- "synctex",
146
- ["view", "-i", `${line}:0:${file}`, "-o", this.build.info.pdf],
147
- { check: false },
148
- ).stdout.toString(),
149
- records: Box[] = [],
159
+ this.index ??= new SynctexIndex(this.build.info.pdf);
160
+ const records: Box[] = [],
150
161
  shipped = new Set<number>(),
151
162
  margins = this.build.margins;
152
- for (const block of result.split(/^Page:/m).slice(1)) {
153
- const fields = Object.fromEntries(
154
- [...block.matchAll(/^(x|y|h|v|W|H):([\d.+-]+)/gm)].map((m) => [
155
- m[1],
156
- Number(m[2]),
157
- ]),
158
- ),
159
- page = Number(block.split("\n")[0]) - 1,
160
- { y, v, H: height } = fields;
163
+ for (const result of this.index.view(file, line)) {
164
+ const [number, , y, h, v, W, height] = result.map(printed),
165
+ page = number - 1,
166
+ fields = { h, W };
161
167
  if (
162
168
  ![page, y, v, height].every(Number.isFinite) ||
163
169
  y < v - height - 4 ||
@@ -182,6 +188,10 @@ export class Locator {
182
188
  this.cache.set(key, filtered);
183
189
  return filtered;
184
190
  }
191
+ close() {
192
+ this.index?.close();
193
+ this.index = null;
194
+ }
185
195
  boxes(name: string, start: number, end: number) {
186
196
  const file = this.build.path(name);
187
197
  if (!fs.existsSync(file) || path.extname(file) !== ".tex") return [];
@@ -0,0 +1,34 @@
1
+ import { PdfDocument } from "./pdf.ts";
2
+ import type { Strip } from "../types.ts";
3
+
4
+ // Where strips start on their page, for rounds rendered before strips
5
+ // recorded it. A strip keeps its characters relative to its top, read from
6
+ // the same PDF page by the same code: the difference between a character's
7
+ // place on the page and in the strip is where the strip starts. A strip
8
+ // without text (a figure alone) stays unplaced.
9
+ export function stripTops(pdf: string, strips: Strip[]) {
10
+ const document = new PdfDocument(pdf);
11
+ try {
12
+ for (const strip of strips) {
13
+ if (strip.top !== undefined || strip.page < 1 || strip.page > document.length) continue;
14
+ const page = document.page(strip.page - 1);
15
+ found: for (const line of strip.text ?? []) {
16
+ if (line.chars.length < 3) continue;
17
+ for (const candidate of page.lines)
18
+ if (
19
+ candidate.chars.length === line.chars.length &&
20
+ candidate.chars.every(
21
+ (char, i) =>
22
+ char.text === line.chars[i].text &&
23
+ Math.abs(char.rect[0] - line.chars[i].rect[0]) < 0.01,
24
+ )
25
+ ) {
26
+ strip.top = Math.round((candidate.chars[0].rect[1] - line.chars[0].rect[1]) * 100) / 100;
27
+ break found;
28
+ }
29
+ }
30
+ }
31
+ } finally {
32
+ document.close();
33
+ }
34
+ }
@@ -0,0 +1,188 @@
1
+ import fs from "node:fs";
2
+
3
+ // SyncTeX's own parser (synctex_parser.c with zlib), compiled to WebAssembly
4
+ // by native/synctex/build.sh. `synctex view` parses the whole .synctex.gz
5
+ // again for every query; an index parses it once and answers each forward
6
+ // query from the parser's table of nodes by input file and line, with the
7
+ // values the CLI prints.
8
+ let compiled: WebAssembly.Module | undefined;
9
+ const wasm = () =>
10
+ (compiled ??= new WebAssembly.Module(
11
+ fs.readFileSync(new URL("./synctex.wasm", import.meta.url)),
12
+ ));
13
+
14
+ interface Exports {
15
+ memory: WebAssembly.Memory;
16
+ _initialize?: () => void;
17
+ st_alloc(size: number): number;
18
+ st_free(pointer: number): void;
19
+ st_open(output: number): number;
20
+ st_close(): void;
21
+ st_view(input: number, line: number): number;
22
+ st_results(): number;
23
+ }
24
+
25
+ // Page (1-based), x, y, h, v, W, H: the fields of a `synctex view` result.
26
+ export type ViewResult = [number, number, number, number, number, number, number];
27
+
28
+ // The few WASI calls the parser makes, served from JavaScript: it reads the
29
+ // .synctex(.gz) file next to the PDF and prints warnings, nothing else.
30
+ // (Node's own WASI can collect garbage inside fd_read while a WebAssembly
31
+ // frame is on the stack, which crashes V8.)
32
+ const ERRNO = { SUCCESS: 0, ACCES: 2, BADF: 8, INVAL: 28, NOENT: 44, NOTSUP: 58 };
33
+ const ROOT = 3;
34
+ function system(memory: () => WebAssembly.Memory) {
35
+ const files = new Map<number, { data: Uint8Array; position: number }>();
36
+ let next = ROOT + 1;
37
+ const view = () => new DataView(memory().buffer),
38
+ bytes = () => new Uint8Array(memory().buffer);
39
+ const vectors = (iovs: number, count: number) =>
40
+ Array.from({ length: count }, (_, i) => [
41
+ view().getUint32(iovs + 8 * i, true),
42
+ view().getUint32(iovs + 8 * i + 4, true),
43
+ ]);
44
+ return {
45
+ environ_sizes_get(count: number, size: number) {
46
+ view().setUint32(count, 0, true);
47
+ view().setUint32(size, 0, true);
48
+ return ERRNO.SUCCESS;
49
+ },
50
+ environ_get: () => ERRNO.SUCCESS,
51
+ // The only preopened directory is the file system root.
52
+ fd_prestat_get(fd: number, out: number) {
53
+ if (fd !== ROOT) return ERRNO.BADF;
54
+ view().setUint8(out, 0);
55
+ view().setUint32(out + 4, 1, true);
56
+ return ERRNO.SUCCESS;
57
+ },
58
+ fd_prestat_dir_name(fd: number, path: number, length: number) {
59
+ if (fd !== ROOT || length < 1) return ERRNO.BADF;
60
+ bytes()[path] = 0x2f;
61
+ return ERRNO.SUCCESS;
62
+ },
63
+ path_open(
64
+ dir: number,
65
+ _lookup: number,
66
+ path: number,
67
+ length: number,
68
+ openFlags: number,
69
+ _rights: bigint,
70
+ _inherited: bigint,
71
+ _fdFlags: number,
72
+ out: number,
73
+ ) {
74
+ if (dir !== ROOT) return ERRNO.BADF;
75
+ if (openFlags !== 0) return ERRNO.ACCES; // read only: no create or truncate
76
+ const name = "/" + new TextDecoder().decode(bytes().subarray(path, path + length));
77
+ let data: Uint8Array;
78
+ try {
79
+ if (!fs.statSync(name).isFile()) return ERRNO.NOENT;
80
+ data = fs.readFileSync(name);
81
+ } catch {
82
+ return ERRNO.NOENT;
83
+ }
84
+ files.set(next, { data, position: 0 });
85
+ view().setUint32(out, next++, true);
86
+ return ERRNO.SUCCESS;
87
+ },
88
+ fd_fdstat_get(fd: number, out: number) {
89
+ const type = fd <= 2 ? 2 : fd === ROOT ? 3 : files.has(fd) ? 4 : -1;
90
+ if (type < 0) return ERRNO.BADF;
91
+ view().setUint8(out, type);
92
+ view().setUint16(out + 2, 0, true);
93
+ view().setBigUint64(out + 8, 0xffffffffffffffffn, true);
94
+ view().setBigUint64(out + 16, 0xffffffffffffffffn, true);
95
+ return ERRNO.SUCCESS;
96
+ },
97
+ fd_read(fd: number, iovs: number, count: number, out: number) {
98
+ const file = files.get(fd);
99
+ if (!file) return fd === 0 ? (view().setUint32(out, 0, true), ERRNO.SUCCESS) : ERRNO.BADF;
100
+ let read = 0;
101
+ for (const [at, size] of vectors(iovs, count)) {
102
+ const chunk = file.data.subarray(file.position, file.position + size);
103
+ bytes().set(chunk, at);
104
+ file.position += chunk.length;
105
+ read += chunk.length;
106
+ if (chunk.length < size) break;
107
+ }
108
+ view().setUint32(out, read, true);
109
+ return ERRNO.SUCCESS;
110
+ },
111
+ fd_seek(fd: number, offset: bigint, whence: number, out: number) {
112
+ const file = files.get(fd);
113
+ if (!file) return ERRNO.BADF;
114
+ const base = whence === 0 ? 0 : whence === 1 ? file.position : whence === 2 ? file.data.length : NaN,
115
+ position = base + Number(offset);
116
+ if (!(position >= 0)) return ERRNO.INVAL;
117
+ file.position = position;
118
+ view().setBigUint64(out, BigInt(position), true);
119
+ return ERRNO.SUCCESS;
120
+ },
121
+ fd_tell(fd: number, out: number) {
122
+ const file = files.get(fd);
123
+ if (!file) return ERRNO.BADF;
124
+ view().setBigUint64(out, BigInt(file.position), true);
125
+ return ERRNO.SUCCESS;
126
+ },
127
+ // Warnings go nowhere; they carry nothing a result does not.
128
+ fd_write(fd: number, iovs: number, count: number, out: number) {
129
+ if (fd > 2) return ERRNO.BADF;
130
+ view().setUint32(out, vectors(iovs, count).reduce((sum, [, size]) => sum + size, 0), true);
131
+ return ERRNO.SUCCESS;
132
+ },
133
+ fd_close(fd: number) {
134
+ return files.delete(fd) ? ERRNO.SUCCESS : ERRNO.BADF;
135
+ },
136
+ path_rename: () => ERRNO.NOTSUP,
137
+ proc_exit(code: number) {
138
+ throw new Error(`SyncTeX index exited with status ${code}`);
139
+ },
140
+ };
141
+ }
142
+
143
+ export class SynctexIndex {
144
+ private exports: Exports | null;
145
+ private open: boolean;
146
+ constructor(pdf: string) {
147
+ let memory: WebAssembly.Memory | undefined;
148
+ const instance = new WebAssembly.Instance(wasm(), {
149
+ wasi_snapshot_preview1: system(() => memory!),
150
+ });
151
+ this.exports = instance.exports as unknown as Exports;
152
+ memory = this.exports.memory;
153
+ this.exports._initialize?.();
154
+ const name = this.string(pdf);
155
+ // Without SyncTeX data the CLI answers every query with no results.
156
+ this.open = this.exports.st_open(name) !== 0;
157
+ this.exports.st_free(name);
158
+ }
159
+ private string(text: string) {
160
+ const encoded = new TextEncoder().encode(text + "\0"),
161
+ at = this.exports!.st_alloc(encoded.length);
162
+ new Uint8Array(this.exports!.memory.buffer, at, encoded.length).set(encoded);
163
+ return at;
164
+ }
165
+ // What `synctex view -i line:0:input -o pdf` reports, in its order.
166
+ view(input: string, line: number): ViewResult[] {
167
+ if (!this.exports || !this.open) return [];
168
+ const name = this.string(input),
169
+ count = this.exports.st_view(name, line);
170
+ this.exports.st_free(name);
171
+ const rows: ViewResult[] = [];
172
+ if (count > 0) {
173
+ const data = new Float64Array(
174
+ this.exports.memory.buffer,
175
+ this.exports.st_results(),
176
+ count * 7,
177
+ );
178
+ for (let i = 0; i < count; i++)
179
+ rows.push(Array.from(data.subarray(i * 7, i * 7 + 7)) as ViewResult);
180
+ }
181
+ return rows;
182
+ }
183
+ close() {
184
+ if (!this.exports) return;
185
+ this.exports.st_close();
186
+ this.exports = null;
187
+ }
188
+ }
Binary file
@@ -0,0 +1,50 @@
1
+ synctex.wasm contains SyncTeX's parser (https://github.com/jlaurens/synctex,
2
+ commit a256d4835b7df2a0472cfa3d557af73a1b83f076) and zlib 1.3.1, built by
3
+ native/synctex/build.sh. Their licenses follow.
4
+
5
+ --- SyncTeX
6
+
7
+ MIT License
8
+
9
+ Copyright (c) 2008-2017 jerome DOT laurens AT u-bourgogne DOT fr
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ --- zlib
30
+
31
+ (C) 1995-2024 Jean-loup Gailly and Mark Adler
32
+
33
+ This software is provided 'as-is', without any express or implied
34
+ warranty. In no event will the authors be held liable for any damages
35
+ arising from the use of this software.
36
+
37
+ Permission is granted to anyone to use this software for any purpose,
38
+ including commercial applications, and to alter it and redistribute it
39
+ freely, subject to the following restrictions:
40
+
41
+ 1. The origin of this software must not be misrepresented; you must not
42
+ claim that you wrote the original software. If you use this software
43
+ in a product, an acknowledgment in the product documentation would be
44
+ appreciated but is not required.
45
+ 2. Altered source versions must be plainly marked as such, and must not be
46
+ misrepresented as being the original software.
47
+ 3. This notice may not be removed or altered from any source distribution.
48
+
49
+ Jean-loup Gailly Mark Adler
50
+ jloup@gzip.org madler@alumni.caltech.edu
@@ -188,9 +188,11 @@ export function createServer(app: App) {
188
188
  server.get<{ Params: { name: string; number: string } }>(
189
189
  "/api/tasks/:name/rounds/:number",
190
190
  (request) =>
191
- app.review.roundDetail(
192
- request.params.name,
193
- Number(request.params.number),
191
+ app.review.positioned(
192
+ app.review.roundDetail(
193
+ request.params.name,
194
+ Number(request.params.number),
195
+ ),
194
196
  ),
195
197
  );
196
198
  // The places an edit changes away from its lines, past those its round
@@ -163,6 +163,14 @@ export interface BuildInfo {
163
163
  seconds: number;
164
164
  warnings: string[];
165
165
  }
166
+ // A running compilation: latexmk's current step, the TeX pass, and the
167
+ // pages typeset in it against the last known page count.
168
+ export interface BuildProgress {
169
+ step: "tex" | "bibliography" | "pdf";
170
+ pass: number;
171
+ pages: number;
172
+ total: number | null;
173
+ }
166
174
  export interface Position {
167
175
  page: number;
168
176
  clip: Rect;
@@ -200,6 +208,8 @@ export interface Strip {
200
208
  clean: string;
201
209
  width: number;
202
210
  height: number;
211
+ /** Where the strip starts on its page, in points; absent in older rounds. */
212
+ top?: number;
203
213
  page: number;
204
214
  label: string;
205
215
  text?: TextLine[];
@@ -400,6 +410,9 @@ export interface RoundDetail extends Omit<RenderedRound, "units"> {
400
410
  warnings: string[];
401
411
  files: string;
402
412
  references?: References;
413
+ /** The new version's complete PDF, when the host serves one, so the review
414
+ * can show the changes in the whole paper. */
415
+ pdf?: string;
403
416
  }
404
417
  export type TaskState =
405
418
  "editing" | "building" | "build_failed" | "reviewing" | "revise" | "done";