ui: Linting & Formatting scripts (#26819)

This commit is contained in:
Aleksander Grygier
2026-08-10 08:38:37 +02:00
committed by GitHub
parent 1e396e72a8
commit 92d1bb0c99
538 changed files with 8806 additions and 6036 deletions
@@ -1,85 +1,85 @@
<script lang="ts">
import '$styles/katex-custom.scss';
import {
getCodeInfoFromTarget,
getHastNodeId,
getMdastNodeHash,
isAppendMode
} from './markdown-utils';
import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks';
import { rehypeFileBadge } from './plugins/rehype/file-badge';
import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
import { rehypeSvgPre } from './plugins/rehype/svg-pre';
import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
import { remarkLiteralHtml } from './plugins/remark/literal-html';
import { browser } from '$app/environment';
import {
ActionIconCopyToClipboard,
CodeBlockActions,
DialogCodePreview,
DialogMermaidPreview
} from '$lib/components/app';
import {
BOOL_TRUE_STRING,
CODE_BLOCK_HEADER_CLASS,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED,
DIAGRAM_VIEW_SOURCE,
IMAGE_NOT_ERROR_BOUND_SELECTOR,
MERMAID_BLOCK_CLASS,
MERMAID_LANGUAGE,
MERMAID_RENDERED_ATTR,
MERMAID_SYNTAX_ATTR,
MERMAID_WRAPPER_CLASS,
SETTINGS_KEYS,
SVG_BLOCK_CLASS,
SVG_INLINE_SHADOW_STYLE,
SVG_LANGUAGE,
SVG_RENDERED_ATTR,
SVG_SOURCE_ATTR,
SVG_TAG_PREFIX,
SVG_WRAPPER_CLASS,
TOGGLE_SOURCE_BTN_CLASS,
XML_LANGUAGE
} from '$lib/constants';
import { ColorMode, UrlProtocol } from '$lib/enums';
import { FileTypeText } from '$lib/enums/files.enums';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import { config } from '$lib/stores/settings.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
import {
copyCodeToClipboard,
copyToClipboard,
getImageErrorFallbackHtml,
preprocessLaTeX,
splitGluedClosingCodeFences
} from '$lib/utils';
import { detectIncompleteCodeBlock, highlightCode, type IncompleteCodeBlock } from '$lib/utils';
import { sanitizeSvg } from '$lib/utils/sanitize-svg';
import { mountSvgShadow } from '$lib/utils/svg-shadow';
import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
import { all as lowlightAll } from 'lowlight';
import type { Root as MdastRoot } from 'mdast';
import { mode } from 'mode-watcher';
import rehypeHighlight from 'rehype-highlight';
import rehypeKatex from 'rehype-katex';
import rehypeStringify from 'rehype-stringify';
import { remark } from 'remark';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeHighlight from 'rehype-highlight';
import { all as lowlightAll } from 'lowlight';
import remarkRehype from 'remark-rehype';
import rehypeKatex from 'rehype-katex';
import rehypeStringify from 'rehype-stringify';
import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
import type { Root as MdastRoot } from 'mdast';
import { browser } from '$app/environment';
import { onDestroy, tick } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
import { rehypeFileBadge } from './plugins/rehype/file-badge';
import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
import { rehypeSvgPre } from './plugins/rehype/svg-pre';
import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks';
import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
import { remarkLiteralHtml } from './plugins/remark/literal-html';
import {
getHastNodeId,
getMdastNodeHash,
isAppendMode,
getCodeInfoFromTarget
} from './markdown-utils';
import {
preprocessLaTeX,
getImageErrorFallbackHtml,
copyCodeToClipboard,
copyToClipboard,
splitGluedClosingCodeFences
} from '$lib/utils';
import {
IMAGE_NOT_ERROR_BOUND_SELECTOR,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
BOOL_TRUE_STRING,
SETTINGS_KEYS,
CODE_BLOCK_HEADER_CLASS,
MERMAID_WRAPPER_CLASS,
MERMAID_BLOCK_CLASS,
MERMAID_LANGUAGE,
MERMAID_SYNTAX_ATTR,
MERMAID_RENDERED_ATTR,
SVG_WRAPPER_CLASS,
SVG_BLOCK_CLASS,
SVG_LANGUAGE,
XML_LANGUAGE,
SVG_TAG_PREFIX,
SVG_SOURCE_ATTR,
SVG_RENDERED_ATTR,
SVG_INLINE_SHADOW_STYLE,
TOGGLE_SOURCE_BTN_CLASS,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED,
DIAGRAM_VIEW_SOURCE
} from '$lib/constants';
import { ColorMode, UrlProtocol } from '$lib/enums';
import { FileTypeText } from '$lib/enums/files.enums';
import { highlightCode, detectIncompleteCodeBlock, type IncompleteCodeBlock } from '$lib/utils';
import { sanitizeSvg } from '$lib/utils/sanitize-svg';
import { mountSvgShadow } from '$lib/utils/svg-shadow';
import '$styles/katex-custom.scss';
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import { mode } from 'mode-watcher';
import {
CodeBlockActions,
DialogCodePreview,
DialogMermaidPreview,
ActionIconCopyToClipboard
} from '$lib/components/app';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
import { config } from '$lib/stores/settings.svelte';
interface Props {
attachments?: DatabaseMessageExtra[];
@@ -94,7 +94,7 @@
contentHash?: string;
}
let { content, attachments, class: className = '', disableMath = false }: Props = $props();
let { attachments, class: className = '', content, disableMath = false }: Props = $props();
let containerRef = $state<HTMLDivElement>();
let renderedBlocks = $state<MarkdownBlock[]>([]);
@@ -102,10 +102,14 @@
let incompleteCodeBlock = $state<IncompleteCodeBlock | null>(null);
const streamingSvgCode = $derived.by(() => {
const block = incompleteCodeBlock;
if (!block) return null;
if (block.language === SVG_LANGUAGE) return block.code;
if (block.language === XML_LANGUAGE && block.code.trimStart().startsWith(SVG_TAG_PREFIX))
return block.code;
return null;
});
const liveSvgHtml = $derived(streamingSvgCode !== null ? sanitizeSvg(streamingSvgCode) : '');
@@ -171,8 +175,8 @@
return proc
.use(rehypeHighlight, {
languages: lowlightAll,
aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] }
aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] },
languages: lowlightAll
}) // Add syntax highlighting
.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables
.use(rehypeEnhanceLinks) // Add target="_blank" to links
@@ -214,6 +218,7 @@
if (!browser) return;
const existingTheme = document.getElementById(themeStyleId);
existingTheme?.remove();
}
@@ -226,9 +231,11 @@
if (!browser) return;
const existingTheme = document.getElementById(themeStyleId);
existingTheme?.remove();
const style = document.createElement('style');
style.id = themeStyleId;
style.textContent = isDark ? githubDarkCss : githubLightCss;
@@ -254,19 +261,19 @@
index: number
): Promise<{ html: string; hash: string }> {
const hash = getMdastNodeHash(node, index);
const cached = transformCache.get(hash);
if (cached) {
return { html: cached, hash };
return { hash, html: cached };
}
const singleNodeRoot = { type: 'root', children: [node] };
const singleNodeRoot = { children: [node], type: 'root' };
const transformedRoot = (await processorInstance.run(singleNodeRoot as MdastRoot)) as HastRoot;
const html = processorInstance.stringify(transformedRoot);
transformCache.set(hash, html);
return { html, hash };
return { hash, html };
}
/**
@@ -358,6 +365,7 @@
unstableBlockHtml = '';
incompleteCodeBlock = null;
previousContent = '';
return;
}
@@ -374,7 +382,6 @@
const ast = processorInstance.parse(normalizedPrefix) as MdastRoot;
const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
const nextBlocks: MarkdownBlock[] = [];
// Check if we're in append mode for cache reuse
const appendMode = isAppendMode(prefixMarkdown, previousContent);
const previousBlockCount = appendMode ? renderedBlocks.length : 0;
@@ -396,13 +403,13 @@
}
// Transform this block (with caching)
const { html, hash } = await transformMdastNode(processorInstance, child, index);
const { hash, html } = await transformMdastNode(processorInstance, child, index);
const id = getHastNodeId(
{ position: (child as { position?: unknown }).position } as HastRootContent,
index
);
nextBlocks.push({ id, html, contentHash: hash });
nextBlocks.push({ contentHash: hash, html, id });
}
renderedBlocks = nextBlocks;
@@ -426,7 +433,6 @@
const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
const stableCount = Math.max(mdastChildren.length - 1, 0);
const nextBlocks: MarkdownBlock[] = [];
// Check if we're in append mode for cache reuse
const appendMode = isAppendMode(markdown, previousContent);
const previousBlockCount = appendMode ? renderedBlocks.length : 0;
@@ -438,6 +444,7 @@
if (appendMode && index < previousBlockCount) {
const prevBlock = renderedBlocks[index];
const currentHash = getMdastNodeHash(child, index);
if (prevBlock?.contentHash === currentHash) {
nextBlocks.push(prevBlock);
@@ -446,20 +453,20 @@
}
// Transform this block (with caching)
const { html, hash } = await transformMdastNode(processorInstance, child, index);
const { hash, html } = await transformMdastNode(processorInstance, child, index);
const id = getHastNodeId(
{ position: (child as { position?: unknown }).position } as HastRootContent,
index
);
nextBlocks.push({ id, html, contentHash: hash });
nextBlocks.push({ contentHash: hash, html, id });
}
let unstableHtml = '';
if (mdastChildren.length > stableCount) {
const unstableChild = mdastChildren[stableCount];
const singleNodeRoot = { type: 'root', children: [unstableChild] };
const singleNodeRoot = { children: [unstableChild], type: 'root' };
const transformedRoot = (await processorInstance.run(
singleNodeRoot as MdastRoot
)) as HastRoot;
@@ -520,21 +527,24 @@
*/
async function handleMermaidClick(event: MouseEvent) {
const target = event.target as HTMLElement;
// Toggle a diagram block between its rendered view and its source view.
// Shared by mermaid and svg, css drives the visibility from the wrapper mode.
const toggleBtn = target.closest(`.${TOGGLE_SOURCE_BTN_CLASS}`);
if (toggleBtn) {
event.preventDefault();
event.stopPropagation();
const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`);
if (!wrapper) return;
const isSource = wrapper.getAttribute(DIAGRAM_VIEW_MODE_ATTR) === DIAGRAM_VIEW_SOURCE;
const next = isSource ? DIAGRAM_VIEW_RENDERED : DIAGRAM_VIEW_SOURCE;
wrapper.setAttribute(DIAGRAM_VIEW_MODE_ATTR, next);
toggleBtn.setAttribute('aria-pressed', String(!isSource));
return;
}
@@ -544,11 +554,13 @@
if (copyBtn || previewBtn) {
const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`);
if (!wrapper) return;
const preElement = wrapper.querySelector<HTMLElement>(
`pre.${MERMAID_BLOCK_CLASS}[${MERMAID_SYNTAX_ATTR}]`
);
if (!preElement) return;
const mermaidSyntax = preElement.getAttribute(MERMAID_SYNTAX_ATTR) ?? '';
@@ -561,6 +573,7 @@
} catch (error) {
console.error('Failed to copy mermaid syntax:', error);
}
return;
}
@@ -568,10 +581,13 @@
event.preventDefault();
event.stopPropagation();
const svg = preElement.querySelector('svg');
if (!svg) return;
mermaidPreviewSvgHtml = svg.outerHTML;
svgPreviewLive = false;
mermaidPreviewOpen = true;
return;
}
}
@@ -582,11 +598,13 @@
if (svgCopyBtn || svgPreviewBtn) {
const wrapper = target.closest(`.${SVG_WRAPPER_CLASS}`);
if (!wrapper) return;
const preElement = wrapper.querySelector<HTMLElement>(
`pre.${SVG_BLOCK_CLASS}[${SVG_SOURCE_ATTR}]`
);
if (!preElement) return;
if (svgCopyBtn) {
@@ -597,6 +615,7 @@
} catch (error) {
console.error('Failed to copy svg source:', error);
}
return;
}
@@ -606,6 +625,7 @@
mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
svgPreviewLive = false;
mermaidPreviewOpen = true;
return;
}
}
@@ -618,23 +638,29 @@
// Open preview when clicking the svg block itself. A final block carries its
// source, a streaming block does not and is mirrored live into the dialog.
const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`);
if (svgEl) {
const source = svgEl.getAttribute(SVG_SOURCE_ATTR);
if (source !== null) {
mermaidPreviewSvgHtml = sanitizeSvg(source);
svgPreviewLive = false;
} else {
svgPreviewLive = true;
}
mermaidPreviewOpen = true;
return;
}
// Otherwise, open preview when clicking on the mermaid diagram itself
const mermaidEl = target.closest(`.${MERMAID_BLOCK_CLASS}`);
if (!mermaidEl) return;
const svg = mermaidEl.querySelector('svg');
if (!svg) return;
mermaidPreviewSvgHtml = svg.outerHTML;
@@ -648,6 +674,7 @@
*/
function handleMermaidPreviewOpenChange(open: boolean) {
mermaidPreviewOpen = open;
if (!open) {
mermaidPreviewSvgHtml = '';
svgPreviewLive = false;
@@ -666,6 +693,7 @@
const nodes = containerRef.querySelectorAll(
`pre.${MERMAID_BLOCK_CLASS}:not([${MERMAID_RENDERED_ATTR}])`
);
if (nodes.length === 0) return;
// Mark nodes immediately to prevent duplicate renders if called again during streaming.
@@ -674,24 +702,23 @@
// Read mode before await so Svelte tracks it reactively.
const isDark = mode.current === ColorMode.DARK;
// lazy load the mermaid dependecy only when needed to reduce bundle size.
const { default: mermaid } = await import('mermaid');
mermaid.initialize({
startOnLoad: false,
theme: isDark ? 'dark' : 'default',
securityLevel: 'strict',
flowchart: {
useMaxWidth: false,
htmlLabels: true
},
sequence: {
htmlLabels: true,
useMaxWidth: false
},
gantt: {
useMaxWidth: false
}
},
securityLevel: 'strict',
sequence: {
useMaxWidth: false
},
startOnLoad: false,
theme: isDark ? 'dark' : 'default'
});
try {
@@ -714,6 +741,7 @@
const nodes = containerRef.querySelectorAll<HTMLElement>(
`pre.${SVG_BLOCK_CLASS}:not([${SVG_RENDERED_ATTR}])`
);
if (nodes.length === 0) return;
nodes.forEach((node) => {
@@ -725,6 +753,7 @@
if (clean) {
node.textContent = '';
const host = document.createElement('div');
node.appendChild(host);
mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE);
}
@@ -737,6 +766,7 @@
*/
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
if (!img || !img.src) return;
// Don't handle data URLs or already-handled images
@@ -745,11 +775,13 @@
img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING
)
return;
img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING;
const src = img.src;
// Create fallback element
const fallback = document.createElement('div');
fallback.className = 'image-load-error';
fallback.innerHTML = getImageErrorFallbackHtml(src);
@@ -775,6 +807,7 @@
try {
while (pendingMarkdown !== null) {
const nextMarkdown = pendingMarkdown;
pendingMarkdown = null;
await processMarkdown(nextMarkdown);