Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/* The serialized file format is pretty generic... each line (using any line
* separator, so we don't mind being moved between platforms) consists of
* a command name, and some parameters (optionally). The commands 'start'
* and 'end' mark the chunks of properties for each object - in this case
* motifs. Every command inside a start/end block is considered a property
* for the object. There are some rules, but we are generally pretty flexible.
*
* Example file:
* START <Array>
* START 0
* "message" "Food%3a%20Mmm...%20food..."
* END
* START 1
* "message" "Busy%3a%20Working."
* END
* START 2
* "message" "Not%20here."
* END
* END
*
* The whitespace at the start of the inner lines is generated by the
* serialisation process, but is ignored when parsing - it is only to make
* the file more readable.
*
* The START command may be followed by one or both of a class name (enclosed
* in angle brackets, as above) and a property name (the first non-<>-enclosed
* word). Top-level START commands must not have a property name, although a
* class name is fine. Only the following class names are supported:
* - Object (the default)
* - Array
*
* For arrays, there are some limitations; saving an array cannot save any
* properties that are not numerics, due to limitations in JS' for...in
* enumeration. Thus, for loading, only items with numeric property names are
* allowed. If an item is STARTed inside an array, and specifies no property
* name, it will be push()ed into the array instead.
*/
function TextSerializer(file) {
this._initialized = false;
this._file = returnFile(file);
this._open = false;
this._buffer = "";
this._lines = [];
this.lineEnd = "\n";
this._initialized = true;
}
/* open(direction)
*
* Opens the serializer on the file specified when created, in either the read
* ("<") or write (">") directions. When the file is open, only the appropriate
* direction of serialization/deserialization may be performed.
*
* Note: serialize and deserialize automatically open the file if it is not
* open.
*/
TextSerializer.prototype.open = function (dir) {
if (!ASSERT(dir == ">" || dir == "<", "Bad serialization direction!")) {
return false;
}
if (this._open) {
return false;
}
this._fileStream = new LocalFile(this._file, dir);
if (typeof this._fileStream == "object" && this._fileStream) {
this._open = true;
}
return this._open;
};
/* close()
*
* Closes the file stream and ends reading or writing.
*/
TextSerializer.prototype.close = function () {
if (this._open) {
this._fileStream.close();
delete this._fileStream;
this._open = false;
}
return true;
};
/* deserialize()
*
* Reads in enough of the file to deserialize (realize) a single object. The
* object deserialized is returned; all sub-properties of the object are
* deserialized with it.
*/
TextSerializer.prototype.deserialize = function () {
if (!this._open) {
this.open("<");
}
if (!ASSERT(this._open, "Unable to open the file for reading!")) {
return false;
}
var obj = null;
var rv = null;
var objs = [];
while (true) {
if (this._lines.length == 0) {
var newData = this._fileStream.read();
if (newData) {
this._buffer += newData;
} else if (this._buffer.length == 0) {
break;
}
// Got more data in the buffer, so split into lines. Unless we're
// done, the last one might not be complete yet, so save that one.
var lines = this._buffer.split(/[\r\n]+/);
if (!newData) {
this._buffer = "";
} else {
this._buffer = lines.pop();
}
this._lines = this._lines.concat(lines);
if (this._lines.length == 0) {
break;
}
}
// Split each line into "command params...".
var parts = this._lines[0].match(/^\s*(\S+)(?:\s+(.*))?$/);
var command = parts[1];
var params = parts[2];
// 'start' and 'end' commands are special.
switch (command.toLowerCase()) {
case "start":
var paramList = [];
if (params) {
paramList = params.split(/\s+/g);
}
var className = "";
if (paramList.length > 0 && /^<\w+>$/i.test(paramList[0])) {
className = paramList[0].substr(1, paramList[0].length - 2);
paramList.shift();
}
if (!rv) {
/* The top-level objects are not allowed a property name
* in their START command (it is meaningless).
*/
ASSERT(paramList.length == 0, "Base object with name!");
// Construct the top-level object.
if (className) {
rv = obj = new window[className]();
} else {
rv = obj = {};
}
} else {
var n;
if (paramList.length == 0) {
/* Create a new object level, but with no name. This is
* only valid if the parent level is an array.
*/
if (!ASSERT(isinstance(obj, Array), "Parent not Array!")) {
return null;
}
if (className) {
n = new window[className]();
} else {
n = {};
}
objs.push(obj);
obj.push(n);
obj = n;
} else {
/* Create a new object level, store the reference on the
* parent, and set the new object as the current.
*/
if (className) {
n = new window[className]();
} else {
n = {};
}
objs.push(obj);
obj[ecmaUnescape(paramList[0])] = n;
obj = n;
}
}
this._lines.shift();
break;
case "end":
this._lines.shift();
if (rv && objs.length == 0) {
// We're done for the day.
return rv;
}
// Return to the previous object level.
obj = objs.pop();
if (!ASSERT(obj, "Waaa! no object level to return to!")) {
return rv;
}
break;
default:
this._lines.shift();
// The property name may be enclosed in quotes.
if (command[0] == '"') {
command = command.substr(1, command.length - 2);
}
// But it is always escaped.
command = ecmaUnescape(command);
if (!obj) {
/* If we find a line that is NOT starting a new object, and
* we don't have a current object, we just assume the START
* command was missed.
*/
rv = obj = {};
}
if (params[0] == '"') {
// String
// Remove quotes, then unescape.
params = params.substr(1, params.length - 2);
obj[command] = ecmaUnescape(params);
} else if (params[0] == "/") {
// RegExp
var p = params.match(/^\/(.*)\/(\w*)$/);
if (ASSERT(p, "RepExp entry malformed, ignored!")) {
var re = new RegExp(ecmaUnescape(p[1]), p[2]);
obj[command] = re;
}
} else if (params == "null") {
// null
obj[command] = null;
} else if (params == "undefined") {
// undefined
obj[command] = undefined;
} else if (params == "true" || params == "false") {
// boolean
obj[command] = params == "true";
} // Number
else {
obj[command] = Number(params);
}
break;
}
}
return null;
};