texlite 0.8.4 → 0.9.1
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/DESIGN.md +75 -8
- package/OPERATIONS.md +19 -2
- package/dist/client/assets/{AdminUsers-CqOmV9rq.js → AdminUsers-Dazar9cj.js} +1 -1
- package/dist/client/assets/{CitationLibraryDialog-BmWFXTW2.js → CitationLibraryDialog-VSUiSPgG.js} +1 -1
- package/dist/client/assets/Dashboard-CoE5ESfb.js +2 -0
- package/dist/client/assets/GitDialog-C9Loz4Al.js +3 -0
- package/dist/client/assets/HistoryDialog-3UNN_jhU.js +7 -0
- package/dist/client/assets/{LatexEditor-DSopAHbC.js → LatexEditor-Bq76v8sO.js} +2 -2
- package/dist/client/assets/{PdfPreview-BDkU20jP.js → PdfPreview-BKhcS-2c.js} +1 -1
- package/dist/client/assets/{ProjectIconPickerDialog-GAd8HGOx.js → ProjectIconPickerDialog-BoWMjQVm.js} +1 -1
- package/dist/client/assets/{ProjectNavigationDialogs-BTLcRY7i.js → ProjectNavigationDialogs-CZKTnxrl.js} +1 -1
- package/dist/client/assets/{ProjectSettings-BJWTkoZq.js → ProjectSettings-DiOQVJ40.js} +1 -1
- package/dist/client/assets/ProjectWorkspace-DqLwkGpT.js +27 -0
- package/dist/client/assets/SelectionHistoryDialog-D3dO83lg.js +3 -0
- package/dist/client/assets/{SystemMetricsDialog-C9P4TSQl.js → SystemMetricsDialog-0zCwLtH_.js} +1 -1
- package/dist/client/assets/{TagManagementDialog-LYbib36_.js → TagManagementDialog-C6KDwtvd.js} +1 -1
- package/dist/client/assets/{WordCountDialog-BFg0GeJy.js → WordCountDialog-TwpyAP3m.js} +1 -1
- package/dist/client/assets/check-sZxOOKtS.js +1 -0
- package/dist/client/assets/chevron-left-32SrOwa5.js +1 -0
- package/dist/client/assets/hard-drive-XHSiS_A7.js +1 -0
- package/dist/client/assets/index-BRr3Kuxh.css +1 -0
- package/dist/client/assets/index-BjzELSQ-.js +12 -0
- package/dist/client/assets/index-Cd6-F29b.js +5 -0
- package/dist/client/assets/{key-round-BaIj7Jch.js → key-round-C-CXCWnz.js} +1 -1
- package/dist/client/assets/minimize-2-BMDPVaF-.js +1 -0
- package/dist/client/assets/{minus-Beso_n-6.js → minus-BIa7-KuM.js} +1 -1
- package/dist/client/assets/{plus-BfEC7mzf.js → plus-BKDWy4Ui.js} +1 -1
- package/dist/client/assets/{refresh-cw-FZs9MtoA.js → refresh-cw-Bo7-a7-I.js} +1 -1
- package/dist/client/assets/{search-DL2UGvdj.js → search-Bz9Ob5mT.js} +1 -1
- package/dist/client/assets/{sigma-CSRwPPkk.js → sigma-DxlHf9gv.js} +1 -1
- package/dist/client/assets/spellCheck-BfZEnpwY.js +1 -0
- package/dist/client/assets/{tags-CjkWzXqX.js → tags-IMZehjzE.js} +1 -1
- package/dist/client/assets/{type-D1mmDdLM.js → type-DTWpFQNQ.js} +1 -1
- package/dist/client/assets/{user-plus-YJ3ftuL5.js → user-plus--aVpd8K6.js} +1 -1
- package/dist/client/assets/x-Tbo3aA0J.js +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/app.js +45 -4
- package/dist/server/cli.js +4 -0
- package/dist/server/collaboration.js +219 -7
- package/dist/server/commentMentions.js +139 -0
- package/dist/server/config.js +9 -2
- package/dist/server/db.js +59 -0
- package/dist/server/editHistory.js +411 -0
- package/dist/server/editHistoryRetry.js +90 -0
- package/dist/server/harper.js +205 -8
- package/dist/server/history.js +189 -23
- package/dist/server/historyRetention.js +37 -0
- package/dist/server/i18n.js +12 -0
- package/dist/server/index.js +6 -0
- package/dist/server/latexSpellMask.js +197 -43
- package/dist/server/logRotation.js +82 -0
- package/dist/server/pm2.js +7 -0
- package/dist/server/routes/comments.js +120 -17
- package/dist/server/routes/projectHistory.js +106 -8
- package/dist/server/routes/projectShared.js +2 -1
- package/dist/server/routes/projects.js +39 -5
- package/package.json +1 -1
- package/texlite.config.example.json +4 -1
- package/dist/client/assets/Dashboard-BfN2bCgZ.js +0 -2
- package/dist/client/assets/GitDialog-BCfSHuMB.js +0 -3
- package/dist/client/assets/HistoryDialog-BtQhyTr9.js +0 -5
- package/dist/client/assets/ProjectWorkspace-WLYA7BSv.js +0 -27
- package/dist/client/assets/check-bnimQPun.js +0 -1
- package/dist/client/assets/chevron-left-DNM7mhNi.js +0 -1
- package/dist/client/assets/hard-drive-CF3WDfkG.js +0 -1
- package/dist/client/assets/index-CzVDvC5h.js +0 -14
- package/dist/client/assets/index-RjUuWk8F.css +0 -1
- package/dist/client/assets/spellCheck-L9gD2d1M.js +0 -1
- package/dist/client/assets/x-BDSEosRE.js +0 -1
package/dist/server/i18n.js
CHANGED
|
@@ -16,6 +16,7 @@ const enMessages = {
|
|
|
16
16
|
COMMENT_FILE_NOT_FOUND: "The file for this comment does not exist.",
|
|
17
17
|
COMMENT_NOT_FOUND: "The comment does not exist.",
|
|
18
18
|
COMMENT_RANGE_INVALID: "The comment source range is invalid.",
|
|
19
|
+
COMMENT_SOURCE_CHANGED: "The source changed while this comment was being prepared. Reselect the passage and try again.",
|
|
19
20
|
COMMENT_UPDATE_EMPTY: "There is no comment change to save.",
|
|
20
21
|
COMPILE_ARTIFACT_NOT_FOUND: "The compiled artifact does not exist.",
|
|
21
22
|
COMPILE_ARTIFACTS_NOT_FOUND: "No compiled artifacts are available yet.",
|
|
@@ -45,9 +46,12 @@ const enMessages = {
|
|
|
45
46
|
GIT_UNAVAILABLE: "Git is not available on this server.",
|
|
46
47
|
HARPER_UNAVAILABLE: "Harper spelling and grammar checking is temporarily unavailable.",
|
|
47
48
|
HISTORY_FILE_NOT_FOUND: "The historical file does not exist.",
|
|
49
|
+
HISTORY_OBJECT_MISSING: "A stored file for this snapshot is missing. It can no longer be viewed or restored.",
|
|
48
50
|
HISTORY_FILE_PREVIEW_UNSUPPORTED: "This historical file cannot be compared as text.",
|
|
49
51
|
HISTORY_TARGET_CONFLICT: "The historical version cannot be restored to the requested path.",
|
|
50
52
|
HISTORY_VERSION_NOT_FOUND: "The history version does not exist.",
|
|
53
|
+
HISTORY_VERSION_CHANGED: "The selected project snapshot changed while it was being reviewed. Refresh it before restoring.",
|
|
54
|
+
SELECTION_HISTORY_SOURCE_CHANGED: "The source changed while opening selection history. Wait for synchronization, then select the passage and open its history again.",
|
|
51
55
|
INVALID_PATH: "The file path is invalid.",
|
|
52
56
|
LAST_ADMIN: "At least one active administrator must remain.",
|
|
53
57
|
LATEXMKRC_DISABLED: "Project latexmkrc files are disabled by the administrator.",
|
|
@@ -59,6 +63,7 @@ const enMessages = {
|
|
|
59
63
|
MAIN_FILE_INVALID: "The main document must be an existing .tex file.",
|
|
60
64
|
MAIN_FILE_NOT_FOUND: "The selected main document does not exist.",
|
|
61
65
|
MEMBERS_MANAGE_FORBIDDEN: "Only the project owner can manage members.",
|
|
66
|
+
MENTION_NOT_FOUND: "The mention notification does not exist.",
|
|
62
67
|
MOVE_INTO_SELF: "A folder cannot be moved into itself.",
|
|
63
68
|
OWNER_MEMBER_FORBIDDEN: "The project owner cannot be added as a member.",
|
|
64
69
|
PASSWORD_TOO_SHORT: "The password must contain at least {{minLength}} characters.",
|
|
@@ -87,6 +92,7 @@ const enMessages = {
|
|
|
87
92
|
SOURCE_FLUSH_FAILED: "Some source files could not be saved. The project operation was cancelled.",
|
|
88
93
|
SPELLCHECK_SOURCE_INVALID: "The source to check is invalid.",
|
|
89
94
|
SPELLCHECK_SOURCE_TOO_LARGE: "The source to check is too large.",
|
|
95
|
+
SPELLCHECK_SUPERSEDED: "A newer writing check replaced this request.",
|
|
90
96
|
SYNCTEX_EXTERNAL_PATH: "SyncTeX returned a source path outside the project.",
|
|
91
97
|
SYNCTEX_NOT_AVAILABLE: "No SyncTeX data is available. Compile the project again.",
|
|
92
98
|
SYMLINK_FORBIDDEN: "Symbolic links are not allowed in project sources.",
|
|
@@ -142,6 +148,7 @@ const zhMessages = {
|
|
|
142
148
|
COMMENT_FILE_NOT_FOUND: "批注对应的文件不存在。",
|
|
143
149
|
COMMENT_NOT_FOUND: "批注不存在。",
|
|
144
150
|
COMMENT_RANGE_INVALID: "批注的源码区间无效。",
|
|
151
|
+
COMMENT_SOURCE_CHANGED: "准备批注时源码已发生变化。请重新选择文本后再试。",
|
|
145
152
|
COMMENT_UPDATE_EMPTY: "没有可保存的批注修改。",
|
|
146
153
|
COMPILE_ARTIFACT_NOT_FOUND: "编译产物不存在。",
|
|
147
154
|
COMPILE_ARTIFACTS_NOT_FOUND: "尚无可用的编译产物。",
|
|
@@ -171,9 +178,12 @@ const zhMessages = {
|
|
|
171
178
|
GIT_UNAVAILABLE: "服务器未安装 Git。",
|
|
172
179
|
HARPER_UNAVAILABLE: "Harper 拼写与语法检查暂不可用。",
|
|
173
180
|
HISTORY_FILE_NOT_FOUND: "历史文件不存在。",
|
|
181
|
+
HISTORY_OBJECT_MISSING: "该快照的已存储文件缺失,无法再查看或恢复。",
|
|
174
182
|
HISTORY_FILE_PREVIEW_UNSUPPORTED: "该历史文件不能作为文本比较。",
|
|
175
183
|
HISTORY_TARGET_CONFLICT: "无法将历史版本恢复到所请求的路径。",
|
|
176
184
|
HISTORY_VERSION_NOT_FOUND: "历史版本不存在。",
|
|
185
|
+
HISTORY_VERSION_CHANGED: "查看期间该项目快照已发生变化。请刷新后再恢复。",
|
|
186
|
+
SELECTION_HISTORY_SOURCE_CHANGED: "打开选区历史时源码已发生变化。请等待同步完成,重新选择文本后再打开历史。",
|
|
177
187
|
INVALID_PATH: "文件路径无效。",
|
|
178
188
|
LAST_ADMIN: "系统必须保留至少一名可用管理员。",
|
|
179
189
|
LATEXMKRC_DISABLED: "管理员已禁用项目级 latexmkrc。",
|
|
@@ -185,6 +195,7 @@ const zhMessages = {
|
|
|
185
195
|
MAIN_FILE_INVALID: "主文档必须是存在的 .tex 文件。",
|
|
186
196
|
MAIN_FILE_NOT_FOUND: "所选主文档不存在。",
|
|
187
197
|
MEMBERS_MANAGE_FORBIDDEN: "只有项目所有者可以管理成员。",
|
|
198
|
+
MENTION_NOT_FOUND: "@ 提醒不存在。",
|
|
188
199
|
MOVE_INTO_SELF: "不能把文件夹移动到自身内部。",
|
|
189
200
|
OWNER_MEMBER_FORBIDDEN: "项目所有者不能作为成员添加。",
|
|
190
201
|
PASSWORD_TOO_SHORT: "密码至少需要 {{minLength}} 个字符。",
|
|
@@ -213,6 +224,7 @@ const zhMessages = {
|
|
|
213
224
|
SOURCE_FLUSH_FAILED: "部分源码未能保存至磁盘,项目操作已取消。",
|
|
214
225
|
SPELLCHECK_SOURCE_INVALID: "待检查源码格式不正确。",
|
|
215
226
|
SPELLCHECK_SOURCE_TOO_LARGE: "待检查源码过大。",
|
|
227
|
+
SPELLCHECK_SUPERSEDED: "更新的写作检查已替代此请求。",
|
|
216
228
|
SYNCTEX_EXTERNAL_PATH: "SyncTeX 返回了项目外部的源码路径。",
|
|
217
229
|
SYNCTEX_NOT_AVAILABLE: "尚无可用的 SyncTeX 数据,请重新编译。",
|
|
218
230
|
SYMLINK_FORBIDDEN: "项目源码中不允许使用符号链接。",
|
package/dist/server/index.js
CHANGED
|
@@ -5,12 +5,16 @@ import { activeAdminCount, openDatabase } from "./db.js";
|
|
|
5
5
|
import { buildApp } from "./app.js";
|
|
6
6
|
import { assertEnvironment } from "./environment.js";
|
|
7
7
|
import { acquireDataDirectoryLock } from "./instanceLock.js";
|
|
8
|
+
import { LogRotation } from "./logRotation.js";
|
|
8
9
|
export async function startServer(configPath) {
|
|
9
10
|
const config = loadConfig(configPath);
|
|
10
11
|
const lock = acquireDataDirectoryLock(config);
|
|
11
12
|
let db = null;
|
|
12
13
|
let app = null;
|
|
14
|
+
const logs = process.env.TEXLITE_MANAGED_LOGS === "1"
|
|
15
|
+
? new LogRotation(config.dataDir, (error) => console.error("TexLite log rotation failed:", error)) : null;
|
|
13
16
|
try {
|
|
17
|
+
await logs?.start();
|
|
14
18
|
const environment = await assertEnvironment(config);
|
|
15
19
|
db = openDatabase(config);
|
|
16
20
|
if (activeAdminCount(db) === 0) {
|
|
@@ -34,6 +38,7 @@ export async function startServer(configPath) {
|
|
|
34
38
|
db?.close();
|
|
35
39
|
}
|
|
36
40
|
finally {
|
|
41
|
+
await logs?.stop();
|
|
37
42
|
lock.release();
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -50,6 +55,7 @@ export async function startServer(configPath) {
|
|
|
50
55
|
db?.close();
|
|
51
56
|
}
|
|
52
57
|
finally {
|
|
58
|
+
await logs?.stop();
|
|
53
59
|
lock.release();
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
// This intentionally recognizes only the parts of LaTeX that are clearly not
|
|
2
2
|
// prose. It is a single-pass lexer, not a parser: malformed delimiters remain
|
|
3
3
|
// narrow, so they never hide the rest of a document or cause backtracking.
|
|
4
|
+
const literalEnvironments = new Set([
|
|
5
|
+
"verbatim", "verbatim*", "bverbatim", "bverbatim*", "lverbatim", "lverbatim*", "saveverbatim", "saveverbatim*", "verbatimout",
|
|
6
|
+
"lstlisting", "minted", "minted*", "filecontents", "filecontents*", "luacode", "luacodestar", "comment"
|
|
7
|
+
]);
|
|
4
8
|
const nonProseEnvironments = new Set([
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
+
// Display and helper math environments commonly used by amsmath/mathtools.
|
|
10
|
+
"math", "displaymath", "equation", "equation*", "align", "align*", "alignat", "alignat*", "aligned", "alignedat", "gather", "gather*", "gathered", "multline", "multline*", "multlined", "flalign", "flalign*", "split", "cases", "dcases", "rcases", "drcases",
|
|
11
|
+
"array", "matrix", "pmatrix", "bmatrix", "vmatrix", "smallmatrix", "psmallmatrix", "bsmallmatrix", "vsmallmatrix",
|
|
12
|
+
// Diagram, table, and literal/code environments. Keep prose containers such
|
|
13
|
+
// as figure, table, theorem, and caption outside this list.
|
|
14
|
+
"tikzpicture", "tikzcd", "axis", "scope", "pgfonlayer", "pgfpicture",
|
|
15
|
+
"tabular", "tabularx", "tabulary", "longtable",
|
|
16
|
+
"verbatim", "verbatim*", "bverbatim", "bverbatim*", "lverbatim", "lverbatim*", "saveverbatim", "saveverbatim*", "verbatimout",
|
|
17
|
+
"lstlisting", "minted", "minted*", "filecontents", "filecontents*", "luacode", "luacodestar", "comment", "algorithmic"
|
|
9
18
|
]);
|
|
10
19
|
const opaqueArgumentCommands = new Set([
|
|
11
20
|
"documentclass", "usepackage", "requirepackage", "includegraphics", "tikzset", "pgfplotsset", "hypersetup", "lstset",
|
|
12
21
|
"label", "hypertarget", "ref", "pageref", "autoref", "nameref", "hyperref", "index", "gls", "glspl",
|
|
13
|
-
"bibliography", "addbibresource", "input", "include", "url", "path", "setlength", "setcounter", "color", "textcolor", "colorbox", "pagecolor"
|
|
22
|
+
"bibliography", "addbibresource", "input", "include", "url", "path", "setlength", "setcounter", "color", "textcolor", "colorbox", "pagecolor",
|
|
23
|
+
// These arguments are code, identifiers, or paths rather than prose.
|
|
24
|
+
"texttt", "detokenize", "lstinputlisting"
|
|
25
|
+
]);
|
|
26
|
+
/** Number of brace arguments after a macro/environment target. */
|
|
27
|
+
const macroDefinitionArgumentCounts = new Map([
|
|
28
|
+
["newcommand", 1], ["renewcommand", 1], ["providecommand", 1], ["declarerobustcommand", 1],
|
|
29
|
+
["newenvironment", 2], ["renewenvironment", 2],
|
|
30
|
+
["newdocumentcommand", 2], ["renewdocumentcommand", 2], ["providedocumentcommand", 2], ["declaredocumentcommand", 2],
|
|
31
|
+
["declaremathoperator", 1], ["declarepaireddelimiter", 2]
|
|
14
32
|
]);
|
|
15
33
|
function addRange(ranges, from, to) {
|
|
16
34
|
if (to > from)
|
|
@@ -26,7 +44,7 @@ function isCommandNameCharacter(character) {
|
|
|
26
44
|
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)
|
|
27
45
|
|| character === "@" || character === ":" || character === "_";
|
|
28
46
|
}
|
|
29
|
-
function balancedArgument(source, start, open, close) {
|
|
47
|
+
function balancedArgument(source, start, open, close, literal = false) {
|
|
30
48
|
if (source[start] !== open)
|
|
31
49
|
return null;
|
|
32
50
|
let depth = 1;
|
|
@@ -35,6 +53,13 @@ function balancedArgument(source, start, open, close) {
|
|
|
35
53
|
index += 1;
|
|
36
54
|
continue;
|
|
37
55
|
}
|
|
56
|
+
if (!literal && source[index] === "%") {
|
|
57
|
+
const lineEnd = source.indexOf("\n", index);
|
|
58
|
+
if (lineEnd < 0)
|
|
59
|
+
return null;
|
|
60
|
+
index = lineEnd;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
38
63
|
if (source[index] === open)
|
|
39
64
|
depth += 1;
|
|
40
65
|
else if (source[index] === close && --depth === 0)
|
|
@@ -48,6 +73,17 @@ function skipWhitespace(source, start) {
|
|
|
48
73
|
index += 1;
|
|
49
74
|
return index;
|
|
50
75
|
}
|
|
76
|
+
/** Skip whitespace and TeX comments between arguments while masking comments. */
|
|
77
|
+
function skipTrivia(source, start, ranges) {
|
|
78
|
+
let index = skipWhitespace(source, start);
|
|
79
|
+
while (source[index] === "%") {
|
|
80
|
+
const lineEnd = source.indexOf("\n", index);
|
|
81
|
+
const end = lineEnd < 0 ? source.length : lineEnd;
|
|
82
|
+
addRange(ranges, index, end);
|
|
83
|
+
index = skipWhitespace(source, lineEnd < 0 ? source.length : lineEnd + 1);
|
|
84
|
+
}
|
|
85
|
+
return index;
|
|
86
|
+
}
|
|
51
87
|
function readCommand(source, start) {
|
|
52
88
|
let end = start + 1;
|
|
53
89
|
if (isCommandNameCharacter(source[end]))
|
|
@@ -64,23 +100,103 @@ function isCitationCommand(name) {
|
|
|
64
100
|
const normalized = name.toLocaleLowerCase("en-US");
|
|
65
101
|
return normalized === "cite" || normalized.startsWith("cite") || normalized.endsWith("cite");
|
|
66
102
|
}
|
|
67
|
-
function
|
|
103
|
+
function opaqueArgumentCount(name) {
|
|
68
104
|
const normalized = name.toLocaleLowerCase("en-US");
|
|
69
|
-
|
|
105
|
+
if (normalized === "definecolor" || normalized === "colorlet")
|
|
106
|
+
return 3;
|
|
107
|
+
if (normalized === "mint" || normalized === "inputminted")
|
|
108
|
+
return 2;
|
|
109
|
+
return opaqueArgumentCommands.has(normalized) || isCitationCommand(normalized) || normalized === "href" ? 1 : 0;
|
|
70
110
|
}
|
|
71
|
-
function
|
|
111
|
+
function maskBraceArguments(source, start, count, ranges) {
|
|
72
112
|
let cursor = start;
|
|
73
|
-
const normalized = name.toLocaleLowerCase("en-US");
|
|
74
|
-
const count = normalized === "definecolor" || normalized === "colorlet" ? 3 : 1;
|
|
75
113
|
for (let index = 0; index < count; index += 1) {
|
|
76
114
|
const argument = balancedArgument(source, cursor, "{", "}");
|
|
77
115
|
if (!argument)
|
|
78
116
|
break;
|
|
79
117
|
addRange(ranges, argument.from, argument.to);
|
|
80
|
-
cursor =
|
|
118
|
+
cursor = skipTrivia(source, argument.to, ranges);
|
|
81
119
|
}
|
|
82
120
|
return cursor;
|
|
83
121
|
}
|
|
122
|
+
function maskMacroTarget(source, start, ranges) {
|
|
123
|
+
const bracedTarget = balancedArgument(source, start, "{", "}");
|
|
124
|
+
if (bracedTarget) {
|
|
125
|
+
addRange(ranges, bracedTarget.from, bracedTarget.to);
|
|
126
|
+
return skipTrivia(source, bracedTarget.to, ranges);
|
|
127
|
+
}
|
|
128
|
+
if (source[start] === "\\") {
|
|
129
|
+
const target = readCommand(source, start);
|
|
130
|
+
addRange(ranges, start, target.end);
|
|
131
|
+
return skipTrivia(source, target.end, ranges);
|
|
132
|
+
}
|
|
133
|
+
return start;
|
|
134
|
+
}
|
|
135
|
+
function maskMacroDefinition(source, start, definitionArgumentCount, ranges) {
|
|
136
|
+
let cursor = maskMacroTarget(source, start, ranges);
|
|
137
|
+
if (cursor === start)
|
|
138
|
+
return cursor;
|
|
139
|
+
// newcommand-style macros may have both an argument-count option and a
|
|
140
|
+
// default-argument option. Mask every immediately following option before
|
|
141
|
+
// their definition bodies, while keeping malformed input narrow.
|
|
142
|
+
for (;;) {
|
|
143
|
+
const option = balancedArgument(source, cursor, "[", "]");
|
|
144
|
+
if (!option)
|
|
145
|
+
break;
|
|
146
|
+
addRange(ranges, option.from, option.to);
|
|
147
|
+
cursor = skipTrivia(source, option.to, ranges);
|
|
148
|
+
}
|
|
149
|
+
return maskBraceArguments(source, cursor, definitionArgumentCount, ranges);
|
|
150
|
+
}
|
|
151
|
+
function maskDelimitedCode(source, start, ranges) {
|
|
152
|
+
const delimiter = source[start];
|
|
153
|
+
if (!delimiter || isWhitespace(delimiter))
|
|
154
|
+
return start;
|
|
155
|
+
// TeX's delimiter-based inline literal commands cannot span a line. If a
|
|
156
|
+
// user is still typing the closing delimiter, hide only the incomplete
|
|
157
|
+
// current line rather than accidentally suppressing all later prose until a
|
|
158
|
+
// matching character happens to occur.
|
|
159
|
+
for (let cursor = start + 1; cursor < source.length; cursor += 1) {
|
|
160
|
+
const character = source[cursor];
|
|
161
|
+
if (character === delimiter) {
|
|
162
|
+
addRange(ranges, start, cursor + 1);
|
|
163
|
+
return skipTrivia(source, cursor + 1, ranges);
|
|
164
|
+
}
|
|
165
|
+
if (character === "\r" || character === "\n") {
|
|
166
|
+
addRange(ranges, start, cursor);
|
|
167
|
+
return cursor;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
addRange(ranges, start, source.length);
|
|
171
|
+
return source.length;
|
|
172
|
+
}
|
|
173
|
+
function maskDelimitedOrBracedCode(source, start, ranges) {
|
|
174
|
+
if (source[start] !== "{")
|
|
175
|
+
return maskDelimitedCode(source, start, ranges);
|
|
176
|
+
// The brace form is also inline literal code. Preserve nested braces and
|
|
177
|
+
// escaped characters, but never let an unfinished argument conceal a later
|
|
178
|
+
// source line from the writing checker.
|
|
179
|
+
let depth = 1;
|
|
180
|
+
for (let cursor = start + 1; cursor < source.length; cursor += 1) {
|
|
181
|
+
const character = source[cursor];
|
|
182
|
+
if (character === "\\" && source[cursor + 1] !== "\r" && source[cursor + 1] !== "\n") {
|
|
183
|
+
cursor += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (character === "\r" || character === "\n") {
|
|
187
|
+
addRange(ranges, start, cursor);
|
|
188
|
+
return cursor;
|
|
189
|
+
}
|
|
190
|
+
if (character === "{")
|
|
191
|
+
depth += 1;
|
|
192
|
+
else if (character === "}" && --depth === 0) {
|
|
193
|
+
addRange(ranges, start, cursor + 1);
|
|
194
|
+
return cursor + 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
addRange(ranges, start, source.length);
|
|
198
|
+
return source.length;
|
|
199
|
+
}
|
|
84
200
|
function rawUrlEnd(source, start) {
|
|
85
201
|
const first = source[start];
|
|
86
202
|
if (first !== "h" && first !== "H" && first !== "f" && first !== "F")
|
|
@@ -115,6 +231,33 @@ export function ignoredLatexRanges(source) {
|
|
|
115
231
|
// The same technique keeps unmatched $ / \[ delimiters cheap and local.
|
|
116
232
|
let pendingMath = null;
|
|
117
233
|
for (let index = 0; index < source.length;) {
|
|
234
|
+
// An open literal environment owns its full body. Do not parse
|
|
235
|
+
// dollars, comments, or nested commands within it: literal code often
|
|
236
|
+
// contains all of those. Only look for its matching \end marker.
|
|
237
|
+
const activeNonProse = nonProseStack.at(-1);
|
|
238
|
+
if (activeNonProse && literalEnvironments.has(activeNonProse.name)) {
|
|
239
|
+
if (source[index] === "\\") {
|
|
240
|
+
const command = readCommand(source, index);
|
|
241
|
+
if (command.name === "end") {
|
|
242
|
+
const cursor = skipTrivia(source, command.end, ranges);
|
|
243
|
+
const environment = balancedArgument(source, cursor, "{", "}");
|
|
244
|
+
const environmentName = environment
|
|
245
|
+
? source.slice(environment.from + 1, environment.to - 1).trim().toLocaleLowerCase("en-US")
|
|
246
|
+
: "";
|
|
247
|
+
if (environment && environmentName === activeNonProse.name) {
|
|
248
|
+
activeNonProse.range.to = skipTrivia(source, environment.to, ranges);
|
|
249
|
+
nonProseStack.pop();
|
|
250
|
+
index = activeNonProse.range.to;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
index = Math.max(index + 1, command.end);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
index += 1;
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
118
261
|
if (pendingMath?.close.startsWith("\\") && source.startsWith(pendingMath.close, index)) {
|
|
119
262
|
pendingMath.range.to = index + pendingMath.close.length;
|
|
120
263
|
index += pendingMath.close.length;
|
|
@@ -171,44 +314,41 @@ export function ignoredLatexRanges(source) {
|
|
|
171
314
|
continue;
|
|
172
315
|
}
|
|
173
316
|
const command = readCommand(source, commandStart);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
317
|
+
const commandName = command.name.toLocaleLowerCase("en-US");
|
|
318
|
+
// Reserve the command span before scanning trivia, which may append spans.
|
|
319
|
+
const commandRange = { from: commandStart, to: command.end };
|
|
320
|
+
ranges.push(commandRange);
|
|
321
|
+
let cursor = commandName === "verb" || commandName === "lstinline"
|
|
322
|
+
? command.end : skipTrivia(source, command.end, ranges);
|
|
323
|
+
if (commandName === "verb") {
|
|
324
|
+
const options = command.name === "Verb" ? balancedArgument(source, cursor, "[", "]") : null;
|
|
325
|
+
if (options) {
|
|
326
|
+
addRange(ranges, options.from, options.to);
|
|
327
|
+
cursor = skipWhitespace(source, options.to);
|
|
184
328
|
}
|
|
329
|
+
cursor = maskDelimitedOrBracedCode(source, cursor, ranges);
|
|
185
330
|
index = Math.max(command.end, cursor);
|
|
186
331
|
continue;
|
|
187
332
|
}
|
|
188
333
|
if (command.name === "begin" || command.name === "end") {
|
|
189
334
|
const environment = balancedArgument(source, cursor, "{", "}");
|
|
190
335
|
if (!environment) {
|
|
191
|
-
addRange(ranges, commandStart, command.end);
|
|
192
336
|
index = command.end;
|
|
193
337
|
continue;
|
|
194
338
|
}
|
|
195
339
|
const environmentName = source.slice(environment.from + 1, environment.to - 1).trim().toLocaleLowerCase("en-US");
|
|
196
|
-
|
|
340
|
+
addRange(ranges, environment.from, environment.to);
|
|
341
|
+
cursor = skipTrivia(source, environment.to, ranges);
|
|
197
342
|
let options = null;
|
|
198
343
|
if (command.name === "begin") {
|
|
199
344
|
options = balancedArgument(source, cursor, "[", "]");
|
|
200
|
-
if (options)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const range = { from: commandStart, to: cursor };
|
|
204
|
-
ranges.push(range);
|
|
205
|
-
nonProseStack.push({ name: environmentName, range });
|
|
345
|
+
if (options) {
|
|
346
|
+
addRange(ranges, options.from, options.to);
|
|
347
|
+
cursor = skipTrivia(source, options.to, ranges);
|
|
206
348
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (options)
|
|
211
|
-
addRange(ranges, options.from, options.to);
|
|
349
|
+
if (nonProseEnvironments.has(environmentName)) {
|
|
350
|
+
commandRange.to = cursor;
|
|
351
|
+
nonProseStack.push({ name: environmentName, range: commandRange });
|
|
212
352
|
}
|
|
213
353
|
}
|
|
214
354
|
else {
|
|
@@ -217,22 +357,36 @@ export function ignoredLatexRanges(source) {
|
|
|
217
357
|
nonProseStack.pop();
|
|
218
358
|
open.range.to = cursor;
|
|
219
359
|
}
|
|
220
|
-
else {
|
|
221
|
-
addRange(ranges, commandStart, command.end);
|
|
222
|
-
addRange(ranges, environment.from, environment.to);
|
|
223
|
-
}
|
|
224
360
|
}
|
|
225
361
|
index = Math.max(command.end, cursor);
|
|
226
362
|
continue;
|
|
227
363
|
}
|
|
228
|
-
addRange(ranges, commandStart, command.end);
|
|
229
364
|
const options = balancedArgument(source, cursor, "[", "]");
|
|
230
365
|
if (options) {
|
|
231
366
|
addRange(ranges, options.from, options.to);
|
|
232
|
-
cursor = skipWhitespace(source, options.to);
|
|
367
|
+
cursor = commandName === "lstinline" ? skipWhitespace(source, options.to) : skipTrivia(source, options.to, ranges);
|
|
368
|
+
}
|
|
369
|
+
if (commandName === "lstinline") {
|
|
370
|
+
cursor = maskDelimitedOrBracedCode(source, cursor, ranges);
|
|
371
|
+
}
|
|
372
|
+
else if (commandName === "mintinline") {
|
|
373
|
+
const language = balancedArgument(source, cursor, "{", "}");
|
|
374
|
+
if (language) {
|
|
375
|
+
addRange(ranges, language.from, language.to);
|
|
376
|
+
cursor = maskDelimitedOrBracedCode(source, skipWhitespace(source, language.to), ranges);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
const macroDefinitionArgumentCount = macroDefinitionArgumentCounts.get(commandName);
|
|
381
|
+
if (macroDefinitionArgumentCount !== undefined) {
|
|
382
|
+
cursor = maskMacroDefinition(source, cursor, macroDefinitionArgumentCount, ranges);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const opaqueCount = opaqueArgumentCount(commandName);
|
|
386
|
+
if (opaqueCount > 0)
|
|
387
|
+
cursor = maskBraceArguments(source, cursor, opaqueCount, ranges);
|
|
388
|
+
}
|
|
233
389
|
}
|
|
234
|
-
if (masksOpaqueArguments(command.name))
|
|
235
|
-
cursor = maskOpaqueArguments(source, cursor, command.name, ranges);
|
|
236
390
|
index = Math.max(command.end, cursor);
|
|
237
391
|
}
|
|
238
392
|
return mergeOrderedRanges(ranges);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
export const LOG_CHECK_INTERVAL_MS = 60_000;
|
|
5
|
+
const LOG_BACKUPS = 3;
|
|
6
|
+
export function managedLogPaths(dataDir) {
|
|
7
|
+
const directory = path.resolve(dataDir, "logs");
|
|
8
|
+
return { stdout: path.join(directory, "stdout.log"), stderr: path.join(directory, "stderr.log") };
|
|
9
|
+
}
|
|
10
|
+
/** PM2 owns the open descriptors: copy/truncate keeps them valid across rotation. */
|
|
11
|
+
export class LogRotation {
|
|
12
|
+
dataDir;
|
|
13
|
+
onError;
|
|
14
|
+
maxBytes;
|
|
15
|
+
pending = null;
|
|
16
|
+
timer = null;
|
|
17
|
+
stopped = false;
|
|
18
|
+
constructor(dataDir, onError, maxBytes = LOG_MAX_BYTES) {
|
|
19
|
+
this.dataDir = dataDir;
|
|
20
|
+
this.onError = onError;
|
|
21
|
+
this.maxBytes = maxBytes;
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
await this.check();
|
|
25
|
+
if (!this.stopped && !this.timer) {
|
|
26
|
+
this.timer = setInterval(() => { void this.check(); }, LOG_CHECK_INTERVAL_MS);
|
|
27
|
+
this.timer.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
check() {
|
|
31
|
+
if (this.stopped)
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
if (this.pending)
|
|
34
|
+
return this.pending;
|
|
35
|
+
this.pending = this.rotateLogs().finally(() => { this.pending = null; });
|
|
36
|
+
return this.pending;
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
this.stopped = true;
|
|
40
|
+
if (this.timer)
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
await this.pending;
|
|
44
|
+
}
|
|
45
|
+
async rotateLogs() {
|
|
46
|
+
for (const file of Object.values(managedLogPaths(this.dataDir))) {
|
|
47
|
+
try {
|
|
48
|
+
await this.rotate(file);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
this.onError(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async rotate(file) {
|
|
56
|
+
const stat = await fs.lstat(file).catch((error) => {
|
|
57
|
+
if (error.code === "ENOENT")
|
|
58
|
+
return null;
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
if (!stat?.isFile() || stat.size < this.maxBytes)
|
|
62
|
+
return;
|
|
63
|
+
const temporary = `${file}.rotate.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
// Do not truncate until a complete backup has been successfully published.
|
|
66
|
+
await fs.copyFile(file, temporary);
|
|
67
|
+
await fs.rm(`${file}.${LOG_BACKUPS}`, { force: true });
|
|
68
|
+
for (let index = LOG_BACKUPS - 1; index >= 1; index--) {
|
|
69
|
+
await fs.rename(`${file}.${index}`, `${file}.${index + 1}`).catch((error) => {
|
|
70
|
+
if (error.code !== "ENOENT")
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
await fs.rename(temporary, `${file}.1`);
|
|
75
|
+
// Writes between the copy and truncate can be lost; these are diagnostic logs.
|
|
76
|
+
await fs.truncate(file, 0);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await fs.rm(temporary, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/server/pm2.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { managedLogPaths } from "./logRotation.js";
|
|
5
6
|
import { defaultConfigPath, packageRootDirectory, packageServerEntry } from "./runtimePaths.js";
|
|
6
7
|
let pm2Client = null;
|
|
7
8
|
async function loadPm2() {
|
|
@@ -64,12 +65,15 @@ export async function managedProcess(configPath) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
function startOptions(config) {
|
|
68
|
+
const logs = managedLogPaths(config.dataDir);
|
|
69
|
+
fs.mkdirSync(path.dirname(logs.stdout), { recursive: true, mode: 0o700 });
|
|
67
70
|
const packageRoot = packageRootDirectory();
|
|
68
71
|
const script = packageServerEntry();
|
|
69
72
|
const environment = {
|
|
70
73
|
...Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
|
|
71
74
|
NODE_ENV: "production",
|
|
72
75
|
TEXLITE_CONFIG: path.resolve(config.configPath),
|
|
76
|
+
TEXLITE_MANAGED_LOGS: "1",
|
|
73
77
|
TEXLITE_CLIENT_DIR: path.resolve(config.clientDir)
|
|
74
78
|
};
|
|
75
79
|
return {
|
|
@@ -78,6 +82,9 @@ function startOptions(config) {
|
|
|
78
82
|
cwd: packageRoot,
|
|
79
83
|
exec_mode: "fork",
|
|
80
84
|
instances: 1,
|
|
85
|
+
output: logs.stdout,
|
|
86
|
+
error: logs.stderr,
|
|
87
|
+
merge_logs: true,
|
|
81
88
|
autorestart: true,
|
|
82
89
|
watch: false,
|
|
83
90
|
wait_ready: true,
|