fix(app): move markdown parsing to worker (#40356)
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
import type { MarkedExtension } from "marked"
|
||||
|
||||
// Keep adjacent tilde and backtick runs separate until markedjs/marked#4011 is released.
|
||||
export const markedCodeSpanBoundary = {
|
||||
tokenizer: {
|
||||
inlineText(src) {
|
||||
const match = /^(`+(?=~)|~+(?=`))/.exec(src)
|
||||
if (!match) return false
|
||||
return {
|
||||
type: "text",
|
||||
raw: match[0],
|
||||
text: match[0],
|
||||
escaped: this.lexer.state.inRawBlock,
|
||||
}
|
||||
},
|
||||
},
|
||||
} satisfies MarkedExtension
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createMarkdownParser } from "./marked-parser"
|
||||
|
||||
const parser = createMarkdownParser((code, language) => `<pre data-language="${language}">${code}</pre>`)
|
||||
|
||||
test("renders links with application attributes", async () => {
|
||||
expect(await parser.parse("[OpenCode](https://opencode.ai)")).toBe(
|
||||
'<p><a href="https://opencode.ai" class="external-link" target="_blank" rel="noopener noreferrer">OpenCode</a></p>\n',
|
||||
)
|
||||
})
|
||||
|
||||
test("renders inline and block math", async () => {
|
||||
expect(await parser.parse("\\(x^2\\)")).toContain('<span class="katex">')
|
||||
expect(await parser.parse("$$\nx^2\n$$\n")).toContain('<span class="katex-display">')
|
||||
})
|
||||
|
||||
test("uses the configured code highlighter", async () => {
|
||||
expect(await parser.parse("```ts\nconst value = 1\n```\n")).toBe('<pre data-language="ts">const value = 1</pre>\n')
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import katex from "katex"
|
||||
import { Marked, type MarkedExtension, type Tokens } from "marked"
|
||||
import markedShiki from "marked-shiki"
|
||||
|
||||
export function createMarkdownParser(highlight: (code: string, language: string) => string | Promise<string>) {
|
||||
return new Marked(
|
||||
{
|
||||
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({ highlight }),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Marked } from "marked"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
|
||||
test("preserves code spans adjacent to tildes", async () => {
|
||||
const marked = new Marked(markedCodeSpanBoundary)
|
||||
const marked = new Marked()
|
||||
|
||||
expect(await marked.parse("~`0.1576` to measurement-window-only `0.00092`")).toBe(
|
||||
"<p>~<code>0.1576</code> to measurement-window-only <code>0.00092</code></p>\n",
|
||||
@@ -0,0 +1,10 @@
|
||||
import { registerCustomTheme } from "@pierre/diffs"
|
||||
import { OpenCodeTheme } from "./marked-theme"
|
||||
|
||||
let registered = false
|
||||
|
||||
export function registerOpenCodeTheme() {
|
||||
if (registered) return
|
||||
registered = true
|
||||
registerCustomTheme("OpenCode", () => Promise.resolve(OpenCodeTheme))
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import type { 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
|
||||
@@ -1,570 +1,28 @@
|
||||
import { marked, type MarkedExtension, type Tokens } from "marked"
|
||||
import markedShiki from "marked-shiki"
|
||||
import katex from "katex"
|
||||
import { getSharedHighlighter } from "@pierre/diffs"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
|
||||
import { createMarkdownParser } from "./marked-parser"
|
||||
import { registerOpenCodeTheme } from "./marked-theme-register"
|
||||
|
||||
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
|
||||
export { OpenCodeTheme } from "./marked-theme"
|
||||
|
||||
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>
|
||||
registerOpenCodeTheme()
|
||||
|
||||
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
|
||||
},
|
||||
init: () =>
|
||||
createMarkdownParser(async (code, language) => {
|
||||
const highlighter = await getSharedHighlighter({
|
||||
themes: ["OpenCode"],
|
||||
langs: [],
|
||||
preferredHighlighter: "shiki-wasm",
|
||||
})
|
||||
const name = language in bundledLanguages ? language : "text"
|
||||
if (!highlighter.getLoadedLanguages().includes(name)) await highlighter.loadLanguage(name as BundledLanguage)
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: name,
|
||||
theme: "OpenCode",
|
||||
tabindex: false,
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user