blob: 75df6604d0513abce61eb7dd8257843aa9ff3e76 [file]
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2020 DeltaXML Ltd. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
BaseToken,
TokenLevelState,
XPathLexer,
LexPosition,
ExitCondition,
ErrorType,
} from './xpLexer'
export enum XMLCharState {
init, // 0 initial state
lSt, // 1 left start tag
rSt, // 2 right start tag
rStNoAtt, // no attribute right start tag
lComment, // 3 left comment
rComment, // 4 right comment
lPi, // 5 left processing instruction
lPi2,
lPiName,
rPiName,
lPiValue,
rPi, // 6 right processing instruction
rPiNameOnly,
lCd, // 7 left cdata
lCdataEnd,
rCd, // 8 right cdata
rCdataEnd,
awaitingRcdata,
lSq, // 9 left single quote att
lDq, // 11 left double quote att
wsBeforeAttname,
rSq,
rDq,
lExclam, // 8 left dtd declaration
rDtd, // 10 right dtd declaration
lWs, // 13 whitspace char start
lCt, // 1 left close tag
lCt2,
lCtName,
rCt, // 2 right close tag
rSelfCtNoAtt, // self-close no att
rSelfCt, // self-close
lEn, // left element-name
rEn, // right element-name
lAn, // left atrribute-name
rAn, // right attribute-name
eqA, // attribute = symbol
lAb, // left angle-bracket
sqAvt,
dqAvt,
escDqAvt, // attribute value template
escSqAvt,
tvt,
tvtCdata,
escTvt,
escTvtCdata,
lsElementNameWs,
wsAfterAttName,
syntaxError,
lsEqWs,
lStEq,
lEntity,
rEntity,
lText,
}
export enum EntityPosition {
text,
attrDq,
attrSq,
}
// for compatibility with legend - add count of XPath enums to this
export enum XSLTokenLevelState {
attributeName,
attributeEquals,
attributeValue,
xmlnsName,
dtd,
dtdEnd,
elementName,
elementValue,
processingInstrName,
processingInstrValue,
entityRef,
xmlComment,
xmlPunctuation,
xslElementName,
xmlText,
}
export interface XslToken extends BaseToken {
line: number
startCharacter: number
length: number
value: string
charType?: XMLCharState
tokenType: TokenLevelState
context?: XslToken | null
error?: any
}
interface XmlElement {
expandText: boolean
}
export interface LanguageConfiguration {
expressionAtts: string[]
variableElementNames: string[]
linkElementAttrNames?: [string, string]
avtAtts?: string[]
nativePrefix: string
tvtAttributes: string[]
nonNativeAvts: boolean
resourceNames?: string[]
featureNames?: string[]
propertyNames?: string[]
isVersion4?: boolean
}
export interface GlobalInstructionData {
name: string
type: GlobalInstructionType
token: BaseToken
idNumber: number
memberNames?: string[]
memberTokens?: BaseToken[]
href?: string
version?: string
}
export class XslLexer {
public debug: boolean = false
public flatten: boolean = false
public timerOn: boolean = false
public provideCharLevelState = false
public globalInstructionData: GlobalInstructionData[] = []
public attributeNameTests: string[] | undefined
public elementNameTests: string[] | undefined
protected globalModeData: GlobalInstructionData[] = []
private lineNumber: number = 0
private charCount = 0
private lineCharCount = 0
private static xpathLegend = XPathLexer.getTextmateTypeLegend()
protected static xpathLegendLength = XslLexer.xpathLegend.length
private commentCharCount = 0
private cdataCharCount = 0
protected entityContext = EntityPosition.text
private languageConfiguration: LanguageConfiguration
private skipTokenChar = false
private nativeTvtAttributes: string[] = []
private genericTvtAttributes: string[] = []
private nativePrefixLength = 0
private dtdNesting = 0
private nonNativeAvts = false
constructor(languageConfiguration: LanguageConfiguration) {
this.nonNativeAvts = languageConfiguration.nonNativeAvts
this.nativePrefixLength =
languageConfiguration.nativePrefix.length === 0
? 0
: languageConfiguration.nativePrefix.length + 1
this.languageConfiguration = languageConfiguration
this.languageConfiguration.tvtAttributes.forEach((tvtAttribute) => {
this.nativeTvtAttributes.push(
this.languageConfiguration.nativePrefix + ':' + tvtAttribute
)
})
this.genericTvtAttributes = this.languageConfiguration.tvtAttributes
}
public static getTextmateTypeLegend(): string[] {
// concat xsl legend to xpath legend
let textmateTypes: string[] = this.xpathLegend.slice(0)
let keyCount: number = Object.keys(XSLTokenLevelState).length / 2
for (let i = 0; i < keyCount; i++) {
textmateTypes.push(XSLTokenLevelState[i])
}
return textmateTypes
}
public static getXsltStartTokenNumber() {
return this.xpathLegend.length
}
private isWhitespace(isCurrentCharNewLine: boolean, char: string) {
return isCurrentCharNewLine || char === ' ' || char == '\t' || char === '\r'
}
public isAvtAtt(name: string) {
return this.languageConfiguration.avtAtts
? this.languageConfiguration.avtAtts.indexOf(name) > -1
: false
}
public isExpressionAtt(name: string) {
return this.languageConfiguration.expressionAtts
? this.languageConfiguration.expressionAtts.indexOf(name) > -1
: false
}
protected calcNewState(
isCurrentCharNewLine: boolean,
char: string,
nextChar: string,
existing: XMLCharState
): XMLCharState {
let rc: XMLCharState = existing
let firstCharOfToken = true
if (firstCharOfToken) {
}
switch (existing) {
case XMLCharState.lCt:
rc = XMLCharState.lCt2
break
case XMLCharState.lCt2:
rc = XMLCharState.lCtName
break
case XMLCharState.lCtName:
if (char === '>') {
rc = XMLCharState.rCt
}
break
case XMLCharState.lPi:
rc = XMLCharState.lPi2
break
case XMLCharState.lPi2:
rc = XMLCharState.lPiName
break
case XMLCharState.lPiName:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
rc = XMLCharState.rPiName
} else if (char === '?' && nextChar === '>') {
rc = XMLCharState.rPiNameOnly
this.skipTokenChar = true
}
break
case XMLCharState.rPiName:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
// no change
} else if (char === '?' && nextChar === '>') {
rc = XMLCharState.rPi
this.skipTokenChar = true
} else {
rc = XMLCharState.lPiValue
}
break
case XMLCharState.lPiValue:
if (char === '?' && nextChar === '>') {
rc = XMLCharState.rPi
this.skipTokenChar = true
}
break
case XMLCharState.rPi:
case XMLCharState.rPiNameOnly:
case XMLCharState.rSelfCt:
case XMLCharState.rSelfCtNoAtt:
if (this.skipTokenChar) {
this.skipTokenChar = false
} else {
rc = this.testChar(char, nextChar, false)
}
break
case XMLCharState.lComment:
if (this.commentCharCount === 0) {
// char === '-' we've already processed <!-
this.commentCharCount++
} else if (this.commentCharCount === 1) {
// if commendCharCount === 1 then we're just in the comment value
if (char === '-' && nextChar === '-') {
this.commentCharCount++
}
} else if (this.commentCharCount === 2) {
// we're on the second '-' at comment end
this.commentCharCount++
} else if (this.commentCharCount === 3) {
// we're expecting the '>' at the comment end
if (char === '>') {
rc = XMLCharState.rComment
}
// stop checking as '--' already encountered without '>'
this.commentCharCount = 4
}
break
case XMLCharState.lExclam:
// assume <![CDATA[
if (char === '[' && nextChar === 'C') {
this.cdataCharCount = 0
rc = XMLCharState.lCd
} else if (char === '-' && nextChar === '-') {
rc = XMLCharState.lComment
this.commentCharCount = 0
} else if (char === '>') {
this.dtdNesting--
if (this.dtdNesting < 0) {
this.dtdNesting = 0
rc = XMLCharState.rDtd
}
} else if (char === '<') {
this.dtdNesting++
}
// TODO: Handle internal DTD subset
break
case XMLCharState.lCd:
switch (this.cdataCharCount) {
case 0:
// [C of <![CDATA[ already checked
case 2:
// DA already checked
case 4:
// TA already checked
this.cdataCharCount++
break
case 1:
if (char === 'D' && nextChar === 'A') {
this.cdataCharCount++
} else {
rc = XMLCharState.init
}
break
case 3:
if (char === 'T' && nextChar === 'A') {
this.cdataCharCount++
} else {
rc = XMLCharState.init
}
break
case 5:
if (char === '[') {
this.cdataCharCount = 0
rc = XMLCharState.lCdataEnd
} else {
rc = XMLCharState.init
}
break
}
break
case XMLCharState.lCdataEnd:
case XMLCharState.awaitingRcdata:
if (char === ']' && nextChar === ']') {
this.cdataCharCount = 0
rc = XMLCharState.rCd
} else if (char === '{') {
if (nextChar === '{') {
rc = XMLCharState.escTvtCdata
} else {
rc = XMLCharState.tvtCdata
}
}
break
// otherwise continue awaiting ]]>
case XMLCharState.rCd:
if (this.cdataCharCount === 0) {
this.cdataCharCount++
} else if (char === '>') {
this.cdataCharCount = 0
rc = XMLCharState.rCdataEnd
} else {
// TODO: ]] not permited on its own in CDATA, show error
this.cdataCharCount = 0
rc = XMLCharState.init
}
break
case XMLCharState.lSt:
if (char === '>') {
// error for: '<>'
rc = XMLCharState.rSt
} else {
// TODO: check first char of element name is oK
rc = XMLCharState.lEn
}
break
// element name started
case XMLCharState.lEn:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
rc = XMLCharState.lsElementNameWs
} else if (char === '>') {
rc = XMLCharState.rStNoAtt
} else if (char === '/' && nextChar === '>') {
rc = XMLCharState.rSelfCtNoAtt
this.skipTokenChar = true
}
break
// whitespace after element name (or after att-value)
case XMLCharState.lsElementNameWs:
case XMLCharState.rSq:
case XMLCharState.rDq:
case XMLCharState.wsBeforeAttname:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
if (existing !== XMLCharState.lsElementNameWs) {
rc = XMLCharState.wsBeforeAttname
}
} else if (char === '>') {
rc = XMLCharState.rSt
} else if (char === '/' && nextChar === '>') {
rc = XMLCharState.rSelfCt
this.skipTokenChar = true
} else {
rc = XMLCharState.lAn
}
break
// attribute name started
case XMLCharState.lAn:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
rc = XMLCharState.wsAfterAttName
} else if (char === '=') {
rc = XMLCharState.lStEq
} else {
const charState = this.testAttNameChar(char, nextChar)
if (charState !== XMLCharState.lText) {
rc = charState
}
}
break
// whitespace after attribute name
case XMLCharState.wsAfterAttName:
if (char === '=') {
rc = XMLCharState.lStEq
} else if (!this.isWhitespace(isCurrentCharNewLine, char)) {
// TODO: force error - guessing intended end of start tag
rc = XMLCharState.syntaxError
}
break
// '=' char after attribute name or
// whitespace after attname and '=' char
case XMLCharState.lStEq:
case XMLCharState.lsEqWs:
if (this.isWhitespace(isCurrentCharNewLine, char)) {
rc = XMLCharState.lsEqWs
} else if (char === '"') {
rc = XMLCharState.lDq
} else if (char === "'") {
rc = XMLCharState.lSq
} else {
rc = XMLCharState.syntaxError
}
break
case XMLCharState.lDq:
if (char === '"') {
rc = XMLCharState.rDq
} else if (char === '{') {
if (nextChar === '{') {
rc = XMLCharState.escDqAvt
} else {
rc = XMLCharState.dqAvt
}
} else if (char === '&') {
rc = XMLCharState.lEntity
this.entityContext = EntityPosition.attrDq
} else {
const charState = this.testAttValueChar(char, nextChar)
if (charState !== XMLCharState.lText) {
rc = charState
}
}
break
case XMLCharState.lSq:
if (char === "'") {
rc = XMLCharState.rSq
} else if (char === '{') {
if (nextChar === '{') {
rc = XMLCharState.escSqAvt
} else {
rc = XMLCharState.sqAvt
}
} else if (char === '&') {
rc = XMLCharState.lEntity
this.entityContext = EntityPosition.attrSq
} else {
const charState = this.testAttValueChar(char, nextChar)
if (charState !== XMLCharState.lText) {
rc = charState
}
}
break
case XMLCharState.escDqAvt:
rc = XMLCharState.lDq
break
case XMLCharState.escSqAvt:
rc = XMLCharState.lSq
break
case XMLCharState.escTvt:
rc = XMLCharState.init
break
case XMLCharState.escTvtCdata:
rc = XMLCharState.awaitingRcdata
break
case XMLCharState.lEntity:
if (char === ';') {
rc = XMLCharState.rEntity
} else if (this.isWhitespace(isCurrentCharNewLine, char)) {
rc = this.testChar(char, nextChar, false)
}
break
case XMLCharState.lText:
rc = this.testChar(char, nextChar, true)
break
default:
// awaiting a new node
rc = this.testChar(char, nextChar, false)
}
return rc
}
private testAttNameChar(char: string, nextChar: string): XMLCharState {
let rc: XMLCharState
switch (char) {
case '<':
switch (nextChar) {
case '?':
rc = XMLCharState.lPi
break
case '!':
rc = XMLCharState.lExclam
break
case '/':
rc = XMLCharState.lCt
break
default:
rc = XMLCharState.lSt
}
break
case '{':
if (nextChar === '{') {
rc = XMLCharState.escTvt
} else {
rc = XMLCharState.tvt
}
break
case '&':
// TODO: check next char is not ';'
rc = XMLCharState.lEntity
this.entityContext = EntityPosition.text
break
case '/':
case '>':
rc = XMLCharState.syntaxError
break
default:
rc = XMLCharState.lText
break
}
return rc
}
private testAttValueChar(char: string, nextChar: string): XMLCharState {
let rc: XMLCharState
switch (char) {
case '<':
switch (nextChar) {
case '?':
rc = XMLCharState.lPi
break
case '!':
rc = XMLCharState.lExclam
break
case '/':
rc = XMLCharState.lCt
break
default:
rc = XMLCharState.lSt
}
break
case '/':
rc = nextChar === '>' ? XMLCharState.syntaxError : XMLCharState.lText
break
default:
rc = XMLCharState.lText
break
}
return rc
}
private testChar(
char: string,
nextChar: string,
isText: boolean
): XMLCharState {
let rc: XMLCharState
switch (char) {
case ' ':
case '\t':
case '\r':
if (isText) {
rc = XMLCharState.lText
} else {
rc = XMLCharState.lWs
}
break
case '\n':
rc = XMLCharState.lWs
break
case '<':
switch (nextChar) {
case '?':
rc = XMLCharState.lPi
break
case '!':
rc = XMLCharState.lExclam
break
case '/':
rc = XMLCharState.lCt
break
default:
rc = XMLCharState.lSt
}
break
case '{':
if (nextChar === '{') {
rc = XMLCharState.escTvt
} else {
rc = XMLCharState.tvt
}
break
case '&':
// TODO: check next char is not ';'
rc = XMLCharState.lEntity
this.entityContext = EntityPosition.text
break
default:
rc = XMLCharState.lText
break
}
return rc
}
public analyse(xsl: string, keepNameTests?: boolean): BaseToken[] {
if (this.timerOn) {
console.time('xslLexer.analyse')
}
this.globalInstructionData.length = 0
this.globalModeData.length = 0
this.lineNumber = 0
this.lineCharCount = -1
this.charCount = -1
if (keepNameTests) {
this.attributeNameTests = []
this.elementNameTests = []
} else {
this.attributeNameTests = undefined
this.elementNameTests = undefined
}
let currentState: XMLCharState = XMLCharState.init
let currentChar: string = ''
let tokenChars: string[] = []
let result: BaseToken[] = []
let attName: string = ''
let avtExit = false
let xpLexer: XPathLexer = new XPathLexer()
xpLexer.elementNameTests = this.elementNameTests
xpLexer.attributeNameTests = this.attributeNameTests
xpLexer.documentText = xsl
xpLexer.documentTokens = result
xpLexer.debug = this.debug
xpLexer.timerOn = this.timerOn
let xslLength = xsl.length
let storeToken = false
let isNativeElement = false
let tagGlobalInstructionType = GlobalInstructionType.Unknown
let tagInstructionNameAdded = false
let tagMatchToken: BaseToken | null = null
let contextGlobalInstructionType = GlobalInstructionType.Unknown
let isGlobalVersion = false
let isXPathAttribute = false
let isExpandTextAttribute = false
let isGlobalInstructionName = false
let isGlobalInstructionMode = false
let isGlobalParameterName = false
let isGlobalUsePackageVersion = false
let isGlobalInstructionMatch = false
let expandTextValue: boolean | null = false
let xmlElementStack: XmlElement[] = []
let tokenStartChar = -1
let tokenStartLine = -1
let attributeNameTokenAdded = false
let collectParamName = false
let xpathEnded = false
if (tokenStartLine) {
}
if (this.debug) {
console.log('xsl:\n' + xsl)
}
if (xslLength === 0) {
return []
}
while (this.charCount < xslLength) {
this.charCount++
this.lineCharCount++
let nextState: XMLCharState = XMLCharState.init
let nextChar: string = xsl.charAt(this.charCount)
const isLastChar: boolean = this.charCount === xslLength
const resultLengthAtLastChar = isLastChar ? result.length : -1
if (currentChar) {
let isCurrentCharNewLIne = currentChar === '\n'
nextState = this.calcNewState(
isCurrentCharNewLIne,
currentChar,
nextChar,
currentState
)
if (nextState === currentState) {
if (isCurrentCharNewLIne || isLastChar) {
// we must split multi-line tokens:
let addToken: XSLTokenLevelState | null = null
switch (nextState) {
case XMLCharState.lPiValue:
addToken = XSLTokenLevelState.processingInstrValue
this.addNewTokenToResult(
tokenStartChar,
addToken,
result,
nextState
)
break
case XMLCharState.lComment:
addToken = XSLTokenLevelState.xmlComment
tokenStartChar =
tokenStartChar === 0 ? tokenStartChar : tokenStartChar - 2
break
case XMLCharState.lDq:
case XMLCharState.lSq:
addToken = XSLTokenLevelState.attributeValue
break
case XMLCharState.lExclam:
addToken = XSLTokenLevelState.dtd
break
}
if (addToken !== null) {
this.addNewTokenToResult(
tokenStartChar,
addToken,
result,
nextState
)
tokenStartChar = 0
} else if (isLastChar) {
let alreadyDone =
result.length > 0 &&
result[result.length - 1].startCharacter === tokenStartChar
if (!alreadyDone) {
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.xmlText,
result,
nextState
)
}
}
} else if (storeToken) {
tokenChars.push(currentChar)
}
} else {
if (currentState === XMLCharState.lText) {
if (nextState === XMLCharState.escTvt) {
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - tokenStartChar,
XSLTokenLevelState.xmlText,
result,
currentState
)
} else {
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - 1 - tokenStartChar,
XSLTokenLevelState.xmlText,
result,
currentState
)
}
} else if (currentState === XMLCharState.escTvt) {
this.addCharTokenToResult(
tokenStartChar,
2,
XSLTokenLevelState.xmlText,
result,
currentState
)
}
if (
currentState === XMLCharState.lEntity &&
nextState !== XMLCharState.rEntity
) {
// recover from syntax error:
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.entityRef,
result,
nextState
)
switch (this.entityContext) {
case EntityPosition.text:
nextState = XMLCharState.init
break
case EntityPosition.attrSq:
nextState = XMLCharState.lSq
break
case EntityPosition.attrDq:
nextState = XMLCharState.lDq
break
}
}
switch (nextState) {
case XMLCharState.lSt:
this.addCharTokenToResult(
this.lineCharCount - 1,
1,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.lCtName:
case XMLCharState.lEn:
expandTextValue = null
if (tokenChars.length < 5) {
tokenChars.push(currentChar)
storeToken = true
} else {
storeToken = false
}
break
case XMLCharState.rStNoAtt:
expandTextValue = this.addToElementStack(
expandTextValue,
xmlElementStack
)
// cascade, so no-break intentional
case XMLCharState.lsElementNameWs:
case XMLCharState.rSelfCtNoAtt:
case XMLCharState.rCt:
let isCloseTag = nextState === XMLCharState.rCt
let isRootChildStartTag =
!isCloseTag && xmlElementStack.length === 1
let elementProperties = this.getElementProperties(
tokenChars,
isRootChildStartTag
)
isNativeElement = elementProperties.isNative
tagGlobalInstructionType = elementProperties.instructionType
tagInstructionNameAdded = false
tagMatchToken = null
collectParamName = false
if (xmlElementStack.length === 0 && tokenChars.length > 5) {
tagGlobalInstructionType = GlobalInstructionType.RootXSLT
}
if (xmlElementStack.length === 1) {
contextGlobalInstructionType = tagGlobalInstructionType
} else if (
xmlElementStack.length === 2 &&
(contextGlobalInstructionType ===
GlobalInstructionType.Function ||
contextGlobalInstructionType ===
GlobalInstructionType.Template) &&
isNativeElement &&
elementProperties.nativeName === 'param'
) {
collectParamName = true
}
if (isCloseTag) {
if (xmlElementStack.length > 0) {
xmlElementStack.pop()
}
}
storeToken = false
tokenChars = []
let newTokenType = isNativeElement
? XSLTokenLevelState.xslElementName
: XSLTokenLevelState.elementName
this.addNewTokenToResult(
tokenStartChar,
newTokenType,
result,
nextState
)
if (nextState !== XMLCharState.lsElementNameWs) {
let punctuationLength =
nextState === XMLCharState.rCt ||
nextState === XMLCharState.rStNoAtt
? 1
: 2
this.addCharTokenToResult(
this.lineCharCount - 1,
punctuationLength,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
}
break
case XMLCharState.rDtd:
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - tokenStartChar,
XSLTokenLevelState.dtdEnd,
result,
currentState
)
break
case XMLCharState.rPiName:
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.processingInstrName,
result,
nextState
)
break
case XMLCharState.rPi:
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.processingInstrValue,
result,
currentState
)
this.addCharTokenToResult(
this.lineCharCount - 1,
2,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.rPiNameOnly:
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.processingInstrName,
result,
currentState
)
this.addCharTokenToResult(
this.lineCharCount - 1,
2,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.rComment:
let startChar = tokenStartChar > 0 ? tokenStartChar - 2 : 0
this.addNewTokenToResult(
startChar,
XSLTokenLevelState.xmlComment,
result,
nextState
)
break
case XMLCharState.wsAfterAttName:
case XMLCharState.syntaxError:
storeToken = false
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.attributeName,
result,
nextState
)
attributeNameTokenAdded = true
break
case XMLCharState.lAn:
tokenChars.push(currentChar)
storeToken = true
attributeNameTokenAdded = false
break
case XMLCharState.lStEq:
let isXMLNSattribute = false
isGlobalInstructionName = false
isGlobalInstructionMode = false
isGlobalParameterName = false
isGlobalInstructionMatch = false
isGlobalUsePackageVersion = false
isGlobalVersion = false
attName = tokenChars.join('')
let attributeNameToken = XSLTokenLevelState.attributeName
if (isNativeElement) {
// if (attName === 'as') {
// isXPathAttribute = true;
// }
if (attName === 'saxon:options') {
isXPathAttribute = true
} else if (this.genericTvtAttributes.indexOf(attName) > -1) {
isXPathAttribute = false
isExpandTextAttribute = true
} else if (attName.startsWith('xmlns')) {
isExpandTextAttribute = false
isXMLNSattribute = true
attributeNameToken = XSLTokenLevelState.xmlnsName
} else if (
tagGlobalInstructionType === GlobalInstructionType.RootXSLT &&
attName === 'version'
) {
isGlobalVersion = true
} else if (
tagGlobalInstructionType !== GlobalInstructionType.Unknown &&
attName === 'name'
) {
isExpandTextAttribute = false
isGlobalInstructionName = true
} else if (
(tagGlobalInstructionType ===
GlobalInstructionType.Template &&
attName === 'mode') ||
(tagGlobalInstructionType === GlobalInstructionType.Mode &&
attName === 'name')
) {
isExpandTextAttribute = false
isGlobalInstructionMode = true
} else if (
tagGlobalInstructionType === GlobalInstructionType.Template &&
attName === 'match'
) {
isExpandTextAttribute = false
isGlobalInstructionMatch = true
isXPathAttribute = true
} else if (collectParamName && attName === 'name') {
isGlobalParameterName = true
} else if (
contextGlobalInstructionType ===
GlobalInstructionType.UsePackage &&
attName === 'package-version'
) {
isExpandTextAttribute = false
isGlobalUsePackageVersion = true
} else {
isExpandTextAttribute = false
isXPathAttribute = this.isExpressionAtt(attName)
}
} else {
if (this.nativeTvtAttributes.indexOf(attName) > -1) {
isExpandTextAttribute = true
} else if (attName.startsWith('xmlns')) {
isExpandTextAttribute = false
isXMLNSattribute = true
attributeNameToken = XSLTokenLevelState.xmlnsName
} else {
isExpandTextAttribute = false
}
}
if (!attributeNameTokenAdded) {
this.addNewTokenToResult(
tokenStartChar,
attributeNameToken,
result,
nextState
)
} else if (isXMLNSattribute) {
result[result.length - 1].tokenType = attributeNameToken
}
this.addCharTokenToResult(
this.lineCharCount - 1,
1,
XSLTokenLevelState.attributeEquals,
result,
nextState
)
tokenChars = []
storeToken = false
break
case XMLCharState.rSt:
if (
tagGlobalInstructionType === GlobalInstructionType.Template &&
!tagInstructionNameAdded &&
tagMatchToken
) {
this.globalInstructionData.push({
type: GlobalInstructionType.TemplateMatch,
name: `${tagMatchToken.value}#${this.globalInstructionData.length}`,
token: tagMatchToken,
idNumber: 0,
})
}
expandTextValue = this.addToElementStack(
expandTextValue,
xmlElementStack
)
this.addCharTokenToResult(
this.lineCharCount - 1,
1,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
storeToken = false
tokenChars = []
break
case XMLCharState.lCt:
case XMLCharState.lPi:
this.addCharTokenToResult(
this.lineCharCount - 1,
2,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.rSelfCt:
this.addCharTokenToResult(
this.lineCharCount - 1,
2,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.lCt2:
break
case XMLCharState.rSq:
case XMLCharState.rDq:
case XMLCharState.escDqAvt:
case XMLCharState.escSqAvt:
if (isExpandTextAttribute) {
let attValue = tokenChars.join('')
expandTextValue =
attValue === 'yes' || attValue === 'true' || attValue === '1'
}
if (xpathEnded) {
tokenStartChar++
xpathEnded = false
}
let newToken = this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.attributeValue,
result,
nextState
)
if (isGlobalInstructionName || isGlobalInstructionMode) {
let attValue = tokenChars.join('')
let newTokenCopy = Object.assign({}, newToken)
let globalType = isGlobalInstructionMode
? GlobalInstructionType.Mode
: tagGlobalInstructionType
let targetGlobal
if (isGlobalInstructionMode) {
targetGlobal = this.globalModeData
} else {
targetGlobal = this.globalInstructionData
tagInstructionNameAdded = true
}
const idNumber =
globalType === GlobalInstructionType.Variable
? result.length
: 0
targetGlobal.push({
type: globalType,
name: attValue,
token: newTokenCopy,
idNumber: idNumber,
})
} else if (isGlobalParameterName) {
let attValue = tokenChars.join('')
if (this.globalInstructionData.length > 0) {
let gd =
this.globalInstructionData[
this.globalInstructionData.length - 1
]
if (gd.memberNames) {
if (gd.memberNames.indexOf(attValue) > -1) {
newToken['error'] = ErrorType.DuplicateParameterName
newToken.value = attValue
}
gd.memberNames.push(attValue)
gd.memberTokens?.push({ ...newToken })
} else {
gd['memberNames'] = [attValue]
gd['memberTokens'] = [{ ...newToken }]
}
gd.idNumber++
}
} else if (isGlobalUsePackageVersion) {
let attValue = tokenChars.join('')
if (this.globalInstructionData.length > 0) {
let gd =
this.globalInstructionData[
this.globalInstructionData.length - 1
]
gd['version'] = attValue
}
} else if (isGlobalInstructionMatch) {
let attValue = tokenChars.join('')
tagMatchToken = newToken
tagMatchToken.value = attValue
} else if (isGlobalVersion) {
isGlobalVersion = false
}
tokenChars = []
storeToken = false
break
case XMLCharState.lSq:
case XMLCharState.lDq:
if (
contextGlobalInstructionType ===
GlobalInstructionType.Function ||
contextGlobalInstructionType ===
GlobalInstructionType.Template ||
contextGlobalInstructionType ===
GlobalInstructionType.UsePackage ||
tagGlobalInstructionType === GlobalInstructionType.RootXSLT
) {
storeToken = true
}
if (
isExpandTextAttribute ||
isGlobalInstructionName ||
isGlobalInstructionMode
) {
storeToken = true
} else if (isXPathAttribute) {
this.addCharTokenToResult(
this.lineCharCount - 1,
1,
XSLTokenLevelState.attributeValue,
result,
nextState
)
let p: LexPosition = {
line: this.lineNumber,
startCharacter: this.lineCharCount,
documentOffset: this.charCount,
}
let exit: ExitCondition
if (nextState === XMLCharState.lSq) {
exit = ExitCondition.SingleQuote
} else {
exit = ExitCondition.DoubleQuote
}
xpLexer.analyse('', exit, p)
this.updateNames(result)
// need to process right double-quote/single-quote
this.lineNumber = p.line
let newCharCount = p.documentOffset - 1
if (newCharCount > this.charCount) {
this.charCount = newCharCount
}
xpathEnded = true
this.lineCharCount = p.startCharacter
nextChar = xsl.charAt(this.charCount)
isXPathAttribute = false
}
break
case XMLCharState.sqAvt:
case XMLCharState.dqAvt:
let exit
if (isNativeElement) {
if ((exit = attName.startsWith('_'))) {
exit = ExitCondition.CurlyBrace
} else {
exit = this.isAvtAtt(attName)
? ExitCondition.CurlyBrace
: ExitCondition.None
}
} else if (this.nonNativeAvts) {
exit = ExitCondition.CurlyBrace
} else {
exit = ExitCondition.None
}
if (exit !== ExitCondition.None) {
this.addNewTokenToResult(
tokenStartChar,
XSLTokenLevelState.attributeValue,
result,
nextState
)
let p: LexPosition = {
line: this.lineNumber,
startCharacter: this.lineCharCount,
documentOffset: this.charCount,
}
xpLexer.analyse('', exit, p)
this.updateNames(result)
// need to process right double-quote
this.lineNumber = p.line
let newCharCount = p.documentOffset - 1
if (newCharCount > this.charCount) {
this.charCount = newCharCount
}
// fix issue with last char of xpath
this.lineCharCount = p.startCharacter
nextChar = xsl.charAt(this.charCount)
avtExit = true
}
nextState =
nextState === XMLCharState.sqAvt
? XMLCharState.lSq
: XMLCharState.lDq
break
case XMLCharState.tvt:
this.addCharTokenToResult(
this.lineCharCount - 1,
1,
XSLTokenLevelState.xmlText,
result,
currentState
)
case XMLCharState.tvtCdata:
let useTvt =
xmlElementStack.length > 0 &&
xmlElementStack[xmlElementStack.length - 1].expandText
if (useTvt) {
let p: LexPosition = {
line: this.lineNumber,
startCharacter: this.lineCharCount,
documentOffset: this.charCount,
}
xpLexer.analyse('', ExitCondition.CurlyBrace, p)
this.updateNames(result)
// need to process right double-quote
this.lineNumber = p.line
let newCharCount = p.documentOffset - 1
if (newCharCount > this.charCount) {
this.charCount = newCharCount
}
this.lineCharCount = p.startCharacter
nextChar = xsl.charAt(this.charCount)
if (nextState === XMLCharState.tvtCdata) {
nextState = XMLCharState.awaitingRcdata
} else {
nextState = XMLCharState.init
}
} else if (nextState === XMLCharState.tvtCdata) {
nextState = XMLCharState.awaitingRcdata
}
break
case XMLCharState.lEntity:
if (this.entityContext !== EntityPosition.text) {
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - 1 - tokenStartChar,
XSLTokenLevelState.attributeValue,
result,
nextState
)
}
break
case XMLCharState.rEntity:
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - tokenStartChar,
XSLTokenLevelState.entityRef,
result,
nextState
)
switch (this.entityContext) {
case EntityPosition.text:
nextState = XMLCharState.init
break
case EntityPosition.attrSq:
nextState = XMLCharState.lSq
avtExit = true
break
case EntityPosition.attrDq:
nextState = XMLCharState.lDq
avtExit = true
break
}
break
case XMLCharState.lCdataEnd:
this.addCharTokenToResult(
tokenStartChar - 2,
9,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
case XMLCharState.rCdataEnd:
this.addCharTokenToResult(
tokenStartChar,
3,
XSLTokenLevelState.xmlPunctuation,
result,
nextState
)
break
}
tokenStartChar = this.lineCharCount > 0 ? this.lineCharCount - 1 : 0
if (avtExit) {
avtExit = false
tokenStartChar++
}
tokenStartLine = this.lineNumber
} // else ends
if (isCurrentCharNewLIne) {
tokenStartChar = 0
tokenStartLine = 0
this.lineNumber++
this.lineCharCount = 0
}
currentState = nextState
}
currentChar = nextChar
if (
isLastChar &&
resultLengthAtLastChar === result.length &&
!(nextState === XMLCharState.lWs || nextState === XMLCharState.lsEqWs)
) {
let alreadyDone =
result.length > 0 &&
result[result.length - 1].startCharacter === tokenStartChar
if (!alreadyDone) {
this.addCharTokenToResult(
tokenStartChar,
this.lineCharCount - tokenStartChar,
XSLTokenLevelState.xmlText,
result,
currentState
)
}
}
}
if (this.timerOn) {
console.timeEnd('xslLexer.analyse')
}
this.globalInstructionData = this.globalInstructionData.concat(
this.globalModeData
)
return result
}
private updateNames(result: BaseToken[]) {
if (this.elementNameTests && this.attributeNameTests && result.length > 0) {
let prevToken = result[result.length - 1]
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)
}
}
}
}
private addNewTokenToResult(
tokenStartChar: number,
newTokenType: XSLTokenLevelState,
result: BaseToken[],
charLevelState: XMLCharState
): BaseToken {
let tokenLength = this.lineCharCount - 1 - tokenStartChar
let localTokenStartChar = tokenStartChar
if (
newTokenType === XSLTokenLevelState.xmlComment ||
newTokenType === XSLTokenLevelState.attributeValue
) {
tokenLength++
}
let tkn: BaseToken = {
line: this.lineNumber,
length: tokenLength,
startCharacter: localTokenStartChar,
value: '',
tokenType: newTokenType + XslLexer.xpathLegendLength,
}
if (this.provideCharLevelState) {
tkn['charType'] = charLevelState
}
result.push(tkn)
return tkn
}
private addCharTokenToResult(
tokenStartChar: number,
tokenLength: number,
newTokenType: XSLTokenLevelState,
result: BaseToken[],
charLevelState: XMLCharState
) {
let tkn: BaseToken = {
line: this.lineNumber,
length: tokenLength,
startCharacter: tokenStartChar,
value: '',
tokenType: newTokenType + XslLexer.xpathLegendLength,
}
if (this.provideCharLevelState) {
tkn['charType'] = charLevelState
}
result.push(tkn)
}
private addToElementStack(
expandTextValue: boolean | null,
xmlElementStack: XmlElement[]
) {
if (expandTextValue === null) {
if (xmlElementStack.length > 0) {
expandTextValue = xmlElementStack[xmlElementStack.length - 1].expandText
} else {
expandTextValue = false
}
}
xmlElementStack.push({ expandText: expandTextValue })
return expandTextValue
}
protected getElementProperties(
tokenChars: string[],
isRootChild: boolean
): ElementProperties {
let elementName = tokenChars.join('')
let isNative =
(this.nativePrefixLength === 0 && elementName.indexOf(':') === -1) ||
(tokenChars.length > this.nativePrefixLength &&
elementName.startsWith(this.languageConfiguration.nativePrefix + ':'))
let instructionType = GlobalInstructionType.Unknown
let nativeName = ''
if (isNative) {
nativeName = elementName.substring(this.nativePrefixLength)
if (isRootChild) {
switch (nativeName) {
case 'variable':
instructionType = GlobalInstructionType.Variable
break
case 'param':
instructionType = GlobalInstructionType.Parameter
break
case 'function':
instructionType = GlobalInstructionType.Function
break
case 'template':
instructionType = GlobalInstructionType.Template
break
case 'key':
instructionType = GlobalInstructionType.Key
break
case 'import':
instructionType = GlobalInstructionType.Import
break
case 'include':
instructionType = GlobalInstructionType.Include
break
case 'accumulator':
instructionType = GlobalInstructionType.Accumulator
break
case 'mode':
instructionType = GlobalInstructionType.Mode
break
case 'attribute-set':
instructionType = GlobalInstructionType.AttributeSet
break
case 'use-package':
instructionType = GlobalInstructionType.UsePackage
break
}
}
}
return {
isNative: isNative,
instructionType: instructionType,
nativeName: nativeName,
}
}
// class ends
}
export interface ElementProperties {
isNative: boolean
instructionType: GlobalInstructionType
nativeName: string
}
export enum GlobalInstructionType {
Variable,
Parameter,
Function,
Mode,
Accumulator,
AttributeSet,
Key,
Template,
TemplateMatch,
Include,
Import,
UsePackage,
RootXMLNS,
RootXSLT,
Unknown,
}
export interface InnerLexerResult {
charCount: number
lineNumber: number
tokens: BaseToken[]
}