Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Move initialization logic to load #1951

Merged
merged 3 commits into from
Jun 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
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'
);
}