texlite 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ import{g as F}from"./react-CsO_xvbl.js";function N(S,f){for(var w=0;w<f.length;w++){const m=f[w];if(typeof m!="string"&&!Array.isArray(m)){for(const o in m)if(o!=="default"&&!(o in S)){const r=Object.getOwnPropertyDescriptor(m,o);r&&Object.defineProperty(S,o,r.get?r:{enumerable:!0,get:()=>m[o]})}}}return Object.freeze(Object.defineProperty(S,Symbol.toStringTag,{value:"Module"}))}var R={exports:{}},O;function B(){return O||(O=1,(function(S){var f=function(){this.Diff_Timeout=1,this.Diff_EditCost=4,this.Match_Threshold=.5,this.Match_Distance=1e3,this.Patch_DeleteThreshold=.5,this.Patch_Margin=4,this.Match_MaxBits=32},w=-1,m=1,o=0;f.Diff=function(r,n){return[r,n]},f.prototype.diff_main=function(r,n,e,t){typeof t>"u"&&(this.Diff_Timeout<=0?t=Number.MAX_VALUE:t=new Date().getTime()+this.Diff_Timeout*1e3);var a=t;if(r==null||n==null)throw new Error("Null input. (diff_main)");if(r==n)return r?[new f.Diff(o,r)]:[];typeof e>"u"&&(e=!0);var i=e,s=this.diff_commonPrefix(r,n),h=r.substring(0,s);r=r.substring(s),n=n.substring(s),s=this.diff_commonSuffix(r,n);var l=r.substring(r.length-s);r=r.substring(0,r.length-s),n=n.substring(0,n.length-s);var g=this.diff_compute_(r,n,i,a);return h&&g.unshift(new f.Diff(o,h)),l&&g.push(new f.Diff(o,l)),this.diff_cleanupMerge(g),g},f.prototype.diff_compute_=function(r,n,e,t){var a;if(!r)return[new f.Diff(m,n)];if(!n)return[new f.Diff(w,r)];var i=r.length>n.length?r:n,s=r.length>n.length?n:r,h=i.indexOf(s);if(h!=-1)return a=[new f.Diff(m,i.substring(0,h)),new f.Diff(o,s),new f.Diff(m,i.substring(h+s.length))],r.length>n.length&&(a[0][0]=a[2][0]=w),a;if(s.length==1)return[new f.Diff(w,r),new f.Diff(m,n)];var l=this.diff_halfMatch_(r,n);if(l){var g=l[0],u=l[1],v=l[2],c=l[3],_=l[4],b=this.diff_main(g,v,e,t),M=this.diff_main(u,c,e,t);return b.concat([new f.Diff(o,_)],M)}return e&&r.length>100&&n.length>100?this.diff_lineMode_(r,n,t):this.diff_bisect_(r,n,t)},f.prototype.diff_lineMode_=function(r,n,e){var t=this.diff_linesToChars_(r,n);r=t.chars1,n=t.chars2;var a=t.lineArray,i=this.diff_main(r,n,!1,e);this.diff_charsToLines_(i,a),this.diff_cleanupSemantic(i),i.push(new f.Diff(o,""));for(var s=0,h=0,l=0,g="",u="";s<i.length;){switch(i[s][0]){case m:l++,u+=i[s][1];break;case w:h++,g+=i[s][1];break;case o:if(h>=1&&l>=1){i.splice(s-h-l,h+l),s=s-h-l;for(var v=this.diff_main(g,u,!1,e),c=v.length-1;c>=0;c--)i.splice(s,0,v[c]);s=s+v.length}l=0,h=0,g="",u="";break}s++}return i.pop(),i},f.prototype.diff_bisect_=function(r,n,e){for(var t=r.length,a=n.length,i=Math.ceil((t+a)/2),s=i,h=2*i,l=new Array(h),g=new Array(h),u=0;u<h;u++)l[u]=-1,g[u]=-1;l[s+1]=0,g[s+1]=0;for(var v=t-a,c=v%2!=0,_=0,b=0,M=0,k=0,p=0;p<i&&!(new Date().getTime()>e);p++){for(var D=-p+_;D<=p-b;D+=2){var A=s+D,P;D==-p||D!=p&&l[A-1]<l[A+1]?P=l[A+1]:P=l[A-1]+1;for(var E=P-D;P<t&&E<a&&r.charAt(P)==n.charAt(E);)P++,E++;if(l[A]=P,P>t)b+=2;else if(E>a)_+=2;else if(c){var y=s+v-D;if(y>=0&&y<h&&g[y]!=-1){var d=t-g[y];if(P>=d)return this.diff_bisectSplit_(r,n,P,E,e)}}}for(var I=-p+M;I<=p-k;I+=2){var y=s+I,d;I==-p||I!=p&&g[y-1]<g[y+1]?d=g[y+1]:d=g[y-1]+1;for(var T=d-I;d<t&&T<a&&r.charAt(t-d-1)==n.charAt(a-T-1);)d++,T++;if(g[y]=d,d>t)k+=2;else if(T>a)M+=2;else if(!c){var A=s+v-I;if(A>=0&&A<h&&l[A]!=-1){var P=l[A],E=s+P-A;if(d=t-d,P>=d)return this.diff_bisectSplit_(r,n,P,E,e)}}}}return[new f.Diff(w,r),new f.Diff(m,n)]},f.prototype.diff_bisectSplit_=function(r,n,e,t,a){var i=r.substring(0,e),s=n.substring(0,t),h=r.substring(e),l=n.substring(t),g=this.diff_main(i,s,!1,a),u=this.diff_main(h,l,!1,a);return g.concat(u)},f.prototype.diff_linesToChars_=function(r,n){var e=[],t={};e[0]="";function a(l){for(var g="",u=0,v=-1,c=e.length;v<l.length-1;){v=l.indexOf(`
2
+ `,u),v==-1&&(v=l.length-1);var _=l.substring(u,v+1);(t.hasOwnProperty?t.hasOwnProperty(_):t[_]!==void 0)?g+=String.fromCharCode(t[_]):(c==i&&(_=l.substring(u),v=l.length),g+=String.fromCharCode(c),t[_]=c,e[c++]=_),u=v+1}return g}var i=4e4,s=a(r);i=65535;var h=a(n);return{chars1:s,chars2:h,lineArray:e}},f.prototype.diff_charsToLines_=function(r,n){for(var e=0;e<r.length;e++){for(var t=r[e][1],a=[],i=0;i<t.length;i++)a[i]=n[t.charCodeAt(i)];r[e][1]=a.join("")}},f.prototype.diff_commonPrefix=function(r,n){if(!r||!n||r.charAt(0)!=n.charAt(0))return 0;for(var e=0,t=Math.min(r.length,n.length),a=t,i=0;e<a;)r.substring(i,a)==n.substring(i,a)?(e=a,i=e):t=a,a=Math.floor((t-e)/2+e);return a},f.prototype.diff_commonSuffix=function(r,n){if(!r||!n||r.charAt(r.length-1)!=n.charAt(n.length-1))return 0;for(var e=0,t=Math.min(r.length,n.length),a=t,i=0;e<a;)r.substring(r.length-a,r.length-i)==n.substring(n.length-a,n.length-i)?(e=a,i=e):t=a,a=Math.floor((t-e)/2+e);return a},f.prototype.diff_commonOverlap_=function(r,n){var e=r.length,t=n.length;if(e==0||t==0)return 0;e>t?r=r.substring(e-t):e<t&&(n=n.substring(0,e));var a=Math.min(e,t);if(r==n)return a;for(var i=0,s=1;;){var h=r.substring(a-s),l=n.indexOf(h);if(l==-1)return i;s+=l,(l==0||r.substring(a-s)==n.substring(0,s))&&(i=s,s++)}},f.prototype.diff_halfMatch_=function(r,n){if(this.Diff_Timeout<=0)return null;var e=r.length>n.length?r:n,t=r.length>n.length?n:r;if(e.length<4||t.length*2<e.length)return null;var a=this;function i(b,M,k){for(var p=b.substring(k,k+Math.floor(b.length/4)),D=-1,A="",P,E,y,d;(D=M.indexOf(p,D+1))!=-1;){var I=a.diff_commonPrefix(b.substring(k),M.substring(D)),T=a.diff_commonSuffix(b.substring(0,k),M.substring(0,D));A.length<T+I&&(A=M.substring(D-T,D)+M.substring(D,D+I),P=b.substring(0,k-T),E=b.substring(k+I),y=M.substring(0,D-T),d=M.substring(D+I))}return A.length*2>=b.length?[P,E,y,d,A]:null}var s=i(e,t,Math.ceil(e.length/4)),h=i(e,t,Math.ceil(e.length/2)),l;if(!s&&!h)return null;h?s?l=s[4].length>h[4].length?s:h:l=h:l=s;var g,u,v,c;r.length>n.length?(g=l[0],u=l[1],v=l[2],c=l[3]):(v=l[0],c=l[1],g=l[2],u=l[3]);var _=l[4];return[g,u,v,c,_]},f.prototype.diff_cleanupSemantic=function(r){for(var n=!1,e=[],t=0,a=null,i=0,s=0,h=0,l=0,g=0;i<r.length;)r[i][0]==o?(e[t++]=i,s=l,h=g,l=0,g=0,a=r[i][1]):(r[i][0]==m?l+=r[i][1].length:g+=r[i][1].length,a&&a.length<=Math.max(s,h)&&a.length<=Math.max(l,g)&&(r.splice(e[t-1],0,new f.Diff(w,a)),r[e[t-1]+1][0]=m,t--,t--,i=t>0?e[t-1]:-1,s=0,h=0,l=0,g=0,a=null,n=!0)),i++;for(n&&this.diff_cleanupMerge(r),this.diff_cleanupSemanticLossless(r),i=1;i<r.length;){if(r[i-1][0]==w&&r[i][0]==m){var u=r[i-1][1],v=r[i][1],c=this.diff_commonOverlap_(u,v),_=this.diff_commonOverlap_(v,u);c>=_?(c>=u.length/2||c>=v.length/2)&&(r.splice(i,0,new f.Diff(o,v.substring(0,c))),r[i-1][1]=u.substring(0,u.length-c),r[i+1][1]=v.substring(c),i++):(_>=u.length/2||_>=v.length/2)&&(r.splice(i,0,new f.Diff(o,u.substring(0,_))),r[i-1][0]=m,r[i-1][1]=v.substring(0,v.length-_),r[i+1][0]=w,r[i+1][1]=u.substring(_),i++),i++}i++}},f.prototype.diff_cleanupSemanticLossless=function(r){function n(_,b){if(!_||!b)return 6;var M=_.charAt(_.length-1),k=b.charAt(0),p=M.match(f.nonAlphaNumericRegex_),D=k.match(f.nonAlphaNumericRegex_),A=p&&M.match(f.whitespaceRegex_),P=D&&k.match(f.whitespaceRegex_),E=A&&M.match(f.linebreakRegex_),y=P&&k.match(f.linebreakRegex_),d=E&&_.match(f.blanklineEndRegex_),I=y&&b.match(f.blanklineStartRegex_);return d||I?5:E||y?4:p&&!A&&P?3:A||P?2:p||D?1:0}for(var e=1;e<r.length-1;){if(r[e-1][0]==o&&r[e+1][0]==o){var t=r[e-1][1],a=r[e][1],i=r[e+1][1],s=this.diff_commonSuffix(t,a);if(s){var h=a.substring(a.length-s);t=t.substring(0,t.length-s),a=h+a.substring(0,a.length-s),i=h+i}for(var l=t,g=a,u=i,v=n(t,a)+n(a,i);a.charAt(0)===i.charAt(0);){t+=a.charAt(0),a=a.substring(1)+i.charAt(0),i=i.substring(1);var c=n(t,a)+n(a,i);c>=v&&(v=c,l=t,g=a,u=i)}r[e-1][1]!=l&&(l?r[e-1][1]=l:(r.splice(e-1,1),e--),r[e][1]=g,u?r[e+1][1]=u:(r.splice(e+1,1),e--))}e++}},f.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,f.whitespaceRegex_=/\s/,f.linebreakRegex_=/[\r\n]/,f.blanklineEndRegex_=/\n\r?\n$/,f.blanklineStartRegex_=/^\r?\n\r?\n/,f.prototype.diff_cleanupEfficiency=function(r){for(var n=!1,e=[],t=0,a=null,i=0,s=!1,h=!1,l=!1,g=!1;i<r.length;)r[i][0]==o?(r[i][1].length<this.Diff_EditCost&&(l||g)?(e[t++]=i,s=l,h=g,a=r[i][1]):(t=0,a=null),l=g=!1):(r[i][0]==w?g=!0:l=!0,a&&(s&&h&&l&&g||a.length<this.Diff_EditCost/2&&s+h+l+g==3)&&(r.splice(e[t-1],0,new f.Diff(w,a)),r[e[t-1]+1][0]=m,t--,a=null,s&&h?(l=g=!0,t=0):(t--,i=t>0?e[t-1]:-1,l=g=!1),n=!0)),i++;n&&this.diff_cleanupMerge(r)},f.prototype.diff_cleanupMerge=function(r){r.push(new f.Diff(o,""));for(var n=0,e=0,t=0,a="",i="",s;n<r.length;)switch(r[n][0]){case m:t++,i+=r[n][1],n++;break;case w:e++,a+=r[n][1],n++;break;case o:e+t>1?(e!==0&&t!==0&&(s=this.diff_commonPrefix(i,a),s!==0&&(n-e-t>0&&r[n-e-t-1][0]==o?r[n-e-t-1][1]+=i.substring(0,s):(r.splice(0,0,new f.Diff(o,i.substring(0,s))),n++),i=i.substring(s),a=a.substring(s)),s=this.diff_commonSuffix(i,a),s!==0&&(r[n][1]=i.substring(i.length-s)+r[n][1],i=i.substring(0,i.length-s),a=a.substring(0,a.length-s))),n-=e+t,r.splice(n,e+t),a.length&&(r.splice(n,0,new f.Diff(w,a)),n++),i.length&&(r.splice(n,0,new f.Diff(m,i)),n++),n++):n!==0&&r[n-1][0]==o?(r[n-1][1]+=r[n][1],r.splice(n,1)):n++,t=0,e=0,a="",i="";break}r[r.length-1][1]===""&&r.pop();var h=!1;for(n=1;n<r.length-1;)r[n-1][0]==o&&r[n+1][0]==o&&(r[n][1].substring(r[n][1].length-r[n-1][1].length)==r[n-1][1]?(r[n][1]=r[n-1][1]+r[n][1].substring(0,r[n][1].length-r[n-1][1].length),r[n+1][1]=r[n-1][1]+r[n+1][1],r.splice(n-1,1),h=!0):r[n][1].substring(0,r[n+1][1].length)==r[n+1][1]&&(r[n-1][1]+=r[n+1][1],r[n][1]=r[n][1].substring(r[n+1][1].length)+r[n+1][1],r.splice(n+1,1),h=!0)),n++;h&&this.diff_cleanupMerge(r)},f.prototype.diff_xIndex=function(r,n){var e=0,t=0,a=0,i=0,s;for(s=0;s<r.length&&(r[s][0]!==m&&(e+=r[s][1].length),r[s][0]!==w&&(t+=r[s][1].length),!(e>n));s++)a=e,i=t;return r.length!=s&&r[s][0]===w?i:i+(n-a)},f.prototype.diff_prettyHtml=function(r){for(var n=[],e=/&/g,t=/</g,a=/>/g,i=/\n/g,s=0;s<r.length;s++){var h=r[s][0],l=r[s][1],g=l.replace(e,"&amp;").replace(t,"&lt;").replace(a,"&gt;").replace(i,"&para;<br>");switch(h){case m:n[s]='<ins style="background:#e6ffe6;">'+g+"</ins>";break;case w:n[s]='<del style="background:#ffe6e6;">'+g+"</del>";break;case o:n[s]="<span>"+g+"</span>";break}}return n.join("")},f.prototype.diff_text1=function(r){for(var n=[],e=0;e<r.length;e++)r[e][0]!==m&&(n[e]=r[e][1]);return n.join("")},f.prototype.diff_text2=function(r){for(var n=[],e=0;e<r.length;e++)r[e][0]!==w&&(n[e]=r[e][1]);return n.join("")},f.prototype.diff_levenshtein=function(r){for(var n=0,e=0,t=0,a=0;a<r.length;a++){var i=r[a][0],s=r[a][1];switch(i){case m:e+=s.length;break;case w:t+=s.length;break;case o:n+=Math.max(e,t),e=0,t=0;break}}return n+=Math.max(e,t),n},f.prototype.diff_toDelta=function(r){for(var n=[],e=0;e<r.length;e++)switch(r[e][0]){case m:n[e]="+"+encodeURI(r[e][1]);break;case w:n[e]="-"+r[e][1].length;break;case o:n[e]="="+r[e][1].length;break}return n.join(" ").replace(/%20/g," ")},f.prototype.diff_fromDelta=function(r,n){for(var e=[],t=0,a=0,i=n.split(/\t/g),s=0;s<i.length;s++){var h=i[s].substring(1);switch(i[s].charAt(0)){case"+":try{e[t++]=new f.Diff(m,decodeURI(h))}catch{throw new Error("Illegal escape in diff_fromDelta: "+h)}break;case"-":case"=":var l=parseInt(h,10);if(isNaN(l)||l<0)throw new Error("Invalid number in diff_fromDelta: "+h);var g=r.substring(a,a+=l);i[s].charAt(0)=="="?e[t++]=new f.Diff(o,g):e[t++]=new f.Diff(w,g);break;default:if(i[s])throw new Error("Invalid diff operation in diff_fromDelta: "+i[s])}}if(a!=r.length)throw new Error("Delta length ("+a+") does not equal source text length ("+r.length+").");return e},f.prototype.match_main=function(r,n,e){if(r==null||n==null||e==null)throw new Error("Null input. (match_main)");return e=Math.max(0,Math.min(e,r.length)),r==n?0:r.length?r.substring(e,e+n.length)==n?e:this.match_bitap_(r,n,e):-1},f.prototype.match_bitap_=function(r,n,e){if(n.length>this.Match_MaxBits)throw new Error("Pattern too long for this browser.");var t=this.match_alphabet_(n),a=this;function i(P,E){var y=P/n.length,d=Math.abs(e-E);return a.Match_Distance?y+d/a.Match_Distance:d?1:y}var s=this.Match_Threshold,h=r.indexOf(n,e);h!=-1&&(s=Math.min(i(0,h),s),h=r.lastIndexOf(n,e+n.length),h!=-1&&(s=Math.min(i(0,h),s)));var l=1<<n.length-1;h=-1;for(var g,u,v=n.length+r.length,c,_=0;_<n.length;_++){for(g=0,u=v;g<u;)i(_,e+u)<=s?g=u:v=u,u=Math.floor((v-g)/2+g);v=u;var b=Math.max(1,e-u+1),M=Math.min(e+u,r.length)+n.length,k=Array(M+2);k[M+1]=(1<<_)-1;for(var p=M;p>=b;p--){var D=t[r.charAt(p-1)];if(_===0?k[p]=(k[p+1]<<1|1)&D:k[p]=(k[p+1]<<1|1)&D|((c[p+1]|c[p])<<1|1)|c[p+1],k[p]&l){var A=i(_,p-1);if(A<=s)if(s=A,h=p-1,h>e)b=Math.max(1,2*e-h);else break}}if(i(_+1,e)>s)break;c=k}return h},f.prototype.match_alphabet_=function(r){for(var n={},e=0;e<r.length;e++)n[r.charAt(e)]=0;for(var e=0;e<r.length;e++)n[r.charAt(e)]|=1<<r.length-e-1;return n},f.prototype.patch_addContext_=function(r,n){if(n.length!=0){if(r.start2===null)throw Error("patch not initialized");for(var e=n.substring(r.start2,r.start2+r.length1),t=0;n.indexOf(e)!=n.lastIndexOf(e)&&e.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;)t+=this.Patch_Margin,e=n.substring(r.start2-t,r.start2+r.length1+t);t+=this.Patch_Margin;var a=n.substring(r.start2-t,r.start2);a&&r.diffs.unshift(new f.Diff(o,a));var i=n.substring(r.start2+r.length1,r.start2+r.length1+t);i&&r.diffs.push(new f.Diff(o,i)),r.start1-=a.length,r.start2-=a.length,r.length1+=a.length+i.length,r.length2+=a.length+i.length}},f.prototype.patch_make=function(r,n,e){var t,a;if(typeof r=="string"&&typeof n=="string"&&typeof e>"u")t=r,a=this.diff_main(t,n,!0),a.length>2&&(this.diff_cleanupSemantic(a),this.diff_cleanupEfficiency(a));else if(r&&typeof r=="object"&&typeof n>"u"&&typeof e>"u")a=r,t=this.diff_text1(a);else if(typeof r=="string"&&n&&typeof n=="object"&&typeof e>"u")t=r,a=n;else if(typeof r=="string"&&typeof n=="string"&&e&&typeof e=="object")t=r,a=e;else throw new Error("Unknown call format to patch_make.");if(a.length===0)return[];for(var i=[],s=new f.patch_obj,h=0,l=0,g=0,u=t,v=t,c=0;c<a.length;c++){var _=a[c][0],b=a[c][1];switch(!h&&_!==o&&(s.start1=l,s.start2=g),_){case m:s.diffs[h++]=a[c],s.length2+=b.length,v=v.substring(0,g)+b+v.substring(g);break;case w:s.length1+=b.length,s.diffs[h++]=a[c],v=v.substring(0,g)+v.substring(g+b.length);break;case o:b.length<=2*this.Patch_Margin&&h&&a.length!=c+1?(s.diffs[h++]=a[c],s.length1+=b.length,s.length2+=b.length):b.length>=2*this.Patch_Margin&&h&&(this.patch_addContext_(s,u),i.push(s),s=new f.patch_obj,h=0,u=v,l=g);break}_!==m&&(l+=b.length),_!==w&&(g+=b.length)}return h&&(this.patch_addContext_(s,u),i.push(s)),i},f.prototype.patch_deepCopy=function(r){for(var n=[],e=0;e<r.length;e++){var t=r[e],a=new f.patch_obj;a.diffs=[];for(var i=0;i<t.diffs.length;i++)a.diffs[i]=new f.Diff(t.diffs[i][0],t.diffs[i][1]);a.start1=t.start1,a.start2=t.start2,a.length1=t.length1,a.length2=t.length2,n[e]=a}return n},f.prototype.patch_apply=function(r,n){if(r.length==0)return[n,[]];r=this.patch_deepCopy(r);var e=this.patch_addPadding(r);n=e+n+e,this.patch_splitMax(r);for(var t=0,a=[],i=0;i<r.length;i++){var s=r[i].start2+t,h=this.diff_text1(r[i].diffs),l,g=-1;if(h.length>this.Match_MaxBits?(l=this.match_main(n,h.substring(0,this.Match_MaxBits),s),l!=-1&&(g=this.match_main(n,h.substring(h.length-this.Match_MaxBits),s+h.length-this.Match_MaxBits),(g==-1||l>=g)&&(l=-1))):l=this.match_main(n,h,s),l==-1)a[i]=!1,t-=r[i].length2-r[i].length1;else{a[i]=!0,t=l-s;var u;if(g==-1?u=n.substring(l,l+h.length):u=n.substring(l,g+this.Match_MaxBits),h==u)n=n.substring(0,l)+this.diff_text2(r[i].diffs)+n.substring(l+h.length);else{var v=this.diff_main(h,u,!1);if(h.length>this.Match_MaxBits&&this.diff_levenshtein(v)/h.length>this.Patch_DeleteThreshold)a[i]=!1;else{this.diff_cleanupSemanticLossless(v);for(var c=0,_,b=0;b<r[i].diffs.length;b++){var M=r[i].diffs[b];M[0]!==o&&(_=this.diff_xIndex(v,c)),M[0]===m?n=n.substring(0,l+_)+M[1]+n.substring(l+_):M[0]===w&&(n=n.substring(0,l+_)+n.substring(l+this.diff_xIndex(v,c+M[1].length))),M[0]!==w&&(c+=M[1].length)}}}}}return n=n.substring(e.length,n.length-e.length),[n,a]},f.prototype.patch_addPadding=function(r){for(var n=this.Patch_Margin,e="",t=1;t<=n;t++)e+=String.fromCharCode(t);for(var t=0;t<r.length;t++)r[t].start1+=n,r[t].start2+=n;var a=r[0],i=a.diffs;if(i.length==0||i[0][0]!=o)i.unshift(new f.Diff(o,e)),a.start1-=n,a.start2-=n,a.length1+=n,a.length2+=n;else if(n>i[0][1].length){var s=n-i[0][1].length;i[0][1]=e.substring(i[0][1].length)+i[0][1],a.start1-=s,a.start2-=s,a.length1+=s,a.length2+=s}if(a=r[r.length-1],i=a.diffs,i.length==0||i[i.length-1][0]!=o)i.push(new f.Diff(o,e)),a.length1+=n,a.length2+=n;else if(n>i[i.length-1][1].length){var s=n-i[i.length-1][1].length;i[i.length-1][1]+=e.substring(0,s),a.length1+=s,a.length2+=s}return e},f.prototype.patch_splitMax=function(r){for(var n=this.Match_MaxBits,e=0;e<r.length;e++)if(!(r[e].length1<=n)){var t=r[e];r.splice(e--,1);for(var a=t.start1,i=t.start2,s="";t.diffs.length!==0;){var h=new f.patch_obj,l=!0;for(h.start1=a-s.length,h.start2=i-s.length,s!==""&&(h.length1=h.length2=s.length,h.diffs.push(new f.Diff(o,s)));t.diffs.length!==0&&h.length1<n-this.Patch_Margin;){var g=t.diffs[0][0],u=t.diffs[0][1];g===m?(h.length2+=u.length,i+=u.length,h.diffs.push(t.diffs.shift()),l=!1):g===w&&h.diffs.length==1&&h.diffs[0][0]==o&&u.length>2*n?(h.length1+=u.length,a+=u.length,l=!1,h.diffs.push(new f.Diff(g,u)),t.diffs.shift()):(u=u.substring(0,n-h.length1-this.Patch_Margin),h.length1+=u.length,a+=u.length,g===o?(h.length2+=u.length,i+=u.length):l=!1,h.diffs.push(new f.Diff(g,u)),u==t.diffs[0][1]?t.diffs.shift():t.diffs[0][1]=t.diffs[0][1].substring(u.length))}s=this.diff_text2(h.diffs),s=s.substring(s.length-this.Patch_Margin);var v=this.diff_text1(t.diffs).substring(0,this.Patch_Margin);v!==""&&(h.length1+=v.length,h.length2+=v.length,h.diffs.length!==0&&h.diffs[h.diffs.length-1][0]===o?h.diffs[h.diffs.length-1][1]+=v:h.diffs.push(new f.Diff(o,v))),l||r.splice(++e,0,h)}}},f.prototype.patch_toText=function(r){for(var n=[],e=0;e<r.length;e++)n[e]=r[e];return n.join("")},f.prototype.patch_fromText=function(r){var n=[];if(!r)return n;for(var e=r.split(`
3
+ `),t=0,a=/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;t<e.length;){var i=e[t].match(a);if(!i)throw new Error("Invalid patch string: "+e[t]);var s=new f.patch_obj;for(n.push(s),s.start1=parseInt(i[1],10),i[2]===""?(s.start1--,s.length1=1):i[2]=="0"?s.length1=0:(s.start1--,s.length1=parseInt(i[2],10)),s.start2=parseInt(i[3],10),i[4]===""?(s.start2--,s.length2=1):i[4]=="0"?s.length2=0:(s.start2--,s.length2=parseInt(i[4],10)),t++;t<e.length;){var h=e[t].charAt(0);try{var l=decodeURI(e[t].substring(1))}catch{throw new Error("Illegal escape in patch_fromText: "+l)}if(h=="-")s.diffs.push(new f.Diff(w,l));else if(h=="+")s.diffs.push(new f.Diff(m,l));else if(h==" ")s.diffs.push(new f.Diff(o,l));else{if(h=="@")break;if(h!=="")throw new Error('Invalid patch mode "'+h+'" in: '+l)}t++}}return n},f.patch_obj=function(){this.diffs=[],this.start1=null,this.start2=null,this.length1=0,this.length2=0},f.patch_obj.prototype.toString=function(){var r,n;this.length1===0?r=this.start1+",0":this.length1==1?r=this.start1+1:r=this.start1+1+","+this.length1,this.length2===0?n=this.start2+",0":this.length2==1?n=this.start2+1:n=this.start2+1+","+this.length2;for(var e=["@@ -"+r+" +"+n+` @@
4
+ `],t,a=0;a<this.diffs.length;a++){switch(this.diffs[a][0]){case m:t="+";break;case w:t="-";break;case o:t=" ";break}e[a+1]=t+encodeURI(this.diffs[a][1])+`
5
+ `}return e.join("").replace(/%20/g," ")},S.exports=f,S.exports.diff_match_patch=f,S.exports.DIFF_DELETE=w,S.exports.DIFF_INSERT=m,S.exports.DIFF_EQUAL=o})(R)),R.exports}var C=B();const L=F(C),j=N({__proto__:null,default:L},[C]);export{L as d,j as i};
@@ -1 +1 @@
1
- import{i as e}from"./index-Bc5CmFMl.js";const i=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 s=e("Minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]);const y=e("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);export{s as M,y as P,i as a};
1
+ import{i as e}from"./index-DbzKrh7H.js";const i=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 s=e("Minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]);const y=e("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);export{s as M,y as P,i as a};
@@ -7,12 +7,12 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=Inter:wght@400;500;600;700;800&amp;family=JetBrains+Mono:wght@400;500;600&amp;family=Source+Code+Pro:wght@400;500;600&amp;display=swap" />
10
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/iosevka@latest/latin-400-normal.css" />
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka@5.2.5/400.css" />
11
11
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
12
12
  <title>TexLite</title>
13
- <script type="module" crossorigin src="/assets/index-Bc5CmFMl.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DbzKrh7H.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-CsO_xvbl.js">
15
- <link rel="stylesheet" crossorigin href="/assets/index-CMqYAbDx.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-CtVAEMSo.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -20,6 +20,7 @@ import { CollaborationService } from "./collaboration.js";
20
20
  import { ProjectGitService } from "./git.js";
21
21
  import { LatexCompletionService } from "./latexCompletion.js";
22
22
  import { ProjectHistoryService } from "./history.js";
23
+ import { LatexFormatterService } from "./latexFormatter.js";
23
24
  import { buildProjectOutline } from "./projectOutline.js";
24
25
  import { replaceProject, searchProject } from "./projectSearch.js";
25
26
  import { MetricRegistry } from "./metrics.js";
@@ -159,6 +160,7 @@ export async function buildApp(config, db, options = {}) {
159
160
  eventLoopDelay.enable();
160
161
  const history = new ProjectHistoryService(config, db);
161
162
  const latexCompletions = new LatexCompletionService(config);
163
+ const latexFormatter = new LatexFormatterService();
162
164
  const recordHistory = (projectId, userId, reason, paths) => {
163
165
  try {
164
166
  return history.record(projectId, userId, reason, paths);
@@ -238,7 +240,7 @@ export async function buildApp(config, db, options = {}) {
238
240
  allowedEngines: config.allowedEngines,
239
241
  allowProjectLatexmkrc: config.allowProjectLatexmkrc
240
242
  }));
241
- app.get("/api/health", async () => ({ ok: true, latexmk: config.latexmk }));
243
+ app.get("/api/health", async () => ({ ok: true, pid: process.pid, latexmk: config.latexmk }));
242
244
  app.get("/api/health/metrics", async (request, reply) => {
243
245
  if (!requireAdmin(request, reply, db))
244
246
  return;
@@ -1023,6 +1025,38 @@ export async function buildApp(config, db, options = {}) {
1023
1025
  return apiError(reply, 404, "PROJECT_NOT_FOUND", "项目不存在");
1024
1026
  return { files: listProjectFiles(config, id) };
1025
1027
  });
1028
+ app.post("/api/projects/:id/format", async (request, reply) => {
1029
+ const user = requireUser(request, reply, db);
1030
+ if (!user)
1031
+ return;
1032
+ const { id } = request.params;
1033
+ const project = accessibleProject(db, id, user);
1034
+ if (!project)
1035
+ return apiError(reply, 404, "PROJECT_NOT_FOUND", "项目不存在");
1036
+ if (!canEdit(project))
1037
+ return apiError(reply, 403, "PROJECT_EDIT_FORBIDDEN", "没有编辑权限");
1038
+ const body = request.body;
1039
+ const filePath = safeRelativePath(typeof body?.path === "string" ? body.path : "");
1040
+ if (!/\.(?:tex|bib|cls|sty)$/i.test(filePath)) {
1041
+ return apiError(reply, 400, "FORMAT_FILE_UNSUPPORTED", "只有 .tex、.bib、.cls 和 .sty 文件支持格式化", { path: filePath });
1042
+ }
1043
+ if (typeof body?.source !== "string")
1044
+ return apiError(reply, 400, "FORMAT_SOURCE_INVALID", "待格式化内容格式不正确");
1045
+ if (Buffer.byteLength(body.source, "utf8") > config.maxUploadBytes) {
1046
+ return apiError(reply, 413, "FILE_TOO_LARGE", `单个文件不能超过 ${Math.floor(config.maxUploadBytes / 1024 / 1024)} MB`, { path: filePath });
1047
+ }
1048
+ try {
1049
+ return await latexFormatter.format(body.source, sourceRoot(config, id));
1050
+ }
1051
+ catch (formatError) {
1052
+ const statusCode = typeof formatError === "object" && formatError !== null && "statusCode" in formatError && typeof formatError.statusCode === "number"
1053
+ ? formatError.statusCode : 422;
1054
+ const code = typeof formatError === "object" && formatError !== null && "code" in formatError && typeof formatError.code === "string"
1055
+ ? formatError.code : "FORMAT_FAILED";
1056
+ const message = formatError instanceof Error ? formatError.message : "LaTeX formatting failed";
1057
+ return apiError(reply, statusCode, code, message);
1058
+ }
1059
+ });
1026
1060
  app.get("/api/projects/:id/outline", async (request, reply) => {
1027
1061
  const user = requireUser(request, reply, db);
1028
1062
  if (!user)
@@ -33,6 +33,7 @@ Commands:
33
33
  Options:
34
34
  --config PATH Use this configuration file instead of TEXLITE_CONFIG or the XDG default.
35
35
  --git Make doctor check the optional Git integration as well.
36
+ --json Print status as JSON for scripts instead of the terminal view.
36
37
  -h, --help Show this help message.
37
38
  -v, --version Print the installed TexLite version.
38
39
  `;
@@ -40,18 +41,23 @@ export function parseArgs(args) {
40
41
  let command = "help";
41
42
  let configPath;
42
43
  let checkGit = false;
44
+ let json = false;
43
45
  for (let index = 0; index < args.length; index += 1) {
44
46
  const argument = args[index];
45
47
  if (!argument)
46
48
  continue;
47
49
  if (argument === "-h" || argument === "--help")
48
- return { command: "help", configPath, checkGit };
50
+ return { command: "help", configPath, checkGit, json };
49
51
  if (argument === "-v" || argument === "--version")
50
- return { command: "version", configPath, checkGit };
52
+ return { command: "version", configPath, checkGit, json };
51
53
  if (argument === "--git") {
52
54
  checkGit = true;
53
55
  continue;
54
56
  }
57
+ if (argument === "--json") {
58
+ json = true;
59
+ continue;
60
+ }
55
61
  if (argument === "--config") {
56
62
  const value = args[++index];
57
63
  if (!value)
@@ -72,7 +78,7 @@ export function parseArgs(args) {
72
78
  throw new Error(`Only one command may be specified (already received ${command})`);
73
79
  command = argument;
74
80
  }
75
- return { command, configPath, checkGit };
81
+ return { command, configPath, checkGit, json };
76
82
  }
77
83
  function output(message) {
78
84
  stdout.write(`${message}\n`);
@@ -136,10 +142,18 @@ async function initialize(options) {
136
142
  let db = null;
137
143
  try {
138
144
  if (!fs.existsSync(configPath)) {
139
- const siteName = rl
140
- ? (await rl.question(`Site name [${CONFIG_DEFAULTS.siteName}]: `)).trim() || CONFIG_DEFAULTS.siteName
141
- : CONFIG_DEFAULTS.siteName;
142
- const adminEmail = rl ? (await rl.question("Administrator contact email (optional): ")).trim() : "";
145
+ const configuredSiteName = process.env.TEXLITE_SITE_NAME;
146
+ const siteName = configuredSiteName !== undefined
147
+ ? configuredSiteName.trim() || CONFIG_DEFAULTS.siteName
148
+ : rl
149
+ ? (await rl.question(`Site name [${CONFIG_DEFAULTS.siteName}]: `)).trim() || CONFIG_DEFAULTS.siteName
150
+ : CONFIG_DEFAULTS.siteName;
151
+ const configuredAdminEmail = process.env.TEXLITE_ADMIN_EMAIL;
152
+ const adminEmail = configuredAdminEmail !== undefined
153
+ ? configuredAdminEmail.trim()
154
+ : rl
155
+ ? (await rl.question("Administrator contact email (optional): ")).trim()
156
+ : "";
143
157
  writeInitialConfig(configPath, siteName, adminEmail);
144
158
  output(`Created configuration file: ${configPath}`);
145
159
  }
@@ -240,10 +254,83 @@ async function start(options, restart = false) {
240
254
  async function status(options) {
241
255
  const { config } = await loadValidatedConfig(options);
242
256
  const result = await processStatus(config);
243
- output(JSON.stringify(result, null, 2));
257
+ output(options.json ? JSON.stringify(result, null, 2) : formatProcessStatus(result, terminalColorsEnabled()));
244
258
  if (result.status !== "online")
245
259
  process.exitCode = 2;
246
260
  }
261
+ const ANSI = {
262
+ bold: "\u001b[1m",
263
+ green: "\u001b[32m",
264
+ yellow: "\u001b[33m",
265
+ red: "\u001b[31m",
266
+ gray: "\u001b[90m",
267
+ reset: "\u001b[0m"
268
+ };
269
+ function terminalColorsEnabled() {
270
+ if (Object.hasOwn(process.env, "NO_COLOR"))
271
+ return false;
272
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
273
+ return true;
274
+ return Boolean(stdout.isTTY);
275
+ }
276
+ function colorize(value, color, enabled) {
277
+ return enabled ? `${ANSI[color]}${value}${ANSI.reset}` : value;
278
+ }
279
+ function duration(value) {
280
+ if (value === null)
281
+ return "n/a";
282
+ if (value < 60)
283
+ return `${value}s`;
284
+ const minutes = Math.floor(value / 60);
285
+ const seconds = value % 60;
286
+ if (minutes < 60)
287
+ return `${minutes}min ${seconds}s`;
288
+ const hours = Math.floor(minutes / 60);
289
+ const remainingMinutes = minutes % 60;
290
+ if (hours < 24)
291
+ return `${hours}h ${remainingMinutes}min`;
292
+ const days = Math.floor(hours / 24);
293
+ return `${days}d ${hours % 24}h`;
294
+ }
295
+ function statusAppearance(status) {
296
+ switch (status) {
297
+ case "online": return { color: "green", active: "active (running)", health: "healthy" };
298
+ case "unhealthy": return { color: "red", active: "failed (unhealthy)", health: "unhealthy" };
299
+ case "errored": return { color: "red", active: "failed (errored)", health: "unhealthy" };
300
+ case "launching": return { color: "yellow", active: "activating (starting)", health: "starting" };
301
+ case "stopped": return { color: "yellow", active: "inactive (stopped)", health: "not running" };
302
+ case "missing": return { color: "gray", active: "inactive (not managed)", health: "not running" };
303
+ default: return { color: "yellow", active: status, health: "unknown" };
304
+ }
305
+ }
306
+ export function formatProcessStatus(status, colors = false) {
307
+ const appearance = statusAppearance(status.status);
308
+ const bullet = colorize("●", appearance.color, colors);
309
+ const title = colors ? `${ANSI.bold}TexLite${ANSI.reset}` : "TexLite";
310
+ const active = colorize(appearance.active, appearance.color, colors);
311
+ const health = colorize(appearance.health, appearance.color, colors);
312
+ const loaded = status.pm2Status === "missing"
313
+ ? colorize(`not-found (PM2 process: ${status.name})`, "gray", colors)
314
+ : `loaded (PM2 process: ${status.name})`;
315
+ const since = status.startedAt && status.uptimeSeconds !== null
316
+ ? ` since ${status.startedAt}; ${duration(status.uptimeSeconds)} ago`
317
+ : "";
318
+ return [
319
+ `${bullet} ${title} - Lightweight collaborative LaTeX editor`,
320
+ ` Loaded: ${loaded}`,
321
+ ` Active: ${active}${since}`,
322
+ ` Health: ${health} (PM2: ${status.pm2Status})`,
323
+ ` Main PID: ${status.pid ?? "n/a"}`,
324
+ ` Version: ${status.version}`,
325
+ ` Restarts: ${status.restarts}`,
326
+ ` Listen: ${status.address}`,
327
+ ` Config: ${status.configPath}`,
328
+ ` Data: ${status.dataDir}`,
329
+ ` CWD: ${status.cwd}`,
330
+ ` Stdout: ${status.outputLog ?? "n/a"}`,
331
+ ` Stderr: ${status.errorLog ?? "n/a"}`
332
+ ].join("\n");
333
+ }
247
334
  async function stop(options) {
248
335
  const { config } = await loadValidatedConfig(options);
249
336
  const managed = await stopManaged(config.configPath);
@@ -29,6 +29,8 @@ export async function startServer(configPath) {
29
29
  }
30
30
  app.log.info(`Environment ready: ${environment.map((item) => `${item.name} ${item.version}`).join(", ")}`);
31
31
  app.log.info(`${config.siteName} running at http://${config.host}:${config.port}`);
32
+ if (typeof process.send === "function")
33
+ process.send("ready");
32
34
  return {
33
35
  address: `http://${config.host}:${config.port}`,
34
36
  close: async () => {
@@ -62,7 +64,7 @@ export async function main() {
62
64
  if (isMainModule()) {
63
65
  main().catch((error) => {
64
66
  console.error(error instanceof Error ? error.message : error);
65
- process.exitCode = 1;
67
+ process.exit(1);
66
68
  });
67
69
  }
68
70
  function isMainModule() {
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_COMMAND = "tex-fmt";
3
+ const FORMAT_TIMEOUT_MS = 30_000;
4
+ const MAX_DIAGNOSTIC_BYTES = 16 * 1024;
5
+ /**
6
+ * Invokes the optional host formatter without involving a shell. Keeping
7
+ * this in the server means project-level tex-fmt.toml files are discovered
8
+ * from the project's source directory and the browser never needs a native
9
+ * executable.
10
+ */
11
+ export class LatexFormatterService {
12
+ command;
13
+ constructor(command = process.env.TEXLITE_TEX_FMT?.trim() || DEFAULT_COMMAND) {
14
+ this.command = command;
15
+ }
16
+ async format(source, cwd) {
17
+ const result = await runCommand(this.command, ["--stdin"], source, cwd, FORMAT_TIMEOUT_MS);
18
+ // tex-fmt is optional like Git, but formatting must never silently switch
19
+ // algorithms: users should know when the host formatter is unavailable.
20
+ if (result.error?.code === "ENOENT")
21
+ throw formatterUnavailable(this.command);
22
+ if (result.error)
23
+ throw formatterError(`无法运行 tex-fmt:${result.error.message}`);
24
+ if (result.status !== 0) {
25
+ const detail = result.stderr.trim() || `退出码 ${result.status ?? "unknown"}`;
26
+ throw formatterError(`tex-fmt 格式化失败:${truncate(detail)}`);
27
+ }
28
+ if (source.trim() && !result.stdout.trim())
29
+ throw formatterError("tex-fmt 未返回格式化后的源码");
30
+ return { formatter: "tex-fmt", formatted: result.stdout };
31
+ }
32
+ }
33
+ function formatterError(message) {
34
+ return Object.assign(new Error(message), { code: "FORMAT_FAILED", statusCode: 422 });
35
+ }
36
+ function formatterUnavailable(command) {
37
+ return Object.assign(new Error(`tex-fmt is not installed or cannot be found as "${command}". Install it from https://github.com/wgunderwood/tex-fmt and restart TexLite.`), { code: "FORMATTER_UNAVAILABLE", statusCode: 503 });
38
+ }
39
+ function truncate(value) {
40
+ const bytes = Buffer.byteLength(value, "utf8");
41
+ if (bytes <= MAX_DIAGNOSTIC_BYTES)
42
+ return value;
43
+ return `${Buffer.from(value, "utf8").subarray(0, MAX_DIAGNOSTIC_BYTES).toString("utf8")}…`;
44
+ }
45
+ function runCommand(command, args, input, cwd, timeoutMs) {
46
+ return new Promise((resolve) => {
47
+ let child;
48
+ try {
49
+ child = spawn(command, args, {
50
+ cwd,
51
+ env: { ...process.env, LC_ALL: "C" },
52
+ shell: false,
53
+ stdio: ["pipe", "pipe", "pipe"]
54
+ });
55
+ }
56
+ catch (error) {
57
+ resolve({
58
+ status: null,
59
+ stdout: "",
60
+ stderr: "",
61
+ error: error instanceof Error ? Object.assign(error, { code: error.code }) : new Error(String(error))
62
+ });
63
+ return;
64
+ }
65
+ const stdout = [];
66
+ const stderr = [];
67
+ let stdoutBytes = 0;
68
+ let stderrBytes = 0;
69
+ let settled = false;
70
+ let timedOut = false;
71
+ let outputTooLarge = false;
72
+ const finish = (result) => {
73
+ if (settled)
74
+ return;
75
+ settled = true;
76
+ clearTimeout(timer);
77
+ resolve({
78
+ ...result,
79
+ stdout: Buffer.concat(stdout).toString("utf8"),
80
+ stderr: Buffer.concat(stderr).toString("utf8")
81
+ });
82
+ };
83
+ const timer = setTimeout(() => {
84
+ timedOut = true;
85
+ child.kill("SIGKILL");
86
+ }, timeoutMs);
87
+ child.stdout.on("data", (chunk) => {
88
+ if (outputTooLarge || stdoutBytes >= 64 * 1024 * 1024)
89
+ return;
90
+ const remaining = 64 * 1024 * 1024 - stdoutBytes;
91
+ const bytes = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
92
+ stdout.push(bytes);
93
+ stdoutBytes += bytes.byteLength;
94
+ if (chunk.byteLength > remaining) {
95
+ outputTooLarge = true;
96
+ child.kill("SIGKILL");
97
+ }
98
+ });
99
+ child.stderr.on("data", (chunk) => {
100
+ if (stderrBytes >= MAX_DIAGNOSTIC_BYTES)
101
+ return;
102
+ const remaining = MAX_DIAGNOSTIC_BYTES - stderrBytes;
103
+ const bytes = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
104
+ stderr.push(bytes);
105
+ stderrBytes += bytes.byteLength;
106
+ });
107
+ child.once("error", (error) => finish({ status: null, error }));
108
+ child.once("close", (status) => {
109
+ finish({
110
+ status,
111
+ error: timedOut ? new Error(`命令超时(${timeoutMs}ms)`) : outputTooLarge ? new Error("tex-fmt 输出过大") : null
112
+ });
113
+ });
114
+ child.stdin.end(input, "utf8");
115
+ });
116
+ }
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
+ import http from "node:http";
3
4
  import path from "node:path";
4
5
  import { defaultConfigPath, packageRootDirectory, packageServerEntry } from "./runtimePaths.js";
5
6
  let pm2Client = null;
@@ -39,7 +40,10 @@ function environmentOf(process) {
39
40
  }
40
41
  export function processName(configPath) {
41
42
  const absoluteConfigPath = path.resolve(configPath);
42
- if (absoluteConfigPath === defaultConfigPath())
43
+ // Reserve the short conventional name for the standard fallback path only.
44
+ // If XDG_CONFIG_HOME points elsewhere, the absolute path must participate in
45
+ // the name or every isolated/test installation would collide on `texlite`.
46
+ if (absoluteConfigPath === defaultConfigPath({}))
43
47
  return "texlite";
44
48
  const digest = crypto.createHash("sha256").update(absoluteConfigPath).digest("hex").slice(0, 8);
45
49
  return `texlite-${digest}`;
@@ -76,11 +80,26 @@ function startOptions(config) {
76
80
  instances: 1,
77
81
  autorestart: true,
78
82
  watch: false,
83
+ wait_ready: true,
84
+ listen_timeout: 45_000,
85
+ min_uptime: 10_000,
86
+ max_restarts: 5,
87
+ restart_delay: 500,
79
88
  max_memory_restart: "512M",
80
89
  kill_timeout: 10_000,
81
90
  env: environment
82
91
  };
83
92
  }
93
+ function launch(config) {
94
+ return callback((done) => {
95
+ pm2Client.start(startOptions(config), (error, process) => done(error ?? null, process));
96
+ });
97
+ }
98
+ async function replaceProcess(existing, config) {
99
+ await callback((done) => pm2Client.delete(processId(existing), (error) => done(error ?? null)));
100
+ const started = await launch(config);
101
+ return Array.isArray(started) ? started[0] : started;
102
+ }
84
103
  export async function startManaged(config) {
85
104
  await connect();
86
105
  try {
@@ -92,13 +111,18 @@ export async function startManaged(config) {
92
111
  && environmentOf(process).TEXLITE_CONFIG === absoluteConfigPath);
93
112
  if (existing) {
94
113
  const status = existing.pm2_env?.status;
95
- if (status === "online" || status === "launching")
114
+ if (status === "launching")
96
115
  return { name, configPath: config.configPath, description: existing };
97
- await callback((done) => pm2Client.restart(processId(existing), (error) => done(error ?? null)));
98
- const refreshed = (await list()).find((process) => process.name === name) ?? existing;
116
+ if (status === "online") {
117
+ const probe = await probeService(config);
118
+ if (probe.ok && probe.pid === existing.pid) {
119
+ return { name, configPath: config.configPath, description: existing };
120
+ }
121
+ }
122
+ const refreshed = await replaceProcess(existing, config);
99
123
  return { name, configPath: config.configPath, description: refreshed };
100
124
  }
101
- const started = await callback((done) => pm2Client.start(startOptions(config), (error, process) => done(error ?? null, process)));
125
+ const started = await launch(config);
102
126
  const description = Array.isArray(started) ? started[0] : started;
103
127
  return { name, configPath: config.configPath, description };
104
128
  }
@@ -136,11 +160,10 @@ export async function restartManaged(config) {
136
160
  const existing = processes.find((process) => process.name === name
137
161
  && environmentOf(process).TEXLITE_CONFIG === absoluteConfigPath);
138
162
  if (!existing) {
139
- const started = await callback((done) => pm2Client.start(startOptions(config), (error, process) => done(error ?? null, process)));
163
+ const started = await launch(config);
140
164
  return { name, configPath: config.configPath, description: Array.isArray(started) ? started[0] : started };
141
165
  }
142
- await callback((done) => pm2Client.restart(processId(existing), (error) => done(error ?? null)));
143
- const description = (await list()).find((process) => process.name === name) ?? existing;
166
+ const description = await replaceProcess(existing, config);
144
167
  return { name, configPath: config.configPath, description };
145
168
  }
146
169
  finally {
@@ -151,12 +174,20 @@ export async function processStatus(config) {
151
174
  const managed = await managedProcess(config.configPath);
152
175
  const description = managed.description;
153
176
  const env = description ? description.pm2_env : undefined;
177
+ const pm2Status = env?.status ?? "missing";
178
+ const probe = pm2Status === "online" ? await probeService(config) : { ok: false, pid: null, error: null };
179
+ const healthy = Boolean(description?.pid && probe.ok && probe.pid === description.pid);
180
+ const active = pm2Status === "online" || pm2Status === "launching";
181
+ const startedAt = active && typeof env?.pm_uptime === "number" ? env.pm_uptime : null;
154
182
  return {
155
183
  name: managed.name,
156
184
  configPath: managed.configPath,
157
- status: env?.status ?? "missing",
185
+ status: pm2Status === "online" && !healthy ? "unhealthy" : pm2Status,
186
+ pm2Status,
187
+ healthy,
158
188
  pid: description?.pid ?? null,
159
- uptime: env?.pm_uptime ?? null,
189
+ startedAt: startedAt === null ? null : new Date(startedAt).toISOString(),
190
+ uptimeSeconds: startedAt === null ? null : Math.max(0, Math.floor((Date.now() - startedAt) / 1000)),
160
191
  restarts: env?.restart_time ?? 0,
161
192
  version: packageVersion(),
162
193
  address: `http://${config.host}:${config.port}`,
@@ -166,23 +197,95 @@ export async function processStatus(config) {
166
197
  errorLog: env?.pm_err_log_path ?? null
167
198
  };
168
199
  }
169
- /** Wait until PM2 has actually launched the service, rather than reporting
170
- * success while it is still in the `launching` state. */
171
- export async function waitForOnline(config, timeoutMs = 15_000) {
200
+ /** Wait for both PM2 and the HTTP service. The health response PID prevents a
201
+ * foreground `texlite serve` process on the same port from being mistaken for
202
+ * the newly launched managed process. */
203
+ export async function waitForOnline(config, timeoutMs = 50_000) {
172
204
  const deadline = Date.now() + timeoutMs;
173
205
  let latest = await managedProcess(config.configPath);
206
+ let consecutiveHealthyChecks = 0;
207
+ let healthyPid = null;
208
+ let latestProbe = { ok: false, pid: null, error: null };
174
209
  while (Date.now() < deadline) {
175
210
  const status = latest.description?.pm2_env?.status;
176
- if (status === "online")
177
- return latest;
211
+ if (status === "online") {
212
+ latestProbe = await probeService(config);
213
+ if (latestProbe.ok && latestProbe.pid === latest.description?.pid) {
214
+ consecutiveHealthyChecks = healthyPid === latestProbe.pid ? consecutiveHealthyChecks + 1 : 1;
215
+ healthyPid = latestProbe.pid;
216
+ if (consecutiveHealthyChecks >= 2)
217
+ return latest;
218
+ }
219
+ else {
220
+ consecutiveHealthyChecks = 0;
221
+ healthyPid = null;
222
+ }
223
+ }
224
+ else {
225
+ consecutiveHealthyChecks = 0;
226
+ healthyPid = null;
227
+ }
178
228
  if (status === "errored" || status === "stopped") {
179
229
  throw new Error(`PM2 could not start TexLite (current status: ${status}). Run texlite logs to inspect the logs.`);
180
230
  }
181
- await new Promise((resolve) => setTimeout(resolve, 150));
231
+ await new Promise((resolve) => setTimeout(resolve, 250));
182
232
  latest = await managedProcess(config.configPath);
183
233
  }
184
234
  const status = latest.description?.pm2_env?.status ?? "missing";
185
- throw new Error(`Timed out waiting for TexLite to start (current status: ${status}). Run texlite status or texlite logs to investigate.`);
235
+ const expectedPid = latest.description?.pid ?? "none";
236
+ const probeDetail = latestProbe.pid !== null
237
+ ? `health endpoint belongs to PID ${latestProbe.pid}, expected PM2 PID ${expectedPid}`
238
+ : latestProbe.error ?? "health endpoint did not respond";
239
+ throw new Error(`Timed out waiting for TexLite to become healthy at ${healthUrl(config)} (PM2 status: ${status}; ${probeDetail}). Run texlite logs to investigate.`);
240
+ }
241
+ function probeHost(host) {
242
+ if (host === "0.0.0.0")
243
+ return "127.0.0.1";
244
+ if (host === "::" || host === "[::]")
245
+ return "::1";
246
+ return host;
247
+ }
248
+ function healthUrl(config) {
249
+ const host = probeHost(config.host);
250
+ const formattedHost = host.includes(":") ? `[${host}]` : host;
251
+ return `http://${formattedHost}:${config.port}/api/health`;
252
+ }
253
+ function probeService(config, timeoutMs = 1_500) {
254
+ return new Promise((resolve) => {
255
+ let settled = false;
256
+ const finish = (result) => {
257
+ if (settled)
258
+ return;
259
+ settled = true;
260
+ resolve(result);
261
+ };
262
+ const request = http.get(healthUrl(config), { timeout: timeoutMs }, (response) => {
263
+ const chunks = [];
264
+ response.on("data", (chunk) => {
265
+ if (Buffer.concat(chunks).length < 16 * 1024)
266
+ chunks.push(chunk);
267
+ });
268
+ response.once("end", () => {
269
+ if (response.statusCode !== 200) {
270
+ finish({ ok: false, pid: null, error: `health endpoint returned HTTP ${response.statusCode ?? "unknown"}` });
271
+ return;
272
+ }
273
+ try {
274
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
275
+ finish({
276
+ ok: body.ok === true && Number.isInteger(body.pid),
277
+ pid: Number.isInteger(body.pid) ? body.pid : null,
278
+ error: body.ok === true && Number.isInteger(body.pid) ? null : "health endpoint returned an invalid response"
279
+ });
280
+ }
281
+ catch {
282
+ finish({ ok: false, pid: null, error: "health endpoint returned invalid JSON" });
283
+ }
284
+ });
285
+ });
286
+ request.once("timeout", () => request.destroy(new Error("health check timed out")));
287
+ request.once("error", (error) => finish({ ok: false, pid: null, error: error.message }));
288
+ });
186
289
  }
187
290
  export async function streamLogs(config) {
188
291
  await connect();
@@ -38,5 +38,5 @@ export function packageClientDirectory(moduleUrl = import.meta.url) {
38
38
  return path.join(packageRootDirectory(moduleUrl), "dist", "client");
39
39
  }
40
40
  export function packageServerEntry(moduleUrl = import.meta.url) {
41
- return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "index.js");
41
+ return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "service.js");
42
42
  }
@@ -0,0 +1,8 @@
1
+ import { main } from "./index.js";
2
+ // PM2 keeps an IPC channel open for managed processes. A failed startup must
3
+ // therefore terminate explicitly: assigning process.exitCode alone can leave
4
+ // an idle process that PM2 incorrectly reports as online.
5
+ main().catch((error) => {
6
+ console.error(error instanceof Error ? error.message : error);
7
+ process.exit(1);
8
+ });