From b89fb67bd9f03f8e2ed69db67dfdf9c349c7f098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 14 May 2021 15:15:42 +0100 Subject: [PATCH 1/3] Move initialization logic to `load` --- src/cheerio.ts | 108 ++++++++------------------------------------- src/load.ts | 116 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 121 insertions(+), 103 deletions(-) diff --git a/src/cheerio.ts b/src/cheerio.ts index c2ec78eb58..ec3e1e912b 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -1,6 +1,4 @@ -import parse from './parse'; -import { InternalOptions, default as defaultOptions } from './options'; -import { isHtml, isCheerio } from './utils'; +import { InternalOptions } from './options'; import type { Node, Document } from 'domhandler'; import { BasicAcceptedElems } from './types'; @@ -16,7 +14,7 @@ type ManipulationType = typeof Manipulation; type CssType = typeof Css; type FormsType = typeof Forms; -export class Cheerio implements ArrayLike { +export abstract class Cheerio implements ArrayLike { length = 0; [index: number]: T; @@ -26,95 +24,39 @@ export class Cheerio implements ArrayLike { * * @private */ - _root: Cheerio | undefined; - /** @function */ - find!: typeof Traversing.find; + _root: Cheerio | null; /** * Instance of cheerio. Methods are specified in the modules. Usage of this * constructor is not recommended. Please use $.load instead. * * @private - * @param selector - The new selection. - * @param context - Context of the selection. + * @param elements - The new selection. * @param root - Sets the root node. * @param options - Options for the instance. */ constructor( - selector?: T extends Node ? BasicAcceptedElems : Cheerio | T[], - context?: BasicAcceptedElems | null, - root?: BasicAcceptedElems | null, - options: InternalOptions = defaultOptions + elements: ArrayLike | undefined, + root: Cheerio | null, + options: InternalOptions ) { this.options = options; - - // $(), $(null), $(undefined), $(false) - if (!selector) return this; - - if (root) { - if (typeof root === 'string') root = parse(root, this.options, false); - this._root = new (this.constructor as typeof Cheerio)( - root, - null, - null, - this.options - ); - // Add a cyclic reference, so that calling methods on `_root` never fails. - this._root._root = this._root; - } - - // $($) - if (isCheerio(selector)) return selector; - - const elements = - typeof selector === 'string' && isHtml(selector) - ? // $() - parse(selector, this.options, false).children - : isNode(selector) - ? // $(dom) - [selector] - : Array.isArray(selector) - ? // $([dom]) - selector - : null; + this._root = root; if (elements) { - elements.forEach((elem, idx) => { - this[idx] = elem; - }); + for (let idx = 0; idx < elements.length; idx++) { + this[idx] = elements[idx]; + } this.length = elements.length; - return this; } - - // We know that our selector is a string now. - let search = selector as string; - - const searchContext: Cheerio | undefined = !context - ? // If we don't have a context, maybe we have a root, from loading - this._root - : typeof context === 'string' - ? isHtml(context) - ? // $('li', '
    ...
') - this._make(parse(context, this.options, false)) - : // $('li', 'ul') - ((search = `${context} ${search}`), this._root) - : isCheerio(context) - ? // $('li', $) - context - : // $('li', node), $('li', [nodes]) - this._make(context); - - // If we still don't have a context, return - if (!searchContext) return this; - - /* - * #id, .class, tag - */ - // @ts-expect-error No good way to type this — we will always return `Cheerio` here. - return searchContext.find(search); } - prevObject: Cheerio | undefined; + abstract _new( + selector?: Cheerio | T[] | (T extends Node ? string : never), + context?: BasicAcceptedElems | null + ): Cheerio; + + prevObject: Cheerio | undefined; /** * Make a cheerio object. * @@ -127,12 +69,7 @@ export class Cheerio implements ArrayLike { dom: Cheerio | T[] | T | string, context?: BasicAcceptedElems ): Cheerio { - const cheerio = new (this.constructor as any)( - dom, - context, - this._root, - this.options - ); + const cheerio = (this as any)._new(dom, context); cheerio.prevObject = this; return cheerio; @@ -171,12 +108,3 @@ Object.assign( Css, Forms ); - -function isNode(obj: any): obj is Node { - return ( - !!obj.name || - obj.type === 'root' || - obj.type === 'text' || - obj.type === 'comment' - ); -} diff --git a/src/load.ts b/src/load.ts index a5dd026e79..e8e73b2d51 100644 --- a/src/load.ts +++ b/src/load.ts @@ -6,6 +6,7 @@ import { } from './options'; import * as staticMethods from './static'; import { Cheerio } from './cheerio'; +import { isHtml, isCheerio } from './utils'; import parse from './parse'; import type { Node, Document, Element } from 'domhandler'; import type * as Load from './load'; @@ -94,30 +95,110 @@ export function load( } const internalOpts = { ...defaultOptions, ...flattenOptions(options) }; - const root = parse(content, internalOpts, isDocument); + const initialRoot = parse(content, internalOpts, isDocument); /** Create an extended class here, so that extensions only live on one instance. */ - class LoadedCheerio extends Cheerio {} - - function initialize( - selector?: T extends Node - ? string | Cheerio | T[] | T - : Cheerio | T[], - context?: string | Cheerio | Node[] | Node, - r: string | Cheerio | Document | null = root, + class LoadedCheerio extends Cheerio { + _new( + selector?: T[] | Cheerio | (T extends Node ? string : never), + context?: BasicAcceptedElems | null + ): Cheerio { + return (initialize as any)(selector, context); + } + } + + function initialize( + selector?: S | BasicAcceptedElems, + context?: BasicAcceptedElems | null, + root: BasicAcceptedElems = initialRoot, opts?: CheerioOptions - ) { - return new LoadedCheerio(selector, context, r, { + ): Cheerio { + type Result = S extends SelectorType ? Element : T; + + // $($) + if (selector && isCheerio(selector)) return selector; + + const options = { ...internalOpts, ...flattenOptions(opts), - }); + }; + const r = + typeof root === 'string' + ? [parse(root, options, false)] + : 'length' in root + ? root + : [root]; + const rootInstance = isCheerio(r) + ? r + : new LoadedCheerio(r, null, options); + // Add a cyclic reference, so that calling methods on `_root` never fails. + rootInstance._root = rootInstance; + + // $(), $(null), $(undefined), $(false) + if (!selector) { + return new LoadedCheerio(undefined, rootInstance, options); + } + + const elements = + typeof selector === 'string' && isHtml(selector) + ? // $() + parse(selector, options, false).children + : isNode(selector) + ? // $(dom) + [selector] + : Array.isArray(selector) + ? // $([dom]) + selector + : undefined; + + const instance = new LoadedCheerio(elements, rootInstance, options); + + if (elements || !selector) { + return instance as any; + } + + if (typeof selector !== 'string') throw new Error(''); + + // We know that our selector is a string now. + let search = selector; + + const searchContext: Cheerio | undefined = !context + ? // If we don't have a context, maybe we have a root, from loading + rootInstance + : typeof context === 'string' + ? isHtml(context) + ? // $('li', '
    ...
') + new LoadedCheerio( + [parse(context, options, false)], + rootInstance, + options + ) + : // $('li', 'ul') + ((search = `${context} ${search}`), rootInstance) + : isCheerio(context) + ? // $('li', $) + context + : // $('li', node), $('li', [nodes]) + new LoadedCheerio( + Array.isArray(context) ? context : [context], + rootInstance, + options + ); + + // If we still don't have a context, return + if (!searchContext) return instance as any; + + /* + * #id, .class, tag + */ + return searchContext.find(search) as Cheerio; } // Add in static methods & properties Object.assign(initialize, staticMethods, { load, // `_root` and `_options` are used in static methods. - _root: root, + _root: initialRoot, _options: internalOpts, // Add `fn` for plugins fn: LoadedCheerio.prototype, @@ -127,3 +208,12 @@ export function load( return initialize as CheerioAPI; } + +function isNode(obj: any): obj is Node { + return ( + !!obj.name || + obj.type === 'root' || + obj.type === 'text' || + obj.type === 'comment' + ); +} From d71c4fd5b6c559ba5233af2f7c59779245c80194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 21 May 2021 16:24:52 +0100 Subject: [PATCH 2/3] Allow passing anything array-like in more places --- src/cheerio.ts | 6 +++--- src/load.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cheerio.ts b/src/cheerio.ts index ec3e1e912b..7e97e255ea 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -52,7 +52,7 @@ export abstract class Cheerio implements ArrayLike { } abstract _new( - selector?: Cheerio | T[] | (T extends Node ? string : never), + selector?: ArrayLike | T | string, context?: BasicAcceptedElems | null ): Cheerio; @@ -66,10 +66,10 @@ export abstract class Cheerio implements ArrayLike { * @returns The new cheerio object. */ _make( - dom: Cheerio | T[] | T | string, + dom: ArrayLike | T | string, context?: BasicAcceptedElems ): Cheerio { - const cheerio = (this as any)._new(dom, context); + const cheerio = this._new(dom, context); cheerio.prevObject = this; return cheerio; diff --git a/src/load.ts b/src/load.ts index e8e73b2d51..2acf3e413c 100644 --- a/src/load.ts +++ b/src/load.ts @@ -100,15 +100,15 @@ export function load( /** Create an extended class here, so that extensions only live on one instance. */ class LoadedCheerio extends Cheerio { _new( - selector?: T[] | Cheerio | (T extends Node ? string : never), + selector?: ArrayLike | T | string, context?: BasicAcceptedElems | null ): Cheerio { - return (initialize as any)(selector, context); + return initialize(selector, context); } } - function initialize( - selector?: S | BasicAcceptedElems, + function initialize( + selector?: ArrayLike | T | S, context?: BasicAcceptedElems | null, root: BasicAcceptedElems = initialRoot, opts?: CheerioOptions @@ -139,7 +139,7 @@ export function load( return new LoadedCheerio(undefined, rootInstance, options); } - const elements = + const elements: Node[] | undefined = typeof selector === 'string' && isHtml(selector) ? // $() parse(selector, options, false).children @@ -151,7 +151,7 @@ export function load( selector : undefined; - const instance = new LoadedCheerio(elements, rootInstance, options); + const instance = new LoadedCheerio(elements, rootInstance, options); if (elements || !selector) { return instance as any; @@ -174,7 +174,7 @@ export function load( options ) : // $('li', 'ul') - ((search = `${context} ${search}`), rootInstance) + ((search = `${context} ${search}` as S), rootInstance) : isCheerio(context) ? // $('li', $) context From 3dae3b5689d7224f612404d8a433c33e0c4db469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 21 Jun 2021 23:01:29 +0100 Subject: [PATCH 3/3] Make `_make` the abstract method --- src/cheerio.ts | 14 ++------------ src/load.ts | 7 +++++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/cheerio.ts b/src/cheerio.ts index 7e97e255ea..96e58fd02c 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -51,11 +51,6 @@ export abstract class Cheerio implements ArrayLike { } } - abstract _new( - selector?: ArrayLike | T | string, - context?: BasicAcceptedElems | null - ): Cheerio; - prevObject: Cheerio | undefined; /** * Make a cheerio object. @@ -65,15 +60,10 @@ export abstract class Cheerio implements ArrayLike { * @param context - The context of the new object. * @returns The new cheerio object. */ - _make( + abstract _make( dom: ArrayLike | T | string, context?: BasicAcceptedElems - ): Cheerio { - const cheerio = this._new(dom, context); - cheerio.prevObject = this; - - return cheerio; - } + ): Cheerio; } export interface Cheerio diff --git a/src/load.ts b/src/load.ts index 2acf3e413c..481e458651 100644 --- a/src/load.ts +++ b/src/load.ts @@ -99,11 +99,14 @@ export function load( /** Create an extended class here, so that extensions only live on one instance. */ class LoadedCheerio extends Cheerio { - _new( + _make( selector?: ArrayLike | T | string, context?: BasicAcceptedElems | null ): Cheerio { - return initialize(selector, context); + const cheerio = initialize(selector, context); + cheerio.prevObject = this; + + return cheerio; } }