diff --git a/core/modules/utils/linked-list.js b/core/modules/utils/linked-list.js index 45f22f90a..917069d16 100644 --- a/core/modules/utils/linked-list.js +++ b/core/modules/utils/linked-list.js @@ -15,10 +15,11 @@ function LinkedList() { LinkedList.prototype.clear = function() { // LinkedList performs the duty of both the head and tail node - this.next = Object.create(null); - this.prev = Object.create(null); - this.first = undefined; - this.last = undefined; + this.next = new LLMap(); + this.prev = new LLMap(); + // Linked list head initially points to itself + this.next.set(null, null); + this.prev.set(null, null); this.length = 0; }; @@ -41,28 +42,29 @@ Push behaves like array.push and accepts multiple string arguments. But it also accepts a single array argument too, to be consistent with its other methods. */ LinkedList.prototype.push = function(/* values */) { - var values = arguments; + var i, values = arguments; if($tw.utils.isArray(values[0])) { values = values[0]; } - for(var i = 0; i < values.length; i++) { + for(i = 0; i < values.length; i++) { _assertString(values[i]); } - for(var i = 0; i < values.length; i++) { + for(i = 0; i < values.length; i++) { _linkToEnd(this,values[i]); } return this.length; }; LinkedList.prototype.pushTop = function(value) { + var t; if($tw.utils.isArray(value)) { - for (var t=0; t 1) { nextEntry.shift(); prevEntry.shift(); } else { - list.next[value] = undefined; - list.prev[value] = undefined; + list.next.set(value,undefined); + list.prev.set(value,undefined); } list.length -= 1; }; // Sticks the given node onto the end of the list. function _linkToEnd(list,value) { - if(list.first === undefined) { - list.first = value; + var old = list.next.get(value); + var last = list.prev.get(null); + // Does it already exists? + if(old !== undefined) { + if(!Array.isArray(old)) { + old = [old]; + list.next.set(value,old); + list.prev.set(value,[list.prev.get(value)]); + } + old.push(null); + list.prev.get(value).push(last); } else { - // Does it already exists? - if(list.first === value || list.prev[value] !== undefined) { - if(typeof list.next[value] === "string") { - list.next[value] = [list.next[value]]; - list.prev[value] = [list.prev[value]]; - } else if(typeof list.next[value] === "undefined") { - // list.next[value] must be undefined. - // Special case. List already has 1 value. It's at the end. - list.next[value] = []; - list.prev[value] = [list.prev[value]]; - } - list.prev[value].push(list.last); - // We do NOT append a new value onto "next" list. Iteration will - // figure out it must point to End-of-List on its own. - } else { - list.prev[value] = list.last; - } - // Make the old last point to this new one. - if(typeof list.next[list.last] === "object") { - list.next[list.last].push(value); - } else { - list.next[list.last] = value; - } + list.next.set(value,null); + list.prev.set(value,last); + } + // Make the old last point to this new one. + if(value !== last) { + var array = list.next.get(last); + if(Array.isArray(array)) { + array[array.length-1] = value; + } else { + list.next.set(last,value); + } + list.prev.set(null,value); + } else { + // Edge case, the pushed value was already the last value. + // The second-to-last nextPtr for that value must point to itself now. + var array = list.next.get(last); + array[array.length-2] = value; } - list.last = value; list.length += 1; }; @@ -195,6 +189,20 @@ function _assertString(value) { } }; +var LLMap = function() { + this.map = Object.create(null); +}; + +// Just a wrapper so our object map can also accept null. +LLMap.prototype = { + set: function(key,val) { + (key === null) ? (this.null = val) : (this.map[key] = val); + }, + get: function(key) { + return (key === null) ? this.null : this.map[key]; + } +}; + exports.LinkedList = LinkedList; })(); diff --git a/editions/test/tiddlers/tests/test-linked-list.js b/editions/test/tiddlers/tests/test-linked-list.js index 16bb33f61..de477257d 100644 --- a/editions/test/tiddlers/tests/test-linked-list.js +++ b/editions/test/tiddlers/tests/test-linked-list.js @@ -59,10 +59,35 @@ describe("LinkedList class tests", function() { return pair; }; + // This returns an array in reverse using a LinkList's prev member. Thus + // testing that prev is not corrupt. It doesn't exist in the LinkList module + // itself to avoid full support for it. Maybe that will change later. + function toReverseArray(list) { + var visits = Object.create(null), + value = list.prev.get(null), + array = []; + while(value !== null) { + array.push(value); + var prev = list.prev.get(value); + if(Array.isArray(prev)) { + var i = (visits[value] || prev.length) - 1; + visits[value] = i; + value = prev[i]; + } else { + value = prev; + } + } + return array; + }; + // compares an array and a linked list to make sure they match up function compare(pair) { - expect(pair.list.toArray()).toEqual(pair.array); + var forward = pair.list.toArray(); + expect(forward).toEqual(pair.array); expect(pair.list.length).toBe(pair.array.length); + // Now we reverse the linked list and test it back to front, thus + // confirming that the list.prev isn't corrupt. + expect(toReverseArray(pair.list)).toEqual(forward.reverse()); return pair; }; @@ -115,7 +140,7 @@ describe("LinkedList class tests", function() { // for list.last to be anything other than a string, but I // can't figure out how to make that corruption manifest a problem. // So I dig into its private members. Bleh... - expect(typeof pair.list.last).toBe("string"); + expect(typeof pair.list.prev.get(null)).toBe("string"); }); it("can pushTop value linked to by a repeat item", function() {