Skip to content

Commit

Permalink
refactor: Move initialization logic to load (#1951)
Browse files Browse the repository at this point in the history
  • Loading branch information
fb55 authored Jun 26, 2021
1 parent b1fcd16 commit b64cb31
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 110 deletions.
112 changes: 15 additions & 97 deletions src/cheerio.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -16,7 +14,7 @@ type ManipulationType = typeof Manipulation;
type CssType = typeof Css;
type FormsType = typeof Forms;

export class Cheerio<T> implements ArrayLike<T> {
export abstract class Cheerio<T> implements ArrayLike<T> {
length = 0;
[index: number]: T;

Expand All @@ -26,95 +24,34 @@ export class Cheerio<T> implements ArrayLike<T> {
*
* @private
*/
_root: Cheerio<Document> | undefined;
/** @function */
find!: typeof Traversing.find;
_root: Cheerio<Document> | 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<T> : Cheerio<T> | T[],
context?: BasicAcceptedElems<Node> | null,
root?: BasicAcceptedElems<Document> | null,
options: InternalOptions = defaultOptions
elements: ArrayLike<T> | undefined,
root: Cheerio<Document> | 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<T>(selector)) return selector;

const elements =
typeof selector === 'string' && isHtml(selector)
? // $(<html>)
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<Node> | undefined = !context
? // If we don't have a context, maybe we have a root, from loading
this._root
: typeof context === 'string'
? isHtml(context)
? // $('li', '<ul>...</ul>')
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<Element>` here.
return searchContext.find(search);
}

prevObject: Cheerio<Node> | undefined;
prevObject: Cheerio<any> | undefined;
/**
* Make a cheerio object.
*
Expand All @@ -123,20 +60,10 @@ export class Cheerio<T> implements ArrayLike<T> {
* @param context - The context of the new object.
* @returns The new cheerio object.
*/
_make<T>(
dom: Cheerio<T> | T[] | T | string,
abstract _make<T>(
dom: ArrayLike<T> | T | string,
context?: BasicAcceptedElems<Node>
): Cheerio<T> {
const cheerio = new (this.constructor as any)(
dom,
context,
this._root,
this.options
);
cheerio.prevObject = this;

return cheerio;
}
): Cheerio<T>;
}

export interface Cheerio<T>
Expand Down Expand Up @@ -171,12 +98,3 @@ Object.assign(
Css,
Forms
);

function isNode(obj: any): obj is Node {
return (
!!obj.name ||
obj.type === 'root' ||
obj.type === 'text' ||
obj.type === 'comment'
);
}
119 changes: 106 additions & 13 deletions src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,30 +95,113 @@ 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<T> extends Cheerio<T> {}

function initialize<T>(
selector?: T extends Node
? string | Cheerio<T> | T[] | T
: Cheerio<T> | T[],
context?: string | Cheerio<Node> | Node[] | Node,
r: string | Cheerio<Document> | Document | null = root,
class LoadedCheerio<T> extends Cheerio<T> {
_make<T>(
selector?: ArrayLike<T> | T | string,
context?: BasicAcceptedElems<Node> | null
): Cheerio<T> {
const cheerio = initialize(selector, context);
cheerio.prevObject = this;

return cheerio;
}
}

function initialize<T = Node, S extends string = string>(
selector?: ArrayLike<T> | T | S,
context?: BasicAcceptedElems<Node> | null,
root: BasicAcceptedElems<Document> = initialRoot,
opts?: CheerioOptions
) {
return new LoadedCheerio<T>(selector, context, r, {
): Cheerio<S extends SelectorType ? Element : T> {
type Result = S extends SelectorType ? Element : T;

// $($)
if (selector && isCheerio<Result>(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<Document>(r)
? r
: new LoadedCheerio<Document>(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<Result>(undefined, rootInstance, options);
}

const elements: Node[] | undefined =
typeof selector === 'string' && isHtml(selector)
? // $(<html>)
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) {

This comment has been minimized.

Copy link
@XhmikosR

XhmikosR Dec 27, 2021

Contributor

!selector seems to be moot here since it's already checked on line 141:

https://lgtm.com/projects/g/cheeriojs/cheerio/?mode=list

This comment has been minimized.

Copy link
@fb55

fb55 Dec 28, 2021

Author Member

Thanks for flagging! I've opened #2279 with a fix for this, as well as some other similar issues.

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<Node> | undefined = !context
? // If we don't have a context, maybe we have a root, from loading
rootInstance
: typeof context === 'string'
? isHtml(context)
? // $('li', '<ul>...</ul>')
new LoadedCheerio<Document>(
[parse(context, options, false)],
rootInstance,
options
)
: // $('li', 'ul')
((search = `${context} ${search}` as S), rootInstance)
: isCheerio<Node>(context)
? // $('li', $)
context
: // $('li', node), $('li', [nodes])
new LoadedCheerio<Node>(
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<Result>;
}

// 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,
Expand All @@ -127,3 +211,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'
);
}

0 comments on commit b64cb31

Please sign in to comment.