pi-studio 0.9.46 → 0.9.48
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 +15 -0
- package/README.md +3 -1
- package/client/studio-client.js +512 -75
- package/client/studio.css +116 -17
- package/index.ts +220 -37
- package/package.json +1 -1
- package/shared/studio-latex-pandoc-compat.js +122 -0
- package/shared/studio-pandoc-resource-flag.js +34 -0
package/client/studio.css
CHANGED
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
background: var(--panel-2);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
button, select
|
|
151
|
+
button, select {
|
|
152
152
|
border: 1px solid var(--control-border);
|
|
153
153
|
background: var(--panel);
|
|
154
154
|
color: var(--text);
|
|
@@ -163,14 +163,12 @@
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
button:not(:disabled):hover,
|
|
166
|
-
select:hover
|
|
167
|
-
.file-label:hover {
|
|
166
|
+
select:hover {
|
|
168
167
|
background: var(--panel-2);
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
button:focus-visible,
|
|
172
|
-
select:focus-visible
|
|
173
|
-
.file-label:focus-within {
|
|
171
|
+
select:focus-visible {
|
|
174
172
|
outline: 2px solid var(--accent-soft-strong);
|
|
175
173
|
outline-offset: 1px;
|
|
176
174
|
}
|
|
@@ -229,15 +227,8 @@
|
|
|
229
227
|
filter: none;
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
.file-
|
|
233
|
-
|
|
234
|
-
display: inline-flex;
|
|
235
|
-
align-items: center;
|
|
236
|
-
gap: 6px;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.file-label input {
|
|
240
|
-
display: none;
|
|
230
|
+
.file-input-hidden {
|
|
231
|
+
display: none !important;
|
|
241
232
|
}
|
|
242
233
|
|
|
243
234
|
main {
|
|
@@ -1749,6 +1740,7 @@
|
|
|
1749
1740
|
|
|
1750
1741
|
.rendered-markdown .studio-pdf-card-header {
|
|
1751
1742
|
display: flex;
|
|
1743
|
+
flex-wrap: wrap;
|
|
1752
1744
|
align-items: center;
|
|
1753
1745
|
justify-content: space-between;
|
|
1754
1746
|
gap: 10px;
|
|
@@ -1777,10 +1769,13 @@
|
|
|
1777
1769
|
}
|
|
1778
1770
|
|
|
1779
1771
|
.rendered-markdown .studio-pdf-card-actions {
|
|
1780
|
-
flex: 0
|
|
1772
|
+
flex: 0 1 auto;
|
|
1781
1773
|
display: inline-flex;
|
|
1774
|
+
flex-wrap: wrap;
|
|
1775
|
+
justify-content: flex-end;
|
|
1782
1776
|
align-items: center;
|
|
1783
1777
|
gap: 8px;
|
|
1778
|
+
margin-left: auto;
|
|
1784
1779
|
}
|
|
1785
1780
|
|
|
1786
1781
|
.rendered-markdown .studio-pdf-card-link,
|
|
@@ -1907,6 +1902,7 @@
|
|
|
1907
1902
|
.studio-pdf-focus-header {
|
|
1908
1903
|
flex: 0 0 auto;
|
|
1909
1904
|
display: flex;
|
|
1905
|
+
flex-wrap: wrap;
|
|
1910
1906
|
align-items: center;
|
|
1911
1907
|
justify-content: space-between;
|
|
1912
1908
|
gap: 12px;
|
|
@@ -1934,10 +1930,13 @@
|
|
|
1934
1930
|
}
|
|
1935
1931
|
|
|
1936
1932
|
.studio-pdf-focus-actions {
|
|
1937
|
-
flex: 0
|
|
1933
|
+
flex: 0 1 auto;
|
|
1938
1934
|
display: inline-flex;
|
|
1935
|
+
flex-wrap: wrap;
|
|
1936
|
+
justify-content: flex-end;
|
|
1939
1937
|
align-items: center;
|
|
1940
1938
|
gap: 8px;
|
|
1939
|
+
margin-left: auto;
|
|
1941
1940
|
}
|
|
1942
1941
|
|
|
1943
1942
|
.studio-pdf-focus-link,
|
|
@@ -2909,6 +2908,10 @@
|
|
|
2909
2908
|
font-style: italic;
|
|
2910
2909
|
}
|
|
2911
2910
|
|
|
2911
|
+
.studio-latex-compatibility-warning {
|
|
2912
|
+
margin: 0 0 0.85em;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2912
2915
|
.marker {
|
|
2913
2916
|
display: inline-block;
|
|
2914
2917
|
padding: 0 4px;
|
|
@@ -5362,13 +5365,13 @@
|
|
|
5362
5365
|
}
|
|
5363
5366
|
|
|
5364
5367
|
body.studio-ui-refresh > header button,
|
|
5365
|
-
body.studio-ui-refresh > header .file-label,
|
|
5366
5368
|
body.studio-ui-refresh #responseActions button,
|
|
5367
5369
|
body.studio-ui-refresh #responseActions select {
|
|
5368
5370
|
padding: 5px 7px;
|
|
5369
5371
|
font-size: 12px;
|
|
5370
5372
|
}
|
|
5371
5373
|
|
|
5374
|
+
|
|
5372
5375
|
body.studio-ui-refresh main {
|
|
5373
5376
|
gap: 0;
|
|
5374
5377
|
padding: 9px;
|
|
@@ -6101,6 +6104,102 @@
|
|
|
6101
6104
|
font-weight: 700;
|
|
6102
6105
|
}
|
|
6103
6106
|
|
|
6107
|
+
.studio-decision-overlay {
|
|
6108
|
+
position: fixed;
|
|
6109
|
+
inset: 0;
|
|
6110
|
+
z-index: 13000;
|
|
6111
|
+
display: grid;
|
|
6112
|
+
place-items: center;
|
|
6113
|
+
padding: 20px;
|
|
6114
|
+
background: rgba(0, 0, 0, 0.58);
|
|
6115
|
+
backdrop-filter: blur(2px);
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
.studio-decision-overlay[hidden] {
|
|
6119
|
+
display: none !important;
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
.studio-decision-dialog {
|
|
6123
|
+
width: min(560px, calc(100vw - 32px));
|
|
6124
|
+
max-height: min(80vh, 680px);
|
|
6125
|
+
overflow: auto;
|
|
6126
|
+
padding: 20px;
|
|
6127
|
+
border: 1px solid var(--panel-border);
|
|
6128
|
+
border-radius: 14px;
|
|
6129
|
+
background: var(--panel);
|
|
6130
|
+
color: var(--text);
|
|
6131
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48);
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6134
|
+
.studio-decision-title {
|
|
6135
|
+
margin: 0;
|
|
6136
|
+
color: var(--text);
|
|
6137
|
+
font-size: 17px;
|
|
6138
|
+
line-height: 1.3;
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
.studio-decision-message {
|
|
6142
|
+
margin-top: 10px;
|
|
6143
|
+
color: var(--studio-info-text, var(--muted));
|
|
6144
|
+
font-size: 13px;
|
|
6145
|
+
line-height: 1.5;
|
|
6146
|
+
white-space: pre-wrap;
|
|
6147
|
+
overflow-wrap: anywhere;
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
.studio-decision-input {
|
|
6151
|
+
width: 100%;
|
|
6152
|
+
margin-top: 16px;
|
|
6153
|
+
padding: 9px 10px;
|
|
6154
|
+
border: 1px solid var(--control-border);
|
|
6155
|
+
border-radius: 8px;
|
|
6156
|
+
background: var(--panel-2);
|
|
6157
|
+
color: var(--text);
|
|
6158
|
+
font-family: var(--font-mono);
|
|
6159
|
+
font-size: 13px;
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
.studio-decision-input[hidden] {
|
|
6163
|
+
display: none !important;
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
.studio-decision-input:focus {
|
|
6167
|
+
border-color: var(--accent);
|
|
6168
|
+
outline: 2px solid var(--accent-soft);
|
|
6169
|
+
outline-offset: 1px;
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
.studio-decision-actions {
|
|
6173
|
+
display: flex;
|
|
6174
|
+
flex-wrap: wrap;
|
|
6175
|
+
justify-content: flex-end;
|
|
6176
|
+
gap: 8px;
|
|
6177
|
+
margin-top: 18px;
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6180
|
+
.studio-decision-confirm:not(.is-destructive) {
|
|
6181
|
+
border-color: var(--accent);
|
|
6182
|
+
background: var(--accent);
|
|
6183
|
+
color: var(--accent-contrast);
|
|
6184
|
+
font-weight: 650;
|
|
6185
|
+
}
|
|
6186
|
+
|
|
6187
|
+
.studio-decision-confirm.is-destructive {
|
|
6188
|
+
border-color: var(--warn-border);
|
|
6189
|
+
background: color-mix(in srgb, var(--warn) 15%, var(--panel));
|
|
6190
|
+
color: var(--warn);
|
|
6191
|
+
font-weight: 650;
|
|
6192
|
+
}
|
|
6193
|
+
|
|
6194
|
+
.studio-decision-confirm.is-destructive:hover,
|
|
6195
|
+
.studio-decision-confirm.is-destructive:focus-visible {
|
|
6196
|
+
background: color-mix(in srgb, var(--warn) 24%, var(--panel));
|
|
6197
|
+
}
|
|
6198
|
+
|
|
6199
|
+
body.studio-decision-open {
|
|
6200
|
+
overflow: hidden;
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6104
6203
|
.studio-preview-link-menu {
|
|
6105
6204
|
position: fixed;
|
|
6106
6205
|
z-index: 12000;
|
package/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
30
30
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
31
31
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
|
+
import { createStudioPandocHtmlResourceFlagResolver } from "./shared/studio-pandoc-resource-flag.js";
|
|
33
|
+
import { prepareStudioLatexForPandoc } from "./shared/studio-latex-pandoc-compat.js";
|
|
32
34
|
import { isStudioCmuxSession, openStudioUrlInBrowser } from "./shared/studio-browser-launcher.js";
|
|
33
35
|
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
34
36
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
@@ -647,6 +649,7 @@ const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
|
|
|
647
649
|
const STUDIO_QUIZ_SNIPPET_MAX_CHARS = 8_000;
|
|
648
650
|
const STUDIO_QUIZ_DISCUSSION_MAX_CHARS = 6_000;
|
|
649
651
|
const REQUEST_BODY_MAX_BYTES = 1_000_000;
|
|
652
|
+
const STUDIO_IMPORT_FILE_MAX_BYTES = 10_000_000;
|
|
650
653
|
const STUDIO_WORKSPACE_STATE_REQUEST_MAX_BYTES = 4_000_000;
|
|
651
654
|
const RESPONSE_HISTORY_LIMIT = 30;
|
|
652
655
|
const CMUX_NOTIFY_TIMEOUT_MS = 1200;
|
|
@@ -6126,28 +6129,45 @@ function decorateStudioPandocSyntaxHtml(html: string): string {
|
|
|
6126
6129
|
);
|
|
6127
6130
|
}
|
|
6128
6131
|
|
|
6129
|
-
|
|
6132
|
+
function buildStudioLatexPandocCompatibilityWarning(omittedPackages: Array<{ name: string }>): string | undefined {
|
|
6133
|
+
const packageNames = Array.from(new Set(
|
|
6134
|
+
omittedPackages
|
|
6135
|
+
.map((entry) => String(entry?.name ?? "").trim())
|
|
6136
|
+
.filter(Boolean),
|
|
6137
|
+
));
|
|
6138
|
+
if (packageNames.length === 0) return undefined;
|
|
6139
|
+
const packageLabel = packageNames
|
|
6140
|
+
.map((name) => name.toLowerCase().endsWith(".sty") ? name : `${name}.sty`)
|
|
6141
|
+
.join(", ");
|
|
6142
|
+
const noun = packageNames.length === 1 ? "package" : "packages";
|
|
6143
|
+
const subject = packageNames.length === 1 ? "it redefines" : "they redefine";
|
|
6144
|
+
return `Studio omitted local LaTeX ${noun} ${packageLabel} from this Pandoc rendering because ${subject} document startup. Package-specific layout may be absent; compile the source directly with LaTeX for authoritative output.`;
|
|
6145
|
+
}
|
|
6130
6146
|
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
6140
|
-
}).then((result) => {
|
|
6141
|
-
if (result.code !== 0) {
|
|
6142
|
-
throw new Error(`pandoc capability probe failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
6143
|
-
}
|
|
6144
|
-
return result.stdout.includes("--embed-resources") ? "--embed-resources" : "--self-contained";
|
|
6145
|
-
});
|
|
6146
|
-
studioPandocHtmlResourceFlagCache.set(pandocCommand, cached);
|
|
6147
|
-
}
|
|
6148
|
-
return cached;
|
|
6147
|
+
function combineStudioWarnings(...warnings: Array<string | undefined>): string | undefined {
|
|
6148
|
+
const messages = Array.from(new Set(warnings.map((warning) => String(warning ?? "").trim()).filter(Boolean)));
|
|
6149
|
+
return messages.length > 0 ? messages.join(" ") : undefined;
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
function renderStudioLatexPandocCompatibilityWarningHtml(warning: string | undefined): string {
|
|
6153
|
+
if (!warning) return "";
|
|
6154
|
+
return `<div class="preview-warning studio-latex-compatibility-warning" role="note">${escapeStudioHtmlText(warning)}</div>`;
|
|
6149
6155
|
}
|
|
6150
6156
|
|
|
6157
|
+
const resolveStudioPandocHtmlResourceFlag = createStudioPandocHtmlResourceFlagResolver(async (pandocCommand: string) => {
|
|
6158
|
+
const result = await runStudioSubprocess(pandocCommand, ["--help"], {
|
|
6159
|
+
timeoutMs: 5_000,
|
|
6160
|
+
stdoutMaxBytes: 250_000,
|
|
6161
|
+
stderrMaxBytes: 20_000,
|
|
6162
|
+
label: "pandoc capability probe",
|
|
6163
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
6164
|
+
});
|
|
6165
|
+
if (result.code !== 0) {
|
|
6166
|
+
throw new Error(`pandoc capability probe failed with exit code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`);
|
|
6167
|
+
}
|
|
6168
|
+
return result.stdout;
|
|
6169
|
+
});
|
|
6170
|
+
|
|
6151
6171
|
function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
|
|
6152
6172
|
return String(latex ?? "").replace(/\\footnotemark\s*\[\s*([^\]\r\n]+?)\s*\]/g, (_match, marker: string) => {
|
|
6153
6173
|
const value = String(marker || "").trim();
|
|
@@ -6157,8 +6177,15 @@ function preprocessStudioLatexFootnotemarksForPreview(latex: string): string {
|
|
|
6157
6177
|
|
|
6158
6178
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
6159
6179
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
6160
|
-
const
|
|
6161
|
-
|
|
6180
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
6181
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
6182
|
+
const latexPandocCompatibility = isLatex
|
|
6183
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
6184
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
6185
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
6186
|
+
const pandocInputSource = latexPandocCompatibility.source;
|
|
6187
|
+
const latexPreviewSource = isLatex ? preprocessStudioLatexFootnotemarksForPreview(pandocInputSource) : pandocInputSource;
|
|
6188
|
+
const markdownWithNormalizedFences = isLatex ? latexPreviewSource : normalizeStudioMarkdownSmartFences(pandocInputSource);
|
|
6162
6189
|
const markdownWithoutHtmlComments = isLatex ? markdownWithNormalizedFences : stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(markdownWithNormalizedFences);
|
|
6163
6190
|
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
6164
6191
|
const latexSubfigurePreviewTransform = isLatex
|
|
@@ -6171,7 +6198,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6171
6198
|
? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
|
|
6172
6199
|
: markdownWithPreviewPageBreaks;
|
|
6173
6200
|
const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
6174
|
-
const bibliographyArgs = buildStudioPandocBibliographyArgs(
|
|
6201
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
|
|
6175
6202
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
6176
6203
|
let htmlTemplateDir: string | null = null;
|
|
6177
6204
|
const useStudioHtmlTemplate = Boolean(resourcePath || isLatex);
|
|
@@ -6186,13 +6213,12 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6186
6213
|
await mkdir(htmlTemplateDir, { recursive: true });
|
|
6187
6214
|
const htmlTemplatePath = join(htmlTemplateDir, "template.html");
|
|
6188
6215
|
await writeFile(htmlTemplatePath, STUDIO_PANDOC_HTML_FRAGMENT_TEMPLATE, "utf-8");
|
|
6189
|
-
if (resourcePath) args.push(await
|
|
6216
|
+
if (resourcePath) args.push(await resolveStudioPandocHtmlResourceFlag(pandocCommand));
|
|
6190
6217
|
args.push("--standalone", `--template=${htmlTemplatePath}`);
|
|
6191
6218
|
}
|
|
6192
6219
|
const normalizedMarkdown = isLatex
|
|
6193
6220
|
? sourceWithResolvedRefs
|
|
6194
6221
|
: normalizeStudioMarkdownFencedBlocks(prepareStudioMarkdownForPandoc(sourceWithResolvedRefs));
|
|
6195
|
-
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
6196
6222
|
|
|
6197
6223
|
let pandocResult: StudioSubprocessResult;
|
|
6198
6224
|
try {
|
|
@@ -6237,6 +6263,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
6237
6263
|
renderedHtml = decorateStudioPreviewPageBreakHtml(renderedHtml);
|
|
6238
6264
|
}
|
|
6239
6265
|
renderedHtml = decorateStudioPandocSyntaxHtml(renderedHtml);
|
|
6266
|
+
renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
|
|
6240
6267
|
return stripMathMlAnnotationTags(renderedHtml);
|
|
6241
6268
|
}
|
|
6242
6269
|
|
|
@@ -6623,8 +6650,15 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
6623
6650
|
sourcePath?: string,
|
|
6624
6651
|
options?: StudioHtmlRenderOptions,
|
|
6625
6652
|
): Promise<{ html: Buffer; warning?: string }> {
|
|
6626
|
-
const
|
|
6627
|
-
|
|
6653
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
6654
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
6655
|
+
const latexPandocCompatibility = isLatex
|
|
6656
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
6657
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
6658
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
6659
|
+
const pandocCompatibleMarkdown = latexPandocCompatibility.source;
|
|
6660
|
+
const delimitedMarkdown = isLatex ? null : formatStudioDelimitedTextAsMarkdown(pandocCompatibleMarkdown, editorLanguage);
|
|
6661
|
+
const input = delimitedMarkdown ?? pandocCompatibleMarkdown;
|
|
6628
6662
|
const effectiveEditorLanguage = delimitedMarkdown ? "markdown" : inferStudioPdfLanguage(input, editorLanguage);
|
|
6629
6663
|
if (!isLatex && isLikelyStandaloneStudioHtml(input, effectiveEditorLanguage)) {
|
|
6630
6664
|
return { html: Buffer.from(String(input ?? ""), "utf-8") };
|
|
@@ -6641,8 +6675,12 @@ async function renderStudioStandaloneHtmlWithPandoc(
|
|
|
6641
6675
|
let renderedHtml = await renderStudioMarkdownWithPandoc(pdfPrepared.markdown, isLatex, resourcePath, sourcePath);
|
|
6642
6676
|
renderedHtml = renderStudioPdfBlocksInHtml(renderedHtml, pdfPrepared.blocks, sourcePath, resourcePath);
|
|
6643
6677
|
renderedHtml = applyStudioAnnotationPlaceholdersToHtml(renderedHtml, annotationPrepared.placeholders);
|
|
6678
|
+
renderedHtml = renderStudioLatexPandocCompatibilityWarningHtml(latexPandocCompatibilityWarning) + renderedHtml;
|
|
6644
6679
|
const standaloneHtml = buildStudioStandaloneHtmlDocument(renderedHtml, resourcePath, options);
|
|
6645
|
-
return {
|
|
6680
|
+
return {
|
|
6681
|
+
html: Buffer.from(standaloneHtml, "utf-8"),
|
|
6682
|
+
warning: latexPandocCompatibilityWarning,
|
|
6683
|
+
};
|
|
6646
6684
|
}
|
|
6647
6685
|
|
|
6648
6686
|
async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
|
|
@@ -7031,19 +7069,26 @@ async function renderStudioPdfWithPandoc(
|
|
|
7031
7069
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
7032
7070
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
7033
7071
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
7072
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath)
|
|
7073
|
+
?? resolveStudioPandocWorkingDir(sourcePath ? dirname(sourcePath) : undefined);
|
|
7074
|
+
const latexPandocCompatibility = isLatex
|
|
7075
|
+
? prepareStudioLatexForPandoc(markdown, pandocWorkingDir)
|
|
7076
|
+
: { source: markdown, omittedPackages: [] as Array<{ name: string; path: string }> };
|
|
7077
|
+
const latexPandocCompatibilityWarning = buildStudioLatexPandocCompatibilityWarning(latexPandocCompatibility.omittedPackages);
|
|
7078
|
+
const pandocInputSource = latexPandocCompatibility.source;
|
|
7034
7079
|
const latexSubfigurePdfTransform = isLatex
|
|
7035
|
-
? preprocessStudioLatexSubfiguresForPdf(
|
|
7036
|
-
: { markdown, groups: [] };
|
|
7080
|
+
? preprocessStudioLatexSubfiguresForPdf(pandocInputSource)
|
|
7081
|
+
: { markdown: pandocInputSource, groups: [] };
|
|
7037
7082
|
const latexPdfSource = isLatex
|
|
7038
7083
|
? preprocessStudioLatexAlgorithmsForPdf(
|
|
7039
7084
|
latexSubfigurePdfTransform.markdown,
|
|
7040
7085
|
sourcePath,
|
|
7041
7086
|
resourcePath,
|
|
7042
7087
|
)
|
|
7043
|
-
:
|
|
7088
|
+
: pandocInputSource;
|
|
7044
7089
|
const sourceWithResolvedRefs = isLatex
|
|
7045
7090
|
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
|
|
7046
|
-
:
|
|
7091
|
+
: pandocInputSource;
|
|
7047
7092
|
const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
|
|
7048
7093
|
const pdfCalloutTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
7049
7094
|
? preprocessStudioMarkdownCalloutsForPdf(sourceWithResolvedRefs)
|
|
@@ -7051,8 +7096,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
7051
7096
|
const pdfAlignedImageTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
7052
7097
|
? preprocessStudioMarkdownImageAlignmentForPdf(pdfCalloutTransform.markdown)
|
|
7053
7098
|
: { markdown: pdfCalloutTransform.markdown, blocks: [] as StudioPdfAlignedImageBlock[] };
|
|
7054
|
-
const
|
|
7055
|
-
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
7099
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(pandocInputSource, isLatex, resourcePath);
|
|
7056
7100
|
|
|
7057
7101
|
const runPandocPdfExport = async (
|
|
7058
7102
|
inputFormat: string,
|
|
@@ -7105,7 +7149,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
7105
7149
|
};
|
|
7106
7150
|
|
|
7107
7151
|
if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || collectStudioInlineAnnotationMarkers(sourceWithResolvedRefs).length > 0)) {
|
|
7108
|
-
|
|
7152
|
+
const rendered = await renderStudioPdfFromGeneratedLatex(
|
|
7109
7153
|
sourceWithResolvedRefs,
|
|
7110
7154
|
pandocCommand,
|
|
7111
7155
|
pdfEngine,
|
|
@@ -7121,6 +7165,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7121
7165
|
"",
|
|
7122
7166
|
themeStyle,
|
|
7123
7167
|
);
|
|
7168
|
+
return {
|
|
7169
|
+
pdf: rendered.pdf,
|
|
7170
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, rendered.warning),
|
|
7171
|
+
};
|
|
7124
7172
|
}
|
|
7125
7173
|
|
|
7126
7174
|
if (!isLatex && effectiveEditorLanguage === "diff") {
|
|
@@ -7194,7 +7242,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7194
7242
|
themeStyle,
|
|
7195
7243
|
);
|
|
7196
7244
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
7197
|
-
return {
|
|
7245
|
+
return {
|
|
7246
|
+
pdf: rendered.pdf,
|
|
7247
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning, rendered.warning),
|
|
7248
|
+
};
|
|
7198
7249
|
}
|
|
7199
7250
|
|
|
7200
7251
|
const hasYamlHeaderIncludesForPdf = !isLatex && hasStudioYamlHeaderIncludes(markdownForPdf);
|
|
@@ -7229,7 +7280,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
7229
7280
|
throw new Error(`pandoc PDF export failed with exit code ${pandocResult.code}${stderr ? `: ${stderr}` : ""}${hint}`);
|
|
7230
7281
|
}
|
|
7231
7282
|
|
|
7232
|
-
return {
|
|
7283
|
+
return {
|
|
7284
|
+
pdf: await readFile(outputPath),
|
|
7285
|
+
warning: combineStudioWarnings(latexPandocCompatibilityWarning, mermaidPrepared.warning),
|
|
7286
|
+
};
|
|
7233
7287
|
} finally {
|
|
7234
7288
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
7235
7289
|
}
|
|
@@ -7539,6 +7593,64 @@ function revealStudioLocalFile(filePath: string): { ok: true; message: string }
|
|
|
7539
7593
|
return { ok: true, message: process.platform === "linux" ? "Opened containing folder." : "Revealed resource in file manager." };
|
|
7540
7594
|
}
|
|
7541
7595
|
|
|
7596
|
+
async function handleImportStudioFileCopyRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7597
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7598
|
+
if (method !== "POST") {
|
|
7599
|
+
res.setHeader("Allow", "POST");
|
|
7600
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7601
|
+
return;
|
|
7602
|
+
}
|
|
7603
|
+
|
|
7604
|
+
const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7605
|
+
let payload: Record<string, unknown> = {};
|
|
7606
|
+
try {
|
|
7607
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
7608
|
+
} catch {
|
|
7609
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
const requestedPath = typeof payload.path === "string" ? payload.path : "";
|
|
7614
|
+
const resolved = resolveStudioPath(requestedPath, studioCwd);
|
|
7615
|
+
if (resolved.ok === false) {
|
|
7616
|
+
respondJson(res, 400, { ok: false, error: resolved.message });
|
|
7617
|
+
return;
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
try {
|
|
7621
|
+
const stats = statSync(resolved.resolved);
|
|
7622
|
+
if (!stats.isFile()) {
|
|
7623
|
+
respondJson(res, 400, { ok: false, error: `Path is not a file: ${resolved.label}` });
|
|
7624
|
+
return;
|
|
7625
|
+
}
|
|
7626
|
+
if (stats.size > STUDIO_IMPORT_FILE_MAX_BYTES) {
|
|
7627
|
+
respondJson(res, 413, {
|
|
7628
|
+
ok: false,
|
|
7629
|
+
error: `File is too large to import into Studio (${stats.size} bytes; limit ${STUDIO_IMPORT_FILE_MAX_BYTES} bytes).`,
|
|
7630
|
+
});
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
} catch (error) {
|
|
7634
|
+
respondJson(res, 404, {
|
|
7635
|
+
ok: false,
|
|
7636
|
+
error: `Could not access file: ${resolved.label} (${error instanceof Error ? error.message : String(error)})`,
|
|
7637
|
+
});
|
|
7638
|
+
return;
|
|
7639
|
+
}
|
|
7640
|
+
|
|
7641
|
+
const file = readStudioFile(resolved.resolved, studioCwd);
|
|
7642
|
+
if (file.ok === false) {
|
|
7643
|
+
respondJson(res, 400, { ok: false, error: file.message });
|
|
7644
|
+
return;
|
|
7645
|
+
}
|
|
7646
|
+
respondJson(res, 200, {
|
|
7647
|
+
ok: true,
|
|
7648
|
+
text: file.text,
|
|
7649
|
+
filename: basename(file.resolvedPath),
|
|
7650
|
+
resolvedPath: file.resolvedPath,
|
|
7651
|
+
});
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7542
7654
|
async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7543
7655
|
const method = (req.method ?? "GET").toUpperCase();
|
|
7544
7656
|
if (method !== "POST") {
|
|
@@ -7574,6 +7686,50 @@ async function handleRevealLocalPreviewResourceRequest(req: IncomingMessage, res
|
|
|
7574
7686
|
}
|
|
7575
7687
|
}
|
|
7576
7688
|
|
|
7689
|
+
async function handleOpenLocalPreviewResourceRequest(req: IncomingMessage, res: ServerResponse, studioCwd: string): Promise<void> {
|
|
7690
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7691
|
+
if (method !== "POST") {
|
|
7692
|
+
res.setHeader("Allow", "POST");
|
|
7693
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
if (isSshSession()) {
|
|
7697
|
+
respondJson(res, 409, { ok: false, error: "Cannot open a system PDF viewer from an SSH/headless Studio session. Copy the path instead." });
|
|
7698
|
+
return;
|
|
7699
|
+
}
|
|
7700
|
+
|
|
7701
|
+
const rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7702
|
+
let payload: Record<string, unknown> = {};
|
|
7703
|
+
try {
|
|
7704
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
7705
|
+
} catch {
|
|
7706
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7707
|
+
return;
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
try {
|
|
7711
|
+
const resource = resolveStudioLocalPreviewResourcePath(
|
|
7712
|
+
typeof payload.path === "string" ? payload.path : "",
|
|
7713
|
+
typeof payload.sourcePath === "string" ? payload.sourcePath : undefined,
|
|
7714
|
+
typeof payload.resourceDir === "string" ? payload.resourceDir : undefined,
|
|
7715
|
+
studioCwd,
|
|
7716
|
+
);
|
|
7717
|
+
if (resource.kind !== "pdf") {
|
|
7718
|
+
respondJson(res, 400, { ok: false, error: "Only local PDF previews can be opened in the system PDF viewer." });
|
|
7719
|
+
return;
|
|
7720
|
+
}
|
|
7721
|
+
await openPathInDefaultViewer(resource.filePath);
|
|
7722
|
+
respondJson(res, 200, {
|
|
7723
|
+
ok: true,
|
|
7724
|
+
message: "Opened PDF in the system viewer.",
|
|
7725
|
+
path: resource.filePath,
|
|
7726
|
+
label: resource.label,
|
|
7727
|
+
});
|
|
7728
|
+
} catch (error) {
|
|
7729
|
+
respondJson(res, 404, { ok: false, error: `Local PDF unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7577
7733
|
function openPathInDefaultViewer(path: string): Promise<void> {
|
|
7578
7734
|
const openCommand =
|
|
7579
7735
|
process.platform === "darwin"
|
|
@@ -10596,7 +10752,8 @@ ${cssVarsBlock}
|
|
|
10596
10752
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
|
|
10597
10753
|
<button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
|
|
10598
10754
|
<button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
|
|
10599
|
-
<
|
|
10755
|
+
<button id="importFileBtn" type="button" title="Import a file as an editable copy.">Import file copy…</button>
|
|
10756
|
+
<input id="fileInput" class="file-input-hidden" type="file" tabindex="-1" aria-hidden="true" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" />
|
|
10600
10757
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
10601
10758
|
<button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
|
|
10602
10759
|
</div>
|
|
@@ -14916,6 +15073,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
14916
15073
|
return;
|
|
14917
15074
|
}
|
|
14918
15075
|
|
|
15076
|
+
if (requestUrl.pathname === "/import-file-copy") {
|
|
15077
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
15078
|
+
if (token !== serverState.token) {
|
|
15079
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
15080
|
+
return;
|
|
15081
|
+
}
|
|
15082
|
+
|
|
15083
|
+
void handleImportStudioFileCopyRequest(req, res, studioCwd).catch((error) => {
|
|
15084
|
+
respondJson(res, 500, { ok: false, error: `File import failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
15085
|
+
});
|
|
15086
|
+
return;
|
|
15087
|
+
}
|
|
15088
|
+
|
|
14919
15089
|
if (requestUrl.pathname === "/local-preview-link") {
|
|
14920
15090
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14921
15091
|
if (token !== serverState.token) {
|
|
@@ -14952,6 +15122,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
14952
15122
|
return;
|
|
14953
15123
|
}
|
|
14954
15124
|
|
|
15125
|
+
if (requestUrl.pathname === "/open-local-resource") {
|
|
15126
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
15127
|
+
if (token !== serverState.token) {
|
|
15128
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
15129
|
+
return;
|
|
15130
|
+
}
|
|
15131
|
+
|
|
15132
|
+
void handleOpenLocalPreviewResourceRequest(req, res, studioCwd).catch((error) => {
|
|
15133
|
+
respondJson(res, 500, { ok: false, error: `Open in system viewer failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
15134
|
+
});
|
|
15135
|
+
return;
|
|
15136
|
+
}
|
|
15137
|
+
|
|
14955
15138
|
if (requestUrl.pathname === "/pdf-resource") {
|
|
14956
15139
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14957
15140
|
if (token !== serverState.token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.48",
|
|
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",
|