papilot 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +110 -0
- package/README.es.md +110 -0
- package/README.fr.md +110 -0
- package/README.it.md +110 -0
- package/README.ja.md +110 -0
- package/README.ko.md +110 -0
- package/README.md +3 -1
- package/README.pt.md +110 -0
- package/README.sv.md +110 -0
- package/README.zh-CN.md +39 -29
- package/README.zh-Hant.md +110 -0
- package/dist/assets/index-oAIkxhfp.js +265 -0
- package/dist/assets/index-t_qCOF_B.css +1 -0
- package/dist/assets/{math-Bu3xEyrC.js → math-CEU4m4-H.js} +1 -1
- package/dist/assets/{pdf-view-PbkCuTZ-.js → pdf-view-HWRGBLsZ.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +11 -4
- package/src/server/acp.ts +9 -5
- package/src/server/agents.ts +4 -2
- package/src/server/app.ts +7 -0
- package/src/server/imports.ts +60 -21
- package/src/server/manuscript.ts +26 -8
- package/src/server/start.ts +8 -1
- package/src/server/store.ts +0 -8
- package/src/server/update-install.ts +22 -2
- package/src/server/updates.ts +27 -13
- package/src/server/workspace.ts +24 -2
- package/src/shared.ts +3 -0
- package/src/skill-metadata.ts +1 -1
- package/vendor/latex-review/dist/web/assets/index-DVs7cTBl.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-ysOpu05D.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/build.ts +180 -97
- package/vendor/latex-review/src/core/git.ts +133 -28
- package/vendor/latex-review/src/core/outline.ts +0 -9
- package/vendor/latex-review/src/locales.ts +47 -0
- package/vendor/latex-review/src/web/i18n.ts +9 -21
- package/vendor/latex-review/src/web/locales/de.json +250 -0
- package/vendor/latex-review/src/web/locales/es.json +250 -0
- package/vendor/latex-review/src/web/locales/fr.json +250 -0
- package/vendor/latex-review/src/web/locales/index.ts +16 -0
- package/vendor/latex-review/src/web/locales/it.json +250 -0
- package/vendor/latex-review/src/web/locales/ja.json +250 -0
- package/vendor/latex-review/src/web/locales/ko.json +250 -0
- package/vendor/latex-review/src/web/locales/pt.json +250 -0
- package/vendor/latex-review/src/web/locales/sv.json +250 -0
- package/vendor/latex-review/src/web/locales/zh-Hant.json +250 -0
- package/vendor/latex-review/src/web/references.tsx +1 -1
- package/vendor/latex-review/src/web/review.tsx +1 -1
- package/vendor/latex-review/src/web/style.css +1 -11
- package/dist/assets/index-BlLyIqT9.js +0 -265
- package/dist/assets/index-DVZtyZvu.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-CXdnUjct.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf)format("opentype");font-weight:400;font-display:swap}@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf)format("opentype");font-weight:700;font-display:swap}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--serif:"Review Serif", Palatino, "Palatino Linotype", "Songti SC", Georgia, serif;--sans:-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans", sans-serif;--frame:#fff;--bar:#fff;--ink:#171717;--muted:#666;--line:#e5e5e5;--paper:#fff;--accent:#356aa0;--accept:#1d7448;--reject:#b0313b;--comment:#356aa0;--before:#fbeeee;--before-edge:#d17c82;--after:#eef8f1;--after-edge:#68ab82;--head:#fafafa;--warn:#8a5300;--warn-soft:#fff8e6;--warn-line:#f1d48c;--ok-soft:#eef8f1;--bad-soft:#fbeeee;--shadow:0 1px 2px #0000000f, 0 4px 18px #0000000d;--paper-top:24px;font:14px/1.5 var(--sans);color:var(--ink);background:var(--frame)}*{box-sizing:border-box}body{background:var(--frame);text-rendering:optimizelegibility;grid-template-rows:auto 1fr;height:100dvh;margin:0;display:grid;overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.38}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}[hidden]{display:none!important}.muted{color:var(--muted)}a{color:var(--accent);text-underline-offset:.18em}h1,h2,.brand{font-family:var(--serif);font-weight:400;line-height:1.3}.brand{letter-spacing:.02em;color:var(--ink);font-size:20px;font-variant-caps:small-caps;text-decoration:none}kbd{border:1px solid var(--line);color:var(--muted);background:var(--frame);vertical-align:1px;border-radius:4px;margin-left:6px;padding:2px 4px;font:600 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.bar{background:var(--bar);border-bottom:1px solid var(--line);z-index:3;align-items:center;gap:18px;min-height:62px;padding:10px 24px;display:flex}.identity{flex:1;align-items:baseline;gap:10px;min-width:0;display:flex}.identity h1{letter-spacing:.02em;white-space:nowrap;text-overflow:ellipsis;margin:0;font-size:20px;font-variant-caps:small-caps;overflow:hidden}.identity span{white-space:nowrap;font-size:13px}.meter{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;align-items:center;gap:10px;font-size:12px;display:flex}.review-progress{flex:none;min-height:28px;padding:0 2px;line-height:1}.review-progress-value{color:var(--ink);font-weight:500}.track{background:var(--line);width:120px;height:2px;overflow:hidden}.track i{background:#999;width:0;height:100%;transition:width .3s;display:block}.bar-end{align-items:center;gap:8px;display:flex}.status:empty{display:none}.status{background:var(--head);color:var(--muted);white-space:nowrap;border-radius:999px;padding:3px 9px;font-size:12px}.status[data-state=build_failed],.status[data-state=offline]{color:var(--reject)}.ghost{color:var(--muted);background:0 0;border:1px solid #0000;border-radius:7px;padding:5px 9px;font-size:13px}.ghost:hover{border-color:var(--line);color:var(--ink);text-underline-offset:.18em;text-decoration:underline}.accept-all,.reject-all{color:var(--ink);border-color:var(--line);border-radius:999px;box-shadow:0 1px 3px #0000000d}.batch-count{text-align:center;font-variant-numeric:tabular-nums;min-width:18px;color:var(--muted);background:#0000000f;border-radius:999px;margin-left:6px;padding:0 5px;font-size:11.5px;line-height:18px;display:inline-block}.overview-link{color:var(--accent);flex:none;text-decoration:none}.ghost.round{border-color:var(--line);border-radius:50%;width:30px;height:30px;padding:0}#paper{padding:var(--paper-top) 24px 90px;overflow-anchor:none;outline:none;scroll-padding:70px 0 90px;overflow:auto}.document{max-width:980px;margin:0 auto}.empty{text-align:center;color:var(--muted);padding:60px 16px}.part+.part{margin-top:30px}.part-title{color:var(--muted);letter-spacing:.02em;margin:14px 0 18px;font-size:19px;font-variant-caps:small-caps;font-weight:400}.part-title .number{font-variant-numeric:tabular-nums;margin-right:8px}.sheet{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;padding:10px 0}.context,.side{line-height:0}.document img{width:100%;height:auto;display:block}.fold{color:#9a9d98;letter-spacing:4px;background:repeating-linear-gradient(90deg,#0000 0 6px,#ececec 6px 12px) 50%/100% 1px no-repeat;justify-content:center;align-items:center;height:22px;margin:4px 0;font-size:16px;display:flex}.change{color:var(--ink);outline:none;scroll-margin:70px;position:relative}.change.standalone{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;margin-bottom:26px}.change.inline{margin:6px 0}.change-head{background:var(--head);color:var(--muted);border-top:1px solid var(--line);border-bottom:1px solid var(--line);align-items:baseline;gap:10px;padding:8px 12px;font-size:12px;line-height:1.6;display:flex}.change-number{color:var(--ink);white-space:nowrap;font-weight:400}.change-where{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font:11px/1.8 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.change-kind{white-space:nowrap}.badge{white-space:nowrap;font-weight:600}.badge:empty{display:none}.link{color:var(--accent);background:0 0;border:0;padding:0 2px;font-size:12px}.link.active{font-weight:600;text-decoration:underline}.edits{white-space:nowrap}.side{border-left:2px solid #0000;position:relative}.side.before{background:var(--before);border-left-color:var(--before-edge)}.side.after{background:var(--after);border-left-color:var(--after-edge)}.side.after.identical{border-left-color:var(--line);background:0 0}.side img{mix-blend-mode:multiply}.side:after{content:attr(aria-label);font:10px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase;color:var(--before-edge);opacity:.9;pointer-events:none;position:absolute;top:4px;right:8px}.side.after:after{color:var(--after-edge)}.side.after.identical:after,.change[data-resolved] .side:after{content:none}.caption{color:var(--muted);margin:0;padding:4px 12px 8px;font-size:12px}.effects{border-top:1px solid var(--line);padding:6px 0 10px}.effects-head,.place-where{color:var(--muted);margin:0;padding:4px 14px;font-size:12px}.effects-head{font-weight:600}.effects-toggle{font:inherit;color:inherit}.effects-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.more-places{margin:6px 14px 0;font-size:12px}.change.current{box-shadow:0 0 0 2px var(--accent);z-index:1}.change.current .change-head{background:color-mix(in srgb, var(--accent) 8%, var(--head));color:var(--ink)}.change[data-state=accepted] .badge{color:var(--accept)}.change[data-state=rejected] .badge{color:var(--reject)}.change[data-state=commented] .badge,.change[data-state=mixed] .badge{color:var(--comment)}.change[data-resolved=after] .side.before,.change[data-resolved=before] .side.after:not(.identical){display:none}.change[data-resolved] .side{background:0 0;border-left-color:#0000}.change[data-resolved] .caption{display:none}.remark,.histories{padding:8px 14px;font-size:13px;line-height:1.55}.remark{background:color-mix(in srgb, var(--comment) 8%, #fff);border-top:1px solid var(--line);color:var(--ink)}.remark p,.histories p{white-space:pre-wrap;margin:0}.histories{border-bottom:1px solid var(--line);color:var(--muted);background:#f6f7fb}.source{background:var(--paper);border-top:1px solid var(--line);padding:10px 14px 12px;line-height:1.5}.source-unit+.source-unit{margin-top:12px}.source-title{color:#555;align-items:center;gap:8px;margin-bottom:6px;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;display:flex}.unit-actions{font:13px var(--sans);align-items:center;gap:2px;margin-left:auto;display:flex}.unit-actions button{border:1px solid var(--line);background:var(--paper);border-radius:0;padding:1px 8px;font-weight:600}.unit-actions button:not(:disabled):hover{background:var(--frame)}.unit-actions .accept{color:var(--accept)}.unit-actions .reject{color:var(--reject)}.unit-actions .comment{color:var(--comment)}.unit-actions .unstage{color:var(--muted)}.unit-state{font-weight:600}.source-unit[data-state=accepted] .unit-state{color:var(--accept)}.source-unit[data-state=rejected] .unit-state{color:var(--reject)}.source-unit[data-state=commented] .unit-state{color:var(--comment)}.source-code{border:1px solid var(--line);white-space:pre-wrap;overflow-wrap:anywhere;color:#2b2f33;background:#fff;border-radius:5px;max-height:420px;margin:0;padding:10px 12px;font:12.5px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:auto}.source-code del{color:#8e1d28;background:#fadfe1;-webkit-text-decoration:line-through #c9636b;text-decoration:line-through #c9636b}.source-code ins{color:#11603a;background:#d6f0df;text-decoration:none}.references{border-top:1px solid var(--line);background:var(--paper)}.reference-entry{border-left:3px solid var(--line);padding:10px 14px 12px}.reference-entry+.reference-entry{border-top:1px solid var(--line)}.reference-entry[data-check=verified],.reference-entry[data-check=confirmed]{border-left-color:var(--accept)}.reference-entry[data-check=mismatch],.reference-entry[data-check=unreadable]{border-left-color:var(--reject)}.reference-entry[data-check=unverified],.reference-entry[data-check=failed],.reference-entry[data-check=skipped],.reference-entry[data-check=off],.reference-entry[data-check=checking]{border-left-color:var(--warn-line)}.reference-head{flex-wrap:wrap;align-items:baseline;gap:4px 10px;margin-bottom:6px;display:flex}.reference-key{overflow-wrap:anywhere;font:600 12.5px ui-monospace,SFMono-Regular,Menlo,monospace}.reference-key del,.reference-entry td del{background:var(--bad-soft);color:var(--reject);text-decoration:line-through}.reference-key ins,.reference-entry td ins{background:var(--ok-soft);color:var(--accept);text-decoration:none}.reference-status{color:var(--muted);font-size:12px}.reference-check{border:1px solid var(--line);white-space:nowrap;border-radius:999px;padding:0 7px;font-size:12px;font-weight:600}.reference-check[data-check=verified],.reference-check[data-check=confirmed]{color:var(--accept);background:var(--ok-soft);border-color:#0000}.reference-check[data-check=mismatch]{color:var(--reject);background:var(--bad-soft);border-color:#0000}.reference-check[data-check=unverified],.reference-check[data-check=failed],.reference-check[data-check=skipped],.reference-check[data-check=off],.reference-check[data-check=checking]{color:var(--warn);background:var(--warn-soft);border-color:#0000}.reference-lines{color:var(--muted);margin-left:auto;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.reference-problem{background:var(--bad-soft);color:var(--reject);border-radius:6px;margin:4px 0 8px;padding:6px 10px;font-size:13px}.reference-uncited{border-top:1px solid var(--line);border-radius:0;margin:0}.reference-fields{border-collapse:collapse;width:100%;font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.reference-fields th{text-align:left;vertical-align:top;width:1%;color:var(--muted);white-space:nowrap;padding:2px 12px 2px 0;font-weight:400}.reference-fields td{overflow-wrap:anywhere;padding:2px 0}.reference-entry[data-status=modified] tr:not([data-change]) td{color:var(--muted)}.reference-entry[data-status=removed] td{text-decoration:line-through;-webkit-text-decoration-color:var(--reject);text-decoration-color:var(--reject);color:var(--muted)}.reference-resolved{color:var(--muted)}.reference-cited{color:var(--muted);margin:6px 0 0;font-size:12px}.reference-verdict{background:var(--head);border-radius:6px;margin-top:8px;padding:8px 10px;font-size:13px}.reference-verdict p{margin:0}.reference-verdict[data-check=mismatch]{background:var(--bad-soft)}.reference-verdict[data-check=unverified],.reference-verdict[data-check=failed],.reference-verdict[data-check=off],.reference-verdict[data-check=skipped]{background:var(--warn-soft)}.reference-differences{grid-template-columns:auto 1fr;gap:6px 12px;margin:8px 0 0;display:grid}.reference-differences dt{white-space:nowrap;font-weight:600}.reference-differences dd{overflow-wrap:anywhere;gap:2px;min-width:0;margin:0;display:grid}.reference-side{letter-spacing:.02em;min-width:5.5em;color:var(--muted);margin-right:6px;font-size:11px;display:inline-block}.reference-record{overflow-wrap:anywhere;margin:8px 0 0!important}.reference-record a{color:var(--accent)}.reference-confirmation{margin:8px 0 0!important}.reference-confirm{border:1px solid var(--line);background:var(--paper);border-radius:6px;padding:3px 10px;font-size:12.5px}.reference-entry[data-check=confirmed] .reference-verdict{background:var(--head)}.references-banner summary strong{font-weight:600}.references-flagged{margin:6px 0 2px;padding-left:18px}.references-flagged li+li{margin-top:3px}.references-confirm ul{margin:0 0 12px;padding-left:18px;font-size:13px}.references-confirm li+li{margin-top:4px}.references-confirm code{overflow-wrap:anywhere;font-size:12.5px}.references-confirm-actions{flex-wrap:wrap;justify-content:flex-end;gap:8px;display:flex}.references-confirm-actions .accept{color:var(--accept);font-weight:600}@media (width<=560px){.reference-fields,.reference-fields tbody,.reference-fields tr,.reference-fields th,.reference-fields td{width:auto;display:block}.reference-fields tr+tr{margin-top:4px}.reference-fields th{padding:0;font-size:11px}.reference-differences{grid-template-columns:1fr;gap:2px}.reference-differences dd+dt{margin-top:6px}.reference-lines{margin-left:0}}.banner{background:color-mix(in srgb, var(--accent) 6%, var(--paper));border:1px solid var(--line);border-radius:8px;max-width:980px;margin:0 auto 14px;padding:10px 14px;font-size:13px}.banner p{margin:3px 0}.banner .details{background:var(--bar);white-space:pre-wrap;border-radius:5px;max-height:220px;margin:6px 0 2px;padding:8px 10px;font:11.5px/1.5 ui-monospace,Menlo,monospace;overflow:auto}.banner summary{cursor:pointer;color:var(--muted)}.waiting{text-align:center;padding:90px 16px}.waiting p{margin:6px 0}.pulse{background:var(--accent);border-radius:50%;width:12px;height:12px;margin:0 auto 18px;animation:1.6s ease-in-out infinite pulse}@keyframes pulse{0%,to{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}.task-list{background:var(--bar);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:8px;margin:0;padding:0;list-style:none}.task-list li+li{border-top:1px solid var(--line)}.index .document{max-width:880px}.overview-heading{margin:32px 0}.overview-heading h1{letter-spacing:.02em;margin:0 0 6px;font-size:34px;font-variant-caps:small-caps}.overview-heading p{margin:6px 0}.overview-summary{color:var(--muted);font-size:13px}.task-tools{gap:10px;margin-bottom:16px;display:flex}.task-tools input,.task-tools select{font:inherit;color:var(--ink);background:var(--bar);border:1px solid var(--line);border-radius:7px;padding:9px 12px}.task-tools input{flex:1;min-width:0}.task-tools input::placeholder{color:var(--muted)}.task-card{color:var(--ink);border-radius:7px;padding:24px 20px;text-decoration:none;display:block}.task-card:hover{background:var(--head)}.task-card:focus-visible,.overview-link:focus-visible,.task-tools :focus-visible{outline:2px solid var(--accent);outline-offset:2px}.task-heading{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.task-heading h2{overflow-wrap:anywhere;margin:0;font-size:21px;font-weight:400}.task-meta{color:var(--muted);flex-wrap:wrap;gap:4px 14px;margin-top:5px;font-size:12px;display:flex}.task-name{overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.task-progress{color:var(--muted);align-items:center;gap:10px;margin-top:14px;font-size:12px;display:flex}.task-progress .track{width:140px}.task-footer{justify-content:space-between;align-items:baseline;gap:10px;margin-top:9px;font-size:12px;display:flex}.task-counts{color:var(--muted);flex-wrap:wrap;gap:4px 14px;display:flex}.task-counts [data-kind=pending]{color:var(--accent);font-weight:600}.task-counts [data-kind=commented]{color:var(--comment)}.task-open{color:var(--accent);flex:none;font-weight:600}.actions{z-index:4;background:var(--bar);border:1px solid var(--line);max-width:calc(100vw - 24px);box-shadow:var(--shadow);border-radius:12px;align-items:center;gap:2px;padding:5px;display:flex;position:fixed;bottom:22px;left:50%;transform:translate(-50%)}.actions button{white-space:nowrap;background:0 0;border:0;border-radius:8px;padding:9px 14px;font-size:14px}.actions button:not(:disabled):hover{background:var(--head)}.actions .nav{color:var(--muted);padding:6px 12px;font-size:20px;line-height:1}.actions .accept{color:var(--accept)}.actions .reject{color:var(--reject)}.actions .comment{color:var(--comment)}.actions .unstage{color:var(--muted)}.actions .skip{font-weight:600}.actions .undo{color:var(--muted);border-left:1px solid var(--line);border-radius:0 8px 8px 0;margin-left:4px;font-size:13px}.actions .working{opacity:.55}.position{text-align:center;min-width:52px;color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.complete{color:var(--muted);white-space:nowrap;padding:0 14px;font-size:13px}.change.current>.change-head{top:calc(-1 * var(--paper-top));z-index:4;position:sticky}.change-head .dock{flex:none;align-items:center;margin:0;display:flex}.change-head .actions{background:var(--bar);border:1px solid var(--line);border-radius:10px;gap:0;max-width:none;padding:3px;position:static;transform:none;box-shadow:0 1px 2px #00000014,0 6px 20px #0000001f}.change-head .actions button{padding:4px 8px;font-size:13px;line-height:1.5}.change-head .actions .nav{padding:2px 8px;font-size:17px}.change-head .actions .undo{margin-left:3px;padding:4px 10px;font-size:12px}.change-head .position{min-width:40px;font-size:11.5px}.change-head .complete{padding:0 8px;font-size:12px}.change-head .composer{width:min(380px,100vw - 24px);position:absolute;inset:calc(100% + 6px) 8px auto auto;transform:none}.composer{z-index:5;background:var(--bar);border:1px solid var(--line);border-radius:12px;width:min(540px,100vw - 24px);padding:12px;position:fixed;bottom:84px;left:50%;transform:translate(-50%);box-shadow:0 6px 24px #00000014}.composer label{color:var(--ink);margin-bottom:6px;font-size:17px;font-weight:400;display:block}.composer textarea{resize:vertical;border:1px solid var(--line);background:var(--paper);color:#1f2328;border-radius:7px;width:100%;min-height:84px;padding:8px 10px}.composer textarea:focus{outline:2px solid color-mix(in srgb, var(--comment) 45%, transparent);border-color:#0000}.composer-row{justify-content:space-between;align-items:center;margin-top:8px;font-size:12px;display:flex}.composer-row kbd{margin-left:0}.composer button{border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:7px;padding:6px 16px}.toast{z-index:6;border:1px solid var(--line);background:var(--paper);max-width:min(640px,100vw - 24px);color:var(--ink);white-space:pre-wrap;padding:9px 14px;font-size:13px;position:fixed;top:74px;left:50%;transform:translate(-50%);box-shadow:0 3px 12px #0000000f}.toast.error{background:#7f2b31}dialog{border:1px solid var(--line);box-shadow:var(--shadow);background:var(--bar);color:var(--ink);border-radius:12px;max-width:min(460px,100vw - 32px);padding:20px 24px}dialog::backdrop{background:#00000040}dialog h2{margin:0 0 12px;font-size:23px}dialog dl{grid-template-columns:auto 1fr;gap:8px 16px;margin:0 0 12px;font-size:13px;display:grid}dialog dt{white-space:nowrap}dialog dt kbd{margin:0 2px 0 0}dialog dd{margin:0}dialog form{text-align:right}dialog button{border:1px solid var(--line);background:var(--frame);border-radius:7px;padding:5px 14px}@media (width<=720px){.bar{grid-template-columns:auto 1fr auto;gap:8px;padding:12px 16px;display:grid}.identity{grid-area:1/1/auto/-1}.bar .meter{justify-self:center;font-size:11px}.bar-end{gap:2px}.bar .ghost{padding:4px 2px;font-size:12px}.side{min-height:20px}.track{width:70px}.actions button{padding:9px}.change-head{flex-wrap:wrap}.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}.change-head .composer{width:calc(100vw - 24px);right:0}:root{--paper-top:10px}#paper{padding:var(--paper-top) 6px 90px}#paper.index{padding:10px 14px 40px}.overview-heading{margin-top:16px}.task-tools{flex-wrap:wrap}.task-tools input{flex-basis:100%}.task-tools select{width:100%}.task-card{padding:14px}.task-heading{flex-direction:column;gap:6px}}@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}body{display:block}#root{grid-template-rows:auto 1fr;grid-template-columns:minmax(0,1fr);height:100dvh;display:grid;overflow:hidden}.review-body{min-height:0;position:relative}.review-body>#paper{scrollbar-width:none;height:100%;padding-right:54px}.review-body>#paper::-webkit-scrollbar{display:none}.change-map{z-index:5;border:1px solid var(--line);touch-action:none;-webkit-user-select:none;user-select:none;background:#fafafa;border-radius:7px;width:22px;position:absolute;top:12px;bottom:12px;right:18px;box-shadow:0 1px 4px #0000000d}.change-map-viewport{cursor:grab;background:#6a6f741c;border:1px solid #c6c9cc;border-radius:5px;position:absolute;left:0;right:0}.change-map-viewport:active{cursor:grabbing}.change-map-mark{--mark:#b18a3c;background:var(--mark);border:0;border-radius:2px;width:calc(100% - 10px);min-height:0;padding:0;position:absolute;left:5px}.change-map-mark[data-state=accepted]{--mark:var(--accept)}.change-map-mark[data-state=rejected]{--mark:var(--reject)}.change-map-mark[data-state=commented]{--mark:var(--comment)}.change-map-mark[data-state=mixed]{--mark:#796195}.change-map-mark[aria-current]{outline:2px solid var(--ink);outline-offset:2px}.change-map-mark:hover,.change-map-mark:focus-visible{z-index:1;outline:2px solid var(--accent);outline-offset:2px}.change-map-tooltip{border:1px solid var(--line);background:var(--paper);width:max-content;max-width:min(360px,100vw - 64px);box-shadow:var(--shadow);color:var(--ink);font:12px/1.5 var(--sans);text-align:left;overflow-wrap:anywhere;pointer-events:none;visibility:hidden;border-radius:7px;padding:7px 10px;position:absolute;top:0;right:calc(100% + 12px)}.change-map-mark:hover .change-map-tooltip,.change-map-mark:focus-visible .change-map-tooltip{visibility:visible}@media (width<=720px){.review-body>#paper{padding-right:46px}.change-map{width:18px;right:16px}.change-map-mark{width:calc(100% - 6px);left:3px}.change-head .actions{flex-wrap:wrap;justify-content:center;max-width:100%}.change-head .actions button{padding:4px 6px;font-size:12px}.change-head .actions .nav{padding:2px 6px}.change-head .position{min-width:32px}}.pdf-strip{isolation:isolate;width:100%;position:relative;container-type:inline-size}.pdf-strip img{-webkit-user-select:none;user-select:none;pointer-events:none;position:absolute;inset:0}.pdf-text{color:#0000;-webkit-user-select:text;user-select:text;cursor:text;line-height:1;position:absolute;inset:0;overflow:hidden}.pdf-text-line{white-space:pre}.pdf-glyph{white-space:pre;transform-origin:0 0;color:#0000;font-family:Times New Roman,serif;font-weight:400;line-height:1;position:absolute}.pdf-glyph::selection{color:#0000;background:#437ee759}.copy-reference{white-space:nowrap}.change-head{flex-wrap:wrap}.change-head .change-where{min-width:160px}.document .source-unit button{line-height:1.5}.help-backdrop{z-index:8;background:#00000040;place-items:center;display:grid;position:fixed;inset:0}.help-backdrop dialog{margin:0;position:static}.composer{line-height:1.5}@media (width<=900px){.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}}#root:has(>.review-mode){grid-template-rows:auto auto minmax(0,1fr)}.reviewed-change,.change.reviewed-change.current{box-shadow:none;background:0 0;border:0}.reviewed-change>.change-head,.reviewed-change>.histories,.reviewed-change>.remark,.reviewed-change>.source,.reviewed-change>.caption,.mixed-reviewed>.side{display:none}.reviewed-placeholder{color:var(--muted);font:13px/1.5 var(--sans);text-align:center;margin:0;padding:10px 14px}.review-mode{border-bottom:1px solid var(--line);background:var(--paper);font:13px/1.4 var(--sans);flex-wrap:wrap;align-items:center;gap:8px;padding:6px 14px;display:flex}.review-mode>span,.review-mode>label{color:var(--muted);margin-right:auto}.review-mode label{align-items:center;gap:8px;display:flex}.review-mode select{border:1px solid var(--line);background:var(--paper);max-width:160px;font:inherit;color:var(--ink);border-radius:6px;padding:4px 8px}.review-mode .ghost{white-space:nowrap;padding:4px 8px;font-size:13px}.review-mode .undo{border:1px solid var(--line);border-radius:6px}.review-complete{max-width:var(--page-width);border:1px solid var(--line);background:var(--paper);font:14px/1.5 var(--sans);border-radius:10px;flex-wrap:wrap;align-items:baseline;gap:2px 8px;margin:0 auto 14px;padding:9px 14px;display:flex}.review-complete strong{font-weight:600}.review-complete span{color:var(--muted)}.review-complete-actions{align-items:center;gap:8px;margin-left:auto;display:flex}.review-complete-actions:empty{display:none}@media (width<=680px){.review-mode{padding:5px 8px}.review-mode .ghost{min-height:36px}.review-mode label{font-size:12px}.review-mode select{max-width:135px}}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
rel="icon"
|
|
10
10
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%232b6e5c'/%3E%3Cpath d='M4 8.5l2.5 2.5L12 5' stroke='white' stroke-width='2' fill='none'/%3E%3C/svg%3E"
|
|
11
11
|
/>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-DVs7cTBl.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ysOpu05D.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
6
|
import { Repo } from "./git.ts";
|
|
6
7
|
import { listFiles, readJson, writeJson, withLock, now } from "./files.ts";
|
|
7
8
|
import { stripComments } from "./outline.ts";
|
|
@@ -28,6 +29,27 @@ const AUXILIARY = new Set([
|
|
|
28
29
|
".out",
|
|
29
30
|
".thm",
|
|
30
31
|
]);
|
|
32
|
+
// Output that TeX writes for this folder: SyncTeX data and the log name its
|
|
33
|
+
// absolute path.
|
|
34
|
+
const OUTPUT = /\.(?:pdf|xdv|dvi|log|synctex(?:\.gz)?)$/i;
|
|
35
|
+
function clone(file: string, target: string) {
|
|
36
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
37
|
+
fs.copyFileSync(file, target, fs.constants.COPYFILE_FICLONE);
|
|
38
|
+
}
|
|
39
|
+
// Output that latexmk kept from an earlier build folder still names that
|
|
40
|
+
// folder. Build folders differ only in their equal-length key, so the paths
|
|
41
|
+
// are replaced in place without moving anything else in the file.
|
|
42
|
+
export function relocate(file: string, from: string, to: string) {
|
|
43
|
+
if (!fs.existsSync(file)) return;
|
|
44
|
+
const old = Buffer.from(from + path.sep), next = Buffer.from(to + path.sep);
|
|
45
|
+
if (old.length !== next.length) return;
|
|
46
|
+
const zipped = file.endsWith(".gz"),
|
|
47
|
+
data = zipped ? gunzipSync(fs.readFileSync(file)) : fs.readFileSync(file);
|
|
48
|
+
let at = data.indexOf(old);
|
|
49
|
+
if (at < 0) return;
|
|
50
|
+
for (; at >= 0; at = data.indexOf(old, at + next.length)) next.copy(data, at);
|
|
51
|
+
fs.writeFileSync(file, zipped ? gzipSync(data) : data);
|
|
52
|
+
}
|
|
31
53
|
const hash = (value: string | Buffer) =>
|
|
32
54
|
createHash("sha256").update(value).digest("hex");
|
|
33
55
|
export const includes = (text: string) =>
|
|
@@ -215,6 +237,7 @@ export class Builder {
|
|
|
215
237
|
}
|
|
216
238
|
const tree = this.repo.treeId(rev, p.source_dir),
|
|
217
239
|
date = buildDate(),
|
|
240
|
+
context = hash(JSON.stringify([signature(p), version])),
|
|
218
241
|
key = hash(
|
|
219
242
|
JSON.stringify([
|
|
220
243
|
tree,
|
|
@@ -239,109 +262,169 @@ export class Builder {
|
|
|
239
262
|
fs.utimesSync(folder, new Date(), new Date());
|
|
240
263
|
return cached;
|
|
241
264
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
main = path.posix.basename(p.main),
|
|
250
|
-
job = path.posix.basename(main, ".tex"),
|
|
251
|
-
out = path.join(cwd, "build");
|
|
252
|
-
fs.mkdirSync(out, { recursive: true });
|
|
253
|
-
for (const target of includes(
|
|
254
|
-
fs.readFileSync(path.join(cwd, main), "utf8"),
|
|
255
|
-
))
|
|
256
|
-
fs.mkdirSync(path.join(out, path.dirname(target)), { recursive: true });
|
|
257
|
-
const args = [
|
|
258
|
-
"-norc",
|
|
259
|
-
...(p.latexmkrc
|
|
260
|
-
? ["-r", path.posix.relative(p.doc_dir || ".", p.latexmkrc)]
|
|
261
|
-
: []),
|
|
262
|
-
{ pdflatex: "-pdf", xelatex: "-xelatex", lualatex: "-lualatex" }[
|
|
263
|
-
p.engine
|
|
264
|
-
],
|
|
265
|
-
"-outdir=build",
|
|
266
|
-
`-jobname=${job}`,
|
|
267
|
-
"-synctex=1",
|
|
268
|
-
"-interaction=nonstopmode",
|
|
269
|
-
"-halt-on-error",
|
|
270
|
-
"-file-line-error",
|
|
271
|
-
];
|
|
272
|
-
if (partial) {
|
|
273
|
-
const seedOut = path.dirname(seed!.pdf);
|
|
274
|
-
for (const file of listFiles(seedOut))
|
|
275
|
-
if (AUXILIARY.has(path.extname(file))) {
|
|
276
|
-
const target = path.join(out, path.relative(seedOut, file));
|
|
277
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
278
|
-
if (!fs.existsSync(target)) fs.copyFileSync(file, target);
|
|
279
|
-
}
|
|
280
|
-
const preload = Object.entries(p.preload)
|
|
281
|
-
.filter(
|
|
282
|
-
([name, file]) =>
|
|
283
|
-
!targets.includes(name) && fs.existsSync(path.join(cwd, file)),
|
|
284
|
-
)
|
|
285
|
-
.map(([, file]) => `\\input{${file}}`)
|
|
286
|
-
.join("");
|
|
287
|
-
args.push(
|
|
288
|
-
`-usepretex=\\includeonly{${targets.join(",")}}` +
|
|
289
|
-
(preload ? `\\AtBeginDocument{${preload}}` : ""),
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
args.push(main);
|
|
293
|
-
const transcript = path.join(folder, "latexmk.log"),
|
|
294
|
-
fd = fs.openSync(transcript, "w"),
|
|
295
|
-
started = now();
|
|
296
|
-
let failure: Error | null = null;
|
|
265
|
+
// A full build continues from the latest completed one, or from the
|
|
266
|
+
// author's own latexmk output, as latexmk does when a folder is
|
|
267
|
+
// compiled again: unchanged files are cloned and LaTeX keeps its state,
|
|
268
|
+
// so it reruns only as often as the edit requires. Manual recompilation
|
|
269
|
+
// starts clean, and so does a retry when leftover state broke a build.
|
|
270
|
+
const start = partial || options.force ? null : this.latest(context) ?? "author";
|
|
271
|
+
let result: BuildInfo;
|
|
297
272
|
try {
|
|
298
|
-
await
|
|
299
|
-
cwd,
|
|
300
|
-
env: { ...process.env, ...date },
|
|
301
|
-
timeout: 1800000,
|
|
302
|
-
output: (chunk) => fs.writeSync(fd, chunk),
|
|
303
|
-
});
|
|
273
|
+
result = await this.compile(rev, tree, key, targets, seed, date, start);
|
|
304
274
|
} catch (error) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
fs.closeSync(fd);
|
|
275
|
+
if (!start || (error instanceof BuildError && !error.details.some(line => /(?:^|\s|\()(?:\.\/)?build\//.test(line)))) throw error;
|
|
276
|
+
result = await this.compile(rev, tree, key, targets, seed, date, null);
|
|
308
277
|
}
|
|
309
|
-
|
|
310
|
-
log = path.join(out, job + ".log"),
|
|
311
|
-
logText = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : "";
|
|
312
|
-
if (failure || !fs.existsSync(pdf)) {
|
|
313
|
-
const transcriptText = fs.readFileSync(transcript, "utf8"),
|
|
314
|
-
details = compilerDiagnostics(logText, transcriptText),
|
|
315
|
-
kept = path.join(p.state, "logs", key + ".log");
|
|
316
|
-
fs.mkdirSync(path.dirname(kept), { recursive: true });
|
|
317
|
-
fs.writeFileSync(
|
|
318
|
-
kept,
|
|
319
|
-
transcriptText + (logText ? "\n\n--- TeX log ---\n" + logText : ""),
|
|
320
|
-
);
|
|
321
|
-
const tail = transcriptText.split("\n").slice(-25);
|
|
322
|
-
fs.rmSync(folder, { recursive: true, force: true });
|
|
323
|
-
throw new BuildError(
|
|
324
|
-
failure?.message.includes("ENOENT")
|
|
325
|
-
? "latexmk was not found. Install TeX Live or MacTeX."
|
|
326
|
-
: "LaTeX failed to compile.",
|
|
327
|
-
details.length ? details : tail,
|
|
328
|
-
kept,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
const result: BuildInfo = {
|
|
332
|
-
key,
|
|
333
|
-
tree,
|
|
334
|
-
targets,
|
|
335
|
-
root: source,
|
|
336
|
-
cwd,
|
|
337
|
-
pdf,
|
|
338
|
-
seconds: Math.round((now() - started) * 10) / 10,
|
|
339
|
-
warnings: warnings(logText),
|
|
340
|
-
};
|
|
341
|
-
writeJson(path.join(folder, "result.json"), result);
|
|
278
|
+
if (!partial) writeJson(path.join(this.cache, "latest.json"), { key, context, date: date.SOURCE_DATE_EPOCH });
|
|
342
279
|
return result;
|
|
343
280
|
});
|
|
344
281
|
}
|
|
282
|
+
private latest(context: string): { info: BuildInfo; date: unknown } | null {
|
|
283
|
+
const latest = readJson<{ key?: unknown; context?: unknown; date?: unknown } | null>(path.join(this.cache, "latest.json"), null);
|
|
284
|
+
if (typeof latest?.key !== "string" || !/^[0-9a-f]{32}$/.test(latest.key) || latest.context !== context) return null;
|
|
285
|
+
const info = readJson<BuildInfo | null>(path.join(this.cache, latest.key, "result.json"), null);
|
|
286
|
+
return info && JSON.stringify(info.targets) === '["full"]' && fs.existsSync(info.pdf) && fs.existsSync(info.root) ? { info, date: latest.date } : null;
|
|
287
|
+
}
|
|
288
|
+
private async compile(
|
|
289
|
+
rev: string,
|
|
290
|
+
tree: string,
|
|
291
|
+
key: string,
|
|
292
|
+
targets: string[],
|
|
293
|
+
seed: BuildInfo | null,
|
|
294
|
+
date: ReturnType<typeof buildDate>,
|
|
295
|
+
start: { info: BuildInfo; date: unknown } | "author" | null,
|
|
296
|
+
): Promise<BuildInfo> {
|
|
297
|
+
const p = this.project,
|
|
298
|
+
partial = JSON.stringify(targets) !== '["full"]',
|
|
299
|
+
folder = path.join(this.cache, key);
|
|
300
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
301
|
+
const source = path.join(folder, "src");
|
|
302
|
+
const base = start && start !== "author" ? start.info : null;
|
|
303
|
+
await this.repo.export(rev, p.source_dir, source, base ? { root: base.root, tree: base.tree } : undefined);
|
|
304
|
+
const cwd = path.join(
|
|
305
|
+
source,
|
|
306
|
+
path.posix.relative(p.source_dir || ".", p.doc_dir || "."),
|
|
307
|
+
),
|
|
308
|
+
main = path.posix.basename(p.main),
|
|
309
|
+
job = path.posix.basename(main, ".tex"),
|
|
310
|
+
out = path.join(cwd, "build");
|
|
311
|
+
fs.mkdirSync(out, { recursive: true });
|
|
312
|
+
const included = includes(fs.readFileSync(path.join(cwd, main), "utf8"));
|
|
313
|
+
for (const target of included)
|
|
314
|
+
fs.mkdirSync(path.join(out, path.dirname(target)), { recursive: true });
|
|
315
|
+
// The PDF, SyncTeX data and log carry over only within a day, so that
|
|
316
|
+
// \today stays current; latexmk then skips TeX when no input changed.
|
|
317
|
+
const reused = base !== null && start !== "author" && start?.date === date.SOURCE_DATE_EPOCH;
|
|
318
|
+
if (base) {
|
|
319
|
+
const previous = path.dirname(base.pdf);
|
|
320
|
+
for (const file of listFiles(previous))
|
|
321
|
+
if (!/\.fls$/i.test(file) && (reused || !OUTPUT.test(file)))
|
|
322
|
+
clone(file, path.join(out, path.relative(previous, file)));
|
|
323
|
+
} else if (start === "author") {
|
|
324
|
+
// Only this job's state: the author's folder may hold other editions.
|
|
325
|
+
const own = path.join(p.root, p.doc_dir, "build");
|
|
326
|
+
if (fs.existsSync(path.join(own, job + ".fdb_latexmk")))
|
|
327
|
+
for (const name of [
|
|
328
|
+
...fs.readdirSync(own).filter((name) => (name.startsWith(job + ".") && !OUTPUT.test(name) && !name.endsWith(".fls")) || name.startsWith("_minted")),
|
|
329
|
+
...included.map((target) => target + ".aux"),
|
|
330
|
+
])
|
|
331
|
+
try {
|
|
332
|
+
if (fs.statSync(path.join(own, name)).isDirectory()) fs.cpSync(path.join(own, name), path.join(out, name), { recursive: true });
|
|
333
|
+
else clone(path.join(own, name), path.join(out, name));
|
|
334
|
+
} catch {
|
|
335
|
+
/* A missing file is regenerated. */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const args = [
|
|
339
|
+
"-norc",
|
|
340
|
+
...(p.latexmkrc
|
|
341
|
+
? ["-r", path.posix.relative(p.doc_dir || ".", p.latexmkrc)]
|
|
342
|
+
: []),
|
|
343
|
+
{ pdflatex: "-pdf", xelatex: "-xelatex", lualatex: "-lualatex" }[
|
|
344
|
+
p.engine
|
|
345
|
+
],
|
|
346
|
+
"-outdir=build",
|
|
347
|
+
`-jobname=${job}`,
|
|
348
|
+
"-synctex=1",
|
|
349
|
+
"-interaction=nonstopmode",
|
|
350
|
+
"-halt-on-error",
|
|
351
|
+
"-file-line-error",
|
|
352
|
+
];
|
|
353
|
+
if (partial) {
|
|
354
|
+
const seedOut = path.dirname(seed!.pdf);
|
|
355
|
+
for (const file of listFiles(seedOut))
|
|
356
|
+
if (AUXILIARY.has(path.extname(file))) {
|
|
357
|
+
const target = path.join(out, path.relative(seedOut, file));
|
|
358
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
359
|
+
if (!fs.existsSync(target)) fs.copyFileSync(file, target);
|
|
360
|
+
}
|
|
361
|
+
const preload = Object.entries(p.preload)
|
|
362
|
+
.filter(
|
|
363
|
+
([name, file]) =>
|
|
364
|
+
!targets.includes(name) && fs.existsSync(path.join(cwd, file)),
|
|
365
|
+
)
|
|
366
|
+
.map(([, file]) => `\\input{${file}}`)
|
|
367
|
+
.join("");
|
|
368
|
+
args.push(
|
|
369
|
+
`-usepretex=\\includeonly{${targets.join(",")}}` +
|
|
370
|
+
(preload ? `\\AtBeginDocument{${preload}}` : ""),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
args.push(main);
|
|
374
|
+
const transcript = path.join(folder, "latexmk.log"),
|
|
375
|
+
fd = fs.openSync(transcript, "w"),
|
|
376
|
+
started = now();
|
|
377
|
+
let failure: Error | null = null;
|
|
378
|
+
try {
|
|
379
|
+
await runAsync("latexmk", args, {
|
|
380
|
+
cwd,
|
|
381
|
+
env: { ...process.env, ...date },
|
|
382
|
+
timeout: 1800000,
|
|
383
|
+
output: (chunk) => fs.writeSync(fd, chunk),
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
failure = error as Error;
|
|
387
|
+
} finally {
|
|
388
|
+
fs.closeSync(fd);
|
|
389
|
+
}
|
|
390
|
+
const pdf = path.join(out, job + ".pdf"),
|
|
391
|
+
log = path.join(out, job + ".log");
|
|
392
|
+
if (reused && !failure)
|
|
393
|
+
for (const file of [path.join(out, job + ".synctex.gz"), log])
|
|
394
|
+
relocate(file, path.dirname(base!.root), folder);
|
|
395
|
+
const logText = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : "";
|
|
396
|
+
if (failure || !fs.existsSync(pdf)) {
|
|
397
|
+
const transcriptText = fs.readFileSync(transcript, "utf8"),
|
|
398
|
+
details = compilerDiagnostics(logText, transcriptText),
|
|
399
|
+
kept = path.join(p.state, "logs", key + ".log");
|
|
400
|
+
fs.mkdirSync(path.dirname(kept), { recursive: true });
|
|
401
|
+
fs.writeFileSync(
|
|
402
|
+
kept,
|
|
403
|
+
transcriptText + (logText ? "\n\n--- TeX log ---\n" + logText : ""),
|
|
404
|
+
);
|
|
405
|
+
const tail = transcriptText.split("\n").slice(-25);
|
|
406
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
407
|
+
throw new BuildError(
|
|
408
|
+
failure?.message.includes("ENOENT")
|
|
409
|
+
? "latexmk was not found. Install TeX Live or MacTeX."
|
|
410
|
+
: "LaTeX failed to compile.",
|
|
411
|
+
details.length ? details : tail,
|
|
412
|
+
kept,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const result: BuildInfo = {
|
|
416
|
+
key,
|
|
417
|
+
tree,
|
|
418
|
+
targets,
|
|
419
|
+
root: source,
|
|
420
|
+
cwd,
|
|
421
|
+
pdf,
|
|
422
|
+
seconds: Math.round((now() - started) * 10) / 10,
|
|
423
|
+
warnings: warnings(logText),
|
|
424
|
+
};
|
|
425
|
+
writeJson(path.join(folder, "result.json"), result);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
345
428
|
async prune(keep: Set<string>, maxAge = 3 * 86400) {
|
|
346
429
|
if (!fs.existsSync(this.cache)) return;
|
|
347
430
|
for (const entry of fs.readdirSync(this.cache, { withFileTypes: true })) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import { run, which } from "./process.ts";
|
|
5
6
|
import { same } from "./files.ts";
|
|
6
7
|
const IDENTITY = {
|
|
@@ -128,6 +129,14 @@ export class Repo {
|
|
|
128
129
|
.split("\0")
|
|
129
130
|
.filter(Boolean);
|
|
130
131
|
}
|
|
132
|
+
entries(rev: string, folder = "") {
|
|
133
|
+
return this.run([
|
|
134
|
+
"ls-tree", "-r", "-l", "-z", rev, ...(folder ? ["--", folder] : []),
|
|
135
|
+
]).toString().split("\0").filter(Boolean).flatMap(entry => {
|
|
136
|
+
const tab = entry.indexOf("\t"), [mode, type, oid, size] = entry.slice(0, tab).split(/ +/);
|
|
137
|
+
return type === "blob" ? [{ mode, oid, size: Number(size), file: entry.slice(tab + 1) }] : [];
|
|
138
|
+
});
|
|
139
|
+
}
|
|
131
140
|
changedFiles(before: string, after: string) {
|
|
132
141
|
return this.run([
|
|
133
142
|
"diff",
|
|
@@ -261,36 +270,132 @@ export class Repo {
|
|
|
261
270
|
return this.text(["write-tree"], { env });
|
|
262
271
|
});
|
|
263
272
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
// Write a revision's files under destination. Given an earlier export of
|
|
274
|
+
// another tree, files with the same Git object are cloned from it
|
|
275
|
+
// (copy-on-write where the file system supports it), so only changed files
|
|
276
|
+
// are read from Git.
|
|
277
|
+
async export(rev: string, folder: string, destination: string, base?: { root: string; tree: string }) {
|
|
278
|
+
const previous = new Map<string, string>();
|
|
279
|
+
if (base) for (const entry of this.entries(base.tree)) previous.set(entry.file, entry.mode + " " + entry.oid);
|
|
280
|
+
const wanted = this.entries(rev, folder).map(entry => {
|
|
281
|
+
const relative = path.posix.relative(folder || ".", entry.file);
|
|
271
282
|
cleanPath(relative);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
return { ...entry, relative, target: path.join(destination, relative) };
|
|
284
|
+
});
|
|
285
|
+
// On APFS, cp clones a whole folder far faster than creating each file
|
|
286
|
+
// separately; files that are not in this tree are then removed again.
|
|
287
|
+
let cloned = false;
|
|
288
|
+
if (base && process.platform === "darwin" && !fs.existsSync(destination))
|
|
289
|
+
try {
|
|
290
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
291
|
+
cloned = run("/bin/cp", ["-c", "-R", base.root, destination], { check: false }).status === 0;
|
|
292
|
+
}
|
|
293
|
+
catch { /* Copy file by file below. */ }
|
|
294
|
+
if (cloned) {
|
|
295
|
+
const keep = new Set(wanted.map(entry => entry.relative));
|
|
296
|
+
(function prune(directory: string, prefix: string) {
|
|
297
|
+
for (const item of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
298
|
+
const relative = prefix + item.name, file = path.join(directory, item.name);
|
|
299
|
+
if (item.isDirectory()) prune(file, relative + "/");
|
|
300
|
+
else if (!keep.has(relative)) fs.rmSync(file, { force: true });
|
|
301
|
+
}
|
|
302
|
+
})(destination, "");
|
|
303
|
+
} else {
|
|
304
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
305
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
const folders = new Set<string>(), entries: typeof wanted = [];
|
|
308
|
+
for (const entry of wanted) {
|
|
309
|
+
const { relative, target } = entry;
|
|
310
|
+
if (!folders.has(path.dirname(target))) { fs.mkdirSync(path.dirname(target), { recursive: true }); folders.add(path.dirname(target)); }
|
|
311
|
+
if (entry.mode !== "120000" && previous.get(relative) === entry.mode + " " + entry.oid) {
|
|
312
|
+
const source = cloned ? target : path.join(base!.root, relative);
|
|
313
|
+
try {
|
|
314
|
+
// A compiler may have rewritten a file in the earlier export.
|
|
315
|
+
const stat = fs.lstatSync(source);
|
|
316
|
+
if (stat.isFile() && stat.size === entry.size) {
|
|
317
|
+
if (!cloned) fs.copyFileSync(source, target, fs.constants.COPYFILE_FICLONE);
|
|
318
|
+
if ((stat.mode & 0o777) !== (entry.mode === "100755" ? 0o755 : 0o644)) fs.chmodSync(target, entry.mode === "100755" ? 0o755 : 0o644);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
} catch { /* Read this file from Git. */ }
|
|
322
|
+
}
|
|
323
|
+
// A cloned link or rewritten file is replaced, never written through.
|
|
324
|
+
if (cloned) fs.rmSync(target, { force: true });
|
|
325
|
+
entries.push(entry);
|
|
326
|
+
}
|
|
327
|
+
if (!entries.length) return;
|
|
328
|
+
// Keep one Git process and stream each blob to disk. Collecting the entire
|
|
329
|
+
// snapshot with blobs() both duplicates memory and hits spawnSync's buffer
|
|
330
|
+
// limit for ordinary repositories containing PDFs and figures.
|
|
331
|
+
const child = spawn(gitBinary(), ["cat-file", "--batch"], {
|
|
332
|
+
cwd: this.root, stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
});
|
|
334
|
+
let stderr = "", timedOut = false;
|
|
335
|
+
child.stderr.on("data", (data: Buffer) => { stderr = (stderr + data.toString()).slice(-65536); });
|
|
336
|
+
child.stdin.on("error", () => { /* Report process failures through finished. */ });
|
|
337
|
+
const finished = new Promise<Error | null>(resolve => {
|
|
338
|
+
child.once("error", resolve);
|
|
339
|
+
child.once("close", code => resolve(code === 0 && !timedOut ? null : new GitError(
|
|
340
|
+
timedOut ? "Git snapshot export timed out." : stderr.trim() || `Git snapshot export failed (${code}).`,
|
|
341
|
+
)));
|
|
342
|
+
});
|
|
343
|
+
const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 300000);
|
|
344
|
+
const output = child.stdout[Symbol.asyncIterator]();
|
|
345
|
+
let chunk: Buffer = Buffer.alloc(0), offset = 0;
|
|
346
|
+
const available = async () => {
|
|
347
|
+
if (offset < chunk.length) return;
|
|
348
|
+
const next = await output.next();
|
|
349
|
+
if (next.done) throw await finished || new GitError("Git snapshot export ended before a file was complete.");
|
|
350
|
+
chunk = next.value; offset = 0;
|
|
351
|
+
};
|
|
352
|
+
const header = async () => {
|
|
353
|
+
const parts: Buffer[] = [];
|
|
354
|
+
while (true) {
|
|
355
|
+
await available();
|
|
356
|
+
const end = chunk.indexOf(10, offset);
|
|
357
|
+
parts.push(chunk.subarray(offset, end < 0 ? chunk.length : end));
|
|
358
|
+
offset = end < 0 ? chunk.length : end + 1;
|
|
359
|
+
if (end >= 0) return Buffer.concat(parts).toString();
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const links: [string, string][] = [];
|
|
363
|
+
try {
|
|
364
|
+
for (const { file, mode, oid, target } of entries) {
|
|
365
|
+
child.stdin.write(oid + "\n");
|
|
366
|
+
const [object, type, size] = (await header()).split(" ");
|
|
367
|
+
let remaining = Number(size);
|
|
368
|
+
if (object !== oid || type !== "blob" || !Number.isSafeInteger(remaining) || remaining < 0)
|
|
369
|
+
throw new GitError(`Git snapshot export could not read: ${file}`);
|
|
370
|
+
const linkParts: Buffer[] = [], fd = mode === "120000" ? null : fs.openSync(target, "w", mode === "100755" ? 0o755 : 0o644);
|
|
371
|
+
try {
|
|
372
|
+
while (remaining) {
|
|
373
|
+
await available();
|
|
374
|
+
const length = Math.min(remaining, chunk.length - offset), data = chunk.subarray(offset, offset + length);
|
|
375
|
+
if (fd === null) linkParts.push(Buffer.from(data));
|
|
376
|
+
else fs.writeFileSync(fd, data);
|
|
377
|
+
remaining -= length; offset += length;
|
|
378
|
+
}
|
|
379
|
+
} finally { if (fd !== null) fs.closeSync(fd); }
|
|
380
|
+
await available();
|
|
381
|
+
if (chunk[offset++] !== 10) throw new GitError(`Git snapshot export returned an invalid file boundary: ${file}`);
|
|
382
|
+
if (mode === "120000") {
|
|
383
|
+
const link = Buffer.concat(linkParts).toString(), resolved = path.resolve(path.dirname(target), link);
|
|
384
|
+
if (path.isAbsolute(link) || !resolved.startsWith(path.resolve(destination) + path.sep))
|
|
385
|
+
throw new GitError(`A link in the snapshot points outside the document: ${file}`);
|
|
386
|
+
links.push([target, link]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
child.stdin.end();
|
|
390
|
+
if (!(await output.next()).done) throw new GitError("Git snapshot export returned unexpected output.");
|
|
391
|
+
const failure = await finished;
|
|
392
|
+
if (failure) throw failure;
|
|
393
|
+
for (const [target, link] of links) fs.symlinkSync(link, target);
|
|
394
|
+
} finally {
|
|
395
|
+
clearTimeout(timer);
|
|
396
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
397
|
+
await finished;
|
|
292
398
|
}
|
|
293
|
-
for (const [target, link] of links) fs.symlinkSync(link, target);
|
|
294
399
|
}
|
|
295
400
|
}
|
|
296
401
|
export function merge3(
|
|
@@ -247,15 +247,6 @@ export function buildOutline(
|
|
|
247
247
|
.slice(0, 20),
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
|
-
export function overlaps(a: Ranges, b: Ranges) {
|
|
251
|
-
return Object.keys(a).some(
|
|
252
|
-
(file) =>
|
|
253
|
-
b[file] &&
|
|
254
|
-
a[file].some(([x, y]) =>
|
|
255
|
-
b[file].some(([u, v]) => Math.max(x, u) <= Math.min(y, v)),
|
|
256
|
-
),
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
250
|
export function selectedRanges(
|
|
260
251
|
nodes: Heading[],
|
|
261
252
|
ids: string[],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared by the workspace and the standalone review viewer.
|
|
2
|
+
export const LANGUAGES = [
|
|
3
|
+
{ id: 'en', name: 'English' },
|
|
4
|
+
{ id: 'zh-Hans', name: '简体中文' },
|
|
5
|
+
{ id: 'zh-Hant', name: '繁體中文' },
|
|
6
|
+
{ id: 'ja', name: '日本語' },
|
|
7
|
+
{ id: 'ko', name: '한국어' },
|
|
8
|
+
{ id: 'de', name: 'Deutsch' },
|
|
9
|
+
{ id: 'fr', name: 'Français' },
|
|
10
|
+
{ id: 'es', name: 'Español' },
|
|
11
|
+
{ id: 'pt', name: 'Português' },
|
|
12
|
+
{ id: 'it', name: 'Italiano' },
|
|
13
|
+
{ id: 'sv', name: 'Svenska' },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type Language = typeof LANGUAGES[number]['id'];
|
|
17
|
+
|
|
18
|
+
// Accept older saved choices and regional browser preferences, but expose only
|
|
19
|
+
// the canonical language/script choices in the interface.
|
|
20
|
+
export function supportedLanguage(tag: string | null | undefined): Language | null {
|
|
21
|
+
if (!tag) return null;
|
|
22
|
+
try {
|
|
23
|
+
const locale = new Intl.Locale(tag);
|
|
24
|
+
if (locale.language === 'zh') return locale.maximize().script === 'Hant' ? 'zh-Hant' : 'zh-Hans';
|
|
25
|
+
return LANGUAGES.find(item => item.id === locale.language)?.id || null;
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rules = new Map<Language, Intl.PluralRules>();
|
|
30
|
+
export function singularCount(language: Language, count: number) {
|
|
31
|
+
let rule = rules.get(language);
|
|
32
|
+
if (!rule) { rule = new Intl.PluralRules(language); rules.set(language, rule); }
|
|
33
|
+
return rule.select(count) === 'one';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const numberFormats = new Map<Language, Intl.NumberFormat>();
|
|
37
|
+
export function interpolateMessage(text: string, language: Language, values?: Record<string, string | number>) {
|
|
38
|
+
if (!values) return text;
|
|
39
|
+
return text.replace(/\{(\w+)\}/g, (match, name: string) => {
|
|
40
|
+
if (!Object.hasOwn(values, name)) return match;
|
|
41
|
+
const value = values[name];
|
|
42
|
+
if (typeof value !== 'number') return value;
|
|
43
|
+
let format = numberFormats.get(language);
|
|
44
|
+
if (!format) { format = new Intl.NumberFormat(language, { maximumFractionDigits: 20 }); numberFormats.set(language, format); }
|
|
45
|
+
return format.format(value);
|
|
46
|
+
});
|
|
47
|
+
}
|