pi-studio 0.5.31 → 0.5.33
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 +24 -0
- package/README.md +1 -1
- package/client/studio-client.js +365 -2
- package/client/studio.css +162 -0
- package/index.ts +1189 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.33] — 2026-03-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Studio browser tabs now use `π Studio` branding plus a simple theme-reactive `π` favicon instead of the generic browser globe.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Markdown preview now preserves `[an: ...]` markers more reliably by replacing them with preview-safe placeholders before pandoc and restoring annotation pills afterwards, preventing long or markdown-like annotations from leaking through as raw text.
|
|
14
|
+
- Preview/PDF markdown preparation now normalizes fenced blocks whose contents contain competing backtick/tilde fence runs, avoiding broken rendering/export for diff-heavy content that itself contains code fences.
|
|
15
|
+
- Diff PDF exports now route highlighted diff content through the generated-LaTeX path more reliably, keeping add/delete/meta/hunk styling and line wrapping on exports that previously rendered poorly or fell back unnecessarily.
|
|
16
|
+
- PDF annotation badges now wrap within the page width instead of overflowing on long notes, preserve inline math inside annotation text, and also render correctly inside diff token lines such as `+[an: ...]`.
|
|
17
|
+
|
|
18
|
+
## [0.5.32] — 2026-03-25
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `/studio-pdf <path>` now accepts a curated set of advanced layout controls for file-based exports, including font size, margins, line stretch, main font, paper size, geometry, heading sizes, heading spacing, and footer skip.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Large-font Markdown/QMD Studio PDF exports now switch to a more suitable LaTeX document class and use a safer default footer skip unless you explicitly override the geometry.
|
|
25
|
+
- PDF callout blocks now render more compactly, reducing extra vertical whitespace around note/tip/warning content.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Studio preview/PDF preparation now treats `.qmd` files like Markdown, strips HTML comments more narrowly, shows standalone LaTeX page-break commands as subtle preview dividers, and supports common Quarto-style callout and `fig-align` patterns in preview/PDF output.
|
|
29
|
+
- Markdown/QMD preview now renders embedded local PDF figures more reliably via `pdf.js`, avoiding grey-box browser embed failures in the Studio preview surface.
|
|
30
|
+
|
|
7
31
|
## [0.5.31] — 2026-03-24
|
|
8
32
|
|
|
9
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
43
43
|
| `/studio --stop` | Stop studio server |
|
|
44
44
|
| `/studio --help` | Show help |
|
|
45
45
|
| `/studio-current <path>` | Load a file into currently open Studio tab(s) without opening a new browser window |
|
|
46
|
-
| `/studio-pdf <path
|
|
46
|
+
| `/studio-pdf <path> [options]` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline, with optional layout controls |
|
|
47
47
|
|
|
48
48
|
## Install
|
|
49
49
|
|
package/client/studio-client.js
CHANGED
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
|
|
189
189
|
// Single source of truth: language -> file extensions (and display label)
|
|
190
190
|
var LANG_EXT_MAP = {
|
|
191
|
-
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx"] },
|
|
191
|
+
markdown: { label: "Markdown", exts: ["md", "markdown", "mdx", "qmd"] },
|
|
192
192
|
javascript: { label: "JavaScript", exts: ["js", "mjs", "cjs", "jsx"] },
|
|
193
193
|
typescript: { label: "TypeScript", exts: ["ts", "mts", "cts", "tsx"] },
|
|
194
194
|
python: { label: "Python", exts: ["py", "pyw"] },
|
|
@@ -242,9 +242,12 @@
|
|
|
242
242
|
let editorHighlightRenderRaf = null;
|
|
243
243
|
let annotationsEnabled = true;
|
|
244
244
|
const ANNOTATION_MARKER_REGEX = /\[an:\s*([^\]]+?)\]/gi;
|
|
245
|
+
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
245
246
|
const EMPTY_OVERLAY_LINE = "\u200b";
|
|
246
247
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
247
248
|
const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
|
|
249
|
+
const PDFJS_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.min.mjs";
|
|
250
|
+
const PDFJS_WORKER_CDN_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/legacy/build/pdf.worker.min.mjs";
|
|
248
251
|
const BOOT = (typeof window.__PI_STUDIO_BOOT__ === "object" && window.__PI_STUDIO_BOOT__)
|
|
249
252
|
? window.__PI_STUDIO_BOOT__
|
|
250
253
|
: {};
|
|
@@ -255,9 +258,12 @@
|
|
|
255
258
|
const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
|
|
256
259
|
const MATHJAX_UNAVAILABLE_MESSAGE = "Math fallback unavailable. Some unsupported equations may remain as raw TeX.";
|
|
257
260
|
const MATHJAX_RENDER_FAIL_MESSAGE = "Math fallback could not render some unsupported equations.";
|
|
261
|
+
const PDF_PREVIEW_UNAVAILABLE_MESSAGE = "PDF figure preview unavailable. Inline PDF rendering is not supported in this Studio browser environment.";
|
|
262
|
+
const PDF_PREVIEW_RENDER_FAIL_MESSAGE = "PDF figure preview could not be rendered.";
|
|
258
263
|
let mermaidModulePromise = null;
|
|
259
264
|
let mermaidInitialized = false;
|
|
260
265
|
let mathJaxPromise = null;
|
|
266
|
+
let pdfJsPromise = null;
|
|
261
267
|
|
|
262
268
|
const DEBUG_ENABLED = (() => {
|
|
263
269
|
try {
|
|
@@ -1178,6 +1184,80 @@
|
|
|
1178
1184
|
return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
|
|
1179
1185
|
}
|
|
1180
1186
|
|
|
1187
|
+
function normalizePreviewAnnotationLabel(text) {
|
|
1188
|
+
return String(text || "")
|
|
1189
|
+
.replace(/\r\n/g, "\n")
|
|
1190
|
+
.replace(/\s*\n\s*/g, " ")
|
|
1191
|
+
.replace(/\s{2,}/g, " ")
|
|
1192
|
+
.trim();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function prepareMarkdownForPandocPreview(markdown) {
|
|
1196
|
+
const source = String(markdown || "").replace(/\r\n/g, "\n");
|
|
1197
|
+
const placeholders = [];
|
|
1198
|
+
if (!source) {
|
|
1199
|
+
return { markdown: source, placeholders: placeholders };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const lines = source.split("\n");
|
|
1203
|
+
const out = [];
|
|
1204
|
+
let plainBuffer = [];
|
|
1205
|
+
let inFence = false;
|
|
1206
|
+
let fenceChar = null;
|
|
1207
|
+
let fenceLength = 0;
|
|
1208
|
+
|
|
1209
|
+
function flushPlain() {
|
|
1210
|
+
if (plainBuffer.length === 0) return;
|
|
1211
|
+
const segment = plainBuffer.join("\n").replace(/\[an:\s*([^\]]+?)\]/gi, function(_match, markerText) {
|
|
1212
|
+
const label = normalizePreviewAnnotationLabel(markerText);
|
|
1213
|
+
if (!label) return "";
|
|
1214
|
+
const token = PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX + placeholders.length + "TOKEN";
|
|
1215
|
+
placeholders.push({ token: token, text: label, title: "[an: " + label + "]" });
|
|
1216
|
+
return token;
|
|
1217
|
+
});
|
|
1218
|
+
out.push(segment);
|
|
1219
|
+
plainBuffer = [];
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
for (const line of lines) {
|
|
1223
|
+
const trimmed = line.trimStart();
|
|
1224
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
1225
|
+
|
|
1226
|
+
if (fenceMatch) {
|
|
1227
|
+
const marker = fenceMatch[1] || "";
|
|
1228
|
+
const markerChar = marker.charAt(0);
|
|
1229
|
+
const markerLength = marker.length;
|
|
1230
|
+
|
|
1231
|
+
if (!inFence) {
|
|
1232
|
+
flushPlain();
|
|
1233
|
+
inFence = true;
|
|
1234
|
+
fenceChar = markerChar;
|
|
1235
|
+
fenceLength = markerLength;
|
|
1236
|
+
out.push(line);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
1241
|
+
inFence = false;
|
|
1242
|
+
fenceChar = null;
|
|
1243
|
+
fenceLength = 0;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
out.push(line);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (inFence) {
|
|
1251
|
+
out.push(line);
|
|
1252
|
+
} else {
|
|
1253
|
+
plainBuffer.push(line);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
flushPlain();
|
|
1258
|
+
return { markdown: out.join("\n"), placeholders: placeholders };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1181
1261
|
function wrapAsFencedCodeBlock(text, language) {
|
|
1182
1262
|
const source = String(text || "").trimEnd();
|
|
1183
1263
|
const lang = String(language || "").trim();
|
|
@@ -1248,11 +1328,210 @@
|
|
|
1248
1328
|
mathMl: true,
|
|
1249
1329
|
svg: true,
|
|
1250
1330
|
},
|
|
1331
|
+
ADD_TAGS: ["embed"],
|
|
1332
|
+
ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
|
|
1333
|
+
ADD_DATA_URI_TAGS: ["embed"],
|
|
1251
1334
|
});
|
|
1252
1335
|
}
|
|
1253
1336
|
return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
|
|
1254
1337
|
}
|
|
1255
1338
|
|
|
1339
|
+
function isPdfPreviewSource(src) {
|
|
1340
|
+
return Boolean(src) && (/^data:application\/pdf(?:;|,)/i.test(src) || /\.pdf(?:$|[?#])/i.test(src));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function decoratePdfEmbeds(targetEl) {
|
|
1344
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const embeds = targetEl.querySelectorAll("embed[src]");
|
|
1349
|
+
embeds.forEach(function(embedEl) {
|
|
1350
|
+
const src = typeof embedEl.getAttribute === "function" ? (embedEl.getAttribute("src") || "") : "";
|
|
1351
|
+
if (!isPdfPreviewSource(src)) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (!embedEl.getAttribute("type")) {
|
|
1355
|
+
embedEl.setAttribute("type", "application/pdf");
|
|
1356
|
+
}
|
|
1357
|
+
if (!embedEl.getAttribute("title")) {
|
|
1358
|
+
embedEl.setAttribute("title", "Embedded PDF figure");
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function decodePdfDataUri(src) {
|
|
1364
|
+
const match = String(src || "").match(/^data:application\/pdf(?:;[^,]*)?,([A-Za-z0-9+/=\s]+)$/i);
|
|
1365
|
+
if (!match) return null;
|
|
1366
|
+
const payload = (match[1] || "").replace(/\s+/g, "");
|
|
1367
|
+
if (!payload) return null;
|
|
1368
|
+
const binary = window.atob(payload);
|
|
1369
|
+
const bytes = new Uint8Array(binary.length);
|
|
1370
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
1371
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1372
|
+
}
|
|
1373
|
+
return bytes;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function ensurePdfJs() {
|
|
1377
|
+
if (window.pdfjsLib && typeof window.pdfjsLib.getDocument === "function") {
|
|
1378
|
+
return Promise.resolve(window.pdfjsLib);
|
|
1379
|
+
}
|
|
1380
|
+
if (pdfJsPromise) {
|
|
1381
|
+
return pdfJsPromise;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
pdfJsPromise = import(PDFJS_CDN_URL)
|
|
1385
|
+
.then((module) => {
|
|
1386
|
+
const api = module && typeof module.getDocument === "function"
|
|
1387
|
+
? module
|
|
1388
|
+
: (module && module.default && typeof module.default.getDocument === "function" ? module.default : null);
|
|
1389
|
+
if (!api || typeof api.getDocument !== "function") {
|
|
1390
|
+
throw new Error("pdf.js did not initialize.");
|
|
1391
|
+
}
|
|
1392
|
+
if (api.GlobalWorkerOptions && !api.GlobalWorkerOptions.workerSrc) {
|
|
1393
|
+
api.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_CDN_URL;
|
|
1394
|
+
}
|
|
1395
|
+
window.pdfjsLib = api;
|
|
1396
|
+
return api;
|
|
1397
|
+
})
|
|
1398
|
+
.catch((error) => {
|
|
1399
|
+
pdfJsPromise = null;
|
|
1400
|
+
throw error;
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
return pdfJsPromise;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function appendPdfPreviewNotice(targetEl, message) {
|
|
1407
|
+
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (targetEl.querySelector(".preview-pdf-warning")) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const warningEl = document.createElement("div");
|
|
1414
|
+
warningEl.className = "preview-warning preview-pdf-warning";
|
|
1415
|
+
warningEl.textContent = String(message || PDF_PREVIEW_UNAVAILABLE_MESSAGE);
|
|
1416
|
+
targetEl.appendChild(warningEl);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async function loadPdfDocumentSource(src) {
|
|
1420
|
+
const embedded = decodePdfDataUri(src);
|
|
1421
|
+
if (embedded) {
|
|
1422
|
+
return { data: embedded };
|
|
1423
|
+
}
|
|
1424
|
+
const response = await fetch(src);
|
|
1425
|
+
if (!response.ok) {
|
|
1426
|
+
throw new Error("Failed to fetch PDF figure for preview.");
|
|
1427
|
+
}
|
|
1428
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1429
|
+
return { data: bytes };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async function renderSinglePdfPreviewEmbed(embedEl, pdfjsLib) {
|
|
1433
|
+
if (!embedEl || embedEl.dataset.studioPdfPreviewRendered === "1") {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const src = embedEl.getAttribute("src") || "";
|
|
1438
|
+
if (!isPdfPreviewSource(src)) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const measuredWidth = Math.max(1, Math.round(embedEl.getBoundingClientRect().width || 0));
|
|
1443
|
+
const styleText = embedEl.getAttribute("style") || "";
|
|
1444
|
+
const widthAttr = embedEl.getAttribute("width") || "";
|
|
1445
|
+
const figAlign = embedEl.getAttribute("data-fig-align") || "";
|
|
1446
|
+
const pdfSource = await loadPdfDocumentSource(src);
|
|
1447
|
+
const loadingTask = pdfjsLib.getDocument(pdfSource);
|
|
1448
|
+
const pdfDocument = await loadingTask.promise;
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
const page = await pdfDocument.getPage(1);
|
|
1452
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
1453
|
+
const cssWidth = Math.max(1, measuredWidth || Math.round(baseViewport.width));
|
|
1454
|
+
const renderScale = Math.max(0.25, cssWidth / baseViewport.width) * Math.min(window.devicePixelRatio || 1, 2);
|
|
1455
|
+
const viewport = page.getViewport({ scale: renderScale });
|
|
1456
|
+
const canvas = document.createElement("canvas");
|
|
1457
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
1458
|
+
if (!context) {
|
|
1459
|
+
throw new Error("Canvas 2D context unavailable.");
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
canvas.width = Math.max(1, Math.ceil(viewport.width));
|
|
1463
|
+
canvas.height = Math.max(1, Math.ceil(viewport.height));
|
|
1464
|
+
canvas.style.width = "100%";
|
|
1465
|
+
canvas.style.height = "auto";
|
|
1466
|
+
canvas.setAttribute("aria-label", "PDF figure preview");
|
|
1467
|
+
|
|
1468
|
+
await page.render({
|
|
1469
|
+
canvasContext: context,
|
|
1470
|
+
viewport,
|
|
1471
|
+
}).promise;
|
|
1472
|
+
|
|
1473
|
+
const wrapper = document.createElement("div");
|
|
1474
|
+
wrapper.className = "studio-pdf-preview";
|
|
1475
|
+
if (styleText) {
|
|
1476
|
+
wrapper.style.cssText = styleText;
|
|
1477
|
+
} else if (widthAttr) {
|
|
1478
|
+
wrapper.style.width = /^\d+(?:\.\d+)?$/.test(widthAttr) ? (widthAttr + "px") : widthAttr;
|
|
1479
|
+
} else {
|
|
1480
|
+
wrapper.style.width = "100%";
|
|
1481
|
+
}
|
|
1482
|
+
if (figAlign) {
|
|
1483
|
+
wrapper.setAttribute("data-fig-align", figAlign);
|
|
1484
|
+
}
|
|
1485
|
+
wrapper.title = "PDF figure preview (page 1)";
|
|
1486
|
+
wrapper.appendChild(canvas);
|
|
1487
|
+
embedEl.dataset.studioPdfPreviewRendered = "1";
|
|
1488
|
+
embedEl.replaceWith(wrapper);
|
|
1489
|
+
return true;
|
|
1490
|
+
} finally {
|
|
1491
|
+
if (typeof pdfDocument.cleanup === "function") {
|
|
1492
|
+
try { pdfDocument.cleanup(); } catch {}
|
|
1493
|
+
}
|
|
1494
|
+
if (typeof pdfDocument.destroy === "function") {
|
|
1495
|
+
try { await pdfDocument.destroy(); } catch {}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
async function renderPdfPreviewsInElement(targetEl) {
|
|
1501
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const embeds = Array.from(targetEl.querySelectorAll("embed[src]"))
|
|
1506
|
+
.filter((embedEl) => isPdfPreviewSource(embedEl.getAttribute("src") || ""));
|
|
1507
|
+
if (embeds.length === 0) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
let pdfjsLib;
|
|
1512
|
+
try {
|
|
1513
|
+
pdfjsLib = await ensurePdfJs();
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
console.error("pdf.js load failed:", error);
|
|
1516
|
+
appendPdfPreviewNotice(targetEl, PDF_PREVIEW_UNAVAILABLE_MESSAGE);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
let hadFailure = false;
|
|
1521
|
+
for (const embedEl of embeds) {
|
|
1522
|
+
try {
|
|
1523
|
+
await renderSinglePdfPreviewEmbed(embedEl, pdfjsLib);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
hadFailure = true;
|
|
1526
|
+
console.error("PDF preview render failed:", error);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (hadFailure) {
|
|
1531
|
+
appendPdfPreviewNotice(targetEl, PDF_PREVIEW_RENDER_FAIL_MESSAGE);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1256
1535
|
function appendMathFallbackNotice(targetEl, message) {
|
|
1257
1536
|
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
1258
1537
|
return;
|
|
@@ -1418,6 +1697,79 @@
|
|
|
1418
1697
|
}
|
|
1419
1698
|
}
|
|
1420
1699
|
|
|
1700
|
+
function applyPreviewAnnotationPlaceholdersToElement(targetEl, placeholders) {
|
|
1701
|
+
if (!targetEl || !Array.isArray(placeholders) || placeholders.length === 0) return;
|
|
1702
|
+
if (typeof document.createTreeWalker !== "function") return;
|
|
1703
|
+
|
|
1704
|
+
const placeholderMap = new Map();
|
|
1705
|
+
const placeholderTokens = [];
|
|
1706
|
+
placeholders.forEach(function(entry) {
|
|
1707
|
+
const token = entry && typeof entry.token === "string" ? entry.token : "";
|
|
1708
|
+
if (!token) return;
|
|
1709
|
+
placeholderMap.set(token, entry);
|
|
1710
|
+
placeholderTokens.push(token);
|
|
1711
|
+
});
|
|
1712
|
+
if (placeholderTokens.length === 0) return;
|
|
1713
|
+
|
|
1714
|
+
const placeholderPattern = new RegExp(placeholderTokens.map(escapeRegExp).join("|"), "g");
|
|
1715
|
+
const walker = document.createTreeWalker(targetEl, NodeFilter.SHOW_TEXT);
|
|
1716
|
+
const textNodes = [];
|
|
1717
|
+
let node = walker.nextNode();
|
|
1718
|
+
while (node) {
|
|
1719
|
+
const textNode = node;
|
|
1720
|
+
const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
1721
|
+
if (value && value.indexOf(PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX) !== -1) {
|
|
1722
|
+
const parent = textNode.parentElement;
|
|
1723
|
+
const tag = parent && parent.tagName ? parent.tagName.toUpperCase() : "";
|
|
1724
|
+
if (tag !== "CODE" && tag !== "PRE" && tag !== "SCRIPT" && tag !== "STYLE" && tag !== "TEXTAREA") {
|
|
1725
|
+
textNodes.push(textNode);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
node = walker.nextNode();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
textNodes.forEach(function(textNode) {
|
|
1732
|
+
const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
1733
|
+
if (!text) return;
|
|
1734
|
+
placeholderPattern.lastIndex = 0;
|
|
1735
|
+
if (!placeholderPattern.test(text)) return;
|
|
1736
|
+
placeholderPattern.lastIndex = 0;
|
|
1737
|
+
|
|
1738
|
+
const fragment = document.createDocumentFragment();
|
|
1739
|
+
let lastIndex = 0;
|
|
1740
|
+
let match;
|
|
1741
|
+
while ((match = placeholderPattern.exec(text)) !== null) {
|
|
1742
|
+
const token = match[0] || "";
|
|
1743
|
+
const entry = placeholderMap.get(token);
|
|
1744
|
+
const start = typeof match.index === "number" ? match.index : 0;
|
|
1745
|
+
if (start > lastIndex) {
|
|
1746
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
|
|
1747
|
+
}
|
|
1748
|
+
if (entry) {
|
|
1749
|
+
const markerEl = document.createElement("span");
|
|
1750
|
+
markerEl.className = "annotation-preview-marker";
|
|
1751
|
+
markerEl.textContent = typeof entry.text === "string" ? entry.text : token;
|
|
1752
|
+
markerEl.title = typeof entry.title === "string" ? entry.title : markerEl.textContent;
|
|
1753
|
+
fragment.appendChild(markerEl);
|
|
1754
|
+
} else {
|
|
1755
|
+
fragment.appendChild(document.createTextNode(token));
|
|
1756
|
+
}
|
|
1757
|
+
lastIndex = start + token.length;
|
|
1758
|
+
if (token.length === 0) {
|
|
1759
|
+
placeholderPattern.lastIndex += 1;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (lastIndex < text.length) {
|
|
1764
|
+
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (textNode.parentNode) {
|
|
1768
|
+
textNode.parentNode.replaceChild(fragment, textNode);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1421
1773
|
function applyAnnotationMarkersToElement(targetEl, mode) {
|
|
1422
1774
|
if (!targetEl || mode === "none") return;
|
|
1423
1775
|
if (typeof document.createTreeWalker !== "function") return;
|
|
@@ -1890,8 +2242,12 @@
|
|
|
1890
2242
|
}
|
|
1891
2243
|
|
|
1892
2244
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
2245
|
+
const previewPrepared = annotationsEnabled
|
|
2246
|
+
? prepareMarkdownForPandocPreview(markdown)
|
|
2247
|
+
: { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
|
|
2248
|
+
|
|
1893
2249
|
try {
|
|
1894
|
-
const renderedHtml = await renderMarkdownWithPandoc(markdown);
|
|
2250
|
+
const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown);
|
|
1895
2251
|
|
|
1896
2252
|
if (pane === "source") {
|
|
1897
2253
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -1901,6 +2257,9 @@
|
|
|
1901
2257
|
|
|
1902
2258
|
finishPreviewRender(targetEl);
|
|
1903
2259
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2260
|
+
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
2261
|
+
decoratePdfEmbeds(targetEl);
|
|
2262
|
+
await renderPdfPreviewsInElement(targetEl);
|
|
1904
2263
|
const annotationMode = (pane === "source" || pane === "response")
|
|
1905
2264
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
1906
2265
|
: "none";
|
|
@@ -2338,6 +2697,10 @@
|
|
|
2338
2697
|
.replace(/'/g, "'");
|
|
2339
2698
|
}
|
|
2340
2699
|
|
|
2700
|
+
function escapeRegExp(text) {
|
|
2701
|
+
return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2341
2704
|
function wrapHighlight(className, text) {
|
|
2342
2705
|
return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
|
|
2343
2706
|
}
|
package/client/studio.css
CHANGED
|
@@ -516,7 +516,11 @@
|
|
|
516
516
|
background: var(--accent-soft);
|
|
517
517
|
border: 1px solid var(--marker-border);
|
|
518
518
|
border-radius: 4px;
|
|
519
|
+
display: inline-block;
|
|
520
|
+
max-width: 100%;
|
|
519
521
|
padding: 0 4px;
|
|
522
|
+
white-space: normal;
|
|
523
|
+
vertical-align: baseline;
|
|
520
524
|
}
|
|
521
525
|
|
|
522
526
|
#sourcePreview {
|
|
@@ -633,6 +637,91 @@
|
|
|
633
637
|
color: var(--md-quote);
|
|
634
638
|
}
|
|
635
639
|
|
|
640
|
+
.rendered-markdown .callout-note,
|
|
641
|
+
.rendered-markdown .callout-tip,
|
|
642
|
+
.rendered-markdown .callout-warning,
|
|
643
|
+
.rendered-markdown .callout-important,
|
|
644
|
+
.rendered-markdown .callout-caution {
|
|
645
|
+
margin: 1.15em 0;
|
|
646
|
+
padding: 0.8em 1rem 0.95em;
|
|
647
|
+
border: 1px solid var(--border-muted);
|
|
648
|
+
border-left-width: 4px;
|
|
649
|
+
border-radius: 10px;
|
|
650
|
+
background: var(--panel-2);
|
|
651
|
+
color: inherit;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.rendered-markdown .callout-note::before,
|
|
655
|
+
.rendered-markdown .callout-tip::before,
|
|
656
|
+
.rendered-markdown .callout-warning::before,
|
|
657
|
+
.rendered-markdown .callout-important::before,
|
|
658
|
+
.rendered-markdown .callout-caution::before {
|
|
659
|
+
display: inline-block;
|
|
660
|
+
margin-bottom: 0.45rem;
|
|
661
|
+
font-size: 0.76em;
|
|
662
|
+
font-weight: 700;
|
|
663
|
+
letter-spacing: 0.08em;
|
|
664
|
+
text-transform: uppercase;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.rendered-markdown .callout-note::before {
|
|
668
|
+
content: "Note";
|
|
669
|
+
color: var(--accent);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.rendered-markdown .callout-tip::before {
|
|
673
|
+
content: "Tip";
|
|
674
|
+
color: var(--ok);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.rendered-markdown .callout-warning::before {
|
|
678
|
+
content: "Warning";
|
|
679
|
+
color: var(--warn);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.rendered-markdown .callout-important::before {
|
|
683
|
+
content: "Important";
|
|
684
|
+
color: var(--error);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.rendered-markdown .callout-caution::before {
|
|
688
|
+
content: "Caution";
|
|
689
|
+
color: var(--error);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.rendered-markdown .callout-note {
|
|
693
|
+
border-left-color: var(--accent);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.rendered-markdown .callout-tip {
|
|
697
|
+
border-left-color: var(--ok);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.rendered-markdown .callout-warning {
|
|
701
|
+
border-left-color: var(--warn);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.rendered-markdown .callout-important,
|
|
705
|
+
.rendered-markdown .callout-caution {
|
|
706
|
+
border-left-color: var(--error);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.rendered-markdown .callout-note > :first-child,
|
|
710
|
+
.rendered-markdown .callout-tip > :first-child,
|
|
711
|
+
.rendered-markdown .callout-warning > :first-child,
|
|
712
|
+
.rendered-markdown .callout-important > :first-child,
|
|
713
|
+
.rendered-markdown .callout-caution > :first-child {
|
|
714
|
+
margin-top: 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.rendered-markdown .callout-note > :last-child,
|
|
718
|
+
.rendered-markdown .callout-tip > :last-child,
|
|
719
|
+
.rendered-markdown .callout-warning > :last-child,
|
|
720
|
+
.rendered-markdown .callout-important > :last-child,
|
|
721
|
+
.rendered-markdown .callout-caution > :last-child {
|
|
722
|
+
margin-bottom: 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
636
725
|
.rendered-markdown pre {
|
|
637
726
|
background: var(--panel-2);
|
|
638
727
|
border: 1px solid var(--md-codeblock-border);
|
|
@@ -765,10 +854,83 @@
|
|
|
765
854
|
margin: 1.25em 0;
|
|
766
855
|
}
|
|
767
856
|
|
|
857
|
+
.rendered-markdown .studio-page-break {
|
|
858
|
+
display: flex;
|
|
859
|
+
align-items: center;
|
|
860
|
+
gap: 0.85rem;
|
|
861
|
+
margin: 1.75em 0;
|
|
862
|
+
color: var(--muted);
|
|
863
|
+
font-size: 0.88em;
|
|
864
|
+
text-transform: uppercase;
|
|
865
|
+
letter-spacing: 0.08em;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.rendered-markdown .studio-page-break-rule {
|
|
869
|
+
flex: 1 1 auto;
|
|
870
|
+
height: 1px;
|
|
871
|
+
background: var(--md-hr);
|
|
872
|
+
opacity: 0.95;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.rendered-markdown .studio-page-break-label {
|
|
876
|
+
flex: 0 0 auto;
|
|
877
|
+
white-space: nowrap;
|
|
878
|
+
}
|
|
879
|
+
|
|
768
880
|
.rendered-markdown img {
|
|
769
881
|
max-width: 100%;
|
|
770
882
|
}
|
|
771
883
|
|
|
884
|
+
.rendered-markdown img[data-fig-align="center"],
|
|
885
|
+
.rendered-markdown embed[data-fig-align="center"],
|
|
886
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
|
|
887
|
+
display: block;
|
|
888
|
+
margin-left: auto;
|
|
889
|
+
margin-right: auto;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.rendered-markdown img[data-fig-align="right"],
|
|
893
|
+
.rendered-markdown embed[data-fig-align="right"],
|
|
894
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="right"] {
|
|
895
|
+
display: block;
|
|
896
|
+
margin-left: auto;
|
|
897
|
+
margin-right: 0;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.rendered-markdown embed[type="application/pdf"],
|
|
901
|
+
.rendered-markdown embed[src^="data:application/pdf"],
|
|
902
|
+
.rendered-markdown embed[src$=".pdf"] {
|
|
903
|
+
display: block;
|
|
904
|
+
width: 100%;
|
|
905
|
+
min-height: 18rem;
|
|
906
|
+
border: 1px solid var(--md-table-border);
|
|
907
|
+
border-radius: 10px;
|
|
908
|
+
background: #fff;
|
|
909
|
+
overflow: hidden;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.rendered-markdown .studio-pdf-preview {
|
|
913
|
+
display: block;
|
|
914
|
+
max-width: 100%;
|
|
915
|
+
border: 1px solid var(--md-table-border);
|
|
916
|
+
border-radius: 10px;
|
|
917
|
+
background: #fff;
|
|
918
|
+
overflow: hidden;
|
|
919
|
+
line-height: 0;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.rendered-markdown .studio-pdf-preview[data-fig-align="center"] {
|
|
923
|
+
margin-left: auto;
|
|
924
|
+
margin-right: auto;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.rendered-markdown .studio-pdf-preview canvas {
|
|
928
|
+
display: block;
|
|
929
|
+
width: 100%;
|
|
930
|
+
height: auto;
|
|
931
|
+
max-width: 100%;
|
|
932
|
+
}
|
|
933
|
+
|
|
772
934
|
.rendered-markdown .studio-subfigure-group {
|
|
773
935
|
margin: 1.25em auto;
|
|
774
936
|
}
|