texlite 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.
Files changed (51) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +403 -0
  3. package/README.zh-CN.md +391 -0
  4. package/dist/client/assets/GitDialog-BHHxSBn3.js +3 -0
  5. package/dist/client/assets/HistoryDialog-Dlv57tYf.js +5 -0
  6. package/dist/client/assets/LatexEditor-B-0VRUPv.js +55 -0
  7. package/dist/client/assets/PdfPreview-617TWFJN.js +1 -0
  8. package/dist/client/assets/ProjectNavigationDialogs-Cpt2LVEt.js +1 -0
  9. package/dist/client/assets/SystemMetricsDialog-fkFHVVzJ.js +1 -0
  10. package/dist/client/assets/editor-JxT6uSYX.js +14 -0
  11. package/dist/client/assets/index-Bc5CmFMl.js +15 -0
  12. package/dist/client/assets/index-CMqYAbDx.css +1 -0
  13. package/dist/client/assets/pdf-DPnwNQHb.js +55 -0
  14. package/dist/client/assets/pdf.worker.min-CHFwMXne.mjs +29 -0
  15. package/dist/client/assets/plus-DzbLkp6l.js +1 -0
  16. package/dist/client/assets/react-CsO_xvbl.js +49 -0
  17. package/dist/client/assets/spellCheck-R6fUM9oA.js +49783 -0
  18. package/dist/client/index.html +20 -0
  19. package/dist/client/logo.svg +4 -0
  20. package/dist/server/anchors.js +95 -0
  21. package/dist/server/app.js +1624 -0
  22. package/dist/server/archive.js +26 -0
  23. package/dist/server/auth.js +42 -0
  24. package/dist/server/cli.js +318 -0
  25. package/dist/server/collaboration.js +635 -0
  26. package/dist/server/compileArtifacts.js +86 -0
  27. package/dist/server/compileDiagnostics.js +227 -0
  28. package/dist/server/compiler.js +514 -0
  29. package/dist/server/config.js +336 -0
  30. package/dist/server/db.js +280 -0
  31. package/dist/server/environment.js +57 -0
  32. package/dist/server/files.js +121 -0
  33. package/dist/server/git.js +414 -0
  34. package/dist/server/history.js +379 -0
  35. package/dist/server/http.js +7 -0
  36. package/dist/server/index.js +75 -0
  37. package/dist/server/latexCompletion.js +296 -0
  38. package/dist/server/metrics.js +34 -0
  39. package/dist/server/pm2.js +234 -0
  40. package/dist/server/projectOutline.js +101 -0
  41. package/dist/server/projectSearch.js +112 -0
  42. package/dist/server/projects.js +29 -0
  43. package/dist/server/routes/compile.js +343 -0
  44. package/dist/server/runtimePaths.js +42 -0
  45. package/dist/server/security.js +29 -0
  46. package/dist/server/spellCheck.js +195 -0
  47. package/dist/server/synctex.js +56 -0
  48. package/dist/server/zip.js +185 -0
  49. package/package.json +110 -0
  50. package/preview.png +0 -0
  51. package/texlite.config.example.json +33 -0
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { publishedCompileArtifacts } from "./compiler.js";
4
+ import { outputRoot, resolveSourcePath, safeRelativePath, sourceRoot } from "./files.js";
5
+ import { hasDocumentClass } from "./zip.js";
6
+ export function availablePdf(config, projectId, mainFile, defaultMainFile) {
7
+ const isDefault = mainFile === defaultMainFile;
8
+ const published = publishedCompileArtifacts(config, projectId, mainFile, isDefault);
9
+ if (published)
10
+ return { path: published.pdf, version: published.runId };
11
+ if (!isDefault)
12
+ return null;
13
+ const retained = retainedPdfPath(config, projectId);
14
+ if (fs.existsSync(retained))
15
+ return { path: retained, version: String(fs.statSync(retained).mtimeMs) };
16
+ const legacy = path.join(outputRoot(config, projectId), `${path.basename(mainFile, ".tex")}.pdf`);
17
+ return fs.existsSync(legacy) ? { path: legacy, version: String(fs.statSync(legacy).mtimeMs) } : null;
18
+ }
19
+ export function syncArtifacts(config, projectId, mainFile, defaultMainFile) {
20
+ const isDefault = mainFile === defaultMainFile;
21
+ const published = publishedCompileArtifacts(config, projectId, mainFile, isDefault);
22
+ if (published?.synctex)
23
+ return { source: published.source, pdf: published.pdf, synctex: published.synctex };
24
+ if (!isDefault)
25
+ return null;
26
+ const retained = {
27
+ source: sourceRoot(config, projectId),
28
+ pdf: retainedPdfPath(config, projectId),
29
+ synctex: retainedSynctexPath(config, projectId)
30
+ };
31
+ if (fs.existsSync(retained.pdf) && fs.existsSync(retained.synctex))
32
+ return retained;
33
+ const basename = path.basename(mainFile, ".tex");
34
+ const legacy = {
35
+ source: sourceRoot(config, projectId),
36
+ pdf: path.join(outputRoot(config, projectId), `${basename}.pdf`),
37
+ synctex: path.join(outputRoot(config, projectId), `${basename}.synctex.gz`)
38
+ };
39
+ return fs.existsSync(legacy.pdf) && fs.existsSync(legacy.synctex) ? legacy : null;
40
+ }
41
+ export function compileMainFile(config, projectId, defaultMainFile, value) {
42
+ let mainFile;
43
+ try {
44
+ mainFile = typeof value === "string" && value ? safeRelativePath(value) : defaultMainFile;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ if (!mainFile.toLocaleLowerCase().endsWith(".tex"))
50
+ return null;
51
+ const absolute = resolveSourcePath(config, projectId, mainFile);
52
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile())
53
+ return null;
54
+ if (mainFile !== defaultMainFile && !hasDocumentClass(fs.readFileSync(absolute, "utf8")))
55
+ return null;
56
+ return mainFile;
57
+ }
58
+ export function compilePdfUrl(projectId, mainFile, version) {
59
+ return `/api/projects/${projectId}/pdf?mainFile=${encodeURIComponent(mainFile)}&run=${encodeURIComponent(version)}`;
60
+ }
61
+ export function listCompileArtifacts(directory) {
62
+ const artifacts = [];
63
+ const visit = (current, prefix) => {
64
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
65
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
66
+ const absolute = path.join(current, entry.name);
67
+ if (entry.isDirectory())
68
+ visit(absolute, relative);
69
+ else if (entry.isFile()) {
70
+ const size = fs.statSync(absolute).size;
71
+ artifacts.push({ path: relative, size, viewable: size <= 2 * 1024 * 1024 && isTextCompileArtifact(relative) });
72
+ }
73
+ }
74
+ };
75
+ visit(directory, "");
76
+ return artifacts.sort((left, right) => left.path.localeCompare(right.path));
77
+ }
78
+ export function isTextCompileArtifact(filePath) {
79
+ return /(?:\.aux|\.bbl|\.bcf|\.blg|\.fls|\.log|\.lof|\.lot|\.nav|\.out|\.run\.xml|\.snm|\.toc|\.vrb|\.fdb_latexmk)$/i.test(filePath);
80
+ }
81
+ function retainedPdfPath(config, projectId) {
82
+ return path.join(outputRoot(config, projectId), ".texlite", "latest.pdf");
83
+ }
84
+ function retainedSynctexPath(config, projectId) {
85
+ return path.join(outputRoot(config, projectId), ".texlite", "latest.synctex.gz");
86
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * A bounded, machine-readable view of a latexmk transcript.
3
+ *
4
+ * The process result is deliberately passed to the parser. latexmk runs
5
+ * several tools and may print an error-looking message from an intermediate
6
+ * pass even when the final PDF was produced successfully. The exit/result
7
+ * status is therefore authoritative for the error list; the transcript is
8
+ * still retained separately for users who need all details.
9
+ */
10
+ const commandPattern = /\b(pdflatex|xelatex|lualatex|bibtex|biber)\b/i;
11
+ const locationPattern = /^(?<file>[^:\n]+):(?<line>\d+)(?::(?<column>\d+))?:\s*(?<rest>.*)$/;
12
+ const inputLinePattern = /\bon input line (?<line>\d+)\b/i;
13
+ const latexLinePattern = /^l\.(?<line>\d+)\b/;
14
+ export function parseCompileDiagnostics(log, outcome = null) {
15
+ const lines = log.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
16
+ const resolvedOutcome = outcome ?? (hasSuccessfulLatexmkResult(log) ? "succeeded" : null);
17
+ const diagnostics = [];
18
+ let phase = "unknown";
19
+ let currentFile;
20
+ let pendingBang;
21
+ let latexPass = 0;
22
+ let lastLatexPass = 0;
23
+ for (const line of lines) {
24
+ const command = line.match(commandPattern);
25
+ if (/^Running\s+['"]?/i.test(line) && command) {
26
+ phase = command[1].toLowerCase();
27
+ if (phase === "pdflatex" || phase === "xelatex" || phase === "lualatex") {
28
+ latexPass += 1;
29
+ lastLatexPass = latexPass;
30
+ }
31
+ const texArgument = [...line.matchAll(/(?:['"]|\s)([^\s'"]+\.tex)(?:['"]|\s|$)/gi)].at(-1)?.[1];
32
+ if (texArgument)
33
+ currentFile = texArgument.replaceAll("\\", "/");
34
+ pendingBang = undefined;
35
+ continue;
36
+ }
37
+ if (/^Latexmk:/i.test(line))
38
+ phase = "latexmk";
39
+ const located = parseLocatedLine(line);
40
+ if (located) {
41
+ currentFile = located.file ?? currentFile;
42
+ const severity = located.severity;
43
+ if (severity) {
44
+ addDiagnostic(diagnostics, {
45
+ severity,
46
+ phase,
47
+ message: located.message,
48
+ raw: line,
49
+ file: located.file ?? currentFile,
50
+ line: located.line,
51
+ column: located.column,
52
+ latexPass: isLatexPhase(phase) ? latexPass : undefined
53
+ });
54
+ pendingBang = undefined;
55
+ continue;
56
+ }
57
+ }
58
+ const packageDiagnostic = parsePackageLine(line);
59
+ if (packageDiagnostic) {
60
+ addDiagnostic(diagnostics, {
61
+ ...packageDiagnostic,
62
+ phase,
63
+ raw: line,
64
+ file: currentFile,
65
+ line: lineFromMessage(packageDiagnostic.message),
66
+ latexPass: isLatexPhase(phase) ? latexPass : undefined
67
+ });
68
+ pendingBang = undefined;
69
+ continue;
70
+ }
71
+ const latexDiagnostic = parseLatexLine(line);
72
+ if (latexDiagnostic) {
73
+ addDiagnostic(diagnostics, {
74
+ ...latexDiagnostic,
75
+ phase: phase === "unknown" ? "pdflatex" : phase,
76
+ raw: line,
77
+ file: currentFile,
78
+ line: lineFromMessage(latexDiagnostic.message),
79
+ latexPass: isLatexPhase(phase) ? latexPass : undefined
80
+ });
81
+ pendingBang = undefined;
82
+ continue;
83
+ }
84
+ if (/^!\s+/.test(line)) {
85
+ pendingBang = addDiagnostic(diagnostics, {
86
+ severity: "error",
87
+ phase: phase === "unknown" ? "pdflatex" : phase,
88
+ message: line.slice(2).trim(),
89
+ raw: line,
90
+ file: currentFile,
91
+ latexPass: isLatexPhase(phase) ? latexPass : undefined
92
+ });
93
+ continue;
94
+ }
95
+ const texLocation = line.match(latexLinePattern);
96
+ if (texLocation && pendingBang) {
97
+ pendingBang.line = Number(texLocation.groups?.line);
98
+ pendingBang.key = diagnosticKey(pendingBang);
99
+ continue;
100
+ }
101
+ const warning = parseGenericWarning(line);
102
+ if (warning) {
103
+ addDiagnostic(diagnostics, {
104
+ ...warning,
105
+ phase,
106
+ raw: line,
107
+ file: currentFile,
108
+ line: lineFromMessage(warning.message),
109
+ latexPass: isLatexPhase(phase) ? latexPass : undefined
110
+ });
111
+ pendingBang = undefined;
112
+ continue;
113
+ }
114
+ if (isLatexmkFailure(line)) {
115
+ addDiagnostic(diagnostics, {
116
+ severity: "error",
117
+ phase: "latexmk",
118
+ message: line.replace(/^Latexmk:\s*/i, "").trim(),
119
+ raw: line
120
+ });
121
+ pendingBang = undefined;
122
+ }
123
+ }
124
+ const unique = dedupeDiagnostics(diagnostics);
125
+ // A successful process/PDF is the source of truth. Do not report a stale
126
+ // BibTeX error from an earlier pass as a compile error after latexmk has
127
+ // completed successfully.
128
+ const errors = resolvedOutcome === "succeeded" ? [] : unique.filter((item) => item.severity === "error");
129
+ const warnings = unique.filter((item) => item.severity === "warning")
130
+ .filter((item) => lastLatexPass < 2 || !isLatexPhase(item.phase) || item.latexPass === lastLatexPass);
131
+ if (resolvedOutcome === "failed" && errors.length === 0) {
132
+ const fallback = {
133
+ severity: "error",
134
+ phase: "system",
135
+ message: "Compilation failed without a parsed diagnostic; inspect the raw log.",
136
+ raw: "",
137
+ key: "system-failure"
138
+ };
139
+ errors.push(fallback);
140
+ }
141
+ return { warnings: publicDiagnostics(warnings), errors: publicDiagnostics(errors) };
142
+ }
143
+ function hasSuccessfulLatexmkResult(log) {
144
+ return /Latexmk: All targets .* are up-to-date/i.test(log)
145
+ && /Output written on .*\.pdf/i.test(log);
146
+ }
147
+ function parseLocatedLine(line) {
148
+ const match = line.match(locationPattern);
149
+ if (!match?.groups)
150
+ return null;
151
+ const rest = match.groups.rest;
152
+ const marker = rest.match(/\b(?:LaTeX(?:\s+[^:]+)?|Package|Class|pdfTeX|XeTeX|LuaTeX)\s+(?<severity>Error|Warning):\s*(?<message>.*)$/i);
153
+ if (!marker?.groups)
154
+ return { file: match.groups.file, line: Number(match.groups.line), column: match.groups.column ? Number(match.groups.column) : undefined, message: rest };
155
+ return {
156
+ file: match.groups.file.replaceAll("\\", "/"),
157
+ line: Number(match.groups.line),
158
+ column: match.groups.column ? Number(match.groups.column) : undefined,
159
+ severity: marker.groups.severity.toLowerCase(),
160
+ message: marker.groups.message.trim()
161
+ };
162
+ }
163
+ function parsePackageLine(line) {
164
+ const match = line.match(/^(?:Package|Class)\s+[^:]+\s+(?<severity>Error|Warning):\s*(?<message>.*)$/i);
165
+ if (!match?.groups)
166
+ return null;
167
+ return { severity: match.groups.severity.toLowerCase(), message: match.groups.message.trim() };
168
+ }
169
+ function parseLatexLine(line) {
170
+ const match = line.match(/^(?:LaTeX(?:\s+[^:]+)?|pdfTeX|XeTeX|LuaTeX)\s+(?<severity>Error|Warning):\s*(?<message>.*)$/i);
171
+ if (!match?.groups)
172
+ return null;
173
+ return { severity: match.groups.severity.toLowerCase(), message: match.groups.message.trim() };
174
+ }
175
+ function parseGenericWarning(line) {
176
+ if (/^(?:Overfull|Underfull)\b/i.test(line) || /^(?:pdfTeX|XeTeX|LuaTeX)\s+warning\b/i.test(line)) {
177
+ return { severity: "warning", message: line };
178
+ }
179
+ return null;
180
+ }
181
+ function lineFromMessage(message) {
182
+ const match = message.match(inputLinePattern);
183
+ return match?.groups?.line ? Number(match.groups.line) : undefined;
184
+ }
185
+ function isLatexmkFailure(line) {
186
+ return /^Latexmk:.*(?:\bErrors?,|did not complete making targets|failure)/i.test(line)
187
+ || /^Collected error summary/i.test(line)
188
+ || /^error, and then rerun latexmk/i.test(line)
189
+ || /\b(?:fatal error|emergency stop|no output pdf|no pages of output)\b/i.test(line)
190
+ || /\b(?:bibtex|biber) errors?:/i.test(line);
191
+ }
192
+ function addDiagnostic(target, input) {
193
+ const diagnostic = { ...input, key: diagnosticKey(input) };
194
+ const existing = target.find((item) => item.key === diagnostic.key
195
+ || (item.severity === diagnostic.severity && item.phase === diagnostic.phase
196
+ && item.message === diagnostic.message && item.file === diagnostic.file
197
+ && (item.line === undefined || diagnostic.line === undefined)));
198
+ if (existing) {
199
+ if (existing.line === undefined && diagnostic.line !== undefined)
200
+ existing.line = diagnostic.line;
201
+ if (!existing.file && diagnostic.file)
202
+ existing.file = diagnostic.file;
203
+ existing.key = diagnosticKey(existing);
204
+ return existing;
205
+ }
206
+ target.push(diagnostic);
207
+ return diagnostic;
208
+ }
209
+ function diagnosticKey(item) {
210
+ return [item.severity, item.phase, item.file ?? "", item.line ?? "", item.column ?? "", item.message].join("\u0000");
211
+ }
212
+ function dedupeDiagnostics(items) {
213
+ const seen = new Set();
214
+ return items.filter((item) => {
215
+ const key = diagnosticKey(item);
216
+ if (seen.has(key))
217
+ return false;
218
+ seen.add(key);
219
+ return true;
220
+ });
221
+ }
222
+ function publicDiagnostics(items) {
223
+ return items.map(({ key: _key, latexPass: _latexPass, ...item }) => item);
224
+ }
225
+ function isLatexPhase(phase) {
226
+ return phase === "pdflatex" || phase === "xelatex" || phase === "lualatex";
227
+ }