UNPKG

docutils-ts

Version:

Port of the Python Docutils library to TypeScript

543 lines (542 loc) 20.3 kB
import { isIterable } from "./utils.js"; import { defaultDebugFlag } from './constants.js'; import { ApplicationError, EOFError, InvalidArgumentsError, InvalidStateError, UnimplementedError as Unimp } from "./exceptions.js"; import StateCorrection from "./stateCorrection.js"; import StringList from "./stringList.js"; import TransitionCorrection from "./transitionCorrection.js"; import UnexpectedIndentationError from "./error/unexpectedIndentationError.js"; import DuplicateStateError from "./error/duplicateStateError.js"; export class StateMachineError extends Error { } export class UnknownStateError extends StateMachineError { } export class TransitionPatternNotFound extends StateMachineError { } export class TransitionMethodNotFound extends StateMachineError { } //export class UnexpectedIndentationError extends StateMachineError { } //export class TransitionCorrection extends Error { } /** * A finite state machine for text filters using regular expressions. * * The input is provided in the form of a list of one-line strings (no * newlines). States are subclasses of the `State` class. Transitions consist * of regular expression patterns and transition methods, and are defined in * each state. * * The state machine is started with the `run()` method, which returns the * results of processing in a list. * */ class StateMachine { hasState(stateName) { return stateName in this.states; } getState2(stateName) { if (!this.hasState(stateName)) { throw new InvalidStateError(`Invalid state ${stateName}`); } return this.states[stateName]; } /* def __init__(self, state_classes, initial_state, debug=False): */ /* export interface StateMachineConstructorArgs { stateFactory?: Statefactory; initialState?: string; debug?: boolean; debugFn?: DebugFunction; } */ constructor(args) { this.states = {}; this.inputLines = new StringList([]); this.debugFn = ((line) => { this.logger.debug(line); }).bind(this); this.lineOffset = -1; this.line = ''; this.inputOffset = 0; const cArgs = { ...args }; this.logger = cArgs.logger; /* Initialize instance junk that we can't do except through this method. */ if (cArgs.debug === undefined) { cArgs.debug = defaultDebugFlag; } if (cArgs.debug && !cArgs.debugFn) { // make this unexpected error? // throw new Error("unexpected lack of debug function"); cArgs.debugFn = this.logger.debug.bind(this.logger); } if (cArgs.stateFactory !== undefined) { this.stateFactory = cArgs.stateFactory; } if (cArgs.debugFn !== undefined) { this.debugFn = cArgs.debugFn; } this.lineOffset = -1; this.debug = cArgs.debug; this.initialState = cArgs.initialState; this.currentState = cArgs.initialState; this.states = {}; if (!cArgs.stateFactory) { throw new Error('need statefactory'); } const stateClasses = cArgs.stateFactory.getStateClasses(); if (!isIterable(stateClasses)) { throw new Error(`expecting iterable, got ${stateClasses}`); } this.addStates(stateClasses); this.observers = []; } createStateMachine(rstStateMachine, initialState, stateFactory = rstStateMachine.stateFactory) { let stateMachine = new StateMachine({ initialState, stateFactory: this.stateFactory, debug: this.debug, debugFn: this.debugFn, logger: this.logger, }); return stateMachine; } unlink() { for (const value of Object.values(this.states)) { value.unlink(); } this.states = {}; } /** * Run the state machine on `input_lines`. Return results (a list). * * Reset `self.line_offset` and `self.current_state`. Run the * beginning-of-file transition. Input one line at a time and check for a * matching transition. If a match is found, call the transition method * and possibly change the state. Store the context returned by the * transition method to be passed on to the next transition matched. * Accumulate the results returned by the transition methods in a list. * Run the end-of-file transition. Finally, return the accumulated * results. * * Parameters: * * - `input_lines`: a list of strings without newlines, or `StringList`. * - `input_offset`: the line offset of `input_lines` from the beginning * of the file. * - `context`: application-specific storage. * - `input_source`: name or path of source of `input_lines`. * - `initial_state`: name of initial state. */ run(inputLines, inputOffset, runContext, inputSource, initialState, ...rest) { this.logger.debug('run'); // RUNTIMEINIT let lines = inputLines; this.runtimeInit(); if (lines instanceof StringList) { this.inputLines = lines; } else if (lines == null) { throw new InvalidArgumentsError('inputLines should not be null or undefined'); } else { if (!Array.isArray(lines)) { lines = [lines]; } let inputLinesAsArray; if (inputLines instanceof StringList) { inputLinesAsArray = inputLines; } else if (Array.isArray(inputLines)) { inputLinesAsArray = inputLines; } else if (typeof inputLines === 'string') { inputLinesAsArray = [inputLines]; } else { throw new InvalidArgumentsError('inputLines should be string or StringList'); } this.inputLines = new StringList(inputLinesAsArray, inputSource || ''); } this.inputOffset = inputOffset; this.lineOffset = -1; this.currentState = initialState || this.initialState; if (this.debug && this.debugFn !== undefined) { this.debugFn(`\nStateMachine.run: input_lines (line_offset=${this.lineOffset}):\n| ${this.inputLines.join('\n| ')}`); } let transitions; const results = []; let state = this.getState(); let nextStateName; let nextStateReturn; let nextStateVar; let nextState; let result = []; let context = []; try { if (this.debug) { this.debugFn('\nStateMachine.run: bof transition'); } [context, result] = state.bof(context); if (!Array.isArray(context)) { throw new Error('expecting array'); } results.push(...result); while (true) { try { try { this.nextLine(); this.logger.debug('line', { value: this.line }); if (this.debug) { if (Number.isNaN(this.lineOffset)) { throw new Error(); } const [source, offset] = this.inputLines.info(this.lineOffset); this.debugFn(`\nStateMachine.run: line (source=${source}, offset=${offset}):\n| ${this.line}`); } if (!Array.isArray(context)) { throw new Error('context should be array'); } const r = this.checkLine(context, state, transitions); if (!isIterable(r)) { throw new Error(`Expect iterable result, got: ${r}`); } [context, nextStateReturn, result] = r; nextStateVar = nextStateReturn.stateName || nextStateReturn; ; if (!Array.isArray(context)) { throw new Error('context should be array'); } if (!isIterable(result)) { throw new Error(`Expect iterable result, got: ${result}`); } results.push(...result); } catch (error) { if (error instanceof EOFError) { if (this.debug) { this.debugFn(`\nStateMachine.run: ${state.constructor.name}.eof transition`); } result = state.eof(context); results.push(...result); break; } else { throw error; } } } catch (error) { if (error instanceof TransitionCorrection) { this.previousLine(); transitions = [error.stateName]; if (this.debug) { this.debugFn(`\nStateMachine.run: TransitionCorrection to ` + `state "${state.stateName}", transition ${transitions[0]}.`); } /* Cant continue, makes no sense? ?? */ continue; } else if (error instanceof StateCorrection) { this.previousLine(); nextStateName = error.args[0]; nextStateVar = nextStateName; let tstr; if (error.args.length === 1) { transitions = undefined; tstr = ''; } else { transitions = [error.args[1]]; tstr = error.args[1].toString(); } if (this.debug) { this.debugFn(`\nStateMachine.run: StateCorrection to state ` + `"${nextState}", transition ${tstr}.`); } } else { throw error; } } /* we need this somehow, its part of a try, except, else */ // transitions = undefined state = this.getState(nextStateVar); } } catch (error) { throw error; } this.observers = []; return results; } /** * Return current state object; set it first if * `next_state` given. Parameter `next_state`: a string, * the name of the next state. Exception: * `UnknownStateError` raised if `next_state` unknown. */ getState(nextState) { if (nextState) { if (this.debug && nextState !== this.currentState) { this.debugFn(`StateMachine.getState: ` + `changing state from "${this.currentState}"` + `to "${nextState}" (input line ${this.absLineNumber()})`); } this.currentState = nextState; } if (this.currentState === undefined) { throw new InvalidStateError(); } if (this.states[this.currentState] === undefined) { throw new UnknownStateError(this.currentState); } return this.states[this.currentState]; } /** Load `self.line` with the `n`'th next line and return it. */ nextLine(n = 1) { // console.log('*** advancing to next line'); this.lineOffset += n; if (this.lineOffset >= this.inputLines.length) { this.line = ''; this.notifyObservers(); throw new EOFError(); } this.line = this.inputLines[this.lineOffset]; this.notifyObservers(); // console.log(`line is "${this.line}"`); return this.line; } isNextLineBlank() { return this.inputLines.length > this.lineOffset + 1 ? !this.inputLines[this.lineOffset + 1].trim() : true; } atEof() { return this.lineOffset >= this.inputLines.length - 1; } atBof() { return this.lineOffset <= 0; } previousLine(n = 1) { this.lineOffset -= n; if (this.lineOffset < 0) { this.line = ''; } else { this.line = this.inputLines[this.lineOffset]; } this.notifyObservers(); return this.line; } gotoLine(lineOffset) { this.lineOffset = lineOffset - this.inputOffset; this.line = this.inputLines[this.lineOffset]; this.notifyObservers(); return this.line; } getSource(lineOffset) { return this.inputLines.source(lineOffset - this.inputOffset); } absLineOffset() { return this.lineOffset + this.inputOffset; } absLineNumber() { return this.lineOffset + this.inputOffset + 1; } getSourceAndLine(lineno) { let offset; let srcoffset; let srcline; let src; if (lineno === undefined) { offset = this.lineOffset; } else { offset = lineno - this.inputOffset - 1; } try { [src, srcoffset] = this.inputLines.info(offset); if (srcoffset !== undefined) { srcline = srcoffset + 1; } } catch (error) { // fixme code smell } return [src, srcline]; } insertInput(inputLines, source) { // self.input_lines.insert(self.line_offset + 1, '', // source='internal padding after '+source, // offset=len(input_lines)) // self.input_lines.insert(self.line_offset + 1, '', // source='internal padding before '+source, // offset=-1) // self.input_lines.insert(self.line_offset + 2, // StringList(input_lines, source)) throw new Unimp(); } getTextBlock(flushLeft = false) { let block; try { block = this.inputLines.getTextBlock(this.lineOffset, flushLeft); this.nextLine(block.length - 1); return block; } catch (error) { if (error instanceof UnexpectedIndentationError) { block = error.block; this.nextLine(block.length - 1); // advance to last line of block } throw error; } } /* * Examine one line of input for a transition match & execute its method. * * Parameters: * * - `context`: application-dependent storage. * - `state`: a `State` object, the current state. * - `transitions`: an optional ordered list of transition names to try, * instead of ``state.transition_order``. * * Return the values returned by the transition method: * * - context: possibly modified from the parameter `context`; * - next state name (`State` subclass name); * - the result output of the transition, a list. * * When there is no match, ``state.no_match()`` is called and its return * value is returned. */ checkLine(context, state, transitions) { this.logger.silly({ kind: 'enterFunction', function: 'checkLine' }); if (!Array.isArray(context)) { throw new Error('context should be array'); } if (transitions === undefined) { transitions = state.transitionOrder; } if (this.debug) { this.debugFn(`\nStateMachine.check_line: ` + `state="${state.constructor.name}", transitions=${transitions}.`); } const stateCorrection = true; for (let i = 0; i < transitions.length; i++) { let t = transitions[i]; const v = state.transitions[t]; const [pattern, method, nextState] = v; if (!pattern.exec) { throw new InvalidStateError(`unexpected pattern ${v}`); } this.logger.silly(`executing for ${t} pattern ${pattern} on line ${this.line}`); const result = pattern.exec(this.line); if (result) { this.logger.debug(`line matched transition ${t}`, { value: t }); if (this.debug) { this.debugFn(`\nStateMachine.checkLine: ` + `Matched transition '"${t}"` + `in state "${state.constructor.name}`); } const r = method.bind(state)({ pattern, result, input: this.line }, context, nextState); // istanbul ignore if if (r === undefined) { throw new Error(); } // console.log(`return is >>> `); // console.log(r); return r; } } return state.noMatch(context, transitions); } addState(stateClass) { let stateName; if (typeof stateClass === 'string') { stateName = stateClass; } else { stateName = stateClass.stateName; } //this.logger.silly(`adding state ${stateName}`, { stateName }); if (this.hasState(stateName)) { throw new DuplicateStateError(stateName); } if (!stateName) { throw new Error(`need statename for ${stateClass}`); } if (this.stateFactory === undefined) { throw new InvalidStateError('stateFacory'); } const r = this.stateFactory.createState(stateName, this); this.states[stateName] = r; } addStates(stateClasses) { if (!stateClasses) { throw new Error(''); } stateClasses.forEach(this.addState.bind(this)); } runtimeInit() { Object.values(this.states).forEach((s) => s.runtimeInit()); } error() { //fixme } attachObserver(observer) { this.observers.push(observer); } detachObserver(observer) { this.observers.splice(this.observers.indexOf(observer), 1); } notifyObservers() { for (const observer of this.observers) { if (observer === undefined) { throw new ApplicationError('undefined observer'); } try { let info; try { info = this.inputLines.info(this.lineOffset); } catch (err) { /* Empty */ } if (info === undefined) { // throw new Error("undefined info"); continue; } if (!isIterable(info)) { throw new Error('isIterable'); } observer(info[0], info[1]); } catch (err) { if (err instanceof Error) { console.log(err.stack); } else { console.log('Unknown error:', err); } } } } } function expandtabs(strVal) { let tabIndex; while ((tabIndex = strVal.indexOf('\t')) !== -1) { strVal = strVal.substring(0, tabIndex) + Array(8 - (tabIndex % 8)).fill(' ').join('') + strVal.substring(tabIndex + 1); } return strVal; } export function string2lines(astring, args) { if (!astring) { astring = ''; } if (!args) { args = {}; } let { tabWidth, convertWhitespace, whitespace } = args; if (whitespace === undefined) { } if (tabWidth === undefined) { tabWidth = 8; } const result = astring.split('\n'); if (astring[astring.length - 1] === '\n') { result.pop(); } return result.map(expandtabs); } export { StateMachine };