/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/ui/src/context/marked.tsx
570 строк
15 KB
Luke Parker
fix(ui): preserve code spans adjacent to tildes (#35835)
14 июл 2026, 03:02
Не верифицирован
14 июл 2026, 03:02
9a51765
Код
Авторство
О чём код?
import { marked, type MarkedExtension, type Tokens } from "marked" import markedShiki from "marked-shiki" import katex from "katex" import { bundledLanguages, type BundledLanguage } from "shiki" import { createSimpleContext } from "./helper" import { markedCodeSpanBoundary } from "./marked-code-span" import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs" export const OpenCodeTheme = { name: "OpenCode", bg: "var(--color-background-stronger)", fg: "var(--text-base)", colors: { "editor.background": "var(--color-background-stronger)", "editor.foreground": "var(--text-base)", "gitDecoration.addedResourceForeground": "var(--syntax-diff-add)", "gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)", "gitDecoration.modifiedResourceForeground": "var(--syntax-diff-unknown)", // "gitDecoration.conflictingResourceForeground": "#ffca00", // "gitDecoration.modifiedResourceForeground": "#1a76d4", // "gitDecoration.untrackedResourceForeground": "#00cab1", // "gitDecoration.ignoredResourceForeground": "#84848A", // "terminal.titleForeground": "#adadb1", // "terminal.titleInactiveForeground": "#84848A", // "terminal.background": "#141415", // "terminal.foreground": "#adadb1", // "terminal.ansiBlack": "#141415", // "terminal.ansiRed": "#ff2e3f", // "terminal.ansiGreen": "#0dbe4e", // "terminal.ansiYellow": "#ffca00", // "terminal.ansiBlue": "#008cff", // "terminal.ansiMagenta": "#c635e4", // "terminal.ansiCyan": "#08c0ef", // "terminal.ansiWhite": "#c6c6c8", // "terminal.ansiBrightBlack": "#141415", // "terminal.ansiBrightRed": "#ff2e3f", // "terminal.ansiBrightGreen": "#0dbe4e", // "terminal.ansiBrightYellow": "#ffca00", // "terminal.ansiBrightBlue": "#008cff", // "terminal.ansiBrightMagenta": "#c635e4", // "terminal.ansiBrightCyan": "#08c0ef", // "terminal.ansiBrightWhite": "#c6c6c8", }, tokenColors: [ { scope: ["comment", "punctuation.definition.comment", "string.comment"], settings: { foreground: "var(--syntax-comment)", }, }, { scope: ["entity.other.attribute-name"], settings: { foreground: "var(--syntax-property)", // maybe attribute }, }, { scope: ["constant", "entity.name.constant", "variable.other.constant", "variable.language", "entity"], settings: { foreground: "var(--syntax-constant)", }, }, { scope: ["entity.name", "meta.export.default", "meta.definition.variable"], settings: { foreground: "var(--syntax-type)", }, }, { scope: ["meta.object.member"], settings: { foreground: "var(--syntax-primitive)", }, }, { scope: [ "variable.parameter.function", "meta.jsx.children", "meta.block", "meta.tag.attributes", "entity.name.constant", "meta.embedded.expression", "meta.template.expression", "string.other.begin.yaml", "string.other.end.yaml", ], settings: { foreground: "var(--syntax-punctuation)", }, }, { scope: ["entity.name.function", "support.type.primitive"], settings: { foreground: "var(--syntax-primitive)", }, }, { scope: ["support.class.component"], settings: { foreground: "var(--syntax-type)", }, }, { scope: "keyword", settings: { foreground: "var(--syntax-keyword)", }, }, { scope: [ "keyword.operator", "storage.type.function.arrow", "punctuation.separator.key-value.css", "entity.name.tag.yaml", "punctuation.separator.key-value.mapping.yaml", ], settings: { foreground: "var(--syntax-operator)", }, }, { scope: ["storage", "storage.type"], settings: { foreground: "var(--syntax-keyword)", }, }, { scope: ["storage.modifier.package", "storage.modifier.import", "storage.type.java"], settings: { foreground: "var(--syntax-primitive)", }, }, { scope: [ "string", "punctuation.definition.string", "string punctuation.section.embedded source", "entity.name.tag", ], settings: { foreground: "var(--syntax-string)", }, }, { scope: "support", settings: { foreground: "var(--syntax-primitive)", }, }, { scope: ["support.type.object.module", "variable.other.object", "support.type.property-name.css"], settings: { foreground: "var(--syntax-object)", }, }, { scope: "meta.property-name", settings: { foreground: "var(--syntax-property)", }, }, { scope: "variable", settings: { foreground: "var(--syntax-variable)", }, }, { scope: "variable.other", settings: { foreground: "var(--syntax-variable)", }, }, { scope: [ "invalid.broken", "invalid.illegal", "invalid.unimplemented", "invalid.deprecated", "message.error", "markup.deleted", "meta.diff.header.from-file", "punctuation.definition.deleted", "brackethighlighter.unmatched", "token.error-token", ], settings: { foreground: "var(--syntax-critical)", }, }, { scope: "carriage-return", settings: { foreground: "var(--syntax-keyword)", }, }, { scope: "string source", settings: { foreground: "var(--syntax-variable)", }, }, { scope: "string variable", settings: { foreground: "var(--syntax-constant)", }, }, { scope: [ "source.regexp", "string.regexp", "string.regexp.character-class", "string.regexp constant.character.escape", "string.regexp source.ruby.embedded", "string.regexp string.regexp.arbitrary-repitition", "string.regexp constant.character.escape", ], settings: { foreground: "var(--syntax-regexp)", }, }, { scope: "support.constant", settings: { foreground: "var(--syntax-primitive)", }, }, { scope: "support.variable", settings: { foreground: "var(--syntax-variable)", }, }, { scope: "meta.module-reference", settings: { foreground: "var(--syntax-info)", }, }, { scope: "punctuation.definition.list.begin.markdown", settings: { foreground: "var(--syntax-punctuation)", }, }, { scope: ["markup.heading", "markup.heading entity.name"], settings: { fontStyle: "bold", foreground: "var(--syntax-info)", }, }, { scope: "markup.quote", settings: { foreground: "var(--syntax-info)", }, }, { scope: "markup.italic", settings: { fontStyle: "italic", // foreground: "", }, }, { scope: "markup.bold", settings: { fontStyle: "bold", foreground: "var(--text-strong)", }, }, { scope: [ "markup.raw", "markup.inserted", "meta.diff.header.to-file", "punctuation.definition.inserted", "markup.changed", "punctuation.definition.changed", "markup.ignored", "markup.untracked", ], settings: { foreground: "var(--text-base)", }, }, { scope: "meta.diff.range", settings: { fontStyle: "bold", foreground: "var(--syntax-unknown)", }, }, { scope: "meta.diff.header", settings: { foreground: "var(--syntax-unknown)", }, }, { scope: "meta.separator", settings: { fontStyle: "bold", foreground: "var(--syntax-unknown)", }, }, { scope: "meta.output", settings: { foreground: "var(--syntax-unknown)", }, }, { scope: "meta.export.default", settings: { foreground: "var(--syntax-unknown)", }, }, { scope: [ "brackethighlighter.tag", "brackethighlighter.curly", "brackethighlighter.round", "brackethighlighter.square", "brackethighlighter.angle", "brackethighlighter.quote", ], settings: { foreground: "var(--syntax-unknown)", }, }, { scope: ["constant.other.reference.link", "string.other.link"], settings: { fontStyle: "underline", foreground: "var(--syntax-unknown)", }, }, { scope: "token.info-token", settings: { foreground: "var(--syntax-info)", }, }, { scope: "token.warn-token", settings: { foreground: "var(--syntax-warning)", }, }, { scope: "token.debug-token", settings: { foreground: "var(--syntax-info)", }, }, ], semanticTokenColors: { comment: "var(--syntax-comment)", string: "var(--syntax-string)", number: "var(--syntax-constant)", regexp: "var(--syntax-regexp)", keyword: "var(--syntax-keyword)", variable: "var(--syntax-variable)", parameter: "var(--syntax-variable)", property: "var(--syntax-property)", function: "var(--syntax-primitive)", method: "var(--syntax-primitive)", type: "var(--syntax-type)", class: "var(--syntax-type)", namespace: "var(--syntax-type)", enumMember: "var(--syntax-primitive)", "variable.constant": "var(--syntax-constant)", "variable.defaultLibrary": "var(--syntax-unknown)", }, } as unknown as ThemeRegistrationResolved registerCustomTheme("OpenCode", () => Promise.resolve(OpenCodeTheme)) function renderMathInText(text: string): string { let result = text // Display math: $$...$$ const displayMathRegex = /\$\$([\s\S]*?)\$\$/g result = result.replace(displayMathRegex, (_, math) => { try { return katex.renderToString(math, { displayMode: true, throwOnError: false, }) } catch { return `$$${math}$$` } }) // Inline math: \(...\) const inlineMathRegex = /\\\(((?:\\.|[^\\\n])*?)\\\)/g result = result.replace(inlineMathRegex, (_, math) => { try { return katex.renderToString(math, { displayMode: false, throwOnError: false, }) } catch { return `\\(${math}\\)` } }) return result } const inlineMathRegex = /^\\\(((?:\\.|[^\\\n])*?)\\\)/ const blockMathRegex = /^\$\$\n([\s\S]+?)\n\$\$(?:\n|$)/ const katexExtension: MarkedExtension = { extensions: [ { name: "inlineKatex", level: "inline", start(src) { const index = src.indexOf("\\(") if (index === -1) return return index }, tokenizer(src) { const match = src.match(inlineMathRegex) if (!match) return return { type: "inlineKatex", raw: match[0], text: match[1].trim(), displayMode: false, } }, renderer: renderKatexToken, }, { name: "blockKatex", level: "block", tokenizer(src) { const match = src.match(blockMathRegex) if (!match) return return { type: "blockKatex", raw: match[0], text: match[1].trim(), displayMode: true, } }, renderer: renderKatexToken, }, ], } function renderKatexToken(token: Tokens.Generic) { return katex.renderToString(typeof token.text === "string" ? token.text : "", { displayMode: token.displayMode === true, throwOnError: false, }) } function renderMathExpressions(html: string): string { // Split on code/pre/kbd tags to avoid processing their contents const codeBlockPattern = /(<(?:pre|code|kbd)[^>]*>[\s\S]*?<\/(?:pre|code|kbd)>)/gi const parts = html.split(codeBlockPattern) return parts .map((part, i) => { // Odd indices are the captured code blocks - leave them alone if (i % 2 === 1) return part // Process math only in non-code parts return renderMathInText(part) }) .join("") } async function highlightCodeBlocks(html: string): Promise<string> { const codeBlockRegex = /<pre><code(?:\s+class="language-([^"]*)")?>([\s\S]*?)<\/code><\/pre>/g const matches = [...html.matchAll(codeBlockRegex)] if (matches.length === 0) return html const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [], preferredHighlighter: "shiki-wasm", }) let result = html for (const match of matches) { const [fullMatch, lang, escapedCode] = match const code = escapedCode .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") let language = lang || "text" if (!(language in bundledLanguages)) { language = "text" } if (!highlighter.getLoadedLanguages().includes(language)) { await highlighter.loadLanguage(language as BundledLanguage) } const highlighted = highlighter.codeToHtml(code, { lang: language, theme: "OpenCode", tabindex: false, }) result = result.replace(fullMatch, () => highlighted) } return result } export type NativeMarkdownParser = (markdown: string) => Promise<string> export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({ name: "Marked", init: (props: { nativeParser?: NativeMarkdownParser }) => { const jsParser = marked.use( markedCodeSpanBoundary, { renderer: { link({ href, title, text }) { const titleAttr = title ? ` title="${title}"` : "" return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>` }, }, }, katexExtension, markedShiki({ async highlight(code, lang) { const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [], preferredHighlighter: "shiki-wasm", }) if (!(lang in bundledLanguages)) { lang = "text" } if (!highlighter.getLoadedLanguages().includes(lang)) { await highlighter.loadLanguage(lang as BundledLanguage) } return highlighter.codeToHtml(code, { lang: lang || "text", theme: "OpenCode", tabindex: false, }) }, }), ) if (props.nativeParser) { const nativeParser = props.nativeParser return { async parse(markdown: string): Promise<string> { const html = await nativeParser(markdown) const withMath = renderMathExpressions(html) return highlightCodeBlocks(withMath) }, } } return jsParser }, })