From b8221e8915a0887ec5b86166883bcfbc6af5cbde Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 27 Jan 2026 09:04:41 +0100 Subject: [PATCH] refactor: Utils --- .../app/misc/MarkdownContent.svelte | 78 ++----------------- tools/server/webui/src/lib/utils/code.ts | 73 +++++++++++++++++ 2 files changed, 78 insertions(+), 73 deletions(-) create mode 100644 tools/server/webui/src/lib/utils/code.ts diff --git a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte index f58dc37486..4c93cde2d4 100644 --- a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte +++ b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte @@ -17,11 +17,15 @@ import { rehypeResolveAttachmentImages } from '$lib/markdown/resolve-attachment-images'; import { remarkLiteralHtml } from '$lib/markdown/literal-html'; import { copyCodeToClipboard, preprocessLaTeX, getImageErrorFallbackHtml } from '$lib/utils'; + import { + highlightCode, + detectIncompleteCodeBlock, + type IncompleteCodeBlock + } from '$lib/utils/code'; 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 hljs from 'highlight.js'; import CodePreviewDialog from './CodePreviewDialog.svelte'; import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte'; import type { DatabaseMessage } from '$lib/types/database'; @@ -38,12 +42,6 @@ html: string; } - interface IncompleteCodeBlock { - language: string; - code: string; - openingIndex: number; - } - let { content, message, class: className = '', disableMath = false }: Props = $props(); let containerRef = $state(); @@ -225,72 +223,6 @@ } } - /** - * Highlights code using highlight.js - * @param code - The code to highlight - * @param language - The programming language - * @returns HTML string with syntax highlighting - */ - function highlightCode(code: string, language: string): string { - if (!code) return ''; - - try { - const lang = language.toLowerCase(); - const isSupported = hljs.getLanguage(lang); - - if (isSupported) { - return hljs.highlight(code, { language: lang }).value; - } else { - return hljs.highlightAuto(code).value; - } - } catch { - // Fallback to escaped plain text - return code.replace(/&/g, '&').replace(//g, '>'); - } - } - - /** - * Detects if markdown ends with an incomplete code block (opened but not closed). - * Returns the code block info if found, null otherwise. - * @param markdown - The raw markdown string to check - * @returns IncompleteCodeBlock info or null - */ - function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock | null { - // Count all code fences in the markdown - // A code block is incomplete if there's an odd number of ``` fences - const fencePattern = /^```|\n```/g; - const fences: number[] = []; - let fenceMatch; - - while ((fenceMatch = fencePattern.exec(markdown)) !== null) { - // Store the position after the ``` - const pos = fenceMatch[0].startsWith('\n') ? fenceMatch.index + 1 : fenceMatch.index; - fences.push(pos); - } - - // If even number of fences (including 0), all code blocks are closed - if (fences.length % 2 === 0) { - return null; - } - - // Odd number means last code block is incomplete - // The last fence is the opening of the incomplete block - const openingIndex = fences[fences.length - 1]; - const afterOpening = markdown.slice(openingIndex + 3); - - // Extract language and code content - const langMatch = afterOpening.match(/^(\w*)\n?/); - const language = langMatch?.[1] || 'text'; - const codeStartIndex = openingIndex + 3 + (langMatch?.[0]?.length ?? 0); - const code = markdown.slice(codeStartIndex); - - return { - language, - code, - openingIndex - }; - } - /** * Handles click events on preview buttons within HTML code blocks. * Opens a preview dialog with the rendered HTML content. diff --git a/tools/server/webui/src/lib/utils/code.ts b/tools/server/webui/src/lib/utils/code.ts new file mode 100644 index 0000000000..c205ee7fbc --- /dev/null +++ b/tools/server/webui/src/lib/utils/code.ts @@ -0,0 +1,73 @@ +import hljs from 'highlight.js'; + +export interface IncompleteCodeBlock { + language: string; + code: string; + openingIndex: number; +} + +/** + * Highlights code using highlight.js + * @param code - The code to highlight + * @param language - The programming language + * @returns HTML string with syntax highlighting + */ +export function highlightCode(code: string, language: string): string { + if (!code) return ''; + + try { + const lang = language.toLowerCase(); + const isSupported = hljs.getLanguage(lang); + + if (isSupported) { + return hljs.highlight(code, { language: lang }).value; + } else { + return hljs.highlightAuto(code).value; + } + } catch { + // Fallback to escaped plain text + return code.replace(/&/g, '&').replace(//g, '>'); + } +} + +/** + * Detects if markdown ends with an incomplete code block (opened but not closed). + * Returns the code block info if found, null otherwise. + * @param markdown - The raw markdown string to check + * @returns IncompleteCodeBlock info or null + */ +export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock | null { + // Count all code fences in the markdown + // A code block is incomplete if there's an odd number of ``` fences + const fencePattern = /^```|\n```/g; + const fences: number[] = []; + let fenceMatch; + + while ((fenceMatch = fencePattern.exec(markdown)) !== null) { + // Store the position after the ``` + const pos = fenceMatch[0].startsWith('\n') ? fenceMatch.index + 1 : fenceMatch.index; + fences.push(pos); + } + + // If even number of fences (including 0), all code blocks are closed + if (fences.length % 2 === 0) { + return null; + } + + // Odd number means last code block is incomplete + // The last fence is the opening of the incomplete block + const openingIndex = fences[fences.length - 1]; + const afterOpening = markdown.slice(openingIndex + 3); + + // Extract language and code content + const langMatch = afterOpening.match(/^(\w*)\n?/); + const language = langMatch?.[1] || 'text'; + const codeStartIndex = openingIndex + 3 + (langMatch?.[0]?.length ?? 0); + const code = markdown.slice(codeStartIndex); + + return { + language, + code, + openingIndex + }; +}