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() +//=> +``` + +#### .appendTo( target ) +Insert every element in the set of matched elements to the end of the target. + +```js +$('
  • Plum
  • ').appendTo('#fruits') +$.html() +//=> +``` + +#### .prepend( content, [content, ...] ) +Inserts content as the *first* child of each of the selected elements. + +```js +$('ul').prepend('
  • Plum
  • ') +$.html() +//=> +``` + +#### .prependTo( target ) +Insert every element in the set of matched elements to the beginning of the target. + +```js +$('
  • Plum
  • ').prependTo('#fruits') +$.html() +//=> +``` + +#### .after( content, [content, ...] ) +Insert content next to each element in the set of matched elements. + +```js +$('.apple').after('
  • Plum
  • ') +$.html() +//=> +``` + +#### .insertAfter( target ) +Insert every element in the set of matched elements after the target. + +```js +$('
  • Plum
  • ').insertAfter('.apple') +$.html() +//=> +``` + +#### .before( content, [content, ...] ) +Insert content previous to each element in the set of matched elements. + +```js +$('.apple').before('
  • Plum
  • ') +$.html() +//=> +``` + +#### .insertBefore( target ) +Insert every element in the set of matched elements before the target. + +```js +$('
  • Plum
  • ').insertBefore('.apple') +$.html() +//=> +``` + +#### .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() +//=> +``` + +#### .replaceWith( content ) +Replaces matched elements with `content`. + +```js +const plum = $('
  • Plum
  • ') +$('.pear').replaceWith(plum) +$.html() +//=> +``` + +#### .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) + +//=> + +const healthy = $('
    ') +$('li').wrap(healthy) + +//=> +``` + +#### .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'); }); + }); + });