Remove client-side isLoggedIn value
[ProtonMail-WebClient.git] / packages / pass / fathom / fathom.js
blobb95530f0007969773c1cf15103eb04add6e24a2f
1 /**
2  * A :func:`rule` depends on another rule which itself depends on the first
3  * rule again, either directly or indirectly.
4  */
5 class CycleError extends Error {}
7 /**
8  * An examined element was not contained in a browser ``window`` object, but
9  * something needed it to be.
10  */
11 class NoWindowError extends Error {}
13 var exceptions = /*#__PURE__*/ Object.freeze({
14     __proto__: null,
15     CycleError: CycleError,
16     NoWindowError: NoWindowError,
17 });
19 /**
20  * Return the passed-in arg. Useful as a default.
21  */
22 function identity(x) {
23     return x;
26 /*eslint-env browser*/
28 /**
29  * From an iterable return the best item, according to an arbitrary comparator
30  * function. In case of a tie, the first item wins.
31  *
32  * @arg by {function} Given an item of the iterable, return a value to compare
33  * @arg isBetter {function} Return whether its first arg is better than its
34  *     second
35  */
36 function best(iterable, by, isBetter) {
37     let bestSoFar, bestKeySoFar;
38     let isFirst = true;
39     forEach(function (item) {
40         const key = by(item);
41         if (isBetter(key, bestKeySoFar) || isFirst) {
42             bestSoFar = item;
43             bestKeySoFar = key;
44             isFirst = false;
45         }
46     }, iterable);
47     if (isFirst) {
48         throw new Error('Tried to call best() on empty iterable');
49     }
50     return bestSoFar;
53 /**
54  * Return the maximum item from an iterable, as defined by >.
55  *
56  * Works with any type that works with >. If multiple items are equally great,
57  * return the first.
58  *
59  * @arg by {function} Given an item of the iterable, returns a value to
60  *     compare
61  */
62 function max(iterable, by = identity) {
63     return best(iterable, by, (a, b) => a > b);
66 /**
67  * Return an Array of maximum items from an iterable, as defined by > and ===.
68  *
69  * If an empty iterable is passed in, return [].
70  */
71 function maxes(iterable, by = identity) {
72     let bests = [];
73     let bestKeySoFar;
74     let isFirst = true;
75     forEach(function (item) {
76         const key = by(item);
77         if (key > bestKeySoFar || isFirst) {
78             bests = [item];
79             bestKeySoFar = key;
80             isFirst = false;
81         } else if (key === bestKeySoFar) {
82             bests.push(item);
83         }
84     }, iterable);
85     return bests;
88 /**
89  * Return the minimum item from an iterable, as defined by <.
90  *
91  * If multiple items are equally great, return the first.
92  */
93 function min(iterable, by = identity) {
94     return best(iterable, by, (a, b) => a < b);
97 /**
98  * Return the sum of an iterable, as defined by the + operator.
99  */
100 function sum(iterable) {
101     let total;
102     let isFirst = true;
103     forEach(function assignOrAdd(addend) {
104         if (isFirst) {
105             total = addend;
106             isFirst = false;
107         } else {
108             total += addend;
109         }
110     }, iterable);
111     return total;
115  * Return the number of items in an iterable, consuming it as a side effect.
116  */
117 function length(iterable) {
118     let num = 0;
119     // eslint-disable-next-line no-unused-vars
120     for (let item of iterable) {
121         num++;
122     }
123     return num;
127  * Iterate, depth first, over a DOM node. Return the original node first.
129  * @arg shouldTraverse {function} Given a node, say whether we should
130  *     include it and its children. Default: always true.
131  */
132 function* walk(element, shouldTraverse = (element) => true) {
133     yield element;
134     for (let child of element.childNodes) {
135         if (shouldTraverse(child)) {
136             for (let w of walk(child, shouldTraverse)) {
137                 yield w;
138             }
139         }
140     }
143 const blockTags = new Set([
144     'ADDRESS',
145     'BLOCKQUOTE',
146     'BODY',
147     'CENTER',
148     'DIR',
149     'DIV',
150     'DL',
151     'FIELDSET',
152     'FORM',
153     'H1',
154     'H2',
155     'H3',
156     'H4',
157     'H5',
158     'H6',
159     'HR',
160     'ISINDEX',
161     'MENU',
162     'NOFRAMES',
163     'NOSCRIPT',
164     'OL',
165     'P',
166     'PRE',
167     'TABLE',
168     'UL',
169     'DD',
170     'DT',
171     'FRAMESET',
172     'LI',
173     'TBODY',
174     'TD',
175     'TFOOT',
176     'TH',
177     'THEAD',
178     'TR',
179     'HTML',
182  * Return whether a DOM element is a block element by default (rather than by
183  * styling).
184  */
185 function isBlock(element) {
186     return blockTags.has(element.tagName);
190  * Yield strings of text nodes within a normalized DOM node and its children,
191  * without venturing into any contained block elements.
193  * @arg shouldTraverse {function} Specify additional elements to exclude by
194  *     returning false
195  */
196 function* inlineTexts(element, shouldTraverse = (element) => true) {
197     // TODO: Could we just use querySelectorAll() with a really long
198     // selector rather than walk(), for speed?
199     for (let child of walk(
200         element,
201         (element) =>
202             !(isBlock(element) || (element.tagName === 'SCRIPT' && element.tagName === 'STYLE')) &&
203             shouldTraverse(element)
204     )) {
205         if (child.nodeType === child.TEXT_NODE) {
206             // wholeText() is not implemented by jsdom, so we use
207             // textContent(). The result should be the same, since
208             // we're calling it on only text nodes, but it may be
209             // slower. On the positive side, it means we don't need to
210             // normalize the DOM tree first.
211             yield child.textContent;
212         }
213     }
217  * Return the total length of the inline text within an element, with
218  * whitespace collapsed.
220  * @arg shouldTraverse {function} Specify additional elements to exclude by
221  *     returning false
222  */
223 function inlineTextLength(element, shouldTraverse = (element) => true) {
224     return sum(map((text) => collapseWhitespace(text).length, inlineTexts(element, shouldTraverse)));
228  * Return a string with each run of whitespace collapsed to a single space.
229  */
230 function collapseWhitespace(str) {
231     return str.replace(/\s{2,}/g, ' ');
235  * Return the ratio of the inline text length of the links in an element to the
236  * inline text length of the entire element.
238  * @arg inlineLength {number} Optionally, the precalculated inline
239  *     length of the fnode. If omitted, we will calculate it ourselves.
240  */
241 function linkDensity(fnode, inlineLength) {
242     if (inlineLength === undefined) {
243         inlineLength = inlineTextLength(fnode.element);
244     }
245     const lengthWithoutLinks = inlineTextLength(fnode.element, (element) => element.tagName !== 'A');
246     return (inlineLength - lengthWithoutLinks) / inlineLength;
250  * Return whether an element is a text node that consist wholly of whitespace.
251  */
252 function isWhitespace(element) {
253     return element.nodeType === element.TEXT_NODE && element.textContent.trim().length === 0;
257  * Get a key of a map, first setting it to a default value if it's missing.
258  */
259 function setDefault(map, key, defaultMaker) {
260     if (map.has(key)) {
261         return map.get(key);
262     }
263     const defaultValue = defaultMaker();
264     map.set(key, defaultValue);
265     return defaultValue;
269  * Get a key of a map or, if it's missing, a default value.
270  */
271 function getDefault(map, key, defaultMaker) {
272     if (map.has(key)) {
273         return map.get(key);
274     }
275     return defaultMaker();
279  * Return an Array, the reverse topological sort of the given nodes.
281  * @arg nodes An iterable of arbitrary things
282  * @arg nodesThatNeed {function} Take a node and returns an Array of nodes
283  *     that depend on it
284  */
285 function toposort(nodes, nodesThatNeed) {
286     const ret = [];
287     const todo = new Set(nodes);
288     const inProgress = new Set();
290     function visit(node) {
291         if (inProgress.has(node)) {
292             throw new CycleError('The graph has a cycle.');
293         }
294         if (todo.has(node)) {
295             inProgress.add(node);
296             for (let needer of nodesThatNeed(node)) {
297                 visit(needer);
298             }
299             inProgress.delete(node);
300             todo.delete(node);
301             ret.push(node);
302         }
303     }
305     while (todo.size > 0) {
306         visit(first(todo));
307     }
308     return ret;
312  * A Set with the additional methods it ought to have had
313  */
314 class NiceSet extends Set {
315     /**
316      * Remove and return an arbitrary item. Throw an Error if I am empty.
317      */
318     pop() {
319         for (let v of this.values()) {
320             this.delete(v);
321             return v;
322         }
323         throw new Error('Tried to pop from an empty NiceSet.');
324     }
326     /**
327      * Union another set or other iterable into myself.
328      *
329      * @return myself, for chaining
330      */
331     extend(otherSet) {
332         for (let item of otherSet) {
333             this.add(item);
334         }
335         return this;
336     }
338     /**
339      * Subtract another set from a copy of me.
340      *
341      * @return a copy of myself excluding the elements in ``otherSet``.
342      */
343     minus(otherSet) {
344         const ret = new NiceSet(this);
345         for (const item of otherSet) {
346             ret.delete(item);
347         }
348         return ret;
349     }
351     /**
352      * Actually show the items in me.
353      */
354     toString() {
355         return '{' + Array.from(this).join(', ') + '}';
356     }
360  * Return the first item of an iterable.
361  */
362 function first(iterable) {
363     for (let i of iterable) {
364         return i;
365     }
369  * Given any node in a DOM tree, return the root element of the tree, generally
370  * an HTML element.
371  */
372 function rootElement(element) {
373     return element.ownerDocument.documentElement;
377  * Return the number of times a regex occurs within the string `haystack`.
379  * Caller must make sure `regex` has the 'g' option set.
380  */
381 function numberOfMatches(regex, haystack) {
382     return (haystack.match(regex) || []).length;
386  * Wrap a scoring callback, and set its element to the page root iff a score is
387  * returned.
389  * This is used to build rulesets which classify entire pages rather than
390  * picking out specific elements.
392  * For example, these rules might classify a page as a "login page", influenced
393  * by whether they have login buttons or username fields:
395  * ``rule(type('loginPage'), score(page(pageContainsLoginButton))),``
396  * ``rule(type('loginPage'), score(page(pageContainsUsernameField)))``
397  */
398 function page(scoringFunction) {
399     function wrapper(fnode) {
400         const scoreAndTypeAndNote = scoringFunction(fnode);
401         if (scoreAndTypeAndNote.score !== undefined) {
402             scoreAndTypeAndNote.element = rootElement(fnode.element);
403         }
404         return scoreAndTypeAndNote;
405     }
406     return wrapper;
410  * Sort the elements by their position in the DOM.
412  * @arg fnodes {iterable} fnodes to sort
413  * @return {Array} sorted fnodes
414  */
415 function domSort(fnodes) {
416     function compare(a, b) {
417         const element = a.element;
418         const position = element.compareDocumentPosition(b.element);
419         if (position & element.DOCUMENT_POSITION_FOLLOWING) {
420             return -1;
421         } else if (position & element.DOCUMENT_POSITION_PRECEDING) {
422             return 1;
423         } else {
424             return 0;
425         }
426     }
427     return Array.from(fnodes).sort(compare);
430 /* istanbul ignore next */
432  * Return the DOM element contained in a passed-in fnode. Return passed-in DOM
433  * elements verbatim.
435  * @arg fnodeOrElement {Node|Fnode}
436  */
437 function toDomElement(fnodeOrElement) {
438     return isDomElement(fnodeOrElement) ? fnodeOrElement : fnodeOrElement.element;
442  * Checks whether any of the element's attribute values satisfy some condition.
444  * Example::
446  *     rule(type('foo'),
447  *          score(attributesMatch(element,
448  *                                attr => attr.includes('good'),
449  *                                ['id', 'alt']) ? 2 : 1))
451  * @arg element {Node} Element whose attributes you want to search
452  * @arg predicate {function} A condition to check. Take a string and
453  *     return a boolean. If an attribute has multiple values (e.g. the class
454  *     attribute), attributesMatch will check each one.
455  * @arg attrs {string[]} An Array of attributes you want to search. If none are
456  *     provided, search all.
457  * @return Whether any of the attribute values satisfy the predicate function
458  */
459 function attributesMatch(element, predicate, attrs = []) {
460     const attributes = attrs.length === 0 ? Array.from(element.attributes).map((a) => a.name) : attrs;
461     for (let i = 0; i < attributes.length; i++) {
462         const attr = element.getAttribute(attributes[i]);
463         // If the attribute is an array, apply the scoring function to each element
464         if (attr && ((Array.isArray(attr) && attr.some(predicate)) || predicate(attr))) {
465             return true;
466         }
467     }
468     return false;
471 /* istanbul ignore next */
473  * Yield an element and each of its ancestors.
474  */
475 function* ancestors(element) {
476     yield element;
477     let parent;
478     while ((parent = element.parentNode) !== null && parent.nodeType === parent.ELEMENT_NODE) {
479         yield parent;
480         element = parent;
481     }
485  * Return the sigmoid of the argument: 1 / (1 + exp(-x)). This is useful for
486  * crunching a feature value that may have a wide range into the range (0, 1)
487  * without a hard ceiling: the sigmoid of even a very large number will be a
488  * little larger than that of a slightly smaller one.
490  * @arg x {Number} a number to be compressed into the range (0, 1)
491  */
492 function sigmoid(x) {
493     return 1 / (1 + Math.exp(-x));
496 /* istanbul ignore next */
498  * Return whether an element is practically visible, considering things like 0
499  * size or opacity, ``visibility: hidden`` and ``overflow: hidden``.
501  * Merely being scrolled off the page in either horizontally or vertically
502  * doesn't count as invisible; the result of this function is meant to be
503  * independent of viewport size.
505  * @throws {NoWindowError} The element (or perhaps one of its ancestors) is not
506  *     in a window, so we can't find the `getComputedStyle()` routine to call.
507  *     That routine is the source of most of the information we use, so you
508  *     should pick a different strategy for non-window contexts.
509  */
510 function isVisible(fnodeOrElement) {
511     // This could be 5x more efficient if https://github.com/w3c/csswg-drafts/issues/4122 happens.
512     const element = toDomElement(fnodeOrElement);
513     const elementWindow = windowForElement(element);
514     const elementRect = element.getBoundingClientRect();
515     const elementStyle = elementWindow.getComputedStyle(element);
516     // Alternative to reading ``display: none`` due to Bug 1381071.
517     if (elementRect.width === 0 && elementRect.height === 0 && elementStyle.overflow !== 'hidden') {
518         return false;
519     }
520     if (elementStyle.visibility === 'hidden') {
521         return false;
522     }
523     // Check if the element is irrevocably off-screen:
524     if (elementRect.x + elementRect.width < 0 || elementRect.y + elementRect.height < 0) {
525         return false;
526     }
527     for (const ancestor of ancestors(element)) {
528         const isElement = ancestor === element;
529         const style = isElement ? elementStyle : elementWindow.getComputedStyle(ancestor);
530         if (style.opacity === '0') {
531             return false;
532         }
533         if (style.display === 'contents') {
534             // ``display: contents`` elements have no box themselves, but children are
535             // still rendered.
536             continue;
537         }
538         const rect = isElement ? elementRect : ancestor.getBoundingClientRect();
539         if ((rect.width === 0 || rect.height === 0) && elementStyle.overflow === 'hidden') {
540             // Zero-sized ancestors don’t make descendants hidden unless the descendant
541             // has ``overflow: hidden``.
542             return false;
543         }
544     }
545     return true;
549  * Return the extracted [r, g, b, a] values from a string like "rgba(0, 5, 255, 0.8)",
550  * and scale them to 0..1. If no alpha is specified, return undefined for it.
551  */
552 function rgbaFromString(str) {
553     const m = str.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)$/i);
554     if (m) {
555         return [m[1] / 255, m[2] / 255, m[3] / 255, m[4] === undefined ? undefined : parseFloat(m[4])];
556     } else {
557         throw new Error('Color ' + str + ' did not match pattern rgb[a](r, g, b[, a]).');
558     }
562  * Return the saturation 0..1 of a color defined by RGB values 0..1.
563  */
564 function saturation(r, g, b) {
565     const cMax = Math.max(r, g, b);
566     const cMin = Math.min(r, g, b);
567     const delta = cMax - cMin;
568     const lightness = (cMax + cMin) / 2;
569     const denom = 1 - Math.abs(2 * lightness - 1);
570     // Return 0 if it's black (R, G, and B all 0).
571     return denom === 0 ? 0 : delta / denom;
575  * Scale a number to the range [0, 1] using a linear slope.
577  * For a rising line, the result is 0 until the input reaches zeroAt, then
578  * increases linearly until oneAt, at which it becomes 1. To make a falling
579  * line, where the result is 1 to the left and 0 to the right, use a zeroAt
580  * greater than oneAt.
581  */
582 function linearScale(number, zeroAt, oneAt) {
583     const isRising = zeroAt < oneAt;
584     if (isRising) {
585         if (number <= zeroAt) {
586             return 0;
587         } else if (number >= oneAt) {
588             return 1;
589         }
590     } else {
591         if (number >= zeroAt) {
592             return 0;
593         } else if (number <= oneAt) {
594             return 1;
595         }
596     }
597     const slope = 1 / (oneAt - zeroAt);
598     return slope * (number - zeroAt);
601 // -------- Routines below this point are private to the framework. --------
604  * Flatten out an iterable of iterables into a single iterable of non-
605  * iterables. Does not consider strings to be iterable.
606  */
607 function* flatten(iterable) {
608     for (const i of iterable) {
609         if (typeof i !== 'string' && !(i instanceof Element) && isIterable(i)) {
610             yield* flatten(i);
611         } else {
612             yield i;
613         }
614     }
618  * A lazy, top-level ``Array.map()`` workalike that works on anything iterable
619  */
620 function* map(fn, iterable) {
621     for (const i of iterable) {
622         yield fn(i);
623     }
627  * A lazy, top-level ``Array.forEach()`` workalike that works on anything
628  * iterable
629  */
630 function forEach(fn, iterable) {
631     for (const i of iterable) {
632         fn(i);
633     }
636 /* istanbul ignore next */
638  * @return whether a thing appears to be a DOM element.
639  */
640 function isDomElement(thing) {
641     return thing.nodeName !== undefined;
644 function isIterable(thing) {
645     return thing && typeof thing[Symbol.iterator] === 'function';
649  * Return an backward iterator over an Array without reversing it in place.
650  */
651 function* reversed(array) {
652     for (let i = array.length - 1; i >= 0; i--) {
653         yield array[i];
654     }
657 /* istanbul ignore next */
659  * Return the window an element is in.
661  * @throws {NoWindowError} There isn't such a window.
662  */
663 function windowForElement(element) {
664     let doc = element.ownerDocument;
665     if (doc === null) {
666         // The element itself was a document.
667         doc = element;
668     }
669     const win = doc.defaultView;
670     if (win === null) {
671         throw new NoWindowError();
672     }
673     return win;
676 var utilsForFrontend = /*#__PURE__*/ Object.freeze({
677     __proto__: null,
678     NiceSet: NiceSet,
679     ancestors: ancestors,
680     attributesMatch: attributesMatch,
681     best: best,
682     collapseWhitespace: collapseWhitespace,
683     domSort: domSort,
684     first: first,
685     flatten: flatten,
686     forEach: forEach,
687     getDefault: getDefault,
688     identity: identity,
689     inlineTextLength: inlineTextLength,
690     inlineTexts: inlineTexts,
691     isBlock: isBlock,
692     isDomElement: isDomElement,
693     isVisible: isVisible,
694     isWhitespace: isWhitespace,
695     length: length,
696     linearScale: linearScale,
697     linkDensity: linkDensity,
698     map: map,
699     max: max,
700     maxes: maxes,
701     min: min,
702     numberOfMatches: numberOfMatches,
703     page: page,
704     reversed: reversed,
705     rgbaFromString: rgbaFromString,
706     rootElement: rootElement,
707     saturation: saturation,
708     setDefault: setDefault,
709     sigmoid: sigmoid,
710     sum: sum,
711     toDomElement: toDomElement,
712     toposort: toposort,
713     walk: walk,
714     windowForElement: windowForElement,
718  * Return the number of stride nodes between 2 DOM nodes *at the same
719  * level of the tree*, without going up or down the tree.
721  * ``left`` xor ``right`` may also be undefined.
722  */
723 function numStrides(left, right) {
724     let num = 0;
726     // Walk right from left node until we hit the right node or run out:
727     let sibling = left;
728     let shouldContinue = sibling && sibling !== right;
729     while (shouldContinue) {
730         sibling = sibling.nextSibling;
731         if ((shouldContinue = sibling && sibling !== right) && !isWhitespace(sibling)) {
732             num += 1;
733         }
734     }
735     if (sibling !== right) {
736         // Don't double-punish if left and right are siblings.
737         // Walk left from right node:
738         sibling = right;
739         while (sibling) {
740             sibling = sibling.previousSibling;
741             if (sibling && !isWhitespace(sibling)) {
742                 num += 1;
743             }
744         }
745     }
746     return num;
750  * Return a topological distance between 2 DOM nodes or :term:`fnodes<fnode>`
751  * weighted according to the similarity of their ancestry in the DOM. For
752  * instance, if one node is situated inside ``<div><span><b><theNode>`` and the
753  * other node is at ``<differentDiv><span><b><otherNode>``, they are considered
754  * close to each other for clustering purposes. This is useful for picking out
755  * nodes which have similar purposes.
757  * Return ``Number.MAX_VALUE`` if one of the nodes contains the other.
759  * This is largely an implementation detail of :func:`clusters`, but you can
760  * call it yourself if you wish to implement your own clustering. Takes O(n log
761  * n) time.
763  * Note that the default costs may change; pass them in explicitly if they are
764  * important to you.
766  * @arg fnodeA {Node|Fnode}
767  * @arg fnodeB {Node|Fnode}
768  * @arg differentDepthCost {number} Cost for each level deeper one node is than
769  *    the other below their common ancestor
770  * @arg differentTagCost {number} Cost for a level below the common ancestor
771  *    where tagNames differ
772  * @arg sameTagCost {number} Cost for a level below the common ancestor where
773  *    tagNames are the same
774  * @arg strideCost {number} Cost for each stride node between A and B. Stride
775  *     nodes are siblings or siblings-of-ancestors that lie between the 2
776  *     nodes. These interposed nodes make it less likely that the 2 nodes
777  *     should be together in a cluster.
778  * @arg additionalCost {function} Return an additional cost, given 2 fnodes or
779  *    nodes.
781  */
782 function distance(
783     fnodeA,
784     fnodeB,
785     {
786         differentDepthCost = 2,
787         differentTagCost = 2,
788         sameTagCost = 1,
789         strideCost = 1,
790         additionalCost = (fnodeA, fnodeB) => 0,
791     } = {}
792 ) {
793     // I was thinking of something that adds little cost for siblings. Up
794     // should probably be more expensive than down (see middle example in the
795     // Nokia paper).
797     // TODO: Test and tune default costs. They're off the cuff at the moment.
799     if (fnodeA === fnodeB) {
800         return 0;
801     }
803     const elementA = isDomElement(fnodeA) ? fnodeA : fnodeA.element;
804     const elementB = isDomElement(fnodeB) ? fnodeB : fnodeB.element;
806     // Stacks that go from the common ancestor all the way to A and B:
807     const aAncestors = [elementA];
808     const bAncestors = [elementB];
810     let aAncestor = elementA;
811     let bAncestor = elementB;
813     // Ascend to common parent, stacking them up for later reference:
814     while (!aAncestor.contains(elementB)) {
815         // Note: an element does contain() itself.
816         aAncestor = aAncestor.parentNode;
817         aAncestors.push(aAncestor); //aAncestors = [a, b]. aAncestor = b // if a is outer: no loop here; aAncestors = [a]. aAncestor = a.
818     }
820     // In compareDocumentPosition()'s opinion, inside implies after. Basically,
821     // before and after pertain to opening tags.
822     const comparison = elementA.compareDocumentPosition(elementB);
824     // If either contains the other, abort. We'd either return a misleading
825     // number or else walk upward right out of the document while trying to
826     // make the ancestor stack.
827     if (comparison & (elementA.DOCUMENT_POSITION_CONTAINS | elementA.DOCUMENT_POSITION_CONTAINED_BY)) {
828         return Number.MAX_VALUE;
829     }
830     // Make an ancestor stack for the right node too so we can walk
831     // efficiently down to it:
832     do {
833         bAncestor = bAncestor.parentNode; // Assumes we've early-returned above if A === B. This walks upward from the outer node and up out of the tree. It STARTS OUT with aAncestor === bAncestor!
834         bAncestors.push(bAncestor);
835     } while (bAncestor !== aAncestor);
837     // Figure out which node is left and which is right, so we can follow
838     // sibling links in the appropriate directions when looking for stride
839     // nodes:
840     let left = aAncestors;
841     let right = bAncestors;
842     let cost = 0;
843     if (comparison & elementA.DOCUMENT_POSITION_FOLLOWING) {
844         // A is before, so it could contain the other node. What did I mean to do if one contained the other?
845         left = aAncestors;
846         right = bAncestors;
847     } else if (comparison & elementA.DOCUMENT_POSITION_PRECEDING) {
848         // A is after, so it might be contained by the other node.
849         left = bAncestors;
850         right = aAncestors;
851     }
853     // Descend to both nodes in parallel, discounting the traversal
854     // cost iff the nodes we hit look similar, implying the nodes dwell
855     // within similar structures.
856     while (left.length || right.length) {
857         const l = left.pop();
858         const r = right.pop();
859         if (l === undefined || r === undefined) {
860             // Punishment for being at different depths: same as ordinary
861             // dissimilarity punishment for now
862             cost += differentDepthCost;
863         } else {
864             // TODO: Consider similarity of classList.
865             cost += l.tagName === r.tagName ? sameTagCost : differentTagCost;
866         }
867         // Optimization: strides might be a good dimension to eliminate.
868         if (strideCost !== 0) {
869             cost += numStrides(l, r) * strideCost;
870         }
871     }
873     return cost + additionalCost(fnodeA, fnodeB);
877  * Return the spatial distance between 2 fnodes or elements, assuming a
878  * rendered page.
880  * Specifically, return the distance in pixels between the centers of
881  * ``fnodeA.element.getBoundingClientRect()`` and
882  * ``fnodeB.element.getBoundingClientRect()``.
883  */
884 function euclidean(fnodeA, fnodeB) {
885     /**
886      * Return the horizontal distance from the left edge of the viewport to the
887      * center of an element, given a DOMRect object for it. It doesn't matter
888      * that the distance is affected by the page's scroll offset, since the 2
889      * elements have the same offset.
890      */
891     function xCenter(domRect) {
892         return domRect.left + domRect.width / 2;
893     }
894     function yCenter(domRect) {
895         return domRect.top + domRect.height / 2;
896     }
898     const elementA = toDomElement(fnodeA);
899     const elementB = toDomElement(fnodeB);
900     const aRect = elementA.getBoundingClientRect();
901     const bRect = elementB.getBoundingClientRect();
902     return Math.sqrt((xCenter(aRect) - xCenter(bRect)) ** 2 + (yCenter(aRect) - yCenter(bRect)) ** 2);
905 /** A lower-triangular matrix of inter-cluster distances */
906 class DistanceMatrix {
907     /**
908      * @arg distance {function} Some notion of distance between 2 given nodes
909      */
910     constructor(elements, distance) {
911         // A sparse adjacency matrix:
912         // {A => {},
913         //  B => {A => 4},
914         //  C => {A => 4, B => 4},
915         //  D => {A => 4, B => 4, C => 4}
916         //  E => {A => 4, B => 4, C => 4, D => 4}}
917         //
918         // A, B, etc. are arrays of [arrays of arrays of...] nodes, each
919         // array being a cluster. In this way, they not only accumulate a
920         // cluster but retain the steps along the way.
921         //
922         // This is an efficient data structure in terms of CPU and memory, in
923         // that we don't have to slide a lot of memory around when we delete a
924         // row or column from the middle of the matrix while merging. Of
925         // course, we lose some practical efficiency by using hash tables, and
926         // maps in particular are slow in their early implementations.
927         this._matrix = new Map();
929         // Convert elements to clusters:
930         const clusters = elements.map((el) => [el]);
932         // Init matrix:
933         for (let outerCluster of clusters) {
934             const innerMap = new Map();
935             for (let innerCluster of this._matrix.keys()) {
936                 innerMap.set(innerCluster, distance(outerCluster[0], innerCluster[0]));
937             }
938             this._matrix.set(outerCluster, innerMap);
939         }
940         this._numClusters = clusters.length;
941     }
943     // Return (distance, a: clusterA, b: clusterB) of closest-together clusters.
944     // Replace this to change linkage criterion.
945     closest() {
946         const self = this;
948         if (this._numClusters < 2) {
949             throw new Error('There must be at least 2 clusters in order to return the closest() ones.');
950         }
952         // Return the distances between every pair of clusters.
953         function clustersAndDistances() {
954             const ret = [];
955             for (let [outerKey, row] of self._matrix.entries()) {
956                 for (let [innerKey, storedDistance] of row.entries()) {
957                     ret.push({ a: outerKey, b: innerKey, distance: storedDistance });
958                 }
959             }
960             return ret;
961         }
962         // Optimizing this by inlining the loop and writing it less
963         // functionally doesn't help:
964         return min(clustersAndDistances(), (x) => x.distance);
965     }
967     // Look up the distance between 2 clusters in me. Try the lookup in the
968     // other direction if the first one falls in the nonexistent half of the
969     // triangle.
970     _cachedDistance(clusterA, clusterB) {
971         let ret = this._matrix.get(clusterA).get(clusterB);
972         if (ret === undefined) {
973             ret = this._matrix.get(clusterB).get(clusterA);
974         }
975         return ret;
976     }
978     // Merge two clusters.
979     merge(clusterA, clusterB) {
980         // An example showing how rows merge:
981         //  A: {}
982         //  B: {A: 1}
983         //  C: {A: 4, B: 4},
984         //  D: {A: 4, B: 4, C: 4}
985         //  E: {A: 4, B: 4, C: 2, D: 4}}
986         //
987         // Step 2:
988         //  C: {}
989         //  D: {C: 4}
990         //  E: {C: 2, D: 4}}
991         //  AB: {C: 4, D: 4, E: 4}
992         //
993         // Step 3:
994         //  D:  {}
995         //  AB: {D: 4}
996         //  CE: {D: 4, AB: 4}
998         // Construct new row, finding min distances from either subcluster of
999         // the new cluster to old clusters.
1000         //
1001         // There will be no repetition in the matrix because, after all,
1002         // nothing pointed to this new cluster before it existed.
1003         const newRow = new Map();
1004         for (let outerKey of this._matrix.keys()) {
1005             if (outerKey !== clusterA && outerKey !== clusterB) {
1006                 newRow.set(
1007                     outerKey,
1008                     Math.min(this._cachedDistance(clusterA, outerKey), this._cachedDistance(clusterB, outerKey))
1009                 );
1010             }
1011         }
1013         // Delete the rows of the clusters we're merging.
1014         this._matrix.delete(clusterA);
1015         this._matrix.delete(clusterB);
1017         // Remove inner refs to the clusters we're merging.
1018         for (let inner of this._matrix.values()) {
1019             inner.delete(clusterA);
1020             inner.delete(clusterB);
1021         }
1023         // Attach new row.
1024         this._matrix.set([clusterA, clusterB], newRow);
1026         // There is a net decrease of 1 cluster:
1027         this._numClusters -= 1;
1028     }
1030     numClusters() {
1031         return this._numClusters;
1032     }
1034     // Return an Array of nodes for each cluster in me.
1035     clusters() {
1036         // TODO: Can't get map to work here. Don't know why.
1037         return Array.from(this._matrix.keys()).map((e) => Array.from(flatten(e)));
1038     }
1042  * Partition the given nodes into one or more clusters by position in the DOM
1043  * tree.
1045  * This implements an agglomerative clustering. It uses single linkage, since
1046  * we're talking about adjacency here more than Euclidean proximity: the
1047  * clusters we're talking about in the DOM will tend to be adjacent, not
1048  * overlapping. We haven't tried other linkage criteria yet.
1050  * In a later release, we may consider score or notes.
1052  * @arg {Fnode[]|Node[]} fnodes :term:`fnodes<fnode>` or DOM nodes to group
1053  *     into clusters
1054  * @arg {number} splittingDistance The closest-nodes :func:`distance` beyond
1055  *     which we will not attempt to unify 2 clusters. Make this larger to make
1056  *     larger clusters.
1057  * @arg getDistance {function} A function that returns some notion of numerical
1058  *    distance between 2 nodes. Default: :func:`distance`
1059  * @return {Array} An Array of Arrays, with each Array containing all the
1060  *     nodes in one cluster. Note that neither the clusters nor the nodes are
1061  *     in any particular order. You may find :func:`domSort` helpful to remedy
1062  *     the latter.
1063  */
1064 function clusters(fnodes, splittingDistance, getDistance = distance) {
1065     const matrix = new DistanceMatrix(fnodes, getDistance);
1066     let closest;
1068     while (matrix.numClusters() > 1 && (closest = matrix.closest()).distance < splittingDistance) {
1069         matrix.merge(closest.a, closest.b);
1070     }
1072     return matrix.clusters();
1075 var clusters$1 = /*#__PURE__*/ Object.freeze({
1076     __proto__: null,
1077     clusters: clusters,
1078     distance: distance,
1079     euclidean: euclidean,
1082 // The left-hand side of a rule
1085  * Take nodes that match a given DOM selector. Example:
1086  * ``dom('meta[property="og:title"]')``
1088  * Every ruleset has at least one ``dom`` or :func:`element` rule, as that is
1089  * where nodes begin to flow into the system. If run against a subtree of a
1090  * document, the root of the subtree is not considered as a possible match.
1091  */
1092 function dom(selector) {
1093     return new DomLhs(selector);
1097  * Take a single given node if it matches a given DOM selector, without looking
1098  * through its descendents or ancestors. Otherwise, take no nodes. Example:
1099  * ``element('input')``
1101  * This is useful for applications in which you want Fathom to classify an
1102  * element the user has selected, rather than scanning the whole page for
1103  * candidates.
1104  */
1105 function element(selector) {
1106     return new ElementLhs(selector);
1110  * PROTON EXTENSION
1111  * Allows running a custom function when selecting
1112  * DOM elements in a fathom LHS rule.
1113  */
1114 function domQuery(query) {
1115     return new DomQueryLhs(query);
1119  * Rules and the LHSs and RHSs that comprise them have no mutable state. This
1120  * lets us make BoundRulesets from Rulesets without duplicating the rules. It
1121  * also lets us share a common cache among rules: multiple ones might care
1122  * about a cached type(), for instance; there isn't a one-to-one relationship
1123  * of storing with caring. There would also, because of the interdependencies
1124  * of rules in a ruleset, be little use in segmenting the caches: if you do
1125  * something that causes one to need to be cleared, you'll need to clear many
1126  * more as well.
1128  * Lhses are responsible for maintaining ruleset.maxCache.
1130  * Lhs and its subclasses are private to the Fathom framework.
1131  */
1132 class Lhs {
1133     constructor() {
1134         this._predicate = () => true;
1135     }
1137     /** Return a new Lhs of the appropriate kind, given its first call. */
1138     static fromFirstCall(firstCall) {
1139         // firstCall is never 'dom', because dom() directly returns a DomLhs.
1140         if (firstCall.method === 'type') {
1141             return new TypeLhs(...firstCall.args);
1142         } else if (firstCall.method === 'and') {
1143             return new AndLhs(firstCall.args);
1144         } else if (firstCall.method === 'nearest') {
1145             return new NearestLhs(firstCall.args);
1146         } else {
1147             throw new Error('The left-hand side of a rule() must start with dom(), type(), and(), or nearest().');
1148         }
1149     }
1151     /**
1152      * Prune nodes from consideration early in run execution, before scoring is
1153      * done.
1154      *
1155      * Reserve this for where you are sure it is always correct or when
1156      * performance demands it. It is generally preferable to use :func:`score`
1157      * and let the :doc:`trainer<training>` determine the relative significance
1158      * of each rule. Human intuition as to what is important is often wrong:
1159      * for example, one might assume that a music player website would include
1160      * the word "play", but this does not hold once you include sites in other
1161      * languages.
1162      *
1163      * Can be chained after :func:`type` or :func:`dom`.
1164      *
1165      * Example: ``dom('p').when(isVisible)``
1166      *
1167      * @arg {function} predicate Accepts a fnode and returns a boolean
1168      */
1169     when(predicate) {
1170         let lhs = this.clone();
1171         lhs._predicate = predicate;
1172         return lhs;
1173     }
1175     /**
1176      * Of all the dom nodes selected by type() or dom(), return only
1177      * the fnodes that satisfy all the predicates imposed by calls to
1178      * when()
1179      */
1180     fnodesSatisfyingWhen(fnodes) {
1181         return Array.from(fnodes).filter(this._predicate);
1182     }
1184     /**
1185      * Return an iterable of output fnodes selected by this left-hand-side
1186      * expression.
1187      *
1188      * Pre: The rules I depend on have already been run, and their results are
1189      * in ruleset.typeCache.
1190      *
1191      * @arg ruleset {BoundRuleset}
1192      */
1193     // fnodes (ruleset) {}
1195     /**
1196      * Check that a RHS-emitted fact is legal for this kind of LHS, and throw
1197      * an error if it isn't.
1198      */
1199     checkFact(fact) {}
1201     /**
1202      * Return the single type the output of the LHS is guaranteed to have.
1203      * Return undefined if there is no such single type we can ascertain.
1204      */
1205     guaranteedType() {}
1207     /**
1208      * Return the type I aggregate if I am an aggregate LHS; return undefined
1209      * otherwise.
1210      */
1211     aggregatedType() {}
1213     /**
1214      * Return each combination of types my selected nodes could be locally (that
1215      * is, by this rule only) constrained to have.
1216      *
1217      * For example, type(A) would return [A]. and(A, or(B, C)) would return
1218      * [AB, AC, ABC]. More examples:
1219      *
1220      * or(A, B) â†’ typeIn(A, B, C)  # Finalizes A, B.   combos A, B, AB: finalizes AB. Optimization: there's no point in returning the last combo in ors. Compilation into 2 rules with identical RHSs will inherently implement this optimization.
1221      * or(A, B) â†’ typeIn(A, B)  # Finalizes A, B
1222      * or(A, B) â†’ A  # Finalizes B
1223      * and(A) -> A  # Finalizes nothing
1224      * and(A, B) -> A  # Finalizes nothing.   AB: Ã˜
1225      * and(A) -> typeIn(A, B)  # Finalizes A.   A
1226      * and(A, B) -> typeIn(A, B)  # Finalizes nothing.   AB
1227      * and(A, B) -> typeIn(A, B, C)  # Finalizes A, B.   AB
1228      * and(A, or(B, C)) -> D  # Finalizes A, B, C.   AB, AC, ABC: ABC
1229      * and(A, or(B, C)) -> B  # Finalizes A, C.   AB, AC, ABC: AC
1230      * type(A).not(and(A, B)) ->
1231      *
1232      * @return {NiceSet[]}
1233      */
1234     // possibleTypeCombinations() {}
1236     /**
1237      * Types mentioned in this LHS.
1238      *
1239      * In other words, the types I need to know the assignment status of before
1240      * I can make my selections
1241      *
1242      * @return NiceSet of strings
1243      */
1244     // typesMentioned() {}
1247 class DomLhs extends Lhs {
1248     constructor(selector) {
1249         super();
1250         if (selector === undefined) {
1251             throw new Error(
1252                 'A querySelector()-style selector is required as the argument to ' + this._callName() + '().'
1253             );
1254         }
1255         this.selector = selector;
1256     }
1258     /**
1259      * Return the name of this kind of LHS, for use in error messages.
1260      */
1261     _callName() {
1262         return 'dom';
1263     }
1265     clone() {
1266         return new this.constructor(this.selector);
1267     }
1269     fnodes(ruleset) {
1270         return this._domNodesToFilteredFnodes(ruleset, ruleset.doc.querySelectorAll(this.selector));
1271     }
1273     /**
1274      * Turn a NodeList of DOM nodes into an array of fnodes, and filter out
1275      * those that don't match the :func:`when()` clause.
1276      */
1277     _domNodesToFilteredFnodes(ruleset, domNodes) {
1278         let ret = [];
1279         for (let i = 0; i < domNodes.length; i++) {
1280             ret.push(ruleset.fnodeForElement(domNodes[i]));
1281         }
1282         return this.fnodesSatisfyingWhen(ret);
1283     }
1285     checkFact(fact) {
1286         if (fact.type === undefined) {
1287             throw new Error(
1288                 `The right-hand side of a ${this._callName()}() rule failed to specify a type. This means there is no way for its output to be used by later rules. All it specified was ${fact}.`
1289             );
1290         }
1291     }
1293     asLhs() {
1294         return this;
1295     }
1297     possibleTypeCombinations() {
1298         return [];
1299     }
1301     typesMentioned() {
1302         return new NiceSet();
1303     }
1306 class ElementLhs extends DomLhs {
1307     _callName() {
1308         return 'element';
1309     }
1311     fnodes(ruleset) {
1312         return this._domNodesToFilteredFnodes(ruleset, ruleset.doc.matches(this.selector) ? [ruleset.doc] : []);
1313     }
1317  * PROTON EXTENSION
1318  * Extends DomLhs rule but instead of passing
1319  * a single string selector, allows passing a
1320  * custom selector function that will be run
1321  * against the document
1322  */
1323 class DomQueryLhs extends DomLhs {
1324     constructor(query) {
1325         /**
1326          * mock selector to avoid parent DomLhs
1327          * throwing in constructor call
1328          */
1329         super('');
1330         this.query = query;
1331     }
1333     _callName() {
1334         return 'domQuery';
1335     }
1337     clone() {
1338         return new DomQueryLhs(this.query);
1339     }
1341     fnodes(ruleset) {
1342         return this._domNodesToFilteredFnodes(ruleset, this.query(ruleset.doc));
1343     }
1346 /** Internal representation of a LHS constrained by type but not by max() */
1347 class TypeLhs extends Lhs {
1348     constructor(type) {
1349         super();
1350         if (type === undefined) {
1351             throw new Error('A type name is required when calling type().');
1352         }
1353         this._type = type; // the input type
1354     }
1356     clone() {
1357         return new this.constructor(this._type);
1358     }
1360     fnodes(ruleset) {
1361         const cached = getDefault(ruleset.typeCache, this._type, () => []);
1362         return this.fnodesSatisfyingWhen(cached);
1363     }
1365     /** Override the type previously specified by this constraint. */
1366     type(inputType) {
1367         // Preserve the class in case this is a TypeMaxLhs.
1368         return new this.constructor(inputType);
1369     }
1371     /**
1372      * Of the nodes selected by a ``type`` call to the left, constrain the LHS
1373      * to return only the max-scoring one. If there is a tie, more than 1 node
1374      * will be returned. Example: ``type('titley').max()``
1375      */
1376     max() {
1377         return new TypeMaxLhs(this._type);
1378     }
1380     /**
1381      * Take the nodes selected by a ``type`` call to the left, group them into
1382      * clusters, and return the nodes in the cluster that has the highest total
1383      * score (on the relevant type).
1384      *
1385      * Nodes come out in arbitrary order, so, if you plan to emit them,
1386      * consider using ``.out('whatever').allThrough(domSort)``. See
1387      * :func:`domSort`.
1388      *
1389      * If multiple clusters have equally high scores, return an arbitrary one,
1390      * because Fathom has no way to represent arrays of arrays in rulesets.
1391      *
1392      * @arg options {Object} The same depth costs taken by :func:`distance`,
1393      *     plus ``splittingDistance``, which is the distance beyond which 2
1394      *     clusters will be considered separate. ``splittingDistance``, if
1395      *     omitted, defaults to 3.
1396      */
1397     bestCluster(options) {
1398         return new BestClusterLhs(this._type, options);
1399     }
1401     // Other clustering calls could be called biggestCluster() (having the most
1402     // nodes) and bestAverageCluster() (having the highest average score).
1404     guaranteedType() {
1405         return this._type;
1406     }
1408     possibleTypeCombinations() {
1409         return [this.typesMentioned()];
1410     }
1412     typesMentioned() {
1413         return new NiceSet([this._type]);
1414     }
1418  * Abstract LHS that is an aggregate function taken across all fnodes of a type
1420  * The main point here is that any aggregate function over a (typed) set of
1421  * nodes depends on first computing all the rules that could emit those nodes
1422  * (nodes of that type).
1423  */
1424 class AggregateTypeLhs extends TypeLhs {
1425     aggregatedType() {
1426         return this._type;
1427     }
1431  * Internal representation of a LHS that has both type and max([NUMBER])
1432  * constraints. max(NUMBER != 1) support is not yet implemented.
1433  */
1434 class TypeMaxLhs extends AggregateTypeLhs {
1435     /**
1436      * Return the max-scoring node (or nodes if there is a tie) of the given
1437      * type.
1438      */
1439     fnodes(ruleset) {
1440         // TODO: Optimize better. Walk the dependency tree, and run only the
1441         // rules that could possibly lead to a max result. As part of this,
1442         // make RHSs expose their max potential scores.
1443         const self = this;
1444         // Work around V8 bug:
1445         // https://stackoverflow.com/questions/32943776/using-super-within-an-
1446         // arrow-function-within-an-arrow-function-within-a-method
1447         const getSuperFnodes = () => super.fnodes(ruleset);
1448         return setDefault(ruleset.maxCache, this._type, function maxFnodesOfType() {
1449             return maxes(getSuperFnodes(), (fnode) => ruleset.weightedScore(fnode.scoresSoFarFor(self._type)));
1450         });
1451     }
1454 class BestClusterLhs extends AggregateTypeLhs {
1455     constructor(type, options) {
1456         super(type);
1457         this._options = options || { splittingDistance: 3 };
1458     }
1460     /**
1461      * Group the nodes of my type into clusters, and return the cluster with
1462      * the highest total score for that type.
1463      */
1464     fnodes(ruleset) {
1465         // Get the nodes of the type:
1466         const fnodesOfType = Array.from(super.fnodes(ruleset));
1467         if (fnodesOfType.length === 0) {
1468             return [];
1469         }
1470         // Cluster them:
1471         const clusts = clusters(fnodesOfType, this._options.splittingDistance, (a, b) => distance(a, b, this._options));
1472         // Tag each cluster with the total of its nodes' scores:
1473         const clustsAndSums = clusts.map((clust) => [clust, sum(clust.map((fnode) => fnode.scoreFor(this._type)))]);
1474         // Return the highest-scoring cluster:
1475         return max(clustsAndSums, (clustAndSum) => clustAndSum[1])[0];
1476     }
1479 class AndLhs extends Lhs {
1480     constructor(lhss) {
1481         super();
1483         // For the moment, we accept only type()s as args. TODO: Generalize to
1484         // type().max() and such later.
1485         this._args = lhss.map(sideToTypeLhs);
1486     }
1488     *fnodes(ruleset) {
1489         // Take an arbitrary one for starters. Optimization: we could always
1490         // choose the pickiest one to start with.
1491         const fnodes = this._args[0].fnodes(ruleset);
1492         // Then keep only the fnodes that have the type of every other arg:
1493         fnodeLoop: for (let fnode of fnodes) {
1494             for (let otherLhs of this._args.slice(1)) {
1495                 // Optimization: could use a .hasTypeSoFar() below
1496                 if (!fnode.hasType(otherLhs.guaranteedType())) {
1497                     // TODO: This is n^2. Why is there no set intersection in JS?!
1498                     continue fnodeLoop;
1499                 }
1500             }
1501             yield fnode;
1502         }
1503     }
1505     possibleTypeCombinations() {
1506         return [this.typesMentioned()];
1507     }
1509     typesMentioned() {
1510         return new NiceSet(this._args.map((arg) => arg.guaranteedType()));
1511     }
1514 function sideToTypeLhs(side) {
1515     const lhs = side.asLhs();
1516     if (!(lhs.constructor === TypeLhs)) {
1517         throw new Error('and() and nearest() support only simple type() calls as arguments for now.');
1518         // TODO: Though we could solve this with a compilation step: and(type(A), type(B).max()) is equivalent to type(B).max() -> type(Bmax); and(type(A), type(Bmax)).
1519         // In fact, we should be able to compile most (any?) arbitrary and()s, including nested ands and and(type(...).max(), ...) constructions into several and(type(A), type(B), ...) rules.
1520     }
1521     return lhs;
1524 class NearestLhs extends Lhs {
1525     constructor([a, b, distance]) {
1526         super();
1527         this._a = sideToTypeLhs(a);
1528         this._b = sideToTypeLhs(b);
1529         this._distance = distance;
1530     }
1532     /**
1533      * Return an iterable of {fnodes, transformer} pairs.
1534      */
1535     *fnodes(ruleset) {
1536         // Go through all the left arg's nodes. For each one, find the closest
1537         // right-arg's node. O(a * b). Once a right-arg's node is used, we
1538         // don't eliminate it from consideration, because then order of left-
1539         // args' nodes would matter.
1541         // TODO: Still not sure how to get the distance to factor into the
1542         // score unless we hard-code nearest() to do that. It's a
1543         // matter of not being able to bind on the RHS to the output of the
1544         // distance function on the LHS. Perhaps we could at least make
1545         // distance part of the note and read it in a props() callback.
1547         // We're assuming here that simple type() calls return just plain
1548         // fnodes, not {fnode, rhsTransformer} pairs:
1549         const as_ = this._a.fnodes(ruleset);
1550         const bs = Array.from(this._b.fnodes(ruleset));
1551         if (bs.length > 0) {
1552             // If bs is empty, there can be no nearest nodes, so don't emit any.
1553             for (const a of as_) {
1554                 const nearest = min(bs, (b) => this._distance(a, b));
1555                 yield {
1556                     fnode: a,
1557                     rhsTransformer: function setNoteIfEmpty(fact) {
1558                         // If note is explicitly set by the RHS, let it take
1559                         // precedence, even though that makes this entire LHS
1560                         // pointless.
1561                         if (fact.note === undefined) {
1562                             fact.note = nearest; // TODO: Wrap this in an object to make room to return distance later.
1563                         }
1564                         return fact;
1565                     },
1566                 };
1567             }
1568         }
1569     }
1571     checkFact(fact) {
1572         // Barf if the fact doesn't set a type at least. It should be a *new* type or at least one that doesn't result in cycles, but we can't deduce that.
1573     }
1575     possibleTypeCombinations() {
1576         return [new NiceSet([this._a.guaranteedType()])];
1577     }
1579     typesMentioned() {
1580         return new NiceSet([this._a.guaranteedType(), this._b.guaranteedType()]);
1581     }
1583     guaranteedType() {
1584         return this._a.guaranteedType();
1585     }
1588 // The right-hand side of a rule
1590 const TYPE = 1;
1591 const NOTE = 2;
1592 const SCORE = 4;
1593 const ELEMENT = 8;
1594 const SUBFACTS = {
1595     type: TYPE,
1596     note: NOTE,
1597     score: SCORE,
1598     element: ELEMENT,
1602  * Expose the output of this rule's LHS as a "final result" to the surrounding
1603  * program. It will be available by calling :func:`~BoundRuleset.get` on the
1604  * ruleset and passing the key. You can run each node through a callback
1605  * function first by adding :func:`through()`, or you can run the entire set of
1606  * nodes through a callback function by adding :func:`allThrough()`.
1607  */
1608 function out(key) {
1609     return new OutwardRhs(key);
1612 class InwardRhs {
1613     constructor(calls = [], max = Infinity, types) {
1614         this._calls = calls.slice();
1615         this._max = max; // max score
1616         this._types = new NiceSet(types); // empty set if unconstrained
1617     }
1619     /**
1620      * Declare that the maximum returned subscore is such and such,
1621      * which helps the optimizer plan efficiently. This doesn't force it to be
1622      * true; it merely throws an error at runtime if it isn't. To lift an
1623      * ``atMost`` constraint, call ``atMost()`` (with no args). The reason
1624      * ``atMost`` and ``typeIn`` apply until explicitly cleared is so that, if
1625      * someone used them for safety reasons on a lexically distant rule you are
1626      * extending, you won't stomp on their constraint and break their
1627      * invariants accidentally.
1628      */
1629     atMost(score) {
1630         return new this.constructor(this._calls, score, this._types);
1631     }
1633     _checkAtMost(fact) {
1634         if (fact.score !== undefined && fact.score > this._max) {
1635             throw new Error(`Score of ${fact.score} exceeds the declared atMost(${this._max}).`);
1636         }
1637     }
1639     /**
1640      * Determine any of type, note, score, and element using a callback. This
1641      * overrides any previous call to `props` and, depending on what
1642      * properties of the callback's return value are filled out, may override
1643      * the effects of other previous calls as well.
1644      *
1645      * The callback should return...
1646      *
1647      * * An optional :term:`subscore`
1648      * * A type (required on ``dom(...)`` rules, defaulting to the input one on
1649      *   ``type(...)`` rules)
1650      * * Optional notes
1651      * * An element, defaulting to the input one. Overriding the default
1652      *   enables a callback to walk around the tree and say things about nodes
1653      *   other than the input one.
1654      */
1655     props(callback) {
1656         function getSubfacts(fnode) {
1657             const subfacts = callback(fnode);
1658             // Filter the raw result down to okayed properties so callbacks
1659             // can't insert arbitrary keys (like conserveScore, which might
1660             // mess up the optimizer).
1661             for (let subfact in subfacts) {
1662                 if (!SUBFACTS.hasOwnProperty(subfact) || !(SUBFACTS[subfact] & getSubfacts.possibleSubfacts)) {
1663                     // The ES5.1 spec says in 12.6.4 that it's fine to delete
1664                     // as we iterate.
1665                     delete subfacts[subfact];
1666                 }
1667             }
1668             return subfacts;
1669         }
1670         // Thse are the subfacts this call could affect:
1671         getSubfacts.possibleSubfacts = TYPE | NOTE | SCORE | ELEMENT;
1672         getSubfacts.kind = 'props';
1673         return new this.constructor(this._calls.concat(getSubfacts), this._max, this._types);
1674     }
1676     /**
1677      * Set the type applied to fnodes processed by this RHS.
1678      */
1679     type(theType) {
1680         // In the future, we might also support providing a callback that receives
1681         // the fnode and returns a type. We couldn't reason based on these, but the
1682         // use would be rather a consise way to to override part of what a previous
1683         // .props() call provides.
1685         // Actually emit a given type.
1686         function getSubfacts() {
1687             return { type: theType };
1688         }
1689         getSubfacts.possibleSubfacts = TYPE;
1690         getSubfacts.type = theType;
1691         getSubfacts.kind = 'type';
1692         return new this.constructor(this._calls.concat(getSubfacts), this._max, this._types);
1693     }
1695     /**
1696      * Constrain this rule to emit 1 of a set of given types. Pass no args to lift
1697      * a previous ``typeIn`` constraint, as you might do when basing a LHS on a
1698      * common value to factor out repetition.
1699      *
1700      * ``typeIn`` is mostly a hint for the query planner when you're emitting types
1701      * dynamically from ``props`` calls—in fact, an error will be raised if
1702      * ``props`` is used without a ``typeIn`` or ``type`` to constrain it—but it
1703      * also checks conformance at runtime to ensure validity.
1704      */
1705     typeIn(...types) {
1706         // Rationale: If we used the spelling "type('a', 'b', ...)" instead of
1707         // this, one might expect type('a', 'b').type(fn) to have the latter
1708         // call override, while expecting type(fn).type('a', 'b') to keep both
1709         // in effect. Then different calls to type() don't consistently
1710         // override each other, and the rules get complicated. Plus you can't
1711         // inherit a type constraint and then sub in another type-returning
1712         // function that still gets the constraint applied.
1713         return new this.constructor(this._calls, this._max, types);
1714     }
1716     /**
1717      * Check a fact for conformance with any typeIn() call.
1718      *
1719      * @arg leftType the type of the LHS, which becomes my emitted type if the
1720      *    fact doesn't specify one
1721      */
1722     _checkTypeIn(result, leftType) {
1723         if (this._types.size > 0) {
1724             if (result.type === undefined) {
1725                 if (!this._types.has(leftType)) {
1726                     throw new Error(
1727                         `A right-hand side claimed, via typeIn(...) to emit one of the types ${this._types} but actually inherited ${leftType} from the left-hand side.`
1728                     );
1729                 }
1730             } else if (!this._types.has(result.type)) {
1731                 throw new Error(
1732                     `A right-hand side claimed, via typeIn(...) to emit one of the types ${this._types} but actually emitted ${result.type}.`
1733                 );
1734             }
1735         }
1736     }
1738     /**
1739      * Whatever the callback returns (even ``undefined``) becomes the note of
1740      * the fact. This overrides any previous call to ``note``.
1741      */
1742     note(callback) {
1743         function getSubfacts(fnode) {
1744             return { note: callback(fnode) };
1745         }
1746         getSubfacts.possibleSubfacts = NOTE;
1747         getSubfacts.kind = 'note';
1748         return new this.constructor(this._calls.concat(getSubfacts), this._max, this._types);
1749     }
1751     /**
1752      * Affect the confidence with which the input node should be considered a
1753      * member of a type.
1754      *
1755      * The parameter is generally between 0 and 1 (inclusive), with 0 meaning
1756      * the node does not have the "smell" this rule checks for and 1 meaning it
1757      * does. The range between 0 and 1 is available to represent "fuzzy"
1758      * confidences. If you have an unbounded range to compress down to [0, 1],
1759      * consider using :func:`sigmoid` or a scaling thereof.
1760      *
1761      * Since every node can have multiple, independent scores (one for each
1762      * type), this applies to the type explicitly set by the RHS or, if none,
1763      * to the type named by the ``type`` call on the LHS. If the LHS has none
1764      * because it's a ``dom(...)`` LHS, an error is raised.
1765      *
1766      * @arg {number|function} scoreOrCallback Can either be a static number,
1767      *     generally 0 to 1 inclusive, or else a callback which takes the fnode
1768      *     and returns such a number. If the callback returns a boolean, it is
1769      *     cast to a number.
1770      */
1771     score(scoreOrCallback) {
1772         let getSubfacts;
1774         function getSubfactsFromNumber(fnode) {
1775             return { score: scoreOrCallback };
1776         }
1778         function getSubfactsFromFunction(fnode) {
1779             let result = scoreOrCallback(fnode);
1780             if (typeof result === 'boolean') {
1781                 // Case bools to numbers for convenience. Boolean features are
1782                 // common. Don't cast other things, as it frustrates ruleset
1783                 // debugging.
1784                 result = Number(result);
1785             }
1786             return { score: result };
1787         }
1789         if (typeof scoreOrCallback === 'number') {
1790             getSubfacts = getSubfactsFromNumber;
1791         } else {
1792             getSubfacts = getSubfactsFromFunction;
1793         }
1794         getSubfacts.possibleSubfacts = SCORE;
1795         getSubfacts.kind = 'score';
1797         return new this.constructor(this._calls.concat(getSubfacts), this._max, this._types);
1798     }
1800     // Future: why not have an .element() method for completeness?
1802     // -------- Methods below this point are private to the framework. --------
1804     /**
1805      * Run all my props().type().note().score() stuff across a given fnode,
1806      * enforce my max() stuff, and return a fact ({element, type, score,
1807      * notes}) for incorporation into that fnode (or a different one, if
1808      * element is specified). Any of the 4 fact properties can be missing;
1809      * filling in defaults is a job for the caller.
1810      *
1811      * @arg leftType The type the LHS takes in
1812      */
1813     fact(fnode, leftType) {
1814         const doneKinds = new Set();
1815         const result = {};
1816         let haveSubfacts = 0;
1817         for (let call of reversed(this._calls)) {
1818             // If we've already called a call of this kind, then forget it.
1819             if (!doneKinds.has(call.kind)) {
1820                 doneKinds.add(call.kind);
1822                 if (~haveSubfacts & call.possibleSubfacts) {
1823                     // This call might provide a subfact we are missing.
1824                     const newSubfacts = call(fnode);
1826                     // We start with an empty object, so we're okay here.
1827                     // eslint-disable-next-line guard-for-in
1828                     for (let subfact in newSubfacts) {
1829                         // A props() callback could insert arbitrary keys into
1830                         // the result, but it shouldn't matter, because nothing
1831                         // pays any attention to them.
1832                         if (!result.hasOwnProperty(subfact)) {
1833                             result[subfact] = newSubfacts[subfact];
1834                         }
1835                         haveSubfacts |= SUBFACTS[subfact];
1836                     }
1837                 }
1838             }
1839         }
1840         this._checkAtMost(result);
1841         this._checkTypeIn(result, leftType);
1842         return result;
1843     }
1845     /**
1846      * Return a record describing the types I might emit (which means either to
1847      * add a type to a fnode or to output a fnode that already has that type).
1848      * {couldChangeType: whether I might add a type to the fnode,
1849      *  possibleTypes: If couldChangeType, the types I might emit; empty set if
1850      *      we cannot infer it. If not couldChangeType, undefined.}
1851      */
1852     possibleEmissions() {
1853         // If there is a typeIn() constraint or there is a type() call to the
1854         // right of all props() calls, we have a constraint. We hunt for the
1855         // tightest constraint we can find, favoring a type() call because it
1856         // gives us a single type but then falling back to a typeIn().
1857         let couldChangeType = false;
1858         for (let call of reversed(this._calls)) {
1859             if (call.kind === 'props') {
1860                 couldChangeType = true;
1861                 break;
1862             } else if (call.kind === 'type') {
1863                 return { couldChangeType: true, possibleTypes: new Set([call.type]) };
1864             }
1865         }
1866         return { couldChangeType, possibleTypes: this._types };
1867     }
1870 class OutwardRhs {
1871     constructor(key, through = (x) => x, allThrough = (x) => x) {
1872         this.key = key;
1873         this.callback = through;
1874         this.allCallback = allThrough;
1875     }
1877     /**
1878      * Append ``.through`` to :func:`out` to run each :term:`fnode` emitted
1879      * from the LHS through an arbitrary function before returning it to the
1880      * containing program. Example::
1881      *
1882      *     out('titleLengths').through(fnode => fnode.noteFor('title').length)
1883      */
1884     through(callback) {
1885         return new this.constructor(this.key, callback, this.allCallback);
1886     }
1888     /**
1889      * Append ``.allThrough`` to :func:`out` to run the entire iterable of
1890      * emitted :term:`fnodes<fnode>` through an arbitrary function before
1891      * returning them to the containing program. Example::
1892      *
1893      *     out('sortedTitles').allThrough(domSort)
1894      */
1895     allThrough(callback) {
1896         return new this.constructor(this.key, this.callback, callback);
1897     }
1899     asRhs() {
1900         return this;
1901     }
1904 function props(callback) {
1905     return new Side({ method: 'props', args: [callback] });
1908 /** Constrain to an input type on the LHS, or apply a type on the RHS. */
1909 function type(theType) {
1910     return new Side({ method: 'type', args: [theType] });
1913 function note(callback) {
1914     return new Side({ method: 'note', args: [callback] });
1917 function score(scoreOrCallback) {
1918     return new Side({ method: 'score', args: [scoreOrCallback] });
1921 function atMost(score) {
1922     return new Side({ method: 'atMost', args: [score] });
1925 function typeIn(...types) {
1926     return new Side({ method: 'typeIn', args: types });
1930  * Pull nodes that conform to multiple conditions at once.
1932  * For example: ``and(type('title'), type('english'))``
1934  * Caveats: ``and`` supports only simple ``type`` calls as arguments for now,
1935  * and it may fire off more rules as prerequisites than strictly necessary.
1936  * ``not`` and ``or`` don't exist yet, but you can express ``or`` the long way
1937  * around by having 2 rules with identical RHSs.
1938  */
1939 function and(...lhss) {
1940     return new Side({ method: 'and', args: lhss });
1944  * Experimental. For each :term:`fnode` from ``typeCallA``, find the closest
1945  * node from ``typeCallB``, and attach it as a note. The note is attached to
1946  * the type specified by the RHS, defaulting to the type of ``typeCallA``. If
1947  * no nodes are emitted from ``typeCallB``, do nothing.
1949  * For example... ::
1951  *     nearest(type('image'), type('price'))
1953  * The score of the ``typeCallA`` can be added to the new type's score by using
1954  * :func:`conserveScore` (though this routine has since been removed)::
1956  *     rule(nearest(type('image'), type('price')),
1957  *          type('imageWithPrice').score(2).conserveScore())
1959  * Caveats: ``nearest`` supports only simple ``type`` calls as arguments ``a``
1960  * and ``b`` for now.
1962  * @arg distance {function} A function that takes 2 fnodes and returns a
1963  *     numerical distance between them. Included options are :func:`distance`,
1964  *     which is a weighted topological distance, and :func:`euclidean`, which
1965  *     is a spatial distance.
1966  */
1967 function nearest(typeCallA, typeCallB, distance = euclidean) {
1968     return new Side({
1969         method: 'nearest',
1970         args: [typeCallA, typeCallB, distance],
1971     });
1975  * A chain of calls that can be compiled into a Rhs or Lhs, depending on its
1976  * position in a Rule. This lets us use type() as a leading call for both RHSs
1977  * and LHSs. I would prefer to do this dynamically, but that wouldn't compile
1978  * down to old versions of ES.
1979  */
1980 class Side {
1981     constructor(...calls) {
1982         // A "call" is like {method: 'dom', args: ['p.smoo']}.
1983         this._calls = calls;
1984     }
1986     max() {
1987         return this._and('max');
1988     }
1990     bestCluster(options) {
1991         return this._and('bestCluster', options);
1992     }
1994     props(callback) {
1995         return this._and('props', callback);
1996     }
1998     type(...types) {
1999         return this._and('type', ...types);
2000     }
2002     note(callback) {
2003         return this._and('note', callback);
2004     }
2006     score(scoreOrCallback) {
2007         return this._and('score', scoreOrCallback);
2008     }
2010     atMost(score) {
2011         return this._and('atMost', score);
2012     }
2014     typeIn(...types) {
2015         return this._and('typeIn', ...types);
2016     }
2018     and(...lhss) {
2019         return this._and('and', lhss);
2020     }
2022     _and(method, ...args) {
2023         return new this.constructor(...this._calls.concat({ method, args }));
2024     }
2026     asLhs() {
2027         return this._asSide(Lhs.fromFirstCall(this._calls[0]), this._calls.slice(1));
2028     }
2030     asRhs() {
2031         return this._asSide(new InwardRhs(), this._calls);
2032     }
2034     _asSide(side, calls) {
2035         for (let call of calls) {
2036             side = side[call.method](...call.args);
2037         }
2038         return side;
2039     }
2041     when(pred) {
2042         return this._and('when', pred);
2043     }
2047  * A wrapper around a DOM node, storing :term:`types<type>`,
2048  * :term:`scores<score>`, and :term:`notes<note>` that apply to it
2049  */
2050 class Fnode {
2051     /**
2052      * @arg element The DOM element described by the fnode.
2053      * @arg ruleset The ruleset which created the fnode.
2054      */
2055     constructor(element, ruleset) {
2056         if (element === undefined) {
2057             throw new Error("Someone tried to make a fnode without specifying the element they're talking about.");
2058         }
2059         /**
2060          * The raw DOM element this fnode describes
2061          */
2062         this.element = element;
2063         this._ruleset = ruleset;
2065         // A map of type => {score: number, note: anything}. `score` is always
2066         // present and defaults to 1. A note is set iff `note` is present and
2067         // not undefined.
2068         this._types = new Map();
2070         // Note: conserveScore() is temporarily absent in 3.0.
2071         //
2072         // By default, a fnode has an independent score for each of its types.
2073         // However, a RHS can opt to conserve the score of an upstream type,
2074         // carrying it forward into another type. To avoid runaway scores in
2075         // the case that multiple rules choose to do this, we limit the
2076         // contribution of an upstream type's score to being multiplied in a
2077         // single time. In this set, we keep track of which upstream types'
2078         // scores have already been multiplied into each type. LHS fnode => Set
2079         // of types whose score for that node have been multiplied into this
2080         // node's score.
2081         this._conservedScores = new Map();
2082     }
2084     /**
2085      * Return whether the given type is one of the ones attached to the wrapped
2086      * HTML node.
2087      */
2088     hasType(type) {
2089         // Run type(theType) against the ruleset to make sure this doesn't
2090         // return false just because we haven't lazily run certain rules yet.
2091         this._computeType(type);
2092         return this._types.has(type);
2093     }
2095     /**
2096      * Return the confidence, in the range (0, 1), that the fnode belongs to the
2097      * given type, 0 by default.
2098      */
2099     scoreFor(type) {
2100         this._computeType(type);
2101         return sigmoid(
2102             this._ruleset.weightedScore(this.scoresSoFarFor(type)) + getDefault(this._ruleset.biases, type, () => 0)
2103         );
2104     }
2106     /**
2107      * Return the fnode's note for the given type, ``undefined`` if none.
2108      */
2109     noteFor(type) {
2110         this._computeType(type);
2111         return this._noteSoFarFor(type);
2112     }
2114     /**
2115      * Return whether this fnode has a note for the given type.
2116      *
2117      * ``undefined`` is not considered a note and may be overwritten with
2118      * impunity.
2119      */
2120     hasNoteFor(type) {
2121         this._computeType(type);
2122         return this._hasNoteSoFarFor(type);
2123     }
2125     // -------- Methods below this point are private to the framework. --------
2127     /**
2128      * Return an iterable of the types tagged onto me by rules that have
2129      * already executed.
2130      */
2131     typesSoFar() {
2132         return this._types.keys();
2133     }
2135     _noteSoFarFor(type) {
2136         return this._typeRecordForGetting(type).note;
2137     }
2139     _hasNoteSoFarFor(type) {
2140         return this._noteSoFarFor(type) !== undefined;
2141     }
2143     /**
2144      * Return the score thus far computed on me for a certain type. Doesn't
2145      * implicitly run any rules. If no score has yet been determined for the
2146      * given type, return undefined.
2147      */
2148     scoresSoFarFor(type) {
2149         return this._typeRecordForGetting(type).score;
2150     }
2152     /**
2153      * Add a given number to one of our per-type scores. Implicitly assign us
2154      * the given type. Keep track of which rule it resulted from so we can
2155      * later mess with the coeffs.
2156      */
2157     addScoreFor(type, score, ruleName) {
2158         this._typeRecordForSetting(type).score.set(ruleName, score);
2159     }
2161     /**
2162      * Set the note attached to one of our types. Implicitly assign us that
2163      * type if we don't have it already.
2164      */
2165     setNoteFor(type, note) {
2166         if (this._hasNoteSoFarFor(type)) {
2167             if (note !== undefined) {
2168                 throw new Error(
2169                     `Someone (likely the right-hand side of a rule) tried to add a note of type ${type} to an element, but one of that type already exists. Overwriting notes is not allowed, since it would make the order of rules matter.`
2170                 );
2171             }
2172             // else the incoming note is undefined and we already have the
2173             // type, so it's a no-op
2174         } else {
2175             // Apply either a type and note or just a type (which means a note
2176             // that is undefined):
2177             this._typeRecordForSetting(type).note = note;
2178         }
2179     }
2181     /**
2182      * Return a score/note record for a type, creating it if it doesn't exist.
2183      */
2184     _typeRecordForSetting(type) {
2185         return setDefault(this._types, type, () => ({ score: new Map() }));
2186     }
2188     /**
2189      * Manifest a temporary type record for reading, working around the lack of
2190      * a .? operator in JS.
2191      */
2192     _typeRecordForGetting(type) {
2193         return getDefault(this._types, type, () => ({ score: new Map() }));
2194     }
2196     /**
2197      * Make sure any scores, notes, and type-tagging for the given type are
2198      * computed for my element.
2199      */
2200     _computeType(theType) {
2201         if (!this._types.has(theType)) {
2202             // Prevent infinite recursion when an A->A rule looks at A's note in a callback.
2203             this._ruleset.get(type(theType));
2204         }
2205     }
2209  * Construct and return the proper type of rule class based on the
2210  * inwardness/outwardness of the RHS.
2212  * @arg lhs {Lhs} The left-hand side of the rule
2213  * @arg rhs {Rhs} The right-hand side of the rule
2214  * @arg options {object} Other, optional information about the rule.
2215  *     Currently, the only recognized option is ``name``, which points to a
2216  *     string that uniquely identifies this rule in a ruleset. The name
2217  *     correlates this rule with one of the coefficients passed into
2218  *     :func:`ruleset`. If no name is given, an identifier is assigned based on
2219  *     the index of this rule in the ruleset, but that is, of course, brittle.
2220  */
2221 function rule(lhs, rhs, options) {
2222     // Since out() is a valid call only on the RHS (unlike type()), we can take
2223     // a shortcut here: any outward RHS will already be an OutwardRhs; we don't
2224     // need to sidetrack it through being a Side. And OutwardRhs has an asRhs()
2225     // that just returns itself.
2226     if (typeof rhs === 'string') {
2227         rhs = out(rhs);
2228     }
2229     return new (rhs instanceof OutwardRhs ? OutwardRule : InwardRule)(lhs, rhs, options);
2232 let nextRuleNumber = 0;
2233 function newInternalRuleName() {
2234     return '_' + nextRuleNumber++;
2238  * We place the in/out distinction in Rules because it determines whether the
2239  * RHS result is cached, and Rules are responsible for maintaining the rulewise
2240  * cache ruleset.ruleCache.
2241  */
2242 class Rule {
2243     // abstract
2244     constructor(lhs, rhs, options) {
2245         this.lhs = lhs.asLhs();
2246         this.rhs = rhs.asRhs();
2247         // TODO: Make auto-generated rule names be based on the out types of
2248         // the rules, e.g. _priceish_4. That way, adding rules for one type
2249         // won't make the coeffs misalign for another.
2250         this.name = (options ? options.name : undefined) || newInternalRuleName();
2251     }
2253     /**
2254      * Return a NiceSet of the rules that this one shallowly depends on in the
2255      * given ruleset. In a BoundRuleset, this may include rules that have
2256      * already been executed.
2257      *
2258      * Depend on emitters of any LHS type this rule finalizes. (See
2259      * _typesFinalized for a definition.) Depend on adders of any other LHS
2260      * types (because, after all, we need to know what nodes have that type in
2261      * order to find the set of LHS nodes). This works for simple rules and
2262      * complex ones like and().
2263      *
2264      * Specific examples (where A is a type):
2265      * * A.max->* depends on anything emitting A.
2266      * * Even A.max->A depends on A emitters, because we have to have all the
2267      *   scores factored in first. For example, what if we did
2268      *   max(A)->score(.5)?
2269      * * A->A depends on anything adding A.
2270      * * A->(something other than A) depends on anything emitting A. (For
2271      *   example, we need the A score finalized before we could transfer it to
2272      *   B using conserveScore().)
2273      * * A->out() also depends on anything emitting A. Fnode methods aren't
2274      *   smart enough to lazily run emitter rules as needed. We could make them
2275      *   so if it was shown to be an advantage.
2276      */
2277     prerequisites(ruleset) {
2278         // Optimization: we could cache the result of this when in a compiled (immutable) ruleset.
2280         // Extend prereqs with rules derived from each of the given types. If
2281         // no rules are found, raise an exception, as that indicates a
2282         // malformed ruleset.
2283         function extendOrThrow(prereqs, types, ruleGetter, verb) {
2284             for (let type of types) {
2285                 const rules = ruleGetter(type);
2286                 if (rules.length > 0) {
2287                     prereqs.extend(rules);
2288                 } else {
2289                     throw new Error(`No rule ${verb} the "${type}" type, but another rule needs it as input.`);
2290                 }
2291             }
2292         }
2294         const prereqs = new NiceSet();
2296         // Add finalized types:
2297         extendOrThrow(prereqs, this._typesFinalized(), (type) => ruleset.inwardRulesThatCouldEmit(type), 'emits');
2299         // Add mentioned types:
2300         // We could say this.lhs.typesMentioned().minus(typesFinalized) as an
2301         // optimization. But since types mentioned are a superset of types
2302         // finalized and rules adding are a subset of rules emitting, we get
2303         // the same result without.
2304         extendOrThrow(prereqs, this.lhs.typesMentioned(), (type) => ruleset.inwardRulesThatCouldAdd(type), 'adds');
2306         return prereqs;
2307     }
2309     /**
2310      * Return the types that this rule finalizes.
2311      *
2312      * To "finalize" a type means to make sure we're finished running all
2313      * possible rules that might change a node's score or notes w.r.t. a given
2314      * type. This is generally done because we're about to use those data for
2315      * something, like computing a new type's score or or an aggregate
2316      * function. Exhaustively, we're about to...
2317      * * change the type of the nodes or
2318      * * aggregate all nodes of a type
2319      *
2320      * This base-class implementation just returns what aggregate functions
2321      * need, since that need spans inward and outward rules.
2322      *
2323      * @return Set of types
2324      */
2325     _typesFinalized() {
2326         // Get the types that are fed to aggregate functions. Aggregate
2327         // functions are more demanding than a simple type() LHS. A type() LHS
2328         // itself does not finalize its nodes because the things it could do to
2329         // them without changing their type (adding notes, adding to score)
2330         // are immutable or commutative (respectively). Thus, we require a RHS
2331         // type change in order to require finalization of a simple type()
2332         // mention. A max(B), OTOH, is not commutative with other B->B rules
2333         // (imagine type(B).max()->score(.5)), so it must depend on B emitters
2334         // and thus finalize B. (This will have to be relaxed or rethought when
2335         // we do the max()/atMost() optimization. Perhaps we can delegate to
2336         // aggregate functions up in Rule.prerequisites() to ask what their
2337         // prereqs are. If they implement such an optimization, they can reply.
2338         // Otherwise, we can assume they are all the nodes of their type.)
2339         //
2340         // TODO: Could arbitrary predicates (once we implement those) matter
2341         // too? Maybe it's not just aggregations.
2342         const type = this.lhs.aggregatedType();
2343         return type === undefined ? new NiceSet() : new NiceSet([type]);
2344     }
2348  * A normal rule, whose results head back into the Fathom knowledgebase, to be
2349  * operated on by further rules.
2350  */
2351 class InwardRule extends Rule {
2352     // TODO: On construct, complain about useless rules, like a dom() rule that
2353     // doesn't assign a type.
2355     /**
2356      * Return an iterable of the fnodes emitted by the RHS of this rule.
2357      * Side effect: update ruleset's store of fnodes, its accounting of which
2358      * rules are done executing, and its cache of results per type.
2359      */
2360     results(ruleset) {
2361         if (ruleset.doneRules.has(this)) {
2362             // shouldn't happen
2363             throw new Error(
2364                 'A bug in Fathom caused results() to be called on an inward rule twice. That could cause redundant score contributions, etc.'
2365             );
2366         }
2367         const self = this;
2368         // For now, we consider most of what a LHS computes to be cheap, aside
2369         // from type() and type().max(), which are cached by their specialized
2370         // LHS subclasses.
2371         const leftResults = this.lhs.fnodes(ruleset);
2372         // Avoid returning a single fnode more than once. LHSs uniquify
2373         // themselves, but the RHS can change the element it's talking
2374         // about and thus end up with dupes.
2375         const returnedFnodes = new Set();
2377         // Merge facts into fnodes:
2378         forEach(
2379             // leftResult can be either a fnode or a {fnode, rhsTransformer} pair.
2380             function updateFnode(leftResult) {
2381                 const leftType = self.lhs.guaranteedType();
2382                 // Get a fnode and a RHS transformer, whether a plain fnode is
2383                 // returned or a {fnode, rhsTransformer} pair:
2384                 const { fnode: leftFnode = leftResult, rhsTransformer = identity } = leftResult;
2385                 // Grab the fact from the RHS, and run the LHS's optional
2386                 // transformer over it to pick up anything special it wants to
2387                 // do:
2388                 const fact = rhsTransformer(self.rhs.fact(leftFnode, leftType));
2389                 self.lhs.checkFact(fact);
2390                 const rightFnode = ruleset.fnodeForElement(fact.element || leftFnode.element);
2391                 // If the RHS doesn't specify a type, default to the
2392                 // type of the LHS, if any:
2393                 const rightType = fact.type || self.lhs.guaranteedType();
2394                 if (fact.score !== undefined) {
2395                     if (rightType !== undefined) {
2396                         rightFnode.addScoreFor(rightType, fact.score, self.name);
2397                     } else {
2398                         throw new Error(
2399                             `The right-hand side of a rule specified a score (${fact.score}) with neither an explicit type nor one we could infer from the left-hand side.`
2400                         );
2401                     }
2402                 }
2403                 if (fact.type !== undefined || fact.note !== undefined) {
2404                     // There's a reason to call setNoteFor.
2405                     if (rightType === undefined) {
2406                         throw new Error(
2407                             `The right-hand side of a rule specified a note (${fact.note}) with neither an explicit type nor one we could infer from the left-hand side. Notes are per-type, per-node, so that's a problem.`
2408                         );
2409                     } else {
2410                         rightFnode.setNoteFor(rightType, fact.note);
2411                     }
2412                 }
2413                 returnedFnodes.add(rightFnode);
2414             },
2415             leftResults
2416         );
2418         // Update ruleset lookup tables.
2419         // First, mark this rule as done:
2420         ruleset.doneRules.add(this);
2421         // Then, stick each fnode in typeCache under all applicable types.
2422         // Optimization: we really only need to loop over the types
2423         // this rule can possibly add.
2424         for (let fnode of returnedFnodes) {
2425             for (let type of fnode.typesSoFar()) {
2426                 setDefault(ruleset.typeCache, type, () => new Set()).add(fnode);
2427             }
2428         }
2429         return returnedFnodes.values();
2430     }
2432     /**
2433      * Return a Set of the types that could be emitted back into the system.
2434      * To emit a type means to either to add it to a fnode emitted from the RHS
2435      * or to leave it on such a fnode where it already exists.
2436      */
2437     typesItCouldEmit() {
2438         const rhs = this.rhs.possibleEmissions();
2439         if (!rhs.couldChangeType && this.lhs.guaranteedType() !== undefined) {
2440             // It's a b -> b rule.
2441             return new Set([this.lhs.guaranteedType()]);
2442         } else if (rhs.possibleTypes.size > 0) {
2443             // We can prove the type emission from the RHS alone.
2444             return rhs.possibleTypes;
2445         } else {
2446             throw new Error(
2447                 'Could not determine the emitted type of a rule because its right-hand side calls props() without calling typeIn().'
2448             );
2449         }
2450     }
2452     /**
2453      * Return a Set of types I could add to fnodes I output (where the fnodes
2454      * did not already have them).
2455      */
2456     typesItCouldAdd() {
2457         const ret = new Set(this.typesItCouldEmit());
2458         ret.delete(this.lhs.guaranteedType());
2459         return ret;
2460     }
2462     /**
2463      * Add the types we could change to the superclass's result.
2464      */
2465     _typesFinalized() {
2466         const self = this;
2467         function typesThatCouldChange() {
2468             const ret = new NiceSet();
2470             // Get types that could change:
2471             const emissions = self.rhs.possibleEmissions();
2472             if (emissions.couldChangeType) {
2473                 // Get the possible guaranteed combinations of types on the LHS
2474                 // (taking just this LHS into account). For each combo, if the RHS
2475                 // adds a type that's not in the combo, the types in the combo get
2476                 // unioned into ret.
2477                 for (let combo of self.lhs.possibleTypeCombinations()) {
2478                     for (let rhsType of emissions.possibleTypes) {
2479                         if (!combo.has(rhsType)) {
2480                             ret.extend(combo);
2481                             break;
2482                         }
2483                     }
2484                 }
2485             }
2486             // Optimization: the possible combos could be later expanded to be
2487             // informed by earlier rules which add the types mentioned in the LHS.
2488             // If the only way for something to get B is to have Q first, then we
2489             // can add Q to each combo and end up with fewer types finalized. Would
2490             // this imply the existence of a Q->B->Q cycle and thus be impossible?
2491             // Think about it. If we do this, we can centralize that logic here,
2492             // rather than repeating it in all the Lhs subclasses).
2493             return ret;
2494         }
2496         return typesThatCouldChange().extend(super._typesFinalized());
2497     }
2501  * A rule whose RHS is an out(). This represents a final goal of a ruleset.
2502  * Its results go out into the world, not inward back into the Fathom
2503  * knowledgebase.
2504  */
2505 class OutwardRule extends Rule {
2506     /**
2507      * Compute the whole thing, including any .through() and .allThrough().
2508      * Do not mark me done in ruleset.doneRules; out rules are never marked as
2509      * done so they can be requested many times without having to cache their
2510      * (potentially big, since they aren't necessarily fnodes?) results. (We
2511      * can add caching later if it proves beneficial.)
2512      */
2513     results(ruleset) {
2514         /**
2515          * From a LHS's ``{fnode, rhsTransform}`` object or plain fnode, pick off just
2516          * the fnode and return it.
2517          */
2518         function justFnode(fnodeOrStruct) {
2519             return fnodeOrStruct instanceof Fnode ? fnodeOrStruct : fnodeOrStruct.fnode;
2520         }
2522         return this.rhs.allCallback(map(this.rhs.callback, map(justFnode, this.lhs.fnodes(ruleset))));
2523     }
2525     /**
2526      * @return the key under which the output of this rule will be available
2527      */
2528     key() {
2529         return this.rhs.key;
2530     }
2532     /**
2533      * OutwardRules finalize all types mentioned.
2534      */
2535     _typesFinalized() {
2536         return this.lhs.typesMentioned().extend(super._typesFinalized());
2537     }
2541  * A shortcut for creating a new :class:`Ruleset`, for symmetry with
2542  * :func:`rule`
2543  */
2544 function ruleset(rules, coeffs = [], biases = []) {
2545     return new Ruleset(rules, coeffs, biases);
2549  * An unbound ruleset. When you bind it by calling :func:`~Ruleset.against()`,
2550  * the resulting :class:`BoundRuleset` will be immutable.
2551  */
2552 class Ruleset {
2553     /**
2554      * @arg rules {Array} Rules returned from :func:`rule`
2555      * @arg coeffs {Map} A map of rule names to numerical weights, typically
2556      *     returned by the :doc:`trainer<training>`. Example:
2557      *     ``[['someRuleName', 5.04], ...]``. If not given, coefficients
2558      *     default to 1.
2559      * @arg biases {object} A map of type names to neural-net biases. These
2560      *      enable accurate confidence estimates. Example: ``[['someType',
2561      *      -2.08], ...]``. If absent, biases default to 0.
2562      */
2563     constructor(rules, coeffs = [], biases = []) {
2564         this._inRules = [];
2565         this._outRules = new Map(); // key -> rule
2566         this._rulesThatCouldEmit = new Map(); // type -> [rules]
2567         this._rulesThatCouldAdd = new Map(); // type -> [rules]
2568         // Private to the framework:
2569         this._coeffs = new Map(coeffs); // rule name => coefficient
2570         this.biases = new Map(biases); // type name => bias
2572         // Separate rules into out ones and in ones, and sock them away. We do
2573         // this here so mistakes raise errors early.
2574         for (let rule of rules) {
2575             if (rule instanceof InwardRule) {
2576                 this._inRules.push(rule);
2578                 // Keep track of what inward rules can emit or add:
2579                 // TODO: Combine these hashes for space efficiency:
2580                 const emittedTypes = rule.typesItCouldEmit();
2581                 for (let type of emittedTypes) {
2582                     setDefault(this._rulesThatCouldEmit, type, () => []).push(rule);
2583                 }
2584                 for (let type of rule.typesItCouldAdd()) {
2585                     setDefault(this._rulesThatCouldAdd, type, () => []).push(rule);
2586                 }
2587             } else if (rule instanceof OutwardRule) {
2588                 this._outRules.set(rule.key(), rule);
2589             } else {
2590                 throw new Error(`This element of ruleset()'s first param wasn't a rule: ${rule}`);
2591             }
2592         }
2593     }
2595     /**
2596      * Commit this ruleset to running against a specific DOM tree or subtree.
2597      *
2598      * When run against a subtree, the root of the subtree is not considered as
2599      * a possible match.
2600      *
2601      * This doesn't actually modify the Ruleset but rather returns a fresh
2602      * :class:`BoundRuleset`, which contains caches and other stateful, per-DOM
2603      * bric-a-brac.
2604      */
2605     against(doc) {
2606         return new BoundRuleset(
2607             doc,
2608             this._inRules,
2609             this._outRules,
2610             this._rulesThatCouldEmit,
2611             this._rulesThatCouldAdd,
2612             this._coeffs,
2613             this.biases
2614         );
2615     }
2617     /**
2618      * Return all the rules (both inward and outward) that make up this ruleset.
2619      *
2620      * From this, you can construct another ruleset like this one but with your
2621      * own rules added.
2622      */
2623     rules() {
2624         return Array.from([...this._inRules, ...this._outRules.values()]);
2625     }
2629  * A ruleset that is earmarked to analyze a certain DOM
2631  * Carries a cache of rule results on that DOM. Typically comes from
2632  * :meth:`~Ruleset.against`.
2633  */
2634 class BoundRuleset {
2635     /**
2636      * @arg inRules {Array} Non-out() rules
2637      * @arg outRules {Map} Output key -> out() rule
2638      */
2639     constructor(doc, inRules, outRules, rulesThatCouldEmit, rulesThatCouldAdd, coeffs, biases) {
2640         this.doc = doc;
2641         this._inRules = inRules;
2642         this._outRules = outRules;
2643         this._rulesThatCouldEmit = rulesThatCouldEmit;
2644         this._rulesThatCouldAdd = rulesThatCouldAdd;
2645         this._coeffs = coeffs;
2647         // Private, for the use of only helper classes:
2648         this.biases = biases;
2649         this._clearCaches();
2650         this.elementCache = new WeakMap(); // DOM element => fnode about it
2651         this.doneRules = new Set(); // InwardRules that have been executed. OutwardRules can be executed more than once because they don't change any fnodes and are thus idempotent.
2652     }
2654     /**
2655      * Change my coefficients and biases after construction.
2656      *
2657      * @arg coeffs See the :class:`Ruleset` constructor.
2658      * @arg biases See the :class:`Ruleset` constructor.
2659      */
2660     setCoeffsAndBiases(coeffs, biases = []) {
2661         // Destructuring assignment doesn't make it through rollup properly
2662         // (https://github.com/rollup/rollup-plugin-commonjs/issues/358):
2663         this._coeffs = new Map(coeffs);
2664         this.biases = new Map(biases);
2665         this._clearCaches();
2666     }
2668     /**
2669      * Clear the typeCache and maxCache, usually in the wake of changing
2670      * ``this._coeffs``, because both of thise depend on weighted scores.
2671      */
2672     _clearCaches() {
2673         this.maxCache = new Map(); // type => Array of max fnode (or fnodes, if tied) of this type
2674         this.typeCache = new Map(); // type => Set of all fnodes of this type found so far. (The dependency resolution during execution ensures that individual types will be comprehensive just in time.)
2675     }
2677     /**
2678      * Return an array of zero or more fnodes.
2679      * @arg thing {string|Lhs|Node} Can be
2680      *
2681      *       (1) A string which matches up with an "out" rule in the ruleset.
2682      *           If the out rule uses through(), the results of through's
2683      *           callback (which might not be fnodes) will be returned.
2684      *       (2) An arbitrary LHS which we calculate and return the results of.
2685      *       (3) A DOM node, for which we will return the corresponding fnode.
2686      *
2687      *     Results are cached for cases (1) and (3).
2688      */
2689     get(thing) {
2690         if (typeof thing === 'string') {
2691             if (this._outRules.has(thing)) {
2692                 return Array.from(this._execute(this._outRules.get(thing)));
2693             } else {
2694                 throw new Error(`There is no out() rule with key "${thing}".`);
2695             }
2696         } else if (isDomElement(thing)) {
2697             // Return the fnode and let it run type(foo) on demand, as people
2698             // ask it things like scoreFor(foo).
2699             return this.fnodeForElement(thing);
2700         } else if (thing.asLhs !== undefined) {
2701             // Make a temporary out rule, and run it. This may add things to
2702             // the ruleset's cache, but that's fine: it doesn't change any
2703             // future results; it just might make them faster. For example, if
2704             // you ask for .get(type('smoo')) twice, the second time will be a
2705             // cache hit.
2706             const outRule = rule(thing, out(Symbol('outKey')));
2707             return Array.from(this._execute(outRule));
2708         } else {
2709             throw new Error(
2710                 'ruleset.get() expects a string, an expression like on the left-hand side of a rule, or a DOM node.'
2711             );
2712         }
2713     }
2715     /**
2716      * Return the weighted sum of the per-rule, per-type scores from a fnode.
2717      *
2718      * @arg mapOfScores a Map of rule name to the [0, 1] score it computed for
2719      *      the type in question
2720      */
2721     weightedScore(mapOfScores) {
2722         let total = 0;
2723         for (const [name, score] of mapOfScores) {
2724             total += score * getDefault(this._coeffs, name, () => 1);
2725         }
2726         return total;
2727     }
2729     // Provide an opaque context object to be made available to all ranker
2730     // functions.
2731     // context (object) {
2732     //     self.context = object;
2733     // }
2735     // -------- Methods below this point are private to the framework. --------
2737     /**
2738      * Return all the thus-far-unexecuted rules that will have to run to run
2739      * the requested rule, in the form of Map(prereq: [rulesItIsNeededBy]).
2740      */
2741     _prerequisitesTo(rule, undonePrereqs = new Map()) {
2742         for (let prereq of rule.prerequisites(this)) {
2743             if (!this.doneRules.has(prereq)) {
2744                 // prereq is not already run. (If it were, we wouldn't care
2745                 // about adding it to the graph.)
2746                 const alreadyAdded = undonePrereqs.has(prereq);
2747                 setDefault(undonePrereqs, prereq, () => []).push(rule);
2749                 // alreadyAdded means we've already computed the prereqs of
2750                 // this prereq and added them to undonePrereqs. So, now
2751                 // that we've hooked up the rule to this prereq in the
2752                 // graph, we can stop. But, if we haven't, then...
2753                 if (!alreadyAdded) {
2754                     this._prerequisitesTo(prereq, undonePrereqs);
2755                 }
2756             }
2757         }
2758         return undonePrereqs;
2759     }
2761     /**
2762      * Run the given rule (and its dependencies, in the proper order), and
2763      * return its results.
2764      *
2765      * The caller is responsible for ensuring that _execute() is not called
2766      * more than once for a given InwardRule, lest non-idempotent
2767      * transformations, like score contributions, be applied to fnodes more
2768      * than once.
2769      *
2770      * The basic idea is to sort rules in topological order (according to input
2771      * and output types) and then run them. On top of that, we do some
2772      * optimizations. We keep a cache of results by type (whether partial or
2773      * comprehensive--either way, the topology ensures that any
2774      * non-comprehensive typeCache entry is made comprehensive before another
2775      * rule needs it). And we prune our search for prerequisite rules at the
2776      * first encountered already-executed rule.
2777      */
2778     _execute(rule) {
2779         const prereqs = this._prerequisitesTo(rule);
2780         let sorted;
2781         try {
2782             sorted = [rule].concat(toposort(prereqs.keys(), (prereq) => prereqs.get(prereq)));
2783         } catch (exc) {
2784             if (exc instanceof CycleError) {
2785                 throw new CycleError('There is a cyclic dependency in the ruleset.');
2786             } else {
2787                 throw exc;
2788             }
2789         }
2790         let fnodes;
2791         for (let eachRule of reversed(sorted)) {
2792             // Sock each set of results away in this.typeCache:
2793             fnodes = eachRule.results(this);
2794         }
2795         return Array.from(fnodes);
2796     }
2798     /** @return {Rule[]} */
2799     inwardRulesThatCouldEmit(type) {
2800         return getDefault(this._rulesThatCouldEmit, type, () => []);
2801     }
2803     /** @return {Rule[]} */
2804     inwardRulesThatCouldAdd(type) {
2805         return getDefault(this._rulesThatCouldAdd, type, () => []);
2806     }
2808     /**
2809      * @return the Fathom node that describes the given DOM element. This does
2810      *     not trigger any execution, so the result may be incomplete.
2811      */
2812     fnodeForElement(element) {
2813         return setDefault(this.elementCache, element, () => new Fnode(element, this));
2814     }
2817 /* This Source Code Form is subject to the terms of the Mozilla Public
2818  * License, v. 2.0. If a copy of the MPL was not distributed with this
2819  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
2821 const version = '3.7.3';
2823 export {
2824     Fnode,
2825     and,
2826     atMost,
2827     clusters$1 as clusters,
2828     dom,
2829     domQuery,
2830     element,
2831     exceptions,
2832     nearest,
2833     note,
2834     out,
2835     props,
2836     rule,
2837     ruleset,
2838     score,
2839     type,
2840     typeIn,
2841     utilsForFrontend as utils,
2842     version,