Coverage

93%
373
347
26

cheerio.js

100%
38
38
0
LineHitsSource
1/*
2 Module dependencies
3*/
4
51var path = require('path'),
6 select = require('cheerio-select'),
7 parse = require('./parse'),
8 evaluate = parse.evaluate,
9 updateDOM = parse.update,
10 isArray = Array.isArray,
11 _ = require('underscore');
12
13/*
14 * The API
15 */
16
171var api = ['attributes', 'traversing', 'manipulation'];
18
19/*
20 * A simple way to check for HTML strings or ID strings
21 */
22
231var quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
24
25/**
26 * Static Methods
27 */
28
291var $ = require('./static');
30
31/*
32 * Instance of cheerio
33 */
34
351var Cheerio = module.exports = function(selector, context, root) {
361020 if(!(this instanceof Cheerio)) return new Cheerio(selector, context, root);
37
38 // $(), $(null), $(undefined), $(false)
39941 if(!selector) return this;
40
41219 if(root) {
4210 if(typeof root === 'string') root = parse(root);
437 this._root = this.make(root, this);
44 }
45
46 // $($)
47219 if(selector.cheerio) return selector;
48
49 // $(dom)
50219 if(selector.name || isArray(selector))
513 return this.make(selector, this);
52
53 // $(<html>)
54216 if(typeof selector === 'string' && isHtml(selector)) {
5564 return this.make(parse(selector).children);
56 }
57
58 // Normalize the context
59152 if(context) {
60193 if( typeof context === 'string' ) context = parse(context).children;
61143 context = this.make(context, this);
62 } else {
639 context = this._root;
64 }
65
66 // If we still don't have a context, return
67156 if(!context) return this;
68
69 // #id, .class, tag
70148 return context.find(selector);
71};
72
73/**
74 * Inherit from `static`
75 */
76
771Cheerio.__proto__ = require('./static');
78
79/*
80 * Set a signature of the object
81 */
82
831Cheerio.prototype.cheerio = '[cheerio object]';
84
85/*
86 * Cheerio default options
87 */
88
891Cheerio.prototype.options = {
90 ignoreWhitespace : false,
91 xmlMode : false,
92 lowerCaseTags : false
93};
94
95/*
96 * Make cheerio an array-like object
97 */
98
991Cheerio.prototype.length = 0;
1001Cheerio.prototype.sort = [].splice;
101
102/*
103 * Check if string is HTML
104 */
1051function isHtml(str) {
106 // Faster than running regex, if str starts with `<` and ends with `>`, assume it's HTML
107280 if ( str.charAt(0) === "<" && str.charAt( str.length - 1 ) === ">" && str.length >= 3 ) return true;
108
109 // Run the regex
110152 var match = quickExpr.exec(str);
111152 return (match && match[1]) ? true : false;
112}
113
114/*
115 * Make a cheerio object
116 */
117
1181Cheerio.prototype.make = function(dom, context) {
119601 if(dom.cheerio) return dom;
120415 dom = (_.isArray(dom)) ? dom : [dom];
121415 return _.extend(context || new Cheerio(), dom, { length : dom.length });
122};
123
124/**
125 * Turn a cheerio object into an array
126 */
127
1281Cheerio.prototype.toArray = function() {
12912 return [].slice.call(this, 0);
130};
131
132/**
133 * Plug in the API
134 */
1351api.forEach(function(mod) {
1363 _.extend(Cheerio.prototype, require('./api/' + mod));
137});

parse.js

100%
35
35
0
LineHitsSource
1/*
2 Module Dependencies
3*/
41var htmlparser = require('htmlparser2'),
5 _ = require('underscore'),
6 isTag = require('./utils').isTag,
7 isArray = Array.isArray;
8
9/*
10 Parser
11*/
121exports = module.exports = function(content, options) {
13121 var dom = evaluate(content, options);
14
15 // Generic root element
16121 var root = {
17 type : 'root',
18 name : 'root',
19 parent : null,
20 prev : null,
21 next : null,
22 children : []
23 };
24
25 // Update the dom using the root
26121 update(dom, root);
27
28121 return root;
29};
30
311var evaluate = exports.evaluate = function(content, options) {
32 // options = options || $.fn.options;
33
34135 var handler = new htmlparser.DomHandler(options),
35 parser = new htmlparser.Parser(handler, options);
36
37135 parser.write(content);
38135 parser.done();
39
40135 return connect(handler.dom);
41};
42
431var connect = exports.connect = function(dom, parent) {
44529 parent = parent || null;
45
46529 var prevElem = null;
47
48529 _.each(dom, function(elem) {
49 // If tag and no attributes, add empty object
50709 if (isTag(elem.type) && elem.attribs === undefined)
5145 elem.attribs = {};
52
53 // Set parent
54709 elem.parent = parent;
55
56 // Previous Sibling
57709 elem.prev = prevElem;
58
59 // Next sibling
60709 elem.next = null;
61890 if (prevElem) prevElem.next = elem;
62
63 // Run through the children
64709 if (elem.children)
65394 connect(elem.children, elem);
66315 else if (isTag(elem.type))
6713 elem.children = [];
68
69 // Get ready for next element
70709 prevElem = elem;
71 });
72
73529 return dom;
74};
75
76/*
77 Update the dom structure, for one changed layer
78
79 * Much faster than reconnecting
80*/
811var update = exports.update = function(arr, parent) {
82 // normalize
83156 arr = isArray(arr) ? arr : [arr];
84
85 // Update neighbors
86156 for (var i = 0; i < arr.length; i++) {
87211 arr[i].prev = arr[i-1] || null;
88211 arr[i].next = arr[i+1] || null;
89211 arr[i].parent = parent || null;
90 }
91
92 // Update parent
93156 parent.children = arr;
94
95156 return parent;
96};
97
98// module.exports = $.extend(exports);

utils.js

100%
7
7
0
LineHitsSource
1/**
2 * Module Dependencies
3 */
41var entities = require('entities');
5
6/**
7 * HTML Tags
8 */
9
101var tags = { tag : true, script : true, style : true };
11
12/**
13 * Check if the DOM element is a tag
14 *
15 * isTag(type) includes <script> and <style> tags
16 */
17
181exports.isTag = function(type) {
191306 if (type.type) type = type.type;
201166 return tags[type] || false;
21};
22
23/**
24 * Expose encode and decode methods from FB55's node-entities library
25 *
26 * 0 = XML, 1 = HTML4 and 2 = HTML5
27 */
28
2919exports.encode = function(str) { return entities.encode(str, 0); };
3055exports.decode = function(str) { return entities.decode(str, 2); };

static.js

96%
26
25
1
LineHitsSource
1/**
2 * Module dependencies
3 */
4
51var select = require('cheerio-select'),
6 parse = require('./parse'),
7 render = require('./render'),
8 decode = require('./utils').decode;
9
10/**
11 * $.load(str)
12 */
13
141var load = exports.load = function(str, options) {
154 var Cheerio = require('./cheerio'),
16 root = parse(str, options);
17
184 function initialize(selector, context, r) {
194 return new Cheerio(selector, context, r || root);
20 }
21
22 // Add in the static methods
234 initialize.__proto__ = exports;
24
25 // Add in the root
264 initialize._root = root;
27
284 return initialize;
29};
30
31/**
32 * $.html([selector | dom])
33 */
34
351var html = exports.html = function(dom) {
3623 if (dom) {
3720 dom = (typeof dom === 'string') ? select(selector, this._root) : dom;
3820 return render(dom);
393 } else if (this._root && this._root.children) {
403 return render(this._root.children);
41 } else {
420 return '';
43 }
44};
45
46/**
47 * $.text(dom)
48 */
49
501var text = exports.text = function(elems) {
5131 if (!elems) return '';
52
5331 var ret = '',
54 len = elems.length,
55 elem;
56
5731 for (var i = 0; i < len; i ++) {
5841 elem = elems[i];
5961 if (elem.type === 'text') ret += decode(elem.data);
6021 else if (elem.children && elem.type !== 'comment') {
6121 ret += text(elem.children);
62 }
63 }
64
6531 return ret;
66};
67
68/**
69 * $.root()
70 */
711var root = exports.root = function() {
721 return this(this._root);
73};

render.js

86%
51
44
7
LineHitsSource
1/*
2 Module dependencies
3*/
41var _ = require('underscore'),
5 isArray = Array.isArray;
6
7// Options affecting rendering
8// var xmlMode = $.fn.options.xmlMode,
9// ignoreWhitespace = $.fn.options.ignoreWhitespace;
10
11/*
12 Boolean Attributes
13*/
141var rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
15
16/*
17 Format attributes
18*/
191var formatAttrs = function(attributes) {
206 if (!attributes) return '';
21
226 var output = [],
23 value;
24
25 // Loop through the attributes
266 for (var key in attributes) {
276 value = attributes[key];
286 if (!value && (rboolean.test(key) || key === '/')) {
290 output.push(key);
30 } else {
316 output.push(key + '="' + value + '"');
32 }
33 }
34
356 return output.join(' ');
36};
37
38/*
39 Self-enclosing tags (stolen from node-htmlparser)
40*/
411var singleTag = {
42 area: 1,
43 base: 1,
44 basefont: 1,
45 br: 1,
46 col: 1,
47 frame: 1,
48 hr: 1,
49 img: 1,
50 input: 1,
51 isindex: 1,
52 link: 1,
53 meta: 1,
54 param: 1,
55 embed: 1,
56 include: 1,
57 'yield': 1
58};
59
60/*
61 Tag types from htmlparser
62*/
631var tagType = {
64 tag : 1,
65 script : 1,
66 link : 1,
67 style : 1,
68 template : 1
69};
70
71// keeps depth for pretty parsing
721var depth = 0;
73
741render = module.exports = function(dom, opts) {
7523 dom = (isArray(dom) || dom.cheerio) ? dom : [dom];
7623 opts = opts || {};
77
7823 var output = [],
79 tidy = !!opts.tidy;
80
8123 var xmlMode = opts.xmlMode || false,
82 ignoreWhitespace = opts.ignoreWhitespace || false;
83
8423 _.each(dom, function(elem) {
8527 var pushVal;
86
8727 if (tagType[elem.type])
8819 pushVal = renderTag(elem);
898 else if (elem.type === 'directive')
900 pushVal = renderDirective(elem);
918 else if (elem.type === 'comment')
920 pushVal = renderComment(elem);
93 else
948 pushVal = renderText(elem);
95
9627 var spacing = '';
9727 if (tidy) {
980 spacing = Array(depth + 1).join(' ');
990 output.push(spacing + pushVal + '\n');
100 } else {
10127 output.push(pushVal);
102 }
103
10427 depth++;
10527 if (elem.children)
10623 output.push(render(elem.children, { tidy: tidy }));
10727 depth--;
108
10927 if ((!singleTag[elem.name] || xmlMode) && tagType[elem.type])
11017 output.push(spacing + '</' + elem.name + '>');
111 });
112
11323 return output.join(tidy ? '\n' : '');
114};
115
1161var renderTag = function(elem) {
11719 var tag = '<' + elem.name;
118
11919 if (elem.attribs && _.size(elem.attribs)) {
1206 tag += ' ' + formatAttrs(elem.attribs);
121 }
122
12319 return tag + '>';
124};
125
1261var renderDirective = function(elem) {
1270 return '<' + elem.data + '>';
128};
129
1301var renderText = function(elem) {
1318 return elem.data;
132};
133
1341var renderComment = function(elem) {
1350 return '<!--' + elem.data + '-->';
136};
137
138// module.exports = $.extend(exports);

api/attributes.js

86%
68
59
9
LineHitsSource
11var _ = require('underscore'),
2 utils = require('../utils'),
3 isTag = utils.isTag,
4 decode = utils.decode,
5 encode = utils.encode,
6 rspace = /\s+/;
7
8/**
9 * Attributes that are booleans
10 */
11
121rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
13
14
151function setAttr(el, name, value) {
1615 if(typeof name === 'object') return _.extend(el.attribs, name);
17
1813 if(value === null) {
190 removeAttribute(el, name);
20 } else {
2113 el.attribs[name] = encode(value);
22 }
23
2413 return el.attribs;
25}
26
271var attr = exports.attr = function(name, value) {
2849 var len = this.length,
29 elem = this[0];
30
3149 if (!elem || !isTag(elem))
321 return undefined;
33
3448 if (!elem.attribs) {
350 elem.attribs = {};
36 }
37
38 // Return the entire attribs object if no attribute specified
3948 if (!name) {
402 for(var a in elem.attribs) {
415 elem.attribs[a] = decode(elem.attribs[a]);
42 }
432 return elem.attribs;
44 }
45
46 // Set the value (with attr map support)
4746 if(typeof name === 'object' || value !== undefined) {
4814 this.each(function(i, el) {
4914 el.attribs = setAttr(el, name, value);
50 });
5114 return this;
5232 } else if(elem.attribs[name]) {
53 // Get the (decoded) attribute
5429 return decode(elem.attribs[name]);
55 }
56};
57
58/**
59 * Remove an attribute
60 */
61
621function removeAttribute(elem, name) {
632 if (!isTag(elem.type) || !elem.attribs || !elem.attribs[name])
640 return;
65
662 if (rboolean.test(elem.attribs[name]))
670 elem.attribs[name] = false;
68 else
692 delete elem.attribs[name];
70}
71
72
731var removeAttr = exports.removeAttr = function(name) {
742 this.each(function(i, elem) {
752 removeAttribute(elem, name);
76 });
77
782 return this;
79};
80
811var hasClass = exports.hasClass = function(className) {
8244 return _.any(this, function(elem) {
8344 var attrs = elem.attribs;
8444 return attrs && _.contains((attrs['class'] || '').split(rspace), className);
85 });
86};
87
881var addClass = exports.addClass = function(value) {
89 // Support functions
908 if (_.isFunction(value)) {
910 this.each(function(i) {
920 var className = this.attr('class') || '';
930 this.addClass(value.call(this, i, className));
94 });
95 }
96
97 // Return if no value or not a string or function
988 if (!value || !_.isString(value)) return this;
99
1008 var classNames = value.split(rspace),
101 numElements = this.length,
102 numClasses,
103 setClass,
104 $elem;
105
106
1078 for (var i = 0; i < numElements; i++) {
10810 $elem = this.make(this[i]);
109 // If selected element isnt a tag, move on
11010 if (!isTag(this[i])) continue;
111
112 // If we don't already have classes
11310 if (!$elem.attr('class')) {
1141 $elem.attr('class', classNames.join(' ').trim());
115 } else {
1169 setClass = ' ' + $elem.attr('class') + ' ';
1179 numClasses = classNames.length;
118
119 // Check if class already exists
1209 for (var j = 0; j < numClasses; j++) {
12113 if (!~setClass.indexOf(' ' + classNames[j] + ' '))
12213 setClass += classNames[j] + ' ';
123 }
124
1259 $elem.attr('class', setClass.trim());
126 }
127 }
128
1298 return this;
130};
131
1321var removeClass = exports.removeClass = function(value) {
1337 var classes = split(value);
134
1357 function split(className) {
13614 return !className ? [] : className.trim().split(rspace);
137 }
138
139 // Handle if value is a function
1407 if (_.isFunction(value)) {
1410 return this.each(function(i, el) {
1420 this.removeClass(value.call(this, i, el.attribs['class'] || ''));
143 });
144 }
145
1467 return this.each(function(i, el) {
1478 if(!isTag(el)) return;
1488 el.attribs['class'] = (!value) ? '' : _.reject(
149 split(el.attribs['class']),
15014 function(name) { return _.contains(classes, name); }
151 ).join('');
152 });
153};

api/traversing.js

90%
60
54
6
LineHitsSource
11var _ = require('underscore'),
2 select = require('cheerio-select'),
3 utils = require('../utils'),
4 isTag = utils.isTag;
5
61var find = exports.find = function(selector) {
7159 if (!selector) return this;
8157 try {
9157 var elem = select(selector, [].slice.call(this));
10157 return this.make(elem);
11 } catch(e) {
120 return this.make([]);
13 }
14};
15
161var parent = exports.parent = function(elem) {
177 if (this[0] && this[0].parent)
187 return this.make(this[0].parent);
19 else
200 return this;
21};
22
231var next = exports.next = function(elem) {
247 if (!this[0]) return this;
25
267 var nextSibling = this[0].next;
277 while (nextSibling) {
2814 if (isTag(nextSibling)) return this.make(nextSibling);
290 nextSibling = nextSibling.next;
30 }
31
320 return this;
33};
34
351var prev = exports.prev = function(elem) {
365 if (!this[0]) return this;
37
385 var prevSibling = this[0].prev;
395 while (prevSibling) {
4010 if (isTag(prevSibling)) return this.make(prevSibling);
410 prevSibling = prevSibling.prev;
42 }
430 return this;
44};
45
461var siblings = exports.siblings = function(elem) {
472 if (!this[0]) return this;
482 var self = this,
49 siblings = (this.parent()) ? this.parent().children()
50 : this.siblingsAndMe();
51
522 siblings = _.filter(siblings, function(elem) {
536 return (elem !== self[0] && isTag(elem));
54 });
55
562 return this.make(siblings);
57};
58
591var children = exports.children = function(selector) {
6018 if (!this[0] || !this[0].children) return this;
61
6218 var children = _.filter(this[0].children, function(elem) {
6358 return (isTag(elem));
64 });
65
6626 if (selector === undefined) return this.make(children);
6718 else if (_.isNumber(selector)) return this.make(children[selector]);
68
692 return this.make(children).find(selector);
70};
71
721var each = exports.each = function(fn) {
7360 var len = this.length,
74 el,
75 $;
76
7760 for(var i = 0; i < len; i++) {
7867 el = this[i];
7967 $ = this.make(el);
8067 fn.call($, i, el);
81 }
82
8360 return this;
84};
85
861var map = exports.map = function(fn) {
871 var len = this.length,
88 ret = [],
89 el,
90 $;
91
921 for(var i = 0; i < len; i++) {
933 el = this[i];
943 $ = this.make(el);
953 ret[ret.length] = fn.call($, i, el);
96 }
97
981 return ret;
99};
100
1011var first = exports.first = function() {
1023 return this[0] ? this.make(this[0]) : this;
103};
104
1051var last = exports.last = function() {
1063 return this[0] ? this.make(this[this.length - 1]) : this;
107};
108
109// Reduce the set of matched elements to the one at the specified index.
1101var eq = exports.eq = function(i) {
11111 i = +i;
11212 if (i < 0) i = this.length + i;
11311 return this[i] ? this.make(this[i]) : this.make([]);
114};

api/manipulation.js

96%
88
85
3
LineHitsSource
11var _ = require('underscore'),
2 parse = require('../parse'),
3 $ = require('../static'),
4 updateDOM = parse.update,
5 evaluate = parse.evaluate,
6 encode = require('../utils').encode,
7 slice = Array.prototype.slice;
8
9/*
10 Creates an array of cheerio objects,
11 parsing strings if necessary
12*/
131var makeCheerioArray = function(elems) {
1419 return _.reduce(elems, function(dom, elem) {
1519 return dom.concat(elem.cheerio ? elem.toArray() : evaluate(elem));
16 }, []);
17};
18
191var append = exports.append = function() {
207 var elems = slice.call(arguments),
21 dom = makeCheerioArray(elems);
22
237 this.each(function(i, el) {
247 if (_.isFunction(elems[0])) {
25 // No yet supported
260 return this;
27 } else {
287 if (!el.children) el.children = [];
297 el.children = el.children.concat(dom);
307 updateDOM(el.children, el);
31 }
32 });
33
347 return this;
35};
36
37
38/*
39 TODO: Refactor, only one line difference between,
40 this function and append
41*/
421var prepend = exports.prepend = function() {
434 var elems = slice.call(arguments),
44 dom = makeCheerioArray(elems);
45
464 this.each(function(i, el) {
474 if (_.isFunction(elems[0])) {
48 // No yet supported
490 return this;
50 } else {
514 if (!el.children) el.children = [];
524 el.children = dom.concat(el.children);
534 updateDOM(el.children, el);
54 }
55 });
56
574 return this;
58};
59
601var after = exports.after = function() {
614 var elems = slice.call(arguments),
62 dom = makeCheerioArray(elems);
63
644 this.each(function(i, el) {
654 var siblings = el.parent.children,
66 index = siblings.indexOf(el);
67
68 // If not found, move on
694 if (!~index) return;
70
71 // Add element after `this` element
724 siblings.splice.apply(siblings, [++index, 0].concat(dom));
73
74 // Update next, prev, and parent pointers
754 updateDOM(siblings, el.parent);
764 el.parent.children = siblings;
77
78 });
79
804 return this;
81};
82
831var before = exports.before = function() {
844 var elems = slice.call(arguments),
85 dom = makeCheerioArray(elems);
86
874 this.each(function(i, el) {
884 var siblings = el.parent.children,
89 index = siblings.indexOf(el);
90
91 // If not found, move on
924 if (!~index) return;
93
94 // Add element before `el` element
954 siblings.splice.apply(siblings, [index, 0].concat(dom));
96
97 // Update next, prev, and parent pointers
984 updateDOM(siblings, el.parent);
994 el.parent.children = siblings;
100
101 });
102
1034 return this;
104};
105
106/*
107 remove([selector])
108*/
1091var remove = exports.remove = function(selector) {
1102 var elems = this;
111
112 // Filter if we have selector
1132 if (selector)
1141 elems = elems.find(selector);
115
1162 elems.each(function(i, el) {
1172 var siblings = el.parent.children,
118 index = siblings.indexOf(el);
119
1202 if (!~index) return;
121
1222 siblings.splice(index, 1);
123
124 // Update next, prev, and parent pointers
1252 updateDOM(siblings, el.parent);
1262 el.parent.children = siblings;
127 });
128
1292 return this;
130};
131
1321var replaceWith = exports.replaceWith = function(content) {
1334 content = content.cheerio ? content.toArray() : evaluate(content);
134
1354 this.each(function(i, el) {
1364 var siblings = el.parent.children,
137 index = siblings.indexOf(el);
138
1394 if (!~index) return;
140
1414 siblings.splice.apply(siblings, [index, 1].concat(content));
142
1434 updateDOM(siblings, el.parent);
1444 el.parent.children = siblings;
145 });
146
1474 return this;
148};
149
1501var empty = exports.empty = function() {
1511 this.each(function(i, el) {
1521 el.children = [];
153 });
1541 return this;
155};
156
1571var tidy = exports.tidy = function() {
1580 return $.tidy(this[0].children);
159};
160
161/**
162 * Set/Get the HTML
163 */
1641var html = exports.html = function(str) {
16513 if (str === undefined) {
16611 if (!this[0] || !this[0].children) return null;
1679 return $.html(this[0].children);
168 }
169
1703 str = str.cheerio ? str.toArray() : evaluate(str);
171
1723 this.each(function(i, el) {
1733 el.children = str;
1743 updateDOM(el.children, el);
175 });
176
1773 return this;
178};
179
1801var text = exports.text = function(str) {
181 // If `str` blank or an object
18216 if (!str || typeof str === 'object') {
18310 return $.text(this);
1846 } else if (_.isFunction(str)) {
185 // Function support
1861 return this.each(function(i, el) {
1871 return this.text(str.call(el, i, this.text()));
188 });
189 }
190
1915 var elem = {
192 data : encode(str),
193 type : 'text',
194 parent : null,
195 prev : null,
196 next : null,
197 children : []
198 };
199
200 // Append text node to each selected elements
2015 this.each(function(i, el) {
2027 el.children = elem;
2037 updateDOM(el.children, el);
204 });
205
2065 return this;
207};
208
2091var clone = exports.clone = function() {
210 // Turn it into HTML, then recreate it,
211 // Seems to be the easiest way to reconnect everything correctly
2121 return this.constructor($.html(this));
213};