| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- // 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;
- }
|