mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(ui): render common TeX math delimiters
This commit is contained in:
@@ -63,6 +63,11 @@ External packages that a workspace package resolves at runtime. `scripts/install
|
||||
| [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT |
|
||||
| [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT |
|
||||
| [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT |
|
||||
| [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT |
|
||||
| [`micromark-factory-space`](https://github.com/micromark/micromark/tree/main/packages/micromark-factory-space) | MIT |
|
||||
| [`micromark-util-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-character) | MIT |
|
||||
| [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT |
|
||||
| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
|
||||
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
|
||||
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
|
||||
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"mdast-util-gfm": "^3.1.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"micromark-extension-math": "^3.1.0",
|
||||
"micromark-factory-space": "^2.0.1",
|
||||
"micromark-util-character": "^2.1.1",
|
||||
"micromark-util-symbol": "^2.0.1",
|
||||
"micromark-util-types": "^2.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -5,11 +5,16 @@ import rehypeKatex from 'rehype-katex'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import { CodeBlock } from './CodeBlock.tsx'
|
||||
import { remarkMathCompatibility } from './remarkMathCompatibility.ts'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import css from './MarkdownText.module.css'
|
||||
|
||||
const streamingRemarkPlugins = [remarkGfm]
|
||||
const settledRemarkPlugins = [remarkGfm, remarkMath]
|
||||
const settledRemarkPlugins = [
|
||||
remarkGfm,
|
||||
remarkMathCompatibility,
|
||||
remarkMath,
|
||||
]
|
||||
const settledRehypePlugins = [rehypeKatex]
|
||||
|
||||
function sanitizeUrl(url: string): string {
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { factorySpace } from 'micromark-factory-space'
|
||||
import type {} from 'micromark-extension-math'
|
||||
import { markdownLineEnding } from 'micromark-util-character'
|
||||
import { codes, constants, types } from 'micromark-util-symbol'
|
||||
import type { Construct, Extension, Previous, State, Tokenizer } from 'micromark-util-types'
|
||||
|
||||
// oxlint-disable typescript/no-this-alias -- micromark binds tokenizer context only on the outer callback.
|
||||
|
||||
interface RemarkProcessor {
|
||||
data(): { micromarkExtensions?: Extension[] }
|
||||
}
|
||||
|
||||
const previousBackslash: Previous = function (code) {
|
||||
return code !== codes.backslash || this.events.at(-1)?.[1].type === types.characterEscape
|
||||
}
|
||||
|
||||
const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) {
|
||||
const self = this
|
||||
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
if (code !== codes.backslash) return nok(code)
|
||||
effects.enter('mathText')
|
||||
effects.enter('mathTextSequence')
|
||||
effects.consume(code)
|
||||
return open
|
||||
}
|
||||
|
||||
function open(code: number | null): State | undefined {
|
||||
if (code !== codes.leftParenthesis) return nok(code)
|
||||
effects.consume(code)
|
||||
effects.exit('mathTextSequence')
|
||||
return between
|
||||
}
|
||||
|
||||
function between(code: number | null): State | undefined {
|
||||
if (code === codes.eof) return nok(code)
|
||||
if (code === codes.backslash && self.previous !== codes.backslash) {
|
||||
return effects.attempt({ partial: true, tokenize: tokenizeClose }, close, dataStart)(code)
|
||||
}
|
||||
if (markdownLineEnding(code)) {
|
||||
effects.enter(types.lineEnding)
|
||||
effects.consume(code)
|
||||
effects.exit(types.lineEnding)
|
||||
return between
|
||||
}
|
||||
return dataStart(code)
|
||||
}
|
||||
|
||||
function dataStart(code: number | null): State | undefined {
|
||||
effects.enter('mathTextData')
|
||||
effects.consume(code)
|
||||
return data
|
||||
}
|
||||
|
||||
function data(code: number | null): State | undefined {
|
||||
if (code === codes.eof || code === codes.backslash || markdownLineEnding(code)) {
|
||||
effects.exit('mathTextData')
|
||||
return between(code)
|
||||
}
|
||||
effects.consume(code)
|
||||
return data
|
||||
}
|
||||
|
||||
function close(code: number | null): State | undefined {
|
||||
effects.exit('mathText')
|
||||
return ok(code)
|
||||
}
|
||||
|
||||
function tokenizeClose(closeEffects: Parameters<Tokenizer>[0], closeOk: State, closeNok: State): State {
|
||||
return slash
|
||||
|
||||
function slash(code: number | null): State | undefined {
|
||||
if (code !== codes.backslash) return closeNok(code)
|
||||
closeEffects.enter('mathTextSequence')
|
||||
closeEffects.consume(code)
|
||||
return parenthesis
|
||||
}
|
||||
|
||||
function parenthesis(code: number | null): State | undefined {
|
||||
if (code !== codes.rightParenthesis) return closeNok(code)
|
||||
closeEffects.consume(code)
|
||||
closeEffects.exit('mathTextSequence')
|
||||
return closeOk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createMathFlow(marker: number, openMarker: number, closeMarker: number, multiline: boolean): Construct {
|
||||
const tokenize: Tokenizer = function (effects, ok, nok) {
|
||||
const self = this
|
||||
const tail = self.events.at(-1)
|
||||
const initialSize = tail?.[1].type === types.linePrefix
|
||||
? tail[2].sliceSerialize(tail[1], true).length
|
||||
: 0
|
||||
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
if (code !== marker) return nok(code)
|
||||
effects.enter('mathFlow')
|
||||
effects.enter('mathFlowFence')
|
||||
effects.enter('mathFlowFenceSequence')
|
||||
effects.consume(code)
|
||||
return open
|
||||
}
|
||||
|
||||
function open(code: number | null): State | undefined {
|
||||
if (code !== openMarker) return nok(code)
|
||||
effects.consume(code)
|
||||
effects.exit('mathFlowFenceSequence')
|
||||
effects.exit('mathFlowFence')
|
||||
return marker === codes.dollarSign ? afterDollarOpen : content
|
||||
}
|
||||
|
||||
function afterDollarOpen(code: number | null): State | undefined {
|
||||
return code === codes.dollarSign ? nok(code) : content(code)
|
||||
}
|
||||
|
||||
function content(code: number | null): State | undefined {
|
||||
if (code === codes.eof) return nok(code)
|
||||
if (code === marker && (marker !== codes.backslash || self.previous !== codes.backslash)) {
|
||||
return effects.attempt({ partial: true, tokenize: tokenizeClosingFence }, closed, markerValueStart)(code)
|
||||
}
|
||||
if (markdownLineEnding(code)) {
|
||||
return multiline
|
||||
? effects.attempt(nonLazyContinuation, afterContinuation, nok)(code)
|
||||
: nok(code)
|
||||
}
|
||||
return valueStart(code)
|
||||
}
|
||||
|
||||
function afterContinuation(code: number | null): State | undefined {
|
||||
return effects.attempt(
|
||||
{ partial: true, tokenize: tokenizeClosingFence },
|
||||
closed,
|
||||
initialSize
|
||||
? factorySpace(effects, content, types.linePrefix, initialSize + 1)
|
||||
: content,
|
||||
)(code)
|
||||
}
|
||||
|
||||
function valueStart(code: number | null): State | undefined {
|
||||
effects.enter('mathFlowValue')
|
||||
effects.consume(code)
|
||||
return value
|
||||
}
|
||||
|
||||
function markerValueStart(code: number | null): State | undefined {
|
||||
effects.enter('mathFlowValue')
|
||||
effects.consume(code)
|
||||
return valueAfterMarker
|
||||
}
|
||||
|
||||
function valueAfterMarker(code: number | null): State | undefined {
|
||||
if (code === marker) {
|
||||
effects.consume(code)
|
||||
return value
|
||||
}
|
||||
return value(code)
|
||||
}
|
||||
|
||||
function value(code: number | null): State | undefined {
|
||||
if (code === codes.eof || code === marker || markdownLineEnding(code)) {
|
||||
effects.exit('mathFlowValue')
|
||||
return content(code)
|
||||
}
|
||||
effects.consume(code)
|
||||
return value
|
||||
}
|
||||
|
||||
function closed(code: number | null): State | undefined {
|
||||
effects.exit('mathFlow')
|
||||
return ok(code)
|
||||
}
|
||||
|
||||
function tokenizeClosingFence(
|
||||
closeEffects: Parameters<Tokenizer>[0],
|
||||
closeOk: State,
|
||||
closeNok: State,
|
||||
): State {
|
||||
return factorySpace(closeEffects, sequenceStart, types.linePrefix, constants.tabSize)
|
||||
|
||||
function sequenceStart(code: number | null): State | undefined {
|
||||
if (code !== marker) return closeNok(code)
|
||||
closeEffects.enter('mathFlowFence')
|
||||
closeEffects.enter('mathFlowFenceSequence')
|
||||
closeEffects.consume(code)
|
||||
return sequenceEnd
|
||||
}
|
||||
|
||||
function sequenceEnd(code: number | null): State | undefined {
|
||||
if (code !== closeMarker) return closeNok(code)
|
||||
closeEffects.consume(code)
|
||||
closeEffects.exit('mathFlowFenceSequence')
|
||||
return factorySpace(closeEffects, after, types.whitespace)
|
||||
}
|
||||
|
||||
function after(code: number | null): State | undefined {
|
||||
if (code !== codes.eof && !markdownLineEnding(code)) return closeNok(code)
|
||||
closeEffects.exit('mathFlowFence')
|
||||
return closeOk(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
concrete: true,
|
||||
name: marker === codes.dollarSign ? 'sameLineDollarMathFlow' : 'backslashMathFlow',
|
||||
tokenize,
|
||||
}
|
||||
}
|
||||
|
||||
const tokenizeNonLazyContinuation: Tokenizer = function (effects, ok, nok) {
|
||||
const self = this
|
||||
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
if (code === codes.eof) return ok(code)
|
||||
if (!markdownLineEnding(code)) return nok(code)
|
||||
effects.enter(types.lineEnding)
|
||||
effects.consume(code)
|
||||
effects.exit(types.lineEnding)
|
||||
return lineStart
|
||||
}
|
||||
|
||||
function lineStart(code: number | null): State | undefined {
|
||||
return self.parser.lazy[self.now().line] ? nok(code) : ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
const nonLazyContinuation: Construct = {
|
||||
partial: true,
|
||||
tokenize: tokenizeNonLazyContinuation,
|
||||
}
|
||||
|
||||
const backslashMathText: Construct = {
|
||||
name: 'backslashMathText',
|
||||
previous: previousBackslash,
|
||||
tokenize: tokenizeBackslashMathText,
|
||||
}
|
||||
|
||||
const backslashMathFlow = createMathFlow(
|
||||
codes.backslash,
|
||||
codes.leftSquareBracket,
|
||||
codes.rightSquareBracket,
|
||||
true,
|
||||
)
|
||||
|
||||
const sameLineDollarMathFlow = createMathFlow(
|
||||
codes.dollarSign,
|
||||
codes.dollarSign,
|
||||
codes.dollarSign,
|
||||
false,
|
||||
)
|
||||
|
||||
const backslashMath: Extension = {
|
||||
flow: {
|
||||
[codes.backslash]: backslashMathFlow,
|
||||
[codes.dollarSign]: sameLineDollarMathFlow,
|
||||
},
|
||||
text: { [codes.backslash]: backslashMathText },
|
||||
}
|
||||
|
||||
/**
|
||||
* Add TeX backslash delimiters and same-line display-dollar blocks to remark.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
export function remarkMathCompatibility(this: RemarkProcessor): undefined {
|
||||
const data = this.data()
|
||||
const extensions = data.micromarkExtensions ?? (data.micromarkExtensions = [])
|
||||
extensions.push(backslashMath)
|
||||
}
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -1697,6 +1697,21 @@ importers:
|
||||
micromark-extension-gfm:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
micromark-extension-math:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
micromark-factory-space:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
micromark-util-character:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
micromark-util-symbol:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
micromark-util-types:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
|
||||
Reference in New Issue
Block a user