${text}
` - }, - } - const key = `markdown-preload-${crypto.randomUUID()}` - - await preloadMarkdown("prepared response", key, parser) - await preloadMarkdown("prepared response", key, parser) - - expect(parsed).toEqual(["prepared response"]) -}) diff --git a/packages/session-ui/src/components/markdown-projection.ts b/packages/session-ui/src/components/markdown-projection.ts new file mode 100644 index 000000000000..459583d41191 --- /dev/null +++ b/packages/session-ui/src/components/markdown-projection.ts @@ -0,0 +1,11 @@ +import type { Block, Projection } from "./markdown-stream" + +export function completedProjection(text: string): Projection { + return { text, blocks: [{ raw: text, src: text, mode: "full" }] } +} + +export function canReusePendingBlock(current: Pick${code}`)
+
+test("renders links with application attributes", async () => {
+ expect(await parser.parse("[OpenCode](https://opencode.ai)")).toBe(
+ '\n',
+ )
+})
+
+test("renders inline and block math", async () => {
+ expect(await parser.parse("\\(x^2\\)")).toContain('')
+ expect(await parser.parse("$$\nx^2\n$$\n")).toContain('')
+})
+
+test("uses the configured code highlighter", async () => {
+ expect(await parser.parse("```ts\nconst value = 1\n```\n")).toBe('const value = 1\n') +}) diff --git a/packages/ui/src/context/marked-parser.tsx b/packages/ui/src/context/marked-parser.tsx new file mode 100644 index 000000000000..71e48351ca53 --- /dev/null +++ b/packages/ui/src/context/marked-parser.tsx @@ -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
~0.1576 to measurement-window-only 0.00092
([\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
+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 `${text}`
- },
- },
- },
- 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 {
- 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,
+ })
+ }),
})