diff --git a/src/common/assets/highlight.module.css b/src/common/assets/highlight.module.css new file mode 100644 index 0000000..b64ff73 --- /dev/null +++ b/src/common/assets/highlight.module.css @@ -0,0 +1,27 @@ +.italic { + font-style: italic; + color: var(--italicColor, #AFAFAF); +} + +.bold { + font-weight: bold; +} + +.quote { + color: var(--quoteColor, #D4E5FF); +} + +.codeBlock { + font-family: monospace; + background: var(--codeBg, #49483e); + padding: 0.5em; + display: block; + border-radius: var(--radius, 4px); +} + +.inlineCode { + font-family: monospace; + background: var(--codeBg, #49483e); + padding: 0.1em 0.3em; + border-radius: 0.2em; +} diff --git a/src/common/highlight.ts b/src/common/highlight.ts new file mode 100644 index 0000000..03c0d0e --- /dev/null +++ b/src/common/highlight.ts @@ -0,0 +1,57 @@ +import styles from './assets/highlight.module.css'; + +export const highlight = (message: string, keepMarkup = true): string => { + let resultHTML = ''; + const tokenRegex = /(\*\*?|"|```|`)/g; + const stack: string[] = []; + let inCodeBlock = false; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = tokenRegex.exec(message)) !== null) { + resultHTML += message.slice(lastIndex, match.index); + lastIndex = tokenRegex.lastIndex; + + const token = match[0]; + const isClose = stack.at(-1) === token; + const keepToken = keepMarkup || token === '"'; + + if (inCodeBlock) { + if (token === '```' && isClose) { + inCodeBlock = false; + stack.pop(); + resultHTML += `${keepToken ? token : ''}`; + } else { + resultHTML += token; + } + continue; + } + + if (isClose) { + stack.pop(); + resultHTML += `${keepToken ? token : ''}`; + } else if (token === '*') { + stack.push(token); + resultHTML += `${keepToken ? token : ''}`; + } else if (token === '**') { + stack.push(token); + resultHTML += `${keepToken ? token : ''}`; + } else if (token === '"') { + stack.push(token); + resultHTML += `"`; + } else if (token === '```') { + stack.push(token); + inCodeBlock = true; + resultHTML += ``; + } else if (token === '`') { + stack.push(token); + resultHTML += ``; + } + } + + resultHTML += message.slice(lastIndex); + + resultHTML += ''.repeat(stack.length); + + return resultHTML; +} diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx index b54faa0..f4e595b 100644 --- a/src/games/storywriter/components/chat-sidebar.tsx +++ b/src/games/storywriter/components/chat-sidebar.tsx @@ -1,10 +1,10 @@ import { useInputState } from "@common/hooks/useInputState"; +import { highlight } from "@common/highlight"; import { Sidebar } from "./sidebar"; import { useAppState, type ChatMessage } from "../contexts/state"; import styles from '../assets/chat-sidebar.module.css'; import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks"; import LLM from "../utils/llm"; -import { highlight } from "../utils/highlight"; import Prompt from "../utils/prompt"; import { Tools } from "../utils/tools"; import clsx from "clsx"; diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx index 49cb82c..5f8b583 100644 --- a/src/games/storywriter/components/editor.tsx +++ b/src/games/storywriter/components/editor.tsx @@ -1,7 +1,7 @@ import { ContentEditable } from "@common/components/ContentEditable"; +import { highlight } from "@common/highlight"; import { useAppState, type Tab } from "../contexts/state"; import styles from '../assets/editor.module.css'; -import { highlight } from "../utils/highlight"; import { useMemo } from "preact/hooks"; import { CharacterEditor } from "./character-editor"; import { LocationEditor } from "./location-editor"; diff --git a/src/games/storywriter/utils/highlight.ts b/src/games/storywriter/utils/highlight.ts deleted file mode 100644 index 2cf128d..0000000 --- a/src/games/storywriter/utils/highlight.ts +++ /dev/null @@ -1,63 +0,0 @@ -export const highlight = (message: string, keepMarkup = true): string => { - let resultHTML = ''; - const replaceRegex = /(\*\*?|"|```|`)/ig; - const splitToken = '___SPLIT_AWOORWA___'; - - const preparedMessage = message.replace(replaceRegex, `${splitToken}$1${splitToken}`); - const parts = preparedMessage.split(splitToken); - - const stack: string[] = []; - let inCodeBlock = false; - - for (const part of parts) { - const isClose = stack.at(-1) === part; - const keepPart = keepMarkup || part === '"'; - - if (inCodeBlock) { - if (part === '```' && isClose) { - inCodeBlock = false; - stack.pop(); - resultHTML += `${keepPart ? part : ''}`; - } else { - resultHTML += part; - } - continue; - } - - if (isClose) { - stack.pop(); - if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') { - resultHTML += `${keepPart ? part : ''}`; - } - } else { - if (part === '*') { - stack.push(part); - resultHTML += `${keepPart ? part : ''}`; - } else if (part === '**') { - stack.push(part); - resultHTML += `${keepPart ? part : ''}`; - } else if (part === '"') { - stack.push(part); - resultHTML += `"`; - } else if (part === '```') { - stack.push(part); - inCodeBlock = true; - resultHTML += ``; - } else if (part === '`') { - stack.push(part); - resultHTML += ``; - } else { - resultHTML += part; - } - } - } - - while (stack.length) { - const part = stack.pop(); - if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') { - resultHTML += ``; - } - } - - return resultHTML; -} \ No newline at end of file