pi-studio 0.5.31 → 0.5.32
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 +1 -1
- package/client/studio-client.js +207 -1
- package/client/studio.css +158 -0
- package/index.ts +876 -51
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -199,17 +199,48 @@ const CMUX_STUDIO_STATUS_COLOR_DARK = "#5ea1ff";
|
|
|
199
199
|
const CMUX_STUDIO_STATUS_COLOR_LIGHT = "#0047ab";
|
|
200
200
|
const STUDIO_PROMPT_METADATA_CUSTOM_TYPE = "pi-studio/direct-prompt";
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
function scaleStudioPdfLength(length: string, factor: number): string | null {
|
|
203
|
+
const match = String(length ?? "").trim().match(/^(\d+(?:\.\d+)?)(pt|bp|mm|cm|in|pc)$/i);
|
|
204
|
+
if (!match) return null;
|
|
205
|
+
const value = Number(match[1]);
|
|
206
|
+
if (!Number.isFinite(value)) return null;
|
|
207
|
+
const scaled = value * factor;
|
|
208
|
+
const formatted = Number.isInteger(scaled) ? String(scaled) : scaled.toFixed(2).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
|
|
209
|
+
return `${formatted}${match[2]}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildStudioPdfHeadingSizeCommand(size: string | undefined, fallback: string): string {
|
|
213
|
+
const trimmed = String(size ?? "").trim();
|
|
214
|
+
if (!trimmed) return fallback;
|
|
215
|
+
const lineHeight = scaleStudioPdfLength(trimmed, 1.2) ?? trimmed;
|
|
216
|
+
return `\\fontsize{${trimmed}}{${lineHeight}}\\selectfont`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildStudioPdfTitleSpacingLength(value: string | undefined, fallback: string): string {
|
|
220
|
+
const trimmed = String(value ?? "").trim();
|
|
221
|
+
return trimmed || fallback;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
|
|
225
|
+
const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
|
|
226
|
+
const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
|
|
227
|
+
const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
|
|
228
|
+
const sectionSpaceBefore = buildStudioPdfTitleSpacingLength(options?.sectionSpaceBefore, "1.5ex plus 0.5ex minus 0.2ex");
|
|
229
|
+
const sectionSpaceAfter = buildStudioPdfTitleSpacingLength(options?.sectionSpaceAfter, "1ex plus 0.2ex");
|
|
230
|
+
const subsectionSpaceBefore = buildStudioPdfTitleSpacingLength(options?.subsectionSpaceBefore, "1.2ex plus 0.4ex minus 0.2ex");
|
|
231
|
+
const subsectionSpaceAfter = buildStudioPdfTitleSpacingLength(options?.subsectionSpaceAfter, "0.6ex plus 0.1ex");
|
|
232
|
+
return `\\usepackage{titlesec}
|
|
233
|
+
\\titleformat{\\section}{${sectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
|
|
234
|
+
\\titleformat{\\subsection}{${subsectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}
|
|
235
|
+
\\titleformat{\\subsubsection}{${subsubsectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}
|
|
236
|
+
\\titlespacing*{\\section}{0pt}{${sectionSpaceBefore}}{${sectionSpaceAfter}}
|
|
237
|
+
\\titlespacing*{\\subsection}{0pt}{${subsectionSpaceBefore}}{${subsectionSpaceAfter}}
|
|
208
238
|
\\usepackage{xcolor}
|
|
209
239
|
\\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
|
|
210
240
|
\\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
|
|
211
241
|
\\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
|
|
212
242
|
\\newcommand{\\studioannotation}[1]{\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}}\\endgroup}
|
|
243
|
+
\\newenvironment{studiocallout}[1]{\\par\\vspace{0.22em}\\noindent\\begingroup\\color{StudioAnnotationBorder}\\hrule height 0.45pt\\color{black}\\vspace{0.08em}\\noindent{\\sffamily\\bfseries\\textcolor{StudioAnnotationText}{#1}}\\par\\vspace{0.02em}\\leftskip=0.7em\\rightskip=0pt\\parindent=0pt\\parskip=0.15em}{\\par\\vspace{0.02em}\\noindent\\color{StudioAnnotationBorder}\\hrule height 0.45pt\\par\\endgroup\\vspace{0.22em}}
|
|
213
244
|
\\usepackage{caption}
|
|
214
245
|
\\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
|
|
215
246
|
\\usepackage{enumitem}
|
|
@@ -225,6 +256,7 @@ const PDF_PREAMBLE = `\\usepackage{titlesec}
|
|
|
225
256
|
}
|
|
226
257
|
\\makeatother
|
|
227
258
|
`;
|
|
259
|
+
}
|
|
228
260
|
|
|
229
261
|
type StudioThemeMode = "dark" | "light";
|
|
230
262
|
|
|
@@ -886,6 +918,53 @@ function parsePathArgument(args: string): string | null {
|
|
|
886
918
|
return trimmed;
|
|
887
919
|
}
|
|
888
920
|
|
|
921
|
+
function tokenizeStudioCommandArgs(input: string): { tokens: string[]; error?: string } {
|
|
922
|
+
const tokens: string[] = [];
|
|
923
|
+
let current = "";
|
|
924
|
+
let quote: '"' | "'" | null = null;
|
|
925
|
+
|
|
926
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
927
|
+
const ch = input[i]!;
|
|
928
|
+
if (quote) {
|
|
929
|
+
if (ch === "\\" && i + 1 < input.length) {
|
|
930
|
+
const next = input[i + 1]!;
|
|
931
|
+
if (next === quote || next === "\\") {
|
|
932
|
+
current += next;
|
|
933
|
+
i += 1;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (ch === quote) {
|
|
938
|
+
quote = null;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
current += ch;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (ch === '"' || ch === "'") {
|
|
946
|
+
quote = ch;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (/\s/.test(ch)) {
|
|
951
|
+
if (current) {
|
|
952
|
+
tokens.push(current);
|
|
953
|
+
current = "";
|
|
954
|
+
}
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
current += ch;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (quote) {
|
|
962
|
+
return { tokens, error: "Unterminated quoted argument." };
|
|
963
|
+
}
|
|
964
|
+
if (current) tokens.push(current);
|
|
965
|
+
return { tokens };
|
|
966
|
+
}
|
|
967
|
+
|
|
889
968
|
function normalizePathInput(pathInput: string): string {
|
|
890
969
|
const trimmed = pathInput.trim();
|
|
891
970
|
if (trimmed.startsWith("@")) return trimmed.slice(1).trim();
|
|
@@ -960,7 +1039,7 @@ function readStudioFile(pathArg: string, cwd: string):
|
|
|
960
1039
|
function inferStudioPdfLanguageFromPath(pathInput: string): string | undefined {
|
|
961
1040
|
const extension = extname(pathInput).toLowerCase();
|
|
962
1041
|
if (extension === ".tex" || extension === ".latex") return "latex";
|
|
963
|
-
if (extension === ".md" || extension === ".markdown" || extension === ".mdx") return "markdown";
|
|
1042
|
+
if (extension === ".md" || extension === ".markdown" || extension === ".mdx" || extension === ".qmd") return "markdown";
|
|
964
1043
|
if (extension === ".diff" || extension === ".patch") return "diff";
|
|
965
1044
|
return undefined;
|
|
966
1045
|
}
|
|
@@ -2563,6 +2642,187 @@ function normalizeMathDelimiters(markdown: string): string {
|
|
|
2563
2642
|
return out.join("\n");
|
|
2564
2643
|
}
|
|
2565
2644
|
|
|
2645
|
+
function stripStudioMarkdownHtmlCommentsInSegment(markdown: string): string {
|
|
2646
|
+
const source = String(markdown ?? "");
|
|
2647
|
+
let out = "";
|
|
2648
|
+
let i = 0;
|
|
2649
|
+
let codeSpanFenceLength = 0;
|
|
2650
|
+
let inHtmlComment = false;
|
|
2651
|
+
|
|
2652
|
+
while (i < source.length) {
|
|
2653
|
+
if (inHtmlComment) {
|
|
2654
|
+
if (source.startsWith("-->", i)) {
|
|
2655
|
+
inHtmlComment = false;
|
|
2656
|
+
i += 3;
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
const ch = source[i]!;
|
|
2660
|
+
if (ch === "\n" || ch === "\r") out += ch;
|
|
2661
|
+
i += 1;
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
if (codeSpanFenceLength > 0) {
|
|
2666
|
+
const fence = "`".repeat(codeSpanFenceLength);
|
|
2667
|
+
if (source.startsWith(fence, i)) {
|
|
2668
|
+
out += fence;
|
|
2669
|
+
i += codeSpanFenceLength;
|
|
2670
|
+
codeSpanFenceLength = 0;
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
out += source[i]!;
|
|
2674
|
+
i += 1;
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
const backtickMatch = source.slice(i).match(/^`+/);
|
|
2679
|
+
if (backtickMatch) {
|
|
2680
|
+
const fence = backtickMatch[0]!;
|
|
2681
|
+
codeSpanFenceLength = fence.length;
|
|
2682
|
+
out += fence;
|
|
2683
|
+
i += fence.length;
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (source.startsWith("<!--", i)) {
|
|
2688
|
+
inHtmlComment = true;
|
|
2689
|
+
i += 4;
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
out += source[i]!;
|
|
2694
|
+
i += 1;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
return out;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function stripStudioMarkdownHtmlComments(markdown: string): string {
|
|
2701
|
+
const lines = String(markdown ?? "").split("\n");
|
|
2702
|
+
const out: string[] = [];
|
|
2703
|
+
let plainBuffer: string[] = [];
|
|
2704
|
+
let inFence = false;
|
|
2705
|
+
let fenceChar: "`" | "~" | undefined;
|
|
2706
|
+
let fenceLength = 0;
|
|
2707
|
+
|
|
2708
|
+
const flushPlain = () => {
|
|
2709
|
+
if (plainBuffer.length === 0) return;
|
|
2710
|
+
out.push(stripStudioMarkdownHtmlCommentsInSegment(plainBuffer.join("\n")));
|
|
2711
|
+
plainBuffer = [];
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2714
|
+
for (const line of lines) {
|
|
2715
|
+
const trimmed = line.trimStart();
|
|
2716
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
2717
|
+
|
|
2718
|
+
if (fenceMatch) {
|
|
2719
|
+
const marker = fenceMatch[1]!;
|
|
2720
|
+
const markerChar = marker[0] as "`" | "~";
|
|
2721
|
+
const markerLength = marker.length;
|
|
2722
|
+
|
|
2723
|
+
if (!inFence) {
|
|
2724
|
+
flushPlain();
|
|
2725
|
+
inFence = true;
|
|
2726
|
+
fenceChar = markerChar;
|
|
2727
|
+
fenceLength = markerLength;
|
|
2728
|
+
out.push(line);
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
2733
|
+
inFence = false;
|
|
2734
|
+
fenceChar = undefined;
|
|
2735
|
+
fenceLength = 0;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
out.push(line);
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
if (inFence) {
|
|
2743
|
+
out.push(line);
|
|
2744
|
+
} else {
|
|
2745
|
+
plainBuffer.push(line);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
flushPlain();
|
|
2750
|
+
return out.join("\n");
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
const STUDIO_PREVIEW_PAGE_BREAK_SENTINEL_PREFIX = "PI_STUDIO_PAGE_BREAK__";
|
|
2754
|
+
|
|
2755
|
+
function replaceStudioPreviewPageBreakCommands(markdown: string): string {
|
|
2756
|
+
const lines = String(markdown ?? "").split("\n");
|
|
2757
|
+
const out: string[] = [];
|
|
2758
|
+
let plainBuffer: string[] = [];
|
|
2759
|
+
let inFence = false;
|
|
2760
|
+
let fenceChar: "`" | "~" | undefined;
|
|
2761
|
+
let fenceLength = 0;
|
|
2762
|
+
|
|
2763
|
+
const flushPlain = () => {
|
|
2764
|
+
if (plainBuffer.length === 0) return;
|
|
2765
|
+
out.push(
|
|
2766
|
+
plainBuffer.map((line) => {
|
|
2767
|
+
const match = line.trim().match(/^\\(newpage|pagebreak|clearpage)(?:\s*\[[^\]]*\])?\s*$/i);
|
|
2768
|
+
if (!match) return line;
|
|
2769
|
+
const command = match[1]!.toLowerCase();
|
|
2770
|
+
return `${STUDIO_PREVIEW_PAGE_BREAK_SENTINEL_PREFIX}${command.toUpperCase()}__`;
|
|
2771
|
+
}).join("\n"),
|
|
2772
|
+
);
|
|
2773
|
+
plainBuffer = [];
|
|
2774
|
+
};
|
|
2775
|
+
|
|
2776
|
+
for (const line of lines) {
|
|
2777
|
+
const trimmed = line.trimStart();
|
|
2778
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
2779
|
+
|
|
2780
|
+
if (fenceMatch) {
|
|
2781
|
+
const marker = fenceMatch[1]!;
|
|
2782
|
+
const markerChar = marker[0] as "`" | "~";
|
|
2783
|
+
const markerLength = marker.length;
|
|
2784
|
+
|
|
2785
|
+
if (!inFence) {
|
|
2786
|
+
flushPlain();
|
|
2787
|
+
inFence = true;
|
|
2788
|
+
fenceChar = markerChar;
|
|
2789
|
+
fenceLength = markerLength;
|
|
2790
|
+
out.push(line);
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
2795
|
+
inFence = false;
|
|
2796
|
+
fenceChar = undefined;
|
|
2797
|
+
fenceLength = 0;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
out.push(line);
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
if (inFence) {
|
|
2805
|
+
out.push(line);
|
|
2806
|
+
} else {
|
|
2807
|
+
plainBuffer.push(line);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
flushPlain();
|
|
2812
|
+
return out.join("\n");
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function decorateStudioPreviewPageBreakHtml(html: string): string {
|
|
2816
|
+
return String(html ?? "").replace(
|
|
2817
|
+
new RegExp(`<p>${STUDIO_PREVIEW_PAGE_BREAK_SENTINEL_PREFIX}(NEWPAGE|PAGEBREAK|CLEARPAGE)__<\\/p>`, "gi"),
|
|
2818
|
+
(_match, command: string) => {
|
|
2819
|
+
const normalized = String(command || "").toLowerCase();
|
|
2820
|
+
const label = normalized === "clearpage" ? "Clear page" : "Page break";
|
|
2821
|
+
return `<div class="studio-page-break" data-page-break-kind="${normalized}"><span class="studio-page-break-rule" aria-hidden="true"></span><span class="studio-page-break-label">${escapeStudioHtmlText(label)}</span><span class="studio-page-break-rule" aria-hidden="true"></span></div>`;
|
|
2822
|
+
},
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2566
2826
|
function normalizeStudioEditorLanguage(language: string | undefined): string | undefined {
|
|
2567
2827
|
const trimmed = typeof language === "string" ? language.trim().toLowerCase() : "";
|
|
2568
2828
|
if (!trimmed) return undefined;
|
|
@@ -2733,6 +2993,515 @@ function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
|
|
|
2733
2993
|
return out.join("\n");
|
|
2734
2994
|
}
|
|
2735
2995
|
|
|
2996
|
+
interface StudioPdfRenderOptions {
|
|
2997
|
+
fontsize?: string;
|
|
2998
|
+
margin?: string;
|
|
2999
|
+
marginTop?: string;
|
|
3000
|
+
marginRight?: string;
|
|
3001
|
+
marginBottom?: string;
|
|
3002
|
+
marginLeft?: string;
|
|
3003
|
+
footskip?: string;
|
|
3004
|
+
linestretch?: string;
|
|
3005
|
+
mainfont?: string;
|
|
3006
|
+
papersize?: string;
|
|
3007
|
+
geometry?: string;
|
|
3008
|
+
sectionSize?: string;
|
|
3009
|
+
subsectionSize?: string;
|
|
3010
|
+
subsubsectionSize?: string;
|
|
3011
|
+
sectionSpaceBefore?: string;
|
|
3012
|
+
sectionSpaceAfter?: string;
|
|
3013
|
+
subsectionSpaceBefore?: string;
|
|
3014
|
+
subsectionSpaceAfter?: string;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
interface StudioParsedPdfCommandArgs {
|
|
3018
|
+
pathArg: string;
|
|
3019
|
+
options: StudioPdfRenderOptions;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
interface StudioPdfMarkdownCalloutBlock {
|
|
3023
|
+
kind: "note" | "tip" | "warning" | "important" | "caution";
|
|
3024
|
+
markerId: number;
|
|
3025
|
+
content: string;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function parseStudioFencedDivOpenLine(line: string): { markerLength: number; info: string } | null {
|
|
3029
|
+
const trimmed = String(line ?? "").trim();
|
|
3030
|
+
const match = trimmed.match(/^(:{3,})(.+)$/);
|
|
3031
|
+
if (!match) return null;
|
|
3032
|
+
const info = String(match[2] ?? "").trim();
|
|
3033
|
+
if (!info) return null;
|
|
3034
|
+
return {
|
|
3035
|
+
markerLength: match[1]!.length,
|
|
3036
|
+
info,
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function parseStudioPdfCalloutStartLine(line: string): { markerLength: number; kind: StudioPdfMarkdownCalloutBlock["kind"] } | null {
|
|
3041
|
+
const open = parseStudioFencedDivOpenLine(line);
|
|
3042
|
+
if (!open) return null;
|
|
3043
|
+
const kindMatch = open.info.match(/(?:^|[\s{])\.callout-(note|tip|warning|important|caution)(?=[\s}]|$)/i);
|
|
3044
|
+
if (!kindMatch) return null;
|
|
3045
|
+
return {
|
|
3046
|
+
markerLength: open.markerLength,
|
|
3047
|
+
kind: kindMatch[1]!.toLowerCase() as StudioPdfMarkdownCalloutBlock["kind"],
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function preprocessStudioMarkdownCalloutsForPdf(markdown: string): { markdown: string; blocks: StudioPdfMarkdownCalloutBlock[] } {
|
|
3052
|
+
const lines = String(markdown ?? "").split("\n");
|
|
3053
|
+
const out: string[] = [];
|
|
3054
|
+
const blocks: StudioPdfMarkdownCalloutBlock[] = [];
|
|
3055
|
+
let inFence = false;
|
|
3056
|
+
let fenceChar: "`" | "~" | undefined;
|
|
3057
|
+
let fenceLength = 0;
|
|
3058
|
+
let markerId = 0;
|
|
3059
|
+
|
|
3060
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3061
|
+
const line = lines[i] ?? "";
|
|
3062
|
+
const trimmed = line.trimStart();
|
|
3063
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
3064
|
+
if (fenceMatch) {
|
|
3065
|
+
const marker = fenceMatch[1]!;
|
|
3066
|
+
const markerChar = marker[0] as "`" | "~";
|
|
3067
|
+
const markerLength = marker.length;
|
|
3068
|
+
if (!inFence) {
|
|
3069
|
+
inFence = true;
|
|
3070
|
+
fenceChar = markerChar;
|
|
3071
|
+
fenceLength = markerLength;
|
|
3072
|
+
out.push(line);
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
3076
|
+
inFence = false;
|
|
3077
|
+
fenceChar = undefined;
|
|
3078
|
+
fenceLength = 0;
|
|
3079
|
+
}
|
|
3080
|
+
out.push(line);
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
if (inFence) {
|
|
3084
|
+
out.push(line);
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
const calloutStart = parseStudioPdfCalloutStartLine(line);
|
|
3089
|
+
if (!calloutStart) {
|
|
3090
|
+
out.push(line);
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
const contentLines: string[] = [];
|
|
3095
|
+
let innerInFence = false;
|
|
3096
|
+
let innerFenceChar: "`" | "~" | undefined;
|
|
3097
|
+
let innerFenceLength = 0;
|
|
3098
|
+
let nestedDivDepth = 0;
|
|
3099
|
+
let closed = false;
|
|
3100
|
+
let j = i + 1;
|
|
3101
|
+
for (; j < lines.length; j += 1) {
|
|
3102
|
+
const innerLine = lines[j] ?? "";
|
|
3103
|
+
const innerTrimmed = innerLine.trimStart();
|
|
3104
|
+
const innerFenceMatch = innerTrimmed.match(/^(`{3,}|~{3,})/);
|
|
3105
|
+
if (innerFenceMatch) {
|
|
3106
|
+
const marker = innerFenceMatch[1]!;
|
|
3107
|
+
const markerChar = marker[0] as "`" | "~";
|
|
3108
|
+
const markerLength = marker.length;
|
|
3109
|
+
if (!innerInFence) {
|
|
3110
|
+
innerInFence = true;
|
|
3111
|
+
innerFenceChar = markerChar;
|
|
3112
|
+
innerFenceLength = markerLength;
|
|
3113
|
+
contentLines.push(innerLine);
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
if (innerFenceChar === markerChar && markerLength >= innerFenceLength) {
|
|
3117
|
+
innerInFence = false;
|
|
3118
|
+
innerFenceChar = undefined;
|
|
3119
|
+
innerFenceLength = 0;
|
|
3120
|
+
}
|
|
3121
|
+
contentLines.push(innerLine);
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (!innerInFence) {
|
|
3125
|
+
const nestedOpen = parseStudioFencedDivOpenLine(innerLine);
|
|
3126
|
+
if (nestedOpen) {
|
|
3127
|
+
nestedDivDepth += 1;
|
|
3128
|
+
contentLines.push(innerLine);
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
if (/^:{3,}\s*$/.test(innerLine.trim())) {
|
|
3132
|
+
if (nestedDivDepth > 0) {
|
|
3133
|
+
nestedDivDepth -= 1;
|
|
3134
|
+
contentLines.push(innerLine);
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
closed = true;
|
|
3138
|
+
break;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
contentLines.push(innerLine);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
if (!closed) {
|
|
3145
|
+
out.push(line);
|
|
3146
|
+
out.push(...contentLines);
|
|
3147
|
+
i = j - 1;
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
const block: StudioPdfMarkdownCalloutBlock = {
|
|
3152
|
+
kind: calloutStart.kind,
|
|
3153
|
+
markerId: markerId += 1,
|
|
3154
|
+
content: contentLines.join("\n").trim(),
|
|
3155
|
+
};
|
|
3156
|
+
blocks.push(block);
|
|
3157
|
+
out.push(`PISTUDIOPDFCALLOUTSTART${block.kind.toUpperCase()}${block.markerId}`);
|
|
3158
|
+
if (block.content) out.push(block.content);
|
|
3159
|
+
out.push(`PISTUDIOPDFCALLOUTEND${block.kind.toUpperCase()}${block.markerId}`);
|
|
3160
|
+
i = j;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
return { markdown: out.join("\n"), blocks };
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
interface StudioPdfAlignedImageBlock {
|
|
3167
|
+
align: "center" | "right";
|
|
3168
|
+
markerId: number;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function preprocessStudioMarkdownImageAlignmentForPdf(markdown: string): { markdown: string; blocks: StudioPdfAlignedImageBlock[] } {
|
|
3172
|
+
const lines = String(markdown ?? "").split("\n");
|
|
3173
|
+
const out: string[] = [];
|
|
3174
|
+
const blocks: StudioPdfAlignedImageBlock[] = [];
|
|
3175
|
+
let inFence = false;
|
|
3176
|
+
let fenceChar: "`" | "~" | undefined;
|
|
3177
|
+
let fenceLength = 0;
|
|
3178
|
+
let markerId = 0;
|
|
3179
|
+
|
|
3180
|
+
for (const line of lines) {
|
|
3181
|
+
const trimmed = line.trimStart();
|
|
3182
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
3183
|
+
if (fenceMatch) {
|
|
3184
|
+
const marker = fenceMatch[1]!;
|
|
3185
|
+
const markerChar = marker[0] as "`" | "~";
|
|
3186
|
+
const markerLength = marker.length;
|
|
3187
|
+
if (!inFence) {
|
|
3188
|
+
inFence = true;
|
|
3189
|
+
fenceChar = markerChar;
|
|
3190
|
+
fenceLength = markerLength;
|
|
3191
|
+
out.push(line);
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
3195
|
+
inFence = false;
|
|
3196
|
+
fenceChar = undefined;
|
|
3197
|
+
fenceLength = 0;
|
|
3198
|
+
}
|
|
3199
|
+
out.push(line);
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
if (inFence) {
|
|
3203
|
+
out.push(line);
|
|
3204
|
+
continue;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
const imageMatch = line.trim().match(/^!\[[^\]]*\]\((?:<[^>]+>|[^)]+)\)(\{[^}]*\})\s*$/);
|
|
3208
|
+
if (!imageMatch) {
|
|
3209
|
+
out.push(line);
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
const attrs = imageMatch[1] ?? "";
|
|
3213
|
+
const alignMatch = attrs.match(/(?:^|\s)fig-align\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s}]+))/i);
|
|
3214
|
+
const alignValue = String(alignMatch?.[1] ?? alignMatch?.[2] ?? alignMatch?.[3] ?? "").trim().toLowerCase();
|
|
3215
|
+
if (alignValue !== "center" && alignValue !== "right") {
|
|
3216
|
+
out.push(line);
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
const block: StudioPdfAlignedImageBlock = {
|
|
3220
|
+
align: alignValue as StudioPdfAlignedImageBlock["align"],
|
|
3221
|
+
markerId: markerId += 1,
|
|
3222
|
+
};
|
|
3223
|
+
blocks.push(block);
|
|
3224
|
+
out.push(`PISTUDIOPDFALIGNSTART${block.align.toUpperCase()}${block.markerId}`);
|
|
3225
|
+
out.push(line);
|
|
3226
|
+
out.push(`PISTUDIOPDFALIGNEND${block.align.toUpperCase()}${block.markerId}`);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
return { markdown: out.join("\n"), blocks };
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
function replaceStudioPdfCalloutBlocksInGeneratedLatex(
|
|
3233
|
+
latex: string,
|
|
3234
|
+
blocks: StudioPdfMarkdownCalloutBlock[],
|
|
3235
|
+
): string {
|
|
3236
|
+
if (blocks.length === 0) return latex;
|
|
3237
|
+
let transformed = String(latex ?? "");
|
|
3238
|
+
for (const block of blocks) {
|
|
3239
|
+
const startMarker = `PISTUDIOPDFCALLOUTSTART${block.kind.toUpperCase()}${block.markerId}`;
|
|
3240
|
+
const endMarker = `PISTUDIOPDFCALLOUTEND${block.kind.toUpperCase()}${block.markerId}`;
|
|
3241
|
+
const startIndex = transformed.indexOf(startMarker);
|
|
3242
|
+
if (startIndex < 0) continue;
|
|
3243
|
+
const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
|
|
3244
|
+
if (endIndex < 0) continue;
|
|
3245
|
+
const inner = transformed.slice(startIndex + startMarker.length, endIndex).trim();
|
|
3246
|
+
const label = block.kind === "note"
|
|
3247
|
+
? "Note"
|
|
3248
|
+
: block.kind === "tip"
|
|
3249
|
+
? "Tip"
|
|
3250
|
+
: block.kind === "warning"
|
|
3251
|
+
? "Warning"
|
|
3252
|
+
: block.kind === "important"
|
|
3253
|
+
? "Important"
|
|
3254
|
+
: "Caution";
|
|
3255
|
+
const replacement = `\\begin{studiocallout}{${label}}\n${inner}\n\\end{studiocallout}`;
|
|
3256
|
+
transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
|
|
3257
|
+
}
|
|
3258
|
+
return transformed;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function replaceStudioPdfAlignedImageBlocksInGeneratedLatex(
|
|
3262
|
+
latex: string,
|
|
3263
|
+
blocks: StudioPdfAlignedImageBlock[],
|
|
3264
|
+
): string {
|
|
3265
|
+
if (blocks.length === 0) return latex;
|
|
3266
|
+
let transformed = String(latex ?? "");
|
|
3267
|
+
for (const block of blocks) {
|
|
3268
|
+
const startMarker = `PISTUDIOPDFALIGNSTART${block.align.toUpperCase()}${block.markerId}`;
|
|
3269
|
+
const endMarker = `PISTUDIOPDFALIGNEND${block.align.toUpperCase()}${block.markerId}`;
|
|
3270
|
+
const startIndex = transformed.indexOf(startMarker);
|
|
3271
|
+
if (startIndex < 0) continue;
|
|
3272
|
+
const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
|
|
3273
|
+
if (endIndex < 0) continue;
|
|
3274
|
+
const inner = transformed.slice(startIndex + startMarker.length, endIndex).trim();
|
|
3275
|
+
const env = block.align === "right" ? "flushright" : "center";
|
|
3276
|
+
const replacement = `\\begin{${env}}\n${inner}\n\\end{${env}}`;
|
|
3277
|
+
transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
|
|
3278
|
+
}
|
|
3279
|
+
return transformed;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function isValidStudioPdfLength(value: string): boolean {
|
|
3283
|
+
return /^\d+(?:\.\d+)?(?:pt|bp|mm|cm|in|pc)$/i.test(value.trim());
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
function isValidStudioPdfLineStretch(value: string): boolean {
|
|
3287
|
+
return /^\d+(?:\.\d+)?$/.test(value.trim());
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function isValidStudioPdfPaperSize(value: string): boolean {
|
|
3291
|
+
return /^[A-Za-z0-9-]+$/.test(value.trim());
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function sanitizeStudioPdfFreeformOption(value: string): string {
|
|
3295
|
+
return String(value ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
function parseStudioPdfCommandArgs(args: string): StudioParsedPdfCommandArgs | { error: string } {
|
|
3299
|
+
const parsed = tokenizeStudioCommandArgs(args);
|
|
3300
|
+
if (parsed.error) return { error: parsed.error };
|
|
3301
|
+
const tokens = parsed.tokens;
|
|
3302
|
+
if (tokens.length === 0) return { error: "Missing file path." };
|
|
3303
|
+
|
|
3304
|
+
const options: StudioPdfRenderOptions = {};
|
|
3305
|
+
let pathArg: string | null = null;
|
|
3306
|
+
|
|
3307
|
+
const takeValue = (flag: string, index: number): { value: string; nextIndex: number } | { error: string } => {
|
|
3308
|
+
if (index + 1 >= tokens.length) return { error: `Missing value for ${flag}.` };
|
|
3309
|
+
return { value: tokens[index + 1]!, nextIndex: index + 1 };
|
|
3310
|
+
};
|
|
3311
|
+
|
|
3312
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
3313
|
+
const token = tokens[i]!;
|
|
3314
|
+
if (!token.startsWith("-")) {
|
|
3315
|
+
if (pathArg !== null) return { error: `Unexpected extra argument: ${token}` };
|
|
3316
|
+
pathArg = token;
|
|
3317
|
+
continue;
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
if (!token.startsWith("--")) {
|
|
3321
|
+
return { error: `Unknown flag: ${token}` };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
const taken = takeValue(token, i);
|
|
3325
|
+
if ("error" in taken) return taken;
|
|
3326
|
+
const value = taken.value.trim();
|
|
3327
|
+
i = taken.nextIndex;
|
|
3328
|
+
if (!value) return { error: `Empty value for ${token}.` };
|
|
3329
|
+
|
|
3330
|
+
switch (token) {
|
|
3331
|
+
case "--fontsize":
|
|
3332
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --fontsize value. Example: 12pt" };
|
|
3333
|
+
options.fontsize = value;
|
|
3334
|
+
break;
|
|
3335
|
+
case "--section-size":
|
|
3336
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --section-size value. Example: 24pt" };
|
|
3337
|
+
options.sectionSize = value;
|
|
3338
|
+
break;
|
|
3339
|
+
case "--subsection-size":
|
|
3340
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --subsection-size value. Example: 18pt" };
|
|
3341
|
+
options.subsectionSize = value;
|
|
3342
|
+
break;
|
|
3343
|
+
case "--subsubsection-size":
|
|
3344
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --subsubsection-size value. Example: 14pt" };
|
|
3345
|
+
options.subsubsectionSize = value;
|
|
3346
|
+
break;
|
|
3347
|
+
case "--section-space-before":
|
|
3348
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --section-space-before value. Example: 10mm" };
|
|
3349
|
+
options.sectionSpaceBefore = value;
|
|
3350
|
+
break;
|
|
3351
|
+
case "--section-space-after":
|
|
3352
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --section-space-after value. Example: 6mm" };
|
|
3353
|
+
options.sectionSpaceAfter = value;
|
|
3354
|
+
break;
|
|
3355
|
+
case "--subsection-space-before":
|
|
3356
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --subsection-space-before value. Example: 8mm" };
|
|
3357
|
+
options.subsectionSpaceBefore = value;
|
|
3358
|
+
break;
|
|
3359
|
+
case "--subsection-space-after":
|
|
3360
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --subsection-space-after value. Example: 4mm" };
|
|
3361
|
+
options.subsectionSpaceAfter = value;
|
|
3362
|
+
break;
|
|
3363
|
+
case "--margin":
|
|
3364
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --margin value. Example: 25mm" };
|
|
3365
|
+
options.margin = value;
|
|
3366
|
+
break;
|
|
3367
|
+
case "--margin-top":
|
|
3368
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --margin-top value. Example: 30mm" };
|
|
3369
|
+
options.marginTop = value;
|
|
3370
|
+
break;
|
|
3371
|
+
case "--margin-right":
|
|
3372
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --margin-right value. Example: 25mm" };
|
|
3373
|
+
options.marginRight = value;
|
|
3374
|
+
break;
|
|
3375
|
+
case "--margin-bottom":
|
|
3376
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --margin-bottom value. Example: 30mm" };
|
|
3377
|
+
options.marginBottom = value;
|
|
3378
|
+
break;
|
|
3379
|
+
case "--margin-left":
|
|
3380
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --margin-left value. Example: 25mm" };
|
|
3381
|
+
options.marginLeft = value;
|
|
3382
|
+
break;
|
|
3383
|
+
case "--footskip":
|
|
3384
|
+
if (!isValidStudioPdfLength(value)) return { error: "Invalid --footskip value. Example: 12mm" };
|
|
3385
|
+
options.footskip = value;
|
|
3386
|
+
break;
|
|
3387
|
+
case "--linestretch":
|
|
3388
|
+
if (!isValidStudioPdfLineStretch(value)) return { error: "Invalid --linestretch value. Example: 1.2" };
|
|
3389
|
+
options.linestretch = value;
|
|
3390
|
+
break;
|
|
3391
|
+
case "--mainfont":
|
|
3392
|
+
options.mainfont = sanitizeStudioPdfFreeformOption(value);
|
|
3393
|
+
if (!options.mainfont) return { error: "Invalid --mainfont value." };
|
|
3394
|
+
break;
|
|
3395
|
+
case "--papersize":
|
|
3396
|
+
if (!isValidStudioPdfPaperSize(value)) return { error: "Invalid --papersize value. Example: a4" };
|
|
3397
|
+
options.papersize = value;
|
|
3398
|
+
break;
|
|
3399
|
+
case "--geometry":
|
|
3400
|
+
options.geometry = sanitizeStudioPdfFreeformOption(value);
|
|
3401
|
+
if (!options.geometry) return { error: "Invalid --geometry value." };
|
|
3402
|
+
break;
|
|
3403
|
+
default:
|
|
3404
|
+
return { error: `Unknown flag: ${token}` };
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
if (!pathArg) return { error: "Missing file path." };
|
|
3409
|
+
if (options.geometry && (options.margin || options.marginTop || options.marginRight || options.marginBottom || options.marginLeft || options.footskip)) {
|
|
3410
|
+
return { error: "Use either --geometry or the --margin/--margin-*/--footskip flags, not both." };
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
return { pathArg, options };
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function getStudioRequestedPdfFontsizePt(options?: StudioPdfRenderOptions): number | null {
|
|
3417
|
+
const raw = String(options?.fontsize ?? "").trim();
|
|
3418
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)pt$/i);
|
|
3419
|
+
if (!match) return null;
|
|
3420
|
+
const value = Number(match[1]);
|
|
3421
|
+
return Number.isFinite(value) ? value : null;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function shouldUseStudioAltMarkdownPdfDocumentClass(options?: StudioPdfRenderOptions): boolean {
|
|
3425
|
+
const sizePt = getStudioRequestedPdfFontsizePt(options);
|
|
3426
|
+
return Boolean(sizePt && sizePt > 12);
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
function getStudioDefaultPdfFootskip(options: StudioPdfRenderOptions | undefined, useAltClass: boolean): string | undefined {
|
|
3430
|
+
if (!useAltClass) return undefined;
|
|
3431
|
+
if (options?.geometry || options?.footskip) return undefined;
|
|
3432
|
+
return "12mm";
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
function buildStudioPdfPandocVariableArgs(options?: StudioPdfRenderOptions, allowAltDocumentClass = false): string[] {
|
|
3436
|
+
const resolved = options ?? {};
|
|
3437
|
+
const args: string[] = [];
|
|
3438
|
+
const useAltClass = allowAltDocumentClass && shouldUseStudioAltMarkdownPdfDocumentClass(resolved);
|
|
3439
|
+
const defaultFootskip = getStudioDefaultPdfFootskip(resolved, useAltClass);
|
|
3440
|
+
|
|
3441
|
+
if (useAltClass) {
|
|
3442
|
+
args.push("-V", "documentclass=scrartcl");
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
if (resolved.geometry) {
|
|
3446
|
+
args.push("-V", `geometry:${resolved.geometry}`);
|
|
3447
|
+
} else {
|
|
3448
|
+
args.push("-V", `geometry:margin=${resolved.margin ?? "2.2cm"}`);
|
|
3449
|
+
if (resolved.marginTop) args.push("-V", `geometry:top=${resolved.marginTop}`);
|
|
3450
|
+
if (resolved.marginRight) args.push("-V", `geometry:right=${resolved.marginRight}`);
|
|
3451
|
+
if (resolved.marginBottom) args.push("-V", `geometry:bottom=${resolved.marginBottom}`);
|
|
3452
|
+
if (resolved.marginLeft) args.push("-V", `geometry:left=${resolved.marginLeft}`);
|
|
3453
|
+
if (resolved.footskip) args.push("-V", `geometry:footskip=${resolved.footskip}`);
|
|
3454
|
+
else if (defaultFootskip) args.push("-V", `geometry:footskip=${defaultFootskip}`);
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
args.push("-V", `fontsize=${resolved.fontsize ?? "11pt"}`);
|
|
3458
|
+
args.push("-V", `linestretch=${resolved.linestretch ?? "1.25"}`);
|
|
3459
|
+
if (resolved.mainfont) args.push("-V", `mainfont=${resolved.mainfont}`);
|
|
3460
|
+
if (resolved.papersize) args.push("-V", `papersize=${resolved.papersize}`);
|
|
3461
|
+
return args;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
function buildStudioLiteralTextPdfTexConfig(options?: StudioPdfRenderOptions): {
|
|
3465
|
+
className: string;
|
|
3466
|
+
classPaperOption: string;
|
|
3467
|
+
geometryOptions: string;
|
|
3468
|
+
fontCommands: string;
|
|
3469
|
+
lineStretch: string;
|
|
3470
|
+
fontSizeCommand: string;
|
|
3471
|
+
} {
|
|
3472
|
+
const resolved = options ?? {};
|
|
3473
|
+
const geometryParts: string[] = [];
|
|
3474
|
+
if (resolved.geometry) {
|
|
3475
|
+
geometryParts.push(sanitizeStudioPdfFreeformOption(resolved.geometry));
|
|
3476
|
+
} else {
|
|
3477
|
+
geometryParts.push(`margin=${resolved.margin ?? "2.2cm"}`);
|
|
3478
|
+
if (resolved.marginTop) geometryParts.push(`top=${resolved.marginTop}`);
|
|
3479
|
+
if (resolved.marginRight) geometryParts.push(`right=${resolved.marginRight}`);
|
|
3480
|
+
if (resolved.marginBottom) geometryParts.push(`bottom=${resolved.marginBottom}`);
|
|
3481
|
+
if (resolved.marginLeft) geometryParts.push(`left=${resolved.marginLeft}`);
|
|
3482
|
+
if (resolved.footskip) geometryParts.push(`footskip=${resolved.footskip}`);
|
|
3483
|
+
}
|
|
3484
|
+
const classPaperOption = resolved.papersize ? `,${resolved.papersize}paper` : "";
|
|
3485
|
+
const fontCommands = resolved.mainfont
|
|
3486
|
+
? `\\usepackage{fontspec}\n\\setmainfont{${sanitizeStudioPdfFreeformOption(resolved.mainfont).replace(/[{}\\]/g, "")}}\n`
|
|
3487
|
+
: "";
|
|
3488
|
+
const lineStretch = sanitizeStudioPdfFreeformOption(resolved.linestretch || "1.25") || "1.25";
|
|
3489
|
+
const useAltClass = shouldUseStudioAltMarkdownPdfDocumentClass(resolved);
|
|
3490
|
+
const defaultFootskip = getStudioDefaultPdfFootskip(resolved, useAltClass);
|
|
3491
|
+
if (!resolved.geometry && !resolved.footskip && defaultFootskip) geometryParts.push(`footskip=${defaultFootskip}`);
|
|
3492
|
+
const fontSizeCommand = resolved.fontsize && !useAltClass
|
|
3493
|
+
? `\\fontsize{${resolved.fontsize}}{${resolved.fontsize}}\\selectfont\n`
|
|
3494
|
+
: "";
|
|
3495
|
+
return {
|
|
3496
|
+
className: useAltClass ? "scrartcl" : "article",
|
|
3497
|
+
classPaperOption,
|
|
3498
|
+
geometryOptions: geometryParts.join(","),
|
|
3499
|
+
fontCommands,
|
|
3500
|
+
lineStretch,
|
|
3501
|
+
fontSizeCommand,
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
|
|
2736
3505
|
function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
|
|
2737
3506
|
if (isLatex) return markdown;
|
|
2738
3507
|
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
|
|
@@ -2743,7 +3512,8 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
2743
3512
|
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
2744
3513
|
? replaceStudioAnnotationMarkersForPdf(source)
|
|
2745
3514
|
: source;
|
|
2746
|
-
|
|
3515
|
+
const commentStrippedSource = stripStudioMarkdownHtmlComments(annotationReadySource);
|
|
3516
|
+
return normalizeObsidianImages(normalizeMathDelimiters(commentStrippedSource));
|
|
2747
3517
|
}
|
|
2748
3518
|
|
|
2749
3519
|
function stripMathMlAnnotationTags(html: string): string {
|
|
@@ -2908,15 +3678,17 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
|
|
|
2908
3678
|
|
|
2909
3679
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
2910
3680
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
3681
|
+
const markdownWithoutHtmlComments = isLatex ? markdown : stripStudioMarkdownHtmlComments(markdown);
|
|
3682
|
+
const markdownWithPreviewPageBreaks = isLatex ? markdownWithoutHtmlComments : replaceStudioPreviewPageBreakCommands(markdownWithoutHtmlComments);
|
|
2911
3683
|
const latexSubfigurePreviewTransform = isLatex
|
|
2912
|
-
? preprocessStudioLatexSubfiguresForPreview(
|
|
2913
|
-
: { markdown, subfigureGroups: [] };
|
|
3684
|
+
? preprocessStudioLatexSubfiguresForPreview(markdownWithPreviewPageBreaks)
|
|
3685
|
+
: { markdown: markdownWithPreviewPageBreaks, subfigureGroups: [] };
|
|
2914
3686
|
const latexAlgorithmPreviewTransform = isLatex
|
|
2915
3687
|
? preprocessStudioLatexAlgorithmsForPreview(latexSubfigurePreviewTransform.markdown)
|
|
2916
|
-
: { markdown, algorithmBlocks: [] };
|
|
3688
|
+
: { markdown: markdownWithPreviewPageBreaks, algorithmBlocks: [] };
|
|
2917
3689
|
const sourceWithResolvedRefs = isLatex
|
|
2918
3690
|
? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
|
|
2919
|
-
:
|
|
3691
|
+
: markdownWithPreviewPageBreaks;
|
|
2920
3692
|
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";
|
|
2921
3693
|
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
2922
3694
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
@@ -2928,7 +3700,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
2928
3700
|
const normalizedMarkdown = isLatex ? sourceWithResolvedRefs : normalizeObsidianImages(normalizeMathDelimiters(sourceWithResolvedRefs));
|
|
2929
3701
|
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
2930
3702
|
|
|
2931
|
-
|
|
3703
|
+
let renderedHtml = await new Promise<string>((resolve, reject) => {
|
|
2932
3704
|
const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
2933
3705
|
const stdoutChunks: Buffer[] = [];
|
|
2934
3706
|
const stderrChunks: Buffer[] = [];
|
|
@@ -2965,22 +3737,24 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
2965
3737
|
child.once("close", (code) => {
|
|
2966
3738
|
if (settled) return;
|
|
2967
3739
|
if (code === 0) {
|
|
2968
|
-
let
|
|
3740
|
+
let html = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
2969
3741
|
// When --standalone was used, extract only the <body> content
|
|
2970
3742
|
if (resourcePath) {
|
|
2971
|
-
const bodyMatch =
|
|
2972
|
-
if (bodyMatch)
|
|
3743
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
3744
|
+
if (bodyMatch) html = bodyMatch[1];
|
|
2973
3745
|
}
|
|
2974
3746
|
if (isLatex) {
|
|
2975
|
-
|
|
2976
|
-
|
|
3747
|
+
html = decorateStudioLatexRenderedHtml(
|
|
3748
|
+
html,
|
|
2977
3749
|
sourcePath,
|
|
2978
3750
|
resourcePath,
|
|
2979
3751
|
latexSubfigurePreviewTransform.subfigureGroups,
|
|
2980
3752
|
latexAlgorithmPreviewTransform.algorithmBlocks,
|
|
2981
3753
|
);
|
|
3754
|
+
} else {
|
|
3755
|
+
html = decorateStudioPreviewPageBreakHtml(html);
|
|
2982
3756
|
}
|
|
2983
|
-
succeed(stripMathMlAnnotationTags(
|
|
3757
|
+
succeed(stripMathMlAnnotationTags(html));
|
|
2984
3758
|
return;
|
|
2985
3759
|
}
|
|
2986
3760
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
@@ -2989,9 +3763,11 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
2989
3763
|
|
|
2990
3764
|
child.stdin.end(normalizedMarkdown);
|
|
2991
3765
|
});
|
|
3766
|
+
|
|
3767
|
+
return renderedHtml;
|
|
2992
3768
|
}
|
|
2993
3769
|
|
|
2994
|
-
async function renderStudioLiteralTextPdf(text: string, title = "Studio export"): Promise<Buffer> {
|
|
3770
|
+
async function renderStudioLiteralTextPdf(text: string, title = "Studio export", options?: StudioPdfRenderOptions): Promise<Buffer> {
|
|
2995
3771
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
2996
3772
|
const tempDir = join(tmpdir(), `pi-studio-text-pdf-${Date.now()}-${randomUUID()}`);
|
|
2997
3773
|
const textPath = join(tempDir, "input.txt");
|
|
@@ -2999,13 +3775,15 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
|
|
|
2999
3775
|
const outputPath = join(tempDir, "input.pdf");
|
|
3000
3776
|
|
|
3001
3777
|
const normalizedText = String(text ?? "").replace(/\r\n/g, "\n");
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
\\usepackage{
|
|
3778
|
+
const literalPdfConfig = buildStudioLiteralTextPdfTexConfig(options);
|
|
3779
|
+
const texDocument = `\\documentclass[${options?.fontsize ?? "11pt"}${literalPdfConfig.classPaperOption}]{${literalPdfConfig.className}}
|
|
3780
|
+
\\usepackage[${literalPdfConfig.geometryOptions}]{geometry}
|
|
3781
|
+
${literalPdfConfig.fontCommands}\\usepackage{fvextra}
|
|
3005
3782
|
\\usepackage{xcolor}
|
|
3006
3783
|
\\usepackage{upquote}
|
|
3007
3784
|
\\begin{document}
|
|
3008
|
-
\\
|
|
3785
|
+
\\renewcommand{\\baselinestretch}{${literalPdfConfig.lineStretch}}\\selectfont
|
|
3786
|
+
${literalPdfConfig.fontSizeCommand}\\section*{${title.replace(/[{}\\]/g, "").trim() || "Studio export"}}
|
|
3009
3787
|
\\VerbatimInput[breaklines,breakanywhere,fontsize=\\small,frame=single,rulecolor=\\color{black!15},framesep=2mm]{input.txt}
|
|
3010
3788
|
\\end{document}
|
|
3011
3789
|
`;
|
|
@@ -3119,6 +3897,10 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
3119
3897
|
bibliographyArgs: string[],
|
|
3120
3898
|
sourcePath: string | undefined,
|
|
3121
3899
|
subfigureGroups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>,
|
|
3900
|
+
inputFormat = "latex",
|
|
3901
|
+
calloutBlocks: StudioPdfMarkdownCalloutBlock[] = [],
|
|
3902
|
+
alignedImageBlocks: StudioPdfAlignedImageBlock[] = [],
|
|
3903
|
+
pdfOptions?: StudioPdfRenderOptions,
|
|
3122
3904
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
3123
3905
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
3124
3906
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
@@ -3126,16 +3908,14 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
3126
3908
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
3127
3909
|
|
|
3128
3910
|
await mkdir(tempDir, { recursive: true });
|
|
3129
|
-
await writeFile(preamblePath,
|
|
3911
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
3130
3912
|
|
|
3131
3913
|
const pandocArgs = [
|
|
3132
|
-
"-f",
|
|
3914
|
+
"-f", inputFormat,
|
|
3133
3915
|
"-t", "latex",
|
|
3134
3916
|
"-s",
|
|
3135
3917
|
"-o", latexPath,
|
|
3136
|
-
|
|
3137
|
-
"-V", "fontsize=11pt",
|
|
3138
|
-
"-V", "linestretch=1.25",
|
|
3918
|
+
...buildStudioPdfPandocVariableArgs(pdfOptions, inputFormat !== "latex"),
|
|
3139
3919
|
"-V", "urlcolor=blue",
|
|
3140
3920
|
"-V", "linkcolor=blue",
|
|
3141
3921
|
"--include-in-header", preamblePath,
|
|
@@ -3188,7 +3968,9 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
3188
3968
|
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
3189
3969
|
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
3190
3970
|
const annotationReadyLatex = replaceStudioAnnotationMarkersInGeneratedLatex(injectedLatex);
|
|
3191
|
-
const
|
|
3971
|
+
const calloutReadyLatex = replaceStudioPdfCalloutBlocksInGeneratedLatex(annotationReadyLatex, calloutBlocks);
|
|
3972
|
+
const alignedReadyLatex = replaceStudioPdfAlignedImageBlocksInGeneratedLatex(calloutReadyLatex, alignedImageBlocks);
|
|
3973
|
+
const normalizedLatex = normalizeStudioGeneratedFigureCaptions(alignedReadyLatex);
|
|
3192
3974
|
await writeFile(latexPath, normalizedLatex, "utf-8");
|
|
3193
3975
|
|
|
3194
3976
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -3253,6 +4035,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
3253
4035
|
resourcePath?: string,
|
|
3254
4036
|
editorPdfLanguage?: string,
|
|
3255
4037
|
sourcePath?: string,
|
|
4038
|
+
pdfOptions?: StudioPdfRenderOptions,
|
|
3256
4039
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
3257
4040
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
3258
4041
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
@@ -3270,6 +4053,12 @@ async function renderStudioPdfWithPandoc(
|
|
|
3270
4053
|
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
|
|
3271
4054
|
: markdown;
|
|
3272
4055
|
const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
|
|
4056
|
+
const pdfCalloutTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
4057
|
+
? preprocessStudioMarkdownCalloutsForPdf(sourceWithResolvedRefs)
|
|
4058
|
+
: { markdown: sourceWithResolvedRefs, blocks: [] as StudioPdfMarkdownCalloutBlock[] };
|
|
4059
|
+
const pdfAlignedImageTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
4060
|
+
? preprocessStudioMarkdownImageAlignmentForPdf(pdfCalloutTransform.markdown)
|
|
4061
|
+
: { markdown: pdfCalloutTransform.markdown, blocks: [] as StudioPdfAlignedImageBlock[] };
|
|
3273
4062
|
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
3274
4063
|
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
3275
4064
|
|
|
@@ -3283,15 +4072,13 @@ async function renderStudioPdfWithPandoc(
|
|
|
3283
4072
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
3284
4073
|
|
|
3285
4074
|
await mkdir(tempDir, { recursive: true });
|
|
3286
|
-
await writeFile(preamblePath,
|
|
4075
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
3287
4076
|
|
|
3288
4077
|
const args = [
|
|
3289
4078
|
"-f", inputFormat,
|
|
3290
4079
|
"-o", outputPath,
|
|
3291
4080
|
`--pdf-engine=${pdfEngine}`,
|
|
3292
|
-
|
|
3293
|
-
"-V", "fontsize=11pt",
|
|
3294
|
-
"-V", "linestretch=1.25",
|
|
4081
|
+
...buildStudioPdfPandocVariableArgs(pdfOptions, inputFormat !== "latex"),
|
|
3295
4082
|
"-V", "urlcolor=blue",
|
|
3296
4083
|
"-V", "linkcolor=blue",
|
|
3297
4084
|
"--include-in-header", preamblePath,
|
|
@@ -3360,6 +4147,10 @@ async function renderStudioPdfWithPandoc(
|
|
|
3360
4147
|
bibliographyArgs,
|
|
3361
4148
|
sourcePath,
|
|
3362
4149
|
latexSubfigurePdfTransform.groups,
|
|
4150
|
+
"latex",
|
|
4151
|
+
[],
|
|
4152
|
+
[],
|
|
4153
|
+
pdfOptions,
|
|
3363
4154
|
);
|
|
3364
4155
|
}
|
|
3365
4156
|
|
|
@@ -3372,7 +4163,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
3372
4163
|
const fenced = parseStudioSingleFencedCodeBlock(diffMarkdown);
|
|
3373
4164
|
const diffText = fenced ? fenced.content : markdown;
|
|
3374
4165
|
return {
|
|
3375
|
-
pdf: await renderStudioLiteralTextPdf(diffText, "Git diff"),
|
|
4166
|
+
pdf: await renderStudioLiteralTextPdf(diffText, "Git diff", pdfOptions),
|
|
3376
4167
|
warning: "Highlighted diff export failed, so Studio used a plain-text fallback without syntax colours.",
|
|
3377
4168
|
};
|
|
3378
4169
|
}
|
|
@@ -3381,27 +4172,44 @@ async function renderStudioPdfWithPandoc(
|
|
|
3381
4172
|
const inputFormat = isLatex
|
|
3382
4173
|
? "latex"
|
|
3383
4174
|
: "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+superscript+subscript-raw_html";
|
|
3384
|
-
const normalizedMarkdown = prepareStudioPdfMarkdown(
|
|
4175
|
+
const normalizedMarkdown = prepareStudioPdfMarkdown(pdfAlignedImageTransform.markdown, isLatex, effectiveEditorLanguage);
|
|
3385
4176
|
|
|
3386
4177
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
3387
4178
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
3388
4179
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
3389
4180
|
|
|
3390
4181
|
await mkdir(tempDir, { recursive: true });
|
|
3391
|
-
await writeFile(preamblePath,
|
|
4182
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
3392
4183
|
|
|
3393
4184
|
const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
|
|
3394
4185
|
? { markdown: normalizedMarkdown, found: 0, replaced: 0, failed: 0, missingCli: false }
|
|
3395
4186
|
: await preprocessStudioMermaidForPdf(normalizedMarkdown, tempDir);
|
|
3396
4187
|
const markdownForPdf = mermaidPrepared.markdown;
|
|
3397
4188
|
|
|
4189
|
+
if (!isLatex && (pdfCalloutTransform.blocks.length > 0 || pdfAlignedImageTransform.blocks.length > 0)) {
|
|
4190
|
+
const rendered = await renderStudioPdfFromGeneratedLatex(
|
|
4191
|
+
markdownForPdf,
|
|
4192
|
+
pandocCommand,
|
|
4193
|
+
pdfEngine,
|
|
4194
|
+
resourcePath,
|
|
4195
|
+
pandocWorkingDir,
|
|
4196
|
+
bibliographyArgs,
|
|
4197
|
+
sourcePath,
|
|
4198
|
+
[],
|
|
4199
|
+
inputFormat,
|
|
4200
|
+
pdfCalloutTransform.blocks,
|
|
4201
|
+
pdfAlignedImageTransform.blocks,
|
|
4202
|
+
pdfOptions,
|
|
4203
|
+
);
|
|
4204
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
4205
|
+
return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
|
|
4206
|
+
}
|
|
4207
|
+
|
|
3398
4208
|
const args = [
|
|
3399
4209
|
"-f", inputFormat,
|
|
3400
4210
|
"-o", outputPath,
|
|
3401
4211
|
`--pdf-engine=${pdfEngine}`,
|
|
3402
|
-
|
|
3403
|
-
"-V", "fontsize=11pt",
|
|
3404
|
-
"-V", "linestretch=1.25",
|
|
4212
|
+
...buildStudioPdfPandocVariableArgs(pdfOptions, !isLatex),
|
|
3405
4213
|
"-V", "urlcolor=blue",
|
|
3406
4214
|
"-V", "linkcolor=blue",
|
|
3407
4215
|
"--include-in-header", preamblePath,
|
|
@@ -4457,7 +5265,7 @@ ${cssVarsBlock}
|
|
|
4457
5265
|
<div class="controls">
|
|
4458
5266
|
<button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
|
|
4459
5267
|
<button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
|
|
4460
|
-
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.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" /></label>
|
|
5268
|
+
<label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" 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" /></label>
|
|
4461
5269
|
<button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
|
|
4462
5270
|
<button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
|
|
4463
5271
|
</div>
|
|
@@ -6868,23 +7676,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
6868
7676
|
const trimmed = args.trim();
|
|
6869
7677
|
if (!trimmed || trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
6870
7678
|
ctx.ui.notify(
|
|
6871
|
-
"Usage: /studio-pdf <path
|
|
6872
|
-
+ " Export a local Markdown/LaTeX file to <name>.studio.pdf using the Studio PDF pipeline
|
|
7679
|
+
"Usage: /studio-pdf <path> [options]\n"
|
|
7680
|
+
+ " Export a local Markdown/LaTeX file to <name>.studio.pdf using the Studio PDF pipeline.\n"
|
|
7681
|
+
+ "Options:\n"
|
|
7682
|
+
+ " --fontsize <value> e.g. 12pt\n"
|
|
7683
|
+
+ " --section-size <value> e.g. 24pt\n"
|
|
7684
|
+
+ " --subsection-size <value>\n"
|
|
7685
|
+
+ " --subsubsection-size <value>\n"
|
|
7686
|
+
+ " --section-space-before <value>\n"
|
|
7687
|
+
+ " --section-space-after <value>\n"
|
|
7688
|
+
+ " --subsection-space-before <value>\n"
|
|
7689
|
+
+ " --subsection-space-after <value>\n"
|
|
7690
|
+
+ " --margin <value> e.g. 25mm\n"
|
|
7691
|
+
+ " --margin-top <value>\n"
|
|
7692
|
+
+ " --margin-right <value>\n"
|
|
7693
|
+
+ " --margin-bottom <value>\n"
|
|
7694
|
+
+ " --margin-left <value>\n"
|
|
7695
|
+
+ " --footskip <value> e.g. 12mm\n"
|
|
7696
|
+
+ " --linestretch <value> e.g. 1.2\n"
|
|
7697
|
+
+ " --mainfont <name> e.g. \"TeX Gyre Pagella\"\n"
|
|
7698
|
+
+ " --papersize <name> e.g. a4\n"
|
|
7699
|
+
+ " --geometry <spec> e.g. \"top=30mm,left=25mm,right=25mm,bottom=30mm,footskip=12mm\"\n"
|
|
7700
|
+
+ " Note: use either --geometry or the --margin/--margin-*/--footskip flags.",
|
|
6873
7701
|
"info",
|
|
6874
7702
|
);
|
|
6875
7703
|
return;
|
|
6876
7704
|
}
|
|
6877
7705
|
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
}
|
|
6882
|
-
|
|
6883
|
-
const pathArg = parsePathArgument(trimmed);
|
|
6884
|
-
if (!pathArg) {
|
|
6885
|
-
ctx.ui.notify("Invalid file path argument.", "error");
|
|
7706
|
+
const parsedArgs = parseStudioPdfCommandArgs(trimmed);
|
|
7707
|
+
if ("error" in parsedArgs) {
|
|
7708
|
+
ctx.ui.notify(parsedArgs.error, "error");
|
|
6886
7709
|
return;
|
|
6887
7710
|
}
|
|
7711
|
+
const { pathArg, options: pdfOptions } = parsedArgs;
|
|
6888
7712
|
|
|
6889
7713
|
const file = readStudioFile(pathArg, ctx.cwd);
|
|
6890
7714
|
if (file.ok === false) {
|
|
@@ -6916,6 +7740,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6916
7740
|
resourcePath,
|
|
6917
7741
|
editorPdfLanguage,
|
|
6918
7742
|
file.resolvedPath,
|
|
7743
|
+
pdfOptions,
|
|
6919
7744
|
);
|
|
6920
7745
|
await writeFile(outputPath, pdf);
|
|
6921
7746
|
|