papilot 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-CwFvCo6r.css +1 -0
- package/dist/assets/index-dByEATYE.js +267 -0
- package/dist/assets/{math-ibBpTslk.js → math-UelUOI-R.js} +1 -1
- package/dist/assets/{pdf-view-CD1kNKYH.js → pdf-view-ChJvfsD8.js} +1 -1
- package/dist/index.html +2 -2
- package/engine/dist/web/assets/{FullPaper-DUgLb01O.js → FullPaper-DbT25OhQ.js} +1 -1
- package/engine/dist/web/assets/index-C_KwLmXQ.js +19 -0
- package/engine/dist/web/index.html +1 -1
- package/engine/src/web/review.tsx +2 -6
- package/engine/src/web/view-preferences.ts +8 -6
- package/package.json +1 -1
- package/src/server/app.ts +2 -1
- package/src/server/imports.ts +40 -18
- package/dist/assets/index-0siHHtfz.js +0 -267
- package/dist/assets/index-CxdYm81a.css +0 -1
- package/engine/dist/web/assets/index-DJywe0wU.js +0 -19
|
@@ -9,7 +9,7 @@
|
|
|
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-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-C_KwLmXQ.js"></script>
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/index-D3_ZFdT8.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
@@ -175,7 +175,7 @@ export function ReviewApp({
|
|
|
175
175
|
const [shortcuts, setShortcuts] = useState(readShortcuts);
|
|
176
176
|
// The whole paper with the changes in place, or the changed sections only.
|
|
177
177
|
// A host may fix the view in the address (Papilot's manuscript chooses its
|
|
178
|
-
// PDF with its own switch); otherwise it
|
|
178
|
+
// PDF with its own switch); otherwise it opens on the stored default.
|
|
179
179
|
const [pinnedView] = useState<ReviewView | null>(() => {
|
|
180
180
|
const value = new URLSearchParams(location.search).get("view");
|
|
181
181
|
return value === "paper" || value === "changes" ? value : null;
|
|
@@ -1144,13 +1144,9 @@ export function ReviewApp({
|
|
|
1144
1144
|
if (article) reveal(article);
|
|
1145
1145
|
});
|
|
1146
1146
|
}, [paperView]);
|
|
1147
|
+
// The switch changes this review only; the default is a Papilot setting.
|
|
1147
1148
|
function chooseView(next: ReviewView) {
|
|
1148
1149
|
setView(next);
|
|
1149
|
-
try {
|
|
1150
|
-
localStorage.setItem(VIEW_STORAGE, next);
|
|
1151
|
-
} catch {
|
|
1152
|
-
/* The choice lasts for this page. */
|
|
1153
|
-
}
|
|
1154
1150
|
}
|
|
1155
1151
|
let index = 0;
|
|
1156
1152
|
return (
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// Whole paper or changed sections only.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// Whole paper ("paper") or changed sections only ("changes"). Reviews open on
|
|
2
|
+
// the stored default: the changed sections, which show fastest, unless the
|
|
3
|
+
// whole paper was chosen in Papilot's settings. Switching views lasts for the
|
|
4
|
+
// open review only. A host such as Papilot shows its own switch and tells this
|
|
5
|
+
// frame; storage events bring a new default to reviews already open.
|
|
6
|
+
export const VIEW_STORAGE = "latex-review:default-view";
|
|
5
7
|
export const VIEW_EVENT = "latex-review:view-changed";
|
|
6
8
|
export type ReviewView = "paper" | "changes";
|
|
7
9
|
export function readView(): ReviewView {
|
|
8
10
|
try {
|
|
9
|
-
return localStorage.getItem(VIEW_STORAGE) === "
|
|
11
|
+
return localStorage.getItem(VIEW_STORAGE) === "paper" ? "paper" : "changes";
|
|
10
12
|
} catch {
|
|
11
|
-
return "
|
|
13
|
+
return "changes";
|
|
12
14
|
}
|
|
13
15
|
}
|
package/package.json
CHANGED
package/src/server/app.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type { Updates } from './updates.ts';
|
|
|
25
25
|
import { Diagnostics } from './diagnostics.ts';
|
|
26
26
|
import { attachmentIds } from './attachments.ts';
|
|
27
27
|
import { MAX_ATTACHMENT_BYTES } from '../shared.ts';
|
|
28
|
-
import { PaperImports } from './imports.ts';
|
|
28
|
+
import { PaperImports, openFolder } from './imports.ts';
|
|
29
29
|
import { DATA } from './paths.ts';
|
|
30
30
|
import { registerAiReviewRoutes } from './ai-review-routes.ts';
|
|
31
31
|
import { detectLatex } from './latex-installation.ts';
|
|
@@ -174,6 +174,7 @@ export async function buildApp(options: AppOptions) {
|
|
|
174
174
|
const string = (value: unknown, name: string, max = 20000) => { if (typeof value !== 'string' || !value.trim() || value.length > max) throw new Error(`${name} is required and must be no longer than ${max} characters.`); return value.trim(); };
|
|
175
175
|
// Adding a paper names a folder on this host, so only the owner can.
|
|
176
176
|
app.post('/api/projects', owner, async request => { const b = request.body as any; return workspace.addProject(string(b.path, 'Repository path', 4000), b.name ? string(b.name, 'Paper name', 120) : undefined); });
|
|
177
|
+
app.post('/api/projects/open', owner, request => openFolder((request.body || {}) as any, root => store.projectByPath(root), (root, name) => workspace.addProject(root, name)));
|
|
177
178
|
app.post('/api/imports', { ...owner, bodyLimit: 90 * 1024 * 1024 }, request => imports!.create((request.body || {}) as any));
|
|
178
179
|
app.get<{ Params: { id: string } }>('/api/imports/:id', owner, request => imports!.get(request.params.id));
|
|
179
180
|
app.post<{ Params: { id: string } }>('/api/imports/:id/build', owner, request => imports!.build(request.params.id, (request.body || {}) as any));
|
package/src/server/imports.ts
CHANGED
|
@@ -88,18 +88,25 @@ export async function inspectPaper(root: string) {
|
|
|
88
88
|
const found = new Set(roots);
|
|
89
89
|
candidates = [...roots, ...files.filter(file => !found.has(file))];
|
|
90
90
|
}
|
|
91
|
-
try {
|
|
92
|
-
// Engine detection also works before Git initialization by reading the source hints.
|
|
93
|
-
if (main) {
|
|
94
|
-
const text = sourceHead(path.join(root, main));
|
|
95
|
-
engine = typeof settings.engine === 'string' ? settings.engine : '';
|
|
96
|
-
const rc = [path.join(path.dirname(main), '.latexmkrc'), path.join(path.dirname(main), 'latexmkrc'), '.latexmkrc', 'latexmkrc'].find(file => fs.existsSync(path.join(root, file)));
|
|
97
|
-
const mode = rc ? fs.readFileSync(path.join(root, rc), 'utf8').match(/^\s*\$pdf_mode\s*=\s*(\d)/m)?.[1] : undefined;
|
|
98
|
-
if (!engine) engine = text.match(/^%\s*!\s*TEX\s+(?:TS-)?program\s*=\s*(pdflatex|xelatex|lualatex)/im)?.[1].toLowerCase() || ({ '1': 'pdflatex', '4': 'lualatex', '5': 'xelatex' } as Record<string, string>)[mode || ''] || (/\\(?:usepackage(?:\[[^\]]*\])?\{[^}]*\b(?:fontspec|xeCJK|unicode-math|polyglossia)\b|documentclass(?:\[[^\]]*\])?\{ctex)/.test(text) ? 'xelatex' : 'pdflatex');
|
|
99
|
-
}
|
|
100
|
-
} catch (failure) { error = (failure as Error).message; }
|
|
91
|
+
try { if (main) engine = engineFor(root, main, settings); } catch (failure) { error = (failure as Error).message; }
|
|
101
92
|
return { candidates, main, engine: engine || 'pdflatex', error };
|
|
102
93
|
}
|
|
94
|
+
// Engine detection also works before Git initialization by reading the source hints.
|
|
95
|
+
function engineFor(root: string, main: string, settings: Record<string, unknown>) {
|
|
96
|
+
if (typeof settings.engine === 'string' && settings.engine) return settings.engine;
|
|
97
|
+
const text = sourceHead(path.join(root, main));
|
|
98
|
+
const rc = [path.join(path.dirname(main), '.latexmkrc'), path.join(path.dirname(main), 'latexmkrc'), '.latexmkrc', 'latexmkrc'].find(file => fs.existsSync(path.join(root, file)));
|
|
99
|
+
const mode = rc ? fs.readFileSync(path.join(root, rc), 'utf8').match(/^\s*\$pdf_mode\s*=\s*(\d)/m)?.[1] : undefined;
|
|
100
|
+
return text.match(/^%\s*!\s*TEX\s+(?:TS-)?program\s*=\s*(pdflatex|xelatex|lualatex)/im)?.[1].toLowerCase() || ({ '1': 'pdflatex', '4': 'lualatex', '5': 'xelatex' } as Record<string, string>)[mode || ''] || (/\\(?:usepackage(?:\[[^\]]*\])?\{[^}]*\b(?:fontspec|xeCJK|unicode-math|polyglossia)\b|documentclass(?:\[[^\]]*\])?\{ctex)/.test(text) ? 'xelatex' : 'pdflatex');
|
|
101
|
+
}
|
|
102
|
+
// Keep the author's settings, including a source folder that still contains
|
|
103
|
+
// the main file; without one, the whole folder compiles.
|
|
104
|
+
function rememberMain(root: string, main: string, engine: string) {
|
|
105
|
+
const settings = config(root), folder = typeof settings.source_dir === 'string' ? settings.source_dir.replace(/\/+$/, '') : null;
|
|
106
|
+
const contains = folder === '' || (folder !== null && main.startsWith(folder + '/'));
|
|
107
|
+
const next = { ...settings, main, engine, ...(contains ? {} : { source_dir: '' }) };
|
|
108
|
+
if (JSON.stringify(next) !== JSON.stringify(settings)) fs.writeFileSync(path.join(root, CONFIG), JSON.stringify(next, null, 2) + '\n');
|
|
109
|
+
}
|
|
103
110
|
async function repositoryRoot(root: string) {
|
|
104
111
|
let top: string | undefined;
|
|
105
112
|
try { top = await git(root, 'rev-parse', '--show-toplevel'); } catch { /* A plain folder needs Git. */ }
|
|
@@ -115,6 +122,28 @@ export async function prepareRepository(root: string) {
|
|
|
115
122
|
await git(root, 'add', '--all');
|
|
116
123
|
await git(root, '-c', 'user.name=Papilot', '-c', 'user.email=papilot@localhost', '-c', 'commit.gpgsign=false', '-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'Import paper into Papilot');
|
|
117
124
|
}
|
|
125
|
+
// The desktop app opens a folder the way an editor does. A paper already in
|
|
126
|
+
// Papilot opens again; any other folder with a clear main file is added as it
|
|
127
|
+
// is and compiles in its own view. Only a folder with several possible main
|
|
128
|
+
// files returns them, so the author can pick one.
|
|
129
|
+
export async function openFolder(input: { path?: unknown; main?: unknown }, known: (root: string) => Project | undefined, register: (root: string, name: string) => Promise<Project>): Promise<{ project: Project } | { name: string; candidates: string[] }> {
|
|
130
|
+
if (typeof input.path !== 'string' || !input.path.trim()) throw new Error('Enter a folder path.');
|
|
131
|
+
const root = fs.realpathSync(input.path);
|
|
132
|
+
if (!fs.statSync(root).isDirectory()) throw new Error('Select a folder containing the paper.');
|
|
133
|
+
const project = known(root);
|
|
134
|
+
if (project) return { project };
|
|
135
|
+
await repositoryRoot(root);
|
|
136
|
+
const detected = await inspectPaper(root);
|
|
137
|
+
if (detected.error) throw new Error(detected.error);
|
|
138
|
+
if (input.main === undefined && !detected.main) {
|
|
139
|
+
if (!detected.candidates.length) throw new Error('Select a folder containing the paper.');
|
|
140
|
+
return { name: path.basename(root), candidates: detected.candidates };
|
|
141
|
+
}
|
|
142
|
+
const main = input.main ?? detected.main;
|
|
143
|
+
if (!existingTex(root, main)) throw new Error('Choose an existing TeX main file.');
|
|
144
|
+
rememberMain(root, main, engineFor(root, main, config(root)));
|
|
145
|
+
return { project: await register(root, path.basename(root)) };
|
|
146
|
+
}
|
|
118
147
|
export async function extractZip(buffer: Buffer, root: string) {
|
|
119
148
|
if (buffer.length > MAX_BYTES) throw new Error('Choose a ZIP smaller than 64 MB.');
|
|
120
149
|
const zip = await new Promise<yauzl.ZipFile>((resolve, reject) => yauzl.fromBuffer(buffer, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (error, value) => error ? reject(error) : resolve(value)));
|
|
@@ -214,14 +243,7 @@ export class PaperImports {
|
|
|
214
243
|
void (async () => {
|
|
215
244
|
try {
|
|
216
245
|
await repositoryRoot(value.path);
|
|
217
|
-
if (!repair)
|
|
218
|
-
// Keep the author's settings, including a source folder that still
|
|
219
|
-
// contains the main file; without one, the whole folder compiles.
|
|
220
|
-
const settings = config(value.path), folder = typeof settings.source_dir === 'string' ? settings.source_dir.replace(/\/+$/, '') : null;
|
|
221
|
-
const contains = folder === '' || (folder !== null && (main as string).startsWith(folder + '/'));
|
|
222
|
-
const next = { ...settings, main, engine, ...(contains ? {} : { source_dir: '' }) };
|
|
223
|
-
if (JSON.stringify(next) !== JSON.stringify(settings)) fs.writeFileSync(path.join(value.path, CONFIG), JSON.stringify(next, null, 2) + '\n');
|
|
224
|
-
}
|
|
246
|
+
if (!repair) rememberMain(value.path, main as string, engine as string);
|
|
225
247
|
await prepareRepository(value.path); value.versioned = true;
|
|
226
248
|
if (repair) { value.summary = await repair(value.path, previousError); Object.assign(value, await inspectPaper(value.path)); }
|
|
227
249
|
value.phase = 'building'; this.save(value);
|