Implement Classic XML Formatting

This commit is contained in:
Josh Johnson 2018-01-27 20:30:03 -05:00
parent 394188a204
commit bedb878549
9 changed files with 233 additions and 30 deletions

View file

@ -0,0 +1,123 @@
import { XmlFormatter } from "../xml-formatter";
import { XmlFormattingOptions } from "../xml-formatting-options";
export class ClassicXmlFormatter implements XmlFormatter {
formatXml(xml: string, options: XmlFormattingOptions): string {
xml = this.minifyXml(xml, options);
xml = xml.replace(/</g, "~::~<");
if (options.splitXmlnsOnFormat) {
xml = xml
.replace(/xmlns\:/g, "~::~xmlns:")
.replace(/xmlns\=/g, "~::~xmlns=");
}
const parts: string[] = xml.split("~::~");
let inComment = false;
let level = 0;
let output = "";
for (let i = 0; i < parts.length; i++) {
// <!
if (parts[i].search(/<!/) > -1) {
output += this._getIndent(options, level, parts[i]);
inComment = true;
// end <!
if (parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1 || parts[i].search(/!DOCTYPE/) > -1) {
inComment = false;
}
} else if (parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1) {
output += parts[i];
inComment = false;
} else if (/^<(\w|:)/.test(parts[i - 1]) && /^<\/(\w|:)/.test(parts[i])
&& /^<[\w:\-\.\,\/]+/.exec(parts[i - 1])[0] === /^<\/[\w:\-\.\,]+/.exec(parts[i])[0].replace("/", "")) {
output += parts[i];
if (!inComment) { level--; }
} else if (parts[i].search(/<(\w|:)/) > -1 && parts[i].search(/<\//) === -1 && parts[i].search(/\/>/) === -1) {
output = (!inComment) ? output += this._getIndent(options, level++, parts[i]) : output += parts[i];
} else if (parts[i].search(/<(\w|:)/) > -1 && parts[i].search(/<\//) > -1) {
output = (!inComment) ? output += this._getIndent(options, level, parts[i]) : output += parts[i];
} else if (parts[i].search(/<\//) > -1) {
output = (!inComment) ? output += this._getIndent(options, --level, parts[i]) : output += parts[i];
} else if (parts[i].search(/\/>/) > -1 && (!options.splitXmlnsOnFormat || parts[i].search(/xmlns(:|=)/) === -1)) {
output = (!inComment) ? output += this._getIndent(options, level, parts[i]) : output += parts[i];
} else if (parts[i].search(/\/>/) > -1 && parts[i].search(/xmlns(:|=)/) > -1 && options.splitXmlnsOnFormat) {
output = (!inComment) ? output += this._getIndent(options, level--, parts[i]) : output += parts[i];
} else if (parts[i].search(/<\?/) > -1) {
output += this._getIndent(options, level, parts[i]);
} else if (options.splitXmlnsOnFormat && (parts[i].search(/xmlns\:/) > -1 || parts[i].search(/xmlns\=/) > -1)) {
output += this._getIndent(options, level, parts[i]);
} else {
output += parts[i];
}
}
// remove leading newline
if (output[0] === options.newLine) {
output = output.slice(1);
} else if (output.substring(0, 1) === options.newLine) {
output = output.slice(2);
}
return output;
}
minifyXml(xml: string, options: XmlFormattingOptions): string {
xml = this._stripLineBreaks(options, xml); // all line breaks outside of CDATA elements
xml = (options.removeCommentsOnMinify) ? xml.replace(/\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>/g, "") : xml;
xml = xml.replace(/>\s{0,}</g, "><"); // insignificant whitespace between tags
xml = xml.replace(/"\s+(?=[^\s]+=)/g, "\" "); // spaces between attributes
xml = xml.replace(/"\s+(?=>)/g, "\""); // spaces between the last attribute and tag close (>)
xml = xml.replace(/"\s+(?=\/>)/g, "\" "); // spaces between the last attribute and tag close (/>)
xml = xml.replace(/[^ <>="]\s+[^ <>="]+=/g, (match: string) => { // spaces between the node name and the first attribute
return match.replace(/\s+/g, " ");
});
return xml;
}
private _getIndent(options: XmlFormattingOptions, level: number, trailingValue?: string): string {
trailingValue = trailingValue || "";
const indentPattern = (options.editorOptions.preferSpaces) ? " ".repeat(options.editorOptions.tabSize) : "\t";
return `${options.newLine}${indentPattern.repeat(level)}${trailingValue}`;
}
private _stripLineBreaks(options: XmlFormattingOptions, xml: string): string {
let output = "";
const inTag = false;
const inTagName = false;
let inCdata = false;
const inAttribute = false;
for (let i = 0; i < xml.length; i++) {
const char: string = xml.charAt(i);
const prev: string = xml.charAt(i - 1);
const next: string = xml.charAt(i + 1);
if (char === "!" && (xml.substr(i, 8) === "![CDATA[" || xml.substr(i, 3) === "!--")) {
inCdata = true;
} else if (char === "]" && (xml.substr(i, 3) === "]]>")) {
inCdata = false;
} else if (char === "-" && (xml.substr(i, 3) === "-->")) {
inCdata = false;
} else if (char.search(/[\r\n]/g) > -1 && !inCdata) {
if (/\r/.test(char) && /\S|\r|\n/.test(prev) && /\S|\r|\n/.test(xml.charAt(i + options.newLine.length))) {
output += char;
} else if (/\n/.test(char) && /\S|\r|\n/.test(xml.charAt(i - options.newLine.length)) && /\S|\r|\n/.test(next)) {
output += char;
}
continue;
}
output += char;
}
return output;
}
}

View file

@ -0,0 +1,6 @@
import { XmlFormattingOptions } from "./xml-formatting-options";
export interface XmlFormatter {
formatXml(xml: string, options: XmlFormattingOptions): string;
minifyXml(xml: string, options: XmlFormattingOptions): string;
}

View file

@ -0,0 +1,66 @@
import { commands, languages } from "vscode";
import {
CancellationToken, DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider, ExtensionContext,
FormattingOptions, ProviderResult, Range, TextDocument, TextEdit, TextEditor, WorkspaceConfiguration
} from "vscode";
import * as extension from "../extension";
import { XmlFormatter } from "./xml-formatter";
import { ClassicXmlFormatter } from "./formatters/classic-xml-formatter";
extension.onActivate((context: ExtensionContext, config: WorkspaceConfiguration) => {
const xmlFormatterImplementationSetting = config.get<string>("xmlFormatterImplementation");
let xmlFormatterImplementation: XmlFormatter;
switch (xmlFormatterImplementationSetting) {
case "classic":
default: xmlFormatterImplementation = new ClassicXmlFormatter(); break;
}
// tslint:disable-next-line:no-use-before-declare
const xmlFormattingEditProvider = new XmlFormattingEditProvider(config, xmlFormatterImplementation);
const formatAsXmlCommand = commands.registerTextEditorCommand("xmlTools.formatAsXml", (textEditor) => {
// TODO: implement command
});
const minifyXmlCommand = commands.registerTextEditorCommand("xmlTools.minifyXml", (textEditor: TextEditor) => {
// TODO: implement command
});
context.subscriptions.push(
formatAsXmlCommand,
minifyXmlCommand,
languages.registerDocumentFormattingEditProvider("xml", xmlFormattingEditProvider),
languages.registerDocumentRangeFormattingEditProvider("xml", xmlFormattingEditProvider)
);
});
export class XmlFormattingEditProvider implements DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider {
constructor(
public workspaceConfiguration: WorkspaceConfiguration,
public xmlFormatter: XmlFormatter
) { }
provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
const lastLine = document.lineAt(document.lineCount - 1);
const documentRange = new Range(document.positionAt(0), lastLine.range.end);
return this.provideDocumentRangeFormattingEdits(document, documentRange, options, token);
}
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
let xml = document.getText(range);
xml = this.xmlFormatter.formatXml(xml, {
editorOptions: options,
newLine: document.eol.toString(),
removeCommentsOnMinify: this.workspaceConfiguration.get<boolean>("removeCommentsOnMinify"),
splitXmlnsOnFormat: this.workspaceConfiguration.get<boolean>("splitXmlnsOnFormat")
});
return [ TextEdit.replace(range, xml) ];
}
}

View file

@ -0,0 +1,8 @@
import { FormattingOptions } from "vscode";
export interface XmlFormattingOptions {
editorOptions: FormattingOptions;
newLine: string;
removeCommentsOnMinify: boolean;
splitXmlnsOnFormat: boolean;
}