Add XPath Evaluation
This commit is contained in:
		
							parent
							
								
									6f0bbe3ed7
								
							
						
					
					
						commit
						1c083d15e3
					
				
					 7 changed files with 190 additions and 1 deletions
				
			
		| 
						 | 
				
			
			@ -14,6 +14,7 @@
 | 
			
		|||
*In Progress*
 | 
			
		||||
 | 
			
		||||
## Extension Settings
 | 
			
		||||
* `xmlTools.enableXmlTreeView`: Enables the XML Tree View for XML documents.
 | 
			
		||||
* `xmlTools.ignoreDefaultNamespace`: Ignore default xmlns attributes when evaluating XPath.
 | 
			
		||||
* `xmlTools.persistXPathQuery`: Remember the last XPath query used.
 | 
			
		||||
* `xmlTools.removeCommentsOnMinify`: Remove XML comments during minification.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -3078,6 +3078,11 @@
 | 
			
		|||
            "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
 | 
			
		||||
            "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk="
 | 
			
		||||
        },
 | 
			
		||||
        "xpath": {
 | 
			
		||||
            "version": "0.0.27",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
 | 
			
		||||
            "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ=="
 | 
			
		||||
        },
 | 
			
		||||
        "xqlint": {
 | 
			
		||||
            "version": "0.4.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/xqlint/-/xqlint-0.4.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -188,6 +188,7 @@
 | 
			
		|||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "xmldom": "^0.1.27",
 | 
			
		||||
        "xpath": "0.0.27",
 | 
			
		||||
        "xqlint": "^0.4.0"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
export const extensionPrefix = "xmlTools";
 | 
			
		||||
 | 
			
		||||
export namespace commands {
 | 
			
		||||
    export const evaluateXPath = "xmlTools.evaluateXPath";
 | 
			
		||||
    export const setContext = "setContext";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,4 +11,11 @@ export namespace contextKeys {
 | 
			
		|||
 | 
			
		||||
export namespace configKeys {
 | 
			
		||||
    export const enableXmlTreeView = "enableXmlTreeView";
 | 
			
		||||
    export const ignoreDefaultNamespace = "ignoreDefaultNamespace";
 | 
			
		||||
    export const persistXPathQuery = "persistXPathQuery";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export namespace stateKeys {
 | 
			
		||||
    export const xpathQueryHistory = "xpathQueryHistory";
 | 
			
		||||
    export const xPathQueryLast = "xPathQueryLast";
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { languages, window, workspace, commands } from "vscode";
 | 
			
		||||
import { ExtensionContext, TextEditor, TextEditorSelectionChangeEvent, WorkspaceConfiguration } from "vscode";
 | 
			
		||||
import { ExtensionContext, Memento, TextEditor, TextEditorSelectionChangeEvent, WorkspaceConfiguration } from "vscode";
 | 
			
		||||
 | 
			
		||||
import { XQueryCompletionItemProvider } from "./completion/xquery-completion-item-provider";
 | 
			
		||||
import { FormatAsXmlCommandName, formatAsXml } from "./formatting/commands/formatAsXml";
 | 
			
		||||
| 
						 | 
				
			
			@ -8,10 +8,16 @@ import { XmlFormatterFactory } from "./formatting/xml-formatter";
 | 
			
		|||
import { XmlFormattingEditProvider } from "./formatting/xml-formatting-edit-provider";
 | 
			
		||||
import { XQueryLinter } from "./linting/xquery-linter";
 | 
			
		||||
import { XmlTreeDataProvider } from "./tree-view/xml-tree-data-provider";
 | 
			
		||||
import { evaluateXPath } from "./xpath/commands/evaluateXPath";
 | 
			
		||||
 | 
			
		||||
import * as constants from "./constants";
 | 
			
		||||
 | 
			
		||||
export const ExtensionState: { global?: Memento, workspace?: Memento } = { };
 | 
			
		||||
 | 
			
		||||
export function activate(context: ExtensionContext) {
 | 
			
		||||
    ExtensionState.global = context.globalState;
 | 
			
		||||
    ExtensionState.workspace = context.workspaceState;
 | 
			
		||||
 | 
			
		||||
    const config = workspace.getConfiguration(constants.extensionPrefix);
 | 
			
		||||
 | 
			
		||||
    /* Completion Features */
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +45,11 @@ export function activate(context: ExtensionContext) {
 | 
			
		|||
    context.subscriptions.push(
 | 
			
		||||
        window.registerTreeDataProvider("xmlTreeView", new XmlTreeDataProvider(context))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /* XPath Features */
 | 
			
		||||
    context.subscriptions.push(
 | 
			
		||||
        commands.registerTextEditorCommand(constants.commands.evaluateXPath, evaluateXPath)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function deactivate() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										104
									
								
								src/xpath/commands/evaluateXPath.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/xpath/commands/evaluateXPath.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,104 @@
 | 
			
		|||
import { window, workspace } from "vscode";
 | 
			
		||||
import { TextEditor, TextEditorEdit, ViewColumn } from "vscode";
 | 
			
		||||
 | 
			
		||||
import * as constants from "../../constants";
 | 
			
		||||
import { ExtensionState } from "../../extension";
 | 
			
		||||
 | 
			
		||||
import { EvaluatorResult, EvaluatorResultType, XPathEvaluator } from "../xpath-evaluator";
 | 
			
		||||
 | 
			
		||||
export async function evaluateXPath(editor: TextEditor, edit: TextEditorEdit): Promise<void> {
 | 
			
		||||
    const config = workspace.getConfiguration(constants.extensionPrefix);
 | 
			
		||||
 | 
			
		||||
    // if there is no workspace, we will track queries in the global Memento
 | 
			
		||||
    const memento = ExtensionState.workspace || ExtensionState.global;
 | 
			
		||||
 | 
			
		||||
    // get the xpath persistence setting
 | 
			
		||||
    const persistQueries = config.get<boolean>(constants.configKeys.persistXPathQuery, true);
 | 
			
		||||
 | 
			
		||||
    // get the last query if there is one for this document
 | 
			
		||||
    // if not, try pulling the last query ran, regardless of document
 | 
			
		||||
    // NOTE: if the user has focus on the output channel when opening the xquery prompt, the channel is the "active" document
 | 
			
		||||
    const history = memento.get<HistoricQuery[]>(constants.stateKeys.xpathQueryHistory, new Array<HistoricQuery>());
 | 
			
		||||
    const globalLastQuery = memento.get<string>(constants.stateKeys.xPathQueryLast, "");
 | 
			
		||||
 | 
			
		||||
    const lastQuery = history.find(x => {
 | 
			
		||||
        return (x.uri === editor.document.uri.toString());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // set the inital display value and prompt the user
 | 
			
		||||
    let query = (lastQuery) ? lastQuery.query : globalLastQuery;
 | 
			
		||||
 | 
			
		||||
    query = await window.showInputBox({
 | 
			
		||||
        placeHolder: "XPath Query",
 | 
			
		||||
        prompt: "Please enter an XPath query to evaluate.",
 | 
			
		||||
        value: query
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // showInputBox() will return undefined if the user dimissed the prompt
 | 
			
		||||
    if (!query) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ignoreDefaultNamespace = config.get<boolean>(constants.configKeys.ignoreDefaultNamespace, true);
 | 
			
		||||
 | 
			
		||||
    // run the query
 | 
			
		||||
    const xml = editor.document.getText();
 | 
			
		||||
    let evalResult: EvaluatorResult;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        evalResult = XPathEvaluator.evaluate(query, xml, ignoreDefaultNamespace);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    catch (error) {
 | 
			
		||||
        console.error(error);
 | 
			
		||||
        window.showErrorMessage(`Something went wrong while evaluating the XPath: ${error}`);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // show the results to the user
 | 
			
		||||
    const outputChannel = window.createOutputChannel("XPath Results");
 | 
			
		||||
 | 
			
		||||
    outputChannel.clear();
 | 
			
		||||
 | 
			
		||||
    outputChannel.appendLine(`XPath Query: ${query}`);
 | 
			
		||||
    outputChannel.append("\n");
 | 
			
		||||
 | 
			
		||||
    if (evalResult.type === EvaluatorResultType.NODE_COLLECTION) {
 | 
			
		||||
        (evalResult.result as Node[]).forEach((node: any) => {
 | 
			
		||||
            outputChannel.appendLine(`[Line ${node.lineNumber}] ${node.localName}: ${node.textContent}`);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    else {
 | 
			
		||||
        outputChannel.appendLine(`[Result]: ${evalResult.result}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    outputChannel.show(ViewColumn.Three);
 | 
			
		||||
 | 
			
		||||
    if (persistQueries) {
 | 
			
		||||
        const historicQuery = new HistoricQuery(editor.document.uri.toString(), query);
 | 
			
		||||
 | 
			
		||||
        const affectedIndex = history.findIndex(x => x.uri === historicQuery.uri);
 | 
			
		||||
 | 
			
		||||
        if (affectedIndex === -1) {
 | 
			
		||||
            history.push(historicQuery);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        else {
 | 
			
		||||
            history[affectedIndex].query = query;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        memento.update(constants.stateKeys.xpathQueryHistory, history);
 | 
			
		||||
        memento.update(constants.stateKeys.xPathQueryLast, query);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HistoricQuery {
 | 
			
		||||
    constructor(uri: string, query: string) {
 | 
			
		||||
        this.uri = uri;
 | 
			
		||||
        this.query = query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uri: string;
 | 
			
		||||
    query: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								src/xpath/xpath-evaluator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/xpath/xpath-evaluator.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
import * as xpath from "xpath";
 | 
			
		||||
import { SelectedValue, XPathSelect } from "xpath";
 | 
			
		||||
import { DOMParser } from "xmldom";
 | 
			
		||||
 | 
			
		||||
export class EvaluatorResult {
 | 
			
		||||
    type: EvaluatorResultType;
 | 
			
		||||
    result: Node[] | number | string | boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class EvaluatorResultType {
 | 
			
		||||
    static SCALAR_TYPE = 0;
 | 
			
		||||
    static NODE_COLLECTION = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class XPathEvaluator {
 | 
			
		||||
    static evaluate(query: string, xml: string, ignoreDefaultNamespace: boolean): EvaluatorResult {
 | 
			
		||||
        if (ignoreDefaultNamespace) {
 | 
			
		||||
            xml = xml.replace(/xmlns=".+"/g, (match: string) => {
 | 
			
		||||
                return match.replace(/xmlns/g, "xmlns:default");
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const nodes = new Array<Node>();
 | 
			
		||||
        const xdoc: Document = new DOMParser().parseFromString(xml, "text/xml");
 | 
			
		||||
        const resolver = (xpath as any).createNSResolver(xdoc);
 | 
			
		||||
        const xPathResult = xpath.evaluate(query, xdoc, resolver, 0, null);
 | 
			
		||||
 | 
			
		||||
        const evaluatorResult = new EvaluatorResult();
 | 
			
		||||
        evaluatorResult.type = EvaluatorResultType.SCALAR_TYPE;
 | 
			
		||||
 | 
			
		||||
        switch (xPathResult.resultType) {
 | 
			
		||||
            case xPathResult.NUMBER_TYPE:
 | 
			
		||||
                evaluatorResult.result = xPathResult.numberValue;
 | 
			
		||||
                break;
 | 
			
		||||
            case xPathResult.STRING_TYPE:
 | 
			
		||||
                evaluatorResult.result = xPathResult.stringValue;
 | 
			
		||||
                break;
 | 
			
		||||
            case xPathResult.BOOLEAN_TYPE:
 | 
			
		||||
                evaluatorResult.result = xPathResult.booleanValue;
 | 
			
		||||
                break;
 | 
			
		||||
            case xPathResult.UNORDERED_NODE_ITERATOR_TYPE:
 | 
			
		||||
            case xPathResult.ORDERED_NODE_ITERATOR_TYPE:
 | 
			
		||||
                evaluatorResult.result = xPathResult.booleanValue;
 | 
			
		||||
 | 
			
		||||
                let node: Node;
 | 
			
		||||
 | 
			
		||||
                while (node = xPathResult.iterateNext()) {
 | 
			
		||||
                    nodes.push(node);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                evaluatorResult.result = nodes;
 | 
			
		||||
                evaluatorResult.type = EvaluatorResultType.NODE_COLLECTION;
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return evaluatorResult;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue