texlite 0.10.0 → 0.12.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 (75) hide show
  1. package/DESIGN.md +44 -26
  2. package/OPERATIONS.md +12 -6
  3. package/README.md +9 -4
  4. package/README.zh-CN.md +7 -4
  5. package/dist/client/assets/AdminUsers-DMsBqC48.js +1 -0
  6. package/dist/client/assets/{CitationLibraryDialog-CU2b0xtm.js → CitationLibraryDialog-C_333dTc.js} +1 -1
  7. package/dist/client/assets/Dashboard-EOaNnS1A.js +2 -0
  8. package/dist/client/assets/GitDialog-DU-sVP-o.js +3 -0
  9. package/dist/client/assets/GitHubAccountDialog-IrB3M7nt.js +1 -0
  10. package/dist/client/assets/HistoryDialog-CDUbT7yG.js +7 -0
  11. package/dist/client/assets/LatexEditor-BYgW2otE.js +138 -0
  12. package/dist/client/assets/PdfPreview--HsIb8Fn.js +1 -0
  13. package/dist/client/assets/ProjectIconPickerDialog-CaF7Kkda.js +1 -0
  14. package/dist/client/assets/{ProjectNavigationDialogs-_JY0yVob.js → ProjectNavigationDialogs-3Ce7-tVp.js} +1 -1
  15. package/dist/client/assets/ProjectSettings-9hYSUKDo.js +1 -0
  16. package/dist/client/assets/ProjectWorkspace-D9zmy0KR.js +32 -0
  17. package/dist/client/assets/{SelectionHistoryDialog-D8CeqjLD.js → SelectionHistoryDialog-COpXO4kL.js} +2 -2
  18. package/dist/client/assets/SystemMetricsDialog-D9dthbAB.js +1 -0
  19. package/dist/client/assets/{TagManagementDialog-4R1Gs43j.js → TagManagementDialog-S8zDQAaB.js} +1 -1
  20. package/dist/client/assets/{WordCountDialog-C-MCscAu.js → WordCountDialog-CMsFa_pM.js} +1 -1
  21. package/dist/client/assets/check-Hv1kodh8.js +1 -0
  22. package/dist/client/assets/chktex-BFELQm91.js +3 -0
  23. package/dist/client/assets/{editor-B9HNwtsd.js → editor-taw1iptB.js} +1 -1
  24. package/dist/client/assets/external-link-C2GCFJxP.js +1 -0
  25. package/dist/client/assets/github-B_Eh2y6s.js +1 -0
  26. package/dist/client/assets/{hard-drive-Bsk-RETn.js → hard-drive-bzz70DQ5.js} +1 -1
  27. package/dist/client/assets/{index-Csp5J5aI.js → index-CXtmuEUm.js} +1 -1
  28. package/dist/client/assets/index-CxrDisif.css +1 -0
  29. package/dist/client/assets/index-Dik-2GCl.js +10 -0
  30. package/dist/client/assets/{minimize-2-92UqFWI9.js → minimize-2-CmPZ9Iwp.js} +1 -1
  31. package/dist/client/assets/{minus-bx4ZLZnb.js → minus-Dzy8rgfA.js} +1 -1
  32. package/dist/client/assets/{plus-DQg_bV50.js → plus-DlX82JCl.js} +1 -1
  33. package/dist/client/assets/{refresh-cw-BoMEkk5J.js → refresh-cw-BWndInnG.js} +1 -1
  34. package/dist/client/assets/{search-UZspmceH.js → search-DMqOy-Ex.js} +1 -1
  35. package/dist/client/assets/{sigma-C0IvFdKV.js → sigma-B0zOJKVj.js} +1 -1
  36. package/dist/client/assets/{spellCheck-DyKIHf5C.js → spellCheck-BjkrQxRU.js} +1 -1
  37. package/dist/client/assets/{tags-BTUtvaAc.js → tags-XvdXg-yc.js} +1 -1
  38. package/dist/client/assets/{type-XvvfEJRs.js → type-DsGlKUKR.js} +1 -1
  39. package/dist/client/assets/{user-plus-Chr9T97G.js → user-plus-Bcq_EsqH.js} +1 -1
  40. package/dist/client/assets/{x-DlFXD9Vl.js → x-BJOMjm8Z.js} +1 -1
  41. package/dist/client/index.html +3 -3
  42. package/dist/server/app.js +8 -0
  43. package/dist/server/chktex.js +377 -0
  44. package/dist/server/collaboration.js +6 -0
  45. package/dist/server/db.js +35 -1
  46. package/dist/server/environment.js +1 -0
  47. package/dist/server/git.js +64 -29
  48. package/dist/server/harper.js +14 -0
  49. package/dist/server/history.js +18 -9
  50. package/dist/server/i18n.js +14 -4
  51. package/dist/server/projectImportUploads.js +133 -0
  52. package/dist/server/routes/projectFiles.js +3 -0
  53. package/dist/server/routes/projectGit.js +10 -11
  54. package/dist/server/routes/projectHistory.js +32 -5
  55. package/dist/server/routes/projectMembers.js +0 -5
  56. package/dist/server/routes/projectShared.js +1 -0
  57. package/dist/server/routes/projects.js +97 -15
  58. package/dist/server/routes/users.js +0 -2
  59. package/dist/shared/historyFiles.js +6 -0
  60. package/dist/shared/writingChecks.js +4 -0
  61. package/package.json +1 -1
  62. package/dist/client/assets/AdminUsers-C7oU_1Y_.js +0 -1
  63. package/dist/client/assets/Dashboard-BB4RO4x2.js +0 -2
  64. package/dist/client/assets/GitDialog-C2Np1i8v.js +0 -3
  65. package/dist/client/assets/HistoryDialog-CdoYeLyX.js +0 -7
  66. package/dist/client/assets/LatexEditor-Dmy6_gqt.js +0 -138
  67. package/dist/client/assets/PdfPreview-fDxKh6hP.js +0 -1
  68. package/dist/client/assets/ProjectIconPickerDialog-CsjMKq7h.js +0 -1
  69. package/dist/client/assets/ProjectSettings-DXGA-lmB.js +0 -1
  70. package/dist/client/assets/ProjectWorkspace-mCYjyc6e.js +0 -32
  71. package/dist/client/assets/SystemMetricsDialog-BZx7ZMQt.js +0 -1
  72. package/dist/client/assets/check-Jf8WBxDX.js +0 -1
  73. package/dist/client/assets/index-B6rP7QsM.js +0 -12
  74. package/dist/client/assets/index-DGZNAyxW.css +0 -1
  75. package/dist/client/assets/key-round-DqptHHKC.js +0 -1
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const n=e("Minimize2",[["polyline",{points:"4 14 10 14 10 20",key:"11kfnr"}],["polyline",{points:"20 10 14 10 14 4",key:"rlmsce"}],["line",{x1:"14",x2:"21",y1:"10",y2:"3",key:"o5lafz"}],["line",{x1:"3",x2:"10",y1:"21",y2:"14",key:"1atl0r"}]]);export{n as M};
1
+ import{c as e}from"./index-Dik-2GCl.js";const n=e("Minimize2",[["polyline",{points:"4 14 10 14 10 20",key:"11kfnr"}],["polyline",{points:"20 10 14 10 14 4",key:"rlmsce"}],["line",{x1:"14",x2:"21",y1:"10",y2:"3",key:"o5lafz"}],["line",{x1:"3",x2:"10",y1:"21",y2:"14",key:"1atl0r"}]]);export{n as M};
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const n=e("Maximize2",[["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["polyline",{points:"9 21 3 21 3 15",key:"1avn1i"}],["line",{x1:"21",x2:"14",y1:"3",y2:"10",key:"ota7mn"}],["line",{x1:"3",x2:"10",y1:"21",y2:"14",key:"1atl0r"}]]);const a=e("Minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]);export{a as M,n as a};
1
+ import{c as e}from"./index-Dik-2GCl.js";const n=e("Maximize2",[["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["polyline",{points:"9 21 3 21 3 15",key:"1avn1i"}],["line",{x1:"21",x2:"14",y1:"3",y2:"10",key:"ota7mn"}],["line",{x1:"3",x2:"10",y1:"21",y2:"14",key:"1atl0r"}]]);const a=e("Minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]);export{a as M,n as a};
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const a=e("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);export{a as P};
1
+ import{c as e}from"./index-Dik-2GCl.js";const a=e("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);export{a as P};
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const t=e("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{t as R};
1
+ import{c as e}from"./index-Dik-2GCl.js";const t=e("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{t as R};
@@ -1 +1 @@
1
- import{c}from"./index-B6rP7QsM.js";const r=c("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);export{r as S};
1
+ import{c}from"./index-Dik-2GCl.js";const r=c("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);export{r as S};
@@ -1 +1 @@
1
- import{c as a}from"./index-B6rP7QsM.js";const e=a("Sigma",[["path",{d:"M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2",key:"wuwx1p"}]]);export{e as S};
1
+ import{c as a}from"./index-Dik-2GCl.js";const e=a("Sigma",[["path",{d:"M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2",key:"wuwx1p"}]]);export{e as S};
@@ -1 +1 @@
1
- import{g as w,A as L}from"./index-B6rP7QsM.js";import{s as y,c as S}from"./ProjectWorkspace-mCYjyc6e.js";import"./react-BVUjbTTn.js";import"./check-Jf8WBxDX.js";import"./x-DlFXD9Vl.js";import"./search-UZspmceH.js";import"./user-plus-Chr9T97G.js";import"./refresh-cw-BoMEkk5J.js";const k=5,f=new Map,C=12,x=S();let E=0;class p extends Error{constructor(){super("A newer writing check replaced this request."),this.name="HarperLintSupersededError"}}function I(e){return e instanceof p}function $(e){const t=[0];let r=0;for(const n of e)r+=n.length,t.push(r);return t}function d(e){return[...new Set(e.map(t=>t.trim()).filter(Boolean))].sort((t,r)=>t.localeCompare(r)).join("\0")}async function v(e,t,r,n){try{return(await w(`/api/projects/${e}/spellcheck`,{method:"POST",body:JSON.stringify({path:t,source:r,clientId:x,sequence:n})})).lints}catch(o){throw o instanceof L&&o.code==="SPELLCHECK_SUPERSEDED"?new p:o}}function A(e,t,r){if(!t.problem)return null;const n={from:t.start,to:t.end},o={from:r[t.start]??-1,to:r[t.end]??-1},i=c=>c.from>=0&&c.to>c.from&&c.to<=e.length,a=c=>i(c)&&e.slice(c.from,c.to)===t.problem;return a(o)?o:a(n)?n:null}function H(e,t,r){const n=A(e,t,r);if(!n)return null;const{from:o,to:i}=n,a=e.slice(o,i);if(!a.trim()||!/[A-Za-z]/.test(a)||t.kind==="Formatting")return null;const c=t.kind==="Spelling"||t.kind==="Typo"?"spelling":"grammar",u=t.suggestions.filter((m,s,l)=>s<k&&l.indexOf(m)===s);return{from:o,to:i,word:a,kind:c,message:t.message,suggestions:u}}function h(e,t){let r=2166136261;for(let n=0;n<t.length;n+=1)r^=t.charCodeAt(n),r=Math.imul(r,16777619);return`${e}\0${t.length}:${r>>>0}`}function g(e,t,r){const n=h(e,t),o=f.get(n);return!o||o.path!==e||o.source!==t||o.dictionary!==r?null:(f.delete(n),f.set(n,o),o.issues)}function R(e,t,r,n){const o=h(e,t);for(f.set(o,{path:e,source:t,dictionary:r,issues:n});f.size>C;){const i=f.keys().next().value;if(i===void 0)break;f.delete(i)}}async function b(e,t,r,n="main.tex"){const o=d(t),i=g(n,e,o);if(i)return i;const a=$(e),c=new Set(t.map(s=>s.trim().toLocaleLowerCase("en-US")).filter(Boolean)),u=new Set,m=r.map(s=>H(e,s,a)).filter(s=>!!s).filter(s=>s.kind!=="spelling"||!c.has(s.word.trim().toLocaleLowerCase("en-US"))).filter(s=>{const l=`${s.from}:${s.to}:${s.kind}`;return u.has(l)?!1:(u.add(l),!0)}).sort((s,l)=>s.from-l.from||s.to-l.to);return R(n,e,o,m),m}async function P(e,t,r,n){const o=d(n),i=g(t,r,o);if(i)return i;const a=await v(e,t,r,++E);return b(r,n,a,t)}function N(e,t,r,n=[]){return y(t)?P(e,t,r,n):Promise.resolve([])}export{p as HarperLintSupersededError,I as isHarperLintSupersededError,N as lintLatex,b as mapLatexLints};
1
+ import{g as w,A as L}from"./index-Dik-2GCl.js";import{s as y,c as S}from"./ProjectWorkspace-D9zmy0KR.js";import"./react-BVUjbTTn.js";import"./check-Hv1kodh8.js";import"./x-BJOMjm8Z.js";import"./search-DMqOy-Ex.js";import"./user-plus-Bcq_EsqH.js";import"./refresh-cw-BWndInnG.js";const k=5,f=new Map,C=12,x=S();let E=0;class p extends Error{constructor(){super("A newer writing check replaced this request."),this.name="HarperLintSupersededError"}}function I(e){return e instanceof p}function $(e){const t=[0];let r=0;for(const n of e)r+=n.length,t.push(r);return t}function d(e){return[...new Set(e.map(t=>t.trim()).filter(Boolean))].sort((t,r)=>t.localeCompare(r)).join("\0")}async function v(e,t,r,n){try{return(await w(`/api/projects/${e}/spellcheck`,{method:"POST",body:JSON.stringify({path:t,source:r,clientId:x,sequence:n})})).lints}catch(o){throw o instanceof L&&o.code==="SPELLCHECK_SUPERSEDED"?new p:o}}function A(e,t,r){if(!t.problem)return null;const n={from:t.start,to:t.end},o={from:r[t.start]??-1,to:r[t.end]??-1},i=c=>c.from>=0&&c.to>c.from&&c.to<=e.length,a=c=>i(c)&&e.slice(c.from,c.to)===t.problem;return a(o)?o:a(n)?n:null}function H(e,t,r){const n=A(e,t,r);if(!n)return null;const{from:o,to:i}=n,a=e.slice(o,i);if(!a.trim()||!/[A-Za-z]/.test(a)||t.kind==="Formatting")return null;const c=t.kind==="Spelling"||t.kind==="Typo"?"spelling":"grammar",u=t.suggestions.filter((m,s,l)=>s<k&&l.indexOf(m)===s);return{from:o,to:i,word:a,kind:c,message:t.message,suggestions:u}}function h(e,t){let r=2166136261;for(let n=0;n<t.length;n+=1)r^=t.charCodeAt(n),r=Math.imul(r,16777619);return`${e}\0${t.length}:${r>>>0}`}function g(e,t,r){const n=h(e,t),o=f.get(n);return!o||o.path!==e||o.source!==t||o.dictionary!==r?null:(f.delete(n),f.set(n,o),o.issues)}function R(e,t,r,n){const o=h(e,t);for(f.set(o,{path:e,source:t,dictionary:r,issues:n});f.size>C;){const i=f.keys().next().value;if(i===void 0)break;f.delete(i)}}async function b(e,t,r,n="main.tex"){const o=d(t),i=g(n,e,o);if(i)return i;const a=$(e),c=new Set(t.map(s=>s.trim().toLocaleLowerCase("en-US")).filter(Boolean)),u=new Set,m=r.map(s=>H(e,s,a)).filter(s=>!!s).filter(s=>s.kind!=="spelling"||!c.has(s.word.trim().toLocaleLowerCase("en-US"))).filter(s=>{const l=`${s.from}:${s.to}:${s.kind}`;return u.has(l)?!1:(u.add(l),!0)}).sort((s,l)=>s.from-l.from||s.to-l.to);return R(n,e,o,m),m}async function P(e,t,r,n){const o=d(n),i=g(t,r,o);if(i)return i;const a=await v(e,t,r,++E);return b(r,n,a,t)}function N(e,t,r,n=[]){return y(t)?P(e,t,r,n):Promise.resolve([])}export{p as HarperLintSupersededError,I as isHarperLintSupersededError,N as lintLatex,b as mapLatexLints};
@@ -1 +1 @@
1
- import{c as a}from"./index-B6rP7QsM.js";const e=a("Tags",[["path",{d:"m15 5 6.3 6.3a2.4 2.4 0 0 1 0 3.4L17 19",key:"1cbfv1"}],["path",{d:"M9.586 5.586A2 2 0 0 0 8.172 5H3a1 1 0 0 0-1 1v5.172a2 2 0 0 0 .586 1.414L8.29 18.29a2.426 2.426 0 0 0 3.42 0l3.58-3.58a2.426 2.426 0 0 0 0-3.42z",key:"135mg7"}],["circle",{cx:"6.5",cy:"9.5",r:".5",fill:"currentColor",key:"5pm5xn"}]]);export{e as T};
1
+ import{c as a}from"./index-Dik-2GCl.js";const e=a("Tags",[["path",{d:"m15 5 6.3 6.3a2.4 2.4 0 0 1 0 3.4L17 19",key:"1cbfv1"}],["path",{d:"M9.586 5.586A2 2 0 0 0 8.172 5H3a1 1 0 0 0-1 1v5.172a2 2 0 0 0 .586 1.414L8.29 18.29a2.426 2.426 0 0 0 3.42 0l3.58-3.58a2.426 2.426 0 0 0 0-3.42z",key:"135mg7"}],["circle",{cx:"6.5",cy:"9.5",r:".5",fill:"currentColor",key:"5pm5xn"}]]);export{e as T};
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const n=e("Type",[["polyline",{points:"4 7 4 4 20 4 20 7",key:"1nosan"}],["line",{x1:"9",x2:"15",y1:"20",y2:"20",key:"swin9y"}],["line",{x1:"12",x2:"12",y1:"4",y2:"20",key:"1tx1rr"}]]);export{n as T};
1
+ import{c as e}from"./index-Dik-2GCl.js";const n=e("Type",[["polyline",{points:"4 7 4 4 20 4 20 7",key:"1nosan"}],["line",{x1:"9",x2:"15",y1:"20",y2:"20",key:"swin9y"}],["line",{x1:"12",x2:"12",y1:"4",y2:"20",key:"1tx1rr"}]]);export{n as T};
@@ -1 +1 @@
1
- import{c as e}from"./index-B6rP7QsM.js";const c=e("UserPlus",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14",key:"1bvyxn"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11",key:"1shjgl"}]]);export{c as U};
1
+ import{c as e}from"./index-Dik-2GCl.js";const c=e("UserPlus",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14",key:"1bvyxn"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11",key:"1shjgl"}]]);export{c as U};
@@ -1 +1 @@
1
- import{c as a}from"./index-B6rP7QsM.js";const t=a("ArrowLeft",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);const h=a("AtSign",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8",key:"7n84p3"}]]);const y=a("BookMarked",[["path",{d:"M10 2v8l3-3 3 3V2",key:"sqw3rj"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20",key:"k3hazp"}]]);const k=a("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);const d=a("ChevronLeft",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]);const p=a("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);const c=a("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);const s=a("FileArchive",[["path",{d:"M10 12v-1",key:"v7bkov"}],["path",{d:"M10 18v-2",key:"1cjy8d"}],["path",{d:"M10 7V6",key:"dljcrl"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01",key:"gkbcor"}],["circle",{cx:"10",cy:"20",r:"2",key:"1xzdoj"}]]);const r=a("FileCode2",[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4",key:"1pf5j1"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"m5 12-3 3 3 3",key:"oke12k"}],["path",{d:"m9 18 3-3-3-3",key:"112psh"}]]);const o=a("FileSearch",[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3",key:"ms7g94"}],["path",{d:"m9 18-1.5-1.5",key:"1j6qii"}],["circle",{cx:"5",cy:"14",r:"3",key:"ufru5t"}]]);const l=a("FileSpreadsheet",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M8 13h2",key:"yr2amv"}],["path",{d:"M14 13h2",key:"un5t4a"}],["path",{d:"M8 17h2",key:"2yhykz"}],["path",{d:"M14 17h2",key:"10kma7"}]]);const n=a("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);const M=a("FolderPlus",[["path",{d:"M12 10v6",key:"1bos4e"}],["path",{d:"M9 13h6",key:"1uhe8q"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);const i=a("Folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);const v=a("GitBranch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]);const x=a("ListTree",[["path",{d:"M21 12h-8",key:"1bmf0i"}],["path",{d:"M21 6H8",key:"1pqkrb"}],["path",{d:"M21 18h-8",key:"1tm79t"}],["path",{d:"M3 6v4c0 1.1.9 2 2 2h3",key:"1ywdgy"}],["path",{d:"M3 10v6c0 1.1.9 2 2 2h3",key:"2wc746"}]]);const m=a("MessageSquare",[["path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",key:"1lielz"}]]);const H=a("NotepadText",[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M12 2v4",key:"3427ic"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"16",height:"18",x:"4",y:"4",rx:"2",key:"1u9h20"}],["path",{d:"M8 10h6",key:"3oa6kw"}],["path",{d:"M8 14h8",key:"1fgep2"}],["path",{d:"M8 18h5",key:"17enja"}]]);const q=a("Pencil",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]);const u=a("ScrollText",[["path",{d:"M15 12h-5",key:"r7krc0"}],["path",{d:"M15 8h-5",key:"1khuty"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4",key:"zz82l3"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3",key:"1ph1d7"}]]);const g=a("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);const f=a("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);const b=a("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);const w=a("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);const z=a("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);export{h as A,y as B,p as C,c as D,o as F,v as G,x as L,m as M,H as N,q as P,u as S,g as T,w as U,z as X,i as a,l as b,r as c,k as d,n as e,t as f,b as g,M as h,d as i,s as j,f as k};
1
+ import{c as a}from"./index-Dik-2GCl.js";const t=a("ArrowLeft",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);const h=a("AtSign",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8",key:"7n84p3"}]]);const y=a("BookMarked",[["path",{d:"M10 2v8l3-3 3 3V2",key:"sqw3rj"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20",key:"k3hazp"}]]);const k=a("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);const d=a("ChevronLeft",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]);const p=a("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);const c=a("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);const s=a("FileArchive",[["path",{d:"M10 12v-1",key:"v7bkov"}],["path",{d:"M10 18v-2",key:"1cjy8d"}],["path",{d:"M10 7V6",key:"dljcrl"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01",key:"gkbcor"}],["circle",{cx:"10",cy:"20",r:"2",key:"1xzdoj"}]]);const r=a("FileCode2",[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4",key:"1pf5j1"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"m5 12-3 3 3 3",key:"oke12k"}],["path",{d:"m9 18 3-3-3-3",key:"112psh"}]]);const o=a("FileSearch",[["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3",key:"ms7g94"}],["path",{d:"m9 18-1.5-1.5",key:"1j6qii"}],["circle",{cx:"5",cy:"14",r:"3",key:"ufru5t"}]]);const l=a("FileSpreadsheet",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M8 13h2",key:"yr2amv"}],["path",{d:"M14 13h2",key:"un5t4a"}],["path",{d:"M8 17h2",key:"2yhykz"}],["path",{d:"M14 17h2",key:"10kma7"}]]);const n=a("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);const M=a("FolderPlus",[["path",{d:"M12 10v6",key:"1bos4e"}],["path",{d:"M9 13h6",key:"1uhe8q"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);const i=a("Folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);const v=a("GitBranch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]);const x=a("ListTree",[["path",{d:"M21 12h-8",key:"1bmf0i"}],["path",{d:"M21 6H8",key:"1pqkrb"}],["path",{d:"M21 18h-8",key:"1tm79t"}],["path",{d:"M3 6v4c0 1.1.9 2 2 2h3",key:"1ywdgy"}],["path",{d:"M3 10v6c0 1.1.9 2 2 2h3",key:"2wc746"}]]);const m=a("MessageSquare",[["path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",key:"1lielz"}]]);const H=a("NotepadText",[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M12 2v4",key:"3427ic"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"16",height:"18",x:"4",y:"4",rx:"2",key:"1u9h20"}],["path",{d:"M8 10h6",key:"3oa6kw"}],["path",{d:"M8 14h8",key:"1fgep2"}],["path",{d:"M8 18h5",key:"17enja"}]]);const q=a("Pencil",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]);const u=a("ScrollText",[["path",{d:"M15 12h-5",key:"r7krc0"}],["path",{d:"M15 8h-5",key:"1khuty"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4",key:"zz82l3"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3",key:"1ph1d7"}]]);const g=a("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);const f=a("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);const b=a("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);const w=a("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);const z=a("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);export{h as A,y as B,p as C,c as D,o as F,v as G,x as L,m as M,H as N,q as P,u as S,g as T,w as U,z as X,i as a,l as b,r as c,k as d,n as e,t as f,b as g,M as h,d as i,s as j,f as k};
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="zh-CN">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,9 +12,9 @@
12
12
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka@5.2.5/400.css" />
13
13
  <link rel="icon" type="image/svg+xml" href="logo.svg" />
14
14
  <title>TexLite</title>
15
- <script type="module" crossorigin src="./assets/index-B6rP7QsM.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-Dik-2GCl.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/react-BVUjbTTn.js">
17
- <link rel="stylesheet" crossorigin href="./assets/index-DGZNAyxW.css">
17
+ <link rel="stylesheet" crossorigin href="./assets/index-CxrDisif.css">
18
18
  </head>
19
19
  <body>
20
20
  <div id="root"></div>
@@ -37,6 +37,7 @@ import { registerSystemRoutes } from "./routes/system.js";
37
37
  import { registerUserManagementRoutes } from "./routes/users.js";
38
38
  import { registerWordCountRoutes } from "./routes/wordCount.js";
39
39
  import { HarperService } from "./harper.js";
40
+ import { ChktexService } from "./chktex.js";
40
41
  import { TexcountService } from "./texcount.js";
41
42
  import { basePathHref, basePathPrefix, withoutBasePath } from "../shared/basePath.js";
42
43
  // Retained public helper for callers and tests; implementation lives with the file routes.
@@ -86,10 +87,15 @@ export async function buildApp(config, db, options = {}) {
86
87
  const latexCompletions = new LatexCompletionService(config);
87
88
  const projectOutlines = new ProjectOutlineService(config);
88
89
  const harper = new HarperService();
90
+ const chktex = new ChktexService();
89
91
  const texcount = new TexcountService();
90
92
  // Probe the optional host Harper CLI without delaying startup. Its absence is
91
93
  // supported: the browser spellchecker remains the writing-check fallback.
92
94
  void harper.preload().catch((error) => app.log.info({ err: error }, "Optional Harper CLI is unavailable"));
95
+ // ChkTeX is optional and disabled by default. Probe it in the background so
96
+ // enabling the project setting does not make the first editor request pay a
97
+ // process-discovery cost.
98
+ void chktex.preload().catch((error) => app.log.info({ err: error }, "Optional ChkTeX command is unavailable"));
93
99
  const failedSnapshots = new Set();
94
100
  const failedEdits = new Map();
95
101
  const signalHistory = (id) => collaboration.setHistoryWarning(id, failedSnapshots.has(id) || failedEdits.has(id));
@@ -175,6 +181,7 @@ export async function buildApp(config, db, options = {}) {
175
181
  historyRetention.dispose();
176
182
  editRetry.dispose();
177
183
  await harper.dispose();
184
+ await chktex.dispose();
178
185
  await texcount.dispose();
179
186
  });
180
187
  await app.register(cookie, { hook: "onRequest" });
@@ -288,6 +295,7 @@ export async function buildApp(config, db, options = {}) {
288
295
  latexCompletions,
289
296
  projectOutlines,
290
297
  harper,
298
+ chktex,
291
299
  recordHistory
292
300
  });
293
301
  registerWordCountRoutes(routes, { config, db, projectMutations, texcount });
@@ -0,0 +1,377 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { detachedProcessGroup, killProcessGroup } from "./processTree.js";
4
+ const commandProbeTimeoutMs = 5_000;
5
+ const checkTimeoutMs = 5_000;
6
+ const maxOutputBytes = 2 * 1024 * 1024;
7
+ const maxCachedSourceBytes = 512 * 1024;
8
+ const maxCachedResults = 24;
9
+ const cacheTtlMs = 15_000;
10
+ const unavailableRetryMs = 15_000;
11
+ const laneSequenceRetentionMs = 10 * 60_000;
12
+ const maxTrackedLaneSequences = 512;
13
+ const maxConcurrency = 8;
14
+ // ChkTeX does not append a record separator to a custom -f format. Without
15
+ // this newline, consecutive diagnostics run together and only the first one
16
+ // survives parseChktexOutput().
17
+ const outputFormat = "%f|%l|%c|%d|%n|%m\n";
18
+ export class ChktexUnavailableError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "ChktexUnavailableError";
22
+ }
23
+ }
24
+ /** A newer idle revision replaced a check which had not started yet. */
25
+ export class ChktexLintSupersededError extends Error {
26
+ constructor() {
27
+ super("A newer ChkTeX check replaced this request.");
28
+ this.name = "ChktexLintSupersededError";
29
+ }
30
+ }
31
+ /**
32
+ * Parse ChkTeX's machine-readable format. Only the first five separators
33
+ * have meaning, so a `|` in the human message is preserved rather than
34
+ * making the whole result invalid.
35
+ */
36
+ export function parseChktexOutput(output) {
37
+ const lints = [];
38
+ const seen = new Set();
39
+ for (const line of output.split(/\r?\n/u)) {
40
+ const match = /^([^|]*)\|(\d+)\|(\d+)\|(-?\d+)\|(\d+)\|(.*)$/u.exec(line);
41
+ if (!match)
42
+ continue;
43
+ const lineNumber = Number(match[2]);
44
+ const column = Number(match[3]);
45
+ const length = Math.max(1, Number(match[4]));
46
+ const number = Number(match[5]);
47
+ const message = match[6].trim();
48
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1 || !Number.isSafeInteger(column) || column < 1 || !message)
49
+ continue;
50
+ const key = `${lineNumber}:${column}:${length}:${number}:${message}`;
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ lints.push({ line: lineNumber, column, length, number: Number.isSafeInteger(number) ? number : null, message });
55
+ }
56
+ return lints;
57
+ }
58
+ function cacheKey(source, filePath) {
59
+ return createHash("sha256").update(filePath.toLowerCase()).update("\0").update(source).digest("base64url");
60
+ }
61
+ /**
62
+ * Optional host-side ChkTeX integration. ChkTeX is intentionally run as a
63
+ * bounded, process-wide queue: at most eight projects/users consume a checker
64
+ * process at once, and a newer revision replaces obsolete waiting work in its
65
+ * own browser lane. The caller still debounces edits; this is the server-side
66
+ * backstop for multiple tabs and collaborators.
67
+ */
68
+ export class ChktexService {
69
+ command;
70
+ availability = "unknown";
71
+ lastUnavailableAt = 0;
72
+ probePromise = null;
73
+ queued = new Set();
74
+ waitingByLane = new Map();
75
+ latestSequenceByLane = new Map();
76
+ inFlight = new Map();
77
+ scheduledByKey = new Map();
78
+ cache = new Map();
79
+ activeChildren = new Set();
80
+ activeOperations = new Set();
81
+ activeCount = 0;
82
+ disposed = false;
83
+ constructor(command = "chktex") {
84
+ this.command = command;
85
+ }
86
+ async preload() {
87
+ await this.ensureAvailable();
88
+ }
89
+ async lint(source, filePath = "main.tex", lane, sequence) {
90
+ if (this.disposed)
91
+ throw new ChktexUnavailableError("ChkTeX service stopped.");
92
+ const key = cacheKey(source, filePath);
93
+ const laneKey = lane || null;
94
+ if (laneKey && sequence !== undefined)
95
+ this.acceptLaneSequence(laneKey, sequence, key);
96
+ const waiting = laneKey ? this.waitingByLane.get(laneKey) : undefined;
97
+ if (waiting?.key === key)
98
+ return waiting.promise;
99
+ if (laneKey && waiting)
100
+ this.releaseWaitingLane(laneKey, waiting);
101
+ const cacheable = Buffer.byteLength(source, "utf8") <= maxCachedSourceBytes;
102
+ if (cacheable) {
103
+ const cached = this.cachedResult(key);
104
+ if (cached)
105
+ return cached;
106
+ }
107
+ const existing = this.inFlight.get(key);
108
+ if (existing) {
109
+ const scheduled = this.scheduledByKey.get(key);
110
+ if (laneKey && scheduled)
111
+ this.addWaitingLane(scheduled, laneKey);
112
+ else if (scheduled && !scheduled.started)
113
+ scheduled.hasUnscopedWaiter = true;
114
+ return existing;
115
+ }
116
+ let resolve;
117
+ let reject;
118
+ const operation = new Promise((resolveOperation, rejectOperation) => {
119
+ resolve = resolveOperation;
120
+ reject = rejectOperation;
121
+ });
122
+ const scheduled = {
123
+ key, source, filePath, started: false, cancelled: false,
124
+ lanes: laneKey ? new Set([laneKey]) : new Set(), hasUnscopedWaiter: laneKey === null,
125
+ promise: operation, resolve, reject
126
+ };
127
+ this.inFlight.set(key, operation);
128
+ this.scheduledByKey.set(key, scheduled);
129
+ if (laneKey)
130
+ this.waitingByLane.set(laneKey, scheduled);
131
+ this.queued.add(scheduled);
132
+ this.drain();
133
+ void operation.then((lints) => { if (cacheable)
134
+ this.cacheResult(key, lints); }, () => undefined).finally(() => {
135
+ if (this.inFlight.get(key) === operation)
136
+ this.inFlight.delete(key);
137
+ if (this.scheduledByKey.get(key) === scheduled)
138
+ this.scheduledByKey.delete(key);
139
+ });
140
+ return operation;
141
+ }
142
+ async dispose() {
143
+ this.disposed = true;
144
+ const stopped = new ChktexUnavailableError("ChkTeX service stopped.");
145
+ for (const scheduled of [...this.queued]) {
146
+ if (scheduled.started || scheduled.cancelled)
147
+ continue;
148
+ scheduled.cancelled = true;
149
+ this.discardQueued(scheduled);
150
+ scheduled.reject(stopped);
151
+ }
152
+ this.waitingByLane.clear();
153
+ this.latestSequenceByLane.clear();
154
+ for (const child of this.activeChildren)
155
+ killProcessGroup(child, "SIGTERM");
156
+ await Promise.allSettled([...this.activeOperations]);
157
+ await this.probePromise?.catch(() => undefined);
158
+ this.inFlight.clear();
159
+ this.scheduledByKey.clear();
160
+ this.cache.clear();
161
+ }
162
+ addWaitingLane(scheduled, lane) {
163
+ if (scheduled.started || scheduled.cancelled)
164
+ return;
165
+ scheduled.lanes.add(lane);
166
+ this.waitingByLane.set(lane, scheduled);
167
+ }
168
+ releaseWaitingLane(lane, scheduled) {
169
+ if (this.waitingByLane.get(lane) === scheduled)
170
+ this.waitingByLane.delete(lane);
171
+ scheduled.lanes.delete(lane);
172
+ if (scheduled.started || scheduled.cancelled || scheduled.hasUnscopedWaiter || scheduled.lanes.size > 0)
173
+ return;
174
+ this.cancelScheduled(scheduled);
175
+ }
176
+ cancelScheduled(scheduled) {
177
+ if (scheduled.started || scheduled.cancelled)
178
+ return;
179
+ scheduled.cancelled = true;
180
+ for (const lane of scheduled.lanes)
181
+ if (this.waitingByLane.get(lane) === scheduled)
182
+ this.waitingByLane.delete(lane);
183
+ scheduled.lanes.clear();
184
+ if (this.inFlight.get(scheduled.key) === scheduled.promise)
185
+ this.inFlight.delete(scheduled.key);
186
+ if (this.scheduledByKey.get(scheduled.key) === scheduled)
187
+ this.scheduledByKey.delete(scheduled.key);
188
+ this.discardQueued(scheduled);
189
+ scheduled.reject(new ChktexLintSupersededError());
190
+ }
191
+ discardQueued(scheduled) {
192
+ this.queued.delete(scheduled);
193
+ scheduled.source = "";
194
+ scheduled.filePath = "";
195
+ }
196
+ acceptLaneSequence(lane, sequence, key) {
197
+ if (!Number.isSafeInteger(sequence) || sequence <= 0)
198
+ throw new ChktexLintSupersededError();
199
+ const now = Date.now();
200
+ for (const [knownLane, state] of this.latestSequenceByLane) {
201
+ if (now - state.seenAt > laneSequenceRetentionMs)
202
+ this.latestSequenceByLane.delete(knownLane);
203
+ }
204
+ const previous = this.latestSequenceByLane.get(lane);
205
+ if (previous && (sequence < previous.sequence || (sequence === previous.sequence && previous.key !== key))) {
206
+ throw new ChktexLintSupersededError();
207
+ }
208
+ this.latestSequenceByLane.delete(lane);
209
+ this.latestSequenceByLane.set(lane, { sequence, key, seenAt: now });
210
+ while (this.latestSequenceByLane.size > maxTrackedLaneSequences) {
211
+ const oldest = this.latestSequenceByLane.keys().next().value;
212
+ if (oldest === undefined)
213
+ break;
214
+ this.latestSequenceByLane.delete(oldest);
215
+ }
216
+ }
217
+ drain() {
218
+ while (!this.disposed && this.activeCount < maxConcurrency) {
219
+ const scheduled = this.queued.values().next().value;
220
+ if (!scheduled)
221
+ return;
222
+ this.queued.delete(scheduled);
223
+ if (scheduled.cancelled)
224
+ continue;
225
+ scheduled.started = true;
226
+ for (const lane of scheduled.lanes)
227
+ if (this.waitingByLane.get(lane) === scheduled)
228
+ this.waitingByLane.delete(lane);
229
+ scheduled.lanes.clear();
230
+ this.activeCount += 1;
231
+ let operation;
232
+ operation = this.runScheduled(scheduled).finally(() => {
233
+ this.activeCount -= 1;
234
+ this.activeOperations.delete(operation);
235
+ this.drain();
236
+ });
237
+ this.activeOperations.add(operation);
238
+ }
239
+ }
240
+ async runScheduled(scheduled) {
241
+ try {
242
+ scheduled.resolve(await this.lintOnce(scheduled.source, scheduled.filePath));
243
+ }
244
+ catch (error) {
245
+ scheduled.reject(error);
246
+ }
247
+ }
248
+ async ensureAvailable() {
249
+ if (this.disposed)
250
+ throw new ChktexUnavailableError("ChkTeX service stopped.");
251
+ if (this.availability === "available")
252
+ return;
253
+ if (this.availability === "unavailable" && Date.now() - this.lastUnavailableAt < unavailableRetryMs) {
254
+ throw new ChktexUnavailableError(`The optional ${this.command} command is not available.`);
255
+ }
256
+ if (this.probePromise)
257
+ return this.probePromise;
258
+ this.probePromise = this.runCommand(["--version"], "", commandProbeTimeoutMs, 64 * 1024)
259
+ .then(({ code, stdout, stderr }) => {
260
+ if (code !== 0)
261
+ throw new Error((stderr || stdout).trim() || `${this.command} exited with code ${code}`);
262
+ this.availability = "available";
263
+ })
264
+ .catch((error) => {
265
+ this.availability = "unavailable";
266
+ this.lastUnavailableAt = Date.now();
267
+ const detail = error instanceof Error ? error.message : String(error);
268
+ throw new ChktexUnavailableError(`The optional ${this.command} command is unavailable: ${detail}`);
269
+ })
270
+ .finally(() => { this.probePromise = null; });
271
+ return this.probePromise;
272
+ }
273
+ async lintOnce(source, filePath) {
274
+ await this.ensureAvailable();
275
+ // With no filename ChkTeX reads stdin. Passing `-` is interpreted as a
276
+ // literal filename by ChkTeX 1.7.x and silently produces no diagnostics.
277
+ // Only the active editor buffer is submitted. Disable ChkTeX's recursive
278
+ // \input traversal so diagnostics from another file cannot be mapped onto
279
+ // this buffer's line and column numbers.
280
+ const result = await this.runCommand(["-q", "-I0", "-f", outputFormat], source, checkTimeoutMs, maxOutputBytes);
281
+ // ChkTeX commonly exits with 2 when it found warnings. Some packaged
282
+ // releases use 1 for the same condition, so a structured diagnostic also
283
+ // makes either status successful; a non-zero status without diagnostics is
284
+ // still an execution failure.
285
+ const lints = parseChktexOutput(result.stdout);
286
+ const expectedDiagnosticExit = (result.code === 1 || result.code === 2) && lints.length > 0;
287
+ if (result.code !== 0 && !expectedDiagnosticExit) {
288
+ const detail = result.stderr.trim() || result.stdout.trim();
289
+ throw new Error(detail || `${this.command} exited with code ${result.code}`);
290
+ }
291
+ return lints;
292
+ }
293
+ cachedResult(key) {
294
+ const cached = this.cache.get(key);
295
+ if (!cached || cached.expiresAt < Date.now()) {
296
+ this.cache.delete(key);
297
+ return null;
298
+ }
299
+ this.cache.delete(key);
300
+ this.cache.set(key, cached);
301
+ return cached.lints;
302
+ }
303
+ cacheResult(key, lints) {
304
+ this.cache.set(key, { expiresAt: Date.now() + cacheTtlMs, lints });
305
+ while (this.cache.size > maxCachedResults) {
306
+ const oldest = this.cache.keys().next().value;
307
+ if (oldest === undefined)
308
+ break;
309
+ this.cache.delete(oldest);
310
+ }
311
+ }
312
+ runCommand(args, input, timeoutMs, outputLimit) {
313
+ return new Promise((resolve, reject) => {
314
+ if (this.disposed) {
315
+ reject(new ChktexUnavailableError("ChkTeX service stopped."));
316
+ return;
317
+ }
318
+ let child;
319
+ try {
320
+ child = spawn(this.command, args, {
321
+ env: { ...process.env, LC_ALL: "C" }, shell: false, detached: detachedProcessGroup(),
322
+ stdio: ["pipe", "pipe", "pipe"]
323
+ });
324
+ }
325
+ catch (error) {
326
+ reject(error);
327
+ return;
328
+ }
329
+ this.activeChildren.add(child);
330
+ const stdout = [];
331
+ const stderr = [];
332
+ let outputBytes = 0;
333
+ let settled = false;
334
+ let timedOut = false;
335
+ const finish = (callback) => {
336
+ if (settled)
337
+ return;
338
+ settled = true;
339
+ clearTimeout(timer);
340
+ this.activeChildren.delete(child);
341
+ callback();
342
+ };
343
+ const terminate = (error) => {
344
+ killProcessGroup(child);
345
+ finish(() => reject(error));
346
+ };
347
+ const timer = setTimeout(() => {
348
+ timedOut = true;
349
+ killProcessGroup(child);
350
+ }, timeoutMs);
351
+ const collect = (target) => (chunk) => {
352
+ outputBytes += chunk.length;
353
+ if (outputBytes > outputLimit) {
354
+ terminate(new Error(`${this.command} produced too much output.`));
355
+ return;
356
+ }
357
+ target.push(chunk);
358
+ };
359
+ child.stdout?.on("data", collect(stdout));
360
+ child.stderr?.on("data", collect(stderr));
361
+ child.stdin?.on("error", (error) => terminate(error));
362
+ child.once("error", (error) => finish(() => {
363
+ if (timedOut)
364
+ return reject(new Error(`${this.command} timed out after ${Math.ceil(timeoutMs / 1000)} seconds.`));
365
+ if (error.code === "ENOENT")
366
+ return reject(new ChktexUnavailableError(`The optional ${this.command} command is not available.`));
367
+ reject(error);
368
+ }));
369
+ child.once("close", (code) => finish(() => {
370
+ if (timedOut)
371
+ return reject(new Error(`${this.command} timed out after ${Math.ceil(timeoutMs / 1000)} seconds.`));
372
+ resolve({ code, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
373
+ }));
374
+ child.stdin?.end(input);
375
+ });
376
+ }
377
+ }
@@ -416,6 +416,12 @@ export class CollaborationService {
416
416
  return;
417
417
  room.doc.transact(() => room.meta.set("dictionaryRevision", randomUUID()), META_ORIGIN);
418
418
  }
419
+ signalChktexSetting(projectId) {
420
+ const room = this.rooms.get(projectId);
421
+ if (!room)
422
+ return;
423
+ room.doc.transact(() => room.meta.set("chktexSettingsRevision", randomUUID()), META_ORIGIN);
424
+ }
419
425
  signalCompileState(projectId, state) {
420
426
  const room = this.rooms.get(projectId);
421
427
  if (!room)
package/dist/server/db.js CHANGED
@@ -13,8 +13,42 @@ const migrationsTable = "texlite_schema_migrations";
13
13
  const databaseMigrations = [
14
14
  // Append future migrations below this entry. Never insert before or modify
15
15
  // the released baseline: recorded databases will intentionally skip it.
16
- { version: 1, name: "baseline_schema_and_legacy_upgrade", apply: applyBaselineMigration }
16
+ { version: 1, name: "baseline_schema_and_legacy_upgrade", apply: applyBaselineMigration },
17
+ { version: 2, name: "add_project_chktex_enabled", apply: (db) => {
18
+ if (!missingColumn(db, "projects", "chktex_enabled"))
19
+ return;
20
+ db.exec("ALTER TABLE projects ADD COLUMN chktex_enabled INTEGER NOT NULL DEFAULT 0 CHECK (chktex_enabled IN (0, 1))");
21
+ } },
22
+ { version: 3, name: "move_github_tokens_to_user_accounts", apply: migrateGitHubTokensToUsers }
17
23
  ];
24
+ function migrateGitHubTokensToUsers(db) {
25
+ db.exec(`
26
+ CREATE TABLE IF NOT EXISTS user_github_settings (
27
+ user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
28
+ token_ciphertext TEXT,
29
+ github_login TEXT,
30
+ created_at TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL
32
+ );
33
+
34
+ WITH ranked_tokens AS (
35
+ SELECT project.owner_id AS user_id, settings.token_ciphertext, settings.github_login,
36
+ settings.created_at, settings.updated_at,
37
+ ROW_NUMBER() OVER (
38
+ PARTITION BY project.owner_id
39
+ ORDER BY settings.updated_at DESC, settings.project_id ASC
40
+ ) AS token_rank
41
+ FROM project_git_settings settings
42
+ JOIN projects project ON project.id = settings.project_id
43
+ WHERE settings.token_ciphertext IS NOT NULL
44
+ )
45
+ INSERT OR IGNORE INTO user_github_settings (user_id, token_ciphertext, github_login, created_at, updated_at)
46
+ SELECT user_id, token_ciphertext, github_login, created_at, updated_at
47
+ FROM ranked_tokens WHERE token_rank = 1;
48
+
49
+ UPDATE project_git_settings SET token_ciphertext = NULL, github_login = NULL;
50
+ `);
51
+ }
18
52
  export function openDatabase(config) {
19
53
  fs.mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
20
54
  fs.mkdirSync(config.projectsDir, { recursive: true, mode: 0o700 });
@@ -83,6 +83,7 @@ function optionalToolDefinitions(gitCommand, gitTimeoutMs) {
83
83
  { id: "bibtex", name: "BibTeX", command: "bibtex", requirement: "optional", purpose: "BibTeX bibliography builds" },
84
84
  { id: "biber", name: "Biber", command: "biber", requirement: "optional", purpose: "Biber bibliography builds" },
85
85
  { id: "makeindex", name: "MakeIndex", command: "makeindex", requirement: "optional", purpose: "Index generation", versionArgs: [] },
86
+ { id: "chktex", name: "ChkTeX", command: "chktex", requirement: "optional", purpose: "LaTeX writing checks", versionArgs: ["--version"] },
86
87
  { id: "harper-cli", name: "Harper CLI", command: "harper-cli", requirement: "optional", purpose: "TexLite spelling and grammar checks" },
87
88
  { id: "harper-ls", name: "Harper language server", command: "harper-ls", requirement: "optional", purpose: "External editor integration (not used by TexLite)" }
88
89
  ];