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