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-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT |
|
||||||
| [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | 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-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-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
|
||||||
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
|
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
|
||||||
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
|
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
|
||||||
|
|||||||
@@ -27,6 +27,11 @@
|
|||||||
"mdast-util-from-markdown": "^2.0.3",
|
"mdast-util-from-markdown": "^2.0.3",
|
||||||
"mdast-util-gfm": "^3.1.0",
|
"mdast-util-gfm": "^3.1.0",
|
||||||
"micromark-extension-gfm": "^3.0.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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import rehypeKatex from 'rehype-katex'
|
|||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
import { CodeBlock } from './CodeBlock.tsx'
|
import { CodeBlock } from './CodeBlock.tsx'
|
||||||
|
import { remarkMathCompatibility } from './remarkMathCompatibility.ts'
|
||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
import css from './MarkdownText.module.css'
|
import css from './MarkdownText.module.css'
|
||||||
|
|
||||||
const streamingRemarkPlugins = [remarkGfm]
|
const streamingRemarkPlugins = [remarkGfm]
|
||||||
const settledRemarkPlugins = [remarkGfm, remarkMath]
|
const settledRemarkPlugins = [
|
||||||
|
remarkGfm,
|
||||||
|
remarkMathCompatibility,
|
||||||
|
remarkMath,
|
||||||
|
]
|
||||||
const settledRehypePlugins = [rehypeKatex]
|
const settledRehypePlugins = [rehypeKatex]
|
||||||
|
|
||||||
function sanitizeUrl(url: string): string {
|
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:
|
micromark-extension-gfm:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 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:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user