| /*--------------------------------------------------------------------------------------------- |
| * 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[] |
| } |