@file-viewer/renderer-text 2.4.0 → 3.0.1
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/README.en.md +11 -4
- package/README.md +11 -4
- package/dist/code.d.ts +3 -5
- package/dist/code.js +176 -35
- package/dist/codeStyle.d.ts +1 -1
- package/dist/codeStyle.js +18 -4
- package/dist/gitBundle.js +10 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/largeText.js +170 -16
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +40 -40
- package/dist/optionalCapabilities.d.ts +21 -0
- package/dist/optionalCapabilities.js +19 -0
- package/dist/patch.js +9 -15
- package/dist/prettyPrint.d.ts +35 -0
- package/dist/prettyPrint.js +232 -0
- package/dist/sanitizeHtml.d.ts +5 -0
- package/dist/sanitizeHtml.js +47 -0
- package/file-viewer.capability.json +12 -0
- package/package.json +15 -8
package/dist/largeText.js
CHANGED
|
@@ -8,6 +8,7 @@ const LARGE_TEXT_INDEX_YIELD_BYTES = 4 * 1024 * 1024;
|
|
|
8
8
|
const LARGE_TEXT_SEARCH_CHUNK_BYTES = 256 * 1024;
|
|
9
9
|
const LARGE_TEXT_MAX_SCROLL_HEIGHT = 8000000;
|
|
10
10
|
const LARGE_TEXT_BASE_LINE_HEIGHT = 22.1;
|
|
11
|
+
const LARGE_TEXT_MEASUREMENT_BLOCK_LINES = 256;
|
|
11
12
|
const clamp = (value, minimum, maximum) => {
|
|
12
13
|
return Number.isFinite(value)
|
|
13
14
|
? Math.max(minimum, Math.min(maximum, value))
|
|
@@ -234,8 +235,6 @@ export const shouldVirtualizeMarkdownBuffer = (buffer, context) => {
|
|
|
234
235
|
const largeTextStyle = `
|
|
235
236
|
.code-viewer--virtual{height:100%;min-height:240px;display:flex;flex-direction:column;overflow:hidden}
|
|
236
237
|
.code-viewer--virtual .code-toolbar{flex:0 0 42px}
|
|
237
|
-
.code-toolbar-meta{display:inline-flex;min-width:0;align-items:center;justify-content:flex-end;gap:10px;white-space:nowrap}
|
|
238
|
-
.code-toolbar-meta span{overflow:hidden;text-overflow:ellipsis}
|
|
239
238
|
.code-virtual-scroll{position:relative;flex:1 1 auto;min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;contain:strict;background:var(--code-bg)}
|
|
240
239
|
.code-virtual-spacer{position:relative;min-width:100%}
|
|
241
240
|
.code-virtual-window{position:absolute;top:0;left:0;min-width:100%;will-change:transform}
|
|
@@ -248,9 +247,15 @@ const largeTextStyle = `
|
|
|
248
247
|
.code-line-segments button{width:22px;height:18px;padding:0;border:1px solid var(--code-border);border-radius:4px;background:var(--code-bg);color:var(--code-muted);font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;cursor:pointer}
|
|
249
248
|
.code-line-segments button:disabled{cursor:not-allowed;opacity:.4}
|
|
250
249
|
.code-line-segments span{min-width:64px;color:var(--code-muted);font-size:11px;line-height:1;text-align:center}
|
|
250
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-scroll{overflow-x:hidden}
|
|
251
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-spacer,.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-window{width:100%;min-width:0}
|
|
252
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-line{width:100%;height:auto;min-height:var(--code-line-height,22.1px);min-width:0;align-items:flex-start;white-space:normal;contain:layout paint style}
|
|
253
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-number{align-self:stretch}
|
|
254
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-line-segments{flex:0 0 auto;align-self:stretch;height:auto}
|
|
255
|
+
.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-content{display:block;min-width:0;flex:1 1 auto;padding:0 18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
|
|
251
256
|
`;
|
|
252
257
|
export default async function renderLargeText(buffer, target, type = 'txt', context) {
|
|
253
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
258
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
254
259
|
const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
|
|
255
260
|
const documentRef = target.ownerDocument;
|
|
256
261
|
const sourceBytes = new Uint8Array(buffer);
|
|
@@ -268,9 +273,11 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
268
273
|
// Undefined preserves the large-text renderer's pre-option behavior. An
|
|
269
274
|
// explicit boolean has the same meaning in both regular and virtual views.
|
|
270
275
|
const showLineNumbers = ((_k = (_j = context === null || context === void 0 ? void 0 : context.options) === null || _j === void 0 ? void 0 : _j.text) === null || _k === void 0 ? void 0 : _k.lineNumbers) !== false;
|
|
276
|
+
const wrapLongLines = ((_m = (_l = context === null || context === void 0 ? void 0 : context.options) === null || _l === void 0 ? void 0 : _l.text) === null || _m === void 0 ? void 0 : _m.wrapLongLines) === true;
|
|
271
277
|
let disposed = false;
|
|
272
278
|
let zoom = 1;
|
|
273
279
|
let scheduledFrame = 0;
|
|
280
|
+
let measurementFrame = 0;
|
|
274
281
|
let lastWindowStart = -1;
|
|
275
282
|
let activeLine = -1;
|
|
276
283
|
let searchGeneration = 0;
|
|
@@ -279,13 +286,17 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
279
286
|
const style = documentRef.createElement('style');
|
|
280
287
|
style.textContent = `${codeStyle}\n${largeTextStyle}`;
|
|
281
288
|
const root = documentRef.createElement('div');
|
|
282
|
-
root.className =
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
root.className = [
|
|
290
|
+
'code-viewer',
|
|
291
|
+
'code-viewer--virtual',
|
|
292
|
+
showLineNumbers ? 'code-viewer--line-numbers' : '',
|
|
293
|
+
wrapLongLines ? 'code-viewer--wrap-lines' : ''
|
|
294
|
+
].filter(Boolean).join(' ');
|
|
285
295
|
root.dataset.viewerZoomProvider = 'code';
|
|
286
296
|
root.dataset.viewerSearchProvider = 'code-virtual';
|
|
287
297
|
root.dataset.textToolbar = String(showToolbar);
|
|
288
298
|
root.dataset.lineNumbers = String(showLineNumbers);
|
|
299
|
+
root.dataset.wrapLongLines = String(wrapLongLines);
|
|
289
300
|
root.dataset.textEncoding = source.encoding;
|
|
290
301
|
const toolbar = documentRef.createElement('div');
|
|
291
302
|
toolbar.className = 'code-toolbar';
|
|
@@ -302,7 +313,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
302
313
|
root.append(toolbar);
|
|
303
314
|
}
|
|
304
315
|
target.replaceChildren(style, root);
|
|
305
|
-
(
|
|
316
|
+
(_o = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _o === void 0 ? void 0 : _o.call(context);
|
|
306
317
|
const index = await buildLargeTextIndex(bytes, source.encoding, target, progress => {
|
|
307
318
|
if (!disposed) {
|
|
308
319
|
status.textContent = t('text.code.indexingLargeFile', { progress });
|
|
@@ -329,29 +340,154 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
329
340
|
root.append(viewport);
|
|
330
341
|
const getLineHeight = () => LARGE_TEXT_BASE_LINE_HEIGHT * zoom;
|
|
331
342
|
const getViewportHeight = () => Math.max(240, viewport.clientHeight || 600);
|
|
343
|
+
const measuredLineHeights = new Map();
|
|
344
|
+
const measuredLineHeightsByBlock = new Map();
|
|
345
|
+
const heightCorrectionTree = new Float64Array(Math.ceil(index.lineCount / LARGE_TEXT_MEASUREMENT_BLOCK_LINES) + 1);
|
|
346
|
+
const updateHeightCorrection = (blockIndex, delta) => {
|
|
347
|
+
for (let treeIndex = blockIndex + 1; treeIndex < heightCorrectionTree.length; treeIndex += treeIndex & -treeIndex) {
|
|
348
|
+
heightCorrectionTree[treeIndex] += delta;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const getHeightCorrectionBeforeBlock = (blockIndex) => {
|
|
352
|
+
let correction = 0;
|
|
353
|
+
for (let treeIndex = blockIndex; treeIndex > 0; treeIndex -= treeIndex & -treeIndex) {
|
|
354
|
+
correction += heightCorrectionTree[treeIndex];
|
|
355
|
+
}
|
|
356
|
+
return correction;
|
|
357
|
+
};
|
|
358
|
+
const getOffsetForLine = (requestedLine) => {
|
|
359
|
+
const lineIndex = clamp(Math.trunc(requestedLine), 0, index.lineCount);
|
|
360
|
+
if (!wrapLongLines || lineIndex === 0) {
|
|
361
|
+
return lineIndex * getLineHeight();
|
|
362
|
+
}
|
|
363
|
+
const blockIndex = Math.floor(lineIndex / LARGE_TEXT_MEASUREMENT_BLOCK_LINES);
|
|
364
|
+
let correction = getHeightCorrectionBeforeBlock(blockIndex);
|
|
365
|
+
const blockMeasurements = measuredLineHeightsByBlock.get(blockIndex);
|
|
366
|
+
if (blockMeasurements) {
|
|
367
|
+
for (const [measuredLine, height] of blockMeasurements) {
|
|
368
|
+
if (measuredLine >= lineIndex) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
correction += height - getLineHeight();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return (lineIndex * getLineHeight()) + correction;
|
|
375
|
+
};
|
|
376
|
+
const getTotalContentHeight = () => getOffsetForLine(index.lineCount);
|
|
332
377
|
const getWindowLineCount = () => Math.min(index.lineCount, Math.ceil(getViewportHeight() / getLineHeight()) + (overscan * 2) + 2);
|
|
333
|
-
const getSpacerHeight = () => Math.min(LARGE_TEXT_MAX_SCROLL_HEIGHT, Math.max(getViewportHeight(),
|
|
334
|
-
const usesCappedScrollHeight = () =>
|
|
378
|
+
const getSpacerHeight = () => Math.min(LARGE_TEXT_MAX_SCROLL_HEIGHT, Math.max(getViewportHeight(), getTotalContentHeight()));
|
|
379
|
+
const usesCappedScrollHeight = () => getTotalContentHeight() > LARGE_TEXT_MAX_SCROLL_HEIGHT;
|
|
335
380
|
const updateSpacerHeight = () => {
|
|
336
381
|
root.style.setProperty('--code-font-size', `${13 * zoom}px`);
|
|
337
382
|
root.style.setProperty('--code-line-height', `${getLineHeight()}px`);
|
|
338
383
|
spacer.style.height = `${getSpacerHeight()}px`;
|
|
339
384
|
};
|
|
385
|
+
const getLineAtOffset = (requestedOffset) => {
|
|
386
|
+
const offset = clamp(requestedOffset, 0, Math.max(0, getTotalContentHeight() - 1));
|
|
387
|
+
let low = 0;
|
|
388
|
+
let high = Math.max(0, index.lineCount - 1);
|
|
389
|
+
while (low < high) {
|
|
390
|
+
const middle = Math.ceil((low + high) / 2);
|
|
391
|
+
if (getOffsetForLine(middle) <= offset) {
|
|
392
|
+
low = middle;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
high = middle - 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return low;
|
|
399
|
+
};
|
|
340
400
|
const getFirstVisibleLine = () => {
|
|
341
401
|
if (!usesCappedScrollHeight()) {
|
|
342
|
-
return
|
|
402
|
+
return wrapLongLines
|
|
403
|
+
? getLineAtOffset(viewport.scrollTop)
|
|
404
|
+
: clamp(Math.floor(viewport.scrollTop / getLineHeight()), 0, index.lineCount - 1);
|
|
343
405
|
}
|
|
344
406
|
const maxScrollTop = Math.max(1, getSpacerHeight() - getViewportHeight());
|
|
345
407
|
return clamp(Math.round((viewport.scrollTop / maxScrollTop) * (index.lineCount - 1)), 0, index.lineCount - 1);
|
|
346
408
|
};
|
|
347
409
|
const getWindowOffset = (startLine, renderedLineCount) => {
|
|
348
410
|
if (!usesCappedScrollHeight()) {
|
|
349
|
-
return startLine
|
|
411
|
+
return getOffsetForLine(startLine);
|
|
350
412
|
}
|
|
351
413
|
const maxStart = Math.max(1, index.lineCount - renderedLineCount);
|
|
352
414
|
const maxOffset = Math.max(0, getSpacerHeight() - (renderedLineCount * getLineHeight()));
|
|
353
415
|
return (startLine / maxStart) * maxOffset;
|
|
354
416
|
};
|
|
417
|
+
const setMeasuredLineHeight = (lineIndex, measuredHeight) => {
|
|
418
|
+
var _a;
|
|
419
|
+
const nextHeight = Math.max(getLineHeight(), measuredHeight);
|
|
420
|
+
const previousHeight = (_a = measuredLineHeights.get(lineIndex)) !== null && _a !== void 0 ? _a : getLineHeight();
|
|
421
|
+
if (Math.abs(nextHeight - previousHeight) < 0.5) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const blockIndex = Math.floor(lineIndex / LARGE_TEXT_MEASUREMENT_BLOCK_LINES);
|
|
425
|
+
measuredLineHeights.set(lineIndex, nextHeight);
|
|
426
|
+
let blockMeasurements = measuredLineHeightsByBlock.get(blockIndex);
|
|
427
|
+
if (!blockMeasurements) {
|
|
428
|
+
blockMeasurements = new Map();
|
|
429
|
+
measuredLineHeightsByBlock.set(blockIndex, blockMeasurements);
|
|
430
|
+
}
|
|
431
|
+
blockMeasurements.set(lineIndex, nextHeight);
|
|
432
|
+
updateHeightCorrection(blockIndex, nextHeight - previousHeight);
|
|
433
|
+
return true;
|
|
434
|
+
};
|
|
435
|
+
const clearMeasuredLineHeights = () => {
|
|
436
|
+
measuredLineHeights.clear();
|
|
437
|
+
measuredLineHeightsByBlock.clear();
|
|
438
|
+
heightCorrectionTree.fill(0);
|
|
439
|
+
};
|
|
440
|
+
const scheduleWrappedMeasurement = (startLine, renderedLineCount) => {
|
|
441
|
+
var _a, _b, _c;
|
|
442
|
+
if (!wrapLongLines || disposed || renderedLineCount === 0) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const view = getWindow(target);
|
|
446
|
+
if (measurementFrame && (view === null || view === void 0 ? void 0 : view.cancelAnimationFrame)) {
|
|
447
|
+
view.cancelAnimationFrame(measurementFrame);
|
|
448
|
+
}
|
|
449
|
+
else if (measurementFrame) {
|
|
450
|
+
(_a = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _a === void 0 ? void 0 : _a.call(view, measurementFrame);
|
|
451
|
+
}
|
|
452
|
+
const measure = () => {
|
|
453
|
+
measurementFrame = 0;
|
|
454
|
+
if (disposed) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const anchorLine = getFirstVisibleLine();
|
|
458
|
+
const anchorOffset = usesCappedScrollHeight()
|
|
459
|
+
? 0
|
|
460
|
+
: viewport.scrollTop - getOffsetForLine(anchorLine);
|
|
461
|
+
let changed = false;
|
|
462
|
+
const rows = Array.from(windowElement.querySelectorAll('.code-virtual-line'));
|
|
463
|
+
for (const row of rows) {
|
|
464
|
+
const lineIndex = Number(row.dataset.line) - 1;
|
|
465
|
+
if (!Number.isInteger(lineIndex) || lineIndex < 0) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const measuredHeight = row.getBoundingClientRect().height || row.offsetHeight || 0;
|
|
469
|
+
if (measuredHeight) {
|
|
470
|
+
changed = setMeasuredLineHeight(lineIndex, measuredHeight) || changed;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!changed) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
updateSpacerHeight();
|
|
477
|
+
if (!usesCappedScrollHeight()) {
|
|
478
|
+
viewport.scrollTop = getOffsetForLine(anchorLine) + anchorOffset;
|
|
479
|
+
}
|
|
480
|
+
windowElement.style.transform = `translateY(${getWindowOffset(startLine, renderedLineCount)}px)`;
|
|
481
|
+
lastWindowStart = -1;
|
|
482
|
+
scheduleRender();
|
|
483
|
+
};
|
|
484
|
+
if (view === null || view === void 0 ? void 0 : view.requestAnimationFrame) {
|
|
485
|
+
measurementFrame = view.requestAnimationFrame(measure);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
measurementFrame = Number((_c = (_b = view === null || view === void 0 ? void 0 : view.setTimeout) === null || _b === void 0 ? void 0 : _b.call(view, measure, 0)) !== null && _c !== void 0 ? _c : setTimeout(measure, 0));
|
|
489
|
+
}
|
|
490
|
+
};
|
|
355
491
|
const appendHighlightedContent = (content, text, query) => {
|
|
356
492
|
if (!query) {
|
|
357
493
|
content.textContent = text || ' ';
|
|
@@ -383,6 +519,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
383
519
|
const row = documentRef.createElement('div');
|
|
384
520
|
row.className = 'code-virtual-line';
|
|
385
521
|
row.dataset.line = String(line.lineIndex + 1);
|
|
522
|
+
row.dataset.logicalLine = String(line.lineIndex + 1);
|
|
386
523
|
if (line.lineIndex === activeLine) {
|
|
387
524
|
row.classList.add('code-virtual-line--match');
|
|
388
525
|
}
|
|
@@ -439,6 +576,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
439
576
|
}
|
|
440
577
|
windowElement.replaceChildren(fragment);
|
|
441
578
|
windowElement.style.transform = `translateY(${getWindowOffset(startLine, lines.length)}px)`;
|
|
579
|
+
scheduleWrappedMeasurement(startLine, lines.length);
|
|
442
580
|
};
|
|
443
581
|
const scheduleRender = () => {
|
|
444
582
|
var _a, _b;
|
|
@@ -470,7 +608,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
470
608
|
: 0;
|
|
471
609
|
}
|
|
472
610
|
else {
|
|
473
|
-
viewport.scrollTop = lineIndex
|
|
611
|
+
viewport.scrollTop = getOffsetForLine(lineIndex);
|
|
474
612
|
}
|
|
475
613
|
lastWindowStart = -1;
|
|
476
614
|
renderWindow(true);
|
|
@@ -592,6 +730,9 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
592
730
|
const setZoom = (scale) => {
|
|
593
731
|
const firstVisibleLine = getFirstVisibleLine();
|
|
594
732
|
zoom = clampZoom(scale);
|
|
733
|
+
if (wrapLongLines) {
|
|
734
|
+
clearMeasuredLineHeights();
|
|
735
|
+
}
|
|
595
736
|
updateSpacerHeight();
|
|
596
737
|
scrollToLine(firstVisibleLine);
|
|
597
738
|
zoomEmitter.emit();
|
|
@@ -612,7 +753,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
612
753
|
getState: getZoomState,
|
|
613
754
|
subscribe: zoomEmitter.subscribe
|
|
614
755
|
});
|
|
615
|
-
(
|
|
756
|
+
(_p = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _p === void 0 ? void 0 : _p.call(context, { print: false, exportHtml: false });
|
|
616
757
|
viewport.addEventListener('scroll', scheduleRender, { passive: true });
|
|
617
758
|
viewport.addEventListener('click', event => {
|
|
618
759
|
var _a, _b;
|
|
@@ -638,9 +779,16 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
638
779
|
lineSegments.set(lineIndex, clamp(next, 0, segmentCount - 1));
|
|
639
780
|
renderWindow(true);
|
|
640
781
|
});
|
|
641
|
-
const ResizeObserverCtor = (
|
|
782
|
+
const ResizeObserverCtor = (_q = getWindow(target)) === null || _q === void 0 ? void 0 : _q.ResizeObserver;
|
|
642
783
|
const resizeObserver = ResizeObserverCtor
|
|
643
784
|
? new ResizeObserverCtor(() => {
|
|
785
|
+
if (wrapLongLines) {
|
|
786
|
+
const firstVisibleLine = getFirstVisibleLine();
|
|
787
|
+
clearMeasuredLineHeights();
|
|
788
|
+
updateSpacerHeight();
|
|
789
|
+
scrollToLine(firstVisibleLine);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
644
792
|
updateSpacerHeight();
|
|
645
793
|
renderWindow(true);
|
|
646
794
|
})
|
|
@@ -651,7 +799,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
651
799
|
return {
|
|
652
800
|
$el: target,
|
|
653
801
|
unmount() {
|
|
654
|
-
var _a, _b;
|
|
802
|
+
var _a, _b, _c;
|
|
655
803
|
disposed = true;
|
|
656
804
|
searchGeneration += 1;
|
|
657
805
|
const view = getWindow(target);
|
|
@@ -661,11 +809,17 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
|
|
|
661
809
|
else if (scheduledFrame) {
|
|
662
810
|
(_a = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _a === void 0 ? void 0 : _a.call(view, scheduledFrame);
|
|
663
811
|
}
|
|
812
|
+
if (measurementFrame && (view === null || view === void 0 ? void 0 : view.cancelAnimationFrame)) {
|
|
813
|
+
view.cancelAnimationFrame(measurementFrame);
|
|
814
|
+
}
|
|
815
|
+
else if (measurementFrame) {
|
|
816
|
+
(_b = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _b === void 0 ? void 0 : _b.call(view, measurementFrame);
|
|
817
|
+
}
|
|
664
818
|
resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
|
|
665
819
|
viewport.removeEventListener('scroll', scheduleRender);
|
|
666
820
|
unregisterFileViewerSearchProvider(root);
|
|
667
821
|
unregisterFileViewerZoomProvider(root);
|
|
668
|
-
(
|
|
822
|
+
(_c = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _c === void 0 ? void 0 : _c.call(context, null);
|
|
669
823
|
target.replaceChildren();
|
|
670
824
|
}
|
|
671
825
|
};
|
package/dist/markdown.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
|
|
2
|
+
export declare const sanitizeMermaidSvg: (documentRef: Document, svg: string) => SVGSVGElement;
|
|
2
3
|
export declare const stripMarkdownFrontmatter: (text: string) => string;
|
|
3
4
|
export default function renderMarkdown(buffer: ArrayBuffer, target: HTMLDivElement, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
|
package/dist/markdown.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import createDOMPurify from 'dompurify';
|
|
3
|
-
import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, resolveFileViewerFitScale, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
3
|
+
import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, assertFileViewerMermaidSourceHasNoExternalResources, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, resolveFileViewerFitScale, unregisterFileViewerZoomProvider, sanitizeFileViewerSvgResources, } from '@file-viewer/core';
|
|
4
|
+
import { getFileViewerMermaidLoader } from './optionalCapabilities.js';
|
|
5
|
+
import { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
|
|
6
|
+
const sanitizeMarkdownHtml = sanitizeFileViewerRichHtml;
|
|
7
|
+
const purifierByDocument = new WeakMap();
|
|
8
|
+
const getMarkdownPurifier = (documentRef) => {
|
|
9
|
+
const cached = purifierByDocument.get(documentRef);
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
10
12
|
const windowRef = documentRef.defaultView;
|
|
11
|
-
if (!windowRef)
|
|
12
|
-
return
|
|
13
|
-
}
|
|
13
|
+
if (!windowRef)
|
|
14
|
+
return null;
|
|
14
15
|
const purifier = createDOMPurify(windowRef);
|
|
15
|
-
if (!purifier.isSupported)
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
return purifier
|
|
19
|
-
RETURN_DOM_FRAGMENT: true,
|
|
20
|
-
USE_PROFILES: { html: true },
|
|
21
|
-
ADD_ATTR: ['target'],
|
|
22
|
-
FORBID_TAGS: ['style', 'iframe', 'object', 'embed', 'form'],
|
|
23
|
-
FORBID_ATTR: ['style', 'srcdoc'],
|
|
24
|
-
});
|
|
16
|
+
if (!purifier.isSupported)
|
|
17
|
+
return null;
|
|
18
|
+
purifierByDocument.set(documentRef, purifier);
|
|
19
|
+
return purifier;
|
|
25
20
|
};
|
|
26
21
|
const hardenMarkdownLinks = (root) => {
|
|
27
22
|
root.querySelectorAll('a[target]').forEach(anchor => {
|
|
@@ -108,35 +103,38 @@ const isDarkTheme = (documentRef, theme) => {
|
|
|
108
103
|
}
|
|
109
104
|
return Boolean((_b = (_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.matchMedia) === null || _b === void 0 ? void 0 : _b.call(_a, '(prefers-color-scheme: dark)').matches);
|
|
110
105
|
};
|
|
111
|
-
const sanitizeMermaidSvg = (documentRef, svg) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const parseError = parsed.querySelector('parsererror');
|
|
116
|
-
if (parseError) {
|
|
117
|
-
throw new Error(parseError.textContent || 'Unable to parse the Mermaid SVG.');
|
|
106
|
+
export const sanitizeMermaidSvg = (documentRef, svg) => {
|
|
107
|
+
const purifier = getMarkdownPurifier(documentRef);
|
|
108
|
+
if (!purifier) {
|
|
109
|
+
throw new Error('Unable to initialize the Mermaid SVG sanitizer.');
|
|
118
110
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
else if (/^(?:href|xlink:href|src)$/i.test(attribute.name) && /^\s*javascript:/i.test(attribute.value)) {
|
|
126
|
-
node.removeAttribute(attribute.name);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
111
|
+
const fragment = purifier.sanitize(svg, {
|
|
112
|
+
RETURN_DOM_FRAGMENT: true,
|
|
113
|
+
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
114
|
+
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
|
|
115
|
+
FORBID_ATTR: ['srcdoc'],
|
|
129
116
|
});
|
|
130
|
-
|
|
117
|
+
const root = fragment.querySelector('svg');
|
|
118
|
+
if (!root) {
|
|
119
|
+
throw new Error('Unable to parse the Mermaid SVG.');
|
|
120
|
+
}
|
|
121
|
+
sanitizeFileViewerSvgResources(fragment);
|
|
122
|
+
return documentRef.importNode(root, true);
|
|
131
123
|
};
|
|
132
124
|
const renderMermaidSvg = async (documentRef, source, theme) => {
|
|
133
125
|
const render = async () => {
|
|
134
|
-
|
|
126
|
+
assertFileViewerMermaidSourceHasNoExternalResources(source);
|
|
127
|
+
const loadMermaid = getFileViewerMermaidLoader();
|
|
128
|
+
if (!loadMermaid) {
|
|
129
|
+
throw new Error('Mermaid support is opt-in. Run `npx file-viewer-cli add mermaid-markdown --write`, then `npx file-viewer-cli install --yes`.');
|
|
130
|
+
}
|
|
131
|
+
const mermaidModule = await loadMermaid();
|
|
135
132
|
const mermaid = mermaidModule.default;
|
|
136
133
|
const id = `file-viewer-markdown-mermaid-${Date.now()}-${mermaidRenderSequence += 1}`;
|
|
137
134
|
mermaid.initialize({
|
|
138
135
|
startOnLoad: false,
|
|
139
136
|
securityLevel: 'strict',
|
|
137
|
+
htmlLabels: false,
|
|
140
138
|
theme: isDarkTheme(documentRef, theme) ? 'dark' : 'default',
|
|
141
139
|
});
|
|
142
140
|
const rendered = await mermaid.render(id, source);
|
|
@@ -171,7 +169,9 @@ const renderEmbeddedMermaid = async (article, theme) => {
|
|
|
171
169
|
const message = documentRef.createElement('p');
|
|
172
170
|
message.className = 'markdown-mermaid-error';
|
|
173
171
|
message.setAttribute('role', 'alert');
|
|
174
|
-
message.textContent =
|
|
172
|
+
message.textContent = error instanceof Error && error.message.includes('npx file-viewer-cli')
|
|
173
|
+
? error.message
|
|
174
|
+
: 'Mermaid diagram could not be rendered. The source is shown above.';
|
|
175
175
|
pre.after(message);
|
|
176
176
|
console.warn('[file-viewer] Unable to render embedded Mermaid diagram.', error);
|
|
177
177
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type FileViewerMermaidModule = {
|
|
2
|
+
default: {
|
|
3
|
+
initialize(options: Record<string, unknown>): void;
|
|
4
|
+
render(id: string, source: string): Promise<{
|
|
5
|
+
svg: string;
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type FileViewerMermaidLoader = () => Promise<FileViewerMermaidModule>;
|
|
10
|
+
export type FileViewerDiffToHtml = (input: string, options: Record<string, unknown>) => string;
|
|
11
|
+
export type FileViewerPakoModule = typeof import('pako');
|
|
12
|
+
/**
|
|
13
|
+
* Enables embedded Mermaid blocks without making Mermaid part of the base text
|
|
14
|
+
* renderer dependency closure. Capability packs call this once at startup.
|
|
15
|
+
*/
|
|
16
|
+
export declare const registerFileViewerMermaidLoader: (loader: FileViewerMermaidLoader | null) => void;
|
|
17
|
+
export declare const getFileViewerMermaidLoader: () => FileViewerMermaidLoader | null;
|
|
18
|
+
export declare const registerFileViewerDiffToHtml: (renderer: FileViewerDiffToHtml | null) => void;
|
|
19
|
+
export declare const getFileViewerDiffToHtml: () => FileViewerDiffToHtml | null;
|
|
20
|
+
export declare const registerFileViewerPakoLoader: (loader: (() => Promise<FileViewerPakoModule>) | null) => void;
|
|
21
|
+
export declare const getFileViewerPakoLoader: () => (() => Promise<FileViewerPakoModule>) | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
let mermaidLoader = null;
|
|
2
|
+
let diffToHtml = null;
|
|
3
|
+
let pakoLoader = null;
|
|
4
|
+
/**
|
|
5
|
+
* Enables embedded Mermaid blocks without making Mermaid part of the base text
|
|
6
|
+
* renderer dependency closure. Capability packs call this once at startup.
|
|
7
|
+
*/
|
|
8
|
+
export const registerFileViewerMermaidLoader = (loader) => {
|
|
9
|
+
mermaidLoader = loader;
|
|
10
|
+
};
|
|
11
|
+
export const getFileViewerMermaidLoader = () => mermaidLoader;
|
|
12
|
+
export const registerFileViewerDiffToHtml = (renderer) => {
|
|
13
|
+
diffToHtml = renderer;
|
|
14
|
+
};
|
|
15
|
+
export const getFileViewerDiffToHtml = () => diffToHtml;
|
|
16
|
+
export const registerFileViewerPakoLoader = (loader) => {
|
|
17
|
+
pakoLoader = loader;
|
|
18
|
+
};
|
|
19
|
+
export const getFileViewerPakoLoader = () => pakoLoader;
|
package/dist/patch.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
|
|
2
|
-
import {
|
|
2
|
+
import { getFileViewerDiffToHtml } from './optionalCapabilities.js';
|
|
3
|
+
import { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
|
|
3
4
|
const patchStyle = `
|
|
4
5
|
.patch-viewer{min-height:100%;--patch-bg:#f6f8fa;--patch-surface:#fff;--patch-border:rgba(31,35,40,.12);--patch-text:#24292f;--patch-muted:#57606a;--patch-add:#dafbe1;--patch-del:#ffebe9;--patch-info:#ddf4ff;--patch-font-size:13px;background:var(--patch-bg);color:var(--patch-text);box-sizing:border-box}
|
|
5
6
|
.patch-toolbar{position:sticky;top:0;z-index:2;display:flex;height:46px;align-items:center;justify-content:space-between;gap:12px;padding:0 16px;border-bottom:1px solid var(--patch-border);background:rgba(255,255,255,.92);backdrop-filter:blur(12px);box-sizing:border-box}
|
|
@@ -52,18 +53,6 @@ const countFiles = (text) => {
|
|
|
52
53
|
}
|
|
53
54
|
return ((_a = text.match(/^---\s+/gm)) === null || _a === void 0 ? void 0 : _a.length) || 1;
|
|
54
55
|
};
|
|
55
|
-
const escapeHtml = (value) => {
|
|
56
|
-
return value.replace(/[&<>"']/g, char => {
|
|
57
|
-
const entities = {
|
|
58
|
-
'&': '&',
|
|
59
|
-
'<': '<',
|
|
60
|
-
'>': '>',
|
|
61
|
-
'"': '"',
|
|
62
|
-
"'": '''
|
|
63
|
-
};
|
|
64
|
-
return entities[char];
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
56
|
export default async function renderPatch(buffer, target, type = 'patch', context) {
|
|
68
57
|
var _a, _b;
|
|
69
58
|
const documentRef = target.ownerDocument || document;
|
|
@@ -76,16 +65,21 @@ export default async function renderPatch(buffer, target, type = 'patch', contex
|
|
|
76
65
|
toolbar.append(createElement(documentRef, 'span', undefined, type.toUpperCase()), createElement(documentRef, 'strong', undefined, `${countFiles(text)} files · side-by-side`));
|
|
77
66
|
const body = createElement(documentRef, 'div', 'patch-body');
|
|
78
67
|
try {
|
|
79
|
-
|
|
68
|
+
const diffToHtml = getFileViewerDiffToHtml();
|
|
69
|
+
if (!diffToHtml) {
|
|
70
|
+
throw new Error('Patch side-by-side rendering is opt-in. Run `npx file-viewer-cli add text-tools --write`, then `npx file-viewer-cli install --yes`.');
|
|
71
|
+
}
|
|
72
|
+
const rendered = diffToHtml(text, {
|
|
80
73
|
drawFileList: true,
|
|
81
74
|
matching: 'lines',
|
|
82
75
|
outputFormat: 'side-by-side',
|
|
83
76
|
renderNothingWhenEmpty: false
|
|
84
77
|
});
|
|
78
|
+
body.replaceChildren(sanitizeFileViewerRichHtml(documentRef, rendered, { allowSvg: true }));
|
|
85
79
|
}
|
|
86
80
|
catch {
|
|
87
81
|
const fallback = createElement(documentRef, 'pre', 'patch-fallback');
|
|
88
|
-
fallback.
|
|
82
|
+
fallback.textContent = text;
|
|
89
83
|
body.replaceChildren(fallback);
|
|
90
84
|
}
|
|
91
85
|
root.append(toolbar, body);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FileViewerTextOptions } from '@file-viewer/core';
|
|
2
|
+
export declare const DEFAULT_PRETTY_PRINT_MAX_BYTES: number;
|
|
3
|
+
export type FileViewerPrettyPrintReason = 'formatted' | 'disabled' | 'unsupported' | 'too-large' | 'whitespace-sensitive' | 'failed' | 'aborted';
|
|
4
|
+
export interface FileViewerPrettyPrintResult {
|
|
5
|
+
text: string;
|
|
6
|
+
formatted: boolean;
|
|
7
|
+
reason: FileViewerPrettyPrintReason;
|
|
8
|
+
parser?: string;
|
|
9
|
+
sourceByteLength: number;
|
|
10
|
+
maxBytes: number;
|
|
11
|
+
}
|
|
12
|
+
type PrettierPlugin = Record<string, unknown>;
|
|
13
|
+
type PrettierRuntime = {
|
|
14
|
+
format: (source: string, options: Record<string, unknown>) => string | Promise<string>;
|
|
15
|
+
plugins: PrettierPlugin[];
|
|
16
|
+
};
|
|
17
|
+
type PrettierPluginName = 'babel' | 'estree' | 'typescript' | 'postcss' | 'html' | 'markdown' | 'yaml' | 'graphql' | 'xml';
|
|
18
|
+
interface PrettierLanguageDefinition {
|
|
19
|
+
parser: string;
|
|
20
|
+
plugins: readonly PrettierPluginName[];
|
|
21
|
+
resolvePlugins?: (source: string) => readonly PrettierPluginName[];
|
|
22
|
+
options?: Readonly<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
export type FileViewerPrettierRuntimeLoader = (definition: PrettierLanguageDefinition, source: string) => Promise<PrettierRuntime>;
|
|
25
|
+
export declare const resolveFileViewerPrettyPrintMaxBytes: (options?: FileViewerTextOptions) => number;
|
|
26
|
+
export declare const supportsFileViewerPrettyPrint: (extension: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Formats a decoded display representation without mutating the source buffer.
|
|
29
|
+
*
|
|
30
|
+
* Parser support and byte limits are resolved before the Prettier runtime or
|
|
31
|
+
* any parser plugin is imported. Failures intentionally return the original
|
|
32
|
+
* source so malformed or unsupported uploads remain previewable.
|
|
33
|
+
*/
|
|
34
|
+
export declare const formatFileViewerTextForDisplay: (source: string, extension: string, options?: FileViewerTextOptions, signal?: AbortSignal, runtimeLoader?: FileViewerPrettierRuntimeLoader) => Promise<FileViewerPrettyPrintResult>;
|
|
35
|
+
export {};
|