texlite 0.7.8 → 0.8.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/DESIGN.md +460 -0
- package/NPM_TESTING.md +130 -0
- package/OPERATIONS.md +246 -0
- package/README.md +78 -306
- package/README.zh-CN.md +61 -330
- package/dist/client/assets/{GitDialog-CqGyUxfP.js → GitDialog-DdLrbz_H.js} +1 -1
- package/dist/client/assets/{HistoryDialog-BMl2zxBa.js → HistoryDialog-AlF9iozX.js} +1 -1
- package/dist/client/assets/{LatexEditor-C4bFVwJZ.js → LatexEditor-DH9v1wgS.js} +1 -1
- package/dist/client/assets/{PdfPreview-BpP2LnX7.js → PdfPreview-CanYnyfJ.js} +1 -1
- package/dist/client/assets/{ProjectNavigationDialogs-B7mxG8Fz.js → ProjectNavigationDialogs-DiXtkTj1.js} +1 -1
- package/dist/client/assets/{SystemMetricsDialog-BK_K3ah8.js → SystemMetricsDialog-DimBI7er.js} +1 -1
- package/dist/client/assets/index-CdiutUgf.css +1 -0
- package/dist/client/assets/index-vpWlpUOr.js +64 -0
- package/dist/client/assets/{minus-BwzSZenU.js → minus-DR-ttrx3.js} +1 -1
- package/dist/client/assets/spellCheck-Wn7_eF9j.js +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/app.js +3 -3
- package/dist/server/harper.js +210 -119
- package/dist/server/latexSpellMask.js +257 -0
- package/dist/server/projects.js +0 -12
- package/dist/server/routes/projectShared.js +1 -1
- package/dist/server/routes/projects.js +20 -19
- package/package.json +4 -2
- package/dist/client/assets/index-B9n8hfV0.js +0 -64
- package/dist/client/assets/index-BhyTKMoO.css +0 -1
- package/dist/client/assets/spellCheck-CvOAlLWp.js +0 -4
package/dist/server/projects.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
export function accessibleProject(db, projectId, user) {
|
|
2
|
-
if (user.role === "admin") {
|
|
3
|
-
const row = db.prepare(`
|
|
4
|
-
SELECT p.*, 'owner' AS permission, owner.username AS owner_username,
|
|
5
|
-
owner.display_name AS owner_display_name,
|
|
6
|
-
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name
|
|
7
|
-
FROM projects p
|
|
8
|
-
JOIN users owner ON owner.id = p.owner_id
|
|
9
|
-
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
10
|
-
WHERE p.id = ?
|
|
11
|
-
`).get(projectId);
|
|
12
|
-
return row ?? null;
|
|
13
|
-
}
|
|
14
2
|
const row = db.prepare(`
|
|
15
3
|
SELECT p.*,
|
|
16
4
|
CASE WHEN p.owner_id = ? THEN 'owner' ELSE pm.permission END AS permission,
|
|
@@ -129,7 +129,7 @@ export function requireEditableProject(db, projectId, user) {
|
|
|
129
129
|
throw httpError(403, "PROJECT_EDIT_FORBIDDEN");
|
|
130
130
|
return project;
|
|
131
131
|
}
|
|
132
|
-
/** Owner permission
|
|
132
|
+
/** Owner permission is granted only to the stored project owner. */
|
|
133
133
|
export function requireProjectOwnerPermission(db, projectId, user) {
|
|
134
134
|
requireActiveUser(db, user);
|
|
135
135
|
const project = accessibleProject(db, projectId, user);
|
|
@@ -9,6 +9,7 @@ import { isMainDocumentCandidate } from "../latexRoot.js";
|
|
|
9
9
|
import { accessibleProject, canEdit } from "../projects.js";
|
|
10
10
|
import { writeProjectArchive } from "../archive.js";
|
|
11
11
|
import { extractProjectZip, ZipValidationError } from "../zip.js";
|
|
12
|
+
import { HarperUnavailableError } from "../harper.js";
|
|
12
13
|
import { commentsSummaryForProject, commentsSummaryForProjects, dictionaryWord, escapeLikePattern, now, projectJson, requireProjectOwnerPermission, tagColors, tagsForProject, tagsForProjects, text } from "./projectShared.js";
|
|
13
14
|
/** Register project catalog, metadata, archive, dictionary, tag, export, and deletion routes. */
|
|
14
15
|
export function registerProjectCatalogRoutes(app, context) {
|
|
@@ -50,14 +51,11 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
50
51
|
const archiveCondition = archivedOnly
|
|
51
52
|
? "EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)"
|
|
52
53
|
: "NOT EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)";
|
|
53
|
-
const from =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: `FROM projects p JOIN users owner ON owner.id = p.owner_id
|
|
57
|
-
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
58
|
-
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
|
|
54
|
+
const from = `FROM projects p JOIN users owner ON owner.id = p.owner_id
|
|
55
|
+
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
56
|
+
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
|
|
59
57
|
const conditions = [
|
|
60
|
-
|
|
58
|
+
"(p.owner_id = :userId OR pm.user_id = :userId)",
|
|
61
59
|
archiveCondition
|
|
62
60
|
];
|
|
63
61
|
const params = { userId: user.id };
|
|
@@ -79,14 +77,10 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
79
77
|
const totalPages = Math.ceil(total / pageSize);
|
|
80
78
|
const currentPage = totalPages === 0 ? 1 : Math.min(page, totalPages);
|
|
81
79
|
const rowsParams = { ...params, limit: pageSize, offset: (currentPage - 1) * pageSize };
|
|
82
|
-
const select =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: `SELECT DISTINCT p.*,
|
|
87
|
-
CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
|
|
88
|
-
owner.username AS owner_username, owner.display_name AS owner_display_name,
|
|
89
|
-
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
|
|
80
|
+
const select = `SELECT DISTINCT p.*,
|
|
81
|
+
CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
|
|
82
|
+
owner.username AS owner_username, owner.display_name AS owner_display_name,
|
|
83
|
+
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
|
|
90
84
|
const rows = db.prepare(`${select} ${from} ${where}
|
|
91
85
|
ORDER BY ${sortColumn} DESC, p.name COLLATE NOCASE ASC
|
|
92
86
|
LIMIT :limit OFFSET :offset`).all(rowsParams);
|
|
@@ -259,16 +253,21 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
259
253
|
if (!accessibleProject(db, id, user))
|
|
260
254
|
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
261
255
|
const body = request.body;
|
|
262
|
-
if (typeof body?.source !== "string")
|
|
256
|
+
if (typeof body?.source !== "string" || typeof body.path !== "string")
|
|
263
257
|
return apiError(reply, 400, "SPELLCHECK_SOURCE_INVALID");
|
|
264
258
|
if (Buffer.byteLength(body.source, "utf8") > maxCollaborativeFileBytes(config)) {
|
|
265
259
|
return apiError(reply, 413, "SPELLCHECK_SOURCE_TOO_LARGE");
|
|
266
260
|
}
|
|
267
261
|
try {
|
|
268
|
-
return { lints: await harper.lint(body.source) };
|
|
262
|
+
return { lints: await harper.lint(body.source, body.path) };
|
|
269
263
|
}
|
|
270
264
|
catch (error) {
|
|
271
|
-
|
|
265
|
+
// A missing optional command is an expected fallback condition. Keep it
|
|
266
|
+
// out of normal logs while preserving diagnostics for an actual failure.
|
|
267
|
+
if (error instanceof HarperUnavailableError)
|
|
268
|
+
request.log.debug({ projectId: id }, "Host Harper CLI unavailable; using browser fallback");
|
|
269
|
+
else
|
|
270
|
+
request.log.error({ err: error, projectId: id }, "Harper writing check failed");
|
|
272
271
|
return apiError(reply, 503, "HARPER_UNAVAILABLE");
|
|
273
272
|
}
|
|
274
273
|
});
|
|
@@ -459,7 +458,9 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
459
458
|
return;
|
|
460
459
|
const { id } = request.params;
|
|
461
460
|
const project = accessibleProject(db, id, user);
|
|
462
|
-
if (!project
|
|
461
|
+
if (!project)
|
|
462
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
463
|
+
if (project.permission !== "owner")
|
|
463
464
|
return apiError(reply, 403, "PROJECT_DELETE_FORBIDDEN");
|
|
464
465
|
return await projectMutations.runExclusive(id, "project deletion", () => {
|
|
465
466
|
collaboration.resetProject(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "texlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A lightweight local-first collaborative LaTeX workspace",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"README.md",
|
|
26
26
|
"README.zh-CN.md",
|
|
27
|
+
"OPERATIONS.md",
|
|
28
|
+
"DESIGN.md",
|
|
29
|
+
"NPM_TESTING.md",
|
|
27
30
|
"LICENSE",
|
|
28
31
|
"preview.png",
|
|
29
32
|
"texlite.config.example.json"
|
|
@@ -72,7 +75,6 @@
|
|
|
72
75
|
"bibtex-tidy": "1.14.0",
|
|
73
76
|
"diff-match-patch": "^1.0.5",
|
|
74
77
|
"fastify": "^5.4.0",
|
|
75
|
-
"harper.js": "^2.7.0",
|
|
76
78
|
"i18next": "^25.3.2",
|
|
77
79
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
78
80
|
"lib0": "^0.2.117",
|