Skip to content

Commit b64cb31

Browse files
authored
refactor: Move initialization logic to load (#1951)
1 parent b1fcd16 commit b64cb31

File tree

2 files changed

+121
-110
lines changed

2 files changed

+121
-110
lines changed

src/cheerio.ts

+15-97
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import parse from './parse';
2-
import { InternalOptions, default as defaultOptions } from './options';
3-
import { isHtml, isCheerio } from './utils';
1+
import { InternalOptions } from './options';
42
import type { Node, Document } from 'domhandler';
53
import { BasicAcceptedElems } from './types';
64

@@ -16,7 +14,7 @@ type ManipulationType = typeof Manipulation;
1614
type CssType = typeof Css;
1715
type FormsType = typeof Forms;
1816

19-
export class Cheerio<T> implements ArrayLike<T> {
17+
export abstract class Cheerio<T> implements ArrayLike<T> {
2018
length = 0;
2119
[index: number]: T;
2220

@@ -26,95 +24,34 @@ export class Cheerio<T> implements ArrayLike<T> {
2624
*
2725
* @private
2826
*/
29-
_root: Cheerio<Document> | undefined;
30-
/** @function */
31-
find!: typeof Traversing.find;
27+
_root: Cheerio<Document> | null;
3228

3329
/**
3430
* Instance of cheerio. Methods are specified in the modules. Usage of this
3531
* constructor is not recommended. Please use $.load instead.
3632
*
3733
* @private
38-
* @param selector - The new selection.
39-
* @param context - Context of the selection.
34+
* @param elements - The new selection.
4035
* @param root - Sets the root node.
4136
* @param options - Options for the instance.
4237
*/
4338
constructor(
44-
selector?: T extends Node ? BasicAcceptedElems<T> : Cheerio<T> | T[],
45-
context?: BasicAcceptedElems<Node> | null,
46-
root?: BasicAcceptedElems<Document> | null,
47-
options: InternalOptions = defaultOptions
39+
elements: ArrayLike<T> | undefined,
40+
root: Cheerio<Document> | null,
41+
options: InternalOptions
4842
) {
4943
this.options = options;
50-
51-
// $(), $(null), $(undefined), $(false)
52-
if (!selector) return this;
53-
54-
if (root) {
55-
if (typeof root === 'string') root = parse(root, this.options, false);
56-
this._root = new (this.constructor as typeof Cheerio)(
57-
root,
58-
null,
59-
null,
60-
this.options
61-
);
62-
// Add a cyclic reference, so that calling methods on `_root` never fails.
63-
this._root._root = this._root;
64-
}
65-
66-
// $($)
67-
if (isCheerio<T>(selector)) return selector;
68-
69-
const elements =
70-
typeof selector === 'string' && isHtml(selector)
71-
? // $(<html>)
72-
parse(selector, this.options, false).children
73-
: isNode(selector)
74-
? // $(dom)
75-
[selector]
76-
: Array.isArray(selector)
77-
? // $([dom])
78-
selector
79-
: null;
44+
this._root = root;
8045

8146
if (elements) {
82-
elements.forEach((elem, idx) => {
83-
this[idx] = elem;
84-
});
47+
for (let idx = 0; idx < elements.length; idx++) {
48+
this[idx] = elements[idx];
49+
}
8550
this.length = elements.length;
86-
return this;
8751
}
88-
89-
// We know that our selector is a string now.
90-
let search = selector as string;
91-
92-
const searchContext: Cheerio<Node> | undefined = !context
93-
? // If we don't have a context, maybe we have a root, from loading
94-
this._root
95-
: typeof context === 'string'
96-
? isHtml(context)
97-
? // $('li', '<ul>...</ul>')
98-
this._make(parse(context, this.options, false))
99-
: // $('li', 'ul')
100-
((search = `${context} ${search}`), this._root)
101-
: isCheerio(context)
102-
? // $('li', $)
103-
context
104-
: // $('li', node), $('li', [nodes])
105-
this._make(context);
106-
107-
// If we still don't have a context, return
108-
if (!searchContext) return this;
109-
110-
/*
111-
* #id, .class, tag
112-
*/
113-
// @ts-expect-error No good way to type this — we will always return `Cheerio<Element>` here.
114-
return searchContext.find(search);
11552
}
11653

117-
prevObject: Cheerio<Node> | undefined;
54+
prevObject: Cheerio<any> | undefined;
11855
/**
11956
* Make a cheerio object.
12057
*
@@ -123,20 +60,10 @@ export class Cheerio<T> implements ArrayLike<T> {
12360
* @param context - The context of the new object.
12461
* @returns The new cheerio object.
12562
*/
126-
_make<T>(
127-
dom: Cheerio<T> | T[] | T | string,
63+
abstract _make<T>(
64+
dom: ArrayLike<T> | T | string,
12865
context?: BasicAcceptedElems<Node>
129-
): Cheerio<T> {
130-
const cheerio = new (this.constructor as any)(
131-
dom,
132-
context,
133-
this._root,
134-
this.options
135-
);
136-
cheerio.prevObject = this;
137-
138-
return cheerio;
139-
}
66+
): Cheerio<T>;
14067
}
14168

14269
export interface Cheerio<T>
@@ -171,12 +98,3 @@ Object.assign(
17198
Css,
17299
Forms
173100
);
174-
175-
function isNode(obj: any): obj is Node {
176-
return (
177-
!!obj.name ||
178-
obj.type === 'root' ||
179-
obj.type === 'text' ||
180-
obj.type === 'comment'
181-
);
182-
}

src/load.ts

+106-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './options';
77
import * as staticMethods from './static';
88
import { Cheerio } from './cheerio';
9+
import { isHtml, isCheerio } from './utils';
910
import parse from './parse';
1011
import type { Node, Document, Element } from 'domhandler';
1112
import type * as Load from './load';
@@ -94,30 +95,113 @@ export function load(
9495
}
9596

9697
const internalOpts = { ...defaultOptions, ...flattenOptions(options) };
97-
const root = parse(content, internalOpts, isDocument);
98+
const initialRoot = parse(content, internalOpts, isDocument);
9899

99100
/** Create an extended class here, so that extensions only live on one instance. */
100-
class LoadedCheerio<T> extends Cheerio<T> {}
101-
102-
function initialize<T>(
103-
selector?: T extends Node
104-
? string | Cheerio<T> | T[] | T
105-
: Cheerio<T> | T[],
106-
context?: string | Cheerio<Node> | Node[] | Node,
107-
r: string | Cheerio<Document> | Document | null = root,
101+
class LoadedCheerio<T> extends Cheerio<T> {
102+
_make<T>(
103+
selector?: ArrayLike<T> | T | string,
104+
context?: BasicAcceptedElems<Node> | null
105+
): Cheerio<T> {
106+
const cheerio = initialize(selector, context);
107+
cheerio.prevObject = this;
108+
109+
return cheerio;
110+
}
111+
}
112+
113+
function initialize<T = Node, S extends string = string>(
114+
selector?: ArrayLike<T> | T | S,
115+
context?: BasicAcceptedElems<Node> | null,
116+
root: BasicAcceptedElems<Document> = initialRoot,
108117
opts?: CheerioOptions
109-
) {
110-
return new LoadedCheerio<T>(selector, context, r, {
118+
): Cheerio<S extends SelectorType ? Element : T> {
119+
type Result = S extends SelectorType ? Element : T;
120+
121+
// $($)
122+
if (selector && isCheerio<Result>(selector)) return selector;
123+
124+
const options = {
111125
...internalOpts,
112126
...flattenOptions(opts),
113-
});
127+
};
128+
const r =
129+
typeof root === 'string'
130+
? [parse(root, options, false)]
131+
: 'length' in root
132+
? root
133+
: [root];
134+
const rootInstance = isCheerio<Document>(r)
135+
? r
136+
: new LoadedCheerio<Document>(r, null, options);
137+
// Add a cyclic reference, so that calling methods on `_root` never fails.
138+
rootInstance._root = rootInstance;
139+
140+
// $(), $(null), $(undefined), $(false)
141+
if (!selector) {
142+
return new LoadedCheerio<Result>(undefined, rootInstance, options);
143+
}
144+
145+
const elements: Node[] | undefined =
146+
typeof selector === 'string' && isHtml(selector)
147+
? // $(<html>)
148+
parse(selector, options, false).children
149+
: isNode(selector)
150+
? // $(dom)
151+
[selector]
152+
: Array.isArray(selector)
153+
? // $([dom])
154+
selector
155+
: undefined;
156+
157+
const instance = new LoadedCheerio(elements, rootInstance, options);
158+
159+
if (elements || !selector) {
160+
return instance as any;
161+
}
162+
163+
if (typeof selector !== 'string') throw new Error('');
164+
165+
// We know that our selector is a string now.
166+
let search = selector;
167+
168+
const searchContext: Cheerio<Node> | undefined = !context
169+
? // If we don't have a context, maybe we have a root, from loading
170+
rootInstance
171+
: typeof context === 'string'
172+
? isHtml(context)
173+
? // $('li', '<ul>...</ul>')
174+
new LoadedCheerio<Document>(
175+
[parse(context, options, false)],
176+
rootInstance,
177+
options
178+
)
179+
: // $('li', 'ul')
180+
((search = `${context} ${search}` as S), rootInstance)
181+
: isCheerio<Node>(context)
182+
? // $('li', $)
183+
context
184+
: // $('li', node), $('li', [nodes])
185+
new LoadedCheerio<Node>(
186+
Array.isArray(context) ? context : [context],
187+
rootInstance,
188+
options
189+
);
190+
191+
// If we still don't have a context, return
192+
if (!searchContext) return instance as any;
193+
194+
/*
195+
* #id, .class, tag
196+
*/
197+
return searchContext.find(search) as Cheerio<Result>;
114198
}
115199

116200
// Add in static methods & properties
117201
Object.assign(initialize, staticMethods, {
118202
load,
119203
// `_root` and `_options` are used in static methods.
120-
_root: root,
204+
_root: initialRoot,
121205
_options: internalOpts,
122206
// Add `fn` for plugins
123207
fn: LoadedCheerio.prototype,
@@ -127,3 +211,12 @@ export function load(
127211

128212
return initialize as CheerioAPI;
129213
}
214+
215+
function isNode(obj: any): obj is Node {
216+
return (
217+
!!obj.name ||
218+
obj.type === 'root' ||
219+
obj.type === 'text' ||
220+
obj.type === 'comment'
221+
);
222+
}

0 commit comments

Comments
 (0)