[gnome] Add clipboard manager extension
This commit is contained in:
@ -0,0 +1,417 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user