import { Lexer } from '../../library-ts/browser/text/lexer/Lexer.js'; export interface MultiLineBlock { type: string; start: RegExp; end: RegExp; cmClass: string; } interface KeywordSet { id: string; words: Set; cmClass: string; tokenTypes: string[]; } export class CodeMirrorLexerMode { private _lexer: Lexer; private _typeToClass: Record; private _multiLineBlocks: MultiLineBlock[]; private _multiLineTypes: Set; private _keywordSets: KeywordSet[] = []; private _cmModeCache: any = null; name: string = ''; constructor( lexer: Lexer, typeToClass: Record, multiLineBlocks: MultiLineBlock[] = [] ) { this._lexer = lexer; this._typeToClass = typeToClass; this._multiLineBlocks = multiLineBlocks; this._multiLineTypes = new Set( multiLineBlocks.map( b => b.type ) ); } setKeywordSet( id: string, words: string[], cmClass: string, tokenTypes: string[] = [ 'CWORD' ] ): this { const existing = this._keywordSets.find( ks => ks.id === id ); if ( existing ) { existing.words = new Set( words ); existing.cmClass = cmClass; existing.tokenTypes = tokenTypes; } else { this._keywordSets.push( { id, words: new Set( words ), cmClass, tokenTypes } ); } return this; } removeKeywordSet( id: string ): this { this._keywordSets = this._keywordSets.filter( ks => ks.id !== id ); return this; } refresh( cm: any ): void { cm.setOption( 'mode', this.name ); } get cmMode(): any { if ( this._cmModeCache ) return this._cmModeCache; const self = this; this._cmModeCache = { startState: () => ( { multiLine: null as string | null } ), token( stream: any, state: any ): string | null { // Ongoing multi-line block if ( state.multiLine !== null ) { const block = self._multiLineBlocks.find( b => b.type === state.multiLine )!; const remaining = stream.string.slice( stream.pos ); const endMatch = block.end.exec( remaining ); if ( endMatch !== null ) { const len = endMatch.index + endMatch[ 0 ].length; for ( let i = 0; i < len; i++ ) stream.next(); state.multiLine = null; } else { stream.skipToEnd(); } return block.cmClass; } // Multi-line block starts for ( const block of self._multiLineBlocks ) { const remaining = stream.string.slice( stream.pos ); const startMatch = block.start.exec( remaining ); if ( startMatch && startMatch.index === 0 ) { const startLen = startMatch[ 0 ].length; const afterStart = remaining.slice( startLen ); const endMatch = block.end.exec( afterStart ); if ( endMatch !== null ) { const totalLen = startLen + endMatch.index + endMatch[ 0 ].length; for ( let i = 0; i < totalLen; i++ ) stream.next(); } else { for ( let i = 0; i < startLen; i++ ) stream.next(); stream.skipToEnd(); state.multiLine = block.type; } return block.cmClass; } } // Regular matchers const matchers = self._lexer.modes.get( 'default' ) ?? []; for ( const matcher of matchers ) { if ( self._multiLineTypes.has( matcher.type ) ) continue; const startPos = stream.pos; const len = matcher.matchLength( stream.string, startPos ); if ( len > 0 ) { for ( let i = 0; i < len; i++ ) stream.next(); const word = stream.string.slice( startPos, stream.pos ); for ( const ks of self._keywordSets ) { if ( ks.tokenTypes.includes( matcher.type ) && ks.words.has( word ) ) { return ks.cmClass; } } return self._typeToClass[ matcher.type ] ?? null; } } stream.next(); return null; } }; return this._cmModeCache; } }