UNPKG

netscape-bookmark-parser

Version:

A TypeScript/JavaScript library for parsing browser bookmark files (HTML format) and manipulating them as structured data. Compatible with both Deno and Node.js runtimes.

160 lines (159 loc) 5.37 kB
import { Node } from "./node.js"; import { HTMLCollection } from "./html-collection.js"; const NodeListFakeClass = (() => { return class NodeList { constructor() { throw new TypeError("Illegal constructor"); } static [Symbol.hasInstance](value) { return value.constructor === NodeListClass; } }; })(); export const nodeListMutatorSym = Symbol("nodeListMutatorSym"); const nodeListCachedMutator = Symbol("nodeListCachedMutator"); // Array methods that we need for NodeList mutator implementation const { push, splice, slice, indexOf, filter } = Array.prototype; // Implementation of a NodeList mutator class NodeListMutatorImpl { constructor(arrayInstance) { Object.defineProperty(this, "arrayInstance", { enumerable: true, configurable: true, writable: true, value: arrayInstance }); // There should only ever be one elementView per element. Element views // are basically just the source of HTMLCollections/.children properties // on elements that are always in sync with their .childNodes counterpart. Object.defineProperty(this, "elementViews", { enumerable: true, configurable: true, writable: true, value: [] }); } push(...items) { // Copy the new items to the element view (if any) for (const view of this.elementViews) { for (const item of items) { if (item.nodeType === Node.ELEMENT_NODE) { push.call(view, item); } } } return push.call(this.arrayInstance, ...items); } splice(index, deleteCount = 0, ...items) { // Delete and insert new elements in an element view (if any) for (const view of this.elementViews) { const toDelete = filter.call(slice.call(this.arrayInstance, index, index + deleteCount), (item) => item.nodeType === Node.ELEMENT_NODE); const toInsert = items.filter((item) => item.nodeType === Node.ELEMENT_NODE); // Find where to start splicing in the element view let elementViewSpliceIndex = -1; for (let idx = index; idx < this.arrayInstance.length; idx++) { const item = this.arrayInstance[idx]; if (item.nodeType === Node.ELEMENT_NODE) { elementViewSpliceIndex = indexOf.call(view, item); break; } } // If no element is found just do everything at the end // of the view if (elementViewSpliceIndex === -1) { elementViewSpliceIndex = view.length; } if (toDelete.length) { splice.call(view, elementViewSpliceIndex, toDelete.length); } // Finally, insert all the found elements splice.call(view, elementViewSpliceIndex, 0, ...toInsert); } return splice.call(this.arrayInstance, index, deleteCount, ...items); } indexOf(item, fromIndex = 0) { return indexOf.call(this.arrayInstance, item, fromIndex); } indexOfElementsView(item, fromIndex = 0) { return indexOf.call(this.elementsView(), item, fromIndex); } // Return the elements-only view for this NodeList. Creates one if // it doesn't already exist. elementsView() { let view = this.elementViews[0]; if (!view) { view = new HTMLCollection(); this.elementViews.push(view); push.call(view, ...filter.call(this.arrayInstance, (item) => item.nodeType === Node.ELEMENT_NODE)); } return view; } } // We define the `NodeList` inside a closure to ensure that its // `.name === "NodeList"` property stays intact, as we need to manipulate // its prototype and completely change its TypeScript-recognized type. const NodeListClass = (() => { // @ts-ignore class NodeList extends Array { forEach(cb, thisArg) { super.forEach(cb, thisArg); } item(index) { return this[index] ?? null; } [nodeListMutatorSym]() { const cachedMutator = this[nodeListCachedMutator]; if (cachedMutator) { return cachedMutator; } else { const cachedMutator = new NodeListMutatorImpl(this); this[nodeListCachedMutator] = cachedMutator; return cachedMutator; } } toString() { return "[object NodeList]"; } } return NodeList; })(); for (const staticMethod of [ "from", "isArray", "of", ]) { NodeListClass[staticMethod] = undefined; } for (const instanceMethod of [ "concat", "copyWithin", "every", "fill", "filter", "find", "findIndex", "flat", "flatMap", "includes", "indexOf", "join", "lastIndexOf", "map", "pop", "push", "reduce", "reduceRight", "reverse", "shift", "slice", "some", "sort", "splice", "toLocaleString", "unshift", ]) { NodeListClass.prototype[instanceMethod] = undefined; } export const NodeList = NodeListClass; export const NodeListPublic = NodeListFakeClass;