1020 lines
27 KiB
JavaScript
1020 lines
27 KiB
JavaScript
/** @file
|
|
PatchXml tool to modify a XML based on patch syntax
|
|
|
|
@copyright
|
|
Copyright (c) 2013 Intel Corporation. All rights reserved
|
|
This software and associated documentation (if any) is furnished
|
|
under a license and may only be used or copied in accordance
|
|
with the terms of the license. Except as permitted by such
|
|
license, no part of this software or documentation may be
|
|
reproduced, stored in a retrieval system, or transmitted in any
|
|
form or by any means without the express written consent of
|
|
Intel Corporation.
|
|
|
|
@version 1.0
|
|
Initial release.
|
|
|
|
**/
|
|
|
|
/*
|
|
Usage:
|
|
cscript PatchXml.js <input> <output> -patch <patch> [[-patch <patch>] ...]
|
|
|
|
Options:
|
|
-patch patch file with patching syntax, supports multiple files
|
|
-help
|
|
|
|
Remarks:
|
|
It is compulsory to include input, output and at least one patch file
|
|
|
|
Examples:
|
|
template.xml contains:
|
|
<root attrib="foo">
|
|
<a/>
|
|
<b foo=" "/>
|
|
</root>
|
|
|
|
patch.p contains:
|
|
# use hash sign to comment a line
|
|
/root/b {
|
|
@foo = "foo"
|
|
append @bar = "bar"
|
|
text "baz"
|
|
}
|
|
/root append /c
|
|
delete /root/a
|
|
/root delete @attrib
|
|
include "anotherPatch.p"
|
|
|
|
anotherPatch.p contains:
|
|
/root append /d
|
|
|
|
And invoked using this:
|
|
cscript PatchXml.js template.xml output.xml -patch patch.p
|
|
|
|
output.xml would contain:
|
|
<root>
|
|
<b foo="foo" bar="bar">baz</b>
|
|
<c/>
|
|
<d/>
|
|
</root>
|
|
|
|
The detailed BNF syntax for the patch file:
|
|
{} means repetition
|
|
[] means optional
|
|
<> means non-terminal
|
|
| means choice
|
|
|
|
<patchxml> ::= {<expression>}
|
|
<command> ::= <expression> | <block>
|
|
<block> ::= '{' {<expression>} '}'
|
|
<expression> ::= XPATH <command> |
|
|
for XPATH <command> |
|
|
'@'<attrib> = <value> |
|
|
'text' <value> |
|
|
'append' <appendOperand> |
|
|
'delete' <deleteOperand> |
|
|
'include' <includeOperand> |
|
|
'import' <importOperand>
|
|
<appendOperand> ::= '/'<node> [<block>] |
|
|
'@'<attrib> '=' <value>
|
|
<deleteOperand> ::= XPATH |
|
|
'@'<attrib>
|
|
<includeOperand> ::= <filename>
|
|
<importOperand> ::= <filename>
|
|
|
|
<value> ::= STRING | 'subst' '('STRING')'
|
|
<node> ::= NAME
|
|
<attrib> ::= NAME
|
|
<filename> ::= STRING
|
|
|
|
Terminal symbols:
|
|
''
|
|
All single quoted symbols are literals.
|
|
NAME
|
|
Alphanumeric characters and "_" starts with alphabet or "_"
|
|
STRING
|
|
A quoted string with the only escape sequences: "\"" and "\n".
|
|
XPATH
|
|
The XPath syntax to specify a node.
|
|
Currently XPATH only supports node expression with prefix of "/".
|
|
XPath selector can be used to point to specific node if any node have duplicating names. Selector are enclosed in square brackets "[]"
|
|
* XPath information can be refered from
|
|
http://www.w3schools.com/xpath/xpath_syntax.asp
|
|
http://msdn.microsoft.com/en-us/library/windows/desktop/ms256471.aspx
|
|
|
|
Operators:
|
|
'@'<attrib> = <value>
|
|
Modify an attribute of current node. The attribute must exist.
|
|
For attribute named 'value', will check for 'value_list' for
|
|
valid options if present.
|
|
text
|
|
Modify the text in a node. Current node must contain text node. Text also
|
|
can be used to append text when appending new child node. See example.
|
|
append
|
|
Append a node as a child of current node. The new child node can have
|
|
the same name with their siblings.
|
|
Also used to append an attribute to current node. The new attribute
|
|
must not exist.
|
|
delete
|
|
Removes attribute or child node (including all descendant elements)
|
|
include
|
|
Parse another patch file from current patch file. This is same as
|
|
specifying multiple patch files from -patch option
|
|
import
|
|
Import the root of another xml file and replace with current node
|
|
for
|
|
Perform command on all nodes matching query
|
|
|
|
Definition of current node:
|
|
Is the node specified by a XPath
|
|
E.g.:
|
|
/root/childA @attrib = "foo"
|
|
|
|
|
[current node]
|
|
|
|
Comment line:
|
|
Comments starts with the '#' character and can only be place in the
|
|
beginning or the ending of a complete expression
|
|
|
|
*/
|
|
|
|
var exit = function (n) { WScript.Quit (n); }
|
|
var log = function (s) { WScript.StdOut.Write (s + '\n'); }
|
|
var debug = function (s) {};
|
|
var error = function (s) { WScript.StdErr.Write (s + '\n'); }
|
|
var shell = WScript.CreateObject ("WScript.Shell");
|
|
|
|
var forReading = 1;
|
|
var fso = new ActiveXObject ("Scripting.FileSystemObject");
|
|
|
|
/**
|
|
Parser object is used to parse a patch file. Object can be created from main
|
|
function, where this is the main patch file, or it can also be created within
|
|
this parser object when "include" operation is used.
|
|
**/
|
|
function Parser (f, xmlDom) {
|
|
//
|
|
// keeps a reference to xmlDom to use the createNode method
|
|
//
|
|
this.xmlDom = xmlDom;
|
|
|
|
this.filename = f;
|
|
this.dir = dirname (f);
|
|
this.location = 0;
|
|
this.line = 1;
|
|
this.peekedChar = ''
|
|
|
|
//
|
|
// Read patch file
|
|
//
|
|
var file = fso.OpenTextFile (this.filename, forReading);
|
|
this.text = file.ReadAll ();
|
|
|
|
//
|
|
// Read the first character and put in peekedChar
|
|
//
|
|
this.next ();
|
|
}
|
|
|
|
/**
|
|
Parser::parsePatchXml the main function to start parsing the patch file.
|
|
There are three place to call this function (1) from main, (2) from include
|
|
operation in patch file without context, or (3) from include operation with
|
|
context. For (1) and (2), pass in the XMLDOM, while for (3), pass in the
|
|
current node
|
|
**/
|
|
Parser.prototype.parsePatchXml = function (node) {
|
|
debug ("parsePatchXml () started");
|
|
|
|
//
|
|
// Consume comments before any statement
|
|
//
|
|
this.consumeAll ();
|
|
while (!this.isEof ()) {
|
|
//
|
|
// while not eof of patch, repeat to handle multiple statement in patch file
|
|
// Consume comments after each statement
|
|
//
|
|
this.parseExpression (node);
|
|
this.consumeAll ();
|
|
}
|
|
debug ("parsePatchXml () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::parseCommand Handles expression or block
|
|
**/
|
|
Parser.prototype.parseCommand = function (node) {
|
|
debug ("parseCommand () started");
|
|
if (this.peek () == "{") {
|
|
this.parseBlock (node);
|
|
} else {
|
|
this.parseExpression (node);
|
|
}
|
|
debug ("parseCommand () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::parseBlock Repeatedly parse expression until "}" is met
|
|
**/
|
|
Parser.prototype.parseBlock = function (node) {
|
|
debug ("parseBlock () started");
|
|
//
|
|
// Consume "{" and the whitespace right after that
|
|
//
|
|
this.next ();
|
|
this.consumeAll ();
|
|
|
|
do {
|
|
//
|
|
// parse the content multiple times and consume white space after any
|
|
// repeated operation
|
|
//
|
|
this.parseExpression (node);
|
|
this.consumeAll ();
|
|
} while (this.peek () != "}");
|
|
|
|
//
|
|
// consume "}"
|
|
//
|
|
this.next ();
|
|
|
|
//
|
|
// consume white space right after "}"
|
|
//
|
|
this.consumeAll ();
|
|
debug ("parseBlock () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::parseExpression Handles more paths or operation
|
|
**/
|
|
Parser.prototype.parseExpression = function (node) {
|
|
debug ("parseExpression () started");
|
|
if (this.peek () == "/") {
|
|
|
|
//
|
|
// This is when the statement starts from a path
|
|
//
|
|
var path = this.parsePath ();
|
|
node = selectSingleNodeSafe (node, path);
|
|
|
|
//
|
|
// Pass the found node
|
|
//
|
|
this.parseCommand (node);
|
|
} else if (this.peek () == ".") {
|
|
throw Error ("XPath must begin with '/'");
|
|
} else {
|
|
this.parseOperation (node);
|
|
}
|
|
debug ("parseExpression () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::parsePath detects the path. Characters complies with XPath syntax.
|
|
**/
|
|
Parser.prototype.parsePath = function () {
|
|
debug ("parsePath () started");
|
|
var xpath = "";
|
|
while (/[\/\.\[\*a-zA-Z0-9_]/.test (this.peek ())) {
|
|
if (this.peek () == "[") {
|
|
xpath += this.parseSelectorOpt ();
|
|
} else {
|
|
xpath += this.next ();
|
|
}
|
|
}
|
|
//
|
|
// Consume white space before start of grouping
|
|
//
|
|
this.consumeWhiteSpace ();
|
|
debug ("Path: " + xpath);
|
|
debug ("parsePath () ended");
|
|
return xpath;
|
|
}
|
|
|
|
/**
|
|
Parser::parseOperation Corresponding function will be called to handle the
|
|
operations. If '@' is peeked, modifyAttribute will be called, else will
|
|
parse the operator
|
|
**/
|
|
Parser.prototype.parseOperation = function (node) {
|
|
debug ("parseOperation () started");
|
|
|
|
if (this.peek () == "@") {
|
|
this.changeAttribute (node, true);
|
|
} else {
|
|
//
|
|
// detect the first word, and find from switch case
|
|
//
|
|
var operator = this.parseName ();
|
|
switch (operator) {
|
|
case "text":
|
|
this.modifyNode (node);
|
|
break;
|
|
case "append":
|
|
this.appendOperand (node);
|
|
break;
|
|
case "delete":
|
|
this.deleteOperand (node);
|
|
break;
|
|
case "include":
|
|
this.includeOperand (node);
|
|
break;
|
|
case "import":
|
|
this.importOperand (node);
|
|
break;
|
|
case "for":
|
|
this.forStatement (node);
|
|
break;
|
|
default:
|
|
throw Error ("No such operator: \"" + operator + "\"");
|
|
}
|
|
}
|
|
debug ("parseOperation () ended");
|
|
}
|
|
|
|
/**
|
|
Perform following command for each node found by XPath query.
|
|
Since we don't build AST, just re-start parser from saved location.
|
|
**/
|
|
Parser.prototype.forStatement = function (node) {
|
|
debug ("forStatement () started");
|
|
var path = this.parsePath ();
|
|
var saveLocation = this.location;
|
|
var savePeekedChar = this.peekedChar;
|
|
var nodes = node.selectNodes (path);
|
|
if (nodes.length == 0) {
|
|
throw Error ("Cannot find any match from node \"" + node.nodeName + "\" using this path : " + path);
|
|
}
|
|
for (var n = 0; n < nodes.length; ++n) {
|
|
this.location = saveLocation;
|
|
this.peekedChar = savePeekedChar;
|
|
this.parseCommand (nodes[n]);
|
|
}
|
|
debug ("forStatement () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::appendOperand a child node or attribute. If append node, it will not
|
|
check if the node is there or no. But if the attribute is there, it will
|
|
throw error.
|
|
**/
|
|
Parser.prototype.appendOperand = function (node) {
|
|
debug ("appendOperand () started");
|
|
|
|
//
|
|
// consume "/" or "@"
|
|
//
|
|
if (this.peek () == "/") {
|
|
this.appendNode (node);
|
|
} else if (this.peek () == "@") {
|
|
this.changeAttribute (node, false);
|
|
} else {
|
|
throw Error ("Append operation only accepts '/' for node and '@' for attribute");
|
|
}
|
|
debug ("appendOperand () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::appendNode helper function for appendOperand
|
|
**/
|
|
Parser.prototype.appendNode = function (node) {
|
|
//
|
|
// consume "/"
|
|
//
|
|
this.next ();
|
|
var nodeToAppend = this.parseName ();
|
|
var toAppend = this.xmlDom.createElement (nodeToAppend);
|
|
|
|
//
|
|
// append the node here
|
|
//
|
|
node.appendChild (toAppend);
|
|
|
|
//
|
|
// detect the grouping here.
|
|
//
|
|
if (this.peek () == "{") {
|
|
this.parseBlock (toAppend);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Parser::changeAttribute modify or append an attribute to a node.
|
|
Use the modifyFlag to specify modify or append attribute
|
|
**/
|
|
Parser.prototype.changeAttribute = function (node, modifyFlag) {
|
|
debug ("changeAttribute () started");
|
|
//
|
|
// consume "@"
|
|
//
|
|
this.next ();
|
|
var attribToChange = this.parseName ();
|
|
|
|
if (this.peek () != "=") {
|
|
throw Error ("Please adhere to syntax to append/modify attribute: @attrib = value");
|
|
}
|
|
//
|
|
// Consume "="
|
|
//
|
|
this.next ();
|
|
var valueToChange = this.parseValue ();
|
|
|
|
//
|
|
// check the existance of the attribute
|
|
//
|
|
if (modifyFlag) {
|
|
if (!node.getAttributeNode (attribToChange)) {
|
|
throw Error ("\"" + attribToChange + "\" does not exist for modification, missing 'append'?");
|
|
}
|
|
} else {
|
|
if (node.getAttributeNode (attribToChange)) {
|
|
throw Error ("Attribute \"" + attribToChange + "\" already exists");
|
|
}
|
|
}
|
|
node.setAttribute (attribToChange, valueToChange);
|
|
if (attribToChange == "value") {
|
|
this.checkValueList (valueToChange, node.getAttribute ("value_list"));
|
|
}
|
|
debug ("changeAttribute () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::modifyNode will append a text node regardless it existed or no,
|
|
and append a text to the text node.
|
|
**/
|
|
Parser.prototype.modifyNode = function (node) {
|
|
debug ("modifyNode () started");
|
|
var valueToModify = this.parseValue ();
|
|
|
|
node.appendChild (this.xmlDom.createTextNode (valueToModify));
|
|
debug ("modifyNode () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::deleteOperand will check if the node is there or no. If it is not,
|
|
throw error.
|
|
**/
|
|
Parser.prototype.deleteOperand = function (node) {
|
|
debug ("deleteOperand () started");
|
|
|
|
//
|
|
// detect the first "/" or "@"
|
|
//
|
|
if (this.peek () == "/") {
|
|
var nodeToDelete = this.parsePath ();
|
|
var toDelete = selectSingleNodeSafe (node, nodeToDelete);
|
|
|
|
//
|
|
// delete node here
|
|
//
|
|
toDelete.parentNode.removeChild (toDelete);
|
|
} else if (this.peek () == "@") {
|
|
//
|
|
// Consume "@"
|
|
//
|
|
this.next ();
|
|
|
|
var attribToDelete = this.parseName ();
|
|
|
|
//
|
|
// delete attrib here
|
|
//
|
|
if (!node.getAttributeNode (attribToDelete)) {
|
|
throw Error ("\"" + attribToDelete + "\" does not exist to delete");
|
|
}
|
|
node.removeAttribute (attribToDelete);
|
|
} else {
|
|
throw Error ("Delete operation only accepts '/' for node and '@' for attribute");
|
|
}
|
|
debug ("deleteOperand () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::includeOperand another patch file. New Parser will be created to
|
|
handle this.
|
|
**/
|
|
Parser.prototype.includeOperand = function (node) {
|
|
debug ("includeOperand () started");
|
|
|
|
var f = this.parseString ();
|
|
|
|
//
|
|
// Check for existence before constructing a new parser
|
|
//
|
|
if (!fso.FileExists (this.dir + f)) {
|
|
throw Error ("File does not exists: \"" + this.dir + f + "\"");
|
|
}
|
|
|
|
try {
|
|
var includePatch = new Parser (this.dir + f, this.xmlDom);
|
|
includePatch.parsePatchXml (node);
|
|
} catch (e) {
|
|
error (includePatch.filename + "(" + includePatch.line + ") : error : " + e.message);
|
|
throw Error ("Included patch file faulty");
|
|
}
|
|
|
|
debug ("includeOperand () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::importOperand will import the root of another xml file to the
|
|
specified node. Current node will be replaced with root of new xml file
|
|
**/
|
|
Parser.prototype.importOperand = function (node) {
|
|
debug ("importOperand () started");
|
|
|
|
var f = this.parseString ();
|
|
var importDom = readXmlFile (this.dir + f);
|
|
node.parentNode.replaceChild (importDom.documentElement, node);
|
|
|
|
debug ("importOperand () ended");
|
|
}
|
|
|
|
/**
|
|
Parser::parseName Alphanumeric, and "_" which starts with Alphabet or "_"
|
|
**/
|
|
Parser.prototype.parseName = function () {
|
|
debug ("parseName () started");
|
|
this.consumeWhiteSpace ();
|
|
|
|
var name = "";
|
|
//
|
|
// Detect the first character and make sure it is alphabet or '_'
|
|
//
|
|
if (/[a-z_]/i.test (this.peek ())) {
|
|
name = this.next ();
|
|
} else {
|
|
throw Error ("Unexpected character : \"" + this.peek () + "\"");
|
|
}
|
|
|
|
//
|
|
// Detect the rest of the NAME characters
|
|
//
|
|
while (/[a-z0-9_]/i.test (this.peek ())) {
|
|
name += this.next ();
|
|
}
|
|
|
|
this.consumeWhiteSpace ();
|
|
debug ("Name: " + name + "");
|
|
debug ("parseName () ended");
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
Parser::parseString parses string quoted in double quotes. Does not return
|
|
the "\"". Will replace "\n" and "\"" escape sequence found in the string.
|
|
**/
|
|
Parser.prototype.parseString = function () {
|
|
debug ("parseString () started");
|
|
this.consumeWhiteSpace ();
|
|
|
|
var str = "";
|
|
|
|
if (this.peek () != "\"") {
|
|
throw Error ("String starts with \", unexpected character: \"" + this.peek () + "\"");
|
|
}
|
|
//
|
|
// Consume "\""
|
|
//
|
|
this.next ();
|
|
|
|
while (this.peek () != "\"") {
|
|
if (this.peek () == "\\") {
|
|
//
|
|
// consume "\\"
|
|
//
|
|
this.next ();
|
|
|
|
var c = this.peek ();
|
|
//
|
|
// Consume whatever it is and append the correct escape character
|
|
//
|
|
if (c == "\"") {
|
|
//
|
|
// Double quotation
|
|
//
|
|
this.next ();
|
|
str += "\"";
|
|
} else if (c == "\\") {
|
|
//
|
|
// Backslash
|
|
//
|
|
this.next ();
|
|
str += "\\";
|
|
} else {
|
|
//
|
|
// Everything else
|
|
//
|
|
this.next ();
|
|
str += ("\\" + c);
|
|
}
|
|
} else {
|
|
str += this.next ();
|
|
}
|
|
}
|
|
//
|
|
// Consume "\""
|
|
//
|
|
this.next ();
|
|
|
|
this.consumeWhiteSpace ();
|
|
debug ("String: " + str);
|
|
debug ("parseString () ended");
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
Parser::parseSelectorOpt will parse the "[]". If find any "\"" in the box
|
|
bracket, will call parseString to handle. Will return the "[" and "]"
|
|
characters. If "[" is not found, will return empty string
|
|
**/
|
|
Parser.prototype.parseSelectorOpt = function () {
|
|
debug ("parseSelectorOpt () started");
|
|
|
|
var sel = "";
|
|
if (this.peek () == "[") {
|
|
//
|
|
// consume and append "["
|
|
//
|
|
sel = this.next ();
|
|
while (this.peek () != "]") {
|
|
if (this.peek () == "\"") {
|
|
//
|
|
// need the "\"" in xpath but parseString is not returning them
|
|
//
|
|
sel += ("\"" + this.parseString () + "\"");
|
|
} else {
|
|
sel += this.next ();
|
|
}
|
|
}
|
|
//
|
|
// consume and append "]"
|
|
//
|
|
sel += this.next ();
|
|
}
|
|
debug ("Selector: " + sel);
|
|
debug ("parseSelectorOpt () ended");
|
|
return sel;
|
|
}
|
|
|
|
/**
|
|
Parser::parseValue will detect if it is a string or the "subst" operator.
|
|
If "subst" operator found, will substitute the envorinment variable. Will
|
|
throw error if the substitution failed.
|
|
**/
|
|
Parser.prototype.parseValue = function () {
|
|
debug ("parseValue () started");
|
|
this.consumeWhiteSpace ();
|
|
|
|
var val = "";
|
|
if (this.peek () != "\"") {
|
|
var subst = this.parseName ();
|
|
if (subst != "subst") {
|
|
throw Error ("Unexpected operator: \"" + subst + "\"");
|
|
}
|
|
if (this.next () != "(") {
|
|
throw Error ("Please adhere to syntax to use environment variable substitution: subst (STRING)");
|
|
}
|
|
var str = this.parseString ();
|
|
val = shell.ExpandEnvironmentStrings (str);
|
|
if (val == str) {
|
|
throw Error ("Substitution failed for this string: \"" + str + "\"");
|
|
}
|
|
if (this.next () != ")") {
|
|
throw Error ("Missing ')'");
|
|
}
|
|
} else {
|
|
val = this.parseString ();
|
|
}
|
|
debug ("Value : " + val);
|
|
debug ("parseValue () ended");
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
Parser::consumeWhiteSpace Consume white spaces
|
|
**/
|
|
Parser.prototype.consumeWhiteSpace = function () {
|
|
while (/\s/.test (this.peek ())) {
|
|
this.next ();
|
|
}
|
|
}
|
|
|
|
/**
|
|
Parser::consumeComment Consume multiline comments, as long as the "#" character is at the first column
|
|
**/
|
|
Parser.prototype.consumeComment = function () {
|
|
while (this.peek () == "#") {
|
|
do {
|
|
var c = this.next();
|
|
} while (!this.isEof () && c != '\n');
|
|
}
|
|
}
|
|
|
|
/**
|
|
Parser::consumeAll Consume comments and white spaces
|
|
**/
|
|
Parser.prototype.consumeAll = function () {
|
|
do {
|
|
this.consumeWhiteSpace ();
|
|
this.consumeComment ();
|
|
} while (/\s/.test (this.peek ()));
|
|
}
|
|
|
|
/**
|
|
Parser::checkValueList user are required to determine if "value" is a valid
|
|
attribute or not before calling this function.
|
|
**/
|
|
Parser.prototype.checkValueList = function (value, list) {
|
|
if (!list) return;
|
|
var values = list.split(",,");
|
|
for (var i = 0; i < values.length; ++i) {
|
|
if (values[i] == value) return;
|
|
}
|
|
throw Error("Value: \"" + value + "\" is not one of:\n " + values.join('\n '));
|
|
}
|
|
|
|
/**
|
|
Parser::peek gives the next character in the stream but do not consume it.
|
|
Because the file system object does not support peek function, the next
|
|
character has to be read out and stored in a variable.
|
|
**/
|
|
Parser.prototype.peek = function () {
|
|
return this.peekedChar;
|
|
}
|
|
|
|
/**
|
|
Parser::next gives the next character in the stream
|
|
**/
|
|
Parser.prototype.next = function () {
|
|
var c = this.peekedChar;
|
|
if (this.location >= this.text.length) {
|
|
if (this.isEof ()) {
|
|
throw Error ("Unexpected end of file");
|
|
}
|
|
this.peekedChar = "";
|
|
} else {
|
|
if (this.peekedChar == '\n') {
|
|
++this.line;
|
|
}
|
|
this.peekedChar = this.text.charAt(this.location++);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
Parser::isEof peekedChar is checked against 0 because it will be set in next
|
|
function if AtEndOfStream is set. AtEndOfStream property cannot be used here
|
|
because the final character is still stored in peekedChar even after
|
|
AtEndOfStream is set.
|
|
**/
|
|
Parser.prototype.isEof = function () {
|
|
return (this.peekedChar === "");
|
|
}
|
|
|
|
/**
|
|
Provides a safe method to select single node from a parent node. This will
|
|
check to make sure the selected child nodes are not ambiguous
|
|
**/
|
|
function selectSingleNodeSafe (parent, childPath) {
|
|
debug ("selectSingleNodeSafe () started");
|
|
//
|
|
// Replacing any occurance of '/' at the beginning of a path with './' .
|
|
// This gives the workaround to selectNodes from xmlDom and any child nodes.
|
|
// This also works to change '//foo' to './/foo'
|
|
//
|
|
childPath = childPath.replace (/^\//, "./");
|
|
|
|
var node = parent.selectNodes (childPath);
|
|
if (!node.length) {
|
|
throw Error ("Cannot find child node from parent node \"" + parent.nodeName + "\" using this path : " + childPath);
|
|
} else if (node.length > 1) {
|
|
throw Error ("Cannot identify non-ambiguous child from parent node \"" + parent.nodeName + "\" using this path : " + childPath);
|
|
}
|
|
|
|
debug ("Found child \"" + childPath + "\" from parent node \"" + parent.nodeName + "\"");
|
|
debug ("selectSingleNodeSafe () ended");
|
|
return node[0];
|
|
}
|
|
|
|
/**
|
|
Process the input arguments and store them in array to be used. Currrently,
|
|
supports one input xml, one output, but multiple patch files.
|
|
**/
|
|
function processArgs (args) {
|
|
var opt;
|
|
var optList = { input : 0,
|
|
patch : [],
|
|
output : 0,
|
|
debug : 0,
|
|
help : 0};
|
|
|
|
for (var index = 0; index < args.length; ++index) {
|
|
opt = args (index);
|
|
if (opt == "-patch") {
|
|
if (index < (args.length - 1)) {
|
|
optList.patch.push (args (++index));
|
|
} else {
|
|
throw Error ("-patch option is not complete");
|
|
}
|
|
} else if (opt == "-help" || opt == "--help" || opt == "-h") {
|
|
optList.help = 1;
|
|
} else if (opt == "-debug") {
|
|
optList.debug = 1;
|
|
} else {
|
|
//
|
|
// For input and output file without "-" option
|
|
//
|
|
if (!optList.input) {
|
|
optList.input = opt;
|
|
} else if (!optList.output) {
|
|
optList.output = opt;
|
|
} else {
|
|
throw Error ("Option not supported: " + opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
return optList;
|
|
}
|
|
|
|
/**
|
|
Create ActiveXObject from list. User is responsible to check for success
|
|
**/
|
|
function createObjectFromList (list) {
|
|
var obj;
|
|
for (var i in list) {
|
|
try {
|
|
obj = new ActiveXObject (list[i]);
|
|
break;
|
|
} catch (e) {}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
Returns the path with trailing "/"
|
|
**/
|
|
function dirname (path) {
|
|
return path.replace (/\\/g,'/').replace (/[^\/]*$/, "");
|
|
}
|
|
|
|
/**
|
|
Open a XML file by creating XMLDOM object
|
|
**/
|
|
function readXmlFile (f) {
|
|
var xmlDom = createObjectFromList (["Msxml2.DOMDocument.6.0", "Msxml2.DOMDocument", "Microsoft.XMLDOM"]);
|
|
if (!xmlDom) {
|
|
throw Error ("Create XMLDOM object failed");
|
|
}
|
|
xmlDom.async = false;
|
|
xmlDom.load (f);
|
|
var err = xmlDom.parseError;
|
|
if (err.errorCode != 0) {
|
|
error (err.url.replace ("file:///", "") + '(' + err.line + ") : error : "
|
|
+ err.reason + err.srcText);
|
|
throw Error ("Cannot read file.");
|
|
}
|
|
return xmlDom;
|
|
}
|
|
|
|
/**
|
|
This function is needed because DOMDocument could not print pretty. However,
|
|
user should validate the existence of reader and writer before using this
|
|
save file routine
|
|
**/
|
|
function saveDomWithIndent (dom, f, rdr, wrtr) {
|
|
var textStream = fso.CreateTextFile (f, true);
|
|
wrtr.indent = true;
|
|
|
|
wrtr.omitXMLDeclaration = (dom.firstChild.nodeTypeString != "processinginstruction");
|
|
var enc = wrtr.encoding;
|
|
wrtr.encoding = "utf-8";
|
|
rdr.contentHandler = wrtr;
|
|
rdr.parse (dom);
|
|
textStream.Write (wrtr.output);
|
|
textStream.Close ();
|
|
}
|
|
|
|
/**
|
|
Print the usage of script
|
|
**/
|
|
function usage () {
|
|
log ("\n\
|
|
PatchXml Tool \n\
|
|
\n\
|
|
Usage: \n\
|
|
cscript PatchXml.js <input> <output> -patch <patch> [[-patch <patch>] ...]\n\
|
|
\n\
|
|
Options: \n\
|
|
-patch patch file with patching syntax, supports multiple files \n\
|
|
-help \n\
|
|
\n\
|
|
Remarks: \n\
|
|
It is compulsory to include input, output and at least one patch file \n\
|
|
Please also refer to header comment for patch format description \n");
|
|
}
|
|
|
|
/**
|
|
Main is here
|
|
**/
|
|
function main () {
|
|
debug ("main () started");
|
|
|
|
//
|
|
// Print usage if no arguments are given
|
|
//
|
|
if (WScript.arguments.length == 0) {
|
|
usage ();
|
|
return 0;
|
|
}
|
|
//
|
|
// Process input arguments
|
|
//
|
|
var options = processArgs (WScript.arguments);
|
|
if (options.help) {
|
|
usage ();
|
|
return 0;
|
|
}
|
|
if (options.debug) {
|
|
debug = log;
|
|
}
|
|
|
|
debug (shell.CurrentDirectory);
|
|
debug (WScript.ScriptFullName);
|
|
debug (WScript.ScriptName);
|
|
|
|
//
|
|
// Checks if any of the arguments are not specified
|
|
//
|
|
if (options.input == "") {
|
|
throw Error ("Input xml file is not specified");
|
|
} else if (options.patch.length == 0) {
|
|
throw Error ("Patch file(s) is not specified");
|
|
} else if (options.output == "") {
|
|
throw Error ("Output file is not specified");
|
|
}
|
|
|
|
//
|
|
// Checks for the existence of each files
|
|
//
|
|
if (!fso.FileExists (options.input)) {
|
|
throw Error ("File does not exists: \"" + options.input + "\"");
|
|
} else {
|
|
for (var i in options.patch) {
|
|
if (!fso.FileExists (options.patch[i])) {
|
|
throw Error ("File does not exists: \"" + options.patch[i] + "\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Read the input xml. Only the first option given will be used
|
|
//
|
|
var xmlDom = readXmlFile (options.input);
|
|
|
|
//
|
|
// Parse the patch files. Multiple patch files can be used
|
|
//
|
|
debug ("Parser started");
|
|
for (var i in options.patch) {
|
|
debug (options.patch[i] + " started");
|
|
try {
|
|
var parser = new Parser (options.patch[i], xmlDom);
|
|
parser.parsePatchXml (xmlDom);
|
|
} catch (e) {
|
|
error (parser.filename + "(" + parser.line + ") : error : " + e.message);
|
|
throw Error ("Patch file faulty");
|
|
}
|
|
debug (options.patch[i] + " ended");
|
|
}
|
|
debug ("Parser ended");
|
|
|
|
//
|
|
// Write the output xml. If the MXXMLWriter or SAXXMLReader are not available,
|
|
// write the xml using XMLDOM object without indentation
|
|
//
|
|
reader = createObjectFromList (["Msxml2.SAXXMLReader.6.0", "Msxml2.SAXXMLReader"]);
|
|
writer = createObjectFromList (["Msxml2.MXXMLWriter.6.0", "Msxml2.MXXMLWriter"]);
|
|
if (reader && writer) {
|
|
saveDomWithIndent (xmlDom, options.output, reader, writer);
|
|
log ("Written XML: " + options.output);
|
|
} else {
|
|
xmlDom.save (options.output);
|
|
log ("Written XML without indentation: " + options.output);
|
|
}
|
|
|
|
debug ("main () ended");
|
|
return 0;
|
|
}
|
|
|
|
try {
|
|
exit (main());
|
|
} catch (e) {
|
|
error (e.name + " : " + e.message);
|
|
exit (1);
|
|
}
|