From 78b3e081671154219fb49751f6a0899580e78ef2 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 12 Nov 2025 14:19:06 +0000 Subject: [PATCH] tabbed to 2 space indentation --- .../shared/filePreview/getFileTypeIcon.tsx | 42 +- .../components/tools/showJS/ShowJSView.css | 154 +++-- .../components/tools/showJS/ShowJSView.tsx | 504 +++++++------- .../src/core/components/tools/showJS/utils.ts | 650 ++++++++++-------- 4 files changed, 719 insertions(+), 631 deletions(-) diff --git a/frontend/src/core/components/shared/filePreview/getFileTypeIcon.tsx b/frontend/src/core/components/shared/filePreview/getFileTypeIcon.tsx index 3d9149dbe..afcacad07 100644 --- a/frontend/src/core/components/shared/filePreview/getFileTypeIcon.tsx +++ b/frontend/src/core/components/shared/filePreview/getFileTypeIcon.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import JavascriptIcon from '@mui/icons-material/Javascript'; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { detectFileExtension } from '@app/utils/fileUtils'; +import React from "react"; +import JavascriptIcon from "@mui/icons-material/Javascript"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { detectFileExtension } from "@app/utils/fileUtils"; type FileLike = File | StirlingFileStub; @@ -12,23 +12,21 @@ type FileLike = File | StirlingFileStub; * - Uses the real file type and extension to decide the icon. * - No any-casts; accepts File or StirlingFileStub. */ -export function getFileTypeIcon(file: FileLike, size: number | string = '2rem'): React.ReactElement { - const name = (file?.name ?? '').toLowerCase(); - const mime = (file?.type ?? '').toLowerCase(); - const ext = detectFileExtension(name); +export function getFileTypeIcon(file: FileLike, size: number | string = "2rem"): React.ReactElement { + const name = (file?.name ?? "").toLowerCase(); + const mime = (file?.type ?? "").toLowerCase(); + const ext = detectFileExtension(name); - // JavaScript - if (ext === 'js' || mime.includes('javascript')) { - return ; - } + // JavaScript + if (ext === "js" || mime.includes("javascript")) { + return ; + } - // PDF - if (ext === 'pdf' || mime === 'application/pdf') { - return ; - } + // PDF + if (ext === "pdf" || mime === "application/pdf") { + return ; + } - // Fallback generic - return ; + // Fallback generic + return ; } - - diff --git a/frontend/src/core/components/tools/showJS/ShowJSView.css b/frontend/src/core/components/tools/showJS/ShowJSView.css index 5024d2c57..27e573dba 100644 --- a/frontend/src/core/components/tools/showJS/ShowJSView.css +++ b/frontend/src/core/components/tools/showJS/ShowJSView.css @@ -1,123 +1,131 @@ .showjs-code { - font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-size: 12px; - line-height: 1.5; - white-space: pre; - tab-size: 2; - margin: 0; - color: var(--text-primary); + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 12px; + line-height: 1.5; + white-space: pre; + tab-size: 2; + margin: 0; + color: var(--text-primary); } -.tok-kw { color: var(--code-kw-color); font-weight: 600; } -.tok-str { color: var(--code-str-color); } -.tok-num { color: var(--code-num-color); } -.tok-com { color: var(--code-com-color); font-style: italic; } +.tok-kw { + color: var(--code-kw-color); + font-weight: 600; +} +.tok-str { + color: var(--code-str-color); +} +.tok-num { + color: var(--code-num-color); +} +.tok-com { + color: var(--code-com-color); + font-style: italic; +} .code-line { - display: flex; - align-items: flex-start; - gap: 8px; + display: flex; + align-items: flex-start; + gap: 8px; } .code-gutter { - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 64px; - color: var(--text-muted); - user-select: none; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 64px; + color: var(--text-muted); + user-select: none; } .line-number { - min-width: 28px; - text-align: right; + min-width: 28px; + text-align: right; } .fold-toggle { - border: none; - background: transparent; - color: var(--text-muted); - cursor: pointer; - padding: 0 2px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + padding: 0 2px; } .fold-collapsed { - transform: translateY(-1px); + transform: translateY(-1px); } .fold-placeholder { - display: inline-block; - width: 10px; + display: inline-block; + width: 10px; } .code-content { - white-space: pre-wrap; - word-break: break-word; - flex: 1 1 auto; + white-space: pre-wrap; + word-break: break-word; + flex: 1 1 auto; } .collapsed-indicator { - color: var(--text-muted); - font-style: italic; - cursor: pointer; - padding-left: 8px; + color: var(--text-muted); + font-style: italic; + cursor: pointer; + padding-left: 8px; } .collapsed-inline { - color: var(--text-muted); - margin-left: 6px; + color: var(--text-muted); + margin-left: 6px; } .search-hit { - background: rgba(255, 235, 59, 0.4); /* yellow highlight */ - border-radius: 2px; + background: rgba(255, 235, 59, 0.4); /* yellow highlight */ + border-radius: 2px; } .search-hit-active { - background: rgba(33, 150, 243, 0.4); /* active blue */ + background: rgba(33, 150, 243, 0.4); /* active blue */ } .showjs-root { - height: 100%; - margin-left: 1rem; - margin-right: 1rem; + height: 100%; + margin-left: 1rem; + margin-right: 1rem; } .showjs-container { - position: relative; - height: 100%; - min-height: 360px; - border: 1px solid var(--mantine-color-gray-4); - border-radius: 8px; - overflow: hidden; - background: var(--right-rail-bg); + position: relative; + height: 100%; + min-height: 360px; + border: 1px solid var(--mantine-color-gray-4); + border-radius: 8px; + overflow: hidden; + background: var(--right-rail-bg); } .showjs-toolbar { - position: sticky; - top: 8px; - right: 8px; - display: flex; - justify-content: space-between; - align-items: center; - z-index: 0; - padding: 8px; - background: transparent; - margin-bottom: 10px; - margin-left: 6px; - pointer-events: none; + position: sticky; + top: 8px; + right: 8px; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 0; + padding: 8px; + background: transparent; + margin-bottom: 10px; + margin-left: 6px; + pointer-events: none; } .showjs-toolbar-controls { - pointer-events: auto; + pointer-events: auto; } .showjs-search-input { - width: 220px; + width: 220px; } .showjs-outline-button { - background: transparent; - border: 1px solid currentColor; - color: var(--mantine-color-blue-5); + background: transparent; + border: 1px solid currentColor; + color: var(--mantine-color-blue-5); } .showjs-scrollarea { - height: calc(100vh - 220px); + height: calc(100vh - 220px); } .showjs-inner { - padding: 40px 24px 24px 24px; + padding: 40px 24px 24px 24px; } - - diff --git a/frontend/src/core/components/tools/showJS/ShowJSView.tsx b/frontend/src/core/components/tools/showJS/ShowJSView.tsx index 8cdc340b0..83dc50869 100644 --- a/frontend/src/core/components/tools/showJS/ShowJSView.tsx +++ b/frontend/src/core/components/tools/showJS/ShowJSView.tsx @@ -1,263 +1,305 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ActionIcon, Box, Button, Group, Stack, Text, ScrollArea, TextInput } from '@mantine/core'; -import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded'; -import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; -import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded'; -import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded'; -import '@app/components/tools/showJS/ShowJSView.css'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ActionIcon, Box, Button, Group, Stack, Text, ScrollArea, TextInput } from "@mantine/core"; +import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded"; +import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; +import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded"; +import "@app/components/tools/showJS/ShowJSView.css"; +import { useTranslation } from "react-i18next"; import { - tokenizeToLines, - computeBlocks, - computeSearchMatches, - copyTextToClipboard, - triggerDownload, - type ShowJsToken -} from '@app/components/tools/showJS/utils'; + tokenizeToLines, + computeBlocks, + computeSearchMatches, + copyTextToClipboard, + triggerDownload, + type ShowJsToken, +} from "@app/components/tools/showJS/utils"; interface ScriptData { - scriptText: string; - downloadUrl?: string | null; - downloadFilename?: string | null; + scriptText: string; + downloadUrl?: string | null; + downloadFilename?: string | null; } interface ShowJSViewProps { - data: string | ScriptData; + data: string | ScriptData; } const ShowJSView: React.FC = ({ data }) => { - const { t } = useTranslation(); - const text = useMemo(() => { - if (typeof data === 'string') return data; - return data?.scriptText ?? ''; - }, [data]); - const downloadUrl = useMemo(() => { - if (typeof data === 'string') return null; - return data?.downloadUrl ?? null; - }, [data]); - const downloadFilename = useMemo(() => { - if (typeof data === 'string') return null; - return data?.downloadFilename ?? null; - }, [data]); - const [copied, setCopied] = useState(false); - const codeRef = useRef(null); - const scrollAreaInnerRef = useRef(null); + const { t } = useTranslation(); + const text = useMemo(() => { + if (typeof data === "string") return data; + return data?.scriptText ?? ""; + }, [data]); + const downloadUrl = useMemo(() => { + if (typeof data === "string") return null; + return data?.downloadUrl ?? null; + }, [data]); + const downloadFilename = useMemo(() => { + if (typeof data === "string") return null; + return data?.downloadFilename ?? null; + }, [data]); + const [copied, setCopied] = useState(false); + const codeRef = useRef(null); + const scrollAreaInnerRef = useRef(null); - const handleCopy = useCallback(async () => { - const ok = await copyTextToClipboard(text || '', codeRef.current); - if (!ok) return; - setCopied(true); - setTimeout(() => setCopied(false), 1200); - }, [text]); + const handleCopy = useCallback(async () => { + const ok = await copyTextToClipboard(text || "", codeRef.current); + if (!ok) return; + setCopied(true); + setTimeout(() => setCopied(false), 1200); + }, [text]); - const handleDownload = useCallback(() => { - if (!downloadUrl) return; - triggerDownload(downloadUrl, downloadFilename || 'extracted.js'); - }, [downloadUrl, downloadFilename]); + const handleDownload = useCallback(() => { + if (!downloadUrl) return; + triggerDownload(downloadUrl, downloadFilename || "extracted.js"); + }, [downloadUrl, downloadFilename]); - const [lines, setLines] = useState([]); - const [blocks, setBlocks] = useState>([]); - const [collapsed, setCollapsed] = useState>(new Set()); + const [lines, setLines] = useState([]); + const [blocks, setBlocks] = useState>([]); + const [collapsed, setCollapsed] = useState>(new Set()); - useEffect(() => { - const src = text || ''; - setLines(tokenizeToLines(src)); - setBlocks(computeBlocks(src)); - setCollapsed(new Set()); - }, [text]); + useEffect(() => { + const src = text || ""; + setLines(tokenizeToLines(src)); + setBlocks(computeBlocks(src)); + setCollapsed(new Set()); + }, [text]); - const startToEnd = useMemo(() => { - const m = new Map(); - for (const b of blocks) if (!m.has(b.start)) m.set(b.start, b.end); - return m; - }, [blocks]); + const startToEnd = useMemo(() => { + const m = new Map(); + for (const b of blocks) if (!m.has(b.start)) m.set(b.start, b.end); + return m; + }, [blocks]); - const isHidden = useCallback((ln: number) => { - for (const s of collapsed) { - const e = startToEnd.get(s); - if (e != null && ln > s && ln <= e) return true; - } - return false; - }, [collapsed, startToEnd]); + const isHidden = useCallback( + (ln: number) => { + for (const s of collapsed) { + const e = startToEnd.get(s); + if (e != null && ln > s && ln <= e) return true; + } + return false; + }, + [collapsed, startToEnd], + ); - const toggleFold = (ln: number) => { - if (!startToEnd.has(ln)) return; - setCollapsed(prev => { - const n = new Set(prev); - if (n.has(ln)) n.delete(ln); else n.add(ln); - return n; - }); - }; + const toggleFold = (ln: number) => { + if (!startToEnd.has(ln)) return; + setCollapsed((prev) => { + const n = new Set(prev); + if (n.has(ln)) n.delete(ln); + else n.add(ln); + return n; + }); + }; - // Search - const [query, setQuery] = useState(''); - const [matches, setMatches] = useState>([]); - const [active, setActive] = useState(0); + // Search + const [query, setQuery] = useState(""); + const [matches, setMatches] = useState>([]); + const [active, setActive] = useState(0); - useEffect(() => { - if (!query) { setMatches([]); setActive(0); return; } - const list = computeSearchMatches(lines, query); - setMatches(list); - setActive(list.length ? 0 : 0); - }, [query, lines]); + useEffect(() => { + if (!query) { + setMatches([]); + setActive(0); + return; + } + const list = computeSearchMatches(lines, query); + setMatches(list); + setActive(list.length ? 0 : 0); + }, [query, lines]); - useEffect(() => { - const m = matches[active]; - if (!m) return; - for (const [s,e] of startToEnd.entries()) { - if (m.line > s && m.line <= e && collapsed.has(s)) { - setCollapsed(prev => { const n = new Set(prev); n.delete(s); return n; }); - } - } - if (scrollAreaInnerRef.current) { - const el = scrollAreaInnerRef.current.querySelector(`[data-code-line="${m.line}"]`) as HTMLElement | null; - if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - }, [active, matches, startToEnd, collapsed]); + useEffect(() => { + const m = matches[active]; + if (!m) return; + for (const [s, e] of startToEnd.entries()) { + if (m.line > s && m.line <= e && collapsed.has(s)) { + setCollapsed((prev) => { + const n = new Set(prev); + n.delete(s); + return n; + }); + } + } + if (scrollAreaInnerRef.current) { + const el = scrollAreaInnerRef.current.querySelector(`[data-code-line="${m.line}"]`) as HTMLElement | null; + if (el) el.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } + }, [active, matches, startToEnd, collapsed]); - return ( - - -
- - setQuery(e.currentTarget.value)} - size="xs" - placeholder={t('search.placeholder', 'Enter search term...')} - className="showjs-search-input" - /> - - {matches.length ? `${active + 1}/${matches.length}` : '0/0'} - - { if (matches.length) setActive((p)=>(p-1+matches.length)%matches.length); }} aria-label={t('common.previous', 'Previous')}> - - - { if (matches.length) setActive((p)=>(p+1)%matches.length); }} aria-label={t('common.next', 'Next')}> - - - - - - - -
- -
-
- {lines.map((tokens, ln) => { - if (isHidden(ln)) return null; - const end = startToEnd.get(ln); - const folded = end != null && collapsed.has(ln); - let pos = 0; - const lineMatches = matches.map((m, idx) => ({ ...m, idx })).filter(m => m.line === ln); - const content: React.ReactNode[] = []; - tokens.forEach((tok, ti) => { - const textSeg = tok.text; - const tokenStart = pos; - const tokenEnd = pos + textSeg.length; + return ( + + +
+ + setQuery(e.currentTarget.value)} + size="xs" + placeholder={t("search.placeholder", "Enter search term...")} + className="showjs-search-input" + /> + + {matches.length ? `${active + 1}/${matches.length}` : "0/0"} + + { + if (matches.length) setActive((p) => (p - 1 + matches.length) % matches.length); + }} + aria-label={t("common.previous", "Previous")} + > + + + { + if (matches.length) setActive((p) => (p + 1) % matches.length); + }} + aria-label={t("common.next", "Next")} + > + + + + + + + +
+ +
+
+ {lines.map((tokens, ln) => { + if (isHidden(ln)) return null; + const end = startToEnd.get(ln); + const folded = end != null && collapsed.has(ln); + let pos = 0; + const lineMatches = matches.map((m, idx) => ({ ...m, idx })).filter((m) => m.line === ln); + const content: React.ReactNode[] = []; + tokens.forEach((tok, ti) => { + const textSeg = tok.text; + const tokenStart = pos; + const tokenEnd = pos + textSeg.length; - if (!query || lineMatches.length === 0) { - const cls = tok.type === 'plain' ? undefined : `tok-${tok.type}`; - content.push({textSeg}); - pos = tokenEnd; - return; - } + if (!query || lineMatches.length === 0) { + const cls = tok.type === "plain" ? undefined : `tok-${tok.type}`; + content.push( + + {textSeg} + , + ); + pos = tokenEnd; + return; + } - // Collect matches that intersect this token - const matchesInToken = lineMatches - .filter(m => m.start < tokenEnd && m.end > tokenStart) - .sort((a, b) => a.start - b.start); + // Collect matches that intersect this token + const matchesInToken = lineMatches + .filter((m) => m.start < tokenEnd && m.end > tokenStart) + .sort((a, b) => a.start - b.start); - if (matchesInToken.length === 0) { - const cls = tok.type === 'plain' ? undefined : `tok-${tok.type}`; - content.push({textSeg}); - pos = tokenEnd; - return; - } + if (matchesInToken.length === 0) { + const cls = tok.type === "plain" ? undefined : `tok-${tok.type}`; + content.push( + + {textSeg} + , + ); + pos = tokenEnd; + return; + } - let cursor = 0; - const tokenCls = tok.type === 'plain' ? '' : `tok-${tok.type}`; + let cursor = 0; + const tokenCls = tok.type === "plain" ? "" : `tok-${tok.type}`; - matchesInToken.forEach((m, mi) => { - const localStart = Math.max(0, m.start - tokenStart); - const localEnd = Math.min(textSeg.length, m.end - tokenStart); + matchesInToken.forEach((m, mi) => { + const localStart = Math.max(0, m.start - tokenStart); + const localEnd = Math.min(textSeg.length, m.end - tokenStart); - // before match - if (localStart > cursor) { - const beforeText = textSeg.slice(cursor, localStart); - const cls = tokenCls || undefined; - content.push({beforeText}); - } - // matched piece - const hitText = textSeg.slice(localStart, localEnd); - const hitCls = ['search-hit', (m.idx === active ? 'search-hit-active' : ''), tokenCls] - .filter(Boolean).join(' ') || undefined; - content.push({hitText}); - cursor = localEnd; - }); + // before match + if (localStart > cursor) { + const beforeText = textSeg.slice(cursor, localStart); + const cls = tokenCls || undefined; + content.push( + + {beforeText} + , + ); + } + // matched piece + const hitText = textSeg.slice(localStart, localEnd); + const hitCls = + ["search-hit", m.idx === active ? "search-hit-active" : "", tokenCls].filter(Boolean).join(" ") || + undefined; + content.push( + + {hitText} + , + ); + cursor = localEnd; + }); - // tail after last match - if (cursor < textSeg.length) { - const tailText = textSeg.slice(cursor); - const cls = tokenCls || undefined; - content.push({tailText}); - } + // tail after last match + if (cursor < textSeg.length) { + const tailText = textSeg.slice(cursor); + const cls = tokenCls || undefined; + content.push( + + {tailText} + , + ); + } - pos = tokenEnd; - }); - return ( -
-
- {end != null ? ( - - ) : } - {ln + 1} -
-
- {content} - {folded && ( - {"{...}"} - )} -
-
- ); - })} -
-
-
-
-
- ); + pos = tokenEnd; + }); + return ( +
+
+ {end != null ? ( + + ) : ( + + )} + {ln + 1} +
+
+ {content} + {folded && {"{...}"}} +
+
+ ); + })} +
+
+
+
+
+ ); }; export default ShowJSView; - - diff --git a/frontend/src/core/components/tools/showJS/utils.ts b/frontend/src/core/components/tools/showJS/utils.ts index be18aa901..c5cca2488 100644 --- a/frontend/src/core/components/tools/showJS/utils.ts +++ b/frontend/src/core/components/tools/showJS/utils.ts @@ -1,342 +1,382 @@ -export type ShowJsTokenType = 'kw' | 'str' | 'num' | 'com' | 'plain'; +export type ShowJsTokenType = "kw" | "str" | "num" | "com" | "plain"; export type ShowJsToken = { type: ShowJsTokenType; text: string }; const JS_KEYWORDS = new Set([ - 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'await', 'of' + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "export", + "extends", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "let", + "new", + "return", + "super", + "switch", + "this", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + "await", + "of", ]); export function tokenizeToLines(src: string, keywords: Set = JS_KEYWORDS): ShowJsToken[][] { - const lines: ShowJsToken[][] = []; - let current: ShowJsToken[] = []; - let i = 0; - let inBlockCom = false; - let inLineCom = false; - let inString: '"' | "'" | '`' | null = null; - let escaped = false; + const lines: ShowJsToken[][] = []; + let current: ShowJsToken[] = []; + let i = 0; + let inBlockCom = false; + let inLineCom = false; + let inString: '"' | "'" | "`" | null = null; + let escaped = false; - const push = (type: ShowJsTokenType, s: string) => { - if (s) { - current.push({ type, text: s }); - } - }; + const push = (type: ShowJsTokenType, s: string) => { + if (s) { + current.push({ type, text: s }); + } + }; - // Named actions for readability - const advance = (n: number = 1) => { i += n; }; - const handleNewline = () => { - lines.push(current); - current = []; - inLineCom = false; - advance(); - }; - const handleInLineCommentChar = (ch: string) => { - push('com', ch); - advance(); - }; - const handleBlockCommentEnd = () => { - push('com', '*/'); - inBlockCom = false; - advance(2); - }; - const handleInBlockCommentChar = (ch: string) => { - push('com', ch); - advance(); - }; - const handleInStringChar = (ch: string) => { - push('str', ch); - if (!escaped) { - const isEscape = ch === '\\'; - const isStringClose = ch === inString; - if (isEscape) { - escaped = true; - } else if (isStringClose) { - inString = null; - } - } else { - escaped = false; - } - advance(); - }; - const startLineComment = () => { - push('com', '//'); - inLineCom = true; - advance(2); - }; - const startBlockComment = () => { - push('com', '/*'); - inBlockCom = true; - advance(2); - }; - const startString = (ch: '"' | "'" | '`') => { - inString = ch; - push('str', ch); - advance(); - }; - const pushNumberToken = () => { - let j = i + 1; - const isNumberContinuation = (c: string) => /[0-9._xobA-Fa-f]/.test(c); - while (j < src.length && isNumberContinuation(src[j])) { - j++; - } - push('num', src.slice(i, j)); - i = j; - }; - const pushIdentifierToken = () => { - let j = i + 1; - const isIdentContinuation = (c: string) => /[A-Za-z0-9_$]/.test(c); - while (j < src.length && isIdentContinuation(src[j])) { - j++; - } - const id = src.slice(i, j); - const isKeyword = keywords.has(id); - push(isKeyword ? 'kw' : 'plain', id); - i = j; - }; - const pushPlainChar = (ch: string) => { - push('plain', ch); - advance(); - }; + // Named actions for readability + const advance = (n: number = 1) => { + i += n; + }; + const handleNewline = () => { + lines.push(current); + current = []; + inLineCom = false; + advance(); + }; + const handleInLineCommentChar = (ch: string) => { + push("com", ch); + advance(); + }; + const handleBlockCommentEnd = () => { + push("com", "*/"); + inBlockCom = false; + advance(2); + }; + const handleInBlockCommentChar = (ch: string) => { + push("com", ch); + advance(); + }; + const handleInStringChar = (ch: string) => { + push("str", ch); + if (!escaped) { + const isEscape = ch === "\\"; + const isStringClose = ch === inString; + if (isEscape) { + escaped = true; + } else if (isStringClose) { + inString = null; + } + } else { + escaped = false; + } + advance(); + }; + const startLineComment = () => { + push("com", "//"); + inLineCom = true; + advance(2); + }; + const startBlockComment = () => { + push("com", "/*"); + inBlockCom = true; + advance(2); + }; + const startString = (ch: '"' | "'" | "`") => { + inString = ch; + push("str", ch); + advance(); + }; + const pushNumberToken = () => { + let j = i + 1; + const isNumberContinuation = (c: string) => /[0-9._xobA-Fa-f]/.test(c); + while (j < src.length && isNumberContinuation(src[j])) { + j++; + } + push("num", src.slice(i, j)); + i = j; + }; + const pushIdentifierToken = () => { + let j = i + 1; + const isIdentContinuation = (c: string) => /[A-Za-z0-9_$]/.test(c); + while (j < src.length && isIdentContinuation(src[j])) { + j++; + } + const id = src.slice(i, j); + const isKeyword = keywords.has(id); + push(isKeyword ? "kw" : "plain", id); + i = j; + }; + const pushPlainChar = (ch: string) => { + push("plain", ch); + advance(); + }; - while (i < src.length) { - const ch = src[i]; - const next = src[i + 1]; + while (i < src.length) { + const ch = src[i]; + const next = src[i + 1]; - // Named conditions - const isNewline = ch === '\n'; - const isLineCommentStart = ch === '/' && next === '/'; - const isBlockCommentStart = ch === '/' && next === '*'; - const isStringDelimiter = ch === '\'' || ch === '"' || ch === '`'; - const isDigit = /[0-9]/.test(ch); - const isIdentifierStart = /[A-Za-z_$]/.test(ch); + // Named conditions + const isNewline = ch === "\n"; + const isLineCommentStart = ch === "/" && next === "/"; + const isBlockCommentStart = ch === "/" && next === "*"; + const isStringDelimiter = ch === "'" || ch === '"' || ch === "`"; + const isDigit = /[0-9]/.test(ch); + const isIdentifierStart = /[A-Za-z_$]/.test(ch); - if (isNewline) { - handleNewline(); - continue; - } + if (isNewline) { + handleNewline(); + continue; + } - if (inLineCom) { - handleInLineCommentChar(ch); - continue; - } + if (inLineCom) { + handleInLineCommentChar(ch); + continue; + } - if (inBlockCom) { - const isBlockCommentEnd = ch === '*' && next === '/'; - if (isBlockCommentEnd) { - handleBlockCommentEnd(); - continue; - } - handleInBlockCommentChar(ch); - continue; - } + if (inBlockCom) { + const isBlockCommentEnd = ch === "*" && next === "/"; + if (isBlockCommentEnd) { + handleBlockCommentEnd(); + continue; + } + handleInBlockCommentChar(ch); + continue; + } - if (inString) { - handleInStringChar(ch); - continue; - } + if (inString) { + handleInStringChar(ch); + continue; + } - if (isLineCommentStart) { - startLineComment(); - continue; - } + if (isLineCommentStart) { + startLineComment(); + continue; + } - if (isBlockCommentStart) { - startBlockComment(); - continue; - } + if (isBlockCommentStart) { + startBlockComment(); + continue; + } - if (isStringDelimiter) { - startString(ch as '"' | "'" | '`'); - continue; - } + if (isStringDelimiter) { + startString(ch as '"' | "'" | "`"); + continue; + } - if (isDigit) { - pushNumberToken(); - continue; - } + if (isDigit) { + pushNumberToken(); + continue; + } - if (isIdentifierStart) { - pushIdentifierToken(); - continue; - } + if (isIdentifierStart) { + pushIdentifierToken(); + continue; + } - pushPlainChar(ch); - } + pushPlainChar(ch); + } - lines.push(current); - return lines; + lines.push(current); + return lines; } export function computeBlocks(src: string): Array<{ start: number; end: number }> { - const res: Array<{ start: number; end: number }> = []; - let i = 0; - let line = 0; - let inBlock = false; - let inLine = false; - let str: '"' | "'" | '`' | null = null; - let esc = false; - const stack: number[] = []; + const res: Array<{ start: number; end: number }> = []; + let i = 0; + let line = 0; + let inBlock = false; + let inLine = false; + let str: '"' | "'" | "`" | null = null; + let esc = false; + const stack: number[] = []; - // Actions - const advance = (n: number = 1) => { i += n; }; - const handleNewline = () => { - line++; - inLine = false; - advance(); - }; - const startLineComment = () => { - inLine = true; - advance(2); - }; - const startBlockComment = () => { - inBlock = true; - advance(2); - }; - const endBlockComment = () => { - inBlock = false; - advance(2); - }; - const startString = (delim: '"' | "'" | '`') => { - str = delim; - advance(); - }; - const handleStringChar = (ch: string) => { - if (!esc) { - const isEscape = ch === '\\'; - const isClose = ch === str; - if (isEscape) { - esc = true; - } else if (isClose) { - str = null; - } - } else { - esc = false; - } - advance(); - }; - const pushOpenBrace = () => { - stack.push(line); - advance(); - }; - const handleCloseBrace = () => { - const s = stack.pop(); - if (s != null && line > s) { - res.push({ start: s, end: line }); - } - advance(); - }; + // Actions + const advance = (n: number = 1) => { + i += n; + }; + const handleNewline = () => { + line++; + inLine = false; + advance(); + }; + const startLineComment = () => { + inLine = true; + advance(2); + }; + const startBlockComment = () => { + inBlock = true; + advance(2); + }; + const endBlockComment = () => { + inBlock = false; + advance(2); + }; + const startString = (delim: '"' | "'" | "`") => { + str = delim; + advance(); + }; + const handleStringChar = (ch: string) => { + if (!esc) { + const isEscape = ch === "\\"; + const isClose = ch === str; + if (isEscape) { + esc = true; + } else if (isClose) { + str = null; + } + } else { + esc = false; + } + advance(); + }; + const pushOpenBrace = () => { + stack.push(line); + advance(); + }; + const handleCloseBrace = () => { + const s = stack.pop(); + if (s != null && line > s) { + res.push({ start: s, end: line }); + } + advance(); + }; - while (i < src.length) { - const ch = src[i]; - const nx = src[i + 1]; + while (i < src.length) { + const ch = src[i]; + const nx = src[i + 1]; - // Conditions - const isNewline = ch === '\n'; - const isLineCommentStart = ch === '/' && nx === '/'; - const isBlockCommentStart = ch === '/' && nx === '*'; - const isBlockCommentEnd = ch === '*' && nx === '/'; - const isStringDelimiter = ch === '\'' || ch === '"' || ch === '`'; - const isOpenBrace = ch === '{'; - const isCloseBrace = ch === '}'; + // Conditions + const isNewline = ch === "\n"; + const isLineCommentStart = ch === "/" && nx === "/"; + const isBlockCommentStart = ch === "/" && nx === "*"; + const isBlockCommentEnd = ch === "*" && nx === "/"; + const isStringDelimiter = ch === "'" || ch === '"' || ch === "`"; + const isOpenBrace = ch === "{"; + const isCloseBrace = ch === "}"; - if (isNewline) { - handleNewline(); - continue; - } - if (inLine) { - advance(); - continue; - } - if (inBlock) { - if (isBlockCommentEnd) { - endBlockComment(); - } else { - advance(); - } - continue; - } - if (str) { - handleStringChar(ch); - continue; - } - if (isLineCommentStart) { - startLineComment(); - continue; - } - if (isBlockCommentStart) { - startBlockComment(); - continue; - } - if (isStringDelimiter) { - startString(ch as '"' | "'" | '`'); - continue; - } - if (isOpenBrace) { - pushOpenBrace(); - continue; - } - if (isCloseBrace) { - handleCloseBrace(); - continue; - } - advance(); - } - return res; + if (isNewline) { + handleNewline(); + continue; + } + if (inLine) { + advance(); + continue; + } + if (inBlock) { + if (isBlockCommentEnd) { + endBlockComment(); + } else { + advance(); + } + continue; + } + if (str) { + handleStringChar(ch); + continue; + } + if (isLineCommentStart) { + startLineComment(); + continue; + } + if (isBlockCommentStart) { + startBlockComment(); + continue; + } + if (isStringDelimiter) { + startString(ch as '"' | "'" | "`"); + continue; + } + if (isOpenBrace) { + pushOpenBrace(); + continue; + } + if (isCloseBrace) { + handleCloseBrace(); + continue; + } + advance(); + } + return res; } -export function computeSearchMatches(lines: ShowJsToken[][], query: string): Array<{ line: number; start: number; end: number }> { - if (!query) { - return []; - } - const q = query.toLowerCase(); - const list: Array<{ line: number; start: number; end: number }> = []; - lines.forEach((toks, ln) => { - const raw = toks.map(t => t.text).join(''); - let idx = 0; - while (true) { - const pos = raw.toLowerCase().indexOf(q, idx); - if (pos === -1) { - break; - } - list.push({ - line: ln, - start: pos, - end: pos + q.length - }); - idx = pos + Math.max(1, q.length); - } - }); - return list; +export function computeSearchMatches( + lines: ShowJsToken[][], + query: string, +): Array<{ line: number; start: number; end: number }> { + if (!query) { + return []; + } + const q = query.toLowerCase(); + const list: Array<{ line: number; start: number; end: number }> = []; + lines.forEach((toks, ln) => { + const raw = toks.map((t) => t.text).join(""); + let idx = 0; + while (true) { + const pos = raw.toLowerCase().indexOf(q, idx); + if (pos === -1) { + break; + } + list.push({ + line: ln, + start: pos, + end: pos + q.length, + }); + idx = pos + Math.max(1, q.length); + } + }); + return list; } export async function copyTextToClipboard(text: string, fallbackElement?: HTMLElement | null): Promise { - try { - if (typeof navigator !== 'undefined' && navigator.clipboard) { - await navigator.clipboard.writeText(text || ''); - return true; - } - } catch { - // fall through to fallback - } - if (typeof document === 'undefined' || !fallbackElement) return false; - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(fallbackElement); - selection?.removeAllRanges(); - selection?.addRange(range); - try { - document.execCommand('copy'); - return true; - } finally { - selection?.removeAllRanges(); - } + try { + if (typeof navigator !== "undefined" && navigator.clipboard) { + await navigator.clipboard.writeText(text || ""); + return true; + } + } catch { + // fall through to fallback + } + if (typeof document === "undefined" || !fallbackElement) return false; + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(fallbackElement); + selection?.removeAllRanges(); + selection?.addRange(range); + try { + document.execCommand("copy"); + return true; + } finally { + selection?.removeAllRanges(); + } } export function triggerDownload(url: string, filename: string): void { - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); } - -