UNPKG

ui5plugin-linter

Version:
389 lines (388 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.XMLFormatter = void 0; class XMLFormatter { constructor(parser, options) { this._bShouldXmlFormatterTagEndByNewline = true; this._bShouldXmlFormatterTagSpaceBeforeSelfClose = true; this._parser = parser; this._bShouldXmlFormatterTagEndByNewline = options?.shouldXmlFormatterTagEndByNewline ?? true; this._bShouldXmlFormatterTagSpaceBeforeSelfClose = options?.shouldXmlFormatterTagSpaceBeforeSelfClose ?? true; this._indentation = options?.indentation ?? "\t"; } formatDocument(document) { const XMLFile = this._parser.textDocumentTransformer.toXMLFile(document, true); const allTags = XMLFile && this._getAllTags(XMLFile); if (!XMLFile || !allTags || allTags.length === 0) { return; } const documentNewline = document.getText().match(/\r?\n/)?.[0] ?? "\n"; const documentNewlineEnding = document .getText() .slice(-2) .match(/\r?\n$/)?.[0] ?? ""; let indentationLevel = 0; const formattedTags = allTags .map(currentTag => { const isComment = currentTag.text.startsWith("<!--"); const isDocTypeTag = currentTag.text.startsWith("<!"); if (isComment || isDocTypeTag) { const indentation = this._getIndentation(indentationLevel); return `${indentation}${currentTag.text}`; } else { let formattedTag; ({ formattedTag, indentationLevel } = this._formatNonCommentTag(currentTag, indentationLevel)); return formattedTag; } }) .reduce(this._removeUnnecessaryTags.bind(this), []); return formattedTags.join(documentNewline) + documentNewlineEnding; } _removeUnnecessaryTags(accumulator, currentTag) { //<Button></Button> -> <Button/> const lastTagInAccumulator = accumulator[accumulator.length - 1]; const lastTagIsAnOpener = lastTagInAccumulator && !lastTagInAccumulator.trim().startsWith("</") && !lastTagInAccumulator.trim().endsWith("/>"); if (lastTagIsAnOpener) { const lastTagName = this._parser.xmlParser.getClassNameFromTag(lastTagInAccumulator.trim()); const currentTagName = this._parser.xmlParser.getClassNameFromTag(currentTag.trim()); const bothTagsAreSameClass = lastTagName && currentTagName && lastTagName === currentTagName; const previousTagIsAClass = lastTagName && lastTagName[0] === lastTagName[0].toUpperCase(); const currentTagIsClosure = currentTag.trim().startsWith("</"); const lastTagIsNotSelfClosed = !lastTagInAccumulator.trim().endsWith("/>"); const nextTagClosesCurrentOne = previousTagIsAClass && bothTagsAreSameClass && currentTagIsClosure && lastTagIsNotSelfClosed; if (nextTagClosesCurrentOne) { accumulator[accumulator.length - 1] = `${lastTagInAccumulator.substring(0, lastTagInAccumulator.length - 1)}/>`; } else { accumulator.push(currentTag); } } else { accumulator.push(currentTag); } return accumulator; } _formatNonCommentTag(currentTag, indentationLevel) { const tagName = this._getTagName(currentTag.text); const tagAttributes = this._getTagAttributes(currentTag.text).map(tag => tag.toString()); let endSubstraction = 1; if (currentTag.text.endsWith("/>")) { endSubstraction = 2; } const tagEnd = currentTag.text.substring(currentTag.text.length - endSubstraction, currentTag.text.length); let beginAddition = 1; if (currentTag.text.startsWith("</")) { beginAddition = 2; } const tagBegin = currentTag.text.substring(0, beginAddition); indentationLevel = this._modifyIndentationLevel(currentTag, indentationLevel, true); let indentation = this._getIndentation(indentationLevel); let formattedTag = `${indentation}${tagBegin}${tagName}\n`; if (tagAttributes.length === 1) { formattedTag = formattedTag.trimEnd(); } formattedTag += tagAttributes.reduce((accumulator, tagAttribute) => { const tagData = this._parser.xmlParser.getAttributeNameAndValue(tagAttribute); const attributeValueIndentation = tagAttributes.length === 1 ? indentation : indentation + this._indentation; const formattedAttributeValue = this._formatAttributeValue(tagData.attributeValue, attributeValueIndentation); accumulator += `${indentation}${this._indentation}${tagData.attributeName}=${formattedAttributeValue}\n`; if (tagAttributes.length === 1) { accumulator = ` ${accumulator.trimStart()}`; } return accumulator; }, ""); if (tagAttributes.length <= 1 || !this._bShouldXmlFormatterTagEndByNewline) { formattedTag = formattedTag.trimEnd(); indentation = ""; if (tagEnd === "/>" && this._bShouldXmlFormatterTagSpaceBeforeSelfClose) { indentation += " "; } } formattedTag += `${indentation}${tagEnd}`; indentationLevel = this._modifyIndentationLevel(currentTag, indentationLevel, false); return { formattedTag, indentationLevel }; } _formatAttributeValue(attributeValue, indentation) { let formattedValue = ""; if (!attributeValue.startsWith("\\")) { let i = 0; while (i < attributeValue.length) { const currentChar = attributeValue[i]; if (this._charIsInString(i, attributeValue)) { formattedValue += currentChar; } else if (currentChar === "(") { const nextChar = attributeValue[i + 1]; if (nextChar !== "{") { indentation += this._indentation; } const nextLine = nextChar === "(" ? `\n${indentation}${this._indentation}` : ""; formattedValue += `${currentChar}${nextLine}`; } else if (currentChar === ")") { const lastFormattedValueChar = formattedValue[formattedValue.length - 1]; indentation = indentation.substring(0, indentation.length - 1); const nextChar = attributeValue[i + 1]; const nextLine = !["\n", "\r", " ", undefined].includes(nextChar) ? `\n${indentation}${this._indentation}` : ""; formattedValue = lastFormattedValueChar === `${this._indentation}` ? formattedValue.substring(0, formattedValue.length - 1) : formattedValue; formattedValue += `${currentChar}${nextLine}`; } else if (currentChar === "{") { const positionEnd = this._getPositionOfObjectEnd(attributeValue, i); const currentBindingValue = attributeValue.substring(i, positionEnd); try { const evaluatedValue = eval(`(${currentBindingValue})`); if (typeof evaluatedValue === "object") { const necessaryIndentation = this._getCurvyBracketsCount(attributeValue, i + 1) === 1 ? indentation : indentation + this._indentation; const formattedBinding = this._formatAttributeObject(evaluatedValue, necessaryIndentation); formattedValue += formattedBinding; } i = positionEnd - 1; } catch (error) { formattedValue += currentChar; } } else if (currentChar === "\n") { const positionEnd = this._getPositionOfIndentationEnd(attributeValue, i); const necessaryIndentation = attributeValue[positionEnd] === "}" ? indentation : indentation + this._indentation; formattedValue += "\n" + necessaryIndentation; i = positionEnd - 1; } else { formattedValue += currentChar; } i++; } formattedValue = `"${formattedValue}"`; } else { if (attributeValue.includes("'")) { formattedValue = `"${attributeValue}"`; } else { formattedValue = `'${attributeValue}'`; } } return formattedValue; } _charIsInString(index, attributeValue) { let i = 0; let quotesQuantity = 0; while (i < index) { if (attributeValue[i] === "'") quotesQuantity++; i++; } return quotesQuantity % 2 === 1; } _getCurvyBracketsCount(attributeValue, positionAt) { let curvedBracketsCount = 0; let i = 0; while (i < attributeValue.length && i < positionAt) { if (attributeValue[i] === "{") { curvedBracketsCount++; } else if (attributeValue[i] === "}") { curvedBracketsCount--; } i++; } return curvedBracketsCount; } _getPositionOfObjectEnd(attributeValue, i) { let curvedBracketsCount = 1; i++; while (i < attributeValue.length && curvedBracketsCount !== 0) { if (attributeValue[i] === "{") { curvedBracketsCount++; } else if (attributeValue[i] === "}") { curvedBracketsCount--; } i++; } return i; } _getPositionOfIndentationEnd(attributeValue, i) { i++; while (i < attributeValue.length && /\s/.test(attributeValue[i])) { i++; } return i; } _formatAttributeObject(anyObject, indentation) { let formattedAttribute = "{\n"; const keys = Object.keys(anyObject); keys.forEach(key => { const value = anyObject[key]; formattedAttribute += `${indentation}${this._indentation}${key}: `; formattedAttribute += this._formatAttributeValuePart(value, indentation); const isLastKey = keys.indexOf(key) === keys.length - 1; if (!isLastKey) { formattedAttribute += ","; } formattedAttribute += "\n"; }); formattedAttribute += `${indentation}}`; return formattedAttribute; } _formatAttributeValuePart(value, indentation) { let formattedAttribute = ""; if (Array.isArray(value)) { const arrayString = "[" + value.map(innerValue => `${this._formatAttributeValuePart(innerValue, indentation)}`).join(", ") + "]"; if (arrayString.length > 80 && !value.every(innerValue => typeof innerValue === "object")) { formattedAttribute += `[\n${indentation}${this._indentation.repeat(2)}` + value .map(innerValue => this._formatAttributeValuePart(innerValue, indentation + this._indentation)) .join(`,\n${indentation}${this._indentation.repeat(2)}`) + `\n${indentation}${this._indentation}]`; } else { formattedAttribute = arrayString; } } else if (typeof value === "object") { formattedAttribute += `${this._formatAttributeObject(value, indentation + this._indentation)}`; } else if (typeof value === "string") { formattedAttribute += `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`; } else if (typeof value === "function") { throw new Error("Parsing error"); } else { formattedAttribute += `${value}`; } return formattedAttribute; } _modifyIndentationLevel(currentTag, indentationLevel, beforeTagGeneration) { if (beforeTagGeneration && currentTag.text.startsWith("</")) { indentationLevel--; } else if (!beforeTagGeneration && currentTag.text.startsWith("<") && !currentTag.text.endsWith("/>") && !currentTag.text.startsWith("</")) { indentationLevel++; } return indentationLevel; } _getIndentation(indentationLevel) { const indentationChar = this._indentation; let indentation = ""; for (let i = 0; i < indentationLevel; i++) { indentation += indentationChar; } return indentation; } _getTagName(tag) { let i = 1; //first char is "<", that's why we start with second char while (!tag[i].match(/(\s|>|\n)/) && i < tag.length) { i++; } tag = tag.substring(1, i); if (tag.startsWith("/")) { tag = tag.substring(1, tag.length); } if (tag.endsWith("/")) { tag = tag.substring(0, tag.length - 1); } return tag; } _getTagAttributes(tag) { const tagAttributes = tag.match(/((?<=\s)(\w|:|\.)*(\s?)=(\s?)"(\s|.)*?")|((?<=\s)(\w|:|\.)*(\s?)=(\s?)'(\s|.)*?')/g) || []; return tagAttributes; } _getAllTags(document) { let i = 0; const tags = []; const allStringsAreClosed = this._getIfAllStringsAreClosed(document); if (allStringsAreClosed) { while (i < document.content.length) { const possiblyDocType = document.content.substring(i, i + 9).toLowerCase(); const isDocType = possiblyDocType === "<!doctype"; const thisIsTagEnd = document.content[i] === ">" && !this._parser.xmlParser.getIfPositionIsInString(document, i) && (this._parser.xmlParser.getIfPositionIsNotInComments(document, i) || document.content.substring(i - 2, i + 1) === "-->"); if (thisIsTagEnd) { const indexOfTagBegining = this._getTagBeginingIndex(document, i); tags.push({ text: document.content.substring(indexOfTagBegining, i + 1), positionBegin: indexOfTagBegining, positionEnd: i }); } else if (isDocType) { const doctypeTag = this._processDocType(document, i); tags.push(doctypeTag); i += doctypeTag.text.length; } i++; } } return tags; } _processDocType(document, i) { const doctypeBeginIndex = i; let doctypeEndIndex = i; let tagOpeningCount = 0; let tagClosingCount = 0; while (doctypeEndIndex === doctypeBeginIndex && i < document.content.length) { if (!this._parser.xmlParser.getIfPositionIsInString(document, i) && this._parser.xmlParser.getIfPositionIsNotInComments(document, i)) { if (document.content[i] === "<") { tagOpeningCount++; } else if (document.content[i] === ">") { tagClosingCount++; } if (tagOpeningCount === tagClosingCount) { doctypeEndIndex = i + 1; } } i++; } return { text: document.content.substring(doctypeBeginIndex, doctypeEndIndex), positionBegin: doctypeBeginIndex, positionEnd: doctypeEndIndex }; } _getIfAllStringsAreClosed(document) { return this._parser.xmlParser.getStringPositionMapping(document).areAllStringsClosed; } _getTagBeginingIndex(document, position) { let i = position; let shouldStop = i < 0; let isThisTagBegining = document.content[i] === "<" && (this._parser.xmlParser.getIfPositionIsNotInComments(document, i) || document.content.substring(i, i + 4) === "<!--"); shouldStop || (shouldStop = isThisTagBegining); while (!shouldStop) { i--; shouldStop = i < 0; isThisTagBegining = document.content[i] === "<" && (this._parser.xmlParser.getIfPositionIsNotInComments(document, i) || document.content.substring(i, i + 4) === "<!--"); shouldStop || (shouldStop = isThisTagBegining); } return i; } } exports.XMLFormatter = XMLFormatter;