418 lines
9.8 KiB
JavaScript
418 lines
9.8 KiB
JavaScript
// Derived from
|
|
// https://github.com/wooorm/linked-list/blob/d2390fe1cab9f780cfd34fa31c8fa8ede4ad674d/index.js
|
|
|
|
export const TYPE_TEXT = 'text';
|
|
|
|
// Creates a new `Iterator` for looping over the `List`.
|
|
class Iterator {
|
|
constructor(item) {
|
|
this.item = item;
|
|
}
|
|
|
|
// Move the `Iterator` to the next item.
|
|
next() {
|
|
this.value = this.item;
|
|
this.done = !this.item;
|
|
this.item = this.item ? this.item.next : undefined;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// Creates a new `Item`:
|
|
// An item is a bit like DOM node: It knows only about its "parent" (`list`),
|
|
// the item before it (`prev`), and the item after it (`next`).
|
|
export class LLNode {
|
|
// Prepends the given item *before* the item operated on.
|
|
prepend(item) {
|
|
const list = this.list;
|
|
|
|
if (!item || !item.append || !item.prepend || !item.detach) {
|
|
throw new Error(
|
|
'An argument without append, prepend, or detach methods was given to `Item#prepend`.',
|
|
);
|
|
}
|
|
|
|
// If self is detached, return false.
|
|
if (!list) {
|
|
return false;
|
|
}
|
|
if (this === item) {
|
|
return false;
|
|
}
|
|
|
|
// Detach the prependee.
|
|
const transient = this.list === item.list;
|
|
item.detach(transient);
|
|
|
|
// If self has a previous item...
|
|
if (this.prev) {
|
|
item.prev = this.prev;
|
|
this.prev.next = item;
|
|
}
|
|
|
|
// Connect the prependee.
|
|
item.next = this;
|
|
item.list = list;
|
|
|
|
// Set the previous item of self to the prependee.
|
|
this.prev = item;
|
|
|
|
// If self is the first item in the parent list, link the lists first item to
|
|
// the prependee.
|
|
if (this === list.head) {
|
|
list.head = item;
|
|
}
|
|
|
|
// If the the parent list has no last item, link the lists last item to self.
|
|
if (!list.tail) {
|
|
list.tail = this;
|
|
}
|
|
|
|
list.length++;
|
|
if (!transient) {
|
|
item._addToIndex();
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
// Appends the given item *after* the item operated on.
|
|
append(item) {
|
|
const list = this.list;
|
|
|
|
if (!item || !item.append || !item.prepend || !item.detach) {
|
|
throw new Error(
|
|
'An argument without append, prepend, or detach methods was given to `Item#append`.',
|
|
);
|
|
}
|
|
|
|
if (!list) {
|
|
return false;
|
|
}
|
|
if (this === item) {
|
|
return false;
|
|
}
|
|
|
|
// Detach the appendee.
|
|
const transient = this.list === item.list;
|
|
item.detach(transient);
|
|
|
|
// If self has a next item...
|
|
if (this.next) {
|
|
item.next = this.next;
|
|
this.next.prev = item;
|
|
}
|
|
|
|
// Connect the appendee.
|
|
item.prev = this;
|
|
item.list = list;
|
|
|
|
// Set the next item of self to the appendee.
|
|
this.next = item;
|
|
|
|
// If the the parent list has no last item or if self is the parent lists last
|
|
// item, link the lists last item to the appendee.
|
|
if (this === list.tail || !list.tail) {
|
|
list.tail = item;
|
|
}
|
|
|
|
list.length++;
|
|
if (!transient) {
|
|
item._addToIndex();
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
// Detaches the item operated on from its parent list.
|
|
detach(transient) {
|
|
const list = this.list;
|
|
|
|
if (!list) {
|
|
return this;
|
|
}
|
|
if (!transient) {
|
|
this._removeFromIndex();
|
|
}
|
|
|
|
// If self is the last item in the parent list, link the lists last item to
|
|
// the previous item.
|
|
if (list.tail === this) {
|
|
list.tail = this.prev;
|
|
}
|
|
|
|
// If self is the first item in the parent list, link the lists first item to
|
|
// the next item.
|
|
if (list.head === this) {
|
|
list.head = this.next;
|
|
}
|
|
|
|
// If both the last and first items in the parent list are the same, remove
|
|
// the link to the last item.
|
|
if (list.tail === list.head) {
|
|
list.tail = null;
|
|
}
|
|
|
|
// If a previous item exists, link its next item to selfs next item.
|
|
if (this.prev) {
|
|
this.prev.next = this.next;
|
|
}
|
|
|
|
// If a next item exists, link its previous item to selfs previous item.
|
|
if (this.next) {
|
|
this.next.prev = this.prev;
|
|
}
|
|
|
|
// Remove links from self to both the next and previous items, and to the
|
|
// parent list.
|
|
this.prev = this.next = this.list = null;
|
|
|
|
list.length--;
|
|
|
|
return this;
|
|
}
|
|
|
|
nextCyclic() {
|
|
return this.next || this.list.head;
|
|
}
|
|
|
|
prevCyclic() {
|
|
return this.prev || this.list.last();
|
|
}
|
|
|
|
_addToIndex() {
|
|
const hash = this._hash();
|
|
if (hash === undefined || hash === null) {
|
|
return;
|
|
}
|
|
|
|
if (this.type === TYPE_TEXT) {
|
|
this.list.bytes += this.text.length;
|
|
}
|
|
|
|
let entries = this.list.invertedIndex[hash];
|
|
if (!entries) {
|
|
entries = [];
|
|
this.list.invertedIndex[hash] = entries;
|
|
}
|
|
entries.push(this.id);
|
|
this.list.idsToItems[this.id] = this;
|
|
}
|
|
|
|
_removeFromIndex() {
|
|
const hash = this._hash();
|
|
if (hash === undefined || hash === null) {
|
|
return;
|
|
}
|
|
|
|
if (this.type === TYPE_TEXT) {
|
|
this.list.bytes -= this.text.length;
|
|
}
|
|
|
|
const entries = this.list.invertedIndex[hash];
|
|
if (entries.length === 1) {
|
|
delete this.list.invertedIndex[hash];
|
|
} else {
|
|
entries.splice(entries.indexOf(this.id), 1);
|
|
}
|
|
delete this.list.idsToItems[this.id];
|
|
}
|
|
|
|
_hash() {
|
|
if (this.type === TYPE_TEXT) {
|
|
return _hashText(this.text);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
LLNode.prototype.next = LLNode.prototype.prev = LLNode.prototype.list = null;
|
|
|
|
// Creates a new List: A linked list is a bit like an Array, but knows nothing
|
|
// about how many items are in it, and knows only about its first (`head`) and
|
|
// last (`tail`) items.
|
|
// Each item (e.g. `head`, `tail`, &c.) knows which item comes before or after
|
|
// it (its more like the implementation of the DOM in JavaScript).
|
|
export class LinkedList {
|
|
// Creates a new list from the arguments (each a list item) passed in.
|
|
static of(...items) {
|
|
return appendAll(new this(), items);
|
|
}
|
|
|
|
// Creates a new list from the given array-like object (each a list item) passed
|
|
// in.
|
|
static from(items) {
|
|
return appendAll(new this(), items);
|
|
}
|
|
|
|
constructor(...items) {
|
|
appendAll(this, items);
|
|
this.idsToItems = {};
|
|
this.invertedIndex = {};
|
|
/** Note: this isn't an accurate count because of UTF encoding and other JS mumbo jumbo. */
|
|
this.bytes = 0;
|
|
}
|
|
|
|
// Returns the list's items as an array.
|
|
// This does *not* detach the items.
|
|
toArray() {
|
|
let item = this.head;
|
|
const result = [];
|
|
|
|
while (item) {
|
|
result.push(item);
|
|
item = item.next;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Prepends the given item to the list.
|
|
// `item` will be the new first item (`head`).
|
|
prepend(item) {
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
|
|
if (!item.append || !item.prepend || !item.detach) {
|
|
throw new Error(
|
|
'An argument without append, prepend, or detach methods was given to `List#prepend`.',
|
|
);
|
|
}
|
|
|
|
if (this.head) {
|
|
return this.head.prepend(item);
|
|
}
|
|
|
|
item.detach();
|
|
item.list = this;
|
|
this.head = item;
|
|
this.length++;
|
|
|
|
item._addToIndex();
|
|
|
|
return item;
|
|
}
|
|
|
|
// Appends the given item to the list.
|
|
// `item` will be the new last item (`tail`) if the list had a first item, and
|
|
// its first item (`head`) otherwise.
|
|
append(item) {
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
|
|
if (!item.append || !item.prepend || !item.detach) {
|
|
throw new Error(
|
|
'An argument without append, prepend, or detach methods was given to `List#append`.',
|
|
);
|
|
}
|
|
|
|
// If self has a last item, defer appending to the last items append method,
|
|
// and return the result.
|
|
if (this.tail) {
|
|
return this.tail.append(item);
|
|
}
|
|
|
|
// If self has a first item, defer appending to the first items append method,
|
|
// and return the result.
|
|
if (this.head) {
|
|
return this.head.append(item);
|
|
}
|
|
|
|
// ...otherwise, there is no `tail` or `head` item yet.
|
|
item.detach();
|
|
item.list = this;
|
|
this.head = item;
|
|
this.length++;
|
|
|
|
item._addToIndex();
|
|
|
|
return item;
|
|
}
|
|
|
|
last() {
|
|
return this.tail || this.head;
|
|
}
|
|
|
|
findById(id) {
|
|
return this.idsToItems[id];
|
|
}
|
|
|
|
findTextItem(text) {
|
|
const entries = this.invertedIndex[_hashText(text)];
|
|
if (!entries) {
|
|
return null;
|
|
}
|
|
|
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
const item = this.idsToItems[entries[i]];
|
|
if (item.type === TYPE_TEXT && item.text === text) {
|
|
return item;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Creates an iterator from the list.
|
|
[Symbol.iterator]() {
|
|
return new Iterator(this.head);
|
|
}
|
|
}
|
|
|
|
LinkedList.prototype.length = 0;
|
|
LinkedList.prototype.tail = LinkedList.prototype.head = null;
|
|
|
|
// Creates a new list from the items passed in.
|
|
export function appendAll(list, items) {
|
|
let index;
|
|
let item;
|
|
let iterator;
|
|
|
|
if (!items) {
|
|
return list;
|
|
}
|
|
|
|
if (items[Symbol.iterator]) {
|
|
iterator = items[Symbol.iterator]();
|
|
item = {};
|
|
|
|
while (!item.done) {
|
|
item = iterator.next();
|
|
list.append(item && item.value);
|
|
}
|
|
} else {
|
|
index = -1;
|
|
|
|
while (++index < items.length) {
|
|
list.append(items[index]);
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
function _hashText(text) {
|
|
// The goal of this hash function is to be extremely fast while minimizing collisions. To do
|
|
// this, we make an assumption about our data. If users copy text, the guess is that there is
|
|
// a very low likelihood of collisions when the text is very long. For example, why would
|
|
// someone copy two different pieces of text that are exactly 29047 characters long? However, for
|
|
// smaller pieces of text, it's very easy to get length collisions. For example, I can copy "the"
|
|
// and "123" to cause a collision. Thus, our hash function returns the string length for longer
|
|
// strings while using an ok-ish hash for short strings.
|
|
|
|
if (text.length > 500) {
|
|
return text.length;
|
|
}
|
|
|
|
// Copied from https://stackoverflow.com/a/7616484/4548500
|
|
let hash = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
let chr = text.charCodeAt(i);
|
|
hash = (hash << 5) - hash + chr;
|
|
hash |= 0; // Convert to integer
|
|
}
|
|
return hash;
|
|
}
|