(null);
+ const rowVirtualizer = useVirtualizer({
+ count: lines.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 25
+ });
useEffect(() => {
// Ignore empty urls
if (!src || src === '#') return;
const controller = new AbortController();
-
- fetch(src, { mode: 'cors', signal: controller.signal })
+ fetch(src, {
+ mode: 'cors',
+ signal: controller.signal
+ })
.then(async (response) => {
if (!response.ok) throw new Error(`Invalid response: ${response.statusText}`);
- const text = await response.text();
-
- if (controller.signal.aborted) return;
-
+ if (!response.body) return;
onLoad?.(new UIEvent('load', {}));
- setTextContent(text);
- if (codeExtension) {
- try {
- const env = await worker.highlight(text, codeExtension);
- if (env && !controller.signal.aborted) {
- const match = text.match(NEW_LINE_EXP);
- setHighlight({
- ...env,
- length: (match ? match.length + 1 : 1) + 1
- });
- }
- } catch (error) {
- console.error(error);
- }
- }
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
+ const ingestLines = async () => {
+ const { done, value } = await reader.read();
+ if (done) return;
+
+ const chunks = value.split('\n');
+ setLines((lines) => [...lines, ...chunks]);
+
+ if (isSidebarPreview) return;
+
+ await ingestLines();
+ };
+ ingestLines();
})
.catch((error) => {
if (!controller.signal.aborted)
@@ -64,40 +61,94 @@ export const TextViewer = memo(
});
return () => controller.abort();
- }, [src, onError, onLoad, codeExtension]);
+ }, [src, onError, onLoad, codeExtension, isSidebarPreview]);
return (
-
- {highlight ? (
- <>
-
- {Array.from(highlight, (_, i) => (
-
- {i + 1}
-
- ))}
-
-
+
+ {rowVirtualizer.getVirtualItems().map((row) => (
+
- >
- ) : (
- textContent
- )}
+ ))}
+
);
}
);
+
+function TextRow({
+ codeExtension,
+ row,
+ content
+}: {
+ codeExtension?: string;
+ row: VirtualItem;
+ content: string;
+}) {
+ const contentRef = useRef(null);
+
+ useEffect(() => {
+ if (contentRef.current) {
+ const cb: IntersectionObserverCallback = (events) => {
+ for (const event of events) {
+ if (
+ !event.isIntersecting ||
+ contentRef.current?.getAttribute('data-highlighted') === 'true'
+ )
+ continue;
+ contentRef.current?.setAttribute('data-highlighted', 'true');
+ Prism.highlightElement(event.target, false); // Prism's async seems to be broken
+
+ // With this class present TOML headers are broken Eg. `[dependencies]` will format over multiple lines
+ const children = contentRef.current?.children;
+ if (children) {
+ for (const elem of children) {
+ elem.classList.remove('table');
+ }
+ }
+ }
+ };
+
+ new IntersectionObserver(cb).observe(contentRef.current);
+ }
+ }, []);
+
+ return (
+
+ {codeExtension && (
+
+ {row.index + 1}
+
+ )}
+
+ {content}
+
+
+ );
+}
diff --git a/interface/components/TextViewer/prism.ts b/interface/components/TextViewer/prism.ts
index d364a31e2..489eb466d 100644
--- a/interface/components/TextViewer/prism.ts
+++ b/interface/components/TextViewer/prism.ts
@@ -2,6 +2,12 @@
// WARNING: Import order matters
+window.Prism = window.Prism || {};
+Prism.manual = true;
+
+import "prismjs";
+import './prism.css';
+
// Languages
// Do not include default ones: markup, html, xml, svg, mathml, ssml, atom, rss, css, clike, javascript, js
import 'prismjs/components/prism-applescript.js';
@@ -52,3 +58,30 @@ import 'prismjs/components/prism-typoscript.js';
import 'prismjs/components/prism-vala.js';
import 'prismjs/components/prism-yaml.js';
import 'prismjs/components/prism-zig.js';
+
+// Mapping between extensions and prismjs language identifier
+// Only for those that are not already internally resolved by prismjs
+// https://prismjs.com/#supported-languages
+export const languageMapping = Object.entries({
+ applescript: ['scpt', 'scptd'],
+ // This is not entirely correct, but better than nothing:
+ // https://github.com/PrismJS/prism/issues/3656
+ // https://github.com/PrismJS/prism/issues/3660
+ sh: ['zsh', 'fish'],
+ c: ['h'],
+ cpp: ['hpp'],
+ js: ['mjs'],
+ crystal: ['cr'],
+ cs: ['csx'],
+ makefile: ['make'],
+ nim: ['nims'],
+ objc: ['m', 'mm'],
+ ocaml: ['ml', 'mli', 'mll', 'mly'],
+ perl: ['pl'],
+ php: ['php', 'php1', 'php2', 'php3', 'php4', 'php5', 'php6', 'phps', 'phpt', 'phtml'],
+ powershell: ['ps1', 'psd1', 'psm1'],
+ rust: ['rs']
+}).reduce