diff --git a/src/formatting/formatters/v2-xml-formatter.ts b/src/formatting/formatters/v2-xml-formatter.ts index ab60c13..f7934d3 100644 --- a/src/formatting/formatters/v2-xml-formatter.ts +++ b/src/formatting/formatters/v2-xml-formatter.ts @@ -30,10 +30,38 @@ export class V2XmlFormatter implements XmlFormatter { let output = ""; let indentLevel = 0; - let location = Location.Text; - let lastNonTextLocation = Location.Text; // hah let attributeQuote = ""; let lineBreakSpree = false; + let lastWordCharacter: string | undefined; + let inMixedContent = false; + + const locationHistory: Location[] = [Location.Text]; + + function isLastNonTextLocation(loc: Location): boolean { + for (let i = (locationHistory.length - 1); i >= 0; i--) { + if (locationHistory[i] !== Location.Text) { + return (loc === locationHistory[i]); + } + } + + return false; + } + + function isLocation(loc: Location): boolean { + return loc === locationHistory[locationHistory.length - 1]; + } + + function refreshMixedContentFlag(): void { + inMixedContent = (isLastNonTextLocation(Location.StartTag) || isLastNonTextLocation(Location.EndTag)) && lastWordCharacter !== undefined; + } + + function setLocation(loc: Location): void { + if (loc === Location.Text) { + lastWordCharacter = undefined; + } + + locationHistory.push(loc); + } // NOTE: all "exiting" checks should appear after their associated "entering" checks for (let i = 0; i < xml.length; i++) { @@ -44,7 +72,7 @@ export class V2XmlFormatter implements XmlFormatter { const ppc = xml.charAt(i - 2); // entering CData - if (location === Location.Text && cc === "<" && nc === "!" && nnc === "[") { + if (isLocation(Location.Text) && cc === "<" && nc === "!" && nnc === "[") { if (pc === ">" && ppc !== "/") { output += "<"; } @@ -53,74 +81,85 @@ export class V2XmlFormatter implements XmlFormatter { output += `${this._getIndent(options, indentLevel)}<`; } - location = Location.CData; + setLocation(Location.CData); } // exiting CData - else if (location === Location.CData && cc === "]" && nc === "]" && nnc === ">") { + else if (isLocation(Location.CData) && cc === "]" && nc === "]" && nnc === ">") { output += "]]>"; i += 2; - lastNonTextLocation = location; - location = Location.Text; + + setLocation(Location.Text); } // entering Comment - else if (location === Location.Text && cc === "<" && nc === "!" && nnc === "-") { + else if (isLocation(Location.Text) && cc === "<" && nc === "!" && nnc === "-") { output += `${this._getIndent(options, indentLevel)}<`; - location = Location.Comment; + + setLocation(Location.Comment); } // exiting Comment - else if (location === Location.Comment && cc === "-" && nc === "-" && nnc === ">") { + else if (isLocation(Location.Comment) && cc === "-" && nc === "-" && nnc === ">") { output += "-->"; i += 2; - lastNonTextLocation = location; - location = Location.Text; + + setLocation(Location.Text); } // entering SpecialTag - else if (location === Location.Text && cc === "<" && (nc === "!" || nc === "?")) { + else if (isLocation(Location.Text) && cc === "<" && (nc === "!" || nc === "?")) { output += `${this._getIndent(options, indentLevel)}<`; - location = Location.SpecialTag; + + setLocation(Location.SpecialTag); } // exiting SpecialTag - else if (location === Location.SpecialTag && cc === ">") { + else if (isLocation(Location.SpecialTag) && cc === ">") { output += `>`; - lastNonTextLocation = location; - location = Location.Text; + + setLocation(Location.Text); } // entering StartTag.StartTagName - else if (location === Location.Text && cc === "<" && ["/", "!"].indexOf(nc) === -1) { + else if (isLocation(Location.Text) && cc === "<" && ["/", "!"].indexOf(nc) === -1) { + refreshMixedContentFlag(); + // if this occurs after another tag, prepend a line break // but do not add one if the previous tag was self-closing (it already adds its own) - if (pc === ">" && ppc !== "/") { + if (pc === ">" && ppc !== "/" && !inMixedContent) { output += `${options.newLine}${this._getIndent(options, indentLevel)}<`; } - else { + else if (!inMixedContent) { // removing trailing non-breaking whitespace here prevents endless indentations (issue #193) output = this._removeTrailingNonBreakingWhitespace(output); output += `${this._getIndent(options, indentLevel)}<`; } + else { + output += "<"; + + indentLevel--; + } + indentLevel++; - location = Location.StartTagName; + + setLocation(Location.StartTagName); } // exiting StartTag.StartTagName, enter StartTag - else if (location === Location.StartTagName && cc === " ") { + else if (isLocation(Location.StartTagName) && cc === " ") { output += " "; - lastNonTextLocation = location; - location = Location.StartTag; + + setLocation(Location.StartTag); } // entering StartTag.Attribute - else if (location === Location.StartTag && [" ", "/", ">"].indexOf(cc) === -1) { - if (lastNonTextLocation === Location.AttributeValue + else if (isLocation(Location.StartTag) && [" ", "/", ">"].indexOf(cc) === -1) { + if (locationHistory[locationHistory.length - 2] === Location.AttributeValue && ((options.splitXmlnsOnFormat && xml.substr(i, 5).toLowerCase() === "xmlns") || options.splitAttributesOnFormat)) { @@ -128,30 +167,30 @@ export class V2XmlFormatter implements XmlFormatter { } output += cc; - lastNonTextLocation = location; - location = Location.Attribute; + + setLocation(Location.Attribute); } // entering StartTag.Attribute.AttributeValue - else if (location === Location.Attribute && (cc === "\"" || cc === "'")) { + else if (isLocation(Location.Attribute) && (cc === "\"" || cc === "'")) { output += cc; - lastNonTextLocation = location; - location = Location.AttributeValue; + + setLocation(Location.AttributeValue); attributeQuote = cc; } // exiting StartTag.Attribute.AttributeValue, entering StartTag - else if (location === Location.AttributeValue && cc === attributeQuote) { + else if (isLocation(Location.AttributeValue) && cc === attributeQuote) { output += cc; - lastNonTextLocation = location; - location = Location.StartTag; + + setLocation(Location.StartTag); attributeQuote = undefined; } // approaching the end of a self-closing tag where there was no whitespace (issue #149) - else if ((location === Location.StartTag || location === Location.StartTagName) + else if ((isLocation(Location.StartTag) || isLocation(Location.StartTagName)) && cc === "/" && pc !== " " && options.enforcePrettySelfClosingTagOnFormat) { @@ -159,7 +198,7 @@ export class V2XmlFormatter implements XmlFormatter { } // exiting StartTag or StartTag.StartTagName, entering Text - else if ((location === Location.StartTag || location === Location.StartTagName) && cc === ">") { + else if ((isLocation(Location.StartTag) || isLocation(Location.StartTagName)) && cc === ">") { // if this was a self-closing tag, we need to decrement the indent level and add a newLine if (pc === "/") { indentLevel--; @@ -175,29 +214,35 @@ export class V2XmlFormatter implements XmlFormatter { output += ">"; } - lastNonTextLocation = location; - location = Location.Text; + // don't go directly from StartTagName to Text; go through StartTag first + if (isLocation(Location.StartTagName)) { + setLocation(Location.StartTag); + } + + setLocation(Location.Text); } // entering EndTag - else if (location === Location.Text && cc === "<" && nc === "/") { + else if (isLocation(Location.Text) && cc === "<" && nc === "/") { indentLevel--; + refreshMixedContentFlag(); + // if the end tag immediately follows a line break, just add an indentation // if the end tag immediately follows another end tag or a self-closing tag (issue #185), add a line break and indent // otherwise, this should be treated as a same-line end tag(ex. text) - if (pc === "\n" || lineBreakSpree) { + if ((pc === "\n" || lineBreakSpree) && !inMixedContent) { // removing trailing non-breaking whitespace here prevents endless indentations (issue #193) output = this._removeTrailingNonBreakingWhitespace(output); output += `${this._getIndent(options, indentLevel)}<`; lineBreakSpree = false; } - else if (lastNonTextLocation === Location.EndTag) { + else if (isLastNonTextLocation(Location.EndTag) && !inMixedContent) { output += `${options.newLine}${this._getIndent(options, indentLevel)}<`; } - else if (pc === ">" && ppc === "/") { + else if (pc === ">" && ppc === "/" && !inMixedContent) { output += `${this._getIndent(options, indentLevel)}<`; } @@ -205,26 +250,33 @@ export class V2XmlFormatter implements XmlFormatter { output += "<"; } - location = Location.EndTag; + setLocation(Location.EndTag); } // exiting EndTag, entering Text - else if (location === Location.EndTag && cc === ">") { + else if (isLocation(Location.EndTag) && cc === ">") { output += ">"; - lastNonTextLocation = location; - location = Location.Text; + + setLocation(Location.Text); + + inMixedContent = false; } // Text else { if (cc === "\n") { lineBreakSpree = true; + lastWordCharacter = undefined; } else if (lineBreakSpree && /\S/.test(cc)) { lineBreakSpree = false; } + if (/[\w\d]/.test(cc)) { + lastWordCharacter = cc; + } + output += cc; } } diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index c09df83..8c3627b 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -88,6 +88,10 @@ describe("V2XmlFormatter", () => { testFormatter(xmlFormatter, options, "issue-194"); }); + it("should support mixed content", () => { + testFormatter(xmlFormatter, options, "issue-200"); + }); + }); }); diff --git a/src/test/test-data/issue-200.formatted.xml b/src/test/test-data/issue-200.formatted.xml new file mode 100644 index 0000000..2453567 --- /dev/null +++ b/src/test/test-data/issue-200.formatted.xml @@ -0,0 +1,4 @@ +beginning text + data + another data +end text \ No newline at end of file diff --git a/src/test/test-data/issue-200.unformatted.xml b/src/test/test-data/issue-200.unformatted.xml new file mode 100644 index 0000000..d11c144 --- /dev/null +++ b/src/test/test-data/issue-200.unformatted.xml @@ -0,0 +1 @@ +beginning textdataanother dataend text \ No newline at end of file