|
|
@@ -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)
|
|
|
+}
|