text-machine
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -9,3 +9,3 @@ module.exports = { | ||
'parserOptions': { | ||
'ecmaVersion': 8 | ||
'ecmaVersion': 2020 | ||
} , | ||
@@ -30,5 +30,6 @@ 'extends': [ 'eslint:recommended' ] , | ||
'no-lonely-if': 'error' , | ||
'no-nested-ternary': 'error' , | ||
'no-nested-ternary': 'off' , // Now I use the streamlined ternary operator a lot | ||
'no-shadow': 'warn' , | ||
'no-shadow-restricted-names': 'error' , | ||
'require-atomic-updates': 'off' , // check for possible race condition on assignment, interesting but too nitpicky | ||
@@ -64,2 +65,3 @@ | ||
'MemberExpression': 1 , | ||
'flatTernaryExpressions': true | ||
} ] , | ||
@@ -134,4 +136,5 @@ 'newline-per-chained-call': [ 'error', { | ||
'ObjectPattern' : { | ||
// object destructuring assigment | ||
'consistent': true , | ||
'minProperties': 6 | ||
'minProperties': 8 | ||
} | ||
@@ -138,0 +141,0 @@ } ] , |
/* | ||
Text Machine | ||
Copyright (c) 2018 Cédric Ronvel | ||
Copyright (c) 2018 - 2022 Cédric Ronvel | ||
@@ -43,6 +43,8 @@ The MIT License (MIT) | ||
this.index = 0 ; | ||
// TODO | ||
//this.offset = 0 ; | ||
//this.savedStateStack = [] ; | ||
this.sanitize() ; | ||
this.reset() ; | ||
@@ -55,8 +57,24 @@ } | ||
TextMachine.prototype.reset = function reset() { | ||
this.stateStack = [ { | ||
name: this.program.config.initState || 'init' | ||
} ] ; | ||
const MATCH_ALWAYS = 0 ; | ||
const MATCH_STRING = 1 ; | ||
const MATCH_SET = 2 ; | ||
const MATCH_REGEXP = 3 ; | ||
const MATCH_FUNCTION = 4 ; | ||
//this.offset = 0 ; | ||
TextMachine.prototype.sanitize = function() { | ||
if ( this.program.states && typeof this.program.states === 'object' ) { | ||
for ( let name in this.program.states ) { | ||
let stateProgram = this.program.states[ name ] ; | ||
if ( stateProgram.branches ) { | ||
for ( let branchProgram of stateProgram.branches ) { this.sanitizeBranchProgram( branchProgram ) ; } | ||
} | ||
if ( stateProgram.bufferBranches ) { | ||
for ( let branchProgram of stateProgram.bufferBranches ) { this.sanitizeBranchProgram( branchProgram ) ; } | ||
} | ||
} | ||
} | ||
} ; | ||
@@ -66,154 +84,270 @@ | ||
TextMachine.prototype.pushEvent = function pushEvent( event , context ) { | ||
var currentState , bufferProgram , stateProgram , eventProgram , | ||
delayedAction , stateHasSwitched = false , oldState , primaryContext , startingContext , actionBuffer , errorAction ; | ||
TextMachine.prototype.sanitizeBranchProgram = function( branchProgram ) { | ||
if ( branchProgram.match === true ) { | ||
branchProgram.matchType = MATCH_ALWAYS ; | ||
} | ||
else if ( typeof branchProgram.match === 'string' ) { | ||
branchProgram.matchType = MATCH_STRING ; | ||
} | ||
else if ( Array.isArray( branchProgram.match ) ) { | ||
branchProgram.match = new Set( branchProgram.match ) ; | ||
branchProgram.matchType = MATCH_SET ; | ||
} | ||
else if ( branchProgram.match instanceof Set ) { | ||
branchProgram.matchType = MATCH_SET ; | ||
} | ||
else if ( branchProgram.match instanceof RegExp ) { | ||
branchProgram.matchType = MATCH_REGEXP ; | ||
} | ||
else if ( typeof branchProgram.match === 'function' ) { | ||
branchProgram.matchType = MATCH_FUNCTION ; | ||
} | ||
} ; | ||
// Get the current state | ||
currentState = this.stateStack[ this.stateStack.length - 1 ] ; | ||
// Active state program | ||
stateProgram = this.program.states[ currentState.name ] ; | ||
delayedAction = false ; | ||
TextMachine.prototype.reset = function() { | ||
this.index = 0 ; | ||
for ( ;; ) { | ||
eventProgram = this.eventProgramMatch( stateProgram.events , event ) ; | ||
this.stateStack = [ { | ||
name: this.program.config.initState || 'init' , | ||
parent: null , | ||
microState: {} , | ||
span: {} | ||
} ] ; | ||
} ; | ||
if ( ! eventProgram ) { break ; } | ||
primaryContext = currentState.context ; | ||
startingContext = currentState.startingContext ; | ||
errorAction = undefined ; | ||
actionBuffer = undefined ; | ||
// Check for state switching | ||
if ( | ||
( eventProgram.state && eventProgram.state !== currentState.name ) || | ||
( eventProgram.return && this.stateStack.length > 1 ) | ||
) { | ||
stateHasSwitched = true ; | ||
TextMachine.prototype.pushEvent = function( event , context ) { | ||
var initialState , state , | ||
initialStateProgram , stateProgram , branchProgram , bufferBranchProgram , | ||
//delayedAction , buffer , | ||
isDelayed = false , | ||
stateHasSwitched = false ; | ||
bufferProgram = this.eventProgramMatch( stateProgram.buffer , currentState.buffer ) ; | ||
//console.error( "\n>>> PUSH: '" + event + "'" ) ; | ||
// Get the current state | ||
initialState = state = this.stateStack[ this.stateStack.length - 1 ] ; | ||
if ( bufferProgram ) { | ||
actionBuffer = currentState.buffer ; | ||
eventProgram = Object.assign( {} , eventProgram , bufferProgram ) ; | ||
} | ||
// Active state program | ||
initialStateProgram = stateProgram = this.program.states[ state.name ] ; | ||
if ( eventProgram.delay ) { | ||
delayedAction = stateProgram.action ; | ||
} | ||
do { | ||
// First we select the branch and apply its feats | ||
oldState = currentState ; | ||
branchProgram = this.branchMatch( state , stateProgram.branches , event ) ; | ||
// Update to the new state | ||
if ( eventProgram.return && this.stateStack.length > 1 ) { | ||
this.stateStack.pop() ; | ||
currentState = this.stateStack[ this.stateStack.length - 1 ] ; | ||
stateProgram = this.program.states[ currentState.name ] ; | ||
startingContext = currentState.enteringContext ; | ||
if ( branchProgram ) { | ||
// Buffer branches are only matched on state-changes | ||
if ( branchProgram.state && branchProgram.state !== state.name ) { | ||
bufferBranchProgram = this.branchMatch( state , stateProgram.bufferBranches , state.buffer ) ; | ||
if ( typeof eventProgram.return === 'string' && eventProgram.return !== currentState.enteringState ) { | ||
// We are returning from an unexpected subProgram | ||
if ( eventProgram.errorAction ) { errorAction = eventProgram.errorAction ; } | ||
// A buffer-branch has matched, it replace the current branch-program | ||
if ( bufferBranchProgram ) { | ||
branchProgram = bufferBranchProgram ; | ||
} | ||
} | ||
else { | ||
if ( eventProgram.return ) { | ||
// We are in a state where we cannot return | ||
if ( eventProgram.errorAction ) { errorAction = eventProgram.errorAction ; } | ||
if ( branchProgram.microState ) { | ||
for ( let name in branchProgram.microState ) { | ||
if ( branchProgram.microState[ name ] ) { state.microState[ name ] = true ; } | ||
else { delete state.microState[ name ] ; } | ||
} | ||
} | ||
stateProgram = this.program.states[ eventProgram.state ] ; | ||
if ( branchProgram.delay ) { isDelayed = true ; } | ||
if ( stateProgram.subProgram ) { | ||
// Save the entering context | ||
currentState.enteringContext = context ; | ||
currentState.enteringState = eventProgram.state ; | ||
// Exec the branch action on the current state now, if any... | ||
if ( branchProgram.return && branchProgram.returnErrorAction && ( | ||
this.stateStack.length <= 1 | ||
|| ( typeof branchProgram.return === 'string' && branchProgram.return !== state.openingState ) | ||
) ) { | ||
// This is a return error (mostly parenthesis/brace/bracket parse errors) | ||
//console.error( ">>> APPLY branch returnErrorAction" ) ; | ||
this.execActions( branchProgram.returnErrorAction , state , context ) ; | ||
} | ||
else if ( branchProgram.action ) { | ||
this.execActions( branchProgram.action , state , context ) ; | ||
} | ||
} | ||
// Create and push a new state | ||
currentState = { | ||
name: eventProgram.state , | ||
context: context | ||
} ; | ||
this.stateStack.push( currentState ) ; | ||
} | ||
else { | ||
// Overwrite the old state with the new one | ||
currentState = this.stateStack[ this.stateStack.length - 1 ] = { | ||
name: eventProgram.state , | ||
previousName: currentState.name , | ||
context: context , | ||
previousContext: currentState.context | ||
} ; | ||
} | ||
// Now it depends on which branching mode occured | ||
currentState.startingContext = eventProgram.continue ? oldState.startingContext : context ; | ||
if ( this.stateStack.length > 1 && ( stateProgram.returnAfter || branchProgram?.return ) ) { | ||
// Returning from sub-state (recursion) | ||
//console.error( "RETURN" , state.name , '-->' , state.openingState , '(wanted: ' + ( stateProgram.returnAfter || branchProgram?.return ) + ')' , '-->-->' , state.parent?.returnState ) ; | ||
if ( stateProgram.buffer ) { | ||
currentState.buffer = eventProgram.preserveBuffer ? oldState.buffer + event : event ; | ||
} | ||
stateHasSwitched = true ; | ||
this.stateStack.length -- ; | ||
state = this.stateStack[ this.stateStack.length - 1 ] ; | ||
stateProgram = this.program.states[ state.name ] ; | ||
if ( state.returnState ) { | ||
// Overwrite the old state with the new one | ||
stateProgram = this.program.states[ state.returnState ] ; | ||
state = this.stateStack[ this.stateStack.length - 1 ] = { | ||
name: state.returnState , | ||
parent: state.parent , | ||
context: context , | ||
previousName: state.name , | ||
previousContext: state.context , | ||
microState: state.microState , | ||
span: state.span , | ||
openingContext: state.openingContext , | ||
openingState: state.openingState , | ||
startingStateContext: context , | ||
buffer: | ||
! stateProgram.bufferBranches ? null : | ||
branchProgram.preserveBuffer ? state.buffer + event : | ||
event | ||
} ; | ||
} | ||
} | ||
else { | ||
currentState.previousName = currentState.name ; | ||
currentState.previousContext = currentState.context ; | ||
currentState.context = context ; | ||
else if ( ! branchProgram || ( | ||
( ! branchProgram.state || branchProgram.state === state.name ) | ||
&& ! branchProgram.subState | ||
&& ( ! branchProgram.return || this.stateStack.length <= 1 ) | ||
) ) { | ||
// Continue / No state change | ||
//console.error( "CONTINUE" , state.name ) ; | ||
if ( stateProgram.buffer ) { | ||
currentState.buffer += event ; | ||
state.previousName = state.name ; | ||
state.previousContext = state.context ; | ||
state.context = context ; | ||
if ( stateProgram.bufferBranches ) { | ||
state.buffer += event ; | ||
} | ||
} | ||
else if ( branchProgram.subState ) { | ||
// Entering sub-state (recursion) | ||
//console.error( "ENTERING SUB STATE" , state.name , '-->' , branchProgram.subState ) ; | ||
// Exec the switching state action now, if any... | ||
if ( errorAction ) { | ||
this.execAction( errorAction , primaryContext , startingContext , actionBuffer ) ; | ||
stateHasSwitched = true ; | ||
stateProgram = this.program.states[ branchProgram.subState ] ; | ||
// Save the opening context | ||
state.returnState = branchProgram.state ; | ||
// Create a new state and push it at the end of the stack | ||
state = this.stateStack[ this.stateStack.length ] = { | ||
name: branchProgram.subState , | ||
parent: state , | ||
context: context , | ||
microState: {} , | ||
span: {} , | ||
openingContext: context , | ||
openingState: branchProgram.subState , | ||
startingStateContext: context | ||
} ; | ||
} | ||
else if ( eventProgram.action ) { | ||
this.execAction( eventProgram.action , primaryContext , startingContext , actionBuffer ) ; | ||
else { | ||
// Switch to state | ||
//console.error( "SWITCH" , state.name , '-->' , branchProgram.state ) ; | ||
stateHasSwitched = true ; | ||
// Now change the state | ||
stateProgram = this.program.states[ branchProgram.state ] ; | ||
state = this.stateStack[ this.stateStack.length - 1 ] = { | ||
name: branchProgram.state , | ||
parent: state.parent , | ||
context: context , | ||
previousName: state.name , | ||
previousContext: state.context , | ||
microState: state.microState , | ||
span: state.span , | ||
openingContext: state.openingContext , | ||
openingState: state.openingState , | ||
startingStateContext: context , | ||
buffer: | ||
! stateProgram.bufferBranches ? null : | ||
branchProgram.preserveBuffer ? state.buffer + event : | ||
event | ||
} ; | ||
} | ||
// Propagate now? | ||
if ( ! stateHasSwitched || ! eventProgram.propagate ) { break ; } | ||
} | ||
// Finally apply state feats | ||
// Exec the finishing state action, if any... | ||
if ( delayedAction !== false ) { this.execAction( delayedAction , context ) ; } | ||
else if ( stateProgram.action ) { this.execAction( stateProgram.action , context ) ; } | ||
} ; | ||
if ( stateProgram.span ) { | ||
let span = state.span[ stateProgram.span ] ; | ||
if ( span && span.end === this.index - 1 ) { | ||
span.end = this.index ; | ||
span.endingContext = context ; | ||
} | ||
else { | ||
span = state.span[ stateProgram.span ] = { | ||
startingContext: context , | ||
endingContext: context , | ||
start: this.index , | ||
end: this.index | ||
} ; | ||
} | ||
} | ||
if ( stateProgram.microState ) { | ||
for ( let name in stateProgram.microState ) { | ||
if ( stateProgram.microState[ name ] ) { state.microState[ name ] = true ; } | ||
else { delete state.microState[ name ] ; } | ||
} | ||
} | ||
// Get the first matching eventProgram | ||
TextMachine.prototype.eventProgramMatch = function eventProgramMatch( eventPrograms , event ) { | ||
if ( ! Array.isArray( eventPrograms ) ) { return ; } | ||
// Exec the action for the state, if any... | ||
if ( isDelayed ) { | ||
if ( initialStateProgram.action ) { | ||
this.execActions( initialStateProgram.action , initialState , context ) ; | ||
} | ||
} | ||
else if ( stateProgram.returnAfter && stateProgram.returnErrorAction && ( | ||
this.stateStack.length <= 1 | ||
|| ( typeof stateProgram.returnAfter === 'string' && stateProgram.returnAfter !== state.openingState ) | ||
) ) { | ||
// This is a return error (mostly parenthesis/brace/bracket parse errors) | ||
//console.error( ">>> APPLY state returnErrorAction" , this.stateStack.length , stateProgram.returnAfter , state.openingState ) ; | ||
this.execActions( stateProgram.returnErrorAction , state , context ) ; | ||
} | ||
else if ( stateProgram.action ) { | ||
this.execActions( stateProgram.action , state , context ) ; | ||
} | ||
var eventProgram , match , isPositive , | ||
i , iMax = eventPrograms.length ; | ||
// Propagate the event to the next state now? | ||
} while ( stateHasSwitched && branchProgram?.propagate ) ; | ||
for ( i = 0 ; i < iMax ; i ++ ) { | ||
eventProgram = eventPrograms[ i ] ; | ||
isPositive = eventProgram.match !== undefined || eventProgram.dontMatch === undefined ; | ||
match = eventProgram.match || eventProgram.dontMatch ; | ||
this.index ++ ; | ||
} ; | ||
if ( | ||
// Always match | ||
match === isPositive || | ||
// Equality | ||
( typeof match === 'string' && ( match === event ) === isPositive ) || | ||
// Included in an array of string | ||
( Array.isArray( match ) && match.includes( event ) === isPositive ) || | ||
// Get the first matching branchProgram | ||
TextMachine.prototype.branchMatch = function( state , branches , event ) { | ||
if ( ! Array.isArray( branches ) ) { return ; } | ||
// Match a RegExp | ||
( match instanceof RegExp && match.test( event ) === isPositive ) || | ||
for ( let branchProgram of branches ) { | ||
let isMatching = | ||
branchProgram.matchType === MATCH_ALWAYS ? true : | ||
branchProgram.matchType === MATCH_STRING ? branchProgram.match === event : | ||
branchProgram.matchType === MATCH_SET ? branchProgram.match.has( event ) : | ||
branchProgram.matchType === MATCH_REGEXP ? branchProgram.match.test( event ) : | ||
branchProgram.matchType === MATCH_FUNCTION ? !! branchProgram.match( event ) : | ||
false ; | ||
// Third party function | ||
( typeof match === 'function' && !! match( event ) === isPositive ) | ||
) { | ||
return eventProgram ; | ||
if ( isMatching === ! branchProgram.inverse ) { | ||
if ( ! branchProgram.hasMicroState ) { return branchProgram ; } | ||
if ( | ||
Array.isArray( branchProgram.hasMicroState ) | ||
&& branchProgram.hasMicroState.every( name => state.microState[ name ] ) | ||
) { | ||
return branchProgram ; | ||
} | ||
if ( state.microState[ branchProgram.hasMicroState ] ) { | ||
return branchProgram ; | ||
} | ||
} | ||
@@ -225,21 +359,48 @@ } | ||
TextMachine.prototype.execAction = function execAction( action , context , startingContext , buffer ) { | ||
if ( Array.isArray( action[ 0 ] ) ) { | ||
// This is an array of array, it contains many actions to execute | ||
action.forEach( action_ => this.execAction( action_ , context , startingContext , buffer ) ) ; | ||
return ; | ||
TextMachine.prototype.execActions = function( actions , state , context ) { | ||
if ( ! actions || ! actions.length ) { return ; } | ||
if ( ! Array.isArray( actions[ 0 ] ) ) { | ||
return this.execAction( actions , state , context ) ; | ||
} | ||
if ( this.api[ action[ 0 ] ] ) { | ||
context.startingContext = startingContext ; | ||
context.buffer = buffer ; | ||
for ( let action of actions ) { | ||
this.execAction( action , state , context ) ; | ||
} | ||
} ; | ||
if ( ! Array.isArray( action[ 1 ] ) ) { | ||
// Pack action's argument once | ||
action[ 1 ] = action.slice( 1 ) ; | ||
TextMachine.prototype.execAction = function( action , state , context ) { | ||
var actionName = action[ 0 ] ; | ||
//console.error( "ACTION:" , actionName ) ; | ||
switch ( actionName ) { | ||
case 'style' : | ||
this.api.style( context , action[ 1 ] ) ; | ||
break ; | ||
case 'starterStyle' : | ||
//console.error( " -> " , state.startingStateContext.x , state.startingStateContext.y ) ; | ||
this.api.style( state.startingStateContext , action[ 1 ] ) ; | ||
break ; | ||
case 'openerStyle' : | ||
//console.error( " -> " , state.openingContext.x , state.openingContext.y ) ; | ||
this.api.style( state.openingContext , action[ 1 ] ) ; | ||
break ; | ||
case 'streakStyle' : | ||
this.api.blockStyle( state.startingStateContext , context , action[ 1 ] ) ; | ||
break ; | ||
case 'spanStyle' : { | ||
let span = state.span[ action[ 1 ] ] ; | ||
//console.error( " -> " , span ) ; | ||
if ( ! span ) { break ; } | ||
this.api.blockStyle( span.startingContext , span.endingContext , action[ 2 ] ) ; | ||
break ; | ||
} | ||
this.api[ action[ 0 ] ]( context , ... action[ 1 ] ) ; | ||
case 'hint' : | ||
// /!\ Should be refactored... | ||
this.api.hint( context , state.buffer , action[ 1 ] ) ; | ||
break ; | ||
} | ||
} ; | ||
{ | ||
"name": "text-machine", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "A state machine for text processing.", | ||
"main": "lib/TextMachine.js", | ||
"engines": { | ||
"node": ">=6.0.0" | ||
"node": ">=14.15.0" | ||
}, | ||
@@ -34,6 +34,7 @@ "directories": { | ||
"years": [ | ||
2018 | ||
2018, | ||
2022 | ||
], | ||
"owner": "Cédric Ronvel" | ||
} | ||
} | ||
} |
/* | ||
Text Machine | ||
Copyright (c) 2015 Cédric Ronvel | ||
Copyright (c) 2018 - 2022 Cédric Ronvel | ||
@@ -35,39 +35,73 @@ The MIT License (MIT) | ||
var idleStyle = { color: 'white' } ; | ||
var keywordStyle = { color: 'brightWhite' , bold: true } ; | ||
var thisStyle = { color: 'brightRed' , bold: true } ; | ||
var constantKeywordStyle = { color: 'brightBlue' , bold: true } ; | ||
var constantStyle = { color: 'brightBlue' } ; | ||
var identifierStyle = { color: 'red' } ; | ||
var numberStyle = { color: 'cyan' } ; | ||
var stringStyle = { color: 'blue' } ; | ||
var escapeStyle = { color: 'brightCyan' , bold: true } ; | ||
var commentStyle = { color: 'brightBlack' } ; | ||
var propertyStyle = { color: 'green' } ; | ||
var methodStyle = { color: 'brightYellow' } ; | ||
var coreMethodStyle = { color: 'brightYellow' , bold: true } ; | ||
var classStyle = { color: 'magenta' } ; | ||
var coreClassOrObjectStyle = { color: 'brightMagenta' , bold: true } ; | ||
const idleStyle = { color: 'white' } ; | ||
const keywordStyle = { color: 'brightWhite' , bold: true } ; | ||
const operatorStyle = { color: 'brightWhite' , bold: true } ; | ||
const assignmentStyle = { color: 'brightWhite' , bold: true } ; | ||
const thisStyle = { color: 'brightRed' , bold: true } ; | ||
const constantKeywordStyle = { color: 'brightBlue' , bold: true } ; | ||
const constantStyle = { color: 'brightBlue' } ; | ||
const identifierStyle = { color: 'red' } ; | ||
const numberStyle = { color: 'cyan' } ; | ||
const stringStyle = { color: 'blue' } ; | ||
const escapeStyle = { color: 'brightCyan' , bold: true } ; | ||
const templatePlaceholderStyle = { color: 'brightCyan' , bold: true } ; | ||
const commentStyle = { color: 'brightBlack' } ; | ||
const propertyStyle = { color: 'green' } ; | ||
const methodStyle = { color: 'brightYellow' } ; | ||
const coreMethodStyle = { color: 'brightYellow' , bold: true } ; | ||
const classStyle = { color: 'magenta' } ; | ||
const coreClassOrObjectStyle = { color: 'brightMagenta' , bold: true } ; | ||
var parseErrorStyle = { color: 'brightWhite' , bgColor: 'red' , bold: true } ; | ||
var braceStyle = { color: 'brightWhite' , bold: true } ; | ||
const regexpStyle = { color: 'blue' } ; | ||
const regexpDelemiterStyle = { color: 'brightMagenta' , bold: true } ; | ||
const regexpParenthesisStyle = { color: 'yellow' , bold: true } ; | ||
const regexpBracketStyle = { color: 'brightMagenta' , bold: true } ; | ||
const regexpAlternativeStyle = { color: 'yellow' , bold: true } ; | ||
const regexpMarkupStyle = { color: 'brightMagenta' } ; | ||
const regexpClassStyle = { color: 'magenta' } ; | ||
const regexpFlagStyle = { color: 'brightCyan' } ; | ||
const parseErrorStyle = { color: 'brightWhite' , bgColor: 'red' , bold: true } ; | ||
const braceStyle = { color: 'brightWhite' , bold: true } ; | ||
var keywords = [ | ||
'do' , 'if' , 'in' , 'for' , 'let' , 'new' , 'try' , 'var' , 'case' , 'else' , 'enum' , | ||
'eval' , 'void' , 'with' , 'await' , 'break' , 'catch' , 'class' , 'const' , | ||
'super' , 'throw' , 'while' , 'yield' , 'delete' , 'export' , 'import' , 'public' , 'return' , | ||
'static' , 'switch' , 'typeof' , 'default' , 'extends' , 'finally' , 'package' , 'private' , | ||
'continue' , 'debugger' , 'function' , 'arguments' , 'interface' , 'protected' , 'implements' , 'instanceof' , | ||
// Node pseudo keywords | ||
'exports' , 'global' , 'module' , 'require' , '__filename' , '__dirname' | ||
const keywords = [ | ||
'var' , 'let' , 'const' , | ||
'do' , 'while' , 'for' , 'in' , 'of' , 'switch' , 'case' , 'default' , 'break' , 'continue' , | ||
'if' , 'else' , | ||
'function' , 'arguments' , 'async' , 'await' , 'return' , 'yield' , | ||
'throw' , 'try' , 'catch' , 'finally' , | ||
'new' , 'class' , 'extends' , 'static' , 'public' , 'protected' , 'private' , 'implements' , 'interface' , 'super' , | ||
'typeof' , 'instanceof' , | ||
'delete' , | ||
'enum' , 'eval' , 'void' , 'with' , | ||
'export' , 'import' , 'package' , | ||
'debugger' , | ||
] ; | ||
var constantKeywords = [ | ||
'true' , 'false' , 'null' , 'undefined' , 'Infinity' , 'NaN' | ||
const operators = [ | ||
'+' , '++' , '-' , '--' , '*' , '**' , '%' , // '/' | ||
'&&' , '||' , '!' , '!!' , | ||
'==' , '===' , '!=' , '!==' , '<' , '<=' , '>' , '>=' , | ||
'&' , '|' , '^' , '<<' , '>>' , '>>>' , | ||
'??' | ||
] ; | ||
var coreMethods = [ | ||
const assignments = [ | ||
'=' , | ||
'+=' , '-=' , '*=' , '%=' , // '/=' | ||
'&=' , '|=' , '^=' | ||
] ; | ||
const constantKeywords = [ | ||
'true' , 'false' , 'null' , 'undefined' , 'Infinity' , 'NaN' , | ||
// Node pseudo-constant | ||
'__filename' , '__dirname' | ||
] ; | ||
const coreMethods = [ | ||
'setTimeout' , 'clearTimeout' , 'setInterval' , 'clearInterval' , 'setImmediate' , 'clearImmediate' , | ||
@@ -77,6 +111,6 @@ 'isNaN' , 'isFinite' , 'parseInt' , 'parseFloat' , | ||
// Node | ||
'unref' , 'ref' | ||
'unref' , 'ref' , 'require' | ||
] ; | ||
var coreClassesOrObjects = [ | ||
const coreClassesOrObjects = [ | ||
'Array' , 'Boolean' , 'Date' , 'Error' , 'Function' , 'Intl' , 'Math' , 'Number' , 'Object' , 'String' , 'RegExp' , | ||
@@ -91,2 +125,3 @@ 'EvalError' , 'RangeError' , 'ReferenceError' , 'SyntaxError' , 'TypeError' , | ||
// Node | ||
'exports' , 'global' , 'module' , | ||
'process' , 'Buffer' , | ||
@@ -98,3 +133,3 @@ | ||
var memberKeywords = [ | ||
const specialMember = [ | ||
'prototype' , 'constructor' | ||
@@ -105,3 +140,3 @@ ] ; | ||
var coreMethodHints = { | ||
const coreMethodHints = { | ||
setTimeout: 'timerID = setTimeout( callback , ms )' , | ||
@@ -118,3 +153,3 @@ clearTimeout: 'clearTimeout( timerID )' , | ||
var prog = { | ||
const prog = { | ||
hostConfig: { // Accessible by the host | ||
@@ -125,17 +160,15 @@ } , | ||
} , | ||
states: { // Every states of the machine | ||
states: { | ||
idle: { | ||
action: [ 'style' , idleStyle ] , // action when this state is active at the end of the event | ||
buffer: false , // if an array, this state start buffering as long as it last | ||
//checkpoint: true , // true if the past will not have influence on the future anymore: help optimizing the host | ||
events: [ | ||
action: [ 'style' , idleStyle ] , | ||
branches: [ | ||
{ | ||
match: /^[a-zA-Z_$]/ , // the event should match this to trigger those actions | ||
state: 'identifier' , // next state | ||
propagate: false , // eat the event or propagate it immediately to the next state? | ||
action: null , // action at trigger time, run before the action of the next state | ||
delay: false // for this time, the old state action will be executed rather than the new one | ||
match: /^[a-zA-Z_$]/ , | ||
state: 'identifier' | ||
} , | ||
{ | ||
match: /^[=.<>^?!&|~*%+-]/ , | ||
state: 'operator' | ||
} , | ||
{ | ||
match: /^[0-9]/ , | ||
@@ -145,2 +178,6 @@ state: 'number' | ||
{ | ||
match: "'" , | ||
state: 'singleQuoteString' | ||
} , | ||
{ | ||
match: '"' , | ||
@@ -150,4 +187,4 @@ state: 'doubleQuoteString' | ||
{ | ||
match: "'" , | ||
state: 'singleQuoteString' | ||
match: '`' , | ||
state: 'templateString' | ||
} , | ||
@@ -160,3 +197,3 @@ { | ||
match: '{' , | ||
state: 'openBrace' | ||
subState: 'openBrace' | ||
} , | ||
@@ -169,3 +206,3 @@ { | ||
match: '[' , | ||
state: 'openBracket' | ||
subState: 'openBracket' | ||
} , | ||
@@ -178,3 +215,3 @@ { | ||
match: '(' , | ||
state: 'openParenthesis' | ||
subState: 'openParenthesis' | ||
} , | ||
@@ -184,8 +221,47 @@ { | ||
state: 'closeParenthesis' | ||
} , | ||
{ | ||
match: ':' , | ||
state: 'colon' , | ||
} | ||
] | ||
} , | ||
// In the middle of an expression, after any constant/value/identifier/function call/etc... | ||
// Mostly like idle, except that slash can be divide sign instead of RegExp | ||
idleAfterValue: { | ||
action: [ 'style' , idleStyle ] , // action when this state is active at the end of the event | ||
branches: [ | ||
{ | ||
match: '/' , | ||
state: 'idleAfterValueSlash' | ||
} , | ||
{ | ||
match: ' ' , | ||
state: 'idleAfterValue' | ||
} , | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
number: { | ||
action: [ 'style' , numberStyle ] , | ||
branches: [ | ||
{ | ||
match: /^[0-9.]/ , | ||
state: 'number' | ||
} , | ||
{ | ||
match: true , | ||
state: 'idleAfterValue' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
identifier: { | ||
action: [ 'style' , identifierStyle ] , | ||
events: [ | ||
span: 'identifier' , | ||
branches: [ | ||
{ | ||
@@ -199,59 +275,133 @@ match: /^[a-zA-Z0-9_$]/ , | ||
propagate: true , | ||
continue: true // Keep the starting context | ||
} | ||
] , | ||
// Buffers are checked on state switching | ||
buffer: [ | ||
bufferBranches: [ | ||
{ | ||
match: 'this' , | ||
// replace the 'action' of the event, also work with any properties of the event except 'match' BTW | ||
action: [ 'blockStyle' , thisStyle ] , | ||
state: 'afterIdentifier' | ||
//propagate: true , | ||
//continue: true | ||
action: [ 'spanStyle' , 'identifier' , thisStyle ] , | ||
state: 'afterIdentifier' , | ||
propagate: true | ||
} , | ||
{ | ||
match: keywords , | ||
action: [ 'blockStyle' , keywordStyle ] , | ||
state: 'idle' | ||
action: [ 'spanStyle' , 'identifier' , keywordStyle ] , | ||
state: 'idle' , | ||
propagate: true | ||
} , | ||
{ | ||
match: constantKeywords , | ||
action: [ 'blockStyle' , constantKeywordStyle ] , | ||
state: 'idle' | ||
action: [ 'spanStyle' , 'identifier' , constantKeywordStyle ] , | ||
state: 'idleAfterValue' , | ||
propagate: true | ||
} , | ||
{ | ||
match: coreMethods , | ||
action: [ [ 'blockStyle' , coreMethodStyle ] , [ 'hint' , coreMethodHints ] ] , | ||
state: 'idle' | ||
action: [ [ 'spanStyle' , 'identifier' , coreMethodStyle ] , [ 'hint' , coreMethodHints ] ] , | ||
state: 'idle' , | ||
propagate: true | ||
} , | ||
{ | ||
match: coreClassesOrObjects , | ||
action: [ 'blockStyle' , coreClassOrObjectStyle ] , | ||
action: [ 'spanStyle' , 'identifier' , coreClassOrObjectStyle ] , | ||
state: 'afterIdentifier' , | ||
propagate: true , | ||
continue: true | ||
propagate: true | ||
} , | ||
{ | ||
match: /^[A-Z][A-Z0-9_]+$/ , | ||
action: [ 'blockStyle' , constantStyle ] , | ||
action: [ 'spanStyle' , 'identifier' , constantStyle ] , | ||
state: 'afterIdentifier' , | ||
propagate: true , | ||
continue: true | ||
propagate: true | ||
} , | ||
{ | ||
match: /^[A-Z]/ , | ||
action: [ 'blockStyle' , classStyle ] , | ||
action: [ 'spanStyle' , 'identifier' , classStyle ] , | ||
state: 'afterIdentifier' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
afterIdentifier: { | ||
action: [ 'style' , idleStyle ] , | ||
branches: [ | ||
{ | ||
match: ' ' , | ||
state: 'afterIdentifier' | ||
} , | ||
{ | ||
match: '.' , | ||
state: 'dotAfterIdentifier' | ||
} , | ||
{ | ||
match: ':' , | ||
hasMicroState: 'ternary' , | ||
state: 'colon' | ||
} , | ||
{ | ||
match: ':' , | ||
action: [ 'spanStyle' , 'identifier' , propertyStyle ] , | ||
state: 'colon' | ||
} , | ||
{ | ||
match: '(' , | ||
subState: 'openParenthesis' , | ||
action: [ 'spanStyle' , 'identifier' , methodStyle ] | ||
} , | ||
{ | ||
match: true , | ||
state: 'idleAfterValue' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
dotAfterIdentifier: { | ||
action: [ 'style' , idleStyle ] , | ||
branches: [ | ||
{ | ||
match: ' ' , | ||
state: 'dotAfterIdentifier' | ||
} , | ||
{ | ||
match: /^[a-zA-Z_$]/ , | ||
state: 'member' | ||
} , | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
member: { | ||
action: [ 'style' , propertyStyle ] , | ||
span: 'identifier' , | ||
branches: [ | ||
{ | ||
match: /^[a-zA-Z0-9_$]/ , | ||
state: 'member' | ||
} , | ||
{ | ||
match: true , | ||
state: 'afterIdentifier' , | ||
propagate: true , | ||
continue: true | ||
} | ||
] , | ||
bufferBranches: [ | ||
{ | ||
match: specialMember , | ||
action: [ 'spanStyle' , 'identifier' , keywordStyle ] , | ||
state: 'afterIdentifier' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
number: { | ||
action: [ 'style' , numberStyle ] , | ||
events: [ | ||
operator: { | ||
action: [ 'style' , idleStyle ] , | ||
span: 'operator' , | ||
branches: [ | ||
{ | ||
match: /^[0-9.]/ , | ||
state: 'number' | ||
match: /^[=.<>^?!&|~*%+-]/ , | ||
state: 'operator' | ||
} , | ||
@@ -261,19 +411,54 @@ { | ||
state: 'idle' , | ||
propagate: true , | ||
} | ||
] , | ||
bufferBranches: [ | ||
{ | ||
match: '?' , | ||
action: [ 'spanStyle' , 'operator' , operatorStyle ] , | ||
state: 'idle' , | ||
microState: { ternary: true } , | ||
propagate: true | ||
} , | ||
{ | ||
match: operators , | ||
action: [ 'spanStyle' , 'operator' , operatorStyle ] , | ||
state: 'idle' , | ||
propagate: true | ||
} , | ||
{ | ||
match: assignments , | ||
action: [ 'spanStyle' , 'operator' , assignmentStyle ] , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
colon: { | ||
action: [ 'style' , operatorStyle ] , | ||
microState: { ternary: false } , | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
singleQuoteString: { | ||
action: [ 'style' , stringStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
match: /^\\/ , | ||
state: 'escape' | ||
match: '\\' , | ||
subState: 'escape' | ||
} , | ||
{ | ||
match: /^['\n]/ , | ||
state: 'idle' , | ||
state: 'idleAfterValue' , | ||
delay: true | ||
@@ -285,10 +470,10 @@ } | ||
action: [ 'style' , stringStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
match: /^\\/ , | ||
state: 'escape' | ||
match: '\\' , | ||
subState: 'escape' | ||
} , | ||
{ | ||
match: /^["\n]/ , | ||
state: 'idle' , | ||
state: 'idleAfterValue' , | ||
delay: true | ||
@@ -298,12 +483,76 @@ } | ||
} , | ||
openBrace: { | ||
subProgram: true , // stack a new state | ||
action: [ 'style' , parseErrorStyle ] , | ||
events: [ | ||
templateString: { | ||
action: [ 'style' , stringStyle ] , | ||
branches: [ | ||
{ | ||
match: '\\' , | ||
subState: 'escape' | ||
} , | ||
{ | ||
match: '`' , | ||
state: 'idleAfterValue' , | ||
delay: true | ||
} , | ||
{ | ||
match: '$' , | ||
state: 'templatePlaceholder' | ||
} | ||
] | ||
} , | ||
templatePlaceholder: { | ||
startSpan: true , | ||
action: [ 'style' , templatePlaceholderStyle ] , | ||
branches: [ | ||
{ | ||
match: '{' , | ||
subState: 'openBrace' , | ||
state: 'templateString' | ||
} , | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
action: [ 'spanStyle' , stringStyle ] , | ||
//clearSpan: true , | ||
state: 'templateString' | ||
} | ||
] | ||
} , | ||
regexp: { | ||
action: [ 'style' , regexpStyle ] , | ||
branches: [ | ||
{ | ||
match: '\\' , | ||
subState: 'escape' | ||
} , | ||
{ | ||
match: /[.?+*^$]/ , | ||
state: 'regexpMarkup' | ||
} , | ||
{ | ||
match: '[' , | ||
state: 'regexpOpenBracket' | ||
} , | ||
{ | ||
match: '(' , | ||
subState: 'regexpOpenParenthesis' | ||
} , | ||
{ | ||
match: ')' , | ||
state: 'regexpCloseParenthesis' | ||
} , | ||
{ | ||
match: '|' , | ||
state: 'regexpAlternative' | ||
} , | ||
{ | ||
match: '/' , | ||
state: 'closeRegexp' | ||
} | ||
] | ||
} , | ||
closeRegexp: { | ||
action: [ 'style' , regexpDelemiterStyle ] , | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'regexpFlag' , | ||
propagate: true | ||
@@ -313,11 +562,12 @@ } | ||
} , | ||
closeBrace: { | ||
action: [ 'style' , braceStyle ] , | ||
events: [ | ||
regexpFlag: { | ||
action: [ 'style' , regexpFlagStyle ] , | ||
branches: [ | ||
{ | ||
match: /[a-z]/ , | ||
state: 'regexpFlag' | ||
} , | ||
{ | ||
match: true , | ||
return: 'openBrace' , // return (unstack), expecting returning from the 'openBrace' subProgram | ||
action: [ 'openingStyle' , braceStyle ] , | ||
errorAction: [ 'style' , parseErrorStyle ] , // if not returning form 'openBrace', we've got a parseError | ||
state: 'idle' , | ||
state: 'idleAfterValue' , | ||
propagate: true | ||
@@ -327,9 +577,8 @@ } | ||
} , | ||
openBracket: { | ||
subProgram: true , | ||
action: [ 'style' , parseErrorStyle ] , | ||
events: [ | ||
regexpMarkup: { | ||
action: [ 'style' , regexpMarkupStyle ] , | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
state: 'regexp' , | ||
propagate: true | ||
@@ -339,11 +588,8 @@ } | ||
} , | ||
closeBracket: { | ||
action: [ 'style' , braceStyle ] , | ||
events: [ | ||
regexpAlternative: { | ||
action: [ 'style' , regexpAlternativeStyle ] , | ||
branches: [ | ||
{ | ||
match: true , | ||
return: 'openBracket' , | ||
action: [ 'openingStyle' , braceStyle ] , | ||
errorAction: [ 'style' , parseErrorStyle ] , | ||
state: 'idle' , | ||
state: 'regexp' , | ||
propagate: true | ||
@@ -353,9 +599,8 @@ } | ||
} , | ||
openParenthesis: { | ||
subProgram: true , | ||
regexpOpenParenthesis: { | ||
action: [ 'style' , parseErrorStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
state: 'regexp' , | ||
propagate: true | ||
@@ -365,11 +610,10 @@ } | ||
} , | ||
closeParenthesis: { | ||
action: [ 'style' , braceStyle ] , | ||
events: [ | ||
regexpCloseParenthesis: { | ||
returnAfter: 'regexpOpenParenthesis' , | ||
action: [ [ 'style' , regexpParenthesisStyle ] , [ 'openerStyle' , regexpParenthesisStyle ] ] , | ||
returnErrorAction: [ 'style' , parseErrorStyle ] , // if not returning from 'openBrace', we've got a parseError | ||
branches: [ | ||
{ | ||
match: true , | ||
return: 'openParenthesis' , | ||
action: [ 'openingStyle' , braceStyle ] , | ||
errorAction: [ 'style' , parseErrorStyle ] , | ||
state: 'idle' , | ||
state: 'regexp' , | ||
propagate: true | ||
@@ -379,23 +623,44 @@ } | ||
} , | ||
afterIdentifier: { | ||
action: [ 'style' , idleStyle ] , | ||
events: [ | ||
regexpOpenBracket: { | ||
action: [ 'style' , regexpBracketStyle ] , | ||
branches: [ | ||
{ | ||
match: ' ' , | ||
state: 'afterIdentifier' | ||
} , | ||
match: true , | ||
state: 'regexpClass' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
regexpClass: { | ||
action: [ 'style' , regexpClassStyle ] , | ||
branches: [ | ||
{ | ||
match: '.' , | ||
state: 'dotAfterIdentifier' | ||
match: '\\' , | ||
subState: 'escape' | ||
} , | ||
{ | ||
match: '(' , | ||
state: 'openParenthesis' , | ||
action: [ 'blockStyle' , methodStyle ] | ||
} , | ||
match: ']' , | ||
state: 'regexpCloseBracket' | ||
} | ||
] | ||
} , | ||
regexpCloseBracket: { | ||
action: [ 'style' , regexpBracketStyle ] , | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'regexp' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
openBrace: { | ||
action: [ 'style' , parseErrorStyle ] , | ||
//microState: { keyValuePair: true } , | ||
branches: [ | ||
{ | ||
match: true , | ||
state: 'idle' , | ||
@@ -406,13 +671,29 @@ propagate: true | ||
} , | ||
dotAfterIdentifier: { | ||
action: [ 'style' , idleStyle ] , | ||
events: [ | ||
closeBrace: { | ||
returnAfter: 'openBrace' , | ||
action: [ [ 'style' , braceStyle ] , [ 'openerStyle' , braceStyle ] ] , | ||
returnErrorAction: [ 'style' , parseErrorStyle ] , // if not returning from 'openBrace', we've got a parseError | ||
branches: [ | ||
{ | ||
match: ' ' , | ||
state: 'dotAfterIdentifier' | ||
} , | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
openBracket: { | ||
action: [ 'style' , parseErrorStyle ] , | ||
branches: [ | ||
{ | ||
match: /^[a-zA-Z_$]/ , | ||
state: 'member' | ||
} , | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] | ||
} , | ||
closeBracket: { | ||
returnAfter: 'openBracket' , | ||
action: [ [ 'style' , braceStyle ] , [ 'openerStyle' , braceStyle ] ] , | ||
returnErrorAction: [ 'style' , parseErrorStyle ] , // if not returning from 'openBrace', we've got a parseError | ||
branches: [ | ||
{ | ||
@@ -425,23 +706,20 @@ match: true , | ||
} , | ||
member: { | ||
action: [ 'style' , propertyStyle ] , | ||
events: [ | ||
openParenthesis: { | ||
action: [ 'style' , parseErrorStyle ] , | ||
branches: [ | ||
{ | ||
match: /^[a-zA-Z0-9_$]/ , | ||
state: 'member' | ||
} , | ||
{ | ||
match: true , | ||
state: 'afterIdentifier' , | ||
propagate: true , | ||
continue: true // continue the block | ||
state: 'idle' , | ||
propagate: true | ||
} | ||
] , | ||
// Checked when an event would change the state | ||
buffer: [ | ||
] | ||
} , | ||
closeParenthesis: { | ||
returnAfter: 'openParenthesis' , | ||
action: [ [ 'style' , braceStyle ] , [ 'openerStyle' , braceStyle ] ] , | ||
returnErrorAction: [ 'style' , parseErrorStyle ] , // if not returning from 'openBrace', we've got a parseError | ||
branches: [ | ||
{ | ||
match: memberKeywords , | ||
// replace the 'action' of the event, also work with any properties of the event except 'match' BTW | ||
action: [ 'blockStyle' , keywordStyle ] , | ||
state: 'afterIdentifier' , | ||
match: true , | ||
state: 'idle' , | ||
propagate: true | ||
@@ -456,7 +734,7 @@ } | ||
action: [ 'style' , idleStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
match: '/' , | ||
state: 'lineComment' , | ||
action: [ 'style' , commentStyle ] | ||
action: [ 'streakStyle' , commentStyle ] | ||
} , | ||
@@ -466,6 +744,26 @@ { | ||
state: 'multiLineComment' , | ||
action: [ 'style' , commentStyle ] | ||
action: [ 'streakStyle' , commentStyle ] | ||
} , | ||
{ | ||
match: true , | ||
state: 'regexp' , | ||
action: [ 'streakStyle' , regexpDelemiterStyle ] | ||
} | ||
] | ||
} , | ||
idleAfterValueSlash: { | ||
action: [ 'style' , idleStyle ] , | ||
branches: [ | ||
{ | ||
match: '/' , | ||
state: 'lineComment' , | ||
action: [ 'streakStyle' , commentStyle ] | ||
} , | ||
{ | ||
match: '*' , | ||
state: 'multiLineComment' , | ||
action: [ 'streakStyle' , commentStyle ] | ||
} , | ||
{ | ||
match: true , | ||
state: 'idle' | ||
@@ -477,3 +775,3 @@ } | ||
action: [ 'style' , commentStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
@@ -487,3 +785,3 @@ match: '\n' , | ||
action: [ 'style' , commentStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
@@ -497,3 +795,3 @@ match: '*' , | ||
action: [ 'style' , commentStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
@@ -518,5 +816,4 @@ match: '/' , | ||
escape: { | ||
subProgram: true , | ||
action: [ 'style' , escapeStyle ] , | ||
events: [ | ||
branches: [ | ||
{ | ||
@@ -537,2 +834,1 @@ match: true , | ||
/* | ||
Text Machine | ||
Copyright (c) 2018 Cédric Ronvel | ||
Copyright (c) 2018 - 2022 Cédric Ronvel | ||
@@ -6,0 +6,0 @@ The MIT License (MIT) |
42707
10
1223