blob: 0669b9f2ad6e70c3b9fc233cbad1991c9a810cc2 [file]
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2020 DeltaXML Ltd. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export enum CharLevelState {
init, // 0 initial state
lB, // 1 left bracket
rB, // 2 right bracket
lC, // 3 left comment
rC, // 4 right comment
lSq, // 5 left single quote
rSq, // 6 right single quote
lDq, // 7 left double quote
rDq, // 8 right double quote
lBr, // 8 left brace
rBr, // 10 right brace
lPr, // 11 left predicate
rPr, // 12 right predicate
lWs, // 13 whitspace char start
escSq, // 14 escaped single quote
escDq, // 15 escaped double quote
sep, // 16 separator
lUri, // 17 left braced URI literal
rUri, // 18 right braced URI literal
lNl, // 20 left numeric literal
rNl, // 21 right numeric literal
dSep, // 22 first char of double char separator
lVar, // 23 variable start: $var
exp, // 24 exponent in numeric literal - allow + or - or digit after it
lName, // 25 node-name, function-name or operator like 'is' etc
lAttr, // 26 attribute-name
dSep2, // 27 2nd char of double char separator
lEnt, // 28 left entity ref
rEnt, // 29 right entity ref
lSqEnt, // 30 left single quote entity
rSqEnt, // 31
lDqEnt, // 33
rDqEnt, // 34
lLiteralSqEnt,
rLiteralSqEnt,
lLiteralDqEnt,
rLiteralDqEnt,
dot,
}
/*
Enum for Token Types.
Note that these names correspond either directly
to Semantic Token types or they are mapped to
Semantic Tokens types via semanticTokenStyleDefaults
property in package.json
*/
export enum TokenLevelState {
attributeNameTest, // struct
comment,
number,
Unset, // macro
operator,
variable, // (xsl)
Whitespace, // not used
string,
uriLiteral, // constant
nodeType, // parameter
simpleType, // parameterType
axisName, // label
nodeNameTest, // (xsl) class
functionNameTest,
complexExpression, // (xsl) keyword
function,
entityRef, // same name as xslLexer entity reference
anonymousFunction,
mapKey,
}
export enum ExitCondition {
None,
SingleQuote,
DoubleQuote,
CurlyBrace,
}
export interface LexPosition {
line: number
startCharacter: number
documentOffset: number
}
export type TokenTypeStrings = keyof typeof TokenLevelState
export enum ModifierState {
UnusedVar = 0, // declaration
UnresolvedRef = 1, // documentation
}
export class Data {
public static separators = [
'!',
'*',
'+',
',',
'-',
'.',
'/',
':',
'<',
'=',
'>',
'?',
'|',
'%',
]
public static doubleSeps = [
'!=',
'*:',
'..',
'//',
'::',
':=',
'->',
'<<',
'<=',
'=>',
'>=',
'>>',
'||',
'!!',
'??',
]
public static anySeps = [
'=',
':',
'.',
'/',
'=',
'<',
'>',
'|',
'!',
'*',
'+',
',',
'-',
'.',
'?',
'[',
]
public static triggerWords = [
'and',
'andAlso',
'array',
'as',
'div',
'else',
'eq',
'except',
'ge',
'gt',
'idiv',
'if',
'in',
'intersect',
'is',
'le',
'lt',
'mod',
'ne',
'of',
'or',
'orElse',
'otherwise',
'return',
'satisfies',
'then',
'to',
'union',
'&lt;',
'&gt;',
]
public static axes = [
'ancestor',
'ancestor-or-self',
'attribute',
'child',
'descendant',
'descendant-or-self',
'following',
'following-sibling',
'namespace',
'parent',
'preceding',
'preceding-sibling',
'self',
]
public static cAxes = [
'ancestor',
'ancestor-or-self',
'descendant',
'descendant-or-self',
'following',
'following-sibling',
'parent',
'preceding',
'preceding-sibling',
'self',
]
public static nodeTypes = [
'attribute',
'comment',
'document-node',
'element',
'empty-sequence',
'item',
'namespace-node',
'node',
'processing-instruction',
'schema-attribute',
'schema-element',
'text',
]
public static nodeTypesBrackets = Data.nodeTypes.map((t) => t + '()')
public static cNodeTypes = [
'attribute',
'comment',
'element',
'item',
'namespace-node',
'node',
'processing-instruction',
'text',
]
// note: 'otherwise' is a Saxon extension operator:
public static keywords = [
'and',
'andAlso',
'array',
'as',
'div',
'else',
'eq',
'except',
'function',
'ge',
'gt',
'idiv',
'if',
'in',
'intersect',
'is',
'le',
'lt',
'map',
'mod',
'ne',
'of',
'or',
'orElse',
'otherwise',
'return',
'satisfies',
'then',
'to',
'treat',
'union',
'&lt;',
'&gt;',
]
// note: 'member' is a proposed Saxon extension: for member $a in array-expression:
public static rangeVars = ['every', 'for', 'let', 'member', 'some', 'return']
public static firstParts = ['cast', 'castable', 'instance', 'treat']
public static secondParts = ['as', 'of']
public static nonFunctionConditional = ['if', 'then', 'else']
public static nonFunctionTypes = [
'map',
'array',
'function',
'enum',
'union',
'record',
]
public static nonFunctionTypesBrackets = Data.nonFunctionTypes.map(
(t) => t + '(*)'
)
public static setAsOperatorIfKeyword(token: Token) {
if (
token.value === 'return' ||
token.value === 'satisfies' ||
token.value === 'in' ||
Data.nonFunctionConditional.indexOf(token.value) > -1
) {
token.tokenType = TokenLevelState.complexExpression
} else if (Data.keywords.indexOf(token.value) > -1) {
token.tokenType = TokenLevelState.operator
} else {
// token['error'] = ErrorType.XPathKeyword;
}
}
public static isPart2andMatchesPart1(
part1Token: Token,
part2Token: Token
): [boolean, boolean] {
let p1 = part1Token.value
let p2 = part2Token.value
let isPart2: boolean
let matchesPart1: boolean
switch (p2) {
case 'else':
isPart2 = true
matchesPart1 = p1 === 'then'
break
case ',':
matchesPart1 = p1 === 'in' || p1 === ':='
// 'then' added here as it must not appear in an if expr:
;((isPart2 = p1 === 'then'), p1 === 'in' || p1 === ':=')
break
default:
isPart2 = false
matchesPart1 = false
break
}
return [isPart2, matchesPart1]
}
}
export class XPathLexer {
public debug: boolean = false
public timerOn: boolean = false
public entityRefOn: boolean = true
public documentText: string = ''
public documentTokens: BaseToken[] = []
public attributeNameTests: string[] | undefined
public elementNameTests: string[] | undefined
private latestRealToken: Token | null = null
private lineNumber: number = 0
private wsCharNumber: number = 0
private tokenCharNumber: number = 0
private wsNewLine = false
private deferWsNewLine = false
public reset() {
this.documentText = ''
this.documentTokens = []
this.attributeNameTests = undefined
this.elementNameTests = undefined
this.latestRealToken = null
this.lineNumber = 0
this.wsCharNumber = 0
this.tokenCharNumber = 0
this.wsNewLine = false
this.deferWsNewLine = false
}
public static getTextmateTypeLegend(): string[] {
let textmateTypes: string[] = []
let keyCount: number = Object.keys(TokenLevelState).length / 2
for (let i = 0; i < keyCount; i++) {
textmateTypes.push(TokenLevelState[i])
}
return textmateTypes
}
private static isPartOperator(
firstPart: string,
secondPart: string
): boolean {
let result = false
switch (firstPart) {
case 'cast':
case 'castable':
case 'treat':
result = secondPart === 'as'
break
case 'instance':
result = secondPart === 'of'
break
}
return result
}
private calcNewState(
isFirstChar: boolean,
nesting: number,
char: string,
nextChar: string,
existing: CharLevelState
): [CharLevelState, number] {
let rv: CharLevelState
let firstCharOfToken = true
switch (existing) {
case CharLevelState.lNl:
let charCode = char.charCodeAt(0)
let nextCharCode = nextChar ? nextChar.charCodeAt(0) : -1
if (XPathLexer.isDigit(charCode) || char === '.') {
rv = existing
} else if (char === 'e' || char === 'E') {
if (
nextChar === '-' ||
nextChar === '+' ||
XPathLexer.isDigit(nextCharCode)
) {
rv = CharLevelState.exp
} else {
rv = existing
}
} else {
;({ rv, nesting } = this.testChar(
existing,
firstCharOfToken,
char,
nextChar,
nesting
))
}
break
case CharLevelState.exp:
rv = CharLevelState.lNl
break
case CharLevelState.rDqEnt:
rv = existing
switch (nesting) {
case 0:
case 2:
nesting++
break
case 1:
if (char === 'u' && nextChar === 'o') {
nesting++
} else {
nesting = 0
rv = CharLevelState.lDqEnt
}
break
case 3:
if (char === 't') {
nesting++
} else {
rv = CharLevelState.lDqEnt
nesting = 0
}
break
case 4:
if (char === ';') {
rv = CharLevelState.rDq
} else {
rv = CharLevelState.lDqEnt
}
nesting = 0
break
}
break
case CharLevelState.rSqEnt:
rv = existing
switch (nesting) {
case 0:
case 2:
nesting++
break
case 1:
if (char === 'p' && nextChar === 'o') {
nesting++
} else {
nesting = 0
rv = CharLevelState.lSqEnt
}
break
case 3:
if (char === 's') {
nesting++
} else {
rv = CharLevelState.lSqEnt
nesting = 0
}
break
case 4:
if (char === ';') {
rv = CharLevelState.rSq
} else {
rv = CharLevelState.lSqEnt
}
nesting = 0
break
}
break
case CharLevelState.lWs:
if (char === ' ' || char === '\t') {
rv = existing
this.wsCharNumber++
} else if (char === '\n') {
rv = existing
this.wsCharNumber = 0
this.wsNewLine = true
this.lineNumber++
} else {
// we must switch to the new state, depending on the char/nextChar
;({ rv, nesting } = this.testChar(
existing,
firstCharOfToken,
char,
nextChar,
nesting
))
}
break
case CharLevelState.lName:
case CharLevelState.lVar:
case CharLevelState.lAttr:
if (
char === '-' ||
char === '.' ||
(char === ':' &&
!(
nextChar === ':' ||
nextChar === '*' ||
nextChar === ' ' ||
nextChar === '\n' ||
nextChar === '\r' ||
nextChar === '\t'
))
) {
rv = existing
} else {
// we must switch to the new state, depending on the char/nextChar
;({ rv, nesting } = this.testChar(
existing,
isFirstChar,
char,
nextChar,
nesting
))
}
break
case CharLevelState.dSep:
rv = CharLevelState.dSep2
break
case CharLevelState.lUri:
rv = char === '}' ? CharLevelState.rUri : existing
break
case CharLevelState.lSqEnt:
rv = char === '&' && nextChar === 'a' ? CharLevelState.rSqEnt : existing
break
case CharLevelState.lDqEnt:
rv = char === '&' && nextChar === 'q' ? CharLevelState.rDqEnt : existing
break
case CharLevelState.lSq:
case CharLevelState.rLiteralSqEnt:
if (char === "'") {
if (nextChar === "'") {
rv = CharLevelState.escSq
} else {
rv = CharLevelState.rSq
}
} else if (char === '&') {
rv = CharLevelState.lLiteralSqEnt
} else {
rv = CharLevelState.lSq
}
break
case CharLevelState.lLiteralSqEnt:
if (char === "'") {
if (nextChar === "'") {
rv = CharLevelState.escSq
} else {
rv = CharLevelState.rSq
}
} else if (char === ';') {
rv = CharLevelState.rLiteralSqEnt
} else {
rv = existing
}
break
case CharLevelState.escSq:
rv = CharLevelState.lSq
break
case CharLevelState.escDq:
rv = CharLevelState.lDq
break
case CharLevelState.lDq:
case CharLevelState.rLiteralDqEnt:
if (char === '"') {
if (nextChar === '"') {
rv = CharLevelState.escDq
} else {
rv = CharLevelState.rDq
}
} else if (char === '&') {
rv = CharLevelState.lLiteralDqEnt
} else {
rv = CharLevelState.lDq
}
break
case CharLevelState.lLiteralDqEnt:
if (char === '"') {
if (nextChar === '"') {
rv = CharLevelState.escDq
} else {
rv = CharLevelState.rDq
}
} else if (char === ';') {
rv = CharLevelState.rLiteralDqEnt
} else {
rv = existing
}
break
case CharLevelState.lC:
if (char === ':' && nextChar === ')') {
rv = nesting === 1 ? CharLevelState.rC : existing
nesting--
} else if (char === '(' && nextChar === ':') {
rv = existing
nesting++
} else {
rv = existing
}
break
case CharLevelState.lEnt:
rv = char === ';' ? CharLevelState.rEnt : existing
break
default:
;({ rv, nesting } = this.testChar(
existing,
isFirstChar,
char,
nextChar,
nesting
))
}
return [rv, nesting]
}
public analyse(
xpathArg: string,
exitCondition: ExitCondition | null,
position: LexPosition
): Token[] {
if (this.timerOn) {
console.time('xplexer.analyse')
}
this.latestRealToken = null
this.lineNumber = position.line
this.wsCharNumber = 0
this.tokenCharNumber = position.startCharacter
this.wsNewLine = false
this.deferWsNewLine = false
let xpath = xpathArg.length === 0 ? this.documentText : xpathArg
let currentState: [CharLevelState, number] = [CharLevelState.init, 0]
let currentChar: string = ''
let tokenChars: string[] = []
if (exitCondition === ExitCondition.None) {
this.documentTokens.length = 0
}
let result = this.documentTokens
let nestedTokenStack: Token[] = []
let poppedContext: Token | undefined | null = null
if (xpath.length === 0) {
return []
}
for (let i = position.documentOffset; i < xpath.length + 1; i++) {
let nextChar: string = xpath.charAt(i)
// deconstruct state:
let [currentLabelState, nestingState] = currentState
let nextState: [CharLevelState, number]
let isFirstTokenChar = tokenChars.length === 0
if (currentChar) {
let exitAnalysis = false
switch (exitCondition) {
case ExitCondition.None:
exitAnalysis = false
break
case ExitCondition.CurlyBrace:
if (
currentLabelState !== CharLevelState.lDq &&
currentLabelState !== CharLevelState.lSq &&
currentLabelState !== CharLevelState.lC &&
currentChar === '}'
) {
let isNestedOk = false
for (var x = 0; x < nestedTokenStack.length; x++) {
if (nestedTokenStack[x].value === '{') {
isNestedOk = true
break
}
}
exitAnalysis = !isNestedOk
}
break
case ExitCondition.DoubleQuote:
exitAnalysis = currentChar === '"'
break
case ExitCondition.SingleQuote:
exitAnalysis = currentChar === "'"
break
}
if (exitAnalysis) {
this.update(poppedContext, result, tokenChars, currentLabelState)
if (result.length > 0) {
let lastToken = result[result.length - 1]
if (lastToken.tokenType === TokenLevelState.string) {
XPathLexer.checkExitStringLiteralEnd(lastToken, result)
} else if (lastToken.tokenType === TokenLevelState.entityRef) {
if (result.length > 1) {
const nextLastToken = result[result.length - 2]
if (nextLastToken.tokenType === TokenLevelState.string) {
if (
!lastToken.value.endsWith('&quot;') &&
!lastToken.value.startsWith('&apos;')
) {
lastToken['error'] = ErrorType.XPathStringLiteral
}
}
}
}
}
position.line = this.lineNumber
position.startCharacter = this.tokenCharNumber
position.documentOffset = i
return result
}
nextState = this.calcNewState(
isFirstTokenChar,
nestingState,
currentChar,
nextChar,
currentLabelState
)
let [nextLabelState] = nextState
if (
(nextLabelState === currentLabelState &&
!this.unChangedStateSignificant(currentLabelState)) ||
(currentLabelState === CharLevelState.exp &&
nextLabelState === CharLevelState.lNl)
) {
// do nothing if state has not changed
// or we're within a number with an exponent
if (
currentChar === '\n' &&
(currentLabelState === CharLevelState.lSq ||
currentLabelState === CharLevelState.lDq ||
currentLabelState === CharLevelState.lC ||
currentLabelState === CharLevelState.lSqEnt ||
currentLabelState === CharLevelState.lDqEnt)
) {
// split multi-line strings or comments - don't include newline char
this.update(poppedContext, result, tokenChars, currentLabelState)
this.lineNumber++
this.tokenCharNumber = 0
} else {
tokenChars.push(currentChar)
}
} else {
// state has changed, so save token and start new token
switch (nextLabelState) {
case CharLevelState.lNl:
case CharLevelState.lVar:
case CharLevelState.lName:
case CharLevelState.lEnt:
case CharLevelState.lLiteralSqEnt:
case CharLevelState.lLiteralDqEnt:
this.update(poppedContext, result, tokenChars, currentLabelState)
tokenChars = []
tokenChars.push(currentChar)
break
case CharLevelState.exp:
case CharLevelState.rSqEnt:
case CharLevelState.rDqEnt:
tokenChars.push(currentChar)
break
case CharLevelState.dSep:
this.update(poppedContext, result, tokenChars, currentLabelState)
let bothChars = currentChar + nextChar
this.updateResult(
poppedContext,
result,
new BasicToken(bothChars, nextLabelState)
)
break
case CharLevelState.dSep2:
break
case CharLevelState.sep:
case CharLevelState.dot:
this.update(poppedContext, result, tokenChars, currentLabelState)
this.updateResult(
poppedContext,
result,
new BasicToken(currentChar, nextLabelState)
)
break
case CharLevelState.escSq:
case CharLevelState.escDq:
tokenChars.push(currentChar)
break
case CharLevelState.rC:
tokenChars.push(':)')
this.update(poppedContext, result, tokenChars, currentLabelState)
break
case CharLevelState.lB:
case CharLevelState.lBr:
case CharLevelState.lPr:
this.update(poppedContext, result, tokenChars, currentLabelState)
let currentToken: Token
currentToken = new FlattenedToken(
currentChar,
nextLabelState,
this.latestRealToken
)
this.updateResult(poppedContext, result, currentToken)
// add to nesting level
nestedTokenStack.push(currentToken)
this.latestRealToken = null
break
case CharLevelState.rB:
case CharLevelState.rBr:
case CharLevelState.rPr:
if (currentLabelState !== CharLevelState.rC) {
let prevToken: Token = new BasicToken(
tokenChars.join(''),
currentLabelState
)
this.updateResult(poppedContext, result, prevToken)
let newToken: Token = new BasicToken(
currentChar,
nextLabelState
)
this.updateResult(poppedContext, result, newToken)
if (nestedTokenStack.length > 0) {
// remove from nesting level
if (
XPathLexer.closeMatchesOpen(
nextLabelState,
nestedTokenStack
)
) {
poppedContext = nestedTokenStack.pop()?.context
} else {
newToken['error'] = ErrorType.BracketNesting
}
} else {
newToken['error'] = ErrorType.BracketNesting
}
tokenChars = []
}
break
case CharLevelState.rEnt:
tokenChars.push(currentChar)
let ent = tokenChars.join('')
if (ent === '&quot;') {
nextState = [CharLevelState.lDqEnt, 0]
} else if (ent === '&apos;') {
nextState = [CharLevelState.lSqEnt, 0]
} else {
let entToken: Token = new BasicToken(ent, CharLevelState.lName)
this.updateResult(poppedContext, result, entToken)
tokenChars.length = 0
}
break
case CharLevelState.rSq:
case CharLevelState.rDq:
case CharLevelState.rUri:
case CharLevelState.rLiteralSqEnt:
case CharLevelState.rLiteralDqEnt:
tokenChars.push(currentChar)
this.update(poppedContext, result, tokenChars, currentLabelState)
break
case CharLevelState.lSq:
case CharLevelState.lDq:
case CharLevelState.lC:
case CharLevelState.lWs:
case CharLevelState.lUri:
if (
currentLabelState !== CharLevelState.escSq &&
currentLabelState !== CharLevelState.escDq
) {
this.update(
poppedContext,
result,
tokenChars,
currentLabelState
)
}
tokenChars.push(currentChar)
break
default:
if (currentLabelState === CharLevelState.rC) {
// in this case, don't include ')' as it is part of last token
tokenChars = []
} else if (currentLabelState === CharLevelState.lWs) {
// set whitespace token and then initial with currentChar
this.update(
poppedContext,
result,
tokenChars,
currentLabelState
)
tokenChars.push(currentChar)
} else {
tokenChars.push(currentChar)
}
break
}
}
if (!nextChar && tokenChars.length > 0) {
this.update(poppedContext, result, tokenChars, nextLabelState)
}
currentState = nextState
} // end if(currentChar)
currentChar = nextChar
} // end iteration over chars
if (this.timerOn) {
console.timeEnd('xplexer.analyse')
}
return result
}
public static checkExitStringLiteralEnd(
lastToken: BaseToken,
result: BaseToken[]
) {
let followsEntityRef = false
let followsString = false
if (result.length > 1) {
const nextLastToken = result[result.length - 2]
followsEntityRef = nextLastToken.tokenType === TokenLevelState.entityRef
followsString = nextLastToken.tokenType === TokenLevelState.string
}
if (followsEntityRef) {
let lastChar = lastToken.value.charAt(lastToken.value.length - 1)
const lastCharIsSingleQuote = lastChar === "'"
if (
lastChar !== '"' &&
!lastCharIsSingleQuote &&
!lastToken.value.endsWith('&quot;') &&
!lastToken.value.startsWith('&apos;')
) {
lastToken['error'] = ErrorType.XPathStringLiteral
} else if (
lastChar === '"' ||
(lastChar === "'" && lastToken.length > 1)
) {
const mod2Chars =
[...lastToken.value].filter((l) => l === lastChar).length % 2
if (mod2Chars === 0) {
lastToken['error'] = ErrorType.XPathStringLiteral
}
}
} else if (!followsString) {
this.checkStringLiteralEnd(lastToken)
}
}
public static checkStringLiteralEnd(lastToken: BaseToken) {
let lastChar = lastToken.value.charAt(lastToken.value.length - 1)
let firstChar = lastToken.value.charAt(0)
if (
!(
(lastChar === firstChar && lastToken.value.length > 1) ||
(lastToken.value.length > 6 &&
lastToken.value.startsWith('&quot;') &&
lastToken.value.endsWith('&quot;')) ||
(lastToken.value.startsWith('&apos;') &&
lastToken.value.endsWith('&apos;'))
)
) {
lastToken['error'] = ErrorType.XPathStringLiteral
}
}
private static closeMatchesOpen(
close: CharLevelState,
stack: Token[]
): boolean {
let open: CharLevelState | undefined = stack[stack.length - 1].charType
let result: boolean = false
switch (close) {
case CharLevelState.rB:
result = open === CharLevelState.lB
break
case CharLevelState.rBr:
result = open === CharLevelState.lBr
break
case CharLevelState.rPr:
result = open === CharLevelState.lPr
}
return result
}
private update(
poppedContext: Token | null | undefined,
result: Token[],
tokenChars: string[],
charState: CharLevelState
) {
if (tokenChars.length > 0) {
this.updateResult(
poppedContext,
result,
new BasicToken(tokenChars.join(''), charState)
)
}
tokenChars.length = 0
}
private unChangedStateSignificant(charState: CharLevelState): boolean {
let result: boolean = false
switch (charState) {
case CharLevelState.lB:
case CharLevelState.lBr:
case CharLevelState.lPr:
case CharLevelState.rB:
case CharLevelState.rBr:
case CharLevelState.rPr:
case CharLevelState.sep:
case CharLevelState.dot:
result = true
}
return result
}
private updateResult(
poppedContext: Token | null | undefined,
result: Token[],
newToken: Token
) {
let cachedRealToken = this.latestRealToken
if (cachedRealToken) {
}
let state = newToken.charType
let newTokenValue = newToken.value
if (newTokenValue !== '') {
newToken.length = newTokenValue.length
newToken.line = this.lineNumber
newToken.startCharacter = this.tokenCharNumber
if (newToken.tokenType === TokenLevelState.nodeNameTest) {
if (newTokenValue.includes('#')) {
newToken.tokenType = TokenLevelState.functionNameTest
}
}
let isWhitespace = newToken.charType === CharLevelState.lWs
if (this.deferWsNewLine) {
if (isWhitespace) {
this.lineNumber++
newToken.line = this.lineNumber
this.tokenCharNumber = this.wsCharNumber
} else {
this.tokenCharNumber = 0
this.lineNumber++
this.wsNewLine = true
}
this.deferWsNewLine = false
} else if (this.wsNewLine) {
if (isWhitespace) {
this.tokenCharNumber = this.wsCharNumber
} else {
this.tokenCharNumber += newTokenValue.length
}
this.wsNewLine = false
} else {
this.tokenCharNumber += newTokenValue.length
}
if (!isWhitespace) {
this.wsCharNumber = 0
}
if (!isWhitespace) {
result.push(newToken)
}
let prevToken = this.latestRealToken
this.setLabelForLastTokenOnly(prevToken, newToken)
this.setLabelsUsingCurrentToken(poppedContext, prevToken, newToken)
if (
prevToken &&
!isWhitespace &&
this.elementNameTests &&
this.attributeNameTests
) {
if (prevToken.tokenType === TokenLevelState.nodeNameTest) {
if (this.elementNameTests.indexOf(prevToken.value) < 0) {
this.elementNameTests.push(prevToken.value)
}
} else if (prevToken.tokenType === TokenLevelState.attributeNameTest) {
if (this.attributeNameTests.indexOf(prevToken.value) < 0) {
this.attributeNameTests.push(prevToken.value)
}
}
}
if (!(state === CharLevelState.lC || state === CharLevelState.lWs)) {
this.latestRealToken = newToken
}
}
}
/* private conditionallyPopStack(stack: Token[], token: Token) {
if (stack.length > 0) {
let [isPart2, matchesPart1] = Data.isPart2andMatchesPart1(stack[stack.length - 1], token);
const initStackVal = stack.length > 0 ? stack[stack.length - 1].value : '';
if ((initStackVal === 'return' || initStackVal === 'satisfies') && token.value === ',') {
stack.pop();
let validStackValue : any = null;
let init = true;
while (stack.length > 0) {
if (init) {
init = false;
let v = stack[stack.length - 1].value;
if (v === ':=' || v === 'in') {
validStackValue = v;
stack.pop();
}
} else if (stack[stack.length - 1].value === validStackValue) {
stack.pop();
} else {
break;
}
}
} else if (isPart2) { if (matchesPart1) { stack.pop(); } else token['error'] = ErrorType.XPathUnexpected; }
}
} */
private updateTokenBeforeBrackets(prevToken: Token) {
if (prevToken.tokenType === TokenLevelState.nodeNameTest) {
if (Data.nodeTypes.indexOf(prevToken.value) > -1) {
prevToken.tokenType = TokenLevelState.nodeType
} else if (Data.nonFunctionConditional.indexOf(prevToken.value) > -1) {
prevToken.tokenType = TokenLevelState.complexExpression
} else if (prevToken.value === 'function') {
prevToken.tokenType = TokenLevelState.anonymousFunction
} else if (prevToken.value === 'map' || prevToken.value === 'array') {
prevToken.tokenType = TokenLevelState.simpleType
} else {
prevToken.tokenType = TokenLevelState.function
}
} else if (
prevToken.tokenType === TokenLevelState.simpleType &&
Data.nodeTypes.indexOf(prevToken.value) > -1
) {
prevToken.tokenType = TokenLevelState.nodeType
}
}
private setLabelForLastTokenOnly(
prevToken: Token | null,
currentToken: Token
) {
let currentState = currentToken.charType
if (prevToken) {
if (prevToken.charType === CharLevelState.lName) {
switch (currentState) {
case CharLevelState.lVar:
if (Data.rangeVars.indexOf(prevToken.value) > -1) {
// every, for, let, some
prevToken.tokenType = TokenLevelState.complexExpression
}
break
case CharLevelState.lB:
this.updateTokenBeforeBrackets(prevToken)
break
case CharLevelState.dSep:
if (currentToken.value === '::') {
if (Data.axes.indexOf(prevToken.value) < 0) {
prevToken['error'] = ErrorType.AxisName
}
prevToken.tokenType = TokenLevelState.axisName
} else if (currentToken.value === '()') {
this.updateTokenBeforeBrackets(prevToken)
} else if (
currentToken.value === '{}' &&
(prevToken.value === 'map' || prevToken.value === 'array')
) {
prevToken.tokenType = TokenLevelState.operator
}
break
case CharLevelState.lBr:
if (prevToken.value === 'map' || prevToken.value === 'array') {
prevToken.tokenType = TokenLevelState.operator
}
break
case CharLevelState.lName:
if (currentToken.value === 'member' && prevToken.value === 'for') {
prevToken.tokenType = TokenLevelState.complexExpression
}
break
}
} else if (
currentState === CharLevelState.sep &&
prevToken.tokenType === TokenLevelState.string &&
currentToken.value === ':'
) {
prevToken.tokenType = TokenLevelState.mapKey
} else if (
(currentState === CharLevelState.lB ||
currentState === CharLevelState.lBr) &&
prevToken.charType === CharLevelState.dSep &&
prevToken.value === '->'
) {
prevToken.tokenType = TokenLevelState.anonymousFunction
}
}
}
private setLabelsUsingCurrentToken(
poppedContext: Token | null | undefined,
prevToken: Token | null,
currentToken: Token
) {
if (!prevToken) {
prevToken = new BasicToken(',', CharLevelState.sep)
prevToken.tokenType = TokenLevelState.operator
}
let currentValue = currentToken.value
switch (currentToken.charType) {
case CharLevelState.lName:
// token is a 'name' that needs resolving:
// a Name cannot follow a Name -- unless it's like 'instance of'
switch (prevToken.charType) {
case CharLevelState.lName:
// previous token was lName and current token is lName
if (
Data.secondParts.indexOf(currentValue) > -1 &&
XPathLexer.isPartOperator(prevToken.value, currentValue)
) {
// castable as etc.
prevToken.tokenType = TokenLevelState.operator
currentToken.tokenType = TokenLevelState.operator
} else if (
XPathLexer.isTokenTypeEqual(prevToken, TokenLevelState.operator)
) {
// don't set to name because it may be a function etc.
//currentToken.tokenType = TokenLevelState.Name;
if (prevToken.value === 'as' || prevToken.value === 'of') {
// e.g. castable as xs:integer
// TODO: check if value equals xs:integer or element?
currentToken.tokenType = TokenLevelState.simpleType
} else if (
prevToken.value === '&gt;' &&
currentToken.value === '&gt;'
) {
currentToken.tokenType = TokenLevelState.operator
} else if (
prevToken.value === '&lt;' &&
(currentToken.value === '&lt;' || currentToken.value === '&gt;')
) {
currentToken.tokenType = TokenLevelState.operator
}
} else if (
prevToken.tokenType === TokenLevelState.nodeNameTest ||
prevToken.tokenType === TokenLevelState.functionNameTest ||
XPathLexer.isTokenTypeAType(prevToken)
) {
Data.setAsOperatorIfKeyword(currentToken)
}
break
case CharLevelState.rB:
case CharLevelState.rBr:
case CharLevelState.rPr:
case CharLevelState.lAttr:
case CharLevelState.lNl:
case CharLevelState.lVar:
case CharLevelState.lSq:
case CharLevelState.lDq:
case CharLevelState.rLiteralSqEnt:
case CharLevelState.rLiteralDqEnt:
case CharLevelState.rDqEnt:
case CharLevelState.rSqEnt:
case CharLevelState.dot:
Data.setAsOperatorIfKeyword(currentToken)
break
case CharLevelState.dSep:
if (
prevToken.value === '()' ||
prevToken.value === '..' ||
prevToken.value === '[]' ||
prevToken.value === '{}'
) {
Data.setAsOperatorIfKeyword(currentToken)
}
break
default:
// current token is an lName but previous token was not
if (
XPathLexer.isTokenTypeUnset(prevToken) &&
Data.keywords.indexOf(currentValue) > -1
) {
currentToken.tokenType = TokenLevelState.operator
} else if (
XPathLexer.isCharTypeEqual(prevToken, CharLevelState.dSep) &&
prevToken.value === '()' &&
Data.keywords.indexOf(currentValue) > -1
) {
currentToken.tokenType = TokenLevelState.operator
} else if (
XPathLexer.isTokenTypeEqual(
prevToken,
TokenLevelState.operator
) &&
(prevToken.value === 'as' || prevToken.value === 'of')
) {
currentToken.tokenType = TokenLevelState.simpleType
}
break
}
break
case CharLevelState.sep:
let prevTokenT = prevToken.tokenType
let isStar = currentToken.value === '*'
if (
isStar &&
(prevTokenT === TokenLevelState.attributeNameTest ||
prevTokenT === TokenLevelState.uriLiteral ||
(prevTokenT === TokenLevelState.operator &&
prevToken.value === '?') ||
(prevTokenT === TokenLevelState.nodeType &&
prevToken.value === '()'))
) {
// @* or {example.com}*
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.nodeType
} else {
let possOccurrentIndicator =
currentToken.value === '?' || currentToken.value === '+' || isStar
if (possOccurrentIndicator) {
if (
prevTokenT === TokenLevelState.simpleType &&
prevToken.length > 1
) {
// xs:integer? etc
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.simpleType
} else if (
(prevTokenT === TokenLevelState.operator &&
prevToken.value === ')') ||
prevToken.value === ']'
) {
// ($a) * 9 or count($a) * 8 or abc as map(*)* or $item as node()+
if (
poppedContext &&
poppedContext.tokenType === TokenLevelState.simpleType
) {
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.nodeType
}
} else if (
isStar &&
(prevTokenT === TokenLevelState.operator ||
prevTokenT === TokenLevelState.complexExpression)
) {
// $a and * or if ($a) then * else book
if (
prevTokenT === TokenLevelState.operator &&
prevToken.charType === CharLevelState.dSep &&
prevToken.value === '()'
) {
// keep the same
} else {
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.nodeType
}
}
}
}
break
case CharLevelState.dSep:
if (currentToken.value === ':*' || currentToken.value === '..') {
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.nodeType
} else if (
currentToken.value === '()' &&
prevToken.tokenType === TokenLevelState.nodeType
) {
currentToken.charType = CharLevelState.lName
currentToken.tokenType = TokenLevelState.nodeType
}
break
}
}
testChar(
existingState: CharLevelState,
isFirstChar: boolean,
char: string,
nextChar: string,
nesting: number
) {
let rv: CharLevelState
if (isFirstChar && char === 'Q' && nextChar === '{') {
rv = CharLevelState.lUri
return { rv, nesting }
}
switch (char) {
case '(':
if (nextChar === ':') {
rv = CharLevelState.lC
nesting++
} else if (nextChar == ')') {
rv = CharLevelState.dSep
} else {
rv = CharLevelState.lB
}
break
case '{':
if (nextChar === '}') {
rv = CharLevelState.dSep
} else {
rv = CharLevelState.lBr
}
break
case '[':
if (nextChar === ']') {
rv = CharLevelState.dSep
} else {
rv = CharLevelState.lPr
}
break
case ')':
rv = CharLevelState.rB
break
case ']':
rv = CharLevelState.rPr
break
case '}':
rv = CharLevelState.rBr
break
case "'":
rv = CharLevelState.lSq
break
case '"':
rv = CharLevelState.lDq
break
case ' ':
case '\t':
rv = CharLevelState.lWs
this.wsCharNumber++
break
case '\r':
rv = CharLevelState.lWs
break
case '\n':
this.deferWsNewLine = true
rv = CharLevelState.lWs
break
case '+':
rv = CharLevelState.sep
break
case '&':
rv = this.entityRefOn ? CharLevelState.lEnt : existingState
break
default:
let doubleChar = char + nextChar
if (nextChar && Data.doubleSeps.indexOf(doubleChar) > -1) {
rv = CharLevelState.dSep
break
} else if (Data.separators.indexOf(char) > -1) {
let isDot = char === '.'
if (
isDot &&
!!nextChar &&
XPathLexer.isDigit(nextChar.charCodeAt(0))
) {
rv = CharLevelState.lNl
} else if (isDot) {
rv = CharLevelState.dot
} else {
rv = CharLevelState.sep
}
} else if (isFirstChar) {
let charCode = char.charCodeAt(0)
let nextCharCode = nextChar ? nextChar.charCodeAt(0) : -1
// check 'dot' char:
if (charCode === 46) {
if (nextCharCode === 46) {
// '..' parent axis
rv = CharLevelState.dSep
} else {
rv = XPathLexer.isDigit(nextCharCode)
? CharLevelState.lNl
: CharLevelState.dot
}
} else if (XPathLexer.isDigit(charCode)) {
rv = CharLevelState.lNl
} else if (char === '$') {
rv = CharLevelState.lVar
} else if (char === '@') {
rv = CharLevelState.lAttr
} else {
rv = CharLevelState.lName
}
} else {
rv = existingState
}
}
return { rv, nesting }
}
private static isDigit(charCode: number) {
return charCode > 47 && charCode < 58
}
public static isCharTypeEqual(
token: Token | null,
type2: CharLevelState
): boolean {
return token ? token.charType === type2 : false
}
private static isTokenTypeEqual(
token: Token,
type2: TokenLevelState
): boolean {
return token.tokenType === type2
}
private static isTokenTypeAType(token: Token): boolean {
return (
token.tokenType === TokenLevelState.simpleType ||
token.tokenType === TokenLevelState.nodeType
)
}
private static isTokenTypeUnset(token: Token): boolean {
return token.tokenType.valueOf() === TokenLevelState.Unset.valueOf()
}
}
export enum ErrorType {
None,
AxisName,
DuplicateVarName,
DuplicateFnName,
DuplicateParameterName,
DuplicateTemplateName,
DuplicateAccumulatorName,
ElementNesting,
XPathTypeName,
AccumulatorNameUnresolved,
XPathName,
EntityName,
XPathFunction,
XPathFunctionNamespace,
XPathFunctionUnexpected,
XPathOperatorUnexpected,
XPathPrefix,
XPathKeyword,
XPathExpectedComplex,
XPathUnexpected,
XPathAwaiting,
XPathStringLiteral,
BracketNesting,
ExpectedElseAfterThen,
ExpectedDollarAfterComma,
XSLTKeyUnresolved,
}
export interface BaseToken {
line: number
startCharacter: number
length: number
value: string
charType?: number
tokenType: number
context?: BaseToken | null
error?: ErrorType
nesting?: number
referenced?: boolean
}
export interface Token extends BaseToken {
charType?: CharLevelState
tokenType: TokenLevelState
context?: Token | null
children?: Token[]
error?: ErrorType
}
export class Utilities {
public static minimiseTokens(tokens: Token[]): Token[] {
let r: Token[] = new Array()
for (let token of tokens) {
if (token.charType !== CharLevelState.lWs) {
delete token.charType
delete token.context
// delete token.line;
// delete token.length;
// delete token.startCharacter;
r.push(token)
}
if (token.children) {
token.children = this.minimiseTokens(token.children)
}
}
return r
}
public static minimiseTokens2(tokens: Token[]): Token[] {
let r: Token[] = new Array()
for (let token of tokens) {
if (token.charType !== CharLevelState.lWs) {
delete token.charType
delete token.context
r.push(token)
}
if (token.children) {
token.children = this.minimiseTokens2(token.children)
}
}
return r
}
}
class BasicToken implements Token {
line = 0
startCharacter = 0
length = 0
value: string
charType: CharLevelState
tokenType: TokenLevelState
constructor(value: string, type: CharLevelState) {
this.value = value
this.charType = type
switch (type) {
case CharLevelState.lWs:
this.tokenType = TokenLevelState.Whitespace
break
case CharLevelState.lName:
this.tokenType = TokenLevelState.nodeNameTest
break
case CharLevelState.dSep:
this.tokenType =
value === ':='
? TokenLevelState.complexExpression
: TokenLevelState.operator
break
case CharLevelState.dot:
this.tokenType = TokenLevelState.nodeType
break
case CharLevelState.sep:
case CharLevelState.lB:
case CharLevelState.lBr:
case CharLevelState.lPr:
case CharLevelState.rB:
case CharLevelState.rBr:
case CharLevelState.rPr:
this.tokenType = TokenLevelState.operator
break
case CharLevelState.lAttr:
this.tokenType = TokenLevelState.attributeNameTest
break
case CharLevelState.lNl:
this.tokenType = TokenLevelState.number
break
case CharLevelState.lVar:
this.tokenType = TokenLevelState.variable
break
case CharLevelState.lSq:
case CharLevelState.lDq:
case CharLevelState.lSqEnt:
case CharLevelState.lDqEnt:
case CharLevelState.rDqEnt:
case CharLevelState.rSqEnt:
case CharLevelState.rLiteralSqEnt:
case CharLevelState.rLiteralDqEnt:
this.tokenType = TokenLevelState.string
break
case CharLevelState.lUri:
this.tokenType = TokenLevelState.uriLiteral
break
case CharLevelState.lC:
this.tokenType = TokenLevelState.comment
break
case CharLevelState.lLiteralSqEnt:
case CharLevelState.lLiteralDqEnt:
this.tokenType = TokenLevelState.entityRef
break
default:
this.tokenType = TokenLevelState.Unset
break
}
}
}
class FlattenedToken implements Token {
line = 0
startCharacter = 0
length = 0
value: string
charType: CharLevelState
tokenType: TokenLevelState
context: Token | null
constructor(value: string, type: CharLevelState, context: Token | null) {
this.value = value
this.charType = type
this.tokenType = TokenLevelState.operator
this.context = context
}
}