diff --git a/src/formatting/formatters/v2-xml-formatter.ts b/src/formatting/formatters/v2-xml-formatter.ts index 8f60585..19b1dd1 100644 --- a/src/formatting/formatters/v2-xml-formatter.ts +++ b/src/formatting/formatters/v2-xml-formatter.ts @@ -25,7 +25,7 @@ export class V2XmlFormatter implements XmlFormatter { output += `${this._getIndent(options, indentLevel)}<`; location = Location.CData; } - + // exiting CData else if (location === Location.CData && cc === "]" && nc === "]" && nnc === ">") { output += "]]>"; @@ -34,13 +34,13 @@ export class V2XmlFormatter implements XmlFormatter { lastNonTextLocation = location; location = Location.Text; } - + // entering Comment else if (location === Location.Text && cc === "<" && nc === "!" && nnc === "-") { output += `${this._getIndent(options, indentLevel)}<`; location = Location.Comment; } - + // exiting Comment else if (location === Location.Comment && cc === "-" && nc === "-" && nnc === ">") { output += "-->"; @@ -49,20 +49,20 @@ export class V2XmlFormatter implements XmlFormatter { lastNonTextLocation = location; location = Location.Text; } - + // entering SpecialTag else if (location === Location.Text && cc === "<" && (nc === "!" || nc === "?")) { output += `${this._getIndent(options, indentLevel)}<`; location = Location.SpecialTag; } - + // exiting SpecialTag else if (location === Location.SpecialTag && cc === ">") { output += `>`; lastNonTextLocation = location; location = Location.Text; } - + // entering StartTag.StartTagName else if (location === Location.Text && cc === "<" && ["/", "!"].indexOf(nc) === -1) { // if this occurs after another tag, prepend a line break @@ -77,17 +77,20 @@ export class V2XmlFormatter implements XmlFormatter { indentLevel++; location = Location.StartTagName; } - + // exiting StartTag.StartTagName, enter StartTag else if (location === Location.StartTagName && cc === " ") { output += " "; lastNonTextLocation = location; location = Location.StartTag; } - + // entering StartTag.Attribute else if (location === Location.StartTag && [" ", "/", ">"].indexOf(cc) === -1) { - if (lastNonTextLocation === Location.AttributeValue && ((options.splitXmlnsOnFormat && xml.substr(i, 5).toLowerCase() === "xmlns") || options.splitAttributesOnFormat)) { + if (lastNonTextLocation === Location.AttributeValue + && ((options.splitXmlnsOnFormat + && xml.substr(i, 5).toLowerCase() === "xmlns") + || options.splitAttributesOnFormat)) { output += `${options.newLine}${this._getIndent(options, indentLevel)}`; } @@ -95,14 +98,14 @@ export class V2XmlFormatter implements XmlFormatter { lastNonTextLocation = location; location = Location.Attribute; } - + // entering StartTag.Attribute.AttributeValue else if (location === Location.Attribute && cc === "\"") { output += "\""; lastNonTextLocation = location; location = Location.AttributeValue; } - + // exiting StartTag.Attribute.AttributeValue, entering StartTag else if (location === Location.AttributeValue && cc === "\"") { output += "\""; @@ -118,6 +121,21 @@ export class V2XmlFormatter implements XmlFormatter { output += `>${options.newLine}`; } + // if this is an open tag followed by a line break, add an indent before the text (after the line break) + // TODO: there could be multiple lines of text here, so we'll need a less naive implementation at some point + else if (nc === "\r" || nc === "\n") { + output += `>${options.newLine}${this._getIndent(options, indentLevel)}`; + + // fast-forward based on what type of line break was used + if (nc === "\r") { + i += 2; + } + + else { + i++; + } + } + else { output += ">"; } @@ -125,31 +143,36 @@ export class V2XmlFormatter implements XmlFormatter { lastNonTextLocation = location; location = Location.Text; } - + // entering EndTag else if (location === Location.Text && cc === "<" && nc === "/") { indentLevel--; // if the end tag immediately follows another end tag, add a line break and indent + // if the end tag immediately follows a line break, just add an indentation // otherwise, this should be treated as a same-line end tag(ex. text) if (lastNonTextLocation === Location.EndTag) { output += `${options.newLine}${this._getIndent(options, indentLevel)}<`; } - + + else if (pc === "\n") { + output += `${this._getIndent(options, indentLevel)}<`; + } + else { output += "<"; } location = Location.EndTag; } - + // exiting EndTag, entering Text else if (location === Location.EndTag && cc === ">") { output += ">"; lastNonTextLocation = location; location = Location.Text; } - + // Text else { output += cc; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d4c8a71..e5de0ab 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,17 +1,42 @@ -// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// The module 'assert' provides assertion methods from node import * as assert from "assert"; +import { FormattingOptions } from "vscode"; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from "vscode"; -import * as myExtension from "../extension"; +import { TestDataLoader } from "./test-utils/test-data-loader"; + +import { XmlFormatter } from "../formatting/xml-formatter"; +import { XmlFormattingOptions } from "../formatting/xml-formatting-options"; +import { V2XmlFormatter } from "../formatting/formatters/v2-xml-formatter"; + +describe("V2XmlFormatter", () => { + + const xmlFormatter = new V2XmlFormatter(); + + describe("#formatXml(xml, options)", () => { + + const options = { + editorOptions: { + insertSpaces: true, + tabSize: 4 + }, + newLine: "\r\n", + removeCommentsOnMinify: false, + splitAttributesOnFormat: false, + splitXmlnsOnFormat: true + }; + + it("should handle basic XML", () => { + testFormatter(xmlFormatter, options, "basic"); + }); + + }); -// Defines a Mocha test suite to group tests of similar kind together -suite("Extension Tests", () => { - // TODO: implement tests }); + +function testFormatter(xmlFormatter: XmlFormatter, options: XmlFormattingOptions, fileLabel: string): void { + const expectedFormattedXml = TestDataLoader.load(`${fileLabel}.formatted.xml`); + const unformattedXml = TestDataLoader.load(`${fileLabel}.unformatted.xml`); + + const actualFormattedXml = xmlFormatter.formatXml(unformattedXml, options); + + assert.equal(actualFormattedXml, expectedFormattedXml, "Actual formatted XML does not match expected formatted XML."); +} diff --git a/src/test/index.ts b/src/test/index.ts index edb6206..f2ca26a 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -15,7 +15,7 @@ import * as testRunner from "vscode/lib/testrunner"; // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ - ui: "tdd", // the TDD UI is being used in extension.test.ts (suite, test, etc.) + ui: "bdd", useColors: true // colored output from test results }); diff --git a/src/test/test-data/basic.formatted.xml b/src/test/test-data/basic.formatted.xml new file mode 100644 index 0000000..1bfeb09 --- /dev/null +++ b/src/test/test-data/basic.formatted.xml @@ -0,0 +1,3 @@ + + text + \ No newline at end of file diff --git a/src/test/test-data/basic.unformatted.xml b/src/test/test-data/basic.unformatted.xml new file mode 100644 index 0000000..cd114a6 --- /dev/null +++ b/src/test/test-data/basic.unformatted.xml @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/src/test/test-utils/test-data-loader.ts b/src/test/test-utils/test-data-loader.ts new file mode 100644 index 0000000..79707af --- /dev/null +++ b/src/test/test-utils/test-data-loader.ts @@ -0,0 +1,7 @@ +import * as fs from "fs"; + +export class TestDataLoader { + static load(fileName: string): string { + return fs.readFileSync(`${__dirname}/../../../src/test/test-data/${fileName}`, "UTF-8"); + } +}