pi-studio 0.9.47 → 0.9.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.47",
3
+ "version": "0.9.49",
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",
@@ -0,0 +1,122 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ const STUDIO_LATEX_LOCAL_STYLE_MAX_BYTES = 2_000_000;
5
+ const STUDIO_LATEX_PACKAGE_PATTERN = /\\(usepackage|RequirePackage)(\s*(?:\[[^\]]*\]\s*)?)\{([^{}]+)\}/g;
6
+ const STUDIO_LATEX_PACKAGE_NAME_PATTERN = /^[A-Za-z0-9._/+:-]+$/;
7
+
8
+ function stripStudioLatexStyleComments(source) {
9
+ return String(source || "")
10
+ .replace(/\r\n/g, "\n")
11
+ .split("\n")
12
+ .map((line) => {
13
+ let out = "";
14
+ let backslashRun = 0;
15
+ for (const ch of line) {
16
+ if (ch === "%" && backslashRun % 2 === 0) break;
17
+ out += ch;
18
+ if (ch === "\\") backslashRun += 1;
19
+ else backslashRun = 0;
20
+ }
21
+ return out;
22
+ })
23
+ .join("\n");
24
+ }
25
+
26
+ export function doesStudioLatexStyleOverrideDocumentStartup(source) {
27
+ const uncommented = stripStudioLatexStyleComments(source);
28
+ return [
29
+ /\\(?:renewcommand|newcommand|providecommand)\s*\*?\s*(?:\{\s*\\document\s*\}|\\document\b)/,
30
+ /\\(?:def|gdef|edef|xdef)\s*\\document(?:\s|#|\{)/,
31
+ /\\let\s*\\document\s*(?:=\s*)?\\?[A-Za-z@]+/,
32
+ /\\(?:renewenvironment|newenvironment)\s*\*?\s*\{\s*document\s*\}/,
33
+ ].some((pattern) => pattern.test(uncommented));
34
+ }
35
+
36
+ function resolveStudioLocalLatexStyle(packageName, baseDir) {
37
+ const normalizedName = String(packageName || "").trim();
38
+ if (!normalizedName || !STUDIO_LATEX_PACKAGE_NAME_PATTERN.test(normalizedName)) return null;
39
+ const relativeStylePath = normalizedName.toLowerCase().endsWith(".sty")
40
+ ? normalizedName
41
+ : `${normalizedName}.sty`;
42
+ const stylePath = resolve(baseDir, relativeStylePath);
43
+ try {
44
+ const info = statSync(stylePath);
45
+ if (!info.isFile() || info.size > STUDIO_LATEX_LOCAL_STYLE_MAX_BYTES) return null;
46
+ return stylePath;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function isStudioPandocIncompatibleLocalStyle(packageName, baseDir) {
53
+ const stylePath = resolveStudioLocalLatexStyle(packageName, baseDir);
54
+ if (!stylePath) return null;
55
+ try {
56
+ const styleSource = readFileSync(stylePath, "utf8");
57
+ return doesStudioLatexStyleOverrideDocumentStartup(styleSource) ? stylePath : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function isStudioLatexSourceOffsetCommented(source, offset) {
64
+ const lineStart = source.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
65
+ let backslashRun = 0;
66
+ for (let index = lineStart; index < offset; index += 1) {
67
+ const ch = source[index];
68
+ if (ch === "%" && backslashRun % 2 === 0) return true;
69
+ if (ch === "\\") backslashRun += 1;
70
+ else backslashRun = 0;
71
+ }
72
+ return false;
73
+ }
74
+
75
+ function findStudioLatexDocumentStart(source) {
76
+ const pattern = /\\begin\s*\{document\}/g;
77
+ for (;;) {
78
+ const match = pattern.exec(source);
79
+ if (!match) return source.length;
80
+ if (!isStudioLatexSourceOffsetCommented(source, match.index)) return match.index;
81
+ }
82
+ }
83
+
84
+ export function prepareStudioLatexForPandoc(source, baseDir) {
85
+ const input = String(source || "");
86
+ const normalizedBaseDir = typeof baseDir === "string" ? baseDir.trim() : "";
87
+ if (!input || !normalizedBaseDir) {
88
+ return { source: input, omittedPackages: [] };
89
+ }
90
+
91
+ const omittedPackages = [];
92
+ const documentStart = findStudioLatexDocumentStart(input);
93
+ const preparedSource = input.replace(
94
+ STUDIO_LATEX_PACKAGE_PATTERN,
95
+ (match, command, optionText, packageList, offset) => {
96
+ if (offset >= documentStart || isStudioLatexSourceOffsetCommented(input, offset)) return match;
97
+ const packageNames = String(packageList || "")
98
+ .split(",")
99
+ .map((name) => name.trim())
100
+ .filter(Boolean);
101
+ if (packageNames.length === 0) return match;
102
+
103
+ const retainedPackages = [];
104
+ for (const packageName of packageNames) {
105
+ const stylePath = isStudioPandocIncompatibleLocalStyle(packageName, normalizedBaseDir);
106
+ if (!stylePath) {
107
+ retainedPackages.push(packageName);
108
+ continue;
109
+ }
110
+ if (!omittedPackages.some((entry) => entry.name === packageName && entry.path === stylePath)) {
111
+ omittedPackages.push({ name: packageName, path: stylePath });
112
+ }
113
+ }
114
+
115
+ if (retainedPackages.length === packageNames.length) return match;
116
+ if (retainedPackages.length === 0) return "\\relax{}";
117
+ return `\\${command}${optionText || ""}{${retainedPackages.join(",")}}`;
118
+ },
119
+ );
120
+
121
+ return { source: preparedSource, omittedPackages };
122
+ }
@@ -0,0 +1,46 @@
1
+ function parseStudioLocalPreviewPage(resourcePath) {
2
+ const raw = String(resourcePath || "");
3
+ const parts = [];
4
+ const queryIndex = raw.indexOf("?");
5
+ if (queryIndex >= 0) {
6
+ const queryEnd = raw.indexOf("#", queryIndex);
7
+ parts.push(raw.slice(queryIndex + 1, queryEnd >= 0 ? queryEnd : raw.length));
8
+ }
9
+ const hashIndex = raw.indexOf("#");
10
+ if (hashIndex >= 0) parts.push(raw.slice(hashIndex + 1));
11
+ for (const part of parts) {
12
+ try {
13
+ const params = new URLSearchParams(part);
14
+ const rawPage = params.get("page") || params.get("p");
15
+ if (rawPage) {
16
+ const page = Number.parseInt(rawPage, 10);
17
+ if (Number.isFinite(page) && page > 0) return page;
18
+ }
19
+ } catch {
20
+ const match = part.match(/(?:^|[&;])page=(\d+)/i) || part.match(/^page=(\d+)$/i);
21
+ if (match && match[1]) {
22
+ const page = Number.parseInt(match[1], 10);
23
+ if (Number.isFinite(page) && page > 0) return page;
24
+ }
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function parseStudioPdfLaunchTarget(pathInput) {
31
+ const raw = String(pathInput || "").trim();
32
+ if (!raw || /\0/.test(raw) || /^\/\//.test(raw)) return null;
33
+ if (/^[a-z][a-z0-9+.-]*:/i.test(raw) && !/^[a-z]:[\\/]/i.test(raw)) return null;
34
+
35
+ const match = raw.match(/^(.*?\.pdf)(?:(?:\?[^#]*)?(?:#.*)?)?$/i);
36
+ if (!match || !match[1]) return null;
37
+ return {
38
+ path: match[1],
39
+ page: parseStudioLocalPreviewPage(raw),
40
+ };
41
+ }
42
+
43
+ export {
44
+ parseStudioLocalPreviewPage,
45
+ parseStudioPdfLaunchTarget,
46
+ };