From a793046ab12afe9cafe7dfbd7719f292572dd127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 7 May 2021 16:58:01 +0100 Subject: [PATCH 01/11] fix: Make `_root` a full Cheerio instance --- src/cheerio.ts | 4 ++-- src/load.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cheerio.ts b/src/cheerio.ts index 0c3452c1c8..335c638fa2 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -87,7 +87,7 @@ export class Cheerio implements ArrayLike { constructor( selector?: T extends Node ? BasicAcceptedElems : Cheerio | T[], context?: BasicAcceptedElems | null, - root?: BasicAcceptedElems, + root?: BasicAcceptedElems | null, options?: CheerioOptions ) { if (!(this instanceof Cheerio)) { @@ -107,7 +107,7 @@ export class Cheerio implements ArrayLike { if (root) { if (typeof root === 'string') root = parse(root, this.options, false); - this._root = (Cheerio as any).call(this, root); + this._root = new (this.constructor as typeof Cheerio)(root, null, null); } // $($) diff --git a/src/load.ts b/src/load.ts index 3b799a3f7f..2d801811fd 100644 --- a/src/load.ts +++ b/src/load.ts @@ -45,7 +45,7 @@ export function load( ? string | Cheerio | T[] | T : Cheerio | T[], context?: string | Cheerio | Node[] | Node, - r: string | Cheerio | Document = root, + r: string | Cheerio | Document | null = root, opts?: CheerioOptions ) { // @ts-expect-error Using `this` before calling the constructor. From f58c14d773476f2d1c5afd681c16a0672a03ec24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 7 May 2021 16:58:26 +0100 Subject: [PATCH 02/11] Move `toArray` to traversing No need to have this in the main class --- src/api/traversing.ts | 18 +++++++++++++++++- src/cheerio.ts | 16 ---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/api/traversing.ts b/src/api/traversing.ts index 0848b2fc36..397e2c73df 100644 --- a/src/api/traversing.ts +++ b/src/api/traversing.ts @@ -941,11 +941,27 @@ export function get(this: Cheerio, i: number): T; export function get(this: Cheerio): T[]; export function get(this: Cheerio, i?: number): T | T[] { if (i == null) { - return Array.prototype.slice.call(this); + return this.toArray(); } return this[i < 0 ? this.length + i : i]; } +/** + * Retrieve all the DOM elements contained in the jQuery set as an array. + * + * @example + * + * ```js + * $('li').toArray(); + * //=> [ {...}, {...}, {...} ] + * ``` + * + * @returns The contained items. + */ +export function toArray(this: Cheerio): T[] { + return Array.prototype.slice.call(this); +} + /** * Search for a given element from among the matched elements. * diff --git a/src/cheerio.ts b/src/cheerio.ts index 335c638fa2..8d966a73c7 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -185,22 +185,6 @@ export class Cheerio implements ArrayLike { return cheerio; } - - /** - * Retrieve all the DOM elements contained in the jQuery set as an array. - * - * @example - * - * ```js - * $('li').toArray(); - * //=> [ {...}, {...}, {...} ] - * ``` - * - * @returns The contained items. - */ - toArray(): T[] { - return this.get(); - } } export interface Cheerio From e590c546adf727a5229c16453811455ba5aafcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 7 May 2021 16:59:25 +0100 Subject: [PATCH 03/11] Stop CheerioAPI from extending Cheerio statics --- src/cheerio.ts | 25 ++++++++++++++++++++----- src/static.ts | 23 ++++++++++------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/cheerio.ts b/src/cheerio.ts index 8d966a73c7..47d1f47341 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -8,7 +8,7 @@ import { import { isHtml, isCheerio } from './utils'; import type { Node, Document, Element } from 'domhandler'; import * as Static from './static'; -import type { load } from './load'; +import type * as Load from './load'; import { SelectorType, BasicAcceptedElems } from './types'; import * as Attributes from './api/attributes'; @@ -23,6 +23,9 @@ type ManipulationType = typeof Manipulation; type CssType = typeof Css; type FormsType = typeof Forms; +type StaticType = typeof Static; +type LoadType = typeof Load; + /* * The API */ @@ -69,7 +72,7 @@ export class Cheerio implements ArrayLike { public static root = Static.root; public static contains = Static.contains; public static merge = Static.merge; - public static load: typeof load; + public static load: typeof Load.load; /** Mimic jQuery's prototype alias for plugin authors. */ public static fn = Cheerio.prototype; @@ -222,19 +225,31 @@ function isNode(obj: any): obj is Node { ); } -type CheerioClassType = typeof Cheerio; - /** * Wrapper around the `Cheerio` class, making it possible to create a new * instance without using `new`. */ -export interface CheerioAPI extends CheerioClassType { +export interface CheerioAPI extends StaticType, LoadType { ( selector?: S | BasicAcceptedElems, context?: BasicAcceptedElems | null, root?: BasicAcceptedElems, options?: CheerioOptions ): Cheerio; + + /** + * The root the document was originally loaded with. Set in `.load`. + * + * @private + */ + _root: Document | undefined; + + /** + * The options the document was originally loaded with. Set in `.load`. + * + * @private + */ + _options: InternalOptions | undefined; } // Make it possible to call Cheerio without using `new`. diff --git a/src/static.ts b/src/static.ts index d43497c2c7..416c81a067 100644 --- a/src/static.ts +++ b/src/static.ts @@ -20,7 +20,7 @@ import { render as renderWithHtmlparser2 } from './parsers/htmlparser2'; * @returns The rendered document. */ function render( - that: typeof Cheerio | undefined, + that: CheerioAPI | undefined, dom: ArrayLike | Node | string | undefined, options: InternalOptions ): string { @@ -63,10 +63,7 @@ function isOptions( * @param options - Options for the renderer. * @returns The rendered document. */ -export function html( - this: typeof Cheerio | void, - options?: CheerioOptions -): string; +export function html(this: CheerioAPI | void, options?: CheerioOptions): string; /** * Renders the document. * @@ -75,12 +72,12 @@ export function html( * @returns The rendered document. */ export function html( - this: typeof Cheerio | void, + this: CheerioAPI | void, dom?: string | ArrayLike | Node, options?: CheerioOptions ): string; export function html( - this: typeof Cheerio | void, + this: CheerioAPI | void, dom?: string | ArrayLike | Node | CheerioOptions, options?: CheerioOptions ): string { @@ -119,7 +116,7 @@ export function html( * @returns THe rendered document. */ export function xml( - this: typeof Cheerio, + this: CheerioAPI, dom?: string | ArrayLike | Node ): string { const options = { ...this._options, xmlMode: true }; @@ -134,7 +131,7 @@ export function xml( * @returns The rendered document. */ export function text( - this: typeof Cheerio | void, + this: CheerioAPI | void, elements?: ArrayLike ): string { const elems = elements ? elements : this ? this.root() : []; @@ -170,14 +167,14 @@ export function text( * @see {@link https://api.jquery.com/jQuery.parseHTML/} */ export function parseHTML( - this: typeof Cheerio, + this: CheerioAPI, data: string, context?: unknown | boolean, keepScripts?: boolean ): Node[]; -export function parseHTML(this: typeof Cheerio, data?: '' | null): null; +export function parseHTML(this: CheerioAPI, data?: '' | null): null; export function parseHTML( - this: typeof Cheerio, + this: CheerioAPI, data?: string | null, context?: unknown | boolean, keepScripts = typeof context === 'boolean' ? context : false @@ -219,7 +216,7 @@ export function parseHTML( * @returns Cheerio instance wrapping the root node. * @alias Cheerio.root */ -export function root(this: typeof Cheerio): Cheerio { +export function root(this: CheerioAPI): Cheerio { const fn = (this as unknown) as CheerioAPI; return fn(this._root); } From 882f11ef0a2c1677e899a510eff62d588875b974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 7 May 2021 17:11:07 +0100 Subject: [PATCH 04/11] Remove _originalRoot We are calling the constructor, which will always retain the original root. --- src/api/manipulation.ts | 12 ++++-------- src/cheerio.ts | 16 ++++------------ src/load.ts | 6 ------ 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/api/manipulation.ts b/src/api/manipulation.ts index 5fd444da58..ede2fd208d 100644 --- a/src/api/manipulation.ts +++ b/src/api/manipulation.ts @@ -168,9 +168,7 @@ export function appendTo( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const appendTarget = isCheerio(target) - ? target - : this._make(target, null, this._originalRoot); + const appendTarget = isCheerio(target) ? target : this._make(target, null); appendTarget.append(this); @@ -202,9 +200,7 @@ export function prependTo( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const prependTarget = isCheerio(target) - ? target - : this._make(target, null, this._originalRoot); + const prependTarget = isCheerio(target) ? target : this._make(target, null); prependTarget.prepend(this); @@ -642,7 +638,7 @@ export function insertAfter( target: BasicAcceptedElems ): Cheerio { if (typeof target === 'string') { - target = this._make(target, null, this._originalRoot); + target = this._make(target, null); } this.remove(); @@ -755,7 +751,7 @@ export function insertBefore( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const targetArr = this._make(target, null, this._originalRoot); + const targetArr = this._make(target, null); this.remove(); diff --git a/src/cheerio.ts b/src/cheerio.ts index 47d1f47341..fcfdf9b2d1 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -45,13 +45,6 @@ export class Cheerio implements ArrayLike { _root: Cheerio | undefined; /** @function */ find!: typeof Traversing.find; - /** - * The root the document was originally loaded with. Same as the static - * `_root` property. - * - * @private - */ - _originalRoot: Document | undefined; /** * The root the document was originally loaded with. Set in `.load`. @@ -145,14 +138,14 @@ export class Cheerio implements ArrayLike { : typeof context === 'string' ? isHtml(context) ? // $('li', '
    ...
') - new Cheerio(parse(context, this.options, false)) + this._make(parse(context, this.options, false)) : // $('li', 'ul') ((search = `${context} ${search}`), this._root) : isCheerio(context) ? // $('li', $) context : // $('li', node), $('li', [nodes]) - new Cheerio(context); + this._make(context); // If we still don't have a context, return if (!searchContext) return this; @@ -175,13 +168,12 @@ export class Cheerio implements ArrayLike { */ _make( dom: Cheerio | T[] | T | string, - context?: BasicAcceptedElems | null, - root: BasicAcceptedElems | undefined = this._root + context?: BasicAcceptedElems | null ): Cheerio { const cheerio = new (this.constructor as any)( dom, context, - root, + undefined, this.options ); cheerio.prevObject = this; diff --git a/src/load.ts b/src/load.ts index 2d801811fd..6cc37ae9aa 100644 --- a/src/load.ts +++ b/src/load.ts @@ -56,12 +56,6 @@ export function load( } } - /* - * Keep a reference to the top-level scope so we can chain methods that implicitly - * resolve selectors; e.g. $("").(".bar"), which otherwise loses ._root - */ - initialize.prototype._originalRoot = root; - // Add in the static methods Object.assign(initialize, staticMethods, { load }); From 36a265cb42b780c5e37eecc6e68aac59f2a40a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 7 May 2021 19:09:56 +0100 Subject: [PATCH 05/11] Stop exporting Cheerio base class We maintain compatibility for now by exporting a sensible default. --- src/__tests__/deprecated.spec.ts | 5 +++-- src/api/traversing.spec.ts | 4 ++-- src/cheerio.spec.ts | 5 ----- src/cheerio.ts | 9 +-------- src/index.ts | 16 +++++++++------- 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/__tests__/deprecated.spec.ts b/src/__tests__/deprecated.spec.ts index 0b290ee657..bdf47944de 100644 --- a/src/__tests__/deprecated.spec.ts +++ b/src/__tests__/deprecated.spec.ts @@ -58,7 +58,7 @@ describe('deprecated APIs', () => { // #1674 - merge, wont accept Cheerio object it('should be a able merge array and cheerio object', () => { - const ret = cheerio.merge(new cheerio(), ['elem1', 'elem2'] as any); + const ret = cheerio.merge(cheerio(), ['elem1', 'elem2'] as any); expect(typeof ret).toBe('object'); expect(ret).toHaveLength(2); }); @@ -202,7 +202,8 @@ describe('deprecated APIs', () => { describe('.root', () => { it('returns an empty selection', () => { const $empty = cheerio.root(); - expect($empty).toHaveLength(0); + expect($empty).toHaveLength(1); + expect($empty[0].children).toHaveLength(0); }); }); }); diff --git a/src/api/traversing.spec.ts b/src/api/traversing.spec.ts index 3720d829da..4d52c77c1e 100644 --- a/src/api/traversing.spec.ts +++ b/src/api/traversing.spec.ts @@ -1,5 +1,5 @@ import cheerio from '../../src'; -import type { CheerioAPI, Cheerio } from '../cheerio'; +import { CheerioAPI, Cheerio } from '../cheerio'; import { Node, Element, isText } from 'domhandler'; import { food, @@ -689,7 +689,7 @@ describe('$(...)', () => { it('() : should return an empty array', () => { const result = $('.orange').closest(); expect(result).toHaveLength(0); - expect(result).toBeInstanceOf(cheerio); + expect(result).toBeInstanceOf(Cheerio); }); it('(selector) : should find the closest element that matches the selector, searching through its ancestors and itself', () => { diff --git a/src/cheerio.spec.ts b/src/cheerio.spec.ts index 1beed70079..28bc07d60a 100644 --- a/src/cheerio.spec.ts +++ b/src/cheerio.spec.ts @@ -215,11 +215,6 @@ describe('cheerio', () => { expect($elem.eq(1).attr('class')).toBe('orange'); }); - it('should gracefully degrade on complex, unmatched queries', () => { - const $elem = cheerio('Eastern States Cup #8-fin <1br>Downhill '); - expect($elem).toHaveLength(0); - }); - it('(extended Array) should not interfere with prototype methods (issue #119)', () => { const extended: any = []; extended.find = extended.children = extended.each = function () { diff --git a/src/cheerio.ts b/src/cheerio.ts index fcfdf9b2d1..e79d02cb77 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -32,7 +32,7 @@ type LoadType = typeof Load; const api = [Attributes, Traversing, Manipulation, Css, Forms]; export class Cheerio implements ArrayLike { - length!: number; + length = 0; [index: number]: T; options!: InternalOptions; @@ -86,15 +86,8 @@ export class Cheerio implements ArrayLike { root?: BasicAcceptedElems | null, options?: CheerioOptions ) { - if (!(this instanceof Cheerio)) { - return new Cheerio(selector, context, root, options); - } - - this.length = 0; - this.options = { ...defaultOptions, - ...this.options, ...flattenOptions(options), }; diff --git a/src/index.ts b/src/index.ts index 236c3ab171..c80e997d16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,5 @@ import Cheerio from './cheerio'; -/** - * The default cheerio instance. - * - * @deprecated Use the function returned by `load` instead. - */ -export default Cheerio; - /** * The main types of Cheerio objects. * @@ -33,8 +26,17 @@ export type { Node, NodeWithChildren, Element, Document } from 'domhandler'; export * from './load'; import { load } from './load'; + // We add this here, to avoid a cyclic depenency Cheerio.load = load; + +/** + * The default cheerio instance. + * + * @deprecated Use the function returned by `load` instead. + */ +export default load([]); + import * as staticMethods from './static'; /** From bb8b01493445a5fab9ec8b0ac1e493757a7d4987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Sat, 8 May 2021 10:55:17 +0100 Subject: [PATCH 06/11] Have `load` export a function instead of the constructor Separates Cheerio and CheerioAPI as different concepts. We no longer have to fight the type system to allow us to call the constructor without `new`. --- src/api/forms.ts | 7 ++--- src/api/traversing.ts | 2 +- src/cheerio.ts | 67 ++++++++++++++++--------------------------- src/index.ts | 5 ---- src/load.ts | 55 ++++++++++++++++++----------------- src/static.ts | 3 +- 6 files changed, 57 insertions(+), 82 deletions(-) diff --git a/src/api/forms.ts b/src/api/forms.ts index 1c7ad70bfa..5ff24d45b4 100644 --- a/src/api/forms.ts +++ b/src/api/forms.ts @@ -1,5 +1,5 @@ import type { Node } from 'domhandler'; -import type { Cheerio, CheerioAPI } from '../cheerio'; +import type { Cheerio } from '../cheerio'; import { isTag } from '../utils'; /* @@ -54,9 +54,8 @@ export function serializeArray( this: Cheerio ): SerializedField[] { // Resolve all form elements from either forms or collections of form elements - const Cheerio = this.constructor as CheerioAPI; return this.map((_, elem) => { - const $elem = Cheerio(elem); + const $elem = this._make(elem); if (isTag(elem) && elem.name === 'form') { return $elem.find(submittableSelector).toArray(); } @@ -72,7 +71,7 @@ export function serializeArray( // Convert each of the elements to its value(s) ) .map((_, elem) => { - const $elem = Cheerio(elem); + const $elem = this._make(elem); const name = $elem.attr('name') as string; // We have filtered for elements with a name before. // If there is no value set (e.g. `undefined`, `null`), then default value to empty const value = $elem.val() ?? ''; diff --git a/src/api/traversing.ts b/src/api/traversing.ts index 397e2c73df..aa10143b62 100644 --- a/src/api/traversing.ts +++ b/src/api/traversing.ts @@ -8,6 +8,7 @@ import { Node, Element, hasChildren } from 'domhandler'; import type { Cheerio } from '../cheerio'; import * as select from 'cheerio-select'; import { domEach, isTag, isCheerio } from '../utils'; +import { contains } from '../static'; import { DomUtils } from 'htmlparser2'; import type { FilterFunction, AcceptedFilters } from '../types'; const { uniqueSort } = DomUtils; @@ -42,7 +43,6 @@ export function find( const context: Node[] = this.toArray(); if (typeof selectorOrHaystack !== 'string') { - const { contains } = this.constructor as typeof Cheerio; const haystack = isCheerio(selectorOrHaystack) ? selectorOrHaystack.get() : [selectorOrHaystack]; diff --git a/src/cheerio.ts b/src/cheerio.ts index e79d02cb77..7b8f8b3a40 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -3,7 +3,6 @@ import { CheerioOptions, InternalOptions, default as defaultOptions, - flatten as flattenOptions, } from './options'; import { isHtml, isCheerio } from './utils'; import type { Node, Document, Element } from 'domhandler'; @@ -26,11 +25,6 @@ type FormsType = typeof Forms; type StaticType = typeof Static; type LoadType = typeof Load; -/* - * The API - */ -const api = [Attributes, Traversing, Manipulation, Css, Forms]; - export class Cheerio implements ArrayLike { length = 0; [index: number]: T; @@ -46,30 +40,6 @@ export class Cheerio implements ArrayLike { /** @function */ find!: typeof Traversing.find; - /** - * The root the document was originally loaded with. Set in `.load`. - * - * @private - */ - static _root: Document | undefined; - /** - * The options the document was originally loaded with. Set in `.load`. - * - * @private - */ - static _options: InternalOptions | undefined; - public static html = Static.html; - public static xml = Static.xml; - public static text = Static.text; - public static parseHTML = Static.parseHTML; - public static root = Static.root; - public static contains = Static.contains; - public static merge = Static.merge; - public static load: typeof Load.load; - - /** Mimic jQuery's prototype alias for plugin authors. */ - public static fn = Cheerio.prototype; - /** * Instance of cheerio. Methods are specified in the modules. Usage of this * constructor is not recommended. Please use $.load instead. @@ -84,19 +54,23 @@ export class Cheerio implements ArrayLike { selector?: T extends Node ? BasicAcceptedElems : Cheerio | T[], context?: BasicAcceptedElems | null, root?: BasicAcceptedElems | null, - options?: CheerioOptions + options: InternalOptions = defaultOptions ) { - this.options = { - ...defaultOptions, - ...flattenOptions(options), - }; + 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._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; } // $($) @@ -166,7 +140,7 @@ export class Cheerio implements ArrayLike { const cheerio = new (this.constructor as any)( dom, context, - undefined, + this._root, this.options ); cheerio.prevObject = this; @@ -180,11 +154,11 @@ export interface Cheerio TraversingType, ManipulationType, CssType, - FormsType { + FormsType, + Iterable { cheerio: '[cheerio object]'; splice: typeof Array.prototype.slice; - [Symbol.iterator](): Iterator; } /** Set a signature of the object. */ @@ -199,7 +173,14 @@ Cheerio.prototype.splice = Array.prototype.splice; Cheerio.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // Plug in the API -api.forEach((mod) => Object.assign(Cheerio.prototype, mod)); +Object.assign( + Cheerio.prototype, + Attributes, + Traversing, + Manipulation, + Css, + Forms +); function isNode(obj: any): obj is Node { return ( @@ -235,7 +216,7 @@ export interface CheerioAPI extends StaticType, LoadType { * @private */ _options: InternalOptions | undefined; -} -// Make it possible to call Cheerio without using `new`. -export default (Cheerio as unknown) as CheerioAPI; + /** Mimic jQuery's prototype alias for plugin authors. */ + fn: typeof Cheerio.prototype; +} diff --git a/src/index.ts b/src/index.ts index c80e997d16..7b24009527 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ -import Cheerio from './cheerio'; - /** * The main types of Cheerio objects. * @@ -27,9 +25,6 @@ export type { Node, NodeWithChildren, Element, Document } from 'domhandler'; export * from './load'; import { load } from './load'; -// We add this here, to avoid a cyclic depenency -Cheerio.load = load; - /** * The default cheerio instance. * diff --git a/src/load.ts b/src/load.ts index 6cc37ae9aa..e3d0320bbd 100644 --- a/src/load.ts +++ b/src/load.ts @@ -30,39 +30,40 @@ export function load( throw new Error('cheerio.load() expects a string'); } - options = { ...defaultOptions, ...flattenOptions(options) }; + const internalOpts = { ...defaultOptions, ...flattenOptions(options) }; if (typeof isDocument === 'undefined') isDocument = true; - const root = parse(content, options, isDocument); + const root = parse(content, internalOpts, isDocument); - class initialize extends Cheerio { - // Mimic jQuery's prototype alias for plugin authors. - static fn = initialize.prototype; + /** Create an extended class here, so that extensions only live on one instance. */ + class LoadedCheerio extends Cheerio {} - constructor( - selector?: T extends Node - ? string | Cheerio | T[] | T - : Cheerio | T[], - context?: string | Cheerio | Node[] | Node, - r: string | Cheerio | Document | null = root, - opts?: CheerioOptions - ) { - // @ts-expect-error Using `this` before calling the constructor. - if (!(this instanceof initialize)) { - return new initialize(selector, context, r, opts); - } - super(selector, context, r, { ...options, ...opts }); - } + function initialize( + selector?: T extends Node + ? string | Cheerio | T[] | T + : Cheerio | T[], + context?: string | Cheerio | Node[] | Node, + r: string | Cheerio | Document | null = root, + opts?: CheerioOptions + ) { + return new LoadedCheerio(selector, context, r, { + ...internalOpts, + ...flattenOptions(opts), + }); } - // Add in the static methods - Object.assign(initialize, staticMethods, { load }); + // Add in static methods & properties + Object.assign(initialize, staticMethods, { + load, + // `_root` and `_options` are used in static methods. + _root: root, + _options: internalOpts, + // Add `fn` for plugins + fn: LoadedCheerio.prototype, + // Add the prototype here to maintain `instanceof` behavior. + prototype: LoadedCheerio.prototype, + }); - // Add in the root - initialize._root = root; - // Store options - initialize._options = options; - - return (initialize as unknown) as CheerioAPI; + return initialize as CheerioAPI; } diff --git a/src/static.ts b/src/static.ts index 416c81a067..69333713cc 100644 --- a/src/static.ts +++ b/src/static.ts @@ -217,8 +217,7 @@ export function parseHTML( * @alias Cheerio.root */ export function root(this: CheerioAPI): Cheerio { - const fn = (this as unknown) as CheerioAPI; - return fn(this._root); + return this(this._root); } /** From ff01152ccef8b5a408ad949ac609fdee8198221a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Sat, 8 May 2021 11:21:29 +0100 Subject: [PATCH 07/11] Move `CheerioAPI` definition to load.ts --- src/api/forms.spec.ts | 2 +- src/api/manipulation.spec.ts | 2 +- src/cheerio.ts | 45 +++--------------------------------- src/index.ts | 2 +- src/load.ts | 41 ++++++++++++++++++++++++++++++-- src/static.ts | 26 ++++++++++----------- 6 files changed, 57 insertions(+), 61 deletions(-) diff --git a/src/api/forms.spec.ts b/src/api/forms.spec.ts index f020260cb6..c6ca427829 100644 --- a/src/api/forms.spec.ts +++ b/src/api/forms.spec.ts @@ -1,5 +1,5 @@ import cheerio from '../../src'; -import type { CheerioAPI } from '../cheerio'; +import type { CheerioAPI } from '../load'; import { forms } from '../__fixtures__/fixtures'; describe('$(...)', () => { diff --git a/src/api/manipulation.spec.ts b/src/api/manipulation.spec.ts index 39e32e5e4d..787d5be208 100644 --- a/src/api/manipulation.spec.ts +++ b/src/api/manipulation.spec.ts @@ -1,5 +1,5 @@ import { load } from '../../src'; -import type { CheerioAPI, Cheerio } from '../cheerio'; +import type { CheerioAPI, Cheerio } from '..'; import { fruits, divcontainers, mixedText } from '../__fixtures__/fixtures'; import type { Node, Element } from 'domhandler'; diff --git a/src/cheerio.ts b/src/cheerio.ts index 7b8f8b3a40..ef09f478a9 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -1,14 +1,8 @@ import parse from './parse'; -import { - CheerioOptions, - InternalOptions, - default as defaultOptions, -} from './options'; +import { InternalOptions, default as defaultOptions } from './options'; import { isHtml, isCheerio } from './utils'; -import type { Node, Document, Element } from 'domhandler'; -import * as Static from './static'; -import type * as Load from './load'; -import { SelectorType, BasicAcceptedElems } from './types'; +import type { Node, Document } from 'domhandler'; +import { BasicAcceptedElems } from './types'; import * as Attributes from './api/attributes'; import * as Traversing from './api/traversing'; @@ -22,9 +16,6 @@ type ManipulationType = typeof Manipulation; type CssType = typeof Css; type FormsType = typeof Forms; -type StaticType = typeof Static; -type LoadType = typeof Load; - export class Cheerio implements ArrayLike { length = 0; [index: number]: T; @@ -190,33 +181,3 @@ function isNode(obj: any): obj is Node { obj.type === 'comment' ); } - -/** - * Wrapper around the `Cheerio` class, making it possible to create a new - * instance without using `new`. - */ -export interface CheerioAPI extends StaticType, LoadType { - ( - selector?: S | BasicAcceptedElems, - context?: BasicAcceptedElems | null, - root?: BasicAcceptedElems, - options?: CheerioOptions - ): Cheerio; - - /** - * The root the document was originally loaded with. Set in `.load`. - * - * @private - */ - _root: Document | undefined; - - /** - * The options the document was originally loaded with. Set in `.load`. - * - * @private - */ - _options: InternalOptions | undefined; - - /** Mimic jQuery's prototype alias for plugin authors. */ - fn: typeof Cheerio.prototype; -} diff --git a/src/index.ts b/src/index.ts index 7b24009527..be3d85ceac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ * * @category Cheerio */ -export type { Cheerio, CheerioAPI } from './cheerio'; +export type { Cheerio } from './cheerio'; /** * Types used in signatures of Cheerio methods. * diff --git a/src/load.ts b/src/load.ts index e3d0320bbd..5b016c6fef 100644 --- a/src/load.ts +++ b/src/load.ts @@ -1,12 +1,49 @@ import { CheerioOptions, + InternalOptions, default as defaultOptions, flatten as flattenOptions, } from './options'; import * as staticMethods from './static'; -import { CheerioAPI, Cheerio } from './cheerio'; +import { Cheerio } from './cheerio'; import parse from './parse'; -import type { Node, Document } from 'domhandler'; +import type { Node, Document, Element } from 'domhandler'; +import * as Static from './static'; +import type * as Load from './load'; +import { SelectorType, BasicAcceptedElems } from './types'; + +type StaticType = typeof Static; +type LoadType = typeof Load; + +/** + * Wrapper around the `Cheerio` class, making it possible to create a new + * instance without using `new`. + */ +export interface CheerioAPI extends StaticType, LoadType { + ( + selector?: S | BasicAcceptedElems, + context?: BasicAcceptedElems | null, + root?: BasicAcceptedElems, + options?: CheerioOptions + ): Cheerio; + + /** + * The root the document was originally loaded with. + * + * @private + */ + _root: Document; + + /** + * The options the document was originally loaded with. + * + * @private + */ + _options: InternalOptions; + + /** Mimic jQuery's prototype alias for plugin authors. */ + fn: typeof Cheerio.prototype; +} /** * Create a querying function, bound to a document created from the provided diff --git a/src/static.ts b/src/static.ts index 69333713cc..71e2b94c40 100644 --- a/src/static.ts +++ b/src/static.ts @@ -1,4 +1,4 @@ -import type { CheerioAPI, Cheerio } from './cheerio'; +import type { CheerioAPI, Cheerio } from '.'; import { Node, Document } from 'domhandler'; import { InternalOptions, @@ -24,20 +24,18 @@ function render( dom: ArrayLike | Node | string | undefined, options: InternalOptions ): string { - if (!dom) { - if (that?._root?.children) { - dom = that._root.children; - } else { - return ''; - } - } else if (typeof dom === 'string') { - dom = select(dom, that?._root ?? [], options); - } + const toRender = dom + ? typeof dom === 'string' + ? select(dom, that?._root ?? [], options) + : dom + : that?._root.children; + + if (!toRender) return ''; return options.xmlMode || options._useHtmlParser2 ? // FIXME: Pull in new version of dom-serializer to fix this. - renderWithHtmlparser2(dom as Node[], options) - : renderWithParse5(dom); + renderWithHtmlparser2(toRender as Node[], options) + : renderWithParse5(toRender); } /** @@ -96,7 +94,7 @@ export function html( * Sometimes `$.html()` is used without preloading html, * so fallback non-existing options to the default ones. */ - options = { + const opts = { ...defaultOptions, ...(this ? this._options : {}), ...flattenOptions(options ?? {}), @@ -105,7 +103,7 @@ export function html( return render( this || undefined, dom as string | Cheerio | Node | undefined, - options + opts ); } From 3a3f3f586624b5da2b86a91481aa711ac8173642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Sat, 8 May 2021 23:54:24 +0100 Subject: [PATCH 08/11] Simplify some things --- src/api/traversing.spec.ts | 3 ++- src/cheerio.ts | 2 +- src/load.ts | 11 +++-------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/api/traversing.spec.ts b/src/api/traversing.spec.ts index 4d52c77c1e..113f13df50 100644 --- a/src/api/traversing.spec.ts +++ b/src/api/traversing.spec.ts @@ -1,5 +1,6 @@ import cheerio from '../../src'; -import { CheerioAPI, Cheerio } from '../cheerio'; +import { Cheerio } from '../cheerio'; +import type { CheerioAPI } from '../load'; import { Node, Element, isText } from 'domhandler'; import { food, diff --git a/src/cheerio.ts b/src/cheerio.ts index ef09f478a9..01091f592b 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -20,7 +20,7 @@ export class Cheerio implements ArrayLike { length = 0; [index: number]: T; - options!: InternalOptions; + options: InternalOptions; /** * The root of the document. Can be overwritten by using the `root` argument * of the constructor. diff --git a/src/load.ts b/src/load.ts index 5b016c6fef..9b25e9850b 100644 --- a/src/load.ts +++ b/src/load.ts @@ -8,11 +8,10 @@ import * as staticMethods from './static'; import { Cheerio } from './cheerio'; import parse from './parse'; import type { Node, Document, Element } from 'domhandler'; -import * as Static from './static'; import type * as Load from './load'; import { SelectorType, BasicAcceptedElems } from './types'; -type StaticType = typeof Static; +type StaticType = typeof staticMethods; type LoadType = typeof Load; /** @@ -51,26 +50,22 @@ export interface CheerioAPI extends StaticType, LoadType { * introduce ``, ``, and `` elements; set `isDocument` to * `false` to switch to fragment mode and disable this. * - * See the README section titled "Loading" for additional usage information. - * * @param content - Markup to be loaded. * @param options - Options for the created instance. * @param isDocument - Allows parser to be switched to fragment mode. * @returns The loaded document. + * @see {@link https://cheerio.js.org#loading} for additional usage information. */ export function load( content: string | Node | Node[] | Buffer, options?: CheerioOptions | null, - isDocument?: boolean + isDocument = true ): CheerioAPI { if ((content as string | null) == null) { throw new Error('cheerio.load() expects a string'); } const internalOpts = { ...defaultOptions, ...flattenOptions(options) }; - - if (typeof isDocument === 'undefined') isDocument = true; - const root = parse(content, internalOpts, isDocument); /** Create an extended class here, so that extensions only live on one instance. */ From dddd380ef331761fd9af7125287c12adaf1832ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Sun, 9 May 2021 16:06:33 +0100 Subject: [PATCH 09/11] Improve docs for CheerioAPI --- Readme.md | 2 +- src/load.ts | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 2ee7278468..0f25fe511b 100644 --- a/Readme.md +++ b/Readme.md @@ -166,7 +166,7 @@ Cheerio's selector implementation is nearly identical to jQuery's, so the API is `selector` searches within the `context` scope which searches within the `root` scope. `selector` and `context` can be a string expression, DOM Element, array of DOM elements, or cheerio object. `root` is typically the HTML document string. -This selector method is the starting point for traversing and manipulating the document. Like jQuery, it's the primary method for selecting elements in the document, but unlike jQuery it's built on top of the CSSSelect library, which implements most of the Sizzle selectors. +This selector method is the starting point for traversing and manipulating the document. Like jQuery, it's the primary method for selecting elements in the document. ```js $('.apple', '#fruits').text(); diff --git a/src/load.ts b/src/load.ts index 9b25e9850b..a5dd026e79 100644 --- a/src/load.ts +++ b/src/load.ts @@ -15,10 +15,38 @@ type StaticType = typeof staticMethods; type LoadType = typeof Load; /** - * Wrapper around the `Cheerio` class, making it possible to create a new - * instance without using `new`. + * A querying function, bound to a document created from the provided markup. + * + * Also provides several helper methods for dealing with the document as a whole. */ export interface CheerioAPI extends StaticType, LoadType { + /** + * This selector method is the starting point for traversing and manipulating + * the document. Like jQuery, it's the primary method for selecting elements + * in the document. + * + * `selector` searches within the `context` scope which searches within the + * `root` scope. + * + * @example + * + * ```js + * $('.apple', '#fruits').text(); + * //=> Apple + * + * $('ul .pear').attr('class'); + * //=> pear + * + * $('li[class=orange]').html(); + * //=> Orange + * ``` + * + * @param selector - Either a selector to look for within the document, or the + * contents of a new Cheerio instance. + * @param context - Either a selector to look for within the root, or the + * contents of the document to query. + * @param root - Optional HTML document string. + */ ( selector?: S | BasicAcceptedElems, context?: BasicAcceptedElems | null, From 043fd85b8de794d0ac9a3413b6e6e5a1e0b73fac 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:21:58 +0100 Subject: [PATCH 10/11] Disallow passing `null` to `_make` --- src/api/manipulation.ts | 8 ++++---- src/cheerio.ts | 2 +- src/static.spec.ts | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/api/manipulation.ts b/src/api/manipulation.ts index ede2fd208d..dc9420212b 100644 --- a/src/api/manipulation.ts +++ b/src/api/manipulation.ts @@ -168,7 +168,7 @@ export function appendTo( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const appendTarget = isCheerio(target) ? target : this._make(target, null); + const appendTarget = isCheerio(target) ? target : this._make(target); appendTarget.append(this); @@ -200,7 +200,7 @@ export function prependTo( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const prependTarget = isCheerio(target) ? target : this._make(target, null); + const prependTarget = isCheerio(target) ? target : this._make(target); prependTarget.prepend(this); @@ -638,7 +638,7 @@ export function insertAfter( target: BasicAcceptedElems ): Cheerio { if (typeof target === 'string') { - target = this._make(target, null); + target = this._make(target); } this.remove(); @@ -751,7 +751,7 @@ export function insertBefore( this: Cheerio, target: BasicAcceptedElems ): Cheerio { - const targetArr = this._make(target, null); + const targetArr = this._make(target); this.remove(); diff --git a/src/cheerio.ts b/src/cheerio.ts index 01091f592b..c92ec0f58b 100644 --- a/src/cheerio.ts +++ b/src/cheerio.ts @@ -126,7 +126,7 @@ export class Cheerio implements ArrayLike { */ _make( dom: Cheerio | T[] | T | string, - context?: BasicAcceptedElems | null + context?: BasicAcceptedElems ): Cheerio { const cheerio = new (this.constructor as any)( dom, diff --git a/src/static.spec.ts b/src/static.spec.ts index 7851f4c1a5..9b7f7f7c16 100644 --- a/src/static.spec.ts +++ b/src/static.spec.ts @@ -1,6 +1,5 @@ import * as fixtures from './__fixtures__/fixtures'; -import cheerio from '.'; -import { CheerioAPI } from './cheerio'; +import cheerio, { CheerioAPI } from '.'; describe('cheerio', () => { describe('.html', () => { From 5a9281bf2e52a9f8958ae6bcf058626f7eff7965 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:28:38 +0100 Subject: [PATCH 11/11] Add test cases from #1706 Co-Authored-By: 5saviahv <49443574+5saviahv@users.noreply.github.com> --- src/api/traversing.spec.ts | 7 +++++++ src/cheerio.spec.ts | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/api/traversing.spec.ts b/src/api/traversing.spec.ts index 113f13df50..63b2504777 100644 --- a/src/api/traversing.spec.ts +++ b/src/api/traversing.spec.ts @@ -1238,6 +1238,13 @@ describe('$(...)', () => { expect($selection[0]).toBe($fruits[0]); expect($selection[1]).toBe($orange[0]); }); + it('is root object preserved', () => { + const $selection = $('
').add('#fruits'); + + expect($selection).toHaveLength(2); + expect($selection.eq(0).is('div')).toBe(true); + expect($selection.eq(1).is($fruits.eq(0))).toBe(true); + }); }); describe('(selector) matched elements :', () => { it('occur before the current selection', () => { diff --git a/src/cheerio.spec.ts b/src/cheerio.spec.ts index 28bc07d60a..fa7cc2d936 100644 --- a/src/cheerio.spec.ts +++ b/src/cheerio.spec.ts @@ -270,6 +270,15 @@ describe('cheerio', () => { expect(lis).toHaveLength(3); }); + it('should preserve root content', () => { + const $ = cheerio.load(fruits); + // Root should not be overwritten + const el = $('
'); + expect(Object.is(el, el._root)).toBe(false); + // Query has to have results + expect($('li', 'ul')).toHaveLength(3); + }); + it('should allow loading a pre-parsed DOM', () => { const dom = parseDOM(food); const $ = cheerio.load(dom);