pi-studio 0.9.17 → 0.9.18
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/CHANGELOG.md +13 -0
- package/README.md +4 -4
- package/client/studio-client.js +835 -70
- package/client/studio.css +175 -1
- package/index.ts +326 -46
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -162,6 +162,7 @@ interface PreparedStudioPdfExport {
|
|
|
162
162
|
createdAt: number;
|
|
163
163
|
filePath?: string;
|
|
164
164
|
tempDirPath?: string;
|
|
165
|
+
persistent?: boolean;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
interface PreparedStudioHtmlExport {
|
|
@@ -171,6 +172,7 @@ interface PreparedStudioHtmlExport {
|
|
|
171
172
|
createdAt: number;
|
|
172
173
|
filePath?: string;
|
|
173
174
|
tempDirPath?: string;
|
|
175
|
+
persistent?: boolean;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
interface StudioHtmlAnnotationPlaceholder {
|
|
@@ -2257,6 +2259,8 @@ function inferStudioPdfLanguageFromPath(pathInput: string): string | undefined {
|
|
|
2257
2259
|
".yml": "yaml",
|
|
2258
2260
|
".toml": "toml",
|
|
2259
2261
|
".lua": "lua",
|
|
2262
|
+
".csv": "csv",
|
|
2263
|
+
".tsv": "tsv",
|
|
2260
2264
|
".txt": "text",
|
|
2261
2265
|
".rst": "text",
|
|
2262
2266
|
".adoc": "text",
|
|
@@ -2299,6 +2303,33 @@ function buildStudioResponseExportOutputPath(cwd: string, extension: "pdf" | "ht
|
|
|
2299
2303
|
return join(cwd || process.cwd(), `studio-response-${formatStudioExportTimestamp()}.studio.${extension}`);
|
|
2300
2304
|
}
|
|
2301
2305
|
|
|
2306
|
+
function buildStudioPreviewExportPath(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string, filename: string): string | null {
|
|
2307
|
+
const cleanFilename = String(filename || "").trim();
|
|
2308
|
+
if (!cleanFilename) return null;
|
|
2309
|
+
const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
|
|
2310
|
+
if (source) {
|
|
2311
|
+
const expanded = recoverLikelyDroppedLeadingSlashPath(expandHome(source));
|
|
2312
|
+
return join(dirname(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded)), cleanFilename);
|
|
2313
|
+
}
|
|
2314
|
+
const resource = normalizeStudioResourceDirectoryInput(typeof resourceDir === "string" ? resourceDir : "");
|
|
2315
|
+
if (resource) {
|
|
2316
|
+
const expanded = recoverLikelyDroppedLeadingSlashPath(expandHome(resource));
|
|
2317
|
+
return join(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded), cleanFilename);
|
|
2318
|
+
}
|
|
2319
|
+
return join(fallbackCwd || process.cwd(), cleanFilename);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function writeStudioPreviewExportFile(path: string | null, data: Buffer): { filePath: string | null; error: string | null } {
|
|
2323
|
+
if (!path) return { filePath: null, error: "No export path was resolved." };
|
|
2324
|
+
try {
|
|
2325
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2326
|
+
writeFileSync(path, data);
|
|
2327
|
+
return { filePath: path, error: null };
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
return { filePath: null, error: error instanceof Error ? error.message : String(error) };
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2302
2333
|
function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
2303
2334
|
| { ok: true; label: string; resolvedPath: string }
|
|
2304
2335
|
| { ok: false; message: string } {
|
|
@@ -2466,6 +2497,7 @@ const STUDIO_LOCAL_LINK_TEXT_EXTENSIONS = new Set([
|
|
|
2466
2497
|
".diff", ".patch",
|
|
2467
2498
|
]);
|
|
2468
2499
|
const STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
2500
|
+
const STUDIO_LOCAL_LINK_OFFICE_EXTENSIONS = new Set([".docx", ".odt"]);
|
|
2469
2501
|
const STUDIO_LOCAL_LINK_TEXT_FILENAMES = new Set([
|
|
2470
2502
|
".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
|
|
2471
2503
|
".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
|
|
@@ -2477,7 +2509,7 @@ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
|
|
|
2477
2509
|
"__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
2478
2510
|
]);
|
|
2479
2511
|
|
|
2480
|
-
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "other";
|
|
2512
|
+
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "office" | "other";
|
|
2481
2513
|
|
|
2482
2514
|
interface StudioLocalPreviewResource {
|
|
2483
2515
|
filePath: string;
|
|
@@ -2573,6 +2605,7 @@ function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: s
|
|
|
2573
2605
|
if (ext === ".pdf") return "pdf";
|
|
2574
2606
|
if (STUDIO_LOCAL_LINK_TEXT_EXTENSIONS.has(ext) || STUDIO_LOCAL_LINK_TEXT_FILENAMES.has(name)) return "text";
|
|
2575
2607
|
if (STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
2608
|
+
if (STUDIO_LOCAL_LINK_OFFICE_EXTENSIONS.has(ext)) return "office";
|
|
2576
2609
|
return "other";
|
|
2577
2610
|
}
|
|
2578
2611
|
|
|
@@ -4304,6 +4337,125 @@ function wrapStudioCodeAsMarkdown(code: string, language?: string): string {
|
|
|
4304
4337
|
return `${marker}${lang}\n${source}\n${marker}`;
|
|
4305
4338
|
}
|
|
4306
4339
|
|
|
4340
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_DATA_ROWS = 200;
|
|
4341
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_COLUMNS = 50;
|
|
4342
|
+
const STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS = 500;
|
|
4343
|
+
|
|
4344
|
+
function getStudioDelimitedTextConfig(language?: string): { label: string; delimiter: string } | null {
|
|
4345
|
+
const normalized = normalizeStudioEditorLanguage(language);
|
|
4346
|
+
if (normalized === "csv") return { label: "CSV", delimiter: "," };
|
|
4347
|
+
if (normalized === "tsv") return { label: "TSV", delimiter: "\t" };
|
|
4348
|
+
return null;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
function parseStudioDelimitedTextRows(text: string, delimiter: string, maxRows: number): { rows: string[][]; truncatedRows: boolean } {
|
|
4352
|
+
const source = String(text ?? "").replace(/^\uFEFF/, "");
|
|
4353
|
+
const limit = Math.max(1, Math.floor(maxRows));
|
|
4354
|
+
const rows: string[][] = [];
|
|
4355
|
+
let row: string[] = [];
|
|
4356
|
+
let cell = "";
|
|
4357
|
+
let inQuotes = false;
|
|
4358
|
+
let truncatedRows = false;
|
|
4359
|
+
|
|
4360
|
+
const pushCell = () => {
|
|
4361
|
+
row.push(cell);
|
|
4362
|
+
cell = "";
|
|
4363
|
+
};
|
|
4364
|
+
const pushRow = (index: number): boolean => {
|
|
4365
|
+
pushCell();
|
|
4366
|
+
rows.push(row);
|
|
4367
|
+
row = [];
|
|
4368
|
+
if (rows.length >= limit) {
|
|
4369
|
+
truncatedRows = index < source.length - 1;
|
|
4370
|
+
return true;
|
|
4371
|
+
}
|
|
4372
|
+
return false;
|
|
4373
|
+
};
|
|
4374
|
+
|
|
4375
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
4376
|
+
if (rows.length >= limit) {
|
|
4377
|
+
truncatedRows = true;
|
|
4378
|
+
break;
|
|
4379
|
+
}
|
|
4380
|
+
const ch = source[i];
|
|
4381
|
+
if (inQuotes) {
|
|
4382
|
+
if (ch === '"') {
|
|
4383
|
+
if (source[i + 1] === '"') {
|
|
4384
|
+
cell += '"';
|
|
4385
|
+
i += 1;
|
|
4386
|
+
} else {
|
|
4387
|
+
inQuotes = false;
|
|
4388
|
+
}
|
|
4389
|
+
} else {
|
|
4390
|
+
cell += ch;
|
|
4391
|
+
}
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
4394
|
+
if (ch === '"' && cell === "") {
|
|
4395
|
+
inQuotes = true;
|
|
4396
|
+
continue;
|
|
4397
|
+
}
|
|
4398
|
+
if (ch === delimiter) {
|
|
4399
|
+
pushCell();
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
if (ch === "\n") {
|
|
4403
|
+
if (pushRow(i)) break;
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
if (ch === "\r") {
|
|
4407
|
+
if (source[i + 1] === "\n") i += 1;
|
|
4408
|
+
if (pushRow(i)) break;
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
cell += ch;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
if (!truncatedRows && rows.length < limit && (cell.length > 0 || row.length > 0)) {
|
|
4415
|
+
pushCell();
|
|
4416
|
+
rows.push(row);
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
return { rows, truncatedRows };
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function formatStudioDelimitedMarkdownCell(value: string | undefined): string {
|
|
4423
|
+
const raw = String(value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4424
|
+
const shortened = raw.length > STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS
|
|
4425
|
+
? `${raw.slice(0, STUDIO_DELIMITED_PREVIEW_MAX_CELL_CHARS)}…`
|
|
4426
|
+
: raw;
|
|
4427
|
+
return shortened.replace(/\n/g, "<br>").replace(/\|/g, "\\|").trim() || " ";
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
function formatStudioDelimitedTextAsMarkdown(text: string, language?: string): string | null {
|
|
4431
|
+
const config = getStudioDelimitedTextConfig(language);
|
|
4432
|
+
if (!config) return null;
|
|
4433
|
+
const parsed = parseStudioDelimitedTextRows(text, config.delimiter, STUDIO_DELIMITED_PREVIEW_MAX_DATA_ROWS + 1);
|
|
4434
|
+
const rows = parsed.rows;
|
|
4435
|
+
if (!rows.length) return `_${config.label} file has no tabular data to preview._`;
|
|
4436
|
+
const rawColumnCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
4437
|
+
const columnCount = Math.min(rawColumnCount, STUDIO_DELIMITED_PREVIEW_MAX_COLUMNS);
|
|
4438
|
+
if (columnCount <= 0) return `_${config.label} file has no tabular data to preview._`;
|
|
4439
|
+
const header = rows[0] ?? [];
|
|
4440
|
+
const dataRows = rows.slice(1);
|
|
4441
|
+
const columnIndexes = Array.from({ length: columnCount }, (_value, index) => index);
|
|
4442
|
+
const lines: string[] = [`**${config.label} preview**`, ""];
|
|
4443
|
+
const notices: string[] = [];
|
|
4444
|
+
if (parsed.truncatedRows) notices.push(`showing first ${Math.max(0, dataRows.length)} data rows`);
|
|
4445
|
+
if (rawColumnCount > columnCount) notices.push(`showing first ${columnCount} of ${rawColumnCount} columns`);
|
|
4446
|
+
if (notices.length) lines.push(`_${notices.join("; ")}._`, "");
|
|
4447
|
+
lines.push(`| ${columnIndexes.map((index) => formatStudioDelimitedMarkdownCell(header[index] || `Column ${index + 1}`)).join(" | ")} |`);
|
|
4448
|
+
lines.push(`| ${columnIndexes.map(() => "---").join(" | ")} |`);
|
|
4449
|
+
if (dataRows.length) {
|
|
4450
|
+
dataRows.forEach((row) => {
|
|
4451
|
+
lines.push(`| ${columnIndexes.map((index) => formatStudioDelimitedMarkdownCell(row[index])).join(" | ")} |`);
|
|
4452
|
+
});
|
|
4453
|
+
} else {
|
|
4454
|
+
lines.push(`| ${columnIndexes.map(() => " ").join(" | ")} |`);
|
|
4455
|
+
}
|
|
4456
|
+
return lines.join("\n");
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4307
4459
|
function extractStudioFenceInfoLanguage(info: string): string | undefined {
|
|
4308
4460
|
const firstToken = String(info ?? "").trim().split(/\s+/)[0]?.replace(/^\./, "") ?? "";
|
|
4309
4461
|
return normalizeStudioEditorLanguage(firstToken || undefined);
|
|
@@ -5254,11 +5406,13 @@ function buildStudioLiteralTextPdfTexConfig(options?: StudioPdfRenderOptions): {
|
|
|
5254
5406
|
|
|
5255
5407
|
function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
|
|
5256
5408
|
if (isLatex) return markdown;
|
|
5257
|
-
const
|
|
5409
|
+
const delimitedMarkdown = formatStudioDelimitedTextAsMarkdown(markdown, editorLanguage);
|
|
5410
|
+
const input = delimitedMarkdown ?? markdown;
|
|
5411
|
+
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
5258
5412
|
const source = effectiveEditorLanguage && effectiveEditorLanguage !== "markdown" && effectiveEditorLanguage !== "latex"
|
|
5259
|
-
&& !isStudioSingleFencedCodeBlock(
|
|
5260
|
-
? wrapStudioCodeAsMarkdown(
|
|
5261
|
-
:
|
|
5413
|
+
&& !isStudioSingleFencedCodeBlock(input)
|
|
5414
|
+
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5415
|
+
: input;
|
|
5262
5416
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
5263
5417
|
? replaceStudioAnnotationMarkersForPdf(source)
|
|
5264
5418
|
: source;
|
|
@@ -5979,17 +6133,19 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
5979
6133
|
sourcePath?: string,
|
|
5980
6134
|
options?: StudioHtmlRenderOptions,
|
|
5981
6135
|
): Promise<{ html: Buffer; warning?: string }> {
|
|
5982
|
-
const
|
|
5983
|
-
|
|
5984
|
-
|
|
6136
|
+
const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(markdown, editorLanguage);
|
|
6137
|
+
const input = delimitedMarkdown ?? markdown;
|
|
6138
|
+
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
6139
|
+
if (!isLatex && isLikelyStandaloneStudioHtml(input, effectiveEditorLanguage)) {
|
|
6140
|
+
return { html: Buffer.from(String(input ?? ""), "utf-8") };
|
|
5985
6141
|
}
|
|
5986
6142
|
const source = !isLatex
|
|
5987
6143
|
&& effectiveEditorLanguage
|
|
5988
6144
|
&& effectiveEditorLanguage !== "markdown"
|
|
5989
6145
|
&& effectiveEditorLanguage !== "latex"
|
|
5990
|
-
&& !isStudioSingleFencedCodeBlock(
|
|
5991
|
-
? wrapStudioCodeAsMarkdown(
|
|
5992
|
-
:
|
|
6146
|
+
&& !isStudioSingleFencedCodeBlock(input)
|
|
6147
|
+
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
6148
|
+
: input;
|
|
5993
6149
|
const annotationPrepared = prepareStudioAnnotationMarkersForHtml(source);
|
|
5994
6150
|
const pdfPrepared = prepareStudioPdfBlocksForHtml(annotationPrepared.markdown);
|
|
5995
6151
|
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
@@ -6660,7 +6816,74 @@ function respondHtmlPreviewResourceJson(req: IncomingMessage, res: ServerRespons
|
|
|
6660
6816
|
});
|
|
6661
6817
|
}
|
|
6662
6818
|
|
|
6663
|
-
function
|
|
6819
|
+
function formatStudioMarkdownAngleTarget(pathText: string): string {
|
|
6820
|
+
return `<${String(pathText || "").replace(/>/g, "%3E")}>`;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
function sanitizeStudioPreviewBlockLine(value: string): string {
|
|
6824
|
+
return String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6827
|
+
function buildStudioLocalResourcePreviewDocument(resource: StudioLocalPreviewResource): InitialStudioDocument {
|
|
6828
|
+
const label = basename(resource.filePath) || resource.label || "local preview";
|
|
6829
|
+
const resourcePath = resource.label || basename(resource.filePath) || resource.filePath;
|
|
6830
|
+
const title = sanitizeStudioPreviewBlockLine(label);
|
|
6831
|
+
let text = "";
|
|
6832
|
+
if (resource.kind === "pdf") {
|
|
6833
|
+
text = "```studio-pdf\n"
|
|
6834
|
+
+ `path: ${sanitizeStudioPreviewBlockLine(resourcePath)}\n`
|
|
6835
|
+
+ `title: ${title || "PDF preview"}\n`
|
|
6836
|
+
+ "height: 820\n"
|
|
6837
|
+
+ "```\n";
|
|
6838
|
+
} else if (resource.kind === "image") {
|
|
6839
|
+
text = `})\n`;
|
|
6840
|
+
} else {
|
|
6841
|
+
throw new Error("This local resource cannot be opened as a preview document.");
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
text,
|
|
6845
|
+
label: `${label} preview`,
|
|
6846
|
+
source: "blank",
|
|
6847
|
+
resourceDir: resource.resourceDir,
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
function getStudioOfficePandocInputFormat(extension: string): string {
|
|
6852
|
+
const ext = String(extension || "").toLowerCase();
|
|
6853
|
+
if (ext === ".docx") return "docx";
|
|
6854
|
+
if (ext === ".odt") return "odt";
|
|
6855
|
+
return ext.replace(/^\./, "") || "docx";
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
async function convertStudioOfficeDocumentToMarkdown(resource: StudioLocalPreviewResource): Promise<{ text: string; label: string }> {
|
|
6859
|
+
if (resource.kind !== "office") throw new Error("This local resource is not a supported convertible document.");
|
|
6860
|
+
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
6861
|
+
const inputFormat = getStudioOfficePandocInputFormat(resource.extension);
|
|
6862
|
+
const result = await runStudioSubprocess(pandocCommand, [
|
|
6863
|
+
"-f", inputFormat,
|
|
6864
|
+
"-t", "markdown",
|
|
6865
|
+
"--wrap=none",
|
|
6866
|
+
resource.filePath,
|
|
6867
|
+
], {
|
|
6868
|
+
cwd: dirname(resource.filePath),
|
|
6869
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
6870
|
+
stdoutMaxBytes: STUDIO_SUBPROCESS_OUTPUT_MAX_BYTES,
|
|
6871
|
+
label: "pandoc document conversion",
|
|
6872
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to convert DOCX/ODT documents in Studio.",
|
|
6873
|
+
});
|
|
6874
|
+
if (result.code !== 0) {
|
|
6875
|
+
throw new Error(`pandoc failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
6876
|
+
}
|
|
6877
|
+
if (result.stdoutTruncated) {
|
|
6878
|
+
throw new Error("Converted document exceeded Studio's import size limit.");
|
|
6879
|
+
}
|
|
6880
|
+
const label = `converted: ${resource.label || basename(resource.filePath) || "document"}`;
|
|
6881
|
+
const note = `<!-- ${label} from ${resource.filePath}. This is a Markdown conversion; saving will not update the original ${resource.extension || "document"} file. -->`;
|
|
6882
|
+
const body = result.stdout.trim();
|
|
6883
|
+
return { text: `${note}\n\n${body}\n`, label };
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse, requestUrl: URL, resource: StudioLocalPreviewResource, serverState: StudioServerState): Promise<void> {
|
|
6664
6887
|
const method = (req.method ?? "GET").toUpperCase();
|
|
6665
6888
|
if (method !== "GET" && method !== "HEAD") {
|
|
6666
6889
|
res.setHeader("Allow", "GET, HEAD");
|
|
@@ -6684,32 +6907,72 @@ function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse,
|
|
|
6684
6907
|
return;
|
|
6685
6908
|
}
|
|
6686
6909
|
|
|
6910
|
+
if (action === "preview-url") {
|
|
6911
|
+
if (resource.kind !== "pdf" && resource.kind !== "image") {
|
|
6912
|
+
respondJson(res, 400, { ok: false, error: "This local resource cannot be opened in a Studio preview tab." });
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
const document = buildStudioLocalResourcePreviewDocument(resource);
|
|
6916
|
+
const docId = storeTransientStudioDocument(document);
|
|
6917
|
+
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
|
|
6918
|
+
const parsedUrl = new URL(url);
|
|
6919
|
+
respondJson(res, 200, {
|
|
6920
|
+
...basePayload,
|
|
6921
|
+
url,
|
|
6922
|
+
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
6923
|
+
});
|
|
6924
|
+
return;
|
|
6925
|
+
}
|
|
6926
|
+
|
|
6687
6927
|
if (action !== "document" && action !== "editor-url") {
|
|
6688
6928
|
respondJson(res, 400, { ok: false, error: "Unsupported local link action." });
|
|
6689
6929
|
return;
|
|
6690
6930
|
}
|
|
6691
|
-
if (resource.kind !== "text") {
|
|
6692
|
-
respondJson(res, 400, { ok: false, error: "This local resource is not a
|
|
6931
|
+
if (resource.kind !== "text" && resource.kind !== "office") {
|
|
6932
|
+
respondJson(res, 400, { ok: false, error: "This local resource is not a document Studio can load into the editor." });
|
|
6693
6933
|
return;
|
|
6694
6934
|
}
|
|
6695
6935
|
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6936
|
+
let document: InitialStudioDocument;
|
|
6937
|
+
let responseText = "";
|
|
6938
|
+
let converted = false;
|
|
6939
|
+
if (resource.kind === "office") {
|
|
6940
|
+
let conversion: { text: string; label: string };
|
|
6941
|
+
try {
|
|
6942
|
+
conversion = await convertStudioOfficeDocumentToMarkdown(resource);
|
|
6943
|
+
} catch (error) {
|
|
6944
|
+
respondJson(res, 400, { ok: false, error: `Document conversion failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
6945
|
+
return;
|
|
6946
|
+
}
|
|
6947
|
+
converted = true;
|
|
6948
|
+
responseText = conversion.text;
|
|
6949
|
+
document = {
|
|
6950
|
+
text: conversion.text,
|
|
6951
|
+
label: conversion.label,
|
|
6952
|
+
source: "blank",
|
|
6953
|
+
resourceDir: resource.resourceDir,
|
|
6954
|
+
};
|
|
6955
|
+
} else {
|
|
6956
|
+
const file = readStudioFile(resource.filePath, dirname(resource.filePath));
|
|
6957
|
+
if (file.ok === false) {
|
|
6958
|
+
respondJson(res, 400, { ok: false, error: file.message });
|
|
6959
|
+
return;
|
|
6960
|
+
}
|
|
6961
|
+
responseText = file.text;
|
|
6962
|
+
document = {
|
|
6963
|
+
text: file.text,
|
|
6964
|
+
label: resource.label || file.label,
|
|
6965
|
+
source: "file",
|
|
6966
|
+
path: file.resolvedPath,
|
|
6967
|
+
resourceDir: resource.resourceDir,
|
|
6968
|
+
};
|
|
6700
6969
|
}
|
|
6701
|
-
|
|
6702
|
-
const document: InitialStudioDocument = {
|
|
6703
|
-
text: file.text,
|
|
6704
|
-
label: resource.label || file.label,
|
|
6705
|
-
source: "file",
|
|
6706
|
-
path: file.resolvedPath,
|
|
6707
|
-
resourceDir: resource.resourceDir,
|
|
6708
|
-
};
|
|
6709
6970
|
if (action === "document") {
|
|
6710
6971
|
respondJson(res, 200, {
|
|
6711
6972
|
...basePayload,
|
|
6712
|
-
text:
|
|
6973
|
+
text: responseText,
|
|
6974
|
+
label: document.label,
|
|
6975
|
+
converted,
|
|
6713
6976
|
resourceDir: resource.resourceDir,
|
|
6714
6977
|
});
|
|
6715
6978
|
return;
|
|
@@ -6720,6 +6983,7 @@ function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResponse,
|
|
|
6720
6983
|
const parsedUrl = new URL(url);
|
|
6721
6984
|
respondJson(res, 200, {
|
|
6722
6985
|
...basePayload,
|
|
6986
|
+
converted,
|
|
6723
6987
|
url,
|
|
6724
6988
|
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
6725
6989
|
});
|
|
@@ -9644,6 +9908,7 @@ ${cssVarsBlock}
|
|
|
9644
9908
|
<option value="c">Syntax highlight: C</option>
|
|
9645
9909
|
<option value="cpp">Syntax highlight: C++</option>
|
|
9646
9910
|
<option value="css">Syntax highlight: CSS</option>
|
|
9911
|
+
<option value="csv">Syntax highlight: CSV</option>
|
|
9647
9912
|
<option value="diff">Syntax highlight: Diff</option>
|
|
9648
9913
|
<option value="fortran">Syntax highlight: Fortran</option>
|
|
9649
9914
|
<option value="go">Syntax highlight: Go</option>
|
|
@@ -9662,6 +9927,7 @@ ${cssVarsBlock}
|
|
|
9662
9927
|
<option value="rust">Syntax highlight: Rust</option>
|
|
9663
9928
|
<option value="swift">Syntax highlight: Swift</option>
|
|
9664
9929
|
<option value="toml">Syntax highlight: TOML</option>
|
|
9930
|
+
<option value="tsv">Syntax highlight: TSV</option>
|
|
9665
9931
|
<option value="typescript">Syntax highlight: TypeScript</option>
|
|
9666
9932
|
<option value="xml">Syntax highlight: XML</option>
|
|
9667
9933
|
<option value="yaml">Syntax highlight: YAML</option>
|
|
@@ -9847,11 +10113,11 @@ ${cssVarsBlock}
|
|
|
9847
10113
|
<div class="shortcuts-header">
|
|
9848
10114
|
<div>
|
|
9849
10115
|
<h2 id="shortcutsTitle">Keyboard shortcuts</h2>
|
|
9850
|
-
<p class="shortcuts-description">Studio navigation and high-frequency actions.</p>
|
|
10116
|
+
<p class="shortcuts-description">Studio navigation and high-frequency actions. Use arrow keys, Page Up/Down, Home/End, or mouse/trackpad to scroll.</p>
|
|
9851
10117
|
</div>
|
|
9852
10118
|
<button id="shortcutsCloseBtn" class="shortcuts-close-btn" type="button" aria-label="Close keyboard shortcuts">Close</button>
|
|
9853
10119
|
</div>
|
|
9854
|
-
<div class="shortcuts-body">
|
|
10120
|
+
<div id="shortcutsBody" class="shortcuts-body" tabindex="0" aria-label="Keyboard shortcuts list">
|
|
9855
10121
|
<section class="shortcuts-group">
|
|
9856
10122
|
<h3>Navigation</h3>
|
|
9857
10123
|
<dl>
|
|
@@ -12073,6 +12339,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12073
12339
|
};
|
|
12074
12340
|
|
|
12075
12341
|
const disposePreparedPdfExport = (entry: PreparedStudioPdfExport | null | undefined) => {
|
|
12342
|
+
if (entry?.persistent) return;
|
|
12076
12343
|
if (!entry?.tempDirPath) return;
|
|
12077
12344
|
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
12078
12345
|
};
|
|
@@ -12101,7 +12368,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12101
12368
|
}
|
|
12102
12369
|
};
|
|
12103
12370
|
|
|
12104
|
-
const storePreparedPdfExport = (pdf: Buffer, filename: string, warning?: string): string => {
|
|
12371
|
+
const storePreparedPdfExport = (pdf: Buffer, filename: string, warning?: string, filePath?: string): string => {
|
|
12105
12372
|
prunePreparedPdfExports();
|
|
12106
12373
|
const exportId = randomUUID();
|
|
12107
12374
|
preparedPdfExports.set(exportId, {
|
|
@@ -12109,6 +12376,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12109
12376
|
filename,
|
|
12110
12377
|
warning,
|
|
12111
12378
|
createdAt: Date.now(),
|
|
12379
|
+
filePath,
|
|
12380
|
+
persistent: Boolean(filePath),
|
|
12112
12381
|
});
|
|
12113
12382
|
return exportId;
|
|
12114
12383
|
};
|
|
@@ -12117,7 +12386,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12117
12386
|
prunePreparedPdfExports();
|
|
12118
12387
|
const entry = preparedPdfExports.get(exportId);
|
|
12119
12388
|
if (!entry) return null;
|
|
12120
|
-
if (entry.filePath && entry.tempDirPath) return entry;
|
|
12389
|
+
if (entry.filePath && (entry.tempDirPath || entry.persistent)) return entry;
|
|
12121
12390
|
|
|
12122
12391
|
const tempDirPath = join(tmpdir(), `pi-studio-prepared-pdf-${Date.now()}-${randomUUID()}`);
|
|
12123
12392
|
const filePath = join(tempDirPath, sanitizePdfFilename(entry.filename));
|
|
@@ -12167,6 +12436,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12167
12436
|
};
|
|
12168
12437
|
|
|
12169
12438
|
const disposePreparedHtmlExport = (entry: PreparedStudioHtmlExport | null | undefined) => {
|
|
12439
|
+
if (entry?.persistent) return;
|
|
12170
12440
|
if (!entry?.tempDirPath) return;
|
|
12171
12441
|
void rm(entry.tempDirPath, { recursive: true, force: true }).catch(() => undefined);
|
|
12172
12442
|
};
|
|
@@ -12195,7 +12465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12195
12465
|
}
|
|
12196
12466
|
};
|
|
12197
12467
|
|
|
12198
|
-
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string): string => {
|
|
12468
|
+
const storePreparedHtmlExport = (html: Buffer, filename: string, warning?: string, filePath?: string): string => {
|
|
12199
12469
|
prunePreparedHtmlExports();
|
|
12200
12470
|
const exportId = randomUUID();
|
|
12201
12471
|
preparedHtmlExports.set(exportId, {
|
|
@@ -12203,6 +12473,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12203
12473
|
filename,
|
|
12204
12474
|
warning,
|
|
12205
12475
|
createdAt: Date.now(),
|
|
12476
|
+
filePath,
|
|
12477
|
+
persistent: Boolean(filePath),
|
|
12206
12478
|
});
|
|
12207
12479
|
return exportId;
|
|
12208
12480
|
};
|
|
@@ -12211,7 +12483,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
12211
12483
|
prunePreparedHtmlExports();
|
|
12212
12484
|
const entry = preparedHtmlExports.get(exportId);
|
|
12213
12485
|
if (!entry) return null;
|
|
12214
|
-
if (entry.filePath && entry.tempDirPath) return entry;
|
|
12486
|
+
if (entry.filePath && (entry.tempDirPath || entry.persistent)) return entry;
|
|
12215
12487
|
|
|
12216
12488
|
const tempDirPath = join(tmpdir(), `pi-studio-prepared-html-${Date.now()}-${randomUUID()}`);
|
|
12217
12489
|
const filePath = join(tempDirPath, sanitizeHtmlFilename(entry.filename));
|
|
@@ -12632,7 +12904,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12632
12904
|
|
|
12633
12905
|
try {
|
|
12634
12906
|
const { pdf, warning } = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath, editorPdfLanguage, sourcePath || undefined);
|
|
12635
|
-
const
|
|
12907
|
+
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
12908
|
+
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
12636
12909
|
const token = serverState?.token ?? "";
|
|
12637
12910
|
let openedExternal = false;
|
|
12638
12911
|
let openError: string | null = null;
|
|
@@ -12649,6 +12922,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12649
12922
|
respondJson(res, 200, {
|
|
12650
12923
|
ok: true,
|
|
12651
12924
|
filename,
|
|
12925
|
+
path: writeResult.filePath,
|
|
12926
|
+
writeError: writeResult.error,
|
|
12652
12927
|
warning: warning ?? null,
|
|
12653
12928
|
openedExternal,
|
|
12654
12929
|
openError,
|
|
@@ -12743,7 +13018,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12743
13018
|
themeVars,
|
|
12744
13019
|
},
|
|
12745
13020
|
);
|
|
12746
|
-
const
|
|
13021
|
+
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), html);
|
|
13022
|
+
const exportId = storePreparedHtmlExport(html, filename, warning, writeResult.filePath ?? undefined);
|
|
12747
13023
|
const token = serverState?.token ?? "";
|
|
12748
13024
|
let openedExternal = false;
|
|
12749
13025
|
let openError: string | null = null;
|
|
@@ -12760,6 +13036,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12760
13036
|
respondJson(res, 200, {
|
|
12761
13037
|
ok: true,
|
|
12762
13038
|
filename,
|
|
13039
|
+
path: writeResult.filePath,
|
|
13040
|
+
writeError: writeResult.error,
|
|
12763
13041
|
warning: warning ?? null,
|
|
12764
13042
|
openedExternal,
|
|
12765
13043
|
openError,
|
|
@@ -13052,17 +13330,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
13052
13330
|
return;
|
|
13053
13331
|
}
|
|
13054
13332
|
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13333
|
+
void (async () => {
|
|
13334
|
+
try {
|
|
13335
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
13336
|
+
requestUrl.searchParams.get("path") ?? "",
|
|
13337
|
+
requestUrl.searchParams.get("sourcePath") ?? undefined,
|
|
13338
|
+
requestUrl.searchParams.get("resourceDir") ?? undefined,
|
|
13339
|
+
studioCwd,
|
|
13340
|
+
);
|
|
13341
|
+
await respondLocalPreviewLinkJson(req, res, requestUrl, resource, serverState);
|
|
13342
|
+
} catch (error) {
|
|
13343
|
+
respondJson(res, 404, { ok: false, error: `Local resource unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
13344
|
+
}
|
|
13345
|
+
})();
|
|
13066
13346
|
return;
|
|
13067
13347
|
}
|
|
13068
13348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.18",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|