Merge pull request #220 from DotJoshJohnson/issue-200
Add Support for Mixed Content
This commit is contained in:
		
						commit
						c6ff7adc6e
					
				
					 4 changed files with 107 additions and 46 deletions
				
			
		| 
						 | 
				
			
			@ -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. <element>text</element>)
 | 
			
		||||
                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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -88,6 +88,10 @@ describe("V2XmlFormatter", () => {
 | 
			
		|||
            testFormatter(xmlFormatter, options, "issue-194");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should support mixed content", () => {
 | 
			
		||||
            testFormatter(xmlFormatter, options, "issue-200");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								src/test/test-data/issue-200.formatted.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/test/test-data/issue-200.formatted.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<mixed>beginning text<inner>
 | 
			
		||||
    <a>data</a>
 | 
			
		||||
    <b>another data</b>
 | 
			
		||||
</inner>end text</mixed>
 | 
			
		||||
							
								
								
									
										1
									
								
								src/test/test-data/issue-200.unformatted.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/test/test-data/issue-200.unformatted.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<mixed>beginning text<inner><a>data</a><b>another data</b></inner>end text</mixed>
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue