143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
|
|
import { resolveArrowLigatureInput } from '../utils/arrowLigatures'
|
||
|
|
import {
|
||
|
|
createRichEditorInputTransformExtension,
|
||
|
|
type RichEditorInputTransform,
|
||
|
|
} from './richEditorInputTransform'
|
||
|
|
|
||
|
|
const PREFIX_CONTEXT_LENGTH = 2
|
||
|
|
|
||
|
|
interface CodeContextSelection {
|
||
|
|
from?: unknown
|
||
|
|
to?: unknown
|
||
|
|
$from?: {
|
||
|
|
depth: number
|
||
|
|
node: (depth?: number) => {
|
||
|
|
type?: {
|
||
|
|
name?: string
|
||
|
|
spec?: { code?: boolean }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ArrowLigatureTransactionArgs<Transaction> {
|
||
|
|
event: InputEvent & { data: string }
|
||
|
|
literalAsciiCursor: number | null
|
||
|
|
view: {
|
||
|
|
state: {
|
||
|
|
doc: {
|
||
|
|
textBetween: (from: number, to: number, blockSeparator: string, leafText: string) => string
|
||
|
|
}
|
||
|
|
selection: CodeContextSelection
|
||
|
|
tr: {
|
||
|
|
insertText: (text: string, from: number, to: number) => Transaction
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ArrowLigatureTransactionResult<Transaction> {
|
||
|
|
nextLiteralAsciiCursor: number | null
|
||
|
|
transaction: Transaction | null
|
||
|
|
}
|
||
|
|
|
||
|
|
function isInsertedCharacter(event: InputEvent): event is InputEvent & { data: string } {
|
||
|
|
return event.inputType === 'insertText' && typeof event.data === 'string'
|
||
|
|
}
|
||
|
|
|
||
|
|
function isCodeContext(selection: CodeContextSelection): boolean {
|
||
|
|
const position = selection.$from
|
||
|
|
if (!position) return false
|
||
|
|
|
||
|
|
for (let depth = position.depth; depth >= 0; depth--) {
|
||
|
|
const type = position.node(depth).type
|
||
|
|
if (type?.spec?.code || type?.name === 'codeBlock') return true
|
||
|
|
}
|
||
|
|
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
function getWritableCursor(selection: CodeContextSelection): number | null {
|
||
|
|
const { from, to } = selection
|
||
|
|
if (typeof from !== 'number' || typeof to !== 'number') return null
|
||
|
|
if (!Number.isFinite(from) || !Number.isFinite(to)) return null
|
||
|
|
|
||
|
|
return from === to ? from : null
|
||
|
|
}
|
||
|
|
|
||
|
|
function withoutTransaction<Transaction>(
|
||
|
|
nextLiteralAsciiCursor: number | null,
|
||
|
|
): ArrowLigatureTransactionResult<Transaction> {
|
||
|
|
return { nextLiteralAsciiCursor, transaction: null }
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildArrowLigatureTransaction<Transaction>({
|
||
|
|
event,
|
||
|
|
literalAsciiCursor,
|
||
|
|
view,
|
||
|
|
}: ArrowLigatureTransactionArgs<Transaction>): ArrowLigatureTransactionResult<Transaction> {
|
||
|
|
try {
|
||
|
|
const { state } = view
|
||
|
|
const { selection } = state
|
||
|
|
const from = getWritableCursor(selection)
|
||
|
|
if (from === null) return withoutTransaction(literalAsciiCursor)
|
||
|
|
if (isCodeContext(selection)) return withoutTransaction(null)
|
||
|
|
|
||
|
|
const beforeText = state.doc.textBetween(
|
||
|
|
Math.max(0, from - PREFIX_CONTEXT_LENGTH),
|
||
|
|
from,
|
||
|
|
'',
|
||
|
|
'',
|
||
|
|
)
|
||
|
|
const resolution = resolveArrowLigatureInput({
|
||
|
|
beforeText,
|
||
|
|
cursor: from,
|
||
|
|
inputText: event.data,
|
||
|
|
literalAsciiCursor,
|
||
|
|
})
|
||
|
|
if (!resolution.change) return withoutTransaction(resolution.nextLiteralAsciiCursor)
|
||
|
|
|
||
|
|
return {
|
||
|
|
nextLiteralAsciiCursor: resolution.nextLiteralAsciiCursor,
|
||
|
|
transaction: state.tr.insertText(
|
||
|
|
resolution.change.insert,
|
||
|
|
resolution.change.from,
|
||
|
|
resolution.change.to,
|
||
|
|
),
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
return withoutTransaction(null)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createArrowLigatureInputTransform(): RichEditorInputTransform {
|
||
|
|
let literalAsciiCursor: number | null = null
|
||
|
|
|
||
|
|
return {
|
||
|
|
handleBeforeInput(event, { view }) {
|
||
|
|
if (!isInsertedCharacter(event)) return null
|
||
|
|
|
||
|
|
const result = buildArrowLigatureTransaction({ event, literalAsciiCursor, view })
|
||
|
|
literalAsciiCursor = result.nextLiteralAsciiCursor
|
||
|
|
if (result.transaction === null) return null
|
||
|
|
|
||
|
|
return {
|
||
|
|
ignoreDispatchError: true,
|
||
|
|
onDispatchError: () => {
|
||
|
|
literalAsciiCursor = null
|
||
|
|
},
|
||
|
|
preventDefault: true,
|
||
|
|
transaction: result.transaction,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
reset() {
|
||
|
|
literalAsciiCursor = null
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const createArrowLigaturesExtension = createRichEditorInputTransformExtension({
|
||
|
|
createTransforms: () => [createArrowLigatureInputTransform()],
|
||
|
|
key: 'arrowLigatures',
|
||
|
|
})
|