texlite 0.9.2 → 0.9.4

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 (68) hide show
  1. package/DESIGN.md +29 -6
  2. package/OPERATIONS.md +9 -7
  3. package/README.md +6 -3
  4. package/README.zh-CN.md +6 -2
  5. package/dist/client/assets/AdminUsers-B12FJUMy.js +1 -0
  6. package/dist/client/assets/CitationLibraryDialog-BTmnCP4V.js +1 -0
  7. package/dist/client/assets/Dashboard-JA5_M3TK.js +2 -0
  8. package/dist/client/assets/{GitDialog-CTWAU8q6.js → GitDialog--MoUIW0n.js} +1 -1
  9. package/dist/client/assets/{HistoryDialog-Mo4p8HEg.js → HistoryDialog-LgKbjsTZ.js} +1 -1
  10. package/dist/client/assets/LatexEditor-Dj2zIaQm.js +138 -0
  11. package/dist/client/assets/{PdfPreview-BUpELZd-.js → PdfPreview-C_W7w360.js} +1 -1
  12. package/dist/client/assets/ProjectIconPickerDialog-GzRuM0oQ.js +1 -0
  13. package/dist/client/assets/{ProjectNavigationDialogs-CpjkXZTz.js → ProjectNavigationDialogs-BiArHmQ9.js} +1 -1
  14. package/dist/client/assets/ProjectSettings-DgJohWUv.js +1 -0
  15. package/dist/client/assets/ProjectWorkspace-C6sYiBF2.js +32 -0
  16. package/dist/client/assets/{SelectionHistoryDialog-9FWlc3Rb.js → SelectionHistoryDialog-Bpv6nvrC.js} +2 -2
  17. package/dist/client/assets/SystemMetricsDialog-hJQATmd4.js +1 -0
  18. package/dist/client/assets/{TagManagementDialog-BHIA0vEA.js → TagManagementDialog-BHC8qULq.js} +1 -1
  19. package/dist/client/assets/{WordCountDialog-DFIWvLrP.js → WordCountDialog-DZhhO94k.js} +1 -1
  20. package/dist/client/assets/check-C1db8iNn.js +1 -0
  21. package/dist/client/assets/{editor-3rgn4IYh.js → editor-B9HNwtsd.js} +1 -1
  22. package/dist/client/assets/hard-drive-BWZnOgnm.js +1 -0
  23. package/dist/client/assets/index-BiwJj0Np.css +1 -0
  24. package/dist/client/assets/{index-CNTGCh7t.js → index-kTamGfTl.js} +1 -1
  25. package/dist/client/assets/index-p1NHkGGG.js +12 -0
  26. package/dist/client/assets/{key-round-BjL9H8-H.js → key-round-CoDwyVJc.js} +1 -1
  27. package/dist/client/assets/{minimize-2-DDCq8c49.js → minimize-2-D100opW2.js} +1 -1
  28. package/dist/client/assets/{minus-DgKZCT9F.js → minus-DDTYB-Oz.js} +1 -1
  29. package/dist/client/assets/{plus-CezUzSIZ.js → plus-BVXOlQXP.js} +1 -1
  30. package/dist/client/assets/{refresh-cw-CDFNL-qu.js → refresh-cw-LtwnqlWh.js} +1 -1
  31. package/dist/client/assets/{search-CrDJjHoj.js → search-DXo_aAp0.js} +1 -1
  32. package/dist/client/assets/{sigma-DOcJr7VG.js → sigma-BgvHGPto.js} +1 -1
  33. package/dist/client/assets/{spellCheck-BgWyesvf.js → spellCheck-BtRppKcC.js} +1 -1
  34. package/dist/client/assets/{tags-BedQ75lh.js → tags-m9kppdi0.js} +1 -1
  35. package/dist/client/assets/{type-glZNdzZR.js → type-YO4aprqE.js} +1 -1
  36. package/dist/client/assets/{user-plus-gQ6q5Nom.js → user-plus-MRmKeDtX.js} +1 -1
  37. package/dist/client/assets/x-eisRQJv_.js +1 -0
  38. package/dist/client/index.html +2 -2
  39. package/dist/client/pdf-download.svg +2 -0
  40. package/dist/client/pdf-file.svg +19 -0
  41. package/dist/client/tex-file.svg +7 -0
  42. package/dist/server/cli.js +5 -2
  43. package/dist/server/collaboration.js +6 -19
  44. package/dist/server/db.js +91 -10
  45. package/dist/server/latexDocumentGraph.js +127 -16
  46. package/dist/server/projectOutline.js +103 -91
  47. package/dist/server/projectReferences.js +1 -1
  48. package/dist/server/routes/comments.js +10 -4
  49. package/dist/server/routes/projectFiles.js +6 -1
  50. package/dist/server/routes/projectShared.js +33 -12
  51. package/dist/shared/collaborationProtocol.js +31 -0
  52. package/dist/shared/latexDependencies.js +83 -23
  53. package/dist/shared/writingChecks.js +2 -2
  54. package/package.json +4 -3
  55. package/dist/client/assets/AdminUsers-Buf5ZclR.js +0 -1
  56. package/dist/client/assets/CitationLibraryDialog-DrKbIlD-.js +0 -1
  57. package/dist/client/assets/Dashboard-DLk2j3oq.js +0 -2
  58. package/dist/client/assets/LatexEditor-CwvakYva.js +0 -135
  59. package/dist/client/assets/ProjectIconPickerDialog-BiWSwDdt.js +0 -1
  60. package/dist/client/assets/ProjectSettings-DoxitHbX.js +0 -1
  61. package/dist/client/assets/ProjectWorkspace-iNqXZlsj.js +0 -29
  62. package/dist/client/assets/SystemMetricsDialog-24kifZO6.js +0 -1
  63. package/dist/client/assets/check-C3GZh6a1.js +0 -1
  64. package/dist/client/assets/chevron-left-l1oupvGO.js +0 -1
  65. package/dist/client/assets/hard-drive-B-NSDVVA.js +0 -1
  66. package/dist/client/assets/index-CrlfruX_.css +0 -1
  67. package/dist/client/assets/index-DIM_KiG2.js +0 -12
  68. package/dist/client/assets/x-CSzFikn2.js +0 -1
@@ -1 +1 @@
1
- import{c as e}from"./index-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.js";import{s as y,c as S}from"./ProjectWorkspace-iNqXZlsj.js";import"./react-BVUjbTTn.js";import"./check-C3GZh6a1.js";import"./x-CSzFikn2.js";import"./search-CrDJjHoj.js";import"./user-plus-gQ6q5Nom.js";import"./refresh-cw-CDFNL-qu.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-p1NHkGGG.js";import{s as y,c as S}from"./ProjectWorkspace-C6sYiBF2.js";import"./react-BVUjbTTn.js";import"./check-C1db8iNn.js";import"./x-eisRQJv_.js";import"./search-DXo_aAp0.js";import"./user-plus-MRmKeDtX.js";import"./refresh-cw-LtwnqlWh.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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-DIM_KiG2.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-p1NHkGGG.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};
@@ -0,0 +1 @@
1
+ import{c as a}from"./index-p1NHkGGG.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};
@@ -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-DIM_KiG2.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-p1NHkGGG.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/react-BVUjbTTn.js">
17
- <link rel="stylesheet" crossorigin href="./assets/index-CrlfruX_.css">
17
+ <link rel="stylesheet" crossorigin href="./assets/index-BiwJj0Np.css">
18
18
  </head>
19
19
  <body>
20
20
  <div id="root"></div>
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_pdf2</title><path d="M24.1,2.072h0l5.564,5.8V29.928H8.879V30H29.735V7.945L24.1,2.072" style="fill:#909090"/><path d="M24.031,2H8.808V29.928H29.664V7.873L24.03,2" style="fill:#f4f4f4"/><path d="M8.655,3.5H2.265v6.827h20.1V3.5H8.655" style="fill:#7a7b7c"/><path d="M22.472,10.211H2.395V3.379H22.472v6.832" style="fill:#dd2025"/><path d="M9.052,4.534h-.03l-.207,0H7.745v4.8H8.773V7.715L9,7.728a2.042,2.042,0,0,0,.647-.117,1.427,1.427,0,0,0,.493-.291,1.224,1.224,0,0,0,.335-.454,2.13,2.13,0,0,0,.105-.908,2.237,2.237,0,0,0-.114-.644,1.173,1.173,0,0,0-.687-.65A2.149,2.149,0,0,0,9.37,4.56a2.232,2.232,0,0,0-.319-.026M8.862,6.828l-.089,0V5.348h.193a.57.57,0,0,1,.459.181.92.92,0,0,1,.183.558c0,.246,0,.469-.222.626a.942.942,0,0,1-.524.114" style="fill:#464648"/><path d="M12.533,4.521c-.111,0-.219.008-.295.011L12,4.538h-.78v4.8h.918a2.677,2.677,0,0,0,1.028-.175,1.71,1.71,0,0,0,.68-.491,1.939,1.939,0,0,0,.373-.749,3.728,3.728,0,0,0,.114-.949,4.416,4.416,0,0,0-.087-1.127,1.777,1.777,0,0,0-.4-.733,1.63,1.63,0,0,0-.535-.4,2.413,2.413,0,0,0-.549-.178,1.282,1.282,0,0,0-.228-.017m-.182,3.937-.1,0V5.392h.013a1.062,1.062,0,0,1,.6.107,1.2,1.2,0,0,1,.324.4,1.3,1.3,0,0,1,.142.526c.009.22,0,.4,0,.549a2.926,2.926,0,0,1-.033.513,1.756,1.756,0,0,1-.169.5,1.13,1.13,0,0,1-.363.36.673.673,0,0,1-.416.106" style="fill:#464648"/><path d="M17.43,4.538H15v4.8h1.028V7.434h1.3V6.542h-1.3V5.43h1.4V4.538" style="fill:#464648"/><path d="M21.781,20.255s3.188-.578,3.188.511S22.994,21.412,21.781,20.255Zm-2.357.083a7.543,7.543,0,0,0-1.473.489l.4-.9c.4-.9.815-2.127.815-2.127a14.216,14.216,0,0,0,1.658,2.252,13.033,13.033,0,0,0-1.4.288Zm-1.262-6.5c0-.949.307-1.208.546-1.208s.508.115.517.939a10.787,10.787,0,0,1-.517,2.434A4.426,4.426,0,0,1,18.161,13.841ZM13.513,24.354c-.978-.585,2.051-2.386,2.6-2.444C16.11,21.911,14.537,24.966,13.513,24.354ZM25.9,20.895c-.01-.1-.1-1.207-2.07-1.16a14.228,14.228,0,0,0-2.453.173,12.542,12.542,0,0,1-2.012-2.655,11.76,11.76,0,0,0,.623-3.1c-.029-1.2-.316-1.888-1.236-1.878s-1.054.815-.933,2.013a9.309,9.309,0,0,0,.665,2.338s-.425,1.323-.987,2.639-.946,2.006-.946,2.006a9.622,9.622,0,0,0-2.725,1.4c-.824.767-1.159,1.356-.725,1.945.374.508,1.683.623,2.853-.91a22.549,22.549,0,0,0,1.7-2.492s1.784-.489,2.339-.623,1.226-.24,1.226-.24,1.629,1.639,3.2,1.581,1.495-.939,1.485-1.035" style="fill:#dd2025"/><path d="M23.954,2.077V7.95h5.633L23.954,2.077Z" style="fill:#909090"/><path d="M24.031,2V7.873h5.633L24.031,2Z" style="fill:#f4f4f4"/><path d="M8.975,4.457h-.03l-.207,0H7.668v4.8H8.7V7.639l.228.013a2.042,2.042,0,0,0,.647-.117,1.428,1.428,0,0,0,.493-.291A1.224,1.224,0,0,0,10.4,6.79a2.13,2.13,0,0,0,.105-.908,2.237,2.237,0,0,0-.114-.644,1.173,1.173,0,0,0-.687-.65,2.149,2.149,0,0,0-.411-.105,2.232,2.232,0,0,0-.319-.026M8.785,6.751l-.089,0V5.271H8.89a.57.57,0,0,1,.459.181.92.92,0,0,1,.183.558c0,.246,0,.469-.222.626a.942.942,0,0,1-.524.114" style="fill:#fff"/><path d="M12.456,4.444c-.111,0-.219.008-.295.011l-.235.006h-.78v4.8h.918a2.677,2.677,0,0,0,1.028-.175,1.71,1.71,0,0,0,.68-.491,1.939,1.939,0,0,0,.373-.749,3.728,3.728,0,0,0,.114-.949,4.416,4.416,0,0,0-.087-1.127,1.777,1.777,0,0,0-.4-.733,1.63,1.63,0,0,0-.535-.4,2.413,2.413,0,0,0-.549-.178,1.282,1.282,0,0,0-.228-.017m-.182,3.937-.1,0V5.315h.013a1.062,1.062,0,0,1,.6.107,1.2,1.2,0,0,1,.324.4,1.3,1.3,0,0,1,.142.526c.009.22,0,.4,0,.549a2.926,2.926,0,0,1-.033.513,1.756,1.756,0,0,1-.169.5,1.13,1.13,0,0,1-.363.36.673.673,0,0,1-.416.106" style="fill:#fff"/><path d="M17.353,4.461h-2.43v4.8h1.028V7.357h1.3V6.465h-1.3V5.353h1.4V4.461" style="fill:#fff"/></svg>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" ?>
2
+
3
+ <svg width="800px" height="800px" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
@@ -0,0 +1,7 @@
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
3
+ <svg fill="#26a269" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 360 360" xml:space="preserve" stroke="#26a269">
4
+
5
+
6
+
7
+
@@ -92,6 +92,9 @@ function writeInitialConfig(configPath, siteName, adminEmail) {
92
92
  const effectiveDataDirectory = configuredDataDirectory
93
93
  ? path.resolve(path.dirname(configPath), configuredDataDirectory)
94
94
  : dataDirectory;
95
+ const configuredPort = Number.parseInt(process.env.TEXLITE_PORT ?? "", 10);
96
+ const initialPort = Number.isInteger(configuredPort) && configuredPort >= 1 && configuredPort <= 65_535
97
+ ? configuredPort : CONFIG_DEFAULTS.port;
95
98
  fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
96
99
  // Data parents are not guaranteed to exist on a fresh account. Create only
97
100
  // the parent here; the configured data directory itself is created by
@@ -107,8 +110,8 @@ function writeInitialConfig(configPath, siteName, adminEmail) {
107
110
  siteName,
108
111
  adminEmail,
109
112
  sessionDays: CONFIG_DEFAULTS.sessionDays,
110
- server: { host: CONFIG_DEFAULTS.host, port: CONFIG_DEFAULTS.port, basePath: CONFIG_DEFAULTS.basePath },
111
- storage: { dataDir: dataDirectory },
113
+ server: { host: process.env.TEXLITE_HOST?.trim() || CONFIG_DEFAULTS.host, port: initialPort, basePath: CONFIG_DEFAULTS.basePath },
114
+ storage: { dataDir: configuredDataDirectory || dataDirectory },
112
115
  uploads: { maxFileSizeMB: CONFIG_DEFAULTS.maxFileSizeMB },
113
116
  pdf: {
114
117
  loadingStrategy: CONFIG_DEFAULTS.pdfLoadingStrategy,
@@ -12,26 +12,14 @@ import { listProjectFilesAsync, outputRoot, resolveSourcePath, safeRelativePath,
12
12
  import { collaborationProjectAccessFromStatement, canEdit, prepareCollaborationProjectAccessStatement } from "./projects.js";
13
13
  import { reanchorFileComments } from "./anchors.js";
14
14
  import { hashText } from "./editHistory.js";
15
- const MESSAGE_SYNC = 0;
16
- const MESSAGE_AWARENESS = 1;
17
- const MESSAGE_QUERY_AWARENESS = 3;
18
- const MESSAGE_FLUSH = 4;
19
- const MESSAGE_PROTOCOL = 5;
20
- const MESSAGE_MAINTENANCE = 6;
21
- const MESSAGE_PERMISSION = 7;
22
- // A small, non-Yjs handshake message used for ephemeral compile metadata.
23
- // Source text still uses the normal Yjs sync protocol; this message prevents
24
- // an old IndexedDB metadata entry from being treated as an active compile.
25
- const MESSAGE_COMPILE_STATES = 8;
26
- // Ephemeral, per-file formatter leases. These are deliberately kept out of
27
- // the Yjs document and source persistence state.
28
- const MESSAGE_FORMAT_LEASE = 9;
15
+ import { COLLABORATION_PROTOCOL_VERSION, CollaborationMessageType, isCollaborationCleanMode, isCollaborationCompileStatus } from "../shared/collaborationProtocol.js";
16
+ export { COLLABORATION_PROTOCOL_VERSION } from "../shared/collaborationProtocol.js";
17
+ const { Sync: MESSAGE_SYNC, Awareness: MESSAGE_AWARENESS, QueryAwareness: MESSAGE_QUERY_AWARENESS, Flush: MESSAGE_FLUSH, Protocol: MESSAGE_PROTOCOL, Maintenance: MESSAGE_MAINTENANCE, Permission: MESSAGE_PERMISSION, CompileStates: MESSAGE_COMPILE_STATES, FormatLease: MESSAGE_FORMAT_LEASE } = CollaborationMessageType;
29
18
  /**
30
19
  * Increment this when a wire-level collaboration change cannot be decoded by
31
20
  * an older browser. The epoch marker is rotated at the same time, which makes
32
21
  * already-open older pages discard their local draft and reload safely.
33
22
  */
34
- export const COLLABORATION_PROTOCOL_VERSION = 3;
35
23
  const VERSIONED_EPOCH_PREFIX = `${COLLABORATION_PROTOCOL_VERSION}:`;
36
24
  const SOURCE_PREFIX = "source:";
37
25
  const MAX_PROJECT_SESSIONS = 10;
@@ -1576,7 +1564,7 @@ export function maxCollaborativeFileBytes(config) {
1576
1564
  return Math.min(config.maxUploadBytes, MAX_COLLABORATIVE_FILE_BYTES);
1577
1565
  }
1578
1566
  export function isCollaborativeTextFile(filePath) {
1579
- return /(?:\.tex|\.bib|\.sty|\.cls|\.txt|\.md|latexmkrc)$/i.test(filePath);
1567
+ return /(?:\.tex|\.bib|\.bst|\.sty|\.cls|\.txt|\.md|latexmkrc)$/i.test(filePath);
1580
1568
  }
1581
1569
  function collaborativeEntrySignature(entry) {
1582
1570
  return `${entry.path}\0${entry.size ?? 0}\0${entry.mtimeMs ?? 0}`;
@@ -1760,9 +1748,8 @@ function isSharedCompileState(mainFile, value) {
1760
1748
  const requestedBy = state.requestedBy;
1761
1749
  return state.mainFile === mainFile
1762
1750
  && typeof state.runId === "string" && state.runId.length > 0
1763
- && (state.status === "queued" || state.status === "running" || state.status === "succeeded"
1764
- || state.status === "failed" || state.status === "cleaned")
1765
- && (state.status !== "cleaned" || state.cleanMode === "cache" || state.cleanMode === "artifacts")
1751
+ && isCollaborationCompileStatus(state.status)
1752
+ && (state.status !== "cleaned" || isCollaborationCleanMode(state.cleanMode))
1766
1753
  && (state.stale === undefined || typeof state.stale === "boolean")
1767
1754
  && typeof state.updatedAt === "string"
1768
1755
  && Boolean(requestedBy && typeof requestedBy === "object"
package/dist/server/db.js CHANGED
@@ -1,5 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import Database from "better-sqlite3";
3
+ const migrationsTable = "texlite_schema_migrations";
4
+ /**
5
+ * Keep this list append-only. Existing migration bodies must never be changed
6
+ * after release: a database records the version only after that migration's
7
+ * transaction has committed.
8
+ *
9
+ * Version 1 deliberately folds the untracked historical schema setup into a
10
+ * single baseline. It upgrades every database released before versioned
11
+ * migrations while preserving the current schema for new installations.
12
+ */
13
+ const databaseMigrations = [
14
+ // Append future migrations below this entry. Never insert before or modify
15
+ // the released baseline: recorded databases will intentionally skip it.
16
+ { version: 1, name: "baseline_schema_and_legacy_upgrade", apply: applyBaselineMigration }
17
+ ];
3
18
  export function openDatabase(config) {
4
19
  fs.mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
5
20
  fs.mkdirSync(config.projectsDir, { recursive: true, mode: 0o700 });
@@ -8,10 +23,67 @@ export function openDatabase(config) {
8
23
  // per-transaction WAL fsync: a sudden host failure can lose the most recent
9
24
  // acknowledged transaction, but the database remains consistent.
10
25
  db.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;");
11
- migrate(db);
26
+ try {
27
+ migrate(db);
28
+ }
29
+ catch (error) {
30
+ // A failed migration must not leave an open SQLite handle holding a lock
31
+ // while startup unwinds. Its transaction has already been rolled back.
32
+ db.close();
33
+ throw error;
34
+ }
12
35
  return db;
13
36
  }
14
37
  function migrate(db) {
38
+ const context = {
39
+ migrateLegacyProjectTags: tableExists(db, "project_tags") && !tableExists(db, "user_tags"),
40
+ backfillAdminCanCreateProjects: missingColumn(db, "users", "can_create_projects"),
41
+ backfillProjectLastModifiedBy: missingColumn(db, "projects", "last_modified_by"),
42
+ backfillCompileRunMainFile: missingColumn(db, "compile_runs", "main_file"),
43
+ backfillEditSegmentBytes: missingColumn(db, "project_edit_segments", "steps_bytes")
44
+ };
45
+ db.exec(`CREATE TABLE IF NOT EXISTS ${migrationsTable} (
46
+ version INTEGER PRIMARY KEY,
47
+ name TEXT NOT NULL,
48
+ applied_at TEXT NOT NULL
49
+ )`);
50
+ const applied = db.prepare(`SELECT version, name FROM ${migrationsTable}`).all()
51
+ .map((migration) => ({ version: Number(migration.version), name: migration.name }));
52
+ const knownMigrations = new Map(databaseMigrations.map((migration) => [migration.version, migration]));
53
+ const unknownMigration = applied.find((migration) => !knownMigrations.has(migration.version));
54
+ if (unknownMigration !== undefined) {
55
+ const latestVersion = databaseMigrations.at(-1)?.version ?? 0;
56
+ const direction = unknownMigration.version > latestVersion ? "newer than" : "not recognized by";
57
+ throw new Error(`Database schema version ${unknownMigration.version} is ${direction} this TexLite release.`);
58
+ }
59
+ const renamedMigration = applied.find((migration) => knownMigrations.get(migration.version)?.name !== migration.name);
60
+ if (renamedMigration) {
61
+ throw new Error(`Database migration ${renamedMigration.version} does not match this TexLite release.`);
62
+ }
63
+ const appliedVersions = new Set(applied.map((migration) => migration.version));
64
+ const missingBeforeApplied = databaseMigrations.find((migration) => !appliedVersions.has(migration.version)
65
+ && applied.some((appliedMigration) => appliedMigration.version > migration.version));
66
+ if (missingBeforeApplied) {
67
+ throw new Error(`Database migration ${missingBeforeApplied.version} is missing before a later migration.`);
68
+ }
69
+ const record = db.prepare(`INSERT INTO ${migrationsTable} (version, name, applied_at) VALUES (?, ?, ?)`);
70
+ for (const migration of databaseMigrations) {
71
+ if (appliedVersions.has(migration.version))
72
+ continue;
73
+ db.transaction(() => {
74
+ migration.apply(db, context);
75
+ record.run(migration.version, migration.name, new Date().toISOString());
76
+ })();
77
+ }
78
+ }
79
+ function tableExists(db, table) {
80
+ return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table));
81
+ }
82
+ function missingColumn(db, table, column) {
83
+ return tableExists(db, table)
84
+ && !db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = ?").get(table, column);
85
+ }
86
+ function applyBaselineMigration(db, context) {
15
87
  db.exec(`
16
88
  CREATE TABLE IF NOT EXISTS users (
17
89
  id TEXT PRIMARY KEY,
@@ -282,10 +354,10 @@ function migrate(db) {
282
354
  `);
283
355
  const editColumns = db.prepare("PRAGMA table_info(project_edit_segments)").all();
284
356
  if (!editColumns.some((column) => column.name === "steps_bytes")) {
285
- db.transaction(() => {
286
- db.exec("ALTER TABLE project_edit_segments ADD COLUMN steps_bytes INTEGER NOT NULL DEFAULT 0");
357
+ db.exec("ALTER TABLE project_edit_segments ADD COLUMN steps_bytes INTEGER NOT NULL DEFAULT 0");
358
+ if (context.backfillEditSegmentBytes) {
287
359
  db.exec("UPDATE project_edit_segments SET steps_bytes = LENGTH(CAST(steps_json AS BLOB))");
288
- })();
360
+ }
289
361
  }
290
362
  const projectColumns = db.prepare("PRAGMA table_info(projects)").all();
291
363
  if (!projectColumns.some((column) => column.name === "latexmkrc")) {
@@ -328,14 +400,23 @@ function migrate(db) {
328
400
  db.exec("ALTER TABLE citation_library_entries ADD COLUMN revision INTEGER NOT NULL DEFAULT 1");
329
401
  }
330
402
  db.exec(`
331
- UPDATE users SET can_create_projects = 1 WHERE role = 'admin';
332
- UPDATE projects SET last_modified_by = owner_id WHERE last_modified_by IS NULL;
333
- UPDATE compile_runs SET main_file = COALESCE(
334
- (SELECT project.main_file FROM projects project WHERE project.id = compile_runs.project_id), ''
335
- ) WHERE main_file = '';
336
403
  CREATE INDEX IF NOT EXISTS compile_runs_project_main_created
337
404
  ON compile_runs(project_id, main_file, created_at DESC);
338
-
405
+ `);
406
+ if (context.backfillProjectLastModifiedBy) {
407
+ db.exec("UPDATE projects SET last_modified_by = owner_id WHERE last_modified_by IS NULL");
408
+ }
409
+ if (context.backfillAdminCanCreateProjects) {
410
+ db.exec("UPDATE users SET can_create_projects = 1 WHERE role = 'admin'");
411
+ }
412
+ if (context.backfillCompileRunMainFile) {
413
+ db.exec(`UPDATE compile_runs SET main_file = COALESCE(
414
+ (SELECT project.main_file FROM projects project WHERE project.id = compile_runs.project_id), ''
415
+ ) WHERE main_file = ''`);
416
+ }
417
+ if (!context.migrateLegacyProjectTags)
418
+ return;
419
+ db.exec(`
339
420
  INSERT OR IGNORE INTO tags (id, name, color, created_by, created_at, updated_at)
340
421
  SELECT legacy.id, legacy.name, legacy.color, p.owner_id, legacy.created_at, legacy.created_at
341
422
  FROM project_tags legacy
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { findLatexBibliographyFiles, findLatexSourceIncludes } from "../shared/latexReferences.js";
2
+ import { findLatexBibliographyFiles, findLatexSourceIncludes } from "../shared/latexDependencies.js";
3
3
  /**
4
4
  * Extract only the small amount of source information needed for project-level
5
5
  * document graph lookups. Keeping this separate from completion symbols means
@@ -7,29 +7,39 @@ import { findLatexBibliographyFiles, findLatexSourceIncludes } from "../shared/l
7
7
  */
8
8
  export function latexDocumentDirectives(source) {
9
9
  return {
10
- includes: findLatexSourceIncludes(source).map((reference) => reference.path),
10
+ includes: findLatexSourceIncludes(source).map(toLatexIncludeDirective),
11
11
  bibliographies: findLatexBibliographyFiles(source).map((reference) => reference.path)
12
12
  };
13
13
  }
14
+ export function toLatexIncludeDirective(reference) {
15
+ return {
16
+ path: reference.path,
17
+ command: reference.command,
18
+ ...(reference.directory === undefined ? {} : { directory: reference.directory })
19
+ };
20
+ }
14
21
  /** Return the selected root followed by its reachable \input-style sources. */
15
22
  export function documentSourceOrder(sources, mainFile) {
16
23
  if (!mainFile || !sources.has(mainFile))
17
24
  return [];
18
25
  const result = [];
19
- const queued = [mainFile];
26
+ const queued = [{ path: mainFile, importBase: "" }];
27
+ // A source can technically be reached under more than one import context.
28
+ // Keeping its first discovered context mirrors the ordinary project graph
29
+ // traversal while bounding recursive import-package structures.
20
30
  const seen = new Set();
21
31
  while (queued.length) {
22
32
  const current = queued.shift();
23
- if (!current || seen.has(current))
33
+ if (!current || seen.has(current.path))
24
34
  continue;
25
- const directives = sources.get(current);
35
+ const directives = sources.get(current.path);
26
36
  if (!directives)
27
37
  continue;
28
- seen.add(current);
29
- result.push(current);
38
+ seen.add(current.path);
39
+ result.push(current.path);
30
40
  for (const include of directives.includes) {
31
- const resolved = resolveProjectReferencePath(include, current, ".tex", sources, false);
32
- if (resolved && !seen.has(resolved))
41
+ const resolved = resolveLatexInclude(include, current.importBase, sources);
42
+ if (resolved && !seen.has(resolved.path))
33
43
  queued.push(resolved);
34
44
  }
35
45
  }
@@ -50,25 +60,107 @@ export function declaredBibliographyPaths(sources, mainFile, bibPaths) {
50
60
  if (!directives)
51
61
  continue;
52
62
  for (const bibliography of directives.bibliographies) {
53
- const resolved = resolveProjectReferencePath(bibliography, sourcePath, ".bib", availableBibliographies, true);
63
+ const resolved = resolveProjectReferencePath(bibliography, sourcePath, ".bib", availableBibliographies, "project-root-then-source-file");
54
64
  if (resolved)
55
65
  declared.push(resolved);
56
66
  }
57
67
  }
58
68
  return uniqueProjectPaths(declared);
59
69
  }
60
- export function resolveProjectReferencePath(rawPath, currentPath, extension, available, rootFirst) {
70
+ export function resolveProjectReferencePath(rawPath, currentPath, extension, available, resolution) {
61
71
  const raw = normalizeReferenceInput(rawPath);
62
72
  if (!raw)
63
73
  return null;
64
74
  const parent = path.posix.dirname(currentPath);
65
- // TeX resolves relative files against the file that declared them. A raw
66
- // ../ reference is valid when this final candidate remains inside the
67
- // project tree (for example chapters/intro.tex -> ../refs.bib).
68
75
  const relative = normalizeProjectPath(parent === "." ? raw : path.posix.join(parent, raw));
69
76
  const root = normalizeProjectPath(raw);
70
- const roots = rootFirst ? [root, relative] : [relative, root];
71
- const candidates = roots.flatMap((candidate) => {
77
+ const roots = resolution === "project-root"
78
+ ? [root]
79
+ : resolution === "source-file"
80
+ ? [relative]
81
+ : [root, relative];
82
+ return resolveCandidatePaths(roots, extension, available);
83
+ }
84
+ /**
85
+ * Resolve import.sty's dynamic path handling. `\\import`, `\\inputfrom`,
86
+ * and `\\includefrom` reset the import path from the compiler root. Their
87
+ * `sub*` counterparts extend the active path. Plain input-style commands use
88
+ * the active path first, then the compiler root, matching import.sty's
89
+ * `\\input@path` fallback behavior.
90
+ */
91
+ export function resolveLatexInclude(include, importBase, available) {
92
+ switch (include.command.toLowerCase()) {
93
+ case "input":
94
+ case "include": {
95
+ const resolved = resolveOrdinaryIncludePath(include.path, importBase, available);
96
+ return resolved ? { path: resolved, importBase } : null;
97
+ }
98
+ case "subfile":
99
+ return resolveSubfilePath(include.path, importBase, available);
100
+ case "import":
101
+ case "includefrom":
102
+ case "inputfrom": {
103
+ const nextImportBase = normalizeImportBase(include.directory ?? "");
104
+ if (nextImportBase === null)
105
+ return null;
106
+ const resolved = resolveProjectPathAtBase(include.path, "", ".tex", available);
107
+ return resolved ? { path: resolved, importBase: nextImportBase } : null;
108
+ }
109
+ case "subimport":
110
+ case "subinputfrom":
111
+ case "subincludefrom": {
112
+ const nextImportBase = extendImportBase(importBase, include.directory ?? "");
113
+ if (nextImportBase === null)
114
+ return null;
115
+ const resolved = resolveProjectPathAtBase(include.path, importBase, ".tex", available);
116
+ return resolved ? { path: resolved, importBase: nextImportBase } : null;
117
+ }
118
+ default:
119
+ return null;
120
+ }
121
+ }
122
+ /** `subfiles` implements \subfile through import.sty's \subimport. */
123
+ function resolveSubfilePath(rawPath, importBase, available) {
124
+ const raw = normalizeReferenceInput(rawPath);
125
+ if (!raw)
126
+ return null;
127
+ const directory = path.posix.dirname(raw);
128
+ const file = path.posix.basename(raw);
129
+ if (!file || file === "." || file === "/")
130
+ return null;
131
+ const nextImportBase = extendImportBase(importBase, directory === "." ? "" : directory);
132
+ if (nextImportBase === null)
133
+ return null;
134
+ const resolved = resolveProjectPathAtBase(file, nextImportBase, ".tex", available);
135
+ return resolved ? { path: resolved, importBase: nextImportBase } : null;
136
+ }
137
+ function resolveOrdinaryIncludePath(rawPath, importBase, available) {
138
+ if (importBase) {
139
+ const fromImportPath = resolveProjectPathAtBase(rawPath, importBase, ".tex", available);
140
+ if (fromImportPath)
141
+ return fromImportPath;
142
+ }
143
+ return resolveProjectPathAtBase(rawPath, "", ".tex", available);
144
+ }
145
+ function resolveProjectPathAtBase(rawPath, base, extension, available) {
146
+ const raw = normalizeReferenceInput(rawPath);
147
+ const normalizedBase = normalizeImportBase(base);
148
+ if (!raw || normalizedBase === null)
149
+ return null;
150
+ const candidate = normalizeProjectPath(normalizedBase ? path.posix.join(normalizedBase, raw) : raw);
151
+ return resolveCandidatePaths(candidate ? [candidate] : [], extension, available);
152
+ }
153
+ function extendImportBase(importBase, extension) {
154
+ const normalizedBase = normalizeImportBase(importBase);
155
+ const relativeExtension = normalizeRelativeImportPath(extension);
156
+ if (normalizedBase === null || relativeExtension === null)
157
+ return null;
158
+ return normalizeImportBase(normalizedBase
159
+ ? path.posix.join(normalizedBase, relativeExtension)
160
+ : relativeExtension);
161
+ }
162
+ function resolveCandidatePaths(paths, extension, available) {
163
+ const candidates = paths.flatMap((candidate) => {
72
164
  if (!candidate)
73
165
  return [];
74
166
  return candidate.toLowerCase().endsWith(extension)
@@ -96,6 +188,25 @@ function normalizeReferenceInput(value) {
96
188
  return null;
97
189
  return trimmed;
98
190
  }
191
+ function normalizeRelativeImportPath(value) {
192
+ const trimmed = value.trim().replaceAll("\\", "/");
193
+ if (trimmed.startsWith("/") || /^[A-Za-z]:\//.test(trimmed) || trimmed.includes("\0"))
194
+ return null;
195
+ return trimmed || ".";
196
+ }
197
+ function normalizeImportBase(value) {
198
+ const relative = normalizeRelativeImportPath(value);
199
+ if (relative === null)
200
+ return null;
201
+ const normalized = path.posix.normalize(relative);
202
+ if (normalized === ".")
203
+ return "";
204
+ if (normalized === ".." || normalized.startsWith("../"))
205
+ return null;
206
+ if (normalized.split("/").some((segment) => segment.toLowerCase() === ".git"))
207
+ return null;
208
+ return normalized;
209
+ }
99
210
  function normalizeProjectPath(value) {
100
211
  const normalized = path.posix.normalize(value);
101
212
  if (normalized === "." || normalized === ".." || normalized.startsWith("../"))