From 25a51d7cb2edac34eb28cd51c89ca7a181ba0041 Mon Sep 17 00:00:00 2001 From: Danny Hurlburt Date: Mon, 25 Dec 2017 10:39:20 -0700 Subject: [PATCH 01/16] Add Missing serialize Documentation (#1111) --- Readme.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Readme.md b/Readme.md index 2f347f9c9c..988a9c077f 100644 --- a/Readme.md +++ b/Readme.md @@ -149,6 +149,7 @@ Does your company use Cheerio in production? Please consider [sponsoring this pr
Forms + - [.serialize()](#serialize) - [.serializeArray()](#serializearray)
@@ -429,6 +430,15 @@ Checks the current list of elements and returns `true` if _any_ of the elements ### Forms +#### .serialize() + +Encodes a set of form elements as a URL query string. + +```js +$('
').serialize() +//=> foo=bar&foo=qux +``` + #### .serializeArray() Encode a set of form elements as an array of names and values. From 7b6684ddab1c6d3bcdfee078db5c192f8e7e5361 Mon Sep 17 00:00:00 2001 From: Gary Katsevman Date: Tue, 27 Feb 2018 16:47:50 -0500 Subject: [PATCH 02/16] Add note about XML namespace to README (#1151) --- Readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Readme.md b/Readme.md index 988a9c077f..273bd3cbe8 100644 --- a/Readme.md +++ b/Readme.md @@ -304,6 +304,13 @@ $('li[class=orange]').html() //=> Orange ``` +##### XML Namespaces +You can select with XML Namespaces but [due to the CSS specification](https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#attribute-selectors), the colon (`:`) needs to be escaped for the selector to be valid. + +```js +$('[xml\\:id="main"'); +``` + ### Attributes Methods for getting and modifying attributes. From 021e3c214ed0fa692f4ab87da5ad566e1fe2a097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= Date: Mon, 5 Mar 2018 17:08:32 -0800 Subject: [PATCH 03/16] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..c9cd136f79 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +cheerio.js.org \ No newline at end of file From d151d8008ddb8af11bc9f4c3dbf1c51ae0f52ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= Date: Mon, 5 Mar 2018 17:08:54 -0800 Subject: [PATCH 04/16] Set theme jekyll-theme-cayman --- _config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 _config.yml diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000000..c4192631f2 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file From 89e7d1ca5cb9c4b8b0e9c968131516ef31bd11ca Mon Sep 17 00:00:00 2001 From: besteman Date: Fri, 9 Mar 2018 08:37:24 -0500 Subject: [PATCH 05/16] Fix spelling (#1157) --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 273bd3cbe8..822353f919 100644 --- a/Readme.md +++ b/Readme.md @@ -978,7 +978,7 @@ $('li').wrap(healthy) // ``` -#### .css( [propertName] )
.css( [ propertyNames] )
.css( [propertyName], [value] )
.css( [propertName], [function] )
.css( [properties] ) +#### .css( [propertyName] )
.css( [ propertyNames] )
.css( [propertyName], [value] )
.css( [propertyName], [function] )
.css( [properties] ) Get the value of a style property for the first element in the set of matched elements or set one or more CSS properties for every matched element. From 320847eba18e2e3249a71c470a24ef1ade49e51d Mon Sep 17 00:00:00 2001 From: Wahaj Dar Date: Thu, 29 Mar 2018 18:15:33 +0500 Subject: [PATCH 06/16] Added support for inherited props in .props(...) validation (#1146) * added support for inherited props in .props(...) validation * added test case for inherited props support --- lib/api/attributes.js | 2 +- test/api/attributes.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/api/attributes.js b/lib/api/attributes.js index 01a1dc9dfe..6abbb2f7dc 100644 --- a/lib/api/attributes.js +++ b/lib/api/attributes.js @@ -93,7 +93,7 @@ exports.attr = function(name, value) { var getProp = function (el, name) { if (!el || !isTag(el)) return; - return el.hasOwnProperty(name) + return name in el ? el[name] : rboolean.test(name) ? getAttr(el, name) !== undefined diff --git a/test/api/attributes.js b/test/api/attributes.js index 1f99e66a8d..61072dd9b9 100644 --- a/test/api/attributes.js +++ b/test/api/attributes.js @@ -130,10 +130,11 @@ describe('$(...)', function() { }); describe('.prop', function () { - var checkbox; + var checkbox, selectMenu; beforeEach(function () { $ = cheerio.load(inputs); + selectMenu = $('select'); checkbox = $('input[name=checkbox_on]'); }); @@ -186,6 +187,10 @@ describe('$(...)', function() { expect($(undefined).prop('prop')).to.be(undefined); expect($(null).prop('prop')).to.be(undefined); }); + + it('(inherited properties) : prop should support inherited properties', function() { + expect(selectMenu.prop('childNodes')).to.equal(selectMenu[0].childNodes); + }); }); describe('.data', function() { From 74be27115020fca7c08a1c177d3b6fc487b9194b Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Mon, 20 Aug 2018 22:22:00 +0200 Subject: [PATCH 07/16] Fix running test on Windows (#1209) Moves lint and test script call from Makefile to package.json. Makes the test run on all platforms. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc5a55ce73..91d4e0aa34 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,6 @@ "xyz": "~1.1.0" }, "scripts": { - "test": "make test" + "test": "jshint lib/ test/ && mocha --recursive --reporter dot" } } From ca6963c09a38eaf67fbe551634fdbbc6984d3da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= Date: Wed, 3 Oct 2018 17:55:28 +0100 Subject: [PATCH 08/16] v1.0.0 (#1145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use parse5 as a default parser (closes #863) * Use documents via $.load * Add test for #997 * Change options format * Update unit test Update test phrasing according to recent changes in parsing logic. * 1.0.0-rc.1 * Improve release process Limit responsibility of "pre-publish" script to simply validate the project's `History.md` file (by verifying an entry for the current release). Define a separate script for history generation. Separating the workflow in this way allows for manual modification of the release notes. * Correct errors in Readme.md * Document advanced usage with htmlparser2 * Update History.md (and include migration guide) * Remove documentation for `xmlMode` option Simply expose an option named `xml` that enables XML parsing via htmlparser2 with the ability to specify additional options for that parser. * Rename `useHtmlParser2` option This flag is used to control parsing behavior internally, but it is not intended for use by consumers. Prefix the name with an underscore in order to discourage users from relying on it. * Re-write migration guide for version 1.0 (#1078) Incorporate recent feedback from consumers who have experimented with the version 1.0 release candidate. * Pass locationInfo option to parse5 (#1155) * Update css-select to the latest version 🚀 (#1158) Breaking change: Invalid selectors now throw Errors, not SyntaxErrors. * Use eslint & prettier, add precommit hook (#1152) * chore(package): update mocha to version 5.0.4 (#1088) * Ensure text nodes expose the DOM level 1 API Since enabling the `withDomLvl1` parsing option, nodes cannot be created with an object literal. Create new text nodes using the `evaluate` function to ensure they expose the correct attributes. * fixing missing prop(‘outerHTML’) implementation.
Added an ‘outerHTML’ case to the switch in the prop function, which wraps a clone of in a container element, and sets to that container's HTML (#945) * Do not lint files excluded from version control (#1162) This includes code coverage reports as generated by the command `make test-cov`. * Correct typo in git hook configuration (#1163) * Correct typo in git hook configuration * Reformat package manifest to satisfy linter * Fix .text with a function as the argument * Fix `.text` being called on a collection with size > 1 with a function * chore(package): update coveralls to version 3.0.0 (#1086) * Update jsdom to the latest version 🚀 (#1008) * Throw a useful error on invalid input to cheerio.load() (#1087) * Procedurally generate API documentation from source (#1165) * Use parse5 to serialize the DOM, use lodash to clone dom * Fix DoS/RCE vulnerability in lodash@4.15.0 (#1179) fixes #1175 * Add eslint-plugin-jsdoc, improve documentation (#1168) * Improve variable names (#1183) Promote consistency in variable names within the project's source and unit tests. This helps to highlight the distinction between the object exported by the module and the function produced by the `load` method. The latter value is expected to mimic the jQuery API, while the former value generally should only be used for a small set of methods specific to Cheerio: - `load` - `html` - `xml` - `text` Other usages of the exported object are discouraged, and a future patch will update the unit tests to reflect the usages that are endorsed for long-term stability. * Formally test deprecated APIs (#1184) Methods named `load`, `html`, `xml`, and `text` are defined in many locations: Today, Cheerio defines multiple versions of methods named `load`, `html`, `xml`, `text`, and `parseHTML`. These alternate versions may be defined in up to three distinct parts of the API: - exported by the Cheerio module - as static methods of the "loaded" Cheerio factory function - as instance methods of the "loaded" Cheerio factory function Some of these are surperfluous, and because some unecessarily conflict with idiomatic jQuery coding patterns, they have been designated for future removal [1]. Add tests for the deprecated methods in order to avoid regressions prior to their removal. Insert comments to delineate the methods which are endorsed and which have been deprecated. For the latter group of methods, include recommendation for the preferred alternatives. [1] https://github.com/cheeriojs/cheerio/issues/1122 * Implement for...of iterator via Symbol.iterator (#1197) * Implement for...of iterator via Symbol.iterator Similar to jQuery: https://github.com/jquery/jquery/blob/1ea092a54b00aa4d902f4e22ada3854d195d4a18/src/core.js#L371-L373 Fixes #1191 * Assert that the iterator ends https://github.com/cheeriojs/cheerio/pull/1197#discussion_r187807553 --- .eslintrc.json | 31 ++ .gitignore | 1 + .jshintrc | 11 - .travis.yml | 6 +- CONTRIBUTING.md | 56 +-- History.md | 95 ++++ Makefile | 17 +- Readme.md | 850 ++------------------------------- benchmark/.eslintrc.json | 5 + benchmark/benchmark.js | 26 +- benchmark/suite.js | 14 +- jsdoc-config.json | 11 + lib/api/attributes.js | 307 +++++++++--- lib/api/css.js | 89 ++-- lib/api/forms.js | 58 ++- lib/api/manipulation.js | 463 +++++++++++++++--- lib/api/traversing.js | 678 +++++++++++++++++++++++---- lib/cheerio.js | 82 ++-- lib/options.js | 18 + lib/parse.js | 36 +- lib/static.js | 148 ++++-- lib/utils.js | 90 ++-- package.json | 50 +- scripts/generate_history.sh | 30 ++ scripts/prepublish | 26 +- test/.eslintrc.json | 5 + test/.jshintrc | 9 - test/api/attributes.js | 160 ++++--- test/api/css.js | 25 +- test/api/forms.js | 59 ++- test/api/manipulation.js | 904 ++++++++++++++++++++++++++++-------- test/api/traversing.js | 281 ++++++----- test/api/utils.js | 172 ++++--- test/cheerio.js | 404 ++++++++++------ test/fixtures.js | 30 +- test/parse.js | 199 ++++++-- test/xml.js | 37 +- 37 files changed, 3424 insertions(+), 2059 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 .jshintrc create mode 100644 benchmark/.eslintrc.json create mode 100644 jsdoc-config.json create mode 100644 lib/options.js create mode 100755 scripts/generate_history.sh create mode 100644 test/.eslintrc.json delete mode 100644 test/.jshintrc diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000..0db30f8ce8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "env": { + "node": true + }, + "plugins": ["jsdoc"], + "extends": ["eslint:recommended", "prettier"], + "globals": {}, + "rules": { + "indent": [ + 2, + 2, + { + "SwitchCase": 1, + "VariableDeclarator": 2 + } + ], + "no-eq-null": 0, + "no-proto": 2, + + "jsdoc/check-param-names": 2, + "jsdoc/check-tag-names": 2, + "jsdoc/check-types": 2, + "jsdoc/newline-after-description": 2, + "jsdoc/require-description-complete-sentence": 2, + "jsdoc/require-hyphen-before-param-description": 2, + "jsdoc/require-param": 2, + "jsdoc/require-param-description": 2, + "jsdoc/require-param-name": 2, + "jsdoc/require-param-type": 2 + } +} diff --git a/.gitignore b/.gitignore index 2ae05e1c97..46b866b577 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ lib-cov cover_html .c9revisions coverage +/docs/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 28a0ff23f0..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "indent": 2, - "eqnull": true, - "laxbreak": true, - "proto": true, - "undef": true, - "unused": true, - "node": true, - "quotmark": "single", - "shadow": "outer" -} diff --git a/.travis.yml b/.travis.yml index b7d7df23f8..8db0083269 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,11 @@ language: node_js node_js: - "stable" - - "unstable" + - "8" - "6" - - "4" - - "0.12" script: make travis-test matrix: fast_finish: true - allow_failures: - - node_js: unstable include: - env: BENCHMARK=true script: "node benchmark/benchmark.js --regex '^(?!.*highmem)'" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc38bb8dd7..24f458fdee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,16 +3,16 @@ Thanks for your interest in contributing to the project! Here's a rundown of how we'd like to work with you: -1. File an issue on GitHub describing the contribution you'd like to make. This - will help us to get you started on the right foot. -2. Create a single commit that addresses the issue: - 1. Follow the project's code style (see below) - 2. Add enough unit tests to "prove" that your patch is correct - 3. Update the project documentation as needed (see below) - 4. Describe your approach with as much detail as necessary in the git - commit message -3. Open a pull request, and reference the initial issue in the pull request - message. +1. File an issue on GitHub describing the contribution you'd like to make. This + will help us to get you started on the right foot. +2. Create a single commit that addresses the issue: + 1. Follow the project's code style (see below) + 2. Add enough unit tests to "prove" that your patch is correct + 3. Update the project documentation as needed (see below) + 4. Describe your approach with as much detail as necessary in the git + commit message +3. Open a pull request, and reference the initial issue in the pull request + message. # Documentation @@ -22,26 +22,16 @@ care to note aspects that make Cheerio distinct. # Code Style -This section is by no means complete. For undocumented stylistic choices, -please try to maintain consistency with the code base. - -- Single quotes: `'` -- Whitespace - - Two-space "soft" tabs - - Once space following control flow statements (`if (condition) {` rather - than `if(condition) {`) - - Remove trailing spaces - - [End each file with a newline - character.](https://github.com/editorconfig/editorconfig/wiki/Newline-at-End-of-File-Support) -- Terminate every statement with a semicolon -- Private functionality (for re-using functionality that isn't part of the - jQuery API) - - *Static methods*: If the functionality does not require a reference to a - Cheerio instance, simply define a named function within the module it is - needed. - - *Instance methods*: If the functionality requires a reference to a Cheerio - instance, informally define the method as "private" using the following - conventions: - - Define the method as a function on the Cheerio prototype - - Prefix the method name with an underscore (`_`) character - - Include `@api private` in the code comment the documents the method +Please make sure commit hooks are run, which will enforce the code style. + +When implementing private functionality that isn't part of the jQuery API, please opt for: + +* _Static methods_: If the functionality does not require a reference to a + Cheerio instance, simply define a named function within the module it is + needed. +* _Instance methods_: If the functionality requires a reference to a Cheerio + instance, informally define the method as "private" using the following + conventions: + * Define the method as a function on the Cheerio prototype + * Prefix the method name with an underscore (`_`) character + * Include `@api private` in the code comment the documents the method diff --git a/History.md b/History.md index c7e38e66a7..462ae0cf28 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,98 @@ +1.0.0-rc.2 / 2017-07-02 +================== + +This release changes Cheerio's default parser to [the Parse5 HTML +parser](https://github.com/inikulin/parse5). Parse5 is an excellent project +that rigorously conforms to the HTML standard. It does not support XML, so +Cheerio continues to use [`htmlparser2`](https://github.com/fb55/htmlparser2/) +when working with XML documents. + +This switch addresses many long-standing bugs in Cheerio, but some users may +experience slower behavior in performance-critical applications. In addition, +`htmlparser2` is more forgiving of invalid markup which can be useful when +input sourced from a third party and cannot be corrected. For these reasons, +the `load` method also accepts a DOM structure as produced by the `htmlparser2` +library. See the project's "readme" file for more details on this usage +pattern. + +### Migrating from version 0.x + +`cheerio.load( html[, options ] )` This method continues to act as a "factory" +function. It produces functions that define an API that is similar to the +global `jQuery` function provided by the jQuery library. The generated function +operates on a DOM structure based on the provided HTML. + +In releases prior to version 1.0, the provided HTML was interpreted as a +document fragment. Following version 1.0, strings provided to the `load` method +are interpreted as documents. The same example will produce a `$` function that +operates on a full HTML document, including an `` document element with +nested `` and `` tags. This mimics web browser behavior much more +closely, but may require alterations to existing code. + +For example, the following code will produce different results between 0.x and +1.0 releases: + + var $ = cheerio.load('

Hello, world!

'); + + $.root().html(); + + //=> In version 0.x: '

Hello, world!

' + //=> In version 1.0: '

Hello, world!

' + +Users wishing to parse, manipulate, and render full documents should not need +to modify their code. Likewise, code that does not interact with the "root" +element should not be effected by this change. (In the above example, the +expression `$('p')` returns the same result across Cheerio versions--a Cheerio +collection whose only member is a paragraph element.) + +However, users wishing to render document fragments should now explicitly +create a "wrapper" element to contain their input. + + // First, create a Cheerio function "bound" to an empty document (this is + // similar to loading an empty page in a web browser) + var $ = cheerio.load(''); + // Next, create a "wrapper" element for the input fragment: + var $wrapper = $('
'); + // Finally, supply the input markup as the content for the wrapper: + $wrapper.append('

Hello, world!

'); + + $wrapper.html(); + //=> '

Hello, world!

' + +--- + +Change log: + + * Update History.md (and include migration guide) (Mike Pennisi) + * Rename `useHtmlParser2` option (Mike Pennisi) + * Remove documentation for `xmlMode` option (Mike Pennisi) + * Document advanced usage with htmlparser2 (Mike Pennisi) + * Correct errors in Readme.md (Mike Pennisi) + * Improve release process (Mike Pennisi) + * 1.0.0-rc.1 (Mike Pennisi) + * Update unit test (Mike Pennisi) + * Normalize code style (Mike Pennisi) + * Added support for nested wrapping. (Diane Looney) + * Add nested wrapping test (Toni Helenius) + * Added $.merge following the specification at https://api.jquery.com/jquery.merge/ Added test cases for $.merge (Diane Looney) + * Clarify project scope in README file (Mike Pennisi) + * .text() ignores script and style tags (#1018) (Haleem Assal) + * Test suite housekeeping (#1016) (DianeLooney) + * experiment with a job board (Matthew) + * Change options format (inikulin) + * Add test for #997 (inikulin) + * Update .filter function docs. (Konstantin) + * Standardise readme on ES6 variable declarations (Dekatron) + * Use documents via $.load (inikulin) + * Use parse5 as a default parser (closes #863) (inikulin) + * Fix small typo in Readme (Darren Scerri) + * Report test failures in CI (Mike Pennisi) + * serializeArray should not ignore input elements without value attributes (Ricardo Gladwell) + * Disallow variable shadowing (Mike Pennisi) + * Update hasClass method (sufisaid) + * Added MIT License fixes #902 (Prasanth Vaaheeswaran) + * chore(package): update dependencies (greenkeeper[bot]) + * Use modular lodash package (#913) (Billy Janitsch) 0.22.0 / 2016-08-23 ================== diff --git a/Makefile b/Makefile index 3617d89b23..52edbb40a9 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ REPORTER = dot XYZ = node_modules/.bin/xyz --message 'Release X.Y.Z' --tag X.Y.Z --repo git@github.com:cheeriojs/cheerio.git --script scripts/prepublish +UPSTREAM = git@github.com:cheeriojs/cheerio.git lint: - @./node_modules/.bin/jshint lib/ test/ + @./node_modules/.bin/eslint --ignore-path .gitignore . test: lint @./node_modules/.bin/mocha --recursive --reporter $(REPORTER) @@ -36,4 +37,16 @@ release-patch: LEVEL = patch release-major release-minor release-patch: @$(XYZ) --increment $(LEVEL) -.PHONY: test build setup subl +docs: + @./node_modules/.bin/jsdoc --configure jsdoc-config.json + +publish-docs: docs + @cd docs; \ + rm -rf .git && \ + git init && \ + git add --all . && \ + git commit -m 'Generate documentation' && \ + git remote add upstream $(UPSTREAM) && \ + git push --force upstream master:gh-pages + +.PHONY: test build setup subl docs publish-docs diff --git a/Readme.md b/Readme.md index 822353f919..c351d8612d 100644 --- a/Readme.md +++ b/Readme.md @@ -30,7 +30,7 @@ $('h2.title').text('Hello there!') $('h2').addClass('welcome') $.html() -//=>

Hello there!

+//=>

Hello there!

``` ## Installation @@ -44,7 +44,7 @@ __ϟ Blazingly fast:__ Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient. Preliminary end-to-end benchmarks suggest that cheerio is about __8x__ faster than JSDOM. __❁ Incredibly flexible:__ -Cheerio wraps around @FB55's forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. +Cheerio wraps around [parse5](https://github.com/inikulin/parse5) parser and can optionally use @FB55's forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. ## Cheerio is not a web browser @@ -122,107 +122,6 @@ Does your company use Cheerio in production? Please consider [sponsoring this pr ## API -### Table of contents - -
- Selectors - - - [$( selector, [context], [root] )](#-selector-context-root-) -
-
- Attributes - - - [.attr( name, value )](#attr-name-value-) - - [.prop( name, value )](#prop-name-value-) - - [.data( name, value )](#data-name-value-) - - [.val( [value] )](#val-value-) - - [.removeAttr( name )](#removeattr-name-) - - [.hasClass( className )](#hasclass-classname-) - - [.addClass( className )](#addclass-classname-) - - [.removeClass( [className] )](#removeclass-classname-) - - [.toggleClass( className, [switch] )](#toggleclass-classname-switch-) - - [.is( selector )](#is-selector-) - - [.is( element )](#is-element-) - - [.is( selection )](#is-selection-) - - [.is( function(index) )](#is-functionindex-) -
-
- Forms - - - [.serialize()](#serialize) - - [.serializeArray()](#serializearray) -
-
- Traversing - - - [.find(selector)](#findselector) - - [.find(selection)](#findselection) - - [.find(node)](#findnode) - - [.parent([selector])](#parentselector) - - [.parents([selector])](#parentsselector) - - [.parentsUntil([selector][,filter])](#parentsuntilselectorfilter) - - [.closest(selector)](#closestselector) - - [.next([selector])](#nextselector) - - [.nextAll([selector])](#nextallselector) - - [.nextUntil([selector], [filter])](#nextuntilselector-filter) - - [.prev([selector])](#prevselector) - - [.prevAll([selector])](#prevallselector) - - [.prevUntil([selector], [filter])](#prevuntilselector-filter) - - [.slice( start, [end] )](#slice-start-end-) - - [.siblings([selector])](#siblingsselector) - - [.children([selector])](#childrenselector) - - [.contents()](#contents) - - [.each( function(index, element) )](#each-functionindex-element-) - - [.map( function(index, element) )](#map-functionindex-element-) - - [.filter( selector )
- .filter( selection )
- .filter( element )
- .filter( function(index, element) )](#filter-selector---filter-selection---filter-element---filter-functionindex-element-) - - [.not( selector )
- .not( selection )
- .not( element )
- .not( function(index, elem) )](#not-selector---not-selection---not-element---not-functionindex-elem-) - - [.has( selector )
- .has( element )](#has-selector---has-element-) - - [.first()](#first) - - [.last()](#last) - - [.eq( i )](#eq-i-) - - [.get( [i] )](#get-i-) - - [.index()](#index) - - [.index( selector )](#index-selector-) - - [.index( nodeOrSelection )](#index-nodeorselection-) - - [.end()](#end) - - [.add( selector [, context] )](#add-selector--context-) - - [.add( element )](#add-element-) - - [.add( elements )](#add-elements-) - - [.add( html )](#add-html-) - - [.add( selection )](#add-selection-) - - [.addBack( [filter] )](#addback-filter-) -
-
- Manipulation - - - [.append( content, [content, ...] )](#append-content-content--) - - [.appendTo( target )](#appendto-target-) - - [.prepend( content, [content, ...] )](#prepend-content-content--) - - [.prependTo( target )](#prependto-target-) - - [.after( content, [content, ...] )](#after-content-content--) - - [.insertAfter( target )](#insertafter-target-) - - [.before( content, [content, ...] )](#before-content-content--) - - [.insertBefore( target )](#insertbefore-target-) - - [.remove( [selector] )](#remove-selector-) - - [.replaceWith( content )](#replacewith-content-) - - [.empty()](#empty) - - [.html( [htmlString] )](#html-htmlstring-) - - [.text( [textString] )](#text-textstring-) - - [.wrap( content )](#wrap-content-) - - [.css( [propertName] )
- .css( [ propertyNames] )
- .css( [propertyName], [value] )
- .css( [propertName], [function] )
- .css( [properties] )](#css-propertname---css--propertynames---css-propertyname-value---css-propertname-function---css-properties-) -
- ### Markup example we'll be using: ```html @@ -259,31 +158,53 @@ const $ = require('cheerio'); $('li', 'ul', '
    ...
'); ``` -You can also pass an extra object to `.load()` if you need to modify any -of the default parsing options: +If you need to modify parsing options for XML input, you may pass an extra +object to `.load()`: ```js const $ = cheerio.load('
    ...
', { - normalizeWhitespace: true, - xmlMode: true + xml: { + normalizeWhitespace: true, + } }); ``` -These parsing options are taken directly from [htmlparser2](https://github.com/fb55/htmlparser2/wiki/Parser-options), therefore any options that can be used in `htmlparser2` are valid in cheerio as well. The default options are: +The options in the `xml` object are taken directly from [htmlparser2](https://github.com/fb55/htmlparser2/wiki/Parser-options), therefore any options that can be used in `htmlparser2` are valid in cheerio as well. The default options are: ```js { withDomLvl1: true, normalizeWhitespace: false, - xmlMode: false, + xmlMode: true, decodeEntities: true } - ``` For a full list of options and their effects, see [this](https://github.com/fb55/DomHandler) and [htmlparser2's options](https://github.com/fb55/htmlparser2/wiki/Parser-options). +Some users may wish to parse markup with the `htmlparser2` library, and +traverse/manipulate the resulting structure with Cheerio. This may be the case +for those upgrading from pre-1.0 releases of Cheerio (which relied on +`htmlparser2`), for those dealing with invalid markup (because `htmlparser2` is +more forgiving), or for those operating in performance-critical situations +(because `htmlparser2` may be faster in some cases). Note that "more forgiving" +means `htmlparser2` has error-correcting mechanisms that aren't always a match +for the standards observed by web browsers. This behavior may be useful when +parsing non-HTML content. + +To support these cases, `load` also accepts a `htmlparser2`-compatible data +structure as its first argument. Users may install `htmlparser2`, use it to +parse input, and pass the result to `load`: + +```js +// Usage as of htmlparser2 version 3: +const htmlparser2 = require('htmlparser2'); +const dom = htmlparser2.parseDOM(document, options); + +const $ = cheerio.load(dom); +``` + ### Selectors Cheerio's selector implementation is nearly identical to jQuery's, so the API is very similar. @@ -311,677 +232,6 @@ You can select with XML Namespaces but [due to the CSS specification](https://ww $('[xml\\:id="main"'); ``` -### Attributes -Methods for getting and modifying attributes. - -#### .attr( name, value ) -Method for getting and setting attributes. Gets the attribute value for only the first element in the matched set. If you set an attribute's value to `null`, you remove that attribute. You may also pass a `map` and `function` like jQuery. - -```js -$('ul').attr('id') -//=> fruits - -$('.apple').attr('id', 'favorite').html() -//=>
  • Apple
  • -``` - -> See http://api.jquery.com/attr/ for more information - -#### .prop( name, value ) -Method for getting and setting properties. Gets the property value for only the first element in the matched set. - -```js -$('input[type="checkbox"]').prop('checked') -//=> false - -$('input[type="checkbox"]').prop('checked', true).val() -//=> ok -``` - -> See http://api.jquery.com/prop/ for more information - -#### .data( name, value ) -Method for getting and setting data attributes. Gets or sets the data attribute value for only the first element in the matched set. - -```js -$('
    ').data() -//=> { appleColor: 'red' } - -$('
    ').data('apple-color') -//=> 'red' - -const apple = $('.apple').data('kind', 'mac') -apple.data('kind') -//=> 'mac' -``` - -> See http://api.jquery.com/data/ for more information - -#### .val( [value] ) -Method for getting and setting the value of input, select, and textarea. Note: Support for `map`, and `function` has not been added yet. - -```js -$('input[type="text"]').val() -//=> input_text - -$('input[type="text"]').val('test').html() -//=> -``` - -#### .removeAttr( name ) -Method for removing attributes by `name`. - -```js -$('.pear').removeAttr('class').html() -//=>
  • Pear
  • -``` - -#### .hasClass( className ) -Check to see if *any* of the matched elements have the given `className`. - -```js -$('.pear').hasClass('pear') -//=> true - -$('apple').hasClass('fruit') -//=> false - -$('li').hasClass('pear') -//=> true -``` - -#### .addClass( className ) -Adds class(es) to all of the matched elements. Also accepts a `function` like jQuery. - -```js -$('.pear').addClass('fruit').html() -//=>
  • Pear
  • - -$('.apple').addClass('fruit red').html() -//=>
  • Apple
  • -``` - -> See http://api.jquery.com/addClass/ for more information. - -#### .removeClass( [className] ) -Removes one or more space-separated classes from the selected elements. If no `className` is defined, all classes will be removed. Also accepts a `function` like jQuery. - -```js -$('.pear').removeClass('pear').html() -//=>
  • Pear
  • - -$('.apple').addClass('red').removeClass().html() -//=>
  • Apple
  • -``` - -> See http://api.jquery.com/removeClass/ for more information. - -#### .toggleClass( className, [switch] ) -Add or remove class(es) from the matched elements, depending on either the class's presence or the value of the switch argument. Also accepts a `function` like jQuery. - -```js -$('.apple.green').toggleClass('fruit green red').html() -//=>
  • Apple
  • - -$('.apple.green').toggleClass('fruit green red', true).html() -//=>
  • Apple
  • -``` - -> See http://api.jquery.com/toggleClass/ for more information. - -#### .is( selector ) -#### .is( element ) -#### .is( selection ) -#### .is( function(index) ) -Checks the current list of elements and returns `true` if _any_ of the elements match the selector. If using an element or Cheerio selection, returns `true` if _any_ of the elements match. If using a predicate function, the function is executed in the context of the selected element, so `this` refers to the current element. - -### Forms - -#### .serialize() - -Encodes a set of form elements as a URL query string. - -```js -$('
    ').serialize() -//=> foo=bar&foo=qux -``` - -#### .serializeArray() - -Encode a set of form elements as an array of names and values. - -```js -$('
    ').serializeArray() -//=> [ { name: 'foo', value: 'bar' } ] -``` - -### Traversing - -#### .find(selector) -#### .find(selection) -#### .find(node) -Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. - -```js -$('#fruits').find('li').length -//=> 3 -$('#fruits').find($('.apple')).length -//=> 1 -``` - -#### .parent([selector]) -Get the parent of each element in the current set of matched elements, optionally filtered by a selector. - -```js -$('.pear').parent().attr('id') -//=> fruits -``` - -#### .parents([selector]) -Get a set of parents filtered by `selector` of each element in the current set of match elements. -```js -$('.orange').parents().length -// => 2 -$('.orange').parents('#fruits').length -// => 1 -``` - -#### .parentsUntil([selector][,filter]) -Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or cheerio object. -```js -$('.orange').parentsUntil('#food').length -// => 1 -``` - -#### .closest(selector) -For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. - -```js -$('.orange').closest() -// => [] -$('.orange').closest('.apple') -// => [] -$('.orange').closest('li') -// => [
  • Orange
  • ] -$('.orange').closest('#fruits') -// => [
      ...
    ] -``` - -#### .next([selector]) -Gets the next sibling of the first selected element, optionally filtered by a selector. - -```js -$('.apple').next().hasClass('orange') -//=> true -``` - -#### .nextAll([selector]) -Gets all the following siblings of the first selected element, optionally filtered by a selector. - -```js -$('.apple').nextAll() -//=> [
  • Orange
  • ,
  • Pear
  • ] -$('.apple').nextAll('.orange') -//=> [
  • Orange
  • ] -``` - -#### .nextUntil([selector], [filter]) -Gets all the following siblings up to but not including the element matched by the selector, optionally filtered by another selector. - -```js -$('.apple').nextUntil('.pear') -//=> [
  • Orange
  • ] -``` - -#### .prev([selector]) -Gets the previous sibling of the first selected element optionally filtered by a selector. - -```js -$('.orange').prev().hasClass('apple') -//=> true -``` - -#### .prevAll([selector]) -Gets all the preceding siblings of the first selected element, optionally filtered by a selector. - -```js -$('.pear').prevAll() -//=> [
  • Orange
  • ,
  • Apple
  • ] -$('.pear').prevAll('.orange') -//=> [
  • Orange
  • ] -``` - -#### .prevUntil([selector], [filter]) -Gets all the preceding siblings up to but not including the element matched by the selector, optionally filtered by another selector. - -```js -$('.pear').prevUntil('.apple') -//=> [
  • Orange
  • ] -``` - -#### .slice( start, [end] ) -Gets the elements matching the specified range - -```js -$('li').slice(1).eq(0).text() -//=> 'Orange' - -$('li').slice(1, 2).length -//=> 1 -``` - -#### .siblings([selector]) -Gets the first selected element's siblings, excluding itself. - -```js -$('.pear').siblings().length -//=> 2 - -$('.pear').siblings('.orange').length -//=> 1 - -``` - -#### .children([selector]) -Gets the children of the first selected element. - -```js -$('#fruits').children().length -//=> 3 - -$('#fruits').children('.pear').text() -//=> Pear -``` - -#### .contents() -Gets the children of each element in the set of matched elements, including text and comment nodes. - -```js -$('#fruits').contents().length -//=> 3 -``` - -#### .each( function(index, element) ) -Iterates over a cheerio object, executing a function for each matched element. When the callback is fired, the function is fired in the context of the DOM element, so `this` refers to the current element, which is equivalent to the function parameter `element`. To break out of the `each` loop early, return with `false`. - -```js -const fruits = []; - -$('li').each(function(i, elem) { - fruits[i] = $(this).text(); -}); - -fruits.join(', '); -//=> Apple, Orange, Pear -``` - -#### .map( function(index, element) ) -Pass each element in the current matched set through a function, producing a new Cheerio object containing the return values. The function can return an individual data item or an array of data items to be inserted into the resulting set. If an array is returned, the elements inside the array are inserted into the set. If the function returns null or undefined, no element will be inserted. - -```js -$('li').map(function(i, el) { - // this === el - return $(this).text(); -}).get().join(' '); -//=> "apple orange pear" -``` - -#### .filter( selector )
    .filter( selection )
    .filter( element )
    .filter( function(index, element) ) - -Iterates over a cheerio object, reducing the set of selector elements to those that match the selector or pass the function's test. When a Cheerio selection is specified, return only the elements contained in that selection. When an element is specified, return only that element (if it is contained in the original selection). If using the function method, the function is executed in the context of the selected element, so `this` refers to the current element. - -Selector: - -```js -$('li').filter('.orange').attr('class'); -//=> orange -``` - -Function: - -```js -$('li').filter(function(i, el) { - // this === el - return $(this).attr('class') === 'orange'; -}).attr('class') -//=> orange -``` - -#### .not( selector )
    .not( selection )
    .not( element )
    .not( function(index, elem) ) - -Remove elements from the set of matched elements. Given a jQuery object that represents a set of DOM elements, the `.not()` method constructs a new jQuery object from a subset of the matching elements. The supplied selector is tested against each element; the elements that don't match the selector will be included in the result. The `.not()` method can take a function as its argument in the same way that `.filter()` does. Elements for which the function returns true are excluded from the filtered set; all other elements are included. - -Selector: - -```js -$('li').not('.apple').length; -//=> 2 -``` - -Function: - -```js -$('li').not(function(i, el) { - // this === el - return $(this).attr('class') === 'orange'; -}).length; -//=> 2 -``` - -#### .has( selector )
    .has( element ) - -Filters the set of matched elements to only those which have the given DOM element as a descendant or which have a descendant that matches the given selector. Equivalent to `.filter(':has(selector)')`. - -Selector: - -```js -$('ul').has('.pear').attr('id'); -//=> fruits -``` - -Element: - -```js -$('ul').has($('.pear')[0]).attr('id'); -//=> fruits -``` - -#### .first() -Will select the first element of a cheerio object - -```js -$('#fruits').children().first().text() -//=> Apple -``` - -#### .last() -Will select the last element of a cheerio object - -```js -$('#fruits').children().last().text() -//=> Pear -``` - -#### .eq( i ) -Reduce the set of matched elements to the one at the specified index. Use `.eq(-i)` to count backwards from the last selected element. - -```js -$('li').eq(0).text() -//=> Apple - -$('li').eq(-1).text() -//=> Pear -``` - -#### .get( [i] ) - -Retrieve the DOM elements matched by the Cheerio object. If an index is specified, retrieve one of the elements matched by the Cheerio object: - -```js -$('li').get(0).tagName -//=> li -``` - -If no index is specified, retrieve all elements matched by the Cheerio object: - -```js -$('li').get().length -//=> 3 -``` - -#### .index() -#### .index( selector ) -#### .index( nodeOrSelection ) - -Search for a given element from among the matched elements. - -```js -$('.pear').index() -//=> 2 -$('.orange').index('li') -//=> 1 -$('.apple').index($('#fruit, li')) -//=> 1 -``` - -#### .end() -End the most recent filtering operation in the current chain and return the set of matched elements to its previous state. - -```js -$('li').eq(0).end().length -//=> 3 -``` - -#### .add( selector [, context] ) -#### .add( element ) -#### .add( elements ) -#### .add( html ) -#### .add( selection ) -Add elements to the set of matched elements. - -```js -$('.apple').add('.orange').length -//=> 2 -``` - -#### .addBack( [filter] ) - -Add the previous set of elements on the stack to the current set, optionally filtered by a selector. - -```js -$('li').eq(0).addBack('.orange').length -//=> 2 -``` - -### Manipulation -Methods for modifying the DOM structure. - -#### .append( content, [content, ...] ) -Inserts content as the *last* child of each of the selected elements. - -```js -$('ul').append('
  • Plum
  • ') -$.html() -//=>
      -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    • Plum
    • -//
    -``` - -#### .appendTo( target ) -Insert every element in the set of matched elements to the end of the target. - -```js -$('
  • Plum
  • ').appendTo('#fruits') -$.html() -//=>
      -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    • Plum
    • -//
    -``` - -#### .prepend( content, [content, ...] ) -Inserts content as the *first* child of each of the selected elements. - -```js -$('ul').prepend('
  • Plum
  • ') -$.html() -//=>
      -//
    • Plum
    • -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .prependTo( target ) -Insert every element in the set of matched elements to the beginning of the target. - -```js -$('
  • Plum
  • ').prependTo('#fruits') -$.html() -//=>
      -//
    • Plum
    • -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .after( content, [content, ...] ) -Insert content next to each element in the set of matched elements. - -```js -$('.apple').after('
  • Plum
  • ') -$.html() -//=>
      -//
    • Apple
    • -//
    • Plum
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .insertAfter( target ) -Insert every element in the set of matched elements after the target. - -```js -$('
  • Plum
  • ').insertAfter('.apple') -$.html() -//=>
      -//
    • Apple
    • -//
    • Plum
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .before( content, [content, ...] ) -Insert content previous to each element in the set of matched elements. - -```js -$('.apple').before('
  • Plum
  • ') -$.html() -//=>
      -//
    • Plum
    • -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .insertBefore( target ) -Insert every element in the set of matched elements before the target. - -```js -$('
  • Plum
  • ').insertBefore('.apple') -$.html() -//=>
      -//
    • Plum
    • -//
    • Apple
    • -//
    • Orange
    • -//
    • Pear
    • -//
    -``` - -#### .remove( [selector] ) -Removes the set of matched elements from the DOM and all their children. `selector` filters the set of matched elements to be removed. - -```js -$('.pear').remove() -$.html() -//=>
      -//
    • Apple
    • -//
    • Orange
    • -//
    -``` - -#### .replaceWith( content ) -Replaces matched elements with `content`. - -```js -const plum = $('
  • Plum
  • ') -$('.pear').replaceWith(plum) -$.html() -//=>
      -//
    • Apple
    • -//
    • Orange
    • -//
    • Plum
    • -//
    -``` - -#### .empty() -Empties an element, removing all its children. - -```js -$('ul').empty() -$.html() -//=>
      -``` - -#### .html( [htmlString] ) -Gets an html content string from the first selected element. If `htmlString` is specified, each selected element's content is replaced by the new content. - -```js -$('.orange').html() -//=> Orange - -$('#fruits').html('
    • Mango
    • ').html() -//=>
    • Mango
    • -``` - -#### .text( [textString] ) -Get the combined text contents of each element in the set of matched elements, including their descendants. If `textString` is specified, each selected element's content is replaced by the new text content. - -```js -$('.orange').text() -//=> Orange - -$('ul').text() -//=> Apple -// Orange -// Pear -``` - -#### .wrap( content ) -The .wrap() function can take any string or object that could be passed to the $() factory function to specify a DOM structure. This structure may be nested several levels deep, but should contain only one inmost element. A copy of this structure will be wrapped around each of the elements in the set of matched elements. This method returns the original set of elements for chaining purposes. - -```js -const redFruit = $('
      ') -$('.apple').wrap(redFruit) - -//=>
        -//
        -//
      • Apple
      • -//
        -//
      • Orange
      • -//
      • Plum
      • -//
      - -const healthy = $('
      ') -$('li').wrap(healthy) - -//=>
        -//
        -//
      • Apple
      • -//
        -//
        -//
      • Orange
      • -//
        -//
        -//
      • Plum
      • -//
        -//
      -``` - -#### .css( [propertyName] )
      .css( [ propertyNames] )
      .css( [propertyName], [value] )
      .css( [propertyName], [function] )
      .css( [properties] ) - -Get the value of a style property for the first element in the set of matched elements or set one or more CSS properties for every matched element. - ### Rendering When you're ready to render the document, you can use the `html` utility function: @@ -1030,44 +280,6 @@ cheerio.text($('div')) //=> This is content. ``` -### Miscellaneous -DOM element methods that don't fit anywhere else - -#### .toArray() -Retrieve all the DOM elements contained in the jQuery set as an array. - -```js -$('li').toArray() -//=> [ {...}, {...}, {...} ] -``` - -#### .clone() #### -Clone the cheerio object. - -```js -const moreFruit = $('#fruits').clone() -``` - -### Utilities - -#### $.root - -Sometimes you need to work with the top-level root element. To query it, you can use `$.root()`. - -```js -$.root().append('
        ').html(); -//=>
          ...
          -``` - -#### $.contains( container, contained ) -Checks to see if the `contained` DOM element is a descendant of the `container` DOM element. - -#### $.parseHTML( data [, context ] [, keepScripts ] ) -Parses a string into an array of DOM nodes. The `context` argument has no meaning for Cheerio, but it is maintained for API compatability. - -#### $.load( html[, options ] ) -Load in the HTML. (See the previous section titled "Loading" for more information.) - ### Plugins Once you have loaded a document, you may extend the prototype or the equivalent `fn` property with custom plugin methods: diff --git a/benchmark/.eslintrc.json b/benchmark/.eslintrc.json new file mode 100644 index 0000000000..7753f32e77 --- /dev/null +++ b/benchmark/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": 0 + } +} diff --git a/benchmark/benchmark.js b/benchmark/benchmark.js index 6d1825d94a..1aa0fdf53a 100755 --- a/benchmark/benchmark.js +++ b/benchmark/benchmark.js @@ -16,10 +16,14 @@ if (process.argv.indexOf('--cheerio-only') >= 0) { } suites.add('Select all', 'jquery.html', { - test: function($) { $('*').length; } + test: function($) { + $('*').length; + } }); suites.add('Select some', 'jquery.html', { - test: function($) { $('li').length; } + test: function($) { + $('li').length; + } }); /* @@ -75,7 +79,7 @@ suites.add('manipulation - replaceWith', 'jquery.html', { setup: function($) { $('body').append('
          '); }, - test: function($, $lis) { + test: function($) { $('#foo').replaceWith('
          '); } }); @@ -107,12 +111,14 @@ suites.add('manipulation - html render', 'jquery.html', { }); suites.add('manipulation - html independent', 'jquery.html', { setup: function() { - return '
          bat
          baz
          ' - + '
          bat
          baz
          ' - + '
          bat
          baz
          ' - + '
          bat
          baz
          ' - + '
          bat
          baz
          ' - + '
          bat
          baz
          '; + return ( + '
          bat
          baz
          ' + + '
          bat
          baz
          ' + + '
          bat
          baz
          ' + + '
          bat
          baz
          ' + + '
          bat
          baz
          ' + + '
          bat
          baz
          ' + ); }, test: function($, content) { $(content).html(); @@ -128,7 +134,6 @@ suites.add('manipulation - text', 'jquery.html', { } }); - /* * Traversing Tests */ @@ -261,7 +266,6 @@ suites.add('traversing - Eq', 'jquery.html', { } }); - /* * Attributes Tests */ diff --git a/benchmark/suite.js b/benchmark/suite.js index da029ad7ec..b5d2cf863a 100644 --- a/benchmark/suite.js +++ b/benchmark/suite.js @@ -2,15 +2,18 @@ var fs = require('fs'); var path = require('path'); var Benchmark = require('benchmark'); -var jsdom = require('jsdom'); +var jsdom = require('jsdom/lib/old-api.js'); var cheerio = require('..'); var documentDir = path.join(__dirname, 'documents'); -var jQuerySrc = path.join(__dirname, '../node_modules/jquery/dist/jquery.slim.js'); +var jQuerySrc = path.join( + __dirname, + '../node_modules/jquery/dist/jquery.slim.js' +); var filterRe = /./; var cheerioOnly = false; -var Suites = module.exports = function() {}; +var Suites = (module.exports = function() {}); Suites.prototype.filter = function(str) { filterRe = new RegExp(str, 'i'); @@ -21,15 +24,14 @@ Suites.prototype.cheerioOnly = function() { }; Suites.prototype.add = function(name, fileName, options) { - var markup, suite, testFn; + var markup, suite; if (!filterRe.test(name)) { return; } markup = fs.readFileSync(path.join(documentDir, fileName), 'utf8'); suite = new Benchmark.Suite(name); - testFn = options.test; - suite.on('start', function(event) { + suite.on('start', function() { console.log('Test: ' + name + ' (file: ' + fileName + ')'); }); suite.on('cycle', function(event) { diff --git a/jsdoc-config.json b/jsdoc-config.json new file mode 100644 index 0000000000..01e9af8b19 --- /dev/null +++ b/jsdoc-config.json @@ -0,0 +1,11 @@ +{ + "opts": { + "destination": "./docs", + "readme": "Readme.md", + "recurse": true + }, + "plugins": ["plugins/markdown"], + "source": { + "include": ["lib"] + } +} diff --git a/lib/api/attributes.js b/lib/api/attributes.js index 6abbb2f7dc..3ce6a90706 100644 --- a/lib/api/attributes.js +++ b/lib/api/attributes.js @@ -1,4 +1,9 @@ -var $ = require('../static'), +/** + * Methods for getting and modifying attributes. + * + * @module attributes + */ +var text = require('../static').text, utils = require('../utils'), isTag = utils.isTag, domEach = utils.domEach, @@ -12,20 +17,17 @@ var $ = require('../static'), extend: require('lodash/assignIn'), some: require('lodash/some') }, - - // Lookup table for coercing string data-* attributes to their corresponding - // JavaScript primitives - primitives = { - null: null, - true: true, - false: false - }, - - // Attributes that are booleans - rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - // Matches strings that look like JSON objects or arrays - rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; - + // Lookup table for coercing string data-* attributes to their corresponding + // JavaScript primitives + primitives = { + null: null, + true: true, + false: false + }, + // Attributes that are booleans + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + // Matches strings that look like JSON objects or arrays + rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; var getAttr = function(elem, name) { if (!elem || !isTag(elem)) return; @@ -46,26 +48,46 @@ var getAttr = function(elem, name) { // Mimic the DOM and return text content as value for `option's` if (elem.name === 'option' && name === 'value') { - return $.text(elem.children); + return text(elem.children); } // Mimic DOM with default value for radios/checkboxes - if (elem.name === 'input' && - (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') && - name === 'value') { + if ( + elem.name === 'input' && + (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') && + name === 'value' + ) { return 'on'; } }; var setAttr = function(el, name, value) { - if (value === null) { removeAttribute(el, name); } else { - el.attribs[name] = value+''; + el.attribs[name] = value + ''; } }; +/** + * Method for getting and setting attributes. Gets the attribute value for only + * the first element in the matched set. If you set an attribute's value to + * `null`, you remove that attribute. You may also pass a `map` and `function` + * like jQuery. + * + * @example + * + * $('ul').attr('id') + * //=> fruits + * + * $('.apple').attr('id', 'favorite').html() + * //=>
        • Apple
        • + * + * @param {string} name - Name of the attribute. + * @param {string} [value] - If specified sets the value of the attribute. + * + * @see {@link http://api.jquery.com/attr/} + */ exports.attr = function(name, value) { // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { @@ -90,31 +112,45 @@ exports.attr = function(name, value) { return getAttr(this[0], name); }; -var getProp = function (el, name) { +var getProp = function(el, name) { if (!el || !isTag(el)) return; return name in el - ? el[name] - : rboolean.test(name) - ? getAttr(el, name) !== undefined - : getAttr(el, name); + ? el[name] + : rboolean.test(name) ? getAttr(el, name) !== undefined : getAttr(el, name); }; -var setProp = function (el, name, value) { +var setProp = function(el, name, value) { el[name] = rboolean.test(name) ? !!value : value; }; -exports.prop = function (name, value) { +/** + * Method for getting and setting properties. Gets the property value for only + * the first element in the matched set. + * + * @example + * + * $('input[type="checkbox"]').prop('checked') + * //=> false + * + * $('input[type="checkbox"]').prop('checked', true).val() + * //=> ok + * + * @param {string} name - Name of the property. + * @param {any} [value] - If specified set the property to this. + * + * @see {@link http://api.jquery.com/prop/} + */ +exports.prop = function(name, value) { var i = 0, property; if (typeof name === 'string' && value === undefined) { - switch (name) { case 'style': property = this.css(); - _.forEach(property, function (v, p) { + _.forEach(property, function(v, p) { property[i++] = p; }); @@ -125,6 +161,12 @@ exports.prop = function (name, value) { case 'nodeName': property = this[0].name.toUpperCase(); break; + case 'outerHTML': + property = this.clone() + .wrap('') + .parent() + .html(); + break; default: property = getProp(this[0], name); } @@ -133,7 +175,6 @@ exports.prop = function (name, value) { } if (typeof name === 'object' || value !== undefined) { - if (typeof value === 'function') { return domEach(this, function(j, el) { setProp(el, name, value.call(el, j, getProp(el, name))); @@ -144,16 +185,13 @@ exports.prop = function (name, value) { if (!isTag(el)) return; if (typeof name === 'object') { - _.forEach(name, function(val, key) { setProp(el, key, val); }); - } else { setProp(el, name, value); } }); - } }; @@ -201,7 +239,9 @@ var readData = function(el, name) { } else if (rbrace.test(value)) { try { value = JSON.parse(value); - } catch(e){ } + } catch (e) { + /* ignore */ + } } el.data[jsName] = value; @@ -211,6 +251,27 @@ var readData = function(el, name) { return readAll ? el.data : value; }; +/** + * Method for getting and setting data attributes. Gets or sets the data + * attribute value for only the first element in the matched set. + * + * @example + * + * $('
          ').data() + * //=> { appleColor: 'red' } + * + * $('
          ').data('apple-color') + * //=> 'red' + * + * const apple = $('.apple').data('kind', 'mac') + * apple.data('kind') + * //=> 'mac' + * + * @param {string} name - Name of the attribute. + * @param {any} [value] - If specified new value. + * + * @see {@link http://api.jquery.com/data/} + */ exports.data = function(name, value) { var elem = this[0]; @@ -239,38 +300,47 @@ exports.data = function(name, value) { }; /** - * Get the value of an element + * Method for getting and setting the value of input, select, and textarea. + * Note: Support for `map`, and `function` has not been added yet. + * + * @example + * + * $('input[type="text"]').val() + * //=> input_text + * + * $('input[type="text"]').val('test').html() + * //=> + * + * @param {string} [value] - If specified new value. + * + * @see {@link http://api.jquery.com/val/} */ - exports.val = function(value) { var querying = arguments.length === 0, element = this[0]; - if(!element) return; + if (!element) return; switch (element.name) { case 'textarea': return this.text(value); case 'input': - switch (this.attr('type')) { - case 'radio': - if (querying) { - return this.attr('value'); - } else { - this.attr('value', value); - return this; - } - break; - default: - return this.attr('value', value); + if (this.attr('type') === 'radio') { + if (querying) { + return this.attr('value'); + } + + this.attr('value', value); + return this; } - return; + + return this.attr('value', value); case 'select': var option = this.find('option:selected'), returnValue; if (option === undefined) return undefined; if (!querying) { - if (!this.attr().hasOwnProperty('multiple') && typeof value == 'object') { + if (!hasOwn.call(this.attr(), 'multiple') && typeof value == 'object') { return this; } if (typeof value != 'object') { @@ -283,7 +353,7 @@ exports.val = function(value) { return this; } returnValue = option.attr('value'); - if (this.attr().hasOwnProperty('multiple')) { + if (hasOwn.call(this.attr(), 'multiple')) { returnValue = []; domEach(option, function(__, el) { returnValue.push(getAttr(el, 'value')); @@ -300,17 +370,30 @@ exports.val = function(value) { }; /** - * Remove an attribute + * Remove an attribute. + * + * @private + * @param {node} elem - Node to remove attribute from. + * @param {string} name - Name of the attribute to remove. */ - var removeAttribute = function(elem, name) { - if (!elem.attribs || !hasOwn.call(elem.attribs, name)) - return; + if (!elem.attribs || !hasOwn.call(elem.attribs, name)) return; delete elem.attribs[name]; }; - +/** + * Method for removing attributes by `name`. + * + * @example + * + * $('.pear').removeAttr('class').html() + * //=>
        • Pear
        • + * + * @param {string} name - Name of the attribute. + * + * @see {@link http://api.jquery.com/removeAttr/} + */ exports.removeAttr = function(name) { domEach(this, function(i, elem) { removeAttribute(elem, name); @@ -319,6 +402,24 @@ exports.removeAttr = function(name) { return this; }; +/** + * Check to see if *any* of the matched elements have the given `className`. + * + * @example + * + * $('.pear').hasClass('pear') + * //=> true + * + * $('apple').hasClass('fruit') + * //=> false + * + * $('li').hasClass('pear') + * //=> true + * + * @param {string} className - Name of the class. + * + * @see {@link http://api.jquery.com/hasClass/} + */ exports.hasClass = function(className) { return _.some(this, function(elem) { var attrs = elem.attribs, @@ -327,11 +428,13 @@ exports.hasClass = function(className) { end; if (clazz && className.length) { - while ((idx = clazz.indexOf(className, idx+1)) > -1) { + while ((idx = clazz.indexOf(className, idx + 1)) > -1) { end = idx + className.length; - if ((idx === 0 || rspace.test(clazz[idx-1])) - && (end === clazz.length || rspace.test(clazz[end]))) { + if ( + (idx === 0 || rspace.test(clazz[idx - 1])) && + (end === clazz.length || rspace.test(clazz[end])) + ) { return true; } } @@ -339,6 +442,22 @@ exports.hasClass = function(className) { }); }; +/** + * Adds class(es) to all of the matched elements. Also accepts a `function` + * like jQuery. + * + * @example + * + * $('.pear').addClass('fruit').html() + * //=>
        • Pear
        • + * + * $('.apple').addClass('fruit red').html() + * //=>
        • Apple
        • + * + * @param {string} value - Name of new class. + * + * @see {@link http://api.jquery.com/addClass/} + */ exports.addClass = function(value) { // Support functions if (typeof value === 'function') { @@ -354,7 +473,6 @@ exports.addClass = function(value) { var classNames = value.split(rspace), numElements = this.length; - for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on if (!isTag(this[i])) continue; @@ -373,8 +491,7 @@ exports.addClass = function(value) { // Check if class already exists for (var j = 0; j < numClasses; j++) { var appendClass = classNames[j] + ' '; - if (setClass.indexOf(' ' + appendClass) < 0) - setClass += appendClass; + if (setClass.indexOf(' ' + appendClass) < 0) setClass += appendClass; } setAttr(this[i], 'class', setClass.trim()); @@ -388,16 +505,31 @@ var splitClass = function(className) { return className ? className.trim().split(rspace) : []; }; +/** + * Removes one or more space-separated classes from the selected elements. If + * no `className` is defined, all classes will be removed. Also accepts a + * `function` like jQuery. + * + * @example + * + * $('.pear').removeClass('pear').html() + * //=>
        • Pear
        • + * + * $('.apple').addClass('red').removeClass().html() + * //=>
        • Apple
        • + * @param {string} value - Name of the class. + * + * @see {@link http://api.jquery.com/removeClass/} + */ exports.removeClass = function(value) { - var classes, - numClasses, - removeAll; + var classes, numClasses, removeAll; // Handle if value is a function if (typeof value === 'function') { return domEach(this, function(i, el) { exports.removeClass.call( - [el], value.call(el, i, el.attribs['class'] || '') + [el], + value.call(el, i, el.attribs['class'] || '') ); }); } @@ -436,6 +568,24 @@ exports.removeClass = function(value) { }); }; +/** + * Add or remove class(es) from the matched elements, depending on either the + * class's presence or the value of the switch argument. Also accepts a + * `function` like jQuery. + * + * @example + * + * $('.apple.green').toggleClass('fruit green red').html() + * //=>
        • Apple
        • + * + * $('.apple.green').toggleClass('fruit green red', true).html() + * //=>
        • Apple
        • + * + * @param {(string|Function)} value - Name of the class. Can also be a function. + * @param {boolean} [stateVal] - If specified the state of the class. + * + * @see {@link http://api.jquery.com/toggleClass/} + */ exports.toggleClass = function(value, stateVal) { // Support functions if (typeof value === 'function') { @@ -452,11 +602,11 @@ exports.toggleClass = function(value, stateVal) { if (!value || typeof value !== 'string') return this; var classNames = value.split(rspace), - numClasses = classNames.length, - state = typeof stateVal === 'boolean' ? stateVal ? 1 : -1 : 0, - numElements = this.length, - elementClasses, - index; + numClasses = classNames.length, + state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0, + numElements = this.length, + elementClasses, + index; for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on @@ -484,7 +634,18 @@ exports.toggleClass = function(value, stateVal) { return this; }; -exports.is = function (selector) { +/** + * Checks the current list of elements and returns `true` if _any_ of the + * elements match the selector. If using an element or Cheerio selection, + * returns `true` if _any_ of the elements match. If using a predicate + * function, the function is executed in the context of the selected element, + * so `this` refers to the current element. + * + * @param {string|Function|cheerio|node} selector - Selector for the selection. + * + * @see {@link http://api.jquery.com/is/} + */ +exports.is = function(selector) { if (selector) { return this.filter(selector).length > 0; } diff --git a/lib/api/css.js b/lib/api/css.js index d9ffae6cfa..990cb973b8 100644 --- a/lib/api/css.js +++ b/lib/api/css.js @@ -1,23 +1,30 @@ +/** + * @module css + */ var domEach = require('../utils').domEach, _ = { - pick: require('lodash/pick'), + pick: require('lodash/pick') }; var toString = Object.prototype.toString; /** - * Set / Get css. + * Get the value of a style property for the first element in the set of + * matched elements or set one or more CSS properties for every matched + * element. * - * @param {String|Object} prop - * @param {String} val - * @return {self} - * @api public + * @param {string|Object} prop - The name of the property. + * @param {string} [val] - If specified the new value. + * @returns {self} + * + * @see {@link http://api.jquery.com/css/} */ - exports.css = function(prop, val) { - if (arguments.length === 2 || + if ( + arguments.length === 2 || // When `prop` is a "plain" object - (toString.call(prop) === '[object Object]')) { + toString.call(prop) === '[object Object]' + ) { return domEach(this, function(idx, el) { setCss(el, prop, val, idx); }); @@ -29,13 +36,13 @@ exports.css = function(prop, val) { /** * Set styles of all elements. * - * @param {String|Object} prop - * @param {String} val - * @param {Number} idx - optional index within the selection - * @return {self} - * @api private + * @param {Object} el - Element to set style of. + * @param {string|Object} prop - Name of property. + * @param {string} val - Value to set property to. + * @param {number} [idx] - Optional index within the selection. + * @returns {self} + * @private */ - function setCss(el, prop, val, idx) { if ('string' == typeof prop) { var styles = getCss(el); @@ -51,7 +58,7 @@ function setCss(el, prop, val, idx) { el.attribs.style = stringify(styles); } else if ('object' == typeof prop) { - Object.keys(prop).forEach(function(k){ + Object.keys(prop).forEach(function(k) { setCss(el, k, prop[k]); }); } @@ -60,11 +67,11 @@ function setCss(el, prop, val, idx) { /** * Get parsed styles of the first element. * - * @param {String} prop - * @return {Object} - * @api private + * @param {node} el - Element to get styles from. + * @param {string} prop - Name of the prop. + * @returns {Object} + * @private */ - function getCss(el, prop) { var styles = parse(el.attribs.style); if (typeof prop === 'string') { @@ -79,43 +86,33 @@ function getCss(el, prop) { /** * Stringify `obj` to styles. * - * @param {Object} obj - * @return {Object} - * @api private + * @param {Object} obj - Object to stringify. + * @returns {Object} + * @private */ - function stringify(obj) { - return Object.keys(obj || {}) - .reduce(function(str, prop){ - return str += '' - + (str ? ' ' : '') - + prop - + ': ' - + obj[prop] - + ';'; - }, ''); + return Object.keys(obj || {}).reduce(function(str, prop) { + return (str += '' + (str ? ' ' : '') + prop + ': ' + obj[prop] + ';'); + }, ''); } /** * Parse `styles`. * - * @param {String} styles - * @return {Object} - * @api private + * @param {string} styles - Styles to be parsed. + * @returns {Object} + * @private */ - function parse(styles) { styles = (styles || '').trim(); if (!styles) return {}; - return styles - .split(';') - .reduce(function(obj, str){ - var n = str.indexOf(':'); - // skip if there is no :, or if it is the first/last character - if (n < 1 || n === str.length-1) return obj; - obj[str.slice(0,n).trim()] = str.slice(n+1).trim(); - return obj; - }, {}); + return styles.split(';').reduce(function(obj, str) { + var n = str.indexOf(':'); + // skip if there is no :, or if it is the first/last character + if (n < 1 || n === str.length - 1) return obj; + obj[str.slice(0, n).trim()] = str.slice(n + 1).trim(); + return obj; + }, {}); } diff --git a/lib/api/forms.js b/lib/api/forms.js index b66f162cd9..ade2adaded 100644 --- a/lib/api/forms.js +++ b/lib/api/forms.js @@ -1,3 +1,6 @@ +/** + * @module forms + */ // https://github.com/jquery/jquery/blob/2.1.3/src/manipulation/var/rcheckableType.js // https://github.com/jquery/jquery/blob/2.1.3/src/serialize.js var submittableSelector = 'input,select,textarea,keygen', @@ -7,6 +10,11 @@ var submittableSelector = 'input,select,textarea,keygen', map: require('lodash/map') }; +/** + * Encode a set of form elements as a string for submission. + * + * @see {@link http://api.jquery.com/serialize/} + */ exports.serialize = function() { // Convert form elements into name/value objects var arr = this.serializeArray(); @@ -20,26 +28,37 @@ exports.serialize = function() { return retArr.join('&').replace(r20, '+'); }; +/** + * Encode a set of form elements as an array of names and values. + * + * @example + * $('
          ').serializeArray() + * //=> [ { name: 'foo', value: 'bar' } ] + * + * @see {@link http://api.jquery.com/serializeArray/} + */ exports.serializeArray = function() { // Resolve all form elements from either forms or collections of form elements var Cheerio = this.constructor; return this.map(function() { - var elem = this; - var $elem = Cheerio(elem); - if (elem.name === 'form') { - return $elem.find(submittableSelector).toArray(); - } else { - return $elem.filter(submittableSelector).toArray(); - } - }).filter( - // Verify elements have a name (`attr.name`) and are not disabled (`:disabled`) - '[name!=""]:not(:disabled)' + var elem = this; + var $elem = Cheerio(elem); + if (elem.name === 'form') { + return $elem.find(submittableSelector).toArray(); + } else { + return $elem.filter(submittableSelector).toArray(); + } + }) + .filter( + // Verify elements have a name (`attr.name`) and are not disabled (`:disabled`) + '[name!=""]:not(:disabled)' + // and cannot be clicked (`[type=submit]`) or are used in `x-www-form-urlencoded` (`[type=file]`) - + ':not(:submit, :button, :image, :reset, :file)' + ':not(:submit, :button, :image, :reset, :file)' + // and are either checked/don't have a checkable state - + ':matches([checked], :not(:checkbox, :radio))' - // Convert each of the elements to its value(s) - ).map(function(i, elem) { + ':matches([checked], :not(:checkbox, :radio))' + // Convert each of the elements to its value(s) + ) + .map(function(i, elem) { var $elem = Cheerio(elem); var name = $elem.attr('name'); var value = $elem.val(); @@ -54,12 +73,13 @@ exports.serializeArray = function() { return _.map(value, function(val) { // We trim replace any line endings (e.g. `\r` or `\r\n` with `\r\n`) to guarantee consistency across platforms // These can occur inside of `'; // Comments var comment = ''; -var conditional = ''; +var conditional = + ''; // Text var text = 'lorem ipsum'; @@ -37,20 +37,17 @@ var styleEmpty = ''; // Directives var directive = ''; - describe('parse', function() { - describe('.eval', function() { - it('should parse basic empty tags: ' + basic, function() { - var tag = parse.evaluate(basic, defaultOpts)[0]; + var tag = parse.evaluate(basic, defaultOpts, true)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('html'); - expect(tag.childNodes).to.be.empty(); + expect(tag.childNodes).to.have.length(2); }); it('should handle sibling tags: ' + siblings, function() { - var dom = parse.evaluate(siblings, defaultOpts), + var dom = parse.evaluate(siblings, defaultOpts, false), h2 = dom[0], p = dom[1]; @@ -60,66 +57,70 @@ describe('parse', function() { }); it('should handle single tags: ' + single, function() { - var tag = parse.evaluate(single, defaultOpts)[0]; + var tag = parse.evaluate(single, defaultOpts, false)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('br'); expect(tag.childNodes).to.be.empty(); }); it('should handle malformatted single tags: ' + singleWrong, function() { - var tag = parse.evaluate(singleWrong, defaultOpts)[0]; + var tag = parse.evaluate(singleWrong, defaultOpts, false)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('br'); expect(tag.childNodes).to.be.empty(); }); it('should handle tags with children: ' + children, function() { - var tag = parse.evaluate(children, defaultOpts)[0]; + var tag = parse.evaluate(children, defaultOpts, true)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('html'); expect(tag.childNodes).to.be.ok(); - expect(tag.childNodes).to.have.length(1); + expect(tag.childNodes[1].tagName).to.equal('body'); + expect(tag.childNodes[1].childNodes).to.have.length(1); }); it('should handle tags with children: ' + li, function() { - var tag = parse.evaluate(li, defaultOpts)[0]; + var tag = parse.evaluate(li, defaultOpts, false)[0]; expect(tag.childNodes).to.have.length(1); expect(tag.childNodes[0].data).to.equal('Durian'); }); it('should handle tags with attributes: ' + attributes, function() { - var attrs = parse.evaluate(attributes, defaultOpts)[0].attribs; + var attrs = parse.evaluate(attributes, defaultOpts, false)[0].attribs; expect(attrs).to.be.ok(); expect(attrs.src).to.equal('hello.png'); expect(attrs.alt).to.equal('man waving'); }); it('should handle value-less attributes: ' + noValueAttribute, function() { - var attrs = parse.evaluate(noValueAttribute, defaultOpts)[0].attribs; + var attrs = parse.evaluate(noValueAttribute, defaultOpts, false)[0] + .attribs; expect(attrs).to.be.ok(); expect(attrs.disabled).to.equal(''); }); it('should handle comments: ' + comment, function() { - var elem = parse.evaluate(comment, defaultOpts)[0]; + var elem = parse.evaluate(comment, defaultOpts, false)[0]; expect(elem.type).to.equal('comment'); expect(elem.data).to.equal(' sexy '); }); it('should handle conditional comments: ' + conditional, function() { - var elem = parse.evaluate(conditional, defaultOpts)[0]; + var elem = parse.evaluate(conditional, defaultOpts, false)[0]; expect(elem.type).to.equal('comment'); - expect(elem.data).to.equal(conditional.replace('', '')); + expect(elem.data).to.equal( + conditional.replace('', '') + ); }); it('should handle text: ' + text, function() { - var text_ = parse.evaluate(text, defaultOpts)[0]; + var text_ = parse.evaluate(text, defaultOpts, false)[0]; expect(text_.type).to.equal('text'); expect(text_.data).to.equal('lorem ipsum'); }); it('should handle script tags: ' + script, function() { - var script_ = parse.evaluate(script, defaultOpts)[0]; + var script_ = parse.evaluate(script, defaultOpts, false)[0]; expect(script_.type).to.equal('script'); expect(script_.tagName).to.equal('script'); expect(script_.attribs.type).to.equal('text/javascript'); @@ -129,7 +130,7 @@ describe('parse', function() { }); it('should handle style tags: ' + style, function() { - var style_ = parse.evaluate(style, defaultOpts)[0]; + var style_ = parse.evaluate(style, defaultOpts, false)[0]; expect(style_.type).to.equal('style'); expect(style_.tagName).to.equal('style'); expect(style_.attribs.type).to.equal('text/css'); @@ -139,16 +140,14 @@ describe('parse', function() { }); it('should handle directives: ' + directive, function() { - var elem = parse.evaluate(directive, defaultOpts)[0]; + var elem = parse.evaluate(directive, defaultOpts, true)[0]; expect(elem.type).to.equal('directive'); - expect(elem.data).to.equal('!doctype html'); + expect(elem.data).to.equal('!DOCTYPE html'); expect(elem.tagName).to.equal('!doctype'); }); - }); describe('.parse', function() { - // root test utility function rootTest(root) { expect(root.tagName).to.equal('root'); @@ -163,14 +162,14 @@ describe('parse', function() { } it('should add root to: ' + basic, function() { - var root = parse(basic, defaultOpts); + var root = parse(basic, defaultOpts, true); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].tagName).to.equal('html'); }); it('should add root to: ' + siblings, function() { - var root = parse(siblings, defaultOpts); + var root = parse(siblings, defaultOpts, false); rootTest(root); expect(root.childNodes).to.have.length(2); expect(root.childNodes[0].tagName).to.equal('h2'); @@ -179,42 +178,46 @@ describe('parse', function() { }); it('should add root to: ' + comment, function() { - var root = parse(comment, defaultOpts); + var root = parse(comment, defaultOpts, false); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('comment'); }); it('should add root to: ' + text, function() { - var root = parse(text, defaultOpts); + var root = parse(text, defaultOpts, false); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('text'); }); it('should add root to: ' + scriptEmpty, function() { - var root = parse(scriptEmpty, defaultOpts); + var root = parse(scriptEmpty, defaultOpts, false); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('script'); }); it('should add root to: ' + styleEmpty, function() { - var root = parse(styleEmpty, defaultOpts); + var root = parse(styleEmpty, defaultOpts, false); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('style'); }); it('should add root to: ' + directive, function() { - var root = parse(directive, defaultOpts); + var root = parse(directive, defaultOpts, true); rootTest(root); - expect(root.childNodes).to.have.length(1); + expect(root.childNodes).to.have.length(2); expect(root.childNodes[0].type).to.equal('directive'); }); it('should expose the DOM level 1 API', function() { - var root = parse('

          ', defaultOpts).childNodes[0]; + var root = parse( + '

          ', + defaultOpts, + false + ).childNodes[0]; var childNodes = root.childNodes; expect(childNodes).to.have.length(3); @@ -247,6 +250,130 @@ describe('parse', function() { expect(childNodes[2].firstChild).to.be(null); expect(childNodes[2].lastChild).to.be(null); }); - }); + it('Should parse less than or equal sign sign', function() { + var root = parse('A<=B', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes[0].tagName).to.be('i'); + expect(childNodes[0].childNodes[0].data).to.be('A'); + expect(childNodes[1].data).to.be('<='); + expect(childNodes[2].tagName).to.be('i'); + expect(childNodes[2].childNodes[0].data).to.be('B'); + }); + + it('Should ignore unclosed CDATA', function() { + var root = parse( + '', + defaultOpts, + false + ); + var childNodes = root.childNodes; + + expect(childNodes[0].tagName).to.be('a'); + expect(childNodes[1].tagName).to.be('script'); + expect(childNodes[1].childNodes[0].data).to.be('foo // to documents', function() { + var root = parse('', defaultOpts, true); + var childNodes = root.childNodes; + + expect(childNodes[0].tagName).to.be('html'); + expect(childNodes[0].childNodes[0].tagName).to.be('head'); + }); + + it('Should implicitly create around ', function() { + var root = parse('
          bar
          ', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes[0].tagName).to.be('table'); + expect(childNodes[0].childNodes.length).to.be(1); + expect(childNodes[0].childNodes[0].tagName).to.be('tbody'); + expect(childNodes[0].childNodes[0].childNodes[0].tagName).to.be('tr'); + expect( + childNodes[0].childNodes[0].childNodes[0].childNodes[0].tagName + ).to.be('td'); + expect( + childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0] + .data + ).to.be('bar'); + }); + + it('Should parse custom tag ', function() { + var root = parse('test', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes.length).to.be(1); + expect(childNodes[0].tagName).to.be('line'); + expect(childNodes[0].childNodes[0].data).to.be('test'); + }); + + it('Should properly parse misnested table tags', function() { + var root = parse( + 'i1i2i3', + defaultOpts, + false + ); + var childNodes = root.childNodes; + + expect(childNodes.length).to.be(3); + + childNodes.forEach(function(child, i) { + expect(child.tagName).to.be('tr'); + expect(child.childNodes[0].tagName).to.be('td'); + expect(child.childNodes[0].childNodes[0].data).to.be('i' + (i + 1)); + }); + }); + + it('Should correctly parse data url attributes', function() { + var html = + '
          '; + var expectedAttr = + 'font-family:"butcherman-caps"; src:url(data:font/opentype;base64,AAEA...);'; + var root = parse(html, defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes[0].attribs.style).to.be(expectedAttr); + }); + + it('Should treat tag content as text', function() { + var root = parse('<xmp><h2>', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes[0].childNodes[0].data).to.be('

          '); + }); + + it('Should correctly parse malformed numbered entities', function() { + var root = parse('

          z&#

          ', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes[0].childNodes[0].data).to.be('z&#'); + }); + + it('Should correctly parse mismatched headings', function() { + var root = parse('

          Test

          ', defaultOpts, false); + var childNodes = root.childNodes; + + expect(childNodes.length).to.be(2); + expect(childNodes[0].tagName).to.be('h2'); + expect(childNodes[1].tagName).to.be('div'); + }); + + it('Should correctly parse tricky
           content', function() {
          +      var root = parse(
          +        '
          \nA <- factor(A, levels = c("c","a","b"))\n
          ', + defaultOpts, + false + ); + var childNodes = root.childNodes; + + expect(childNodes.length).to.be(1); + expect(childNodes[0].tagName).to.be('pre'); + expect(childNodes[0].childNodes[0].data).to.be( + 'A <- factor(A, levels = c("c","a","b"))\n' + ); + }); + }); }); diff --git a/test/xml.js b/test/xml.js index cc23133532..d24e3feb77 100644 --- a/test/xml.js +++ b/test/xml.js @@ -5,9 +5,9 @@ var expect = require('expect.js'), }; var xml = function(str, options) { - options = _.extend({ xmlMode: true }, options); - var dom = cheerio.load(str, options); - return dom.xml(); + options = _.extend({ xml: true }, options); + var $ = cheerio.load(str, options); + return $.xml(); }; var dom = function(str, options) { @@ -16,12 +16,13 @@ var dom = function(str, options) { }; describe('render', function() { - describe('(xml)', function() { - it('should render tags correctly', function() { - var str = ''; - expect(xml(str)).to.equal(''); + var str = + ''; + expect(xml(str)).to.equal( + '' + ); }); it('should render tags (RSS) correctly', function() { @@ -29,32 +30,34 @@ describe('render', function() { expect(xml(str)).to.equal('http://www.github.com/'); }); - it('should escape entities', function(){ + it('should escape entities', function() { var str = ''; expect(xml(str)).to.equal(str); }); - }); - describe('(dom)', function () { - + describe('(dom)', function() { it('should keep camelCase for new nodes', function() { var str = 'hello'; - expect(dom(str, {xmlMode: false})).to.equal('hello'); + expect(dom(str, { xml: false })).to.equal( + 'hello' + ); }); it('should keep camelCase for new nodes', function() { var str = 'hello'; - expect(dom(str, {xmlMode: true})).to.equal('hello'); + expect(dom(str, { xml: true })).to.equal( + 'hello' + ); }); it('should maintain the parsing options of distinct contexts independently', function() { var str = 'hello'; - var $x = cheerio.load('', { xmlMode: false }); + var $ = cheerio.load('', { xml: false }); - expect($x(str).html()).to.equal('hello'); + expect($(str).html()).to.equal( + 'hello' + ); }); - }); - }); From df695a4e6cb21f405dfb13bf12c461ff3ee571e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= Date: Wed, 3 Oct 2018 18:04:08 +0100 Subject: [PATCH 09/16] Update Readme.md Document the current state of master --- Readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Readme.md b/Readme.md index c351d8612d..3171f0f6dd 100644 --- a/Readme.md +++ b/Readme.md @@ -33,6 +33,10 @@ $.html() //=>

          Hello there!

          ``` +## Note + +We are currently working on the 1.0.0 release of cheerio on the `master` branch. The source code for the last published version, `0.22.0`, can be found [here](https://github.com/cheeriojs/cheerio/tree/aa90399c9c02f12432bfff97b8f1c7d8ece7c307). + ## Installation `npm install cheerio` From e2f8436170e56190beb0dbbd284065624ca4d329 Mon Sep 17 00:00:00 2001 From: jugglinmike Date: Mon, 8 Oct 2018 15:13:46 -0400 Subject: [PATCH 10/16] Revert "v1.0.0 (#1145)" (#1231) This reverts commit ca6963c09a38eaf67fbe551634fdbbc6984d3da8. The `v1.0.0` branch was a long-running feature branch containing a large amount of disparate commits from many authors. It was squashed into a single commit prior to being merged to the project's `master` branch. For the purposes of attribution, reference, and history analysis, the composite commits should be preserved, and the branch should be incorporated into `master` via a dedicated merge commit. --- .eslintrc.json | 31 -- .gitignore | 1 - .jshintrc | 11 + .travis.yml | 6 +- CONTRIBUTING.md | 56 ++- History.md | 95 ---- Makefile | 17 +- Readme.md | 850 +++++++++++++++++++++++++++++++-- benchmark/.eslintrc.json | 5 - benchmark/benchmark.js | 26 +- benchmark/suite.js | 14 +- jsdoc-config.json | 11 - lib/api/attributes.js | 307 +++--------- lib/api/css.js | 89 ++-- lib/api/forms.js | 58 +-- lib/api/manipulation.js | 463 +++--------------- lib/api/traversing.js | 678 ++++----------------------- lib/cheerio.js | 82 ++-- lib/options.js | 18 - lib/parse.js | 36 +- lib/static.js | 148 ++---- lib/utils.js | 90 ++-- package.json | 50 +- scripts/generate_history.sh | 30 -- scripts/prepublish | 26 +- test/.eslintrc.json | 5 - test/.jshintrc | 9 + test/api/attributes.js | 160 +++---- test/api/css.js | 25 +- test/api/forms.js | 59 +-- test/api/manipulation.js | 904 ++++++++---------------------------- test/api/traversing.js | 281 +++++------ test/api/utils.js | 172 +++---- test/cheerio.js | 404 ++++++---------- test/fixtures.js | 30 +- test/parse.js | 199 ++------ test/xml.js | 37 +- 37 files changed, 2059 insertions(+), 3424 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 .jshintrc delete mode 100644 benchmark/.eslintrc.json delete mode 100644 jsdoc-config.json delete mode 100644 lib/options.js delete mode 100755 scripts/generate_history.sh delete mode 100644 test/.eslintrc.json create mode 100644 test/.jshintrc diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 0db30f8ce8..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "env": { - "node": true - }, - "plugins": ["jsdoc"], - "extends": ["eslint:recommended", "prettier"], - "globals": {}, - "rules": { - "indent": [ - 2, - 2, - { - "SwitchCase": 1, - "VariableDeclarator": 2 - } - ], - "no-eq-null": 0, - "no-proto": 2, - - "jsdoc/check-param-names": 2, - "jsdoc/check-tag-names": 2, - "jsdoc/check-types": 2, - "jsdoc/newline-after-description": 2, - "jsdoc/require-description-complete-sentence": 2, - "jsdoc/require-hyphen-before-param-description": 2, - "jsdoc/require-param": 2, - "jsdoc/require-param-description": 2, - "jsdoc/require-param-name": 2, - "jsdoc/require-param-type": 2 - } -} diff --git a/.gitignore b/.gitignore index 46b866b577..2ae05e1c97 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ lib-cov cover_html .c9revisions coverage -/docs/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..28a0ff23f0 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,11 @@ +{ + "indent": 2, + "eqnull": true, + "laxbreak": true, + "proto": true, + "undef": true, + "unused": true, + "node": true, + "quotmark": "single", + "shadow": "outer" +} diff --git a/.travis.yml b/.travis.yml index 8db0083269..b7d7df23f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,15 @@ language: node_js node_js: - "stable" - - "8" + - "unstable" - "6" + - "4" + - "0.12" script: make travis-test matrix: fast_finish: true + allow_failures: + - node_js: unstable include: - env: BENCHMARK=true script: "node benchmark/benchmark.js --regex '^(?!.*highmem)'" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24f458fdee..cc38bb8dd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,16 +3,16 @@ Thanks for your interest in contributing to the project! Here's a rundown of how we'd like to work with you: -1. File an issue on GitHub describing the contribution you'd like to make. This - will help us to get you started on the right foot. -2. Create a single commit that addresses the issue: - 1. Follow the project's code style (see below) - 2. Add enough unit tests to "prove" that your patch is correct - 3. Update the project documentation as needed (see below) - 4. Describe your approach with as much detail as necessary in the git - commit message -3. Open a pull request, and reference the initial issue in the pull request - message. +1. File an issue on GitHub describing the contribution you'd like to make. This + will help us to get you started on the right foot. +2. Create a single commit that addresses the issue: + 1. Follow the project's code style (see below) + 2. Add enough unit tests to "prove" that your patch is correct + 3. Update the project documentation as needed (see below) + 4. Describe your approach with as much detail as necessary in the git + commit message +3. Open a pull request, and reference the initial issue in the pull request + message. # Documentation @@ -22,16 +22,26 @@ care to note aspects that make Cheerio distinct. # Code Style -Please make sure commit hooks are run, which will enforce the code style. - -When implementing private functionality that isn't part of the jQuery API, please opt for: - -* _Static methods_: If the functionality does not require a reference to a - Cheerio instance, simply define a named function within the module it is - needed. -* _Instance methods_: If the functionality requires a reference to a Cheerio - instance, informally define the method as "private" using the following - conventions: - * Define the method as a function on the Cheerio prototype - * Prefix the method name with an underscore (`_`) character - * Include `@api private` in the code comment the documents the method +This section is by no means complete. For undocumented stylistic choices, +please try to maintain consistency with the code base. + +- Single quotes: `'` +- Whitespace + - Two-space "soft" tabs + - Once space following control flow statements (`if (condition) {` rather + than `if(condition) {`) + - Remove trailing spaces + - [End each file with a newline + character.](https://github.com/editorconfig/editorconfig/wiki/Newline-at-End-of-File-Support) +- Terminate every statement with a semicolon +- Private functionality (for re-using functionality that isn't part of the + jQuery API) + - *Static methods*: If the functionality does not require a reference to a + Cheerio instance, simply define a named function within the module it is + needed. + - *Instance methods*: If the functionality requires a reference to a Cheerio + instance, informally define the method as "private" using the following + conventions: + - Define the method as a function on the Cheerio prototype + - Prefix the method name with an underscore (`_`) character + - Include `@api private` in the code comment the documents the method diff --git a/History.md b/History.md index 462ae0cf28..c7e38e66a7 100644 --- a/History.md +++ b/History.md @@ -1,98 +1,3 @@ -1.0.0-rc.2 / 2017-07-02 -================== - -This release changes Cheerio's default parser to [the Parse5 HTML -parser](https://github.com/inikulin/parse5). Parse5 is an excellent project -that rigorously conforms to the HTML standard. It does not support XML, so -Cheerio continues to use [`htmlparser2`](https://github.com/fb55/htmlparser2/) -when working with XML documents. - -This switch addresses many long-standing bugs in Cheerio, but some users may -experience slower behavior in performance-critical applications. In addition, -`htmlparser2` is more forgiving of invalid markup which can be useful when -input sourced from a third party and cannot be corrected. For these reasons, -the `load` method also accepts a DOM structure as produced by the `htmlparser2` -library. See the project's "readme" file for more details on this usage -pattern. - -### Migrating from version 0.x - -`cheerio.load( html[, options ] )` This method continues to act as a "factory" -function. It produces functions that define an API that is similar to the -global `jQuery` function provided by the jQuery library. The generated function -operates on a DOM structure based on the provided HTML. - -In releases prior to version 1.0, the provided HTML was interpreted as a -document fragment. Following version 1.0, strings provided to the `load` method -are interpreted as documents. The same example will produce a `$` function that -operates on a full HTML document, including an `` document element with -nested `` and `` tags. This mimics web browser behavior much more -closely, but may require alterations to existing code. - -For example, the following code will produce different results between 0.x and -1.0 releases: - - var $ = cheerio.load('

          Hello, world!

          '); - - $.root().html(); - - //=> In version 0.x: '

          Hello, world!

          ' - //=> In version 1.0: '

          Hello, world!

          ' - -Users wishing to parse, manipulate, and render full documents should not need -to modify their code. Likewise, code that does not interact with the "root" -element should not be effected by this change. (In the above example, the -expression `$('p')` returns the same result across Cheerio versions--a Cheerio -collection whose only member is a paragraph element.) - -However, users wishing to render document fragments should now explicitly -create a "wrapper" element to contain their input. - - // First, create a Cheerio function "bound" to an empty document (this is - // similar to loading an empty page in a web browser) - var $ = cheerio.load(''); - // Next, create a "wrapper" element for the input fragment: - var $wrapper = $('
          '); - // Finally, supply the input markup as the content for the wrapper: - $wrapper.append('

          Hello, world!

          '); - - $wrapper.html(); - //=> '

          Hello, world!

          ' - ---- - -Change log: - - * Update History.md (and include migration guide) (Mike Pennisi) - * Rename `useHtmlParser2` option (Mike Pennisi) - * Remove documentation for `xmlMode` option (Mike Pennisi) - * Document advanced usage with htmlparser2 (Mike Pennisi) - * Correct errors in Readme.md (Mike Pennisi) - * Improve release process (Mike Pennisi) - * 1.0.0-rc.1 (Mike Pennisi) - * Update unit test (Mike Pennisi) - * Normalize code style (Mike Pennisi) - * Added support for nested wrapping. (Diane Looney) - * Add nested wrapping test (Toni Helenius) - * Added $.merge following the specification at https://api.jquery.com/jquery.merge/ Added test cases for $.merge (Diane Looney) - * Clarify project scope in README file (Mike Pennisi) - * .text() ignores script and style tags (#1018) (Haleem Assal) - * Test suite housekeeping (#1016) (DianeLooney) - * experiment with a job board (Matthew) - * Change options format (inikulin) - * Add test for #997 (inikulin) - * Update .filter function docs. (Konstantin) - * Standardise readme on ES6 variable declarations (Dekatron) - * Use documents via $.load (inikulin) - * Use parse5 as a default parser (closes #863) (inikulin) - * Fix small typo in Readme (Darren Scerri) - * Report test failures in CI (Mike Pennisi) - * serializeArray should not ignore input elements without value attributes (Ricardo Gladwell) - * Disallow variable shadowing (Mike Pennisi) - * Update hasClass method (sufisaid) - * Added MIT License fixes #902 (Prasanth Vaaheeswaran) - * chore(package): update dependencies (greenkeeper[bot]) - * Use modular lodash package (#913) (Billy Janitsch) 0.22.0 / 2016-08-23 ================== diff --git a/Makefile b/Makefile index 52edbb40a9..3617d89b23 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,8 @@ REPORTER = dot XYZ = node_modules/.bin/xyz --message 'Release X.Y.Z' --tag X.Y.Z --repo git@github.com:cheeriojs/cheerio.git --script scripts/prepublish -UPSTREAM = git@github.com:cheeriojs/cheerio.git lint: - @./node_modules/.bin/eslint --ignore-path .gitignore . + @./node_modules/.bin/jshint lib/ test/ test: lint @./node_modules/.bin/mocha --recursive --reporter $(REPORTER) @@ -37,16 +36,4 @@ release-patch: LEVEL = patch release-major release-minor release-patch: @$(XYZ) --increment $(LEVEL) -docs: - @./node_modules/.bin/jsdoc --configure jsdoc-config.json - -publish-docs: docs - @cd docs; \ - rm -rf .git && \ - git init && \ - git add --all . && \ - git commit -m 'Generate documentation' && \ - git remote add upstream $(UPSTREAM) && \ - git push --force upstream master:gh-pages - -.PHONY: test build setup subl docs publish-docs +.PHONY: test build setup subl diff --git a/Readme.md b/Readme.md index 3171f0f6dd..8d8fdc325a 100644 --- a/Readme.md +++ b/Readme.md @@ -30,7 +30,7 @@ $('h2.title').text('Hello there!') $('h2').addClass('welcome') $.html() -//=>

          Hello there!

          +//=>

          Hello there!

          ``` ## Note @@ -48,7 +48,7 @@ __ϟ Blazingly fast:__ Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient. Preliminary end-to-end benchmarks suggest that cheerio is about __8x__ faster than JSDOM. __❁ Incredibly flexible:__ -Cheerio wraps around [parse5](https://github.com/inikulin/parse5) parser and can optionally use @FB55's forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. +Cheerio wraps around @FB55's forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. ## Cheerio is not a web browser @@ -126,6 +126,107 @@ Does your company use Cheerio in production? Please consider [sponsoring this pr ## API +### Table of contents + +
          + Selectors + + - [$( selector, [context], [root] )](#-selector-context-root-) +
          +
          + Attributes + + - [.attr( name, value )](#attr-name-value-) + - [.prop( name, value )](#prop-name-value-) + - [.data( name, value )](#data-name-value-) + - [.val( [value] )](#val-value-) + - [.removeAttr( name )](#removeattr-name-) + - [.hasClass( className )](#hasclass-classname-) + - [.addClass( className )](#addclass-classname-) + - [.removeClass( [className] )](#removeclass-classname-) + - [.toggleClass( className, [switch] )](#toggleclass-classname-switch-) + - [.is( selector )](#is-selector-) + - [.is( element )](#is-element-) + - [.is( selection )](#is-selection-) + - [.is( function(index) )](#is-functionindex-) +
          +
          + Forms + + - [.serialize()](#serialize) + - [.serializeArray()](#serializearray) +
          +
          + Traversing + + - [.find(selector)](#findselector) + - [.find(selection)](#findselection) + - [.find(node)](#findnode) + - [.parent([selector])](#parentselector) + - [.parents([selector])](#parentsselector) + - [.parentsUntil([selector][,filter])](#parentsuntilselectorfilter) + - [.closest(selector)](#closestselector) + - [.next([selector])](#nextselector) + - [.nextAll([selector])](#nextallselector) + - [.nextUntil([selector], [filter])](#nextuntilselector-filter) + - [.prev([selector])](#prevselector) + - [.prevAll([selector])](#prevallselector) + - [.prevUntil([selector], [filter])](#prevuntilselector-filter) + - [.slice( start, [end] )](#slice-start-end-) + - [.siblings([selector])](#siblingsselector) + - [.children([selector])](#childrenselector) + - [.contents()](#contents) + - [.each( function(index, element) )](#each-functionindex-element-) + - [.map( function(index, element) )](#map-functionindex-element-) + - [.filter( selector )
          + .filter( selection )
          + .filter( element )
          + .filter( function(index, element) )](#filter-selector---filter-selection---filter-element---filter-functionindex-element-) + - [.not( selector )
          + .not( selection )
          + .not( element )
          + .not( function(index, elem) )](#not-selector---not-selection---not-element---not-functionindex-elem-) + - [.has( selector )
          + .has( element )](#has-selector---has-element-) + - [.first()](#first) + - [.last()](#last) + - [.eq( i )](#eq-i-) + - [.get( [i] )](#get-i-) + - [.index()](#index) + - [.index( selector )](#index-selector-) + - [.index( nodeOrSelection )](#index-nodeorselection-) + - [.end()](#end) + - [.add( selector [, context] )](#add-selector--context-) + - [.add( element )](#add-element-) + - [.add( elements )](#add-elements-) + - [.add( html )](#add-html-) + - [.add( selection )](#add-selection-) + - [.addBack( [filter] )](#addback-filter-) +
          +
          + Manipulation + + - [.append( content, [content, ...] )](#append-content-content--) + - [.appendTo( target )](#appendto-target-) + - [.prepend( content, [content, ...] )](#prepend-content-content--) + - [.prependTo( target )](#prependto-target-) + - [.after( content, [content, ...] )](#after-content-content--) + - [.insertAfter( target )](#insertafter-target-) + - [.before( content, [content, ...] )](#before-content-content--) + - [.insertBefore( target )](#insertbefore-target-) + - [.remove( [selector] )](#remove-selector-) + - [.replaceWith( content )](#replacewith-content-) + - [.empty()](#empty) + - [.html( [htmlString] )](#html-htmlstring-) + - [.text( [textString] )](#text-textstring-) + - [.wrap( content )](#wrap-content-) + - [.css( [propertName] )
          + .css( [ propertyNames] )
          + .css( [propertyName], [value] )
          + .css( [propertName], [function] )
          + .css( [properties] )](#css-propertname---css--propertynames---css-propertyname-value---css-propertname-function---css-properties-) +
          + ### Markup example we'll be using: ```html @@ -162,53 +263,31 @@ const $ = require('cheerio'); $('li', 'ul', '
            ...
          '); ``` -If you need to modify parsing options for XML input, you may pass an extra -object to `.load()`: +You can also pass an extra object to `.load()` if you need to modify any +of the default parsing options: ```js const $ = cheerio.load('
            ...
          ', { - xml: { - normalizeWhitespace: true, - } + normalizeWhitespace: true, + xmlMode: true }); ``` -The options in the `xml` object are taken directly from [htmlparser2](https://github.com/fb55/htmlparser2/wiki/Parser-options), therefore any options that can be used in `htmlparser2` are valid in cheerio as well. The default options are: +These parsing options are taken directly from [htmlparser2](https://github.com/fb55/htmlparser2/wiki/Parser-options), therefore any options that can be used in `htmlparser2` are valid in cheerio as well. The default options are: ```js { withDomLvl1: true, normalizeWhitespace: false, - xmlMode: true, + xmlMode: false, decodeEntities: true } + ``` For a full list of options and their effects, see [this](https://github.com/fb55/DomHandler) and [htmlparser2's options](https://github.com/fb55/htmlparser2/wiki/Parser-options). -Some users may wish to parse markup with the `htmlparser2` library, and -traverse/manipulate the resulting structure with Cheerio. This may be the case -for those upgrading from pre-1.0 releases of Cheerio (which relied on -`htmlparser2`), for those dealing with invalid markup (because `htmlparser2` is -more forgiving), or for those operating in performance-critical situations -(because `htmlparser2` may be faster in some cases). Note that "more forgiving" -means `htmlparser2` has error-correcting mechanisms that aren't always a match -for the standards observed by web browsers. This behavior may be useful when -parsing non-HTML content. - -To support these cases, `load` also accepts a `htmlparser2`-compatible data -structure as its first argument. Users may install `htmlparser2`, use it to -parse input, and pass the result to `load`: - -```js -// Usage as of htmlparser2 version 3: -const htmlparser2 = require('htmlparser2'); -const dom = htmlparser2.parseDOM(document, options); - -const $ = cheerio.load(dom); -``` - ### Selectors Cheerio's selector implementation is nearly identical to jQuery's, so the API is very similar. @@ -236,6 +315,677 @@ You can select with XML Namespaces but [due to the CSS specification](https://ww $('[xml\\:id="main"'); ``` +### Attributes +Methods for getting and modifying attributes. + +#### .attr( name, value ) +Method for getting and setting attributes. Gets the attribute value for only the first element in the matched set. If you set an attribute's value to `null`, you remove that attribute. You may also pass a `map` and `function` like jQuery. + +```js +$('ul').attr('id') +//=> fruits + +$('.apple').attr('id', 'favorite').html() +//=>
        • Apple
        • +``` + +> See http://api.jquery.com/attr/ for more information + +#### .prop( name, value ) +Method for getting and setting properties. Gets the property value for only the first element in the matched set. + +```js +$('input[type="checkbox"]').prop('checked') +//=> false + +$('input[type="checkbox"]').prop('checked', true).val() +//=> ok +``` + +> See http://api.jquery.com/prop/ for more information + +#### .data( name, value ) +Method for getting and setting data attributes. Gets or sets the data attribute value for only the first element in the matched set. + +```js +$('
          ').data() +//=> { appleColor: 'red' } + +$('
          ').data('apple-color') +//=> 'red' + +const apple = $('.apple').data('kind', 'mac') +apple.data('kind') +//=> 'mac' +``` + +> See http://api.jquery.com/data/ for more information + +#### .val( [value] ) +Method for getting and setting the value of input, select, and textarea. Note: Support for `map`, and `function` has not been added yet. + +```js +$('input[type="text"]').val() +//=> input_text + +$('input[type="text"]').val('test').html() +//=> +``` + +#### .removeAttr( name ) +Method for removing attributes by `name`. + +```js +$('.pear').removeAttr('class').html() +//=>
        • Pear
        • +``` + +#### .hasClass( className ) +Check to see if *any* of the matched elements have the given `className`. + +```js +$('.pear').hasClass('pear') +//=> true + +$('apple').hasClass('fruit') +//=> false + +$('li').hasClass('pear') +//=> true +``` + +#### .addClass( className ) +Adds class(es) to all of the matched elements. Also accepts a `function` like jQuery. + +```js +$('.pear').addClass('fruit').html() +//=>
        • Pear
        • + +$('.apple').addClass('fruit red').html() +//=>
        • Apple
        • +``` + +> See http://api.jquery.com/addClass/ for more information. + +#### .removeClass( [className] ) +Removes one or more space-separated classes from the selected elements. If no `className` is defined, all classes will be removed. Also accepts a `function` like jQuery. + +```js +$('.pear').removeClass('pear').html() +//=>
        • Pear
        • + +$('.apple').addClass('red').removeClass().html() +//=>
        • Apple
        • +``` + +> See http://api.jquery.com/removeClass/ for more information. + +#### .toggleClass( className, [switch] ) +Add or remove class(es) from the matched elements, depending on either the class's presence or the value of the switch argument. Also accepts a `function` like jQuery. + +```js +$('.apple.green').toggleClass('fruit green red').html() +//=>
        • Apple
        • + +$('.apple.green').toggleClass('fruit green red', true).html() +//=>
        • Apple
        • +``` + +> See http://api.jquery.com/toggleClass/ for more information. + +#### .is( selector ) +#### .is( element ) +#### .is( selection ) +#### .is( function(index) ) +Checks the current list of elements and returns `true` if _any_ of the elements match the selector. If using an element or Cheerio selection, returns `true` if _any_ of the elements match. If using a predicate function, the function is executed in the context of the selected element, so `this` refers to the current element. + +### Forms + +#### .serialize() + +Encodes a set of form elements as a URL query string. + +```js +$('
          ').serialize() +//=> foo=bar&foo=qux +``` + +#### .serializeArray() + +Encode a set of form elements as an array of names and values. + +```js +$('
          ').serializeArray() +//=> [ { name: 'foo', value: 'bar' } ] +``` + +### Traversing + +#### .find(selector) +#### .find(selection) +#### .find(node) +Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + +```js +$('#fruits').find('li').length +//=> 3 +$('#fruits').find($('.apple')).length +//=> 1 +``` + +#### .parent([selector]) +Get the parent of each element in the current set of matched elements, optionally filtered by a selector. + +```js +$('.pear').parent().attr('id') +//=> fruits +``` + +#### .parents([selector]) +Get a set of parents filtered by `selector` of each element in the current set of match elements. +```js +$('.orange').parents().length +// => 2 +$('.orange').parents('#fruits').length +// => 1 +``` + +#### .parentsUntil([selector][,filter]) +Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or cheerio object. +```js +$('.orange').parentsUntil('#food').length +// => 1 +``` + +#### .closest(selector) +For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + +```js +$('.orange').closest() +// => [] +$('.orange').closest('.apple') +// => [] +$('.orange').closest('li') +// => [
        • Orange
        • ] +$('.orange').closest('#fruits') +// => [
            ...
          ] +``` + +#### .next([selector]) +Gets the next sibling of the first selected element, optionally filtered by a selector. + +```js +$('.apple').next().hasClass('orange') +//=> true +``` + +#### .nextAll([selector]) +Gets all the following siblings of the first selected element, optionally filtered by a selector. + +```js +$('.apple').nextAll() +//=> [
        • Orange
        • ,
        • Pear
        • ] +$('.apple').nextAll('.orange') +//=> [
        • Orange
        • ] +``` + +#### .nextUntil([selector], [filter]) +Gets all the following siblings up to but not including the element matched by the selector, optionally filtered by another selector. + +```js +$('.apple').nextUntil('.pear') +//=> [
        • Orange
        • ] +``` + +#### .prev([selector]) +Gets the previous sibling of the first selected element optionally filtered by a selector. + +```js +$('.orange').prev().hasClass('apple') +//=> true +``` + +#### .prevAll([selector]) +Gets all the preceding siblings of the first selected element, optionally filtered by a selector. + +```js +$('.pear').prevAll() +//=> [
        • Orange
        • ,
        • Apple
        • ] +$('.pear').prevAll('.orange') +//=> [
        • Orange
        • ] +``` + +#### .prevUntil([selector], [filter]) +Gets all the preceding siblings up to but not including the element matched by the selector, optionally filtered by another selector. + +```js +$('.pear').prevUntil('.apple') +//=> [
        • Orange
        • ] +``` + +#### .slice( start, [end] ) +Gets the elements matching the specified range + +```js +$('li').slice(1).eq(0).text() +//=> 'Orange' + +$('li').slice(1, 2).length +//=> 1 +``` + +#### .siblings([selector]) +Gets the first selected element's siblings, excluding itself. + +```js +$('.pear').siblings().length +//=> 2 + +$('.pear').siblings('.orange').length +//=> 1 + +``` + +#### .children([selector]) +Gets the children of the first selected element. + +```js +$('#fruits').children().length +//=> 3 + +$('#fruits').children('.pear').text() +//=> Pear +``` + +#### .contents() +Gets the children of each element in the set of matched elements, including text and comment nodes. + +```js +$('#fruits').contents().length +//=> 3 +``` + +#### .each( function(index, element) ) +Iterates over a cheerio object, executing a function for each matched element. When the callback is fired, the function is fired in the context of the DOM element, so `this` refers to the current element, which is equivalent to the function parameter `element`. To break out of the `each` loop early, return with `false`. + +```js +const fruits = []; + +$('li').each(function(i, elem) { + fruits[i] = $(this).text(); +}); + +fruits.join(', '); +//=> Apple, Orange, Pear +``` + +#### .map( function(index, element) ) +Pass each element in the current matched set through a function, producing a new Cheerio object containing the return values. The function can return an individual data item or an array of data items to be inserted into the resulting set. If an array is returned, the elements inside the array are inserted into the set. If the function returns null or undefined, no element will be inserted. + +```js +$('li').map(function(i, el) { + // this === el + return $(this).text(); +}).get().join(' '); +//=> "apple orange pear" +``` + +#### .filter( selector )
          .filter( selection )
          .filter( element )
          .filter( function(index, element) ) + +Iterates over a cheerio object, reducing the set of selector elements to those that match the selector or pass the function's test. When a Cheerio selection is specified, return only the elements contained in that selection. When an element is specified, return only that element (if it is contained in the original selection). If using the function method, the function is executed in the context of the selected element, so `this` refers to the current element. + +Selector: + +```js +$('li').filter('.orange').attr('class'); +//=> orange +``` + +Function: + +```js +$('li').filter(function(i, el) { + // this === el + return $(this).attr('class') === 'orange'; +}).attr('class') +//=> orange +``` + +#### .not( selector )
          .not( selection )
          .not( element )
          .not( function(index, elem) ) + +Remove elements from the set of matched elements. Given a jQuery object that represents a set of DOM elements, the `.not()` method constructs a new jQuery object from a subset of the matching elements. The supplied selector is tested against each element; the elements that don't match the selector will be included in the result. The `.not()` method can take a function as its argument in the same way that `.filter()` does. Elements for which the function returns true are excluded from the filtered set; all other elements are included. + +Selector: + +```js +$('li').not('.apple').length; +//=> 2 +``` + +Function: + +```js +$('li').not(function(i, el) { + // this === el + return $(this).attr('class') === 'orange'; +}).length; +//=> 2 +``` + +#### .has( selector )
          .has( element ) + +Filters the set of matched elements to only those which have the given DOM element as a descendant or which have a descendant that matches the given selector. Equivalent to `.filter(':has(selector)')`. + +Selector: + +```js +$('ul').has('.pear').attr('id'); +//=> fruits +``` + +Element: + +```js +$('ul').has($('.pear')[0]).attr('id'); +//=> fruits +``` + +#### .first() +Will select the first element of a cheerio object + +```js +$('#fruits').children().first().text() +//=> Apple +``` + +#### .last() +Will select the last element of a cheerio object + +```js +$('#fruits').children().last().text() +//=> Pear +``` + +#### .eq( i ) +Reduce the set of matched elements to the one at the specified index. Use `.eq(-i)` to count backwards from the last selected element. + +```js +$('li').eq(0).text() +//=> Apple + +$('li').eq(-1).text() +//=> Pear +``` + +#### .get( [i] ) + +Retrieve the DOM elements matched by the Cheerio object. If an index is specified, retrieve one of the elements matched by the Cheerio object: + +```js +$('li').get(0).tagName +//=> li +``` + +If no index is specified, retrieve all elements matched by the Cheerio object: + +```js +$('li').get().length +//=> 3 +``` + +#### .index() +#### .index( selector ) +#### .index( nodeOrSelection ) + +Search for a given element from among the matched elements. + +```js +$('.pear').index() +//=> 2 +$('.orange').index('li') +//=> 1 +$('.apple').index($('#fruit, li')) +//=> 1 +``` + +#### .end() +End the most recent filtering operation in the current chain and return the set of matched elements to its previous state. + +```js +$('li').eq(0).end().length +//=> 3 +``` + +#### .add( selector [, context] ) +#### .add( element ) +#### .add( elements ) +#### .add( html ) +#### .add( selection ) +Add elements to the set of matched elements. + +```js +$('.apple').add('.orange').length +//=> 2 +``` + +#### .addBack( [filter] ) + +Add the previous set of elements on the stack to the current set, optionally filtered by a selector. + +```js +$('li').eq(0).addBack('.orange').length +//=> 2 +``` + +### Manipulation +Methods for modifying the DOM structure. + +#### .append( content, [content, ...] ) +Inserts content as the *last* child of each of the selected elements. + +```js +$('ul').append('
        • Plum
        • ') +$.html() +//=>
            +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          • Plum
          • +//
          +``` + +#### .appendTo( target ) +Insert every element in the set of matched elements to the end of the target. + +```js +$('
        • Plum
        • ').appendTo('#fruits') +$.html() +//=>
            +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          • Plum
          • +//
          +``` + +#### .prepend( content, [content, ...] ) +Inserts content as the *first* child of each of the selected elements. + +```js +$('ul').prepend('
        • Plum
        • ') +$.html() +//=>
            +//
          • Plum
          • +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .prependTo( target ) +Insert every element in the set of matched elements to the beginning of the target. + +```js +$('
        • Plum
        • ').prependTo('#fruits') +$.html() +//=>
            +//
          • Plum
          • +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .after( content, [content, ...] ) +Insert content next to each element in the set of matched elements. + +```js +$('.apple').after('
        • Plum
        • ') +$.html() +//=>
            +//
          • Apple
          • +//
          • Plum
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .insertAfter( target ) +Insert every element in the set of matched elements after the target. + +```js +$('
        • Plum
        • ').insertAfter('.apple') +$.html() +//=>
            +//
          • Apple
          • +//
          • Plum
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .before( content, [content, ...] ) +Insert content previous to each element in the set of matched elements. + +```js +$('.apple').before('
        • Plum
        • ') +$.html() +//=>
            +//
          • Plum
          • +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .insertBefore( target ) +Insert every element in the set of matched elements before the target. + +```js +$('
        • Plum
        • ').insertBefore('.apple') +$.html() +//=>
            +//
          • Plum
          • +//
          • Apple
          • +//
          • Orange
          • +//
          • Pear
          • +//
          +``` + +#### .remove( [selector] ) +Removes the set of matched elements from the DOM and all their children. `selector` filters the set of matched elements to be removed. + +```js +$('.pear').remove() +$.html() +//=>
            +//
          • Apple
          • +//
          • Orange
          • +//
          +``` + +#### .replaceWith( content ) +Replaces matched elements with `content`. + +```js +const plum = $('
        • Plum
        • ') +$('.pear').replaceWith(plum) +$.html() +//=>
            +//
          • Apple
          • +//
          • Orange
          • +//
          • Plum
          • +//
          +``` + +#### .empty() +Empties an element, removing all its children. + +```js +$('ul').empty() +$.html() +//=>
            +``` + +#### .html( [htmlString] ) +Gets an html content string from the first selected element. If `htmlString` is specified, each selected element's content is replaced by the new content. + +```js +$('.orange').html() +//=> Orange + +$('#fruits').html('
          • Mango
          • ').html() +//=>
          • Mango
          • +``` + +#### .text( [textString] ) +Get the combined text contents of each element in the set of matched elements, including their descendants. If `textString` is specified, each selected element's content is replaced by the new text content. + +```js +$('.orange').text() +//=> Orange + +$('ul').text() +//=> Apple +// Orange +// Pear +``` + +#### .wrap( content ) +The .wrap() function can take any string or object that could be passed to the $() factory function to specify a DOM structure. This structure may be nested several levels deep, but should contain only one inmost element. A copy of this structure will be wrapped around each of the elements in the set of matched elements. This method returns the original set of elements for chaining purposes. + +```js +const redFruit = $('
            ') +$('.apple').wrap(redFruit) + +//=>
              +//
              +//
            • Apple
            • +//
              +//
            • Orange
            • +//
            • Plum
            • +//
            + +const healthy = $('
            ') +$('li').wrap(healthy) + +//=>
              +//
              +//
            • Apple
            • +//
              +//
              +//
            • Orange
            • +//
              +//
              +//
            • Plum
            • +//
              +//
            +``` + +#### .css( [propertyName] )
            .css( [ propertyNames] )
            .css( [propertyName], [value] )
            .css( [propertyName], [function] )
            .css( [properties] ) + +Get the value of a style property for the first element in the set of matched elements or set one or more CSS properties for every matched element. + ### Rendering When you're ready to render the document, you can use the `html` utility function: @@ -284,6 +1034,44 @@ cheerio.text($('div')) //=> This is content. ``` +### Miscellaneous +DOM element methods that don't fit anywhere else + +#### .toArray() +Retrieve all the DOM elements contained in the jQuery set as an array. + +```js +$('li').toArray() +//=> [ {...}, {...}, {...} ] +``` + +#### .clone() #### +Clone the cheerio object. + +```js +const moreFruit = $('#fruits').clone() +``` + +### Utilities + +#### $.root + +Sometimes you need to work with the top-level root element. To query it, you can use `$.root()`. + +```js +$.root().append('
              ').html(); +//=>
                ...
                +``` + +#### $.contains( container, contained ) +Checks to see if the `contained` DOM element is a descendant of the `container` DOM element. + +#### $.parseHTML( data [, context ] [, keepScripts ] ) +Parses a string into an array of DOM nodes. The `context` argument has no meaning for Cheerio, but it is maintained for API compatability. + +#### $.load( html[, options ] ) +Load in the HTML. (See the previous section titled "Loading" for more information.) + ### Plugins Once you have loaded a document, you may extend the prototype or the equivalent `fn` property with custom plugin methods: diff --git a/benchmark/.eslintrc.json b/benchmark/.eslintrc.json deleted file mode 100644 index 7753f32e77..0000000000 --- a/benchmark/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-console": 0 - } -} diff --git a/benchmark/benchmark.js b/benchmark/benchmark.js index 1aa0fdf53a..6d1825d94a 100755 --- a/benchmark/benchmark.js +++ b/benchmark/benchmark.js @@ -16,14 +16,10 @@ if (process.argv.indexOf('--cheerio-only') >= 0) { } suites.add('Select all', 'jquery.html', { - test: function($) { - $('*').length; - } + test: function($) { $('*').length; } }); suites.add('Select some', 'jquery.html', { - test: function($) { - $('li').length; - } + test: function($) { $('li').length; } }); /* @@ -79,7 +75,7 @@ suites.add('manipulation - replaceWith', 'jquery.html', { setup: function($) { $('body').append('
                '); }, - test: function($) { + test: function($, $lis) { $('#foo').replaceWith('
                '); } }); @@ -111,14 +107,12 @@ suites.add('manipulation - html render', 'jquery.html', { }); suites.add('manipulation - html independent', 'jquery.html', { setup: function() { - return ( - '
                bat
                baz
                ' + - '
                bat
                baz
                ' + - '
                bat
                baz
                ' + - '
                bat
                baz
                ' + - '
                bat
                baz
                ' + - '
                bat
                baz
                ' - ); + return '
                bat
                baz
                ' + + '
                bat
                baz
                ' + + '
                bat
                baz
                ' + + '
                bat
                baz
                ' + + '
                bat
                baz
                ' + + '
                bat
                baz
                '; }, test: function($, content) { $(content).html(); @@ -134,6 +128,7 @@ suites.add('manipulation - text', 'jquery.html', { } }); + /* * Traversing Tests */ @@ -266,6 +261,7 @@ suites.add('traversing - Eq', 'jquery.html', { } }); + /* * Attributes Tests */ diff --git a/benchmark/suite.js b/benchmark/suite.js index b5d2cf863a..da029ad7ec 100644 --- a/benchmark/suite.js +++ b/benchmark/suite.js @@ -2,18 +2,15 @@ var fs = require('fs'); var path = require('path'); var Benchmark = require('benchmark'); -var jsdom = require('jsdom/lib/old-api.js'); +var jsdom = require('jsdom'); var cheerio = require('..'); var documentDir = path.join(__dirname, 'documents'); -var jQuerySrc = path.join( - __dirname, - '../node_modules/jquery/dist/jquery.slim.js' -); +var jQuerySrc = path.join(__dirname, '../node_modules/jquery/dist/jquery.slim.js'); var filterRe = /./; var cheerioOnly = false; -var Suites = (module.exports = function() {}); +var Suites = module.exports = function() {}; Suites.prototype.filter = function(str) { filterRe = new RegExp(str, 'i'); @@ -24,14 +21,15 @@ Suites.prototype.cheerioOnly = function() { }; Suites.prototype.add = function(name, fileName, options) { - var markup, suite; + var markup, suite, testFn; if (!filterRe.test(name)) { return; } markup = fs.readFileSync(path.join(documentDir, fileName), 'utf8'); suite = new Benchmark.Suite(name); + testFn = options.test; - suite.on('start', function() { + suite.on('start', function(event) { console.log('Test: ' + name + ' (file: ' + fileName + ')'); }); suite.on('cycle', function(event) { diff --git a/jsdoc-config.json b/jsdoc-config.json deleted file mode 100644 index 01e9af8b19..0000000000 --- a/jsdoc-config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "opts": { - "destination": "./docs", - "readme": "Readme.md", - "recurse": true - }, - "plugins": ["plugins/markdown"], - "source": { - "include": ["lib"] - } -} diff --git a/lib/api/attributes.js b/lib/api/attributes.js index 3ce6a90706..6abbb2f7dc 100644 --- a/lib/api/attributes.js +++ b/lib/api/attributes.js @@ -1,9 +1,4 @@ -/** - * Methods for getting and modifying attributes. - * - * @module attributes - */ -var text = require('../static').text, +var $ = require('../static'), utils = require('../utils'), isTag = utils.isTag, domEach = utils.domEach, @@ -17,17 +12,20 @@ var text = require('../static').text, extend: require('lodash/assignIn'), some: require('lodash/some') }, - // Lookup table for coercing string data-* attributes to their corresponding - // JavaScript primitives - primitives = { - null: null, - true: true, - false: false - }, - // Attributes that are booleans - rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - // Matches strings that look like JSON objects or arrays - rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; + + // Lookup table for coercing string data-* attributes to their corresponding + // JavaScript primitives + primitives = { + null: null, + true: true, + false: false + }, + + // Attributes that are booleans + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + // Matches strings that look like JSON objects or arrays + rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; + var getAttr = function(elem, name) { if (!elem || !isTag(elem)) return; @@ -48,46 +46,26 @@ var getAttr = function(elem, name) { // Mimic the DOM and return text content as value for `option's` if (elem.name === 'option' && name === 'value') { - return text(elem.children); + return $.text(elem.children); } // Mimic DOM with default value for radios/checkboxes - if ( - elem.name === 'input' && - (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') && - name === 'value' - ) { + if (elem.name === 'input' && + (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') && + name === 'value') { return 'on'; } }; var setAttr = function(el, name, value) { + if (value === null) { removeAttribute(el, name); } else { - el.attribs[name] = value + ''; + el.attribs[name] = value+''; } }; -/** - * Method for getting and setting attributes. Gets the attribute value for only - * the first element in the matched set. If you set an attribute's value to - * `null`, you remove that attribute. You may also pass a `map` and `function` - * like jQuery. - * - * @example - * - * $('ul').attr('id') - * //=> fruits - * - * $('.apple').attr('id', 'favorite').html() - * //=>
              • Apple
              • - * - * @param {string} name - Name of the attribute. - * @param {string} [value] - If specified sets the value of the attribute. - * - * @see {@link http://api.jquery.com/attr/} - */ exports.attr = function(name, value) { // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { @@ -112,45 +90,31 @@ exports.attr = function(name, value) { return getAttr(this[0], name); }; -var getProp = function(el, name) { +var getProp = function (el, name) { if (!el || !isTag(el)) return; return name in el - ? el[name] - : rboolean.test(name) ? getAttr(el, name) !== undefined : getAttr(el, name); + ? el[name] + : rboolean.test(name) + ? getAttr(el, name) !== undefined + : getAttr(el, name); }; -var setProp = function(el, name, value) { +var setProp = function (el, name, value) { el[name] = rboolean.test(name) ? !!value : value; }; -/** - * Method for getting and setting properties. Gets the property value for only - * the first element in the matched set. - * - * @example - * - * $('input[type="checkbox"]').prop('checked') - * //=> false - * - * $('input[type="checkbox"]').prop('checked', true).val() - * //=> ok - * - * @param {string} name - Name of the property. - * @param {any} [value] - If specified set the property to this. - * - * @see {@link http://api.jquery.com/prop/} - */ -exports.prop = function(name, value) { +exports.prop = function (name, value) { var i = 0, property; if (typeof name === 'string' && value === undefined) { + switch (name) { case 'style': property = this.css(); - _.forEach(property, function(v, p) { + _.forEach(property, function (v, p) { property[i++] = p; }); @@ -161,12 +125,6 @@ exports.prop = function(name, value) { case 'nodeName': property = this[0].name.toUpperCase(); break; - case 'outerHTML': - property = this.clone() - .wrap('') - .parent() - .html(); - break; default: property = getProp(this[0], name); } @@ -175,6 +133,7 @@ exports.prop = function(name, value) { } if (typeof name === 'object' || value !== undefined) { + if (typeof value === 'function') { return domEach(this, function(j, el) { setProp(el, name, value.call(el, j, getProp(el, name))); @@ -185,13 +144,16 @@ exports.prop = function(name, value) { if (!isTag(el)) return; if (typeof name === 'object') { + _.forEach(name, function(val, key) { setProp(el, key, val); }); + } else { setProp(el, name, value); } }); + } }; @@ -239,9 +201,7 @@ var readData = function(el, name) { } else if (rbrace.test(value)) { try { value = JSON.parse(value); - } catch (e) { - /* ignore */ - } + } catch(e){ } } el.data[jsName] = value; @@ -251,27 +211,6 @@ var readData = function(el, name) { return readAll ? el.data : value; }; -/** - * Method for getting and setting data attributes. Gets or sets the data - * attribute value for only the first element in the matched set. - * - * @example - * - * $('
                ').data() - * //=> { appleColor: 'red' } - * - * $('
                ').data('apple-color') - * //=> 'red' - * - * const apple = $('.apple').data('kind', 'mac') - * apple.data('kind') - * //=> 'mac' - * - * @param {string} name - Name of the attribute. - * @param {any} [value] - If specified new value. - * - * @see {@link http://api.jquery.com/data/} - */ exports.data = function(name, value) { var elem = this[0]; @@ -300,47 +239,38 @@ exports.data = function(name, value) { }; /** - * Method for getting and setting the value of input, select, and textarea. - * Note: Support for `map`, and `function` has not been added yet. - * - * @example - * - * $('input[type="text"]').val() - * //=> input_text - * - * $('input[type="text"]').val('test').html() - * //=> - * - * @param {string} [value] - If specified new value. - * - * @see {@link http://api.jquery.com/val/} + * Get the value of an element */ + exports.val = function(value) { var querying = arguments.length === 0, element = this[0]; - if (!element) return; + if(!element) return; switch (element.name) { case 'textarea': return this.text(value); case 'input': - if (this.attr('type') === 'radio') { - if (querying) { - return this.attr('value'); - } - - this.attr('value', value); - return this; + switch (this.attr('type')) { + case 'radio': + if (querying) { + return this.attr('value'); + } else { + this.attr('value', value); + return this; + } + break; + default: + return this.attr('value', value); } - - return this.attr('value', value); + return; case 'select': var option = this.find('option:selected'), returnValue; if (option === undefined) return undefined; if (!querying) { - if (!hasOwn.call(this.attr(), 'multiple') && typeof value == 'object') { + if (!this.attr().hasOwnProperty('multiple') && typeof value == 'object') { return this; } if (typeof value != 'object') { @@ -353,7 +283,7 @@ exports.val = function(value) { return this; } returnValue = option.attr('value'); - if (hasOwn.call(this.attr(), 'multiple')) { + if (this.attr().hasOwnProperty('multiple')) { returnValue = []; domEach(option, function(__, el) { returnValue.push(getAttr(el, 'value')); @@ -370,30 +300,17 @@ exports.val = function(value) { }; /** - * Remove an attribute. - * - * @private - * @param {node} elem - Node to remove attribute from. - * @param {string} name - Name of the attribute to remove. + * Remove an attribute */ + var removeAttribute = function(elem, name) { - if (!elem.attribs || !hasOwn.call(elem.attribs, name)) return; + if (!elem.attribs || !hasOwn.call(elem.attribs, name)) + return; delete elem.attribs[name]; }; -/** - * Method for removing attributes by `name`. - * - * @example - * - * $('.pear').removeAttr('class').html() - * //=>
              • Pear
              • - * - * @param {string} name - Name of the attribute. - * - * @see {@link http://api.jquery.com/removeAttr/} - */ + exports.removeAttr = function(name) { domEach(this, function(i, elem) { removeAttribute(elem, name); @@ -402,24 +319,6 @@ exports.removeAttr = function(name) { return this; }; -/** - * Check to see if *any* of the matched elements have the given `className`. - * - * @example - * - * $('.pear').hasClass('pear') - * //=> true - * - * $('apple').hasClass('fruit') - * //=> false - * - * $('li').hasClass('pear') - * //=> true - * - * @param {string} className - Name of the class. - * - * @see {@link http://api.jquery.com/hasClass/} - */ exports.hasClass = function(className) { return _.some(this, function(elem) { var attrs = elem.attribs, @@ -428,13 +327,11 @@ exports.hasClass = function(className) { end; if (clazz && className.length) { - while ((idx = clazz.indexOf(className, idx + 1)) > -1) { + while ((idx = clazz.indexOf(className, idx+1)) > -1) { end = idx + className.length; - if ( - (idx === 0 || rspace.test(clazz[idx - 1])) && - (end === clazz.length || rspace.test(clazz[end])) - ) { + if ((idx === 0 || rspace.test(clazz[idx-1])) + && (end === clazz.length || rspace.test(clazz[end]))) { return true; } } @@ -442,22 +339,6 @@ exports.hasClass = function(className) { }); }; -/** - * Adds class(es) to all of the matched elements. Also accepts a `function` - * like jQuery. - * - * @example - * - * $('.pear').addClass('fruit').html() - * //=>
              • Pear
              • - * - * $('.apple').addClass('fruit red').html() - * //=>
              • Apple
              • - * - * @param {string} value - Name of new class. - * - * @see {@link http://api.jquery.com/addClass/} - */ exports.addClass = function(value) { // Support functions if (typeof value === 'function') { @@ -473,6 +354,7 @@ exports.addClass = function(value) { var classNames = value.split(rspace), numElements = this.length; + for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on if (!isTag(this[i])) continue; @@ -491,7 +373,8 @@ exports.addClass = function(value) { // Check if class already exists for (var j = 0; j < numClasses; j++) { var appendClass = classNames[j] + ' '; - if (setClass.indexOf(' ' + appendClass) < 0) setClass += appendClass; + if (setClass.indexOf(' ' + appendClass) < 0) + setClass += appendClass; } setAttr(this[i], 'class', setClass.trim()); @@ -505,31 +388,16 @@ var splitClass = function(className) { return className ? className.trim().split(rspace) : []; }; -/** - * Removes one or more space-separated classes from the selected elements. If - * no `className` is defined, all classes will be removed. Also accepts a - * `function` like jQuery. - * - * @example - * - * $('.pear').removeClass('pear').html() - * //=>
              • Pear
              • - * - * $('.apple').addClass('red').removeClass().html() - * //=>
              • Apple
              • - * @param {string} value - Name of the class. - * - * @see {@link http://api.jquery.com/removeClass/} - */ exports.removeClass = function(value) { - var classes, numClasses, removeAll; + var classes, + numClasses, + removeAll; // Handle if value is a function if (typeof value === 'function') { return domEach(this, function(i, el) { exports.removeClass.call( - [el], - value.call(el, i, el.attribs['class'] || '') + [el], value.call(el, i, el.attribs['class'] || '') ); }); } @@ -568,24 +436,6 @@ exports.removeClass = function(value) { }); }; -/** - * Add or remove class(es) from the matched elements, depending on either the - * class's presence or the value of the switch argument. Also accepts a - * `function` like jQuery. - * - * @example - * - * $('.apple.green').toggleClass('fruit green red').html() - * //=>
              • Apple
              • - * - * $('.apple.green').toggleClass('fruit green red', true).html() - * //=>
              • Apple
              • - * - * @param {(string|Function)} value - Name of the class. Can also be a function. - * @param {boolean} [stateVal] - If specified the state of the class. - * - * @see {@link http://api.jquery.com/toggleClass/} - */ exports.toggleClass = function(value, stateVal) { // Support functions if (typeof value === 'function') { @@ -602,11 +452,11 @@ exports.toggleClass = function(value, stateVal) { if (!value || typeof value !== 'string') return this; var classNames = value.split(rspace), - numClasses = classNames.length, - state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0, - numElements = this.length, - elementClasses, - index; + numClasses = classNames.length, + state = typeof stateVal === 'boolean' ? stateVal ? 1 : -1 : 0, + numElements = this.length, + elementClasses, + index; for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on @@ -634,18 +484,7 @@ exports.toggleClass = function(value, stateVal) { return this; }; -/** - * Checks the current list of elements and returns `true` if _any_ of the - * elements match the selector. If using an element or Cheerio selection, - * returns `true` if _any_ of the elements match. If using a predicate - * function, the function is executed in the context of the selected element, - * so `this` refers to the current element. - * - * @param {string|Function|cheerio|node} selector - Selector for the selection. - * - * @see {@link http://api.jquery.com/is/} - */ -exports.is = function(selector) { +exports.is = function (selector) { if (selector) { return this.filter(selector).length > 0; } diff --git a/lib/api/css.js b/lib/api/css.js index 990cb973b8..d9ffae6cfa 100644 --- a/lib/api/css.js +++ b/lib/api/css.js @@ -1,30 +1,23 @@ -/** - * @module css - */ var domEach = require('../utils').domEach, _ = { - pick: require('lodash/pick') + pick: require('lodash/pick'), }; var toString = Object.prototype.toString; /** - * Get the value of a style property for the first element in the set of - * matched elements or set one or more CSS properties for every matched - * element. + * Set / Get css. * - * @param {string|Object} prop - The name of the property. - * @param {string} [val] - If specified the new value. - * @returns {self} - * - * @see {@link http://api.jquery.com/css/} + * @param {String|Object} prop + * @param {String} val + * @return {self} + * @api public */ + exports.css = function(prop, val) { - if ( - arguments.length === 2 || + if (arguments.length === 2 || // When `prop` is a "plain" object - toString.call(prop) === '[object Object]' - ) { + (toString.call(prop) === '[object Object]')) { return domEach(this, function(idx, el) { setCss(el, prop, val, idx); }); @@ -36,13 +29,13 @@ exports.css = function(prop, val) { /** * Set styles of all elements. * - * @param {Object} el - Element to set style of. - * @param {string|Object} prop - Name of property. - * @param {string} val - Value to set property to. - * @param {number} [idx] - Optional index within the selection. - * @returns {self} - * @private + * @param {String|Object} prop + * @param {String} val + * @param {Number} idx - optional index within the selection + * @return {self} + * @api private */ + function setCss(el, prop, val, idx) { if ('string' == typeof prop) { var styles = getCss(el); @@ -58,7 +51,7 @@ function setCss(el, prop, val, idx) { el.attribs.style = stringify(styles); } else if ('object' == typeof prop) { - Object.keys(prop).forEach(function(k) { + Object.keys(prop).forEach(function(k){ setCss(el, k, prop[k]); }); } @@ -67,11 +60,11 @@ function setCss(el, prop, val, idx) { /** * Get parsed styles of the first element. * - * @param {node} el - Element to get styles from. - * @param {string} prop - Name of the prop. - * @returns {Object} - * @private + * @param {String} prop + * @return {Object} + * @api private */ + function getCss(el, prop) { var styles = parse(el.attribs.style); if (typeof prop === 'string') { @@ -86,33 +79,43 @@ function getCss(el, prop) { /** * Stringify `obj` to styles. * - * @param {Object} obj - Object to stringify. - * @returns {Object} - * @private + * @param {Object} obj + * @return {Object} + * @api private */ + function stringify(obj) { - return Object.keys(obj || {}).reduce(function(str, prop) { - return (str += '' + (str ? ' ' : '') + prop + ': ' + obj[prop] + ';'); - }, ''); + return Object.keys(obj || {}) + .reduce(function(str, prop){ + return str += '' + + (str ? ' ' : '') + + prop + + ': ' + + obj[prop] + + ';'; + }, ''); } /** * Parse `styles`. * - * @param {string} styles - Styles to be parsed. - * @returns {Object} - * @private + * @param {String} styles + * @return {Object} + * @api private */ + function parse(styles) { styles = (styles || '').trim(); if (!styles) return {}; - return styles.split(';').reduce(function(obj, str) { - var n = str.indexOf(':'); - // skip if there is no :, or if it is the first/last character - if (n < 1 || n === str.length - 1) return obj; - obj[str.slice(0, n).trim()] = str.slice(n + 1).trim(); - return obj; - }, {}); + return styles + .split(';') + .reduce(function(obj, str){ + var n = str.indexOf(':'); + // skip if there is no :, or if it is the first/last character + if (n < 1 || n === str.length-1) return obj; + obj[str.slice(0,n).trim()] = str.slice(n+1).trim(); + return obj; + }, {}); } diff --git a/lib/api/forms.js b/lib/api/forms.js index ade2adaded..b66f162cd9 100644 --- a/lib/api/forms.js +++ b/lib/api/forms.js @@ -1,6 +1,3 @@ -/** - * @module forms - */ // https://github.com/jquery/jquery/blob/2.1.3/src/manipulation/var/rcheckableType.js // https://github.com/jquery/jquery/blob/2.1.3/src/serialize.js var submittableSelector = 'input,select,textarea,keygen', @@ -10,11 +7,6 @@ var submittableSelector = 'input,select,textarea,keygen', map: require('lodash/map') }; -/** - * Encode a set of form elements as a string for submission. - * - * @see {@link http://api.jquery.com/serialize/} - */ exports.serialize = function() { // Convert form elements into name/value objects var arr = this.serializeArray(); @@ -28,37 +20,26 @@ exports.serialize = function() { return retArr.join('&').replace(r20, '+'); }; -/** - * Encode a set of form elements as an array of names and values. - * - * @example - * $('
                ').serializeArray() - * //=> [ { name: 'foo', value: 'bar' } ] - * - * @see {@link http://api.jquery.com/serializeArray/} - */ exports.serializeArray = function() { // Resolve all form elements from either forms or collections of form elements var Cheerio = this.constructor; return this.map(function() { - var elem = this; - var $elem = Cheerio(elem); - if (elem.name === 'form') { - return $elem.find(submittableSelector).toArray(); - } else { - return $elem.filter(submittableSelector).toArray(); - } - }) - .filter( - // Verify elements have a name (`attr.name`) and are not disabled (`:disabled`) - '[name!=""]:not(:disabled)' + + var elem = this; + var $elem = Cheerio(elem); + if (elem.name === 'form') { + return $elem.find(submittableSelector).toArray(); + } else { + return $elem.filter(submittableSelector).toArray(); + } + }).filter( + // Verify elements have a name (`attr.name`) and are not disabled (`:disabled`) + '[name!=""]:not(:disabled)' // and cannot be clicked (`[type=submit]`) or are used in `x-www-form-urlencoded` (`[type=file]`) - ':not(:submit, :button, :image, :reset, :file)' + + + ':not(:submit, :button, :image, :reset, :file)' // and are either checked/don't have a checkable state - ':matches([checked], :not(:checkbox, :radio))' - // Convert each of the elements to its value(s) - ) - .map(function(i, elem) { + + ':matches([checked], :not(:checkbox, :radio))' + // Convert each of the elements to its value(s) + ).map(function(i, elem) { var $elem = Cheerio(elem); var name = $elem.attr('name'); var value = $elem.val(); @@ -73,13 +54,12 @@ exports.serializeArray = function() { return _.map(value, function(val) { // We trim replace any line endings (e.g. `\r` or `\r\n` with `\r\n`) to guarantee consistency across platforms // These can occur inside of `'; // Comments var comment = ''; -var conditional = - ''; +var conditional = ''; // Text var text = 'lorem ipsum'; @@ -37,17 +37,20 @@ var styleEmpty = ''; // Directives var directive = ''; + describe('parse', function() { + describe('.eval', function() { + it('should parse basic empty tags: ' + basic, function() { - var tag = parse.evaluate(basic, defaultOpts, true)[0]; + var tag = parse.evaluate(basic, defaultOpts)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('html'); - expect(tag.childNodes).to.have.length(2); + expect(tag.childNodes).to.be.empty(); }); it('should handle sibling tags: ' + siblings, function() { - var dom = parse.evaluate(siblings, defaultOpts, false), + var dom = parse.evaluate(siblings, defaultOpts), h2 = dom[0], p = dom[1]; @@ -57,70 +60,66 @@ describe('parse', function() { }); it('should handle single tags: ' + single, function() { - var tag = parse.evaluate(single, defaultOpts, false)[0]; + var tag = parse.evaluate(single, defaultOpts)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('br'); expect(tag.childNodes).to.be.empty(); }); it('should handle malformatted single tags: ' + singleWrong, function() { - var tag = parse.evaluate(singleWrong, defaultOpts, false)[0]; + var tag = parse.evaluate(singleWrong, defaultOpts)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('br'); expect(tag.childNodes).to.be.empty(); }); it('should handle tags with children: ' + children, function() { - var tag = parse.evaluate(children, defaultOpts, true)[0]; + var tag = parse.evaluate(children, defaultOpts)[0]; expect(tag.type).to.equal('tag'); expect(tag.tagName).to.equal('html'); expect(tag.childNodes).to.be.ok(); - expect(tag.childNodes[1].tagName).to.equal('body'); - expect(tag.childNodes[1].childNodes).to.have.length(1); + expect(tag.childNodes).to.have.length(1); }); it('should handle tags with children: ' + li, function() { - var tag = parse.evaluate(li, defaultOpts, false)[0]; + var tag = parse.evaluate(li, defaultOpts)[0]; expect(tag.childNodes).to.have.length(1); expect(tag.childNodes[0].data).to.equal('Durian'); }); it('should handle tags with attributes: ' + attributes, function() { - var attrs = parse.evaluate(attributes, defaultOpts, false)[0].attribs; + var attrs = parse.evaluate(attributes, defaultOpts)[0].attribs; expect(attrs).to.be.ok(); expect(attrs.src).to.equal('hello.png'); expect(attrs.alt).to.equal('man waving'); }); it('should handle value-less attributes: ' + noValueAttribute, function() { - var attrs = parse.evaluate(noValueAttribute, defaultOpts, false)[0] - .attribs; + var attrs = parse.evaluate(noValueAttribute, defaultOpts)[0].attribs; expect(attrs).to.be.ok(); expect(attrs.disabled).to.equal(''); }); it('should handle comments: ' + comment, function() { - var elem = parse.evaluate(comment, defaultOpts, false)[0]; + var elem = parse.evaluate(comment, defaultOpts)[0]; expect(elem.type).to.equal('comment'); expect(elem.data).to.equal(' sexy '); }); it('should handle conditional comments: ' + conditional, function() { - var elem = parse.evaluate(conditional, defaultOpts, false)[0]; + var elem = parse.evaluate(conditional, defaultOpts)[0]; expect(elem.type).to.equal('comment'); - expect(elem.data).to.equal( - conditional.replace('', '') - ); + expect(elem.data).to.equal(conditional.replace('', '')); }); it('should handle text: ' + text, function() { - var text_ = parse.evaluate(text, defaultOpts, false)[0]; + var text_ = parse.evaluate(text, defaultOpts)[0]; expect(text_.type).to.equal('text'); expect(text_.data).to.equal('lorem ipsum'); }); it('should handle script tags: ' + script, function() { - var script_ = parse.evaluate(script, defaultOpts, false)[0]; + var script_ = parse.evaluate(script, defaultOpts)[0]; expect(script_.type).to.equal('script'); expect(script_.tagName).to.equal('script'); expect(script_.attribs.type).to.equal('text/javascript'); @@ -130,7 +129,7 @@ describe('parse', function() { }); it('should handle style tags: ' + style, function() { - var style_ = parse.evaluate(style, defaultOpts, false)[0]; + var style_ = parse.evaluate(style, defaultOpts)[0]; expect(style_.type).to.equal('style'); expect(style_.tagName).to.equal('style'); expect(style_.attribs.type).to.equal('text/css'); @@ -140,14 +139,16 @@ describe('parse', function() { }); it('should handle directives: ' + directive, function() { - var elem = parse.evaluate(directive, defaultOpts, true)[0]; + var elem = parse.evaluate(directive, defaultOpts)[0]; expect(elem.type).to.equal('directive'); - expect(elem.data).to.equal('!DOCTYPE html'); + expect(elem.data).to.equal('!doctype html'); expect(elem.tagName).to.equal('!doctype'); }); + }); describe('.parse', function() { + // root test utility function rootTest(root) { expect(root.tagName).to.equal('root'); @@ -162,14 +163,14 @@ describe('parse', function() { } it('should add root to: ' + basic, function() { - var root = parse(basic, defaultOpts, true); + var root = parse(basic, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].tagName).to.equal('html'); }); it('should add root to: ' + siblings, function() { - var root = parse(siblings, defaultOpts, false); + var root = parse(siblings, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(2); expect(root.childNodes[0].tagName).to.equal('h2'); @@ -178,46 +179,42 @@ describe('parse', function() { }); it('should add root to: ' + comment, function() { - var root = parse(comment, defaultOpts, false); + var root = parse(comment, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('comment'); }); it('should add root to: ' + text, function() { - var root = parse(text, defaultOpts, false); + var root = parse(text, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('text'); }); it('should add root to: ' + scriptEmpty, function() { - var root = parse(scriptEmpty, defaultOpts, false); + var root = parse(scriptEmpty, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('script'); }); it('should add root to: ' + styleEmpty, function() { - var root = parse(styleEmpty, defaultOpts, false); + var root = parse(styleEmpty, defaultOpts); rootTest(root); expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('style'); }); it('should add root to: ' + directive, function() { - var root = parse(directive, defaultOpts, true); + var root = parse(directive, defaultOpts); rootTest(root); - expect(root.childNodes).to.have.length(2); + expect(root.childNodes).to.have.length(1); expect(root.childNodes[0].type).to.equal('directive'); }); it('should expose the DOM level 1 API', function() { - var root = parse( - '

                ', - defaultOpts, - false - ).childNodes[0]; + var root = parse('

                ', defaultOpts).childNodes[0]; var childNodes = root.childNodes; expect(childNodes).to.have.length(3); @@ -250,130 +247,6 @@ describe('parse', function() { expect(childNodes[2].firstChild).to.be(null); expect(childNodes[2].lastChild).to.be(null); }); - - it('Should parse less than or equal sign sign', function() { - var root = parse('A<=B', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes[0].tagName).to.be('i'); - expect(childNodes[0].childNodes[0].data).to.be('A'); - expect(childNodes[1].data).to.be('<='); - expect(childNodes[2].tagName).to.be('i'); - expect(childNodes[2].childNodes[0].data).to.be('B'); - }); - - it('Should ignore unclosed CDATA', function() { - var root = parse( - '', - defaultOpts, - false - ); - var childNodes = root.childNodes; - - expect(childNodes[0].tagName).to.be('a'); - expect(childNodes[1].tagName).to.be('script'); - expect(childNodes[1].childNodes[0].data).to.be('foo // to documents', function() { - var root = parse('', defaultOpts, true); - var childNodes = root.childNodes; - - expect(childNodes[0].tagName).to.be('html'); - expect(childNodes[0].childNodes[0].tagName).to.be('head'); - }); - - it('Should implicitly create around ', function() { - var root = parse('
                bar
                ', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes[0].tagName).to.be('table'); - expect(childNodes[0].childNodes.length).to.be(1); - expect(childNodes[0].childNodes[0].tagName).to.be('tbody'); - expect(childNodes[0].childNodes[0].childNodes[0].tagName).to.be('tr'); - expect( - childNodes[0].childNodes[0].childNodes[0].childNodes[0].tagName - ).to.be('td'); - expect( - childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0] - .data - ).to.be('bar'); - }); - - it('Should parse custom tag ', function() { - var root = parse('test', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes.length).to.be(1); - expect(childNodes[0].tagName).to.be('line'); - expect(childNodes[0].childNodes[0].data).to.be('test'); - }); - - it('Should properly parse misnested table tags', function() { - var root = parse( - 'i1i2i3', - defaultOpts, - false - ); - var childNodes = root.childNodes; - - expect(childNodes.length).to.be(3); - - childNodes.forEach(function(child, i) { - expect(child.tagName).to.be('tr'); - expect(child.childNodes[0].tagName).to.be('td'); - expect(child.childNodes[0].childNodes[0].data).to.be('i' + (i + 1)); - }); - }); - - it('Should correctly parse data url attributes', function() { - var html = - '
                '; - var expectedAttr = - 'font-family:"butcherman-caps"; src:url(data:font/opentype;base64,AAEA...);'; - var root = parse(html, defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes[0].attribs.style).to.be(expectedAttr); - }); - - it('Should treat tag content as text', function() { - var root = parse('<xmp><h2>', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes[0].childNodes[0].data).to.be('

                '); - }); - - it('Should correctly parse malformed numbered entities', function() { - var root = parse('

                z&#

                ', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes[0].childNodes[0].data).to.be('z&#'); - }); - - it('Should correctly parse mismatched headings', function() { - var root = parse('

                Test

                ', defaultOpts, false); - var childNodes = root.childNodes; - - expect(childNodes.length).to.be(2); - expect(childNodes[0].tagName).to.be('h2'); - expect(childNodes[1].tagName).to.be('div'); - }); - - it('Should correctly parse tricky
                 content', function() {
                -      var root = parse(
                -        '
                \nA <- factor(A, levels = c("c","a","b"))\n
                ', - defaultOpts, - false - ); - var childNodes = root.childNodes; - - expect(childNodes.length).to.be(1); - expect(childNodes[0].tagName).to.be('pre'); - expect(childNodes[0].childNodes[0].data).to.be( - 'A <- factor(A, levels = c("c","a","b"))\n' - ); - }); }); + }); diff --git a/test/xml.js b/test/xml.js index d24e3feb77..cc23133532 100644 --- a/test/xml.js +++ b/test/xml.js @@ -5,9 +5,9 @@ var expect = require('expect.js'), }; var xml = function(str, options) { - options = _.extend({ xml: true }, options); - var $ = cheerio.load(str, options); - return $.xml(); + options = _.extend({ xmlMode: true }, options); + var dom = cheerio.load(str, options); + return dom.xml(); }; var dom = function(str, options) { @@ -16,13 +16,12 @@ var dom = function(str, options) { }; describe('render', function() { + describe('(xml)', function() { + it('should render tags correctly', function() { - var str = - ''; - expect(xml(str)).to.equal( - '' - ); + var str = ''; + expect(xml(str)).to.equal(''); }); it('should render tags (RSS) correctly', function() { @@ -30,34 +29,32 @@ describe('render', function() { expect(xml(str)).to.equal('http://www.github.com/'); }); - it('should escape entities', function() { + it('should escape entities', function(){ var str = ''; expect(xml(str)).to.equal(str); }); + }); - describe('(dom)', function() { + describe('(dom)', function () { + it('should keep camelCase for new nodes', function() { var str = 'hello'; - expect(dom(str, { xml: false })).to.equal( - 'hello' - ); + expect(dom(str, {xmlMode: false})).to.equal('hello'); }); it('should keep camelCase for new nodes', function() { var str = 'hello'; - expect(dom(str, { xml: true })).to.equal( - 'hello' - ); + expect(dom(str, {xmlMode: true})).to.equal('hello'); }); it('should maintain the parsing options of distinct contexts independently', function() { var str = 'hello'; - var $ = cheerio.load('', { xml: false }); + var $x = cheerio.load('', { xmlMode: false }); - expect($(str).html()).to.equal( - 'hello' - ); + expect($x(str).html()).to.equal('hello'); }); + }); + }); From 426b555173fbb6ac4718a38c23e261ce0235ca6a Mon Sep 17 00:00:00 2001 From: Luan Muniz Date: Wed, 30 Jan 2019 01:27:54 -0200 Subject: [PATCH 11/16] Fix error for cheerio.css cheerio.css on empty elements set throws, jQuery returns undefined. This PR Checks for the element before checking the styles. Include tests Signed-off-by: Luan --- lib/api/css.js | 4 ++++ test/api/css.js | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/api/css.js b/lib/api/css.js index d9ffae6cfa..d784b68d22 100644 --- a/lib/api/css.js +++ b/lib/api/css.js @@ -66,6 +66,10 @@ function setCss(el, prop, val, idx) { */ function getCss(el, prop) { + if(!el || !el.attribs) { + return undefined; + } + var styles = parse(el.attribs.style); if (typeof prop === 'string') { return styles[prop]; diff --git a/test/api/css.js b/test/api/css.js index 9e394c44f0..1f75abb4a4 100644 --- a/test/api/css.js +++ b/test/api/css.js @@ -42,6 +42,16 @@ describe('$(...)', function() { expect(el.css()).to.eql({position:'absolute'}); }); + it('(prop): should return undefined for unmatched elements', function() { + var $ = cheerio.load('
              • '); + expect($('ul').css('background-image')).to.eql(undefined); + }); + + it('(prop): should return undefined for unmatched styles', function() { + var el = cheerio('
              • '); + expect(el.css('margin')).to.eql(undefined); + }); + describe('(prop, function):', function() { beforeEach(function() { this.$el = cheerio('
                '); From bfd857f84cf30236d4257317d04adf25389c85d3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 12 Oct 2018 11:44:51 +0200 Subject: [PATCH 12/16] Do not use the deprecated Buffer() constructor Refs: https://github.com/nodejs/citgm/issues/605 Refs: https://nodejs.org/api/deprecations.html#deprecations_dep0005_buffer_constructor --- test/api/utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/api/utils.js b/test/api/utils.js index d6983c8a1c..a750f28377 100644 --- a/test/api/utils.js +++ b/test/api/utils.js @@ -101,8 +101,12 @@ describe('cheerio', function() { // }); it('(buffer) : should accept a buffer', function() { - var $html = cheerio.load(new Buffer('
                foo
                ')); - expect($html.html()).to.be('
                foo
                '); + var html = '
                foo
                '; + var buf = Buffer.from && Buffer.from !== Uint8Array.from + ? Buffer.from(html) + : new Buffer(html); + var $html = cheerio.load(buf); + expect($html.html()).to.be(html); }); }); From 9afacf89ff37e4e1f8371d2910c7e7c08ba8bb12 Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Wed, 13 Feb 2019 21:16:12 +0100 Subject: [PATCH 13/16] (docs) fixed some small errors --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 8d8fdc325a..e057f75061 100644 --- a/Readme.md +++ b/Readme.md @@ -1067,7 +1067,7 @@ $.root().append('
                  ').html(); Checks to see if the `contained` DOM element is a descendant of the `container` DOM element. #### $.parseHTML( data [, context ] [, keepScripts ] ) -Parses a string into an array of DOM nodes. The `context` argument has no meaning for Cheerio, but it is maintained for API compatability. +Parses a string into an array of DOM nodes. The `context` argument has no meaning for Cheerio, but it is maintained for API compatibility. #### $.load( html[, options ] ) Load in the HTML. (See the previous section titled "Loading" for more information.) @@ -1087,7 +1087,7 @@ $('body').logHtml(); // logs "Hello, world!" to the console ### The "DOM Node" object -Cheerio collections are made up of objects that bear some resemblence to [browser-based DOM nodes](https://developer.mozilla.org/en-US/docs/Web/API/Node). You can expect them to define the following properties: +Cheerio collections are made up of objects that bear some resemblance to [browser-based DOM nodes](https://developer.mozilla.org/en-US/docs/Web/API/Node). You can expect them to define the following properties: - `tagName` - `parentNode` From 7a9ff22f68bd6dbdf04f8e3a52e28a6b369b5fa7 Mon Sep 17 00:00:00 2001 From: dzcpy Date: Mon, 22 Apr 2019 01:38:12 +0800 Subject: [PATCH 14/16] Update dependencies & fix vulnerability (#1281) * update dependencies * update dependencies * Update package.json * Update package.json * Fix weird xml mode issue * Fix package-lock.json and update vulnerable modules * Remove Node 0.12 from Travis test environments * Fix JSDOM API issue * Fix JSDOM new API * Delete package-lock.json --- .travis.yml | 1 - benchmark/suite.js | 25 ++++++++++++------------- package.json | 26 +++++++++++++------------- test/api/traversing.js | 8 ++------ test/cheerio.js | 8 +------- test/mocha.opts | 1 - 6 files changed, 28 insertions(+), 41 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7d7df23f8..e8b962b198 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ node_js: - "unstable" - "6" - "4" - - "0.12" script: make travis-test matrix: fast_finish: true diff --git a/benchmark/suite.js b/benchmark/suite.js index da029ad7ec..d45ccf7eb8 100644 --- a/benchmark/suite.js +++ b/benchmark/suite.js @@ -62,20 +62,19 @@ Suites.prototype.add = function(name, fileName, options) { Suites.prototype._benchJsDom = function(suite, markup, options) { var testFn = options.test; - jsdom.env({ - html: markup, - scripts: jQuerySrc, - done: function(err, window) { - var setupData; - if (options.setup) { - setupData = options.setup.call(null, window.$); - } - suite.add('jsdom', function() { - testFn.call(null, window.$, setupData); - }); - suite.run(); - } + var { JSDOM } = jsdom; + var { window } = new JSDOM(markup); + window.$ = require(jQuerySrc)(window); + + var setupData; + if (options.setup) { + setupData = options.setup.call(null, window.$); + } + suite.add('jsdom', function() { + testFn.call(null, window.$, setupData); }); + suite.run(); + }; Suites.prototype._benchCheerio = function(suite, markup, options) { diff --git a/package.json b/package.json index 91d4e0aa34..e824ae1578 100644 --- a/package.json +++ b/package.json @@ -25,22 +25,22 @@ "node": ">= 0.6" }, "dependencies": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0" + "css-select": "~2.0.2", + "dom-serializer": "~0.1.1", + "entities": "~1.1.2", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.11" }, "devDependencies": { - "benchmark": "^2.1.0", - "coveralls": "^2.11.9", + "benchmark": "^2.1.4", + "coveralls": "^3.0.2", "expect.js": "~0.3.1", - "istanbul": "^0.4.3", - "jsdom": "^9.2.1", - "jquery": "^3.0.0", - "jshint": "^2.9.2", - "mocha": "^3.1.2", - "xyz": "~1.1.0" + "istanbul": "^0.4.5", + "jquery": "^3.3.1", + "jsdom": "^13.2.0", + "jshint": "^2.10.1", + "mocha": "^5.2.0", + "xyz": "~3.0.0" }, "scripts": { "test": "jshint lib/ test/ && mocha --recursive --reporter dot" diff --git a/test/api/traversing.js b/test/api/traversing.js index 06a969ca42..939ffb36d1 100644 --- a/test/api/traversing.js +++ b/test/api/traversing.js @@ -63,9 +63,7 @@ describe('$(...)', function() { it('should throw a SyntaxError if given an invalid selector', function() { expect(function() { $('#fruits').find(':bah'); - }).to.throwException(function(err) { - expect(err).to.be.a(SyntaxError); - }); + }).to.throwException(/unmatched pseudo-class/); }); describe('(cheerio object) :', function() { @@ -456,9 +454,7 @@ describe('$(...)', function() { it('(selector) : should throw a SyntaxError if given an invalid selector', function() { expect(function() { $('.orange').siblings(':bah'); - }).to.throwException(function(err) { - expect(err).to.be.a(SyntaxError); - }); + }).to.throwException(/unmatched pseudo-class/); }); it('(selector) : does not consider the contents of siblings when filtering (GH-374)', function() { diff --git a/test/cheerio.js b/test/cheerio.js index e477add9d3..be2785a6fc 100644 --- a/test/cheerio.js +++ b/test/cheerio.js @@ -162,12 +162,6 @@ describe('cheerio', function() { expect($fruits[0].attribs.id).to.equal('fruits'); }); - it('should select first element $(:first)'); - // var $elem = $(':first', fruits); - // var $h2 = $('

                  fruits

                  '); - // console.log($elem.before('hi')); - // console.log($elem.before($h2)); - it('should be able to select immediate children: $("#fruits > .pear")', function() { var $food = $(food); $('.pear', $food).append('
                • Another Pear!
                • '); @@ -368,7 +362,7 @@ describe('cheerio', function() { var str = '', // since parsing done without xmlMode flag, all tags converted to lowercase expectedXml = '', - expectedNoXml = '', + expectedNoXml = '', dom = $.load(str); expect(dom('MixedCaseTag').get(0).tagName).to.equal('mixedcasetag'); diff --git a/test/mocha.opts b/test/mocha.opts index 9431de4c17..b8a05a88fc 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,2 +1 @@ --reporter list ---growl \ No newline at end of file From 6a4886232f4d140b631b3dc5664c5a59962f9286 Mon Sep 17 00:00:00 2001 From: Matthew Mueller Date: Sun, 5 May 2019 18:47:38 +0700 Subject: [PATCH 15/16] Remove 8x performance claim This was true many years ago, but JSDOM is a lot faster now. See [this issue](https://github.com/cheeriojs/cheerio/issues/700) for more details. --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index e057f75061..51dce11389 100644 --- a/Readme.md +++ b/Readme.md @@ -45,7 +45,7 @@ __❤ Familiar syntax:__ Cheerio implements a subset of core jQuery. Cheerio removes all the DOM inconsistencies and browser cruft from the jQuery library, revealing its truly gorgeous API. __ϟ Blazingly fast:__ -Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient. Preliminary end-to-end benchmarks suggest that cheerio is about __8x__ faster than JSDOM. +Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient. __❁ Incredibly flexible:__ Cheerio wraps around @FB55's forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. From 3368605edb3c5babecc8576602a9d54ccfdaef1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= Date: Mon, 10 Jun 2019 10:22:58 -0700 Subject: [PATCH 16/16] Create FUNDING.yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..f3fc95057c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: cheerio +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with a single custom sponsorship URL