pi-studio 0.5.19 → 0.5.20
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 +7 -0
- package/client/studio-client.js +1 -1
- package/client/studio.css +5 -0
- package/index.ts +106 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.20] — 2026-03-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- LaTeX preview/PDF export now runs pandoc from the resolved source/working directory, so project-relative `\input{...}` files, shared macros, and similar local assets resolve more reliably for multi-file documents.
|
|
11
|
+
- LaTeX preview/PDF export now also detects basic bibliography directives such as `\bibliography{...}` and `\addbibresource{...}` and passes the resolved `.bib` files to pandoc citeproc, so references show up more often in Studio without a full `latexmk` build.
|
|
12
|
+
- Display-math blocks in preview are now styled to center more naturally, and the raw-editor highlight cutoff is bumped to `100_000` characters so moderately large `.tex` files still get inline syntax colouring.
|
|
13
|
+
|
|
7
14
|
## [0.5.19] — 2026-03-19
|
|
8
15
|
|
|
9
16
|
### Fixed
|
package/client/studio-client.js
CHANGED
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
};
|
|
164
164
|
let activePane = "left";
|
|
165
165
|
let paneFocusTarget = "off";
|
|
166
|
-
const EDITOR_HIGHLIGHT_MAX_CHARS =
|
|
166
|
+
const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
|
|
167
167
|
const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
|
|
168
168
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
169
169
|
// Single source of truth: language -> file extensions (and display label)
|
package/client/studio.css
CHANGED
|
@@ -758,9 +758,14 @@
|
|
|
758
758
|
max-width: 100%;
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
+
.rendered-markdown math {
|
|
762
|
+
font-family: "STIX Two Math", "Cambria Math", "Latin Modern Math", "STIXGeneral", serif;
|
|
763
|
+
}
|
|
764
|
+
|
|
761
765
|
.rendered-markdown math[display="block"] {
|
|
762
766
|
display: block;
|
|
763
767
|
margin: 1em 0;
|
|
768
|
+
text-align: center;
|
|
764
769
|
overflow-x: auto;
|
|
765
770
|
overflow-y: hidden;
|
|
766
771
|
}
|
package/index.ts
CHANGED
|
@@ -994,20 +994,111 @@ function buildStudioSyntheticNewFileDiff(filePath: string, content: string): str
|
|
|
994
994
|
return diffLines.join("\n");
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
-
function
|
|
997
|
+
function resolveStudioBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
998
998
|
const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
|
|
999
999
|
if (source) {
|
|
1000
|
-
|
|
1000
|
+
const expanded = expandHome(source);
|
|
1001
|
+
return dirname(isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded));
|
|
1001
1002
|
}
|
|
1002
1003
|
|
|
1003
1004
|
const resource = typeof resourceDir === "string" ? resourceDir.trim() : "";
|
|
1004
1005
|
if (resource) {
|
|
1005
|
-
|
|
1006
|
+
const expanded = expandHome(resource);
|
|
1007
|
+
return isAbsolute(expanded) ? expanded : resolve(fallbackCwd, expanded);
|
|
1006
1008
|
}
|
|
1007
1009
|
|
|
1008
1010
|
return fallbackCwd;
|
|
1009
1011
|
}
|
|
1010
1012
|
|
|
1013
|
+
function resolveStudioGitDiffBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
1014
|
+
return resolveStudioBaseDir(sourcePath, resourceDir, fallbackCwd);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function resolveStudioPandocWorkingDir(baseDir: string | undefined): string | undefined {
|
|
1018
|
+
const normalized = typeof baseDir === "string" ? baseDir.trim() : "";
|
|
1019
|
+
if (!normalized) return undefined;
|
|
1020
|
+
try {
|
|
1021
|
+
return statSync(normalized).isDirectory() ? normalized : undefined;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return undefined;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function stripStudioLatexComments(text: string): string {
|
|
1028
|
+
const lines = String(text ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
1029
|
+
return lines.map((line) => {
|
|
1030
|
+
let out = "";
|
|
1031
|
+
let backslashRun = 0;
|
|
1032
|
+
for (let i = 0; i < line.length; i++) {
|
|
1033
|
+
const ch = line[i]!;
|
|
1034
|
+
if (ch === "%" && backslashRun % 2 === 0) break;
|
|
1035
|
+
out += ch;
|
|
1036
|
+
if (ch === "\\") backslashRun++;
|
|
1037
|
+
else backslashRun = 0;
|
|
1038
|
+
}
|
|
1039
|
+
return out;
|
|
1040
|
+
}).join("\n");
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function collectStudioLatexBibliographyCandidates(markdown: string): string[] {
|
|
1044
|
+
const stripped = stripStudioLatexComments(markdown);
|
|
1045
|
+
const candidates: string[] = [];
|
|
1046
|
+
const seen = new Set<string>();
|
|
1047
|
+
const pushCandidate = (raw: string) => {
|
|
1048
|
+
let candidate = String(raw ?? "").trim().replace(/^file:/i, "").replace(/^['"]|['"]$/g, "");
|
|
1049
|
+
if (!candidate) return;
|
|
1050
|
+
if (!/\.[A-Za-z0-9]+$/.test(candidate)) candidate += ".bib";
|
|
1051
|
+
if (seen.has(candidate)) return;
|
|
1052
|
+
seen.add(candidate);
|
|
1053
|
+
candidates.push(candidate);
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
for (const match of stripped.matchAll(/\\bibliography\s*\{([^}]+)\}/g)) {
|
|
1057
|
+
const rawList = match[1] ?? "";
|
|
1058
|
+
for (const part of rawList.split(",")) {
|
|
1059
|
+
pushCandidate(part);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
for (const match of stripped.matchAll(/\\addbibresource(?:\[[^\]]*\])?\s*\{([^}]+)\}/g)) {
|
|
1064
|
+
pushCandidate(match[1] ?? "");
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return candidates;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function resolveStudioLatexBibliographyPaths(markdown: string, baseDir: string | undefined): string[] {
|
|
1071
|
+
const workingDir = resolveStudioPandocWorkingDir(baseDir);
|
|
1072
|
+
if (!workingDir) return [];
|
|
1073
|
+
const resolvedPaths: string[] = [];
|
|
1074
|
+
const seen = new Set<string>();
|
|
1075
|
+
|
|
1076
|
+
for (const candidate of collectStudioLatexBibliographyCandidates(markdown)) {
|
|
1077
|
+
const expanded = expandHome(candidate);
|
|
1078
|
+
const resolvedPath = isAbsolute(expanded) ? expanded : resolve(workingDir, expanded);
|
|
1079
|
+
try {
|
|
1080
|
+
if (!statSync(resolvedPath).isFile()) continue;
|
|
1081
|
+
if (seen.has(resolvedPath)) continue;
|
|
1082
|
+
seen.add(resolvedPath);
|
|
1083
|
+
resolvedPaths.push(resolvedPath);
|
|
1084
|
+
} catch {
|
|
1085
|
+
// Ignore missing bibliography files; pandoc can still render the document body.
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return resolvedPaths;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function buildStudioPandocBibliographyArgs(markdown: string, isLatex: boolean | undefined, baseDir: string | undefined): string[] {
|
|
1093
|
+
if (!isLatex) return [];
|
|
1094
|
+
const bibliographyPaths = resolveStudioLatexBibliographyPaths(markdown, baseDir);
|
|
1095
|
+
if (bibliographyPaths.length === 0) return [];
|
|
1096
|
+
return [
|
|
1097
|
+
"--citeproc",
|
|
1098
|
+
...bibliographyPaths.flatMap((path) => ["--bibliography", path]),
|
|
1099
|
+
];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1011
1102
|
function readStudioGitDiff(baseDir: string):
|
|
1012
1103
|
| { ok: true; text: string; label: string }
|
|
1013
1104
|
| { ok: false; level: "info" | "warning" | "error"; message: string } {
|
|
@@ -1593,16 +1684,18 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
|
|
|
1593
1684
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string): Promise<string> {
|
|
1594
1685
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
1595
1686
|
const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
1596
|
-
const
|
|
1687
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
1688
|
+
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
1597
1689
|
if (resourcePath) {
|
|
1598
1690
|
args.push(`--resource-path=${resourcePath}`);
|
|
1599
1691
|
// Embed images as data URIs so they render in the browser preview
|
|
1600
1692
|
args.push("--embed-resources", "--standalone");
|
|
1601
1693
|
}
|
|
1602
1694
|
const normalizedMarkdown = isLatex ? markdown : normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
1695
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
1603
1696
|
|
|
1604
1697
|
return await new Promise<string>((resolve, reject) => {
|
|
1605
|
-
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1698
|
+
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
1606
1699
|
const stdoutChunks: Buffer[] = [];
|
|
1607
1700
|
const stderrChunks: Buffer[] = [];
|
|
1608
1701
|
let settled = false;
|
|
@@ -1743,6 +1836,8 @@ async function renderStudioPdfWithPandoc(
|
|
|
1743
1836
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
1744
1837
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
1745
1838
|
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorPdfLanguage);
|
|
1839
|
+
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
1840
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
1746
1841
|
|
|
1747
1842
|
const runPandocPdfExport = async (
|
|
1748
1843
|
inputFormat: string,
|
|
@@ -1766,12 +1861,13 @@ async function renderStudioPdfWithPandoc(
|
|
|
1766
1861
|
"-V", "urlcolor=blue",
|
|
1767
1862
|
"-V", "linkcolor=blue",
|
|
1768
1863
|
"--include-in-header", preamblePath,
|
|
1864
|
+
...bibliographyArgs,
|
|
1769
1865
|
];
|
|
1770
1866
|
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
1771
1867
|
|
|
1772
1868
|
try {
|
|
1773
1869
|
await new Promise<void>((resolve, reject) => {
|
|
1774
|
-
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1870
|
+
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
1775
1871
|
const stderrChunks: Buffer[] = [];
|
|
1776
1872
|
let settled = false;
|
|
1777
1873
|
|
|
@@ -1862,12 +1958,13 @@ async function renderStudioPdfWithPandoc(
|
|
|
1862
1958
|
"-V", "urlcolor=blue",
|
|
1863
1959
|
"-V", "linkcolor=blue",
|
|
1864
1960
|
"--include-in-header", preamblePath,
|
|
1961
|
+
...bibliographyArgs,
|
|
1865
1962
|
];
|
|
1866
1963
|
if (resourcePath) args.push(`--resource-path=${resourcePath}`);
|
|
1867
1964
|
|
|
1868
1965
|
try {
|
|
1869
1966
|
await new Promise<void>((resolve, reject) => {
|
|
1870
|
-
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1967
|
+
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
1871
1968
|
const stderrChunks: Buffer[] = [];
|
|
1872
1969
|
let settled = false;
|
|
1873
1970
|
|
|
@@ -4054,7 +4151,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4054
4151
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
4055
4152
|
? (parsedBody as { resourceDir: string }).resourceDir
|
|
4056
4153
|
: "";
|
|
4057
|
-
const resourcePath = sourcePath
|
|
4154
|
+
const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
|
|
4058
4155
|
const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
|
|
4059
4156
|
const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath);
|
|
4060
4157
|
respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
|
|
@@ -4108,7 +4205,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4108
4205
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
4109
4206
|
? (parsedBody as { resourceDir: string }).resourceDir
|
|
4110
4207
|
: "";
|
|
4111
|
-
const resourcePath = sourcePath
|
|
4208
|
+
const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
|
|
4112
4209
|
const requestedIsLatex =
|
|
4113
4210
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { isLatex?: unknown }).isLatex === "boolean"
|
|
4114
4211
|
? (parsedBody as { isLatex: boolean }).isLatex
|