Files
dotfiles/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/dataStructures.js

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