Merge pull request #220 from DotJoshJohnson/issue-200

Add Support for Mixed Content
This commit is contained in:
Josh Johnson 2018-08-17 20:46:16 -04:00 committed by GitHub
commit c6ff7adc6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 46 deletions

View File

@ -30,10 +30,38 @@ export class V2XmlFormatter implements XmlFormatter {
let output = ""; let output = "";
let indentLevel = 0; let indentLevel = 0;
let location = Location.Text;
let lastNonTextLocation = Location.Text; // hah
let attributeQuote = ""; let attributeQuote = "";
let lineBreakSpree = false; 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 // NOTE: all "exiting" checks should appear after their associated "entering" checks
for (let i = 0; i < xml.length; i++) { for (let i = 0; i < xml.length; i++) {
@ -44,7 +72,7 @@ export class V2XmlFormatter implements XmlFormatter {
const ppc = xml.charAt(i - 2); const ppc = xml.charAt(i - 2);
// entering CData // entering CData
if (location === Location.Text && cc === "<" && nc === "!" && nnc === "[") { if (isLocation(Location.Text) && cc === "<" && nc === "!" && nnc === "[") {
if (pc === ">" && ppc !== "/") { if (pc === ">" && ppc !== "/") {
output += "<"; output += "<";
} }
@ -53,74 +81,85 @@ export class V2XmlFormatter implements XmlFormatter {
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
} }
location = Location.CData; setLocation(Location.CData);
} }
// exiting CData // exiting CData
else if (location === Location.CData && cc === "]" && nc === "]" && nnc === ">") { else if (isLocation(Location.CData) && cc === "]" && nc === "]" && nnc === ">") {
output += "]]>"; output += "]]>";
i += 2; i += 2;
lastNonTextLocation = location;
location = Location.Text; setLocation(Location.Text);
} }
// entering Comment // entering Comment
else if (location === Location.Text && cc === "<" && nc === "!" && nnc === "-") { else if (isLocation(Location.Text) && cc === "<" && nc === "!" && nnc === "-") {
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
location = Location.Comment;
setLocation(Location.Comment);
} }
// exiting Comment // exiting Comment
else if (location === Location.Comment && cc === "-" && nc === "-" && nnc === ">") { else if (isLocation(Location.Comment) && cc === "-" && nc === "-" && nnc === ">") {
output += "-->"; output += "-->";
i += 2; i += 2;
lastNonTextLocation = location;
location = Location.Text; setLocation(Location.Text);
} }
// entering SpecialTag // entering SpecialTag
else if (location === Location.Text && cc === "<" && (nc === "!" || nc === "?")) { else if (isLocation(Location.Text) && cc === "<" && (nc === "!" || nc === "?")) {
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
location = Location.SpecialTag;
setLocation(Location.SpecialTag);
} }
// exiting SpecialTag // exiting SpecialTag
else if (location === Location.SpecialTag && cc === ">") { else if (isLocation(Location.SpecialTag) && cc === ">") {
output += `>`; output += `>`;
lastNonTextLocation = location;
location = Location.Text; setLocation(Location.Text);
} }
// entering StartTag.StartTagName // 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 // 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) // 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)}<`; output += `${options.newLine}${this._getIndent(options, indentLevel)}<`;
} }
else { else if (!inMixedContent) {
// removing trailing non-breaking whitespace here prevents endless indentations (issue #193) // removing trailing non-breaking whitespace here prevents endless indentations (issue #193)
output = this._removeTrailingNonBreakingWhitespace(output); output = this._removeTrailingNonBreakingWhitespace(output);
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
} }
else {
output += "<";
indentLevel--;
}
indentLevel++; indentLevel++;
location = Location.StartTagName;
setLocation(Location.StartTagName);
} }
// exiting StartTag.StartTagName, enter StartTag // exiting StartTag.StartTagName, enter StartTag
else if (location === Location.StartTagName && cc === " ") { else if (isLocation(Location.StartTagName) && cc === " ") {
output += " "; output += " ";
lastNonTextLocation = location;
location = Location.StartTag; setLocation(Location.StartTag);
} }
// entering StartTag.Attribute // entering StartTag.Attribute
else if (location === Location.StartTag && [" ", "/", ">"].indexOf(cc) === -1) { else if (isLocation(Location.StartTag) && [" ", "/", ">"].indexOf(cc) === -1) {
if (lastNonTextLocation === Location.AttributeValue if (locationHistory[locationHistory.length - 2] === Location.AttributeValue
&& ((options.splitXmlnsOnFormat && ((options.splitXmlnsOnFormat
&& xml.substr(i, 5).toLowerCase() === "xmlns") && xml.substr(i, 5).toLowerCase() === "xmlns")
|| options.splitAttributesOnFormat)) { || options.splitAttributesOnFormat)) {
@ -128,30 +167,30 @@ export class V2XmlFormatter implements XmlFormatter {
} }
output += cc; output += cc;
lastNonTextLocation = location;
location = Location.Attribute; setLocation(Location.Attribute);
} }
// entering StartTag.Attribute.AttributeValue // entering StartTag.Attribute.AttributeValue
else if (location === Location.Attribute && (cc === "\"" || cc === "'")) { else if (isLocation(Location.Attribute) && (cc === "\"" || cc === "'")) {
output += cc; output += cc;
lastNonTextLocation = location;
location = Location.AttributeValue; setLocation(Location.AttributeValue);
attributeQuote = cc; attributeQuote = cc;
} }
// exiting StartTag.Attribute.AttributeValue, entering StartTag // exiting StartTag.Attribute.AttributeValue, entering StartTag
else if (location === Location.AttributeValue && cc === attributeQuote) { else if (isLocation(Location.AttributeValue) && cc === attributeQuote) {
output += cc; output += cc;
lastNonTextLocation = location;
location = Location.StartTag; setLocation(Location.StartTag);
attributeQuote = undefined; attributeQuote = undefined;
} }
// approaching the end of a self-closing tag where there was no whitespace (issue #149) // 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 === "/" && cc === "/"
&& pc !== " " && pc !== " "
&& options.enforcePrettySelfClosingTagOnFormat) { && options.enforcePrettySelfClosingTagOnFormat) {
@ -159,7 +198,7 @@ export class V2XmlFormatter implements XmlFormatter {
} }
// exiting StartTag or StartTag.StartTagName, entering Text // 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 this was a self-closing tag, we need to decrement the indent level and add a newLine
if (pc === "/") { if (pc === "/") {
indentLevel--; indentLevel--;
@ -175,29 +214,35 @@ export class V2XmlFormatter implements XmlFormatter {
output += ">"; output += ">";
} }
lastNonTextLocation = location; // don't go directly from StartTagName to Text; go through StartTag first
location = Location.Text; if (isLocation(Location.StartTagName)) {
setLocation(Location.StartTag);
}
setLocation(Location.Text);
} }
// entering EndTag // entering EndTag
else if (location === Location.Text && cc === "<" && nc === "/") { else if (isLocation(Location.Text) && cc === "<" && nc === "/") {
indentLevel--; indentLevel--;
refreshMixedContentFlag();
// if the end tag immediately follows a line break, just add an indentation // 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 // 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. <element>text</element>) // otherwise, this should be treated as a same-line end tag(ex. <element>text</element>)
if (pc === "\n" || lineBreakSpree) { if ((pc === "\n" || lineBreakSpree) && !inMixedContent) {
// removing trailing non-breaking whitespace here prevents endless indentations (issue #193) // removing trailing non-breaking whitespace here prevents endless indentations (issue #193)
output = this._removeTrailingNonBreakingWhitespace(output); output = this._removeTrailingNonBreakingWhitespace(output);
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
lineBreakSpree = false; lineBreakSpree = false;
} }
else if (lastNonTextLocation === Location.EndTag) { else if (isLastNonTextLocation(Location.EndTag) && !inMixedContent) {
output += `${options.newLine}${this._getIndent(options, indentLevel)}<`; output += `${options.newLine}${this._getIndent(options, indentLevel)}<`;
} }
else if (pc === ">" && ppc === "/") { else if (pc === ">" && ppc === "/" && !inMixedContent) {
output += `${this._getIndent(options, indentLevel)}<`; output += `${this._getIndent(options, indentLevel)}<`;
} }
@ -205,26 +250,33 @@ export class V2XmlFormatter implements XmlFormatter {
output += "<"; output += "<";
} }
location = Location.EndTag; setLocation(Location.EndTag);
} }
// exiting EndTag, entering Text // exiting EndTag, entering Text
else if (location === Location.EndTag && cc === ">") { else if (isLocation(Location.EndTag) && cc === ">") {
output += ">"; output += ">";
lastNonTextLocation = location;
location = Location.Text; setLocation(Location.Text);
inMixedContent = false;
} }
// Text // Text
else { else {
if (cc === "\n") { if (cc === "\n") {
lineBreakSpree = true; lineBreakSpree = true;
lastWordCharacter = undefined;
} }
else if (lineBreakSpree && /\S/.test(cc)) { else if (lineBreakSpree && /\S/.test(cc)) {
lineBreakSpree = false; lineBreakSpree = false;
} }
if (/[\w\d]/.test(cc)) {
lastWordCharacter = cc;
}
output += cc; output += cc;
} }
} }

View File

@ -88,6 +88,10 @@ describe("V2XmlFormatter", () => {
testFormatter(xmlFormatter, options, "issue-194"); testFormatter(xmlFormatter, options, "issue-194");
}); });
it("should support mixed content", () => {
testFormatter(xmlFormatter, options, "issue-200");
});
}); });
}); });

View File

@ -0,0 +1,4 @@
<mixed>beginning text<inner>
<a>data</a>
<b>another data</b>
</inner>end text</mixed>

View File

@ -0,0 +1 @@
<mixed>beginning text<inner><a>data</a><b>another data</b></inner>end text</mixed>