forked from external/vscode-xml
		
	Implement Classic XML Formatting
This commit is contained in:
		
							parent
							
								
									394188a204
								
							
						
					
					
						commit
						bedb878549
					
				
					 9 changed files with 233 additions and 30 deletions
				
			
		| 
						 | 
				
			
			@ -82,6 +82,11 @@
 | 
			
		|||
                    "default": true,
 | 
			
		||||
                    "description": "Put each xmlns attribute on a new line when formatting XML."
 | 
			
		||||
                },
 | 
			
		||||
                "xmlTools.xmlFormatterImplementation": {
 | 
			
		||||
                    "type": "string",
 | 
			
		||||
                    "default": "classic",
 | 
			
		||||
                    "description": "Supported XML Formatters: classic"
 | 
			
		||||
                },
 | 
			
		||||
                "xmlTools.xqueryExecutionArguments": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "default": ["-xquery", "$(script)", "-in", "$(input)", "-out", "$(input.output.xml"],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +1,27 @@
 | 
			
		|||
"use strict";
 | 
			
		||||
// The module 'vscode' contains the VS Code extensibility API
 | 
			
		||||
// Import the module and reference it with the alias vscode in your code below
 | 
			
		||||
import * as vscode from "vscode";
 | 
			
		||||
import { workspace } from "vscode";
 | 
			
		||||
import { ExtensionContext, WorkspaceConfiguration } from "vscode";
 | 
			
		||||
 | 
			
		||||
// this method is called when your extension is activated
 | 
			
		||||
// your extension is activated the very first time the command is executed
 | 
			
		||||
export function activate(context: vscode.ExtensionContext) {
 | 
			
		||||
const onActivateHandlers: OnActivateHandler[] = [];
 | 
			
		||||
const onDeactivateHandlers: OnDeactivateHandler[] = [];
 | 
			
		||||
 | 
			
		||||
    // Use the console to output diagnostic information (console.log) and errors (console.error)
 | 
			
		||||
    // This line of code will only be executed once when your extension is activated
 | 
			
		||||
    console.log("Congratulations, your extension \"xml\" is now active!");
 | 
			
		||||
export function activate(context: ExtensionContext) {
 | 
			
		||||
    const workspaceConfiguration = workspace.getConfiguration("xmlTools");
 | 
			
		||||
 | 
			
		||||
    // The command has been defined in the package.json file
 | 
			
		||||
    // Now provide the implementation of the command with  registerCommand
 | 
			
		||||
    // The commandId parameter must match the command field in package.json
 | 
			
		||||
    const disposable = vscode.commands.registerCommand("extension.sayHello", () => {
 | 
			
		||||
        // The code you place here will be executed every time your command is executed
 | 
			
		||||
 | 
			
		||||
        // Display a message box to the user
 | 
			
		||||
        vscode.window.showInformationMessage("Hello World!");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    context.subscriptions.push(disposable);
 | 
			
		||||
    onActivateHandlers.forEach(x => x(context, workspaceConfiguration));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// this method is called when your extension is deactivated
 | 
			
		||||
export function deactivate() {
 | 
			
		||||
    onDeactivateHandlers.forEach(x => x());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function onActivate(handler: OnActivateHandler): void {
 | 
			
		||||
    onActivateHandlers.push(handler);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function onDeactivate(handler: OnDeactivateHandler): void {
 | 
			
		||||
    onDeactivateHandlers.push(handler);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type OnActivateHandler = (context: ExtensionContext, config: WorkspaceConfiguration) => void;
 | 
			
		||||
 | 
			
		||||
export type OnDeactivateHandler = () => void;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										123
									
								
								src/formatting/formatters/classic-xml-formatter.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/formatting/formatters/classic-xml-formatter.ts
									
										
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/formatting/xml-formatter.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/formatting/xml-formatter.ts
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/formatting/xml-formatting-edit-provider.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/formatting/xml-formatting-edit-provider.ts
									
										
									
									
									
										Normal 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) ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/formatting/xml-formatting-options.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/formatting/xml-formatting-options.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { FormattingOptions } from "vscode";
 | 
			
		||||
 | 
			
		||||
export interface XmlFormattingOptions {
 | 
			
		||||
    editorOptions: FormattingOptions;
 | 
			
		||||
    newLine: string;
 | 
			
		||||
    removeCommentsOnMinify: boolean;
 | 
			
		||||
    splitXmlnsOnFormat: boolean;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,10 +13,5 @@ import * as myExtension from "../extension";
 | 
			
		|||
 | 
			
		||||
// Defines a Mocha test suite to group tests of similar kind together
 | 
			
		||||
suite("Extension Tests", () => {
 | 
			
		||||
 | 
			
		||||
    // Defines a Mocha unit test
 | 
			
		||||
    test("Something 1", () => {
 | 
			
		||||
        assert.equal(-1, [1, 2, 3].indexOf(5));
 | 
			
		||||
        assert.equal(-1, [1, 2, 3].indexOf(0));
 | 
			
		||||
    });
 | 
			
		||||
    // TODO: implement tests
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,9 @@
 | 
			
		|||
            "es6"
 | 
			
		||||
        ],
 | 
			
		||||
        "sourceMap": true,
 | 
			
		||||
        "rootDir": "src"
 | 
			
		||||
        "rootDir": "src",
 | 
			
		||||
        "strict": true,
 | 
			
		||||
        "strictNullChecks": false
 | 
			
		||||
    },
 | 
			
		||||
    "exclude": [
 | 
			
		||||
        "node_modules",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@
 | 
			
		|||
        "label-position": true,
 | 
			
		||||
        "max-line-length": [
 | 
			
		||||
            true,
 | 
			
		||||
            140
 | 
			
		||||
            165
 | 
			
		||||
        ],
 | 
			
		||||
        "member-access": false,
 | 
			
		||||
        "member-ordering": [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue