Repository: cheeriojs/cheerio Branch: main Commit: 1e7f681c84a1 Files: 104 Total size: 713.4 KB Directory structure: gitextract_2mrqq2gl/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── issue_template.md │ └── workflows/ │ ├── benchmark.yml │ ├── ci.yml │ ├── codeql.yml │ ├── dependabot-automerge.yml │ ├── lint.yml │ ├── site.yml │ └── sponsors.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── SECURITY.md ├── benchmark/ │ ├── benchmark.ts │ └── documents/ │ └── jquery.html ├── biome.json ├── eslint.config.js ├── package.json ├── scripts/ │ └── fetch-sponsors.mts ├── src/ │ ├── __fixtures__/ │ │ └── fixtures.ts │ ├── __tests__/ │ │ ├── deprecated.spec.ts │ │ └── xml.spec.ts │ ├── api/ │ │ ├── attributes.spec.ts │ │ ├── attributes.ts │ │ ├── css.spec.ts │ │ ├── css.ts │ │ ├── extract.spec.ts │ │ ├── extract.ts │ │ ├── forms.spec.ts │ │ ├── forms.ts │ │ ├── manipulation.spec.ts │ │ ├── manipulation.ts │ │ ├── traversing.spec.ts │ │ └── traversing.ts │ ├── cheerio.spec.ts │ ├── cheerio.ts │ ├── index-browser.mts │ ├── index.spec.ts │ ├── index.ts │ ├── load-parse.ts │ ├── load.spec.ts │ ├── load.ts │ ├── options.ts │ ├── parse.spec.ts │ ├── parse.ts │ ├── parsers/ │ │ └── parse5-adapter.ts │ ├── slim.ts │ ├── static.spec.ts │ ├── static.ts │ ├── types.ts │ ├── utils.spec.ts │ └── utils.ts ├── tsconfig.json ├── tsconfig.typedoc.json ├── vitest.config.ts └── website/ ├── README.md ├── astro.config.mjs ├── package.json ├── sponsors.json ├── src/ │ ├── components/ │ │ ├── Features.astro │ │ ├── Footer.astro │ │ ├── Hero.astro │ │ ├── LiveEditor.astro │ │ ├── Navbar.astro │ │ ├── Sidebar.astro │ │ ├── Sponsors.astro │ │ ├── TableOfContents.astro │ │ ├── Testimonials.astro │ │ └── live-code.tsx │ ├── content/ │ │ ├── blog/ │ │ │ ├── 2023-02-13-new-website.md │ │ │ └── 2024-08-07-version-1.md │ │ ├── config.ts │ │ └── docs/ │ │ ├── advanced/ │ │ │ ├── configuring-cheerio.md │ │ │ ├── extending-cheerio.md │ │ │ └── extract.md │ │ ├── basics/ │ │ │ ├── loading.md │ │ │ ├── manipulation.md │ │ │ ├── selecting.md │ │ │ └── traversing.mdx │ │ └── intro.md │ ├── env.d.ts │ ├── layouts/ │ │ ├── BaseLayout.astro │ │ ├── BlogLayout.astro │ │ └── DocsLayout.astro │ ├── pages/ │ │ ├── blog/ │ │ │ ├── [slug].astro │ │ │ ├── index.astro │ │ │ └── rss.xml.ts │ │ ├── docs/ │ │ │ ├── [slug].astro │ │ │ ├── advanced/ │ │ │ │ └── [slug].astro │ │ │ ├── api/ │ │ │ │ └── [...slug].astro │ │ │ └── basics/ │ │ │ └── [slug].astro │ │ └── index.astro │ ├── plugins/ │ │ ├── rehype-external-links.ts │ │ ├── remark-admonitions.ts │ │ ├── remark-fix-typedoc-links.ts │ │ └── remark-live-code.ts │ └── styles/ │ └── global.css ├── tsconfig.json └── typedoc.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Enforce Unix newlines * text=auto eol=lf benchmark/documents/* binary benchmark/jquery*.js binary ================================================ FILE: .github/FUNDING.yml ================================================ github: [cheeriojs, fb55] open_collective: cheerio ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: '/' schedule: interval: daily open-pull-requests-limit: 10 versioning-strategy: increase - package-ecosystem: npm directory: '/website' schedule: interval: daily open-pull-requests-limit: 4 versioning-strategy: increase - package-ecosystem: 'github-actions' directory: '/' schedule: interval: daily ================================================ FILE: .github/issue_template.md ================================================ ================================================ FILE: .github/workflows/benchmark.yml ================================================ name: Benchmark on: push: branches-ignore: - 'dependabot/**' pull_request: env: FORCE_COLOR: 2 permissions: contents: read jobs: benchmark: runs-on: ubuntu-latest if: "!contains(github.event.commits[0].message, '[bench skip]') && !contains(github.event.commits[0].message, '[skip bench]')" steps: - name: Clone repository uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6.3.0 with: node-version: lts/* cache: 'npm' - name: Install npm dependencies run: npm ci - name: Run benchmarks run: npm run benchmark env: BENCHMARK: true ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches-ignore: - 'dependabot/**' pull_request: env: FORCE_COLOR: 2 NODE_COV: lts/* # The Node.js version to run coveralls on permissions: contents: read jobs: run: permissions: checks: write # for coverallsapp/github-action to create new checks contents: read # for actions/checkout to fetch code name: Node ${{ matrix.node }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: - 20 - 22 - 24 - lts/* steps: - name: Clone repository uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6.3.0 with: node-version: '${{ matrix.node }}' cache: 'npm' - name: Install npm dependencies run: npm ci - name: Run tests run: npm run test:vi if: matrix.node != env.NODE_COV - name: Run tests with coverage run: npm run test:vi -- --coverage if: matrix.node == env.NODE_COV - name: Run Coveralls uses: coverallsapp/github-action@v2.3.7 if: matrix.node == env.NODE_COV continue-on-error: true with: github-token: '${{ secrets.GITHUB_TOKEN }}' ================================================ FILE: .github/workflows/codeql.yml ================================================ name: 'CodeQL' on: push: branches: - main - '!dependabot/**' pull_request: # The branches below must be a subset of the branches above branches: - main - '!dependabot/**' schedule: - cron: '0 0 * * 0' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: 'javascript' - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ================================================ FILE: .github/workflows/dependabot-automerge.yml ================================================ # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request name: Dependabot auto-merge on: pull_request_target permissions: pull-requests: write contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2.5.0 with: github-token: '${{ secrets.GITHUB_TOKEN }}' - name: Enable auto-merge for Dependabot PRs # Automatically merge semver-patch and semver-minor PRs if: "${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch' }}" run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches-ignore: - 'dependabot/**' pull_request: env: FORCE_COLOR: 2 permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6.3.0 with: node-version: lts/* cache: 'npm' - name: Install npm dependencies run: npm ci - name: Install website npm dependencies run: npm ci working-directory: website - name: Build Cheerio run: npm run build - name: Undo changes to package.json run: git restore package.json - name: Build website run: npm run build working-directory: website - name: Run lint run: npm run lint ================================================ FILE: .github/workflows/site.yml ================================================ name: Deploy website to GitHub Pages # Based on https://raw.githubusercontent.com/actions/starter-workflows on: # Runs on pushes targeting the main branch push: branches: [main] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: env: FORCE_COLOR: 2 # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow one concurrent deployment concurrency: group: 'pages' cancel-in-progress: true jobs: # Build job build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node uses: actions/setup-node@v6.3.0 with: # Use current Node LTS version node-version: lts/* cache: 'npm' - name: Setup Pages id: pages uses: actions/configure-pages@v5 - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Install website dependencies working-directory: website run: npm ci - name: Build docs working-directory: website run: npm run build - name: Upload artifact uses: actions/upload-pages-artifact@v4 with: path: ./website/dist # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build if: ${{github.repository == 'cheeriojs/cheerio'}} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/sponsors.yml ================================================ name: Update Sponsors on: schedule: # Run once a day, at 4pm - cron: '0 16 * * *' # Allow manual trigger workflow_dispatch: env: FORCE_COLOR: 2 permissions: contents: read jobs: fetch: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6.3.0 with: node-version: lts/* cache: 'npm' - name: Install npm dependencies run: npm ci - name: Update the README run: npm run update-sponsors env: CHEERIO_SPONSORS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IMGIX_TOKEN: ${{ secrets.IMGIX_TOKEN }} - name: Create Pull Request uses: peter-evans/create-pull-request@v8 continue-on-error: true with: commit-message: 'docs(readme): Update Sponsors' title: Update Sponsors branch: docs/sponsors delete-branch: true ================================================ FILE: .gitignore ================================================ node_modules npm-debug.log .DS_Store .docusaurus .cache-loader /coverage /.tshy /.tshy-build /dist # Website build artifacts website/.astro/ website/dist/ website/src/content/docs/api ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ lint-staged ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Cheerio 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. Fork the project, and make your changes in a new branch based off of the `main` branch: 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 Any API change should be reflected in the project's README.md file. Reuse [jQuery's documentation](https://api.jquery.com) wherever possible, but take 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 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 The Cheerio contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================

cheerio

The fast, flexible, and elegant library for parsing and manipulating HTML and XML.
Build Status Coverage OpenCollective backers OpenCollective sponsors

[中文文档 (Chinese Readme)](https://github.com/cheeriojs/cheerio/wiki/Chinese-README) ```js import * as cheerio from 'cheerio'; const $ = cheerio.load('

Hello world

'); $('h2.title').text('Hello there!'); $('h2').addClass('welcome'); $.html(); //=>

Hello there!

``` ## Installation Install Cheerio using a package manager like npm, yarn, or bun. ```bash npm install cheerio # or bun add cheerio ``` ## Features **❤ Proven 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. **❁ Incredibly flexible:** Cheerio wraps around [parse5](https://github.com/inikulin/parse5) for parsing HTML and can optionally use the forgiving [htmlparser2](https://github.com/fb55/htmlparser2/). Cheerio can parse nearly any HTML or XML document. Cheerio works in both browser and server environments. ## API ### Loading First you need to load in the HTML. This step in jQuery is implicit, since jQuery operates on the one, baked-in DOM. With Cheerio, we need to pass in the HTML document. ```js // ESM or TypeScript: import * as cheerio from 'cheerio'; // In other environments: const cheerio = require('cheerio'); const $ = cheerio.load(''); $.html(); //=> ``` ### Selectors Once you've loaded the HTML, you can use jQuery-style selectors to find elements within the document. #### \$( selector, [context], [root] ) `selector` searches within the `context` scope which searches within the `root` scope. `selector` and `context` can be a string expression, DOM Element, array of DOM elements, or cheerio object. `root`, if provided, is typically the HTML document string. This selector method is the starting point for traversing and manipulating the document. Like in jQuery, it's the primary method for selecting elements in the document. ```js $('.apple', '#fruits').text(); //=> Apple $('ul .pear').attr('class'); //=> pear $('li[class=orange]').html(); //=> Orange ``` ### Rendering When you're ready to render the document, you can call the `html` method on the "root" selection: ```js $.root().html(); //=> // // // // // ``` If you want to render the [`outerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML) of a selection, you can use the `outerHTML` prop: ```js $('.pear').prop('outerHTML'); //=>
  • Pear
  • ``` You may also render the text content of a Cheerio object using the `text` method: ```js const $ = cheerio.load('This is content.'); $('body').text(); //=> This is content. ``` ### The "DOM Node" object 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` - `previousSibling` - `nextSibling` - `nodeValue` - `firstChild` - `childNodes` - `lastChild` ## Screencasts [https://vimeo.com/31950192](https://vimeo.com/31950192) > This video tutorial is a follow-up to Nettut's "How to Scrape Web Pages with > Node.js and jQuery", using cheerio instead of JSDOM + jQuery. This video shows > how easy it is to use cheerio and how much faster cheerio is than JSDOM + > jQuery. ## Cheerio in the real world Are you using cheerio in production? Add it to the [wiki](https://github.com/cheeriojs/cheerio/wiki/Cheerio-in-Production)! ## Sponsors Does your company use Cheerio in production? Please consider [sponsoring this project](https://github.com/cheeriojs/cheerio?sponsor=1)! Your help will allow maintainers to dedicate more time and resources to its development and support. **Headlining Sponsors** Github AirBnB HasData brand.dev **Other Sponsors** OnlineCasinosSpelen Nieuwe-Casinos.net ## Backers [Become a backer](https://github.com/cheeriojs/cheerio?sponsor=1) to show your support for Cheerio and help us maintain and improve this open source project. Vasy Kafidoff ## License MIT ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Only the latest release will receive security updates. ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ================================================ FILE: benchmark/benchmark.ts ================================================ import fs from 'node:fs/promises'; import { Script } from 'node:vm'; import type { Element } from 'domhandler'; import { JSDOM } from 'jsdom'; import { Bench } from 'tinybench'; import type { Cheerio } from '../src/cheerio.js'; import type { CheerioAPI } from '../src/load.js'; import { load } from '../src/load-parse.js'; const documentDir = new URL('documents/', import.meta.url); const jQuerySrc = await fs.readFile( new URL('../node_modules/jquery/dist/jquery.slim.js', import.meta.url), 'utf8', ); const jQueryScript = new Script(jQuerySrc); const filterIndex = process.argv.indexOf('--filter') + 1; const benchmarkFilter = filterIndex > 0 ? process.argv[filterIndex] : ''; const cheerioOnly = process.argv.includes('--cheerio-only'); type SuiteOptions = T extends void ? { test(this: void, $: CheerioAPI): void; setup?: (this: void, $: CheerioAPI) => T; } : { test(this: void, $: CheerioAPI, data: T): void; setup(this: void, $: CheerioAPI): T; }; async function benchmark( name: string, fileName: string, options: SuiteOptions, ): Promise { if (!name.includes(benchmarkFilter)) { return; } const markup = await fs.readFile(new URL(fileName, documentDir), 'utf8'); console.log(`Test: ${name} (file: ${fileName})`); const bench = new Bench(); const { test, setup } = options; // Add Cheerio test const $ = load(markup); const setupData = setup?.($) as T; bench.add('cheerio', () => { test($, setupData); }); // Add JSDOM test if (!cheerioOnly) { const dom = new JSDOM(markup, { runScripts: 'outside-only' }); jQueryScript.runInContext(dom.getInternalVMContext()); const setupData = setup?.(dom.window['$'] as CheerioAPI) as T; bench.add('jsdom', () => test(dom.window['$'] as CheerioAPI, setupData)); } await bench.run(); console.table(bench.table()); } await benchmark('Select all', 'jquery.html', { test: ($) => $('*').length, }); await benchmark('Select some', 'jquery.html', { test: ($) => $('li').length, }); /* * Manipulation Tests */ const DIVS_MARKUP = '
    '.repeat(50); await benchmark>('manipulation - append', 'jquery.html', { setup: ($) => $('body'), test: (_, $body) => $body.append(DIVS_MARKUP), }); // JSDOM used to run out of memory on these tests await benchmark>( 'manipulation - prepend - highmem', 'jquery.html', { setup: ($) => $('body'), test: (_, $body) => $body.prepend(DIVS_MARKUP), }, ); await benchmark>( 'manipulation - after - highmem', 'jquery.html', { setup: ($) => $('body'), test: (_, $body) => $body.after(DIVS_MARKUP), }, ); await benchmark>( 'manipulation - before - highmem', 'jquery.html', { setup: ($) => $('body'), test: (_, $body) => $body.before(DIVS_MARKUP), }, ); await benchmark>('manipulation - remove', 'jquery.html', { setup: ($) => $('body'), test($, $lis) { const child = $('
    '); $lis.append(child); child.remove(); }, }); await benchmark('manipulation - replaceWith', 'jquery.html', { setup($) { $('body').append('
    '); }, test($) { $('#foo').replaceWith('
    '); }, }); await benchmark>('manipulation - empty', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.empty(); }, }); await benchmark>('manipulation - html', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.html(); $lis.html('foo'); }, }); await benchmark>('manipulation - html render', 'jquery.html', { setup: ($) => $('body'), test(_, $lis) { $lis.html(); }, }); const HTML_INDEPENDENT_MARKUP = '
    bat
    baz
    '.repeat(6); await benchmark('manipulation - html independent', 'jquery.html', { test: ($) => $(HTML_INDEPENDENT_MARKUP).html(), }); await benchmark>('manipulation - text', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.text(); $lis.text('foo'); }, }); /* * Traversing Tests */ await benchmark>('traversing - Find', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.find('li').length, }); await benchmark>('traversing - Parent', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.parent('div').length, }); await benchmark>('traversing - Parents', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.parents('div').length, }); await benchmark>('traversing - Closest', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.closest('div').length, }); await benchmark>('traversing - next', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.next().length, }); await benchmark>('traversing - nextAll', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.nextAll('li').length, }); await benchmark>('traversing - nextUntil', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.nextUntil('li').length, }); await benchmark>('traversing - prev', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.prev().length, }); await benchmark>('traversing - prevAll', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.prevAll('li').length, }); await benchmark>('traversing - prevUntil', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.prevUntil('li').length, }); await benchmark>('traversing - siblings', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.siblings('li').length, }); await benchmark>('traversing - Children', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.children('a').length, }); await benchmark>('traversing - Filter', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.filter('li').length, }); await benchmark>('traversing - First', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.first().first().length, }); await benchmark>('traversing - Last', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.last().last().length, }); await benchmark>('traversing - Eq', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.eq(0).eq(0).length, }); /* * Attributes Tests */ await benchmark>('attributes - Attributes', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.attr('foo', 'bar'); $lis.attr('foo'); $lis.removeAttr('foo'); }, }); await benchmark>( 'attributes - Single Attribute', 'jquery.html', { setup: ($) => $('body'), test(_, $lis) { $lis.attr('foo', 'bar'); $lis.attr('foo'); $lis.removeAttr('foo'); }, }, ); await benchmark>('attributes - Data', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.data('foo', 'bar'); $lis.data('foo'); }, }); await benchmark>('attributes - Val', 'jquery.html', { setup: ($) => $('select,input,textarea,option'), test($, $lis) { $lis.each(function () { $(this).val(); $(this).val('foo'); }); }, }); await benchmark>('attributes - Has class', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.hasClass('foo'), }); await benchmark>('attributes - Toggle class', 'jquery.html', { setup: ($) => $('li'), test: (_, $lis) => $lis.toggleClass('foo'), }); await benchmark>( 'attributes - Add Remove class', 'jquery.html', { setup: ($) => $('li'), test(_, $lis) { $lis.addClass('foo'); $lis.removeClass('foo'); }, }, ); ================================================ FILE: benchmark/documents/jquery.html ================================================ jQuery() | jQuery API Documentation

    jQuery()


    Return a collection of matched elements either found in the DOM based on passed argument(s) or created by passing an HTML string.

    jQuery( selector [, context ] )Returns: jQuery

    Description: Accepts a string containing a CSS selector which is then used to match a set of elements.

    In the first formulation listed above, jQuery() — which can also be written as $() — searches through the DOM for any elements that match the provided selector and creates a new jQuery object that references these elements:

    1
    $( "div.foo" );

    If no elements match the provided selector, the new jQuery object is "empty"; that is, it contains no elements and has .length property of 0.

    Selector Context

    By default, selectors perform their searches within the DOM starting at the document root. However, an alternate context can be given for the search by using the optional second parameter to the $() function. For example, to do a search within an event handler, the search can be restricted like so:

    1
    2
    3
    $( "div.foo" ).click(function() {
    $( "span", this ).addClass( "bar" );
    });

    When the search for the span selector is restricted to the context of this, only spans within the clicked element will get the additional class.

    Internally, selector context is implemented with the .find() method, so $( "span", this ) is equivalent to $( this ).find( "span" ).

    Using DOM elements

    The second and third formulations of this function create a jQuery object using one or more DOM elements that were already selected in some other way.

    Note: These formulations are meant to consume only DOM elements; feeding mixed data to the elementArray form is particularly discouraged.

    A common use of this facility is to call jQuery methods on an element that has been passed to a callback function through the keyword this:

    1
    2
    3
    $( "div.foo" ).click(function() {
    $( this ).slideUp();
    });

    This example causes elements to be hidden with a sliding animation when clicked. Because the handler receives the clicked item in the this keyword as a bare DOM element, the element must be passed to the $() function before applying jQuery methods to it.

    XML data returned from an Ajax call can be passed to the $() function so individual elements of the XML structure can be retrieved using .find() and other DOM traversal methods.

    1
    2
    3
    $.post( "url.xml", function( data ) {
    var $child = $( data ).find( "child" );
    });

    Cloning jQuery Objects

    When a jQuery object is passed to the $() function, a clone of the object is created. This new jQuery object references the same DOM elements as the initial one.

    Returning an Empty Set

    As of jQuery 1.4, calling the jQuery() method with no arguments returns an empty jQuery set (with a .length property of 0). In previous versions of jQuery, this would return a set containing the document node.

    Working With Plain Objects

    At present, the only operations supported on plain JavaScript objects wrapped in jQuery are: .data(),.prop(),.on(), .off(), .trigger() and .triggerHandler(). The use of .data() (or any method requiring .data()) on a plain object will result in a new property on the object called jQuery{randomNumber} (eg. jQuery123456789).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // Define a plain object
    var foo = { foo: "bar", hello: "world" };
    // Pass it to the jQuery function
    var $foo = $( foo );
    // Test accessing property values
    var test1 = $foo.prop( "foo" ); // bar
    // Test setting property values
    $foo.prop( "foo", "foobar" );
    var test2 = $foo.prop( "foo" ); // foobar
    // Test using .data() as summarized above
    $foo.data( "keyName", "someValue" );
    console.log( $foo ); // will now contain a jQuery{randomNumber} property
    // Test binding an event name and triggering
    $foo.on( "eventName", function () {
    console.log( "eventName was called" );
    });
    $foo.trigger( "eventName" ); // Logs "eventName was called"

    Should .trigger( "eventName" ) be used, it will search for an "eventName" property on the object and attempt to execute it after any attached jQuery handlers are executed. It does not check whether the property is a function or not. To avoid this behavior, .triggerHandler( "eventName" ) should be used instead.

    1
    $foo.triggerHandler( "eventName" ); // Also logs "eventName was called"

    Examples:

    Example: Find all p elements that are children of a div element and apply a border to them.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <title>jQuery demo</title>
    <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
    </head>
    <body>
    <p>one</p>
    <div><p>two</p></div>
    <p>three</p>
    <script>
    $( "div > p" ).css( "border", "1px solid gray" );
    </script>
    </body>
    </html>

    Demo:

    Example: Find all inputs of type radio within the first form in the document.

    1
    $( "input:radio", document.forms[ 0 ] );

    Example: Find all div elements within an XML document from an Ajax response.

    1
    $( "div", xml.responseXML );

    Example: Set the background color of the page to black.

    1
    $( document.body ).css( "background", "black" );

    Example: Hide all the input elements within a form.

    1
    $( myForm.elements ).hide();

    jQuery( html [, ownerDocument ] )Returns: jQuery

    Description: Creates DOM elements on the fly from the provided string of raw HTML.

    Creating New Elements

    If a string is passed as the parameter to $(), jQuery examines the string to see if it looks like HTML (i.e., it starts with <tag ... >). If not, the string is interpreted as a selector expression, as explained above. But if the string appears to be an HTML snippet, jQuery attempts to create new DOM elements as described by the HTML. Then a jQuery object is created and returned that refers to these elements. You can perform any of the usual jQuery methods on this object:

    1
    $( "<p id='test'>My <em>new</em> text</p>" ).appendTo( "body" );

    For explicit parsing of a string to HTML, use the $.parseHTML() method.

    By default, elements are created with an ownerDocument matching the document into which the jQuery library was loaded. Elements being injected into a different document should be created using that document, e.g., $("<p>hello iframe</p>", $("#myiframe").prop("contentWindow").document).

    If the HTML is more complex than a single tag without attributes, as it is in the above example, the actual creation of the elements is handled by the browser's innerHTML mechanism. In most cases, jQuery creates a new <div> element and sets the innerHTML property of the element to the HTML snippet that was passed in. When the parameter has a single tag (with optional closing tag or quick-closing) — $( "<img />" ) or $( "<img>" ), $( "<a></a>" ) or $( "<a>" ) — jQuery creates the element using the native JavaScript createElement() function.

    When passing in complex HTML, some browsers may not generate a DOM that exactly replicates the HTML source provided. As mentioned, jQuery uses the browser"s .innerHTML property to parse the passed HTML and insert it into the current document. During this process, some browsers filter out certain elements such as <html>, <title>, or <head> elements. As a result, the elements inserted may not be representative of the original string passed.

    Filtering isn't, however, limited to these tags. For example, Internet Explorer prior to version 8 will also convert all href properties on links to absolute URLs, and Internet Explorer prior to version 9 will not correctly handle HTML5 elements without the addition of a separate compatibility layer.

    To ensure cross-platform compatibility, the snippet must be well-formed. Tags that can contain other elements should be paired with a closing tag:

    1
    $( "<a href='http://jquery.com'></a>" );

    Tags that cannot contain elements may be quick-closed or not:

    1
    2
    $( "<img>" );
    $( "<input>" );

    When passing HTML to jQuery(), please also note that text nodes are not treated as DOM elements. With the exception of a few methods (such as .content()), they are generally otherwise ignored or removed. E.g:

    1
    2
    var el = $( "1<br>2<br>3" ); // returns [<br>, "2", <br>]
    el = $( "1<br>2<br>3 >" ); // returns [<br>, "2", <br>, "3 &gt;"]

    This behavior is expected.

    As of jQuery 1.4, the second argument to jQuery() can accept a plain object consisting of a superset of the properties that can be passed to the .attr() method.

    Important: If the second argument is passed, the HTML string in the first argument must represent a a simple element with no attributes. As of jQuery 1.4, any event type can be passed in, and the following jQuery methods can be called: val, css, html, text, data, width, height, or offset.

    As of jQuery 1.8, any jQuery instance method (a method of jQuery.fn) can be used as a property of the object passed to the second parameter:

    1
    2
    3
    4
    5
    6
    7
    8
    $( "<div></div>", {
    "class": "my-div",
    on: {
    touchstart: function( event ) {
    // Do something
    }
    }
    }).appendTo( "body" );

    The name "class" must be quoted in the object since it is a JavaScript reserved word, and "className" cannot be used since it refers to the DOM property, not the attribute.

    While the second argument is convenient, its flexibility can lead to unintended consequences (e.g. $( "<input>", {size: "4"} ) calling the .size() method instead of setting the size attribute). The previous code block could thus be written instead as:

    1
    2
    3
    4
    5
    6
    7
    8
    $( "<div></div>" )
    .addClass( "my-div" )
    .on({
    touchstart: function( event ) {
    // Do something
    }
    })
    .appendTo( "body" );

    Examples:

    Example: Create a div element (and all of its contents) dynamically and append it to the body element. Internally, an element is created and its innerHTML property set to the given markup.

    1
    $( "<div><p>Hello</p></div>" ).appendTo( "body" )

    Example: Create some DOM elements.

    1
    2
    3
    4
    5
    6
    7
    8
    $( "<div/>", {
    "class": "test",
    text: "Click me!",
    click: function() {
    $( this ).toggleClass( "test" );
    }
    })
    .appendTo( "body" );

    jQuery( callback )Returns: jQuery

    Description: Binds a function to be executed when the DOM has finished loading.

    This function behaves just like $( document ).ready(), in that it should be used to wrap other $() operations on your page that depend on the DOM being ready. While this function is, technically, chainable, there really isn"t much use for chaining against it.

    Examples:

    Example: Execute the function when the DOM is ready to be used.

    1
    2
    3
    $(function() {
    // Document is ready
    });

    Example: Use both the shortcut for $(document).ready() and the argument to write failsafe jQuery code using the $ alias, without relying on the global alias.

    1
    2
    3
    jQuery(function( $ ) {
    // Your code using failsafe $ alias here...
    });
    ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 }, "javascript": { "formatter": { "quoteStyle": "single" } }, "linter": { "enabled": true, "rules": { "recommended": true, "complexity": { "useLiteralKeys": "off", "noArguments": "off", "noCommaOperator": "off", "noUselessStringConcat": "error", "noUselessUndefined": "error", "useSimplifiedLogicExpression": "error", "useWhile": "error" }, "style": { "noNonNullAssertion": "off", "noInferrableTypes": "error", "noNegationElse": "error", "noUnusedTemplateLiteral": "error", "noUselessElse": "error", "noYodaExpression": "error", "useAsConstAssertion": "error", "useCollapsedElseIf": "error", "useCollapsedIf": "error", "useConsistentArrayType": "error", "useConsistentArrowReturn": "error", "useConsistentMemberAccessibility": "error", "useConsistentObjectDefinitions": "error", "useConsistentTypeDefinitions": "error", "useDefaultParameterLast": "error", "useExplicitLengthCheck": "error", "useFilenamingConvention": "error", "useNumberNamespace": "error", "useNumericSeparators": "error", "useObjectSpread": "error", "useShorthandAssign": "error", "useUnifiedTypeSignatures": "error" }, "suspicious": { "noAssignInExpressions": "off", "noConfusingVoidType": "off", "noConstEnum": "off", "noExplicitAny": "off", "noImplicitAnyLet": "off", "noShadowRestrictedNames": "off", "noUnsafeDeclarationMerging": "off", "noConstantBinaryExpressions": "error", "useAwait": "error" }, "performance": { "useTopLevelRegex": "error" } } }, "css": { "parser": { "tailwindDirectives": true } }, "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } }, "overrides": [ { "includes": ["**/*.{test,spec}.ts", "test/**/*.ts"], "javascript": { "globals": [ "jest", "describe", "it", "beforeEach", "afterEach", "expect", "vi" ] } }, { "includes": ["**/*.astro"], "linter": { "rules": { "correctness": { "noUnusedImports": "off", "noUnusedVariables": "off" }, "style": { "useFilenamingConvention": "off" }, "performance": { "useTopLevelRegex": "off" } } } } ] } ================================================ FILE: eslint.config.js ================================================ import { fileURLToPath } from 'node:url'; // Added for .gitignore path import { includeIgnoreFile } from '@eslint/compat'; // Added for .gitignore import feedicFlatConfig from '@feedic/eslint-config'; import { commonTypeScriptRules } from '@feedic/eslint-config/typescript'; import eslintPluginVitest from '@vitest/eslint-plugin'; import { defineConfig } from 'eslint/config'; import eslintConfigBiome from 'eslint-config-biome'; import globals from 'globals'; import tseslint from 'typescript-eslint'; const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url)); export default defineConfig( includeIgnoreFile(gitignorePath), // Handle .gitignore patterns // Global linter options { linterOptions: { reportUnusedDisableDirectives: 'error', }, }, // Base configurations for all relevant files ...feedicFlatConfig, { rules: { 'jsdoc/tag-lines': [2, 'any', { startLines: 1 }], 'jsdoc/require-param-type': 0, 'jsdoc/require-returns-type': 0, 'jsdoc/no-types': 2, 'jsdoc/require-returns-check': 0, 'jsdoc/check-tag-names': [ 2, { definedTags: ['private'], }, ], }, }, // Global custom rules and language options { languageOptions: { globals: globals.node, parserOptions: { projectService: { allowDefaultProject: ['*.js'], defaultProject: 'tsconfig.json', }, tsconfigRootDir: import.meta.dirname, // eslint-disable-line n/no-unsupported-features/node-builtins }, }, rules: { 'n/file-extension-in-import': [2, 'always'], 'no-lonely-if': 2, 'no-proto': 2, 'no-else-return': [2, { allowElseIf: false }], 'no-unused-expressions': 2, 'no-useless-call': 2, 'no-constant-binary-expression': 2, 'no-void': 2, 'unicorn/no-array-callback-reference': 0, 'unicorn/no-array-reduce': 0, 'unicorn/no-for-loop': 0, 'unicorn/no-useless-undefined': 0, 'unicorn/prefer-array-find': 0, 'unicorn/prevent-abbreviations': 0, }, }, // TypeScript specific configurations { // Custom overrides and settings for TypeScript files files: ['**/*.{c,m,}ts', '**/*.tsx'], // Ensure this block specifically targets TS files extends: [ ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked, ], languageOptions: { parser: tseslint.parser, }, rules: { ...commonTypeScriptRules, // Enabling this in cheerio currently triggers broad churn across src + website. '@typescript-eslint/no-unnecessary-condition': 0, }, }, // Vitest specific configuration (for *.spec.ts files) { files: ['**/*.spec.ts'], plugins: { vitest: eslintPluginVitest }, languageOptions: { globals: globals.vitest, // Add Vitest globals }, rules: { // Assuming "recommended" is the flat config equivalent for "legacy-recommended" ...eslintPluginVitest.configs.recommended.rules, 'n/no-unpublished-import': 0, // Allow importing devDependencies }, }, // Website specific configuration { files: ['website/**/*.{m,}ts{x,}'], languageOptions: { parserOptions: { projectService: { allowDefaultProject: ['*.mjs'], }, tsconfigRootDir: `${import.meta.dirname}/website`, // eslint-disable-line n/no-unsupported-features/node-builtins }, }, }, // Prettier - must be the last configuration to override styling rules eslintConfigBiome, ); ================================================ FILE: package.json ================================================ { "name": "cheerio", "version": "1.2.0", "description": "The fast, flexible & elegant library for parsing and manipulating HTML and XML.", "keywords": [ "htmlparser", "jquery", "selector", "scraper", "parser", "dom", "xml", "html" ], "homepage": "https://cheerio.js.org/", "bugs": { "url": "https://github.com/cheeriojs/cheerio/issues" }, "repository": { "type": "git", "url": "git://github.com/cheeriojs/cheerio.git" }, "funding": "https://github.com/cheeriojs/cheerio?sponsor=1", "license": "MIT", "author": "Matt Mueller ", "sideEffects": false, "maintainers": [ "Felix Boehm " ], "type": "module", "exports": { ".": { "browser": { "types": "./dist/browser/index.d.ts", "default": "./dist/browser/index.js" }, "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } }, "./package.json": "./package.json", "./slim": { "browser": { "types": "./dist/browser/slim.d.ts", "default": "./dist/browser/slim.js" }, "import": { "types": "./dist/esm/slim.d.ts", "default": "./dist/esm/slim.js" }, "require": { "types": "./dist/commonjs/slim.d.ts", "default": "./dist/commonjs/slim.js" } }, "./utils": { "browser": { "types": "./dist/browser/utils.d.ts", "default": "./dist/browser/utils.js" }, "import": { "types": "./dist/esm/utils.d.ts", "default": "./dist/esm/utils.js" }, "require": { "types": "./dist/commonjs/utils.d.ts", "default": "./dist/commonjs/utils.js" } } }, "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", "browser": "./dist/browser/index.js", "types": "./dist/commonjs/index.d.ts", "files": [ "dist", "src", "!**/*.spec.{t,j}s", "!**/__tests__/*", "!**/__fixtures__/*" ], "scripts": { "benchmark": "node --import=tsx benchmark/benchmark.ts", "build": "tshy", "format": "npm run format:es && npm run format:biome", "format:biome": "biome check --write .", "format:es": "npm run lint:es -- --fix", "lint": "npm run lint:es && npm run lint:ts && npm run lint:biome", "lint:biome": "biome check .", "lint:es": "eslint .", "lint:ts": "tsc --noEmit", "prepare": "husky", "prepublishOnly": "npm run build", "test": "npm run lint && npm run test:vi", "test:vi": "vitest run", "update-sponsors": "tsx scripts/fetch-sponsors.mts" }, "lint-staged": { "*.js": [ "biome check --write", "eslint --fix" ], "*.{json,md,ts}": [ "biome check --write" ] }, "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.24.4", "whatwg-mimetype": "^4.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.4", "@eslint/compat": "^2.0.3", "@feedic/eslint-config": "^0.3.1", "@imgix/js-core": "^3.8.0", "@octokit/graphql": "^9.0.3", "@types/jsdom": "^28.0.0", "@types/node": "^25.5.0", "@types/whatwg-mimetype": "^3.0.2", "@vitest/coverage-v8": "^4.1.0", "@vitest/eslint-plugin": "^1.6.12", "eslint": "^10.0.3", "eslint-config-biome": "^2.1.3", "globals": "^17.4.0", "husky": "^9.1.7", "jquery": "^4.0.0", "jsdom": "^29.0.0", "lint-staged": "^16.4.0", "prettier-plugin-jsdoc": "^1.8.0", "tinybench": "^6.0.0", "tshy": "^3.3.2", "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.57.1", "vitest": "^4.0.18" }, "engines": { "node": ">=20.18.1" }, "tshy": { "esmDialects": [ "browser" ], "exports": { ".": "./src/index.ts", "./slim": "./src/slim.ts", "./utils": "./src/utils.ts", "./package.json": "./package.json" }, "exclude": [ "**/*.spec.ts", "**/__fixtures__/*", "**/__tests__/*" ] } } ================================================ FILE: scripts/fetch-sponsors.mts ================================================ /** * @file Script To fetch sponsor data from Open Collective and GitHub. * * Adapted from * https://github.com/eslint/website/blob/230e73457dcdc2353ad7934e876a5a222a17b1d7/_tools/fetch-sponsors.js. */ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/prefer-nullish-coalescing */ import * as fs from 'node:fs/promises'; import ImgixClient from '@imgix/js-core'; import { graphql as githubGraphQL } from '@octokit/graphql'; import { request } from 'undici'; type Tier = 'headliner' | 'sponsor' | 'professional' | 'backer'; interface Sponsor { createdAt: string; name: string; image: string; url: string; type: 'ORGANIZATION' | 'INDIVIDUAL' | 'FUND'; monthlyDonation: number; totalDonations: number; source: 'github' | 'opencollective' | 'manual'; tier: Tier | null; } const tierSponsors: Record = { headliner: [ // Some sponsors are manually added here. { createdAt: '2022-06-24', name: 'Github', image: 'https://github.com/github.png', url: 'https://github.com/', type: 'ORGANIZATION', monthlyDonation: 0, totalDonations: 0, source: 'manual', tier: 'headliner', }, { createdAt: '2018-05-02', name: 'AirBnB', image: 'https://github.com/airbnb.png', url: 'https://www.airbnb.com/', type: 'ORGANIZATION', monthlyDonation: 0, totalDonations: 0, source: 'manual', tier: 'headliner', }, { createdAt: '2026-01-21', name: 'HasData', image: 'https://hasdata.com/favicon.svg', url: 'https://hasdata.com', type: 'ORGANIZATION', monthlyDonation: 0, totalDonations: 0, source: 'manual', tier: 'headliner', }, { createdAt: '2026-01-28', name: 'brand.dev', image: 'https://github.com/brand-dot-dev.png', url: 'https://brand.dev/', type: 'ORGANIZATION', monthlyDonation: 0, totalDonations: 0, source: 'manual', tier: 'headliner', }, ], sponsor: [], professional: [], backer: [], }; const { CHEERIO_SPONSORS_GITHUB_TOKEN, IMGIX_TOKEN } = process.env; if (!CHEERIO_SPONSORS_GITHUB_TOKEN) { throw new Error('Missing CHEERIO_SPONSORS_GITHUB_TOKEN.'); } // @ts-expect-error - Types don't have a constructor const imgix = new ImgixClient({ domain: 'humble.imgix.net', secureURLToken: IMGIX_TOKEN, }); /** * Returns the tier ID for a given donation amount. * * @param monthlyDonation - The monthly donation in dollars. * @returns The ID of the tier the donation belongs to. */ function getTierSlug(monthlyDonation: number): Tier | null { if (monthlyDonation >= 250) { return 'headliner'; } if (monthlyDonation >= 100) { return 'sponsor'; } if (monthlyDonation >= 25) { return 'professional'; } if (monthlyDonation >= 5) { return 'backer'; } return null; } /** * Fetch order data from Open Collective using the GraphQL API. * * @returns An array of sponsors. */ async function fetchOpenCollectiveSponsors(): Promise { const endpoint = 'https://api.opencollective.com/graphql/v2'; const query = `{ account(slug: "cheerio") { orders(status: ACTIVE, filter: INCOMING) { nodes { createdAt fromAccount { name website imageUrl type } amount { value } tier { slug } frequency totalDonations { value } } } } }`; const { body } = await request(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }), }); const payload: any = await body.json(); return payload.data.account.orders.nodes.map((order: any): Sponsor => { const donation = order.amount.value * 100; const monthlyDonation = order.frequency === 'YEARLY' ? Math.round(donation / 12) : donation; return { createdAt: order.createdAt, name: order.fromAccount.name, url: order.fromAccount.website, image: order.fromAccount.imageUrl, type: order.fromAccount.type, monthlyDonation, totalDonations: order.totalDonations.value * 100, source: 'opencollective', tier: getTierSlug(monthlyDonation / 100), }; }); } function getMonthsActive(date: string): number { const now = new Date(); const then = new Date(date); const months = (now.getFullYear() - then.getFullYear()) * 12; return months - then.getMonth() + now.getMonth() + 1; } /** * Fetches GitHub Sponsors data using the GraphQL API. * * @returns An array of sponsors. */ async function fetchGitHubSponsors(): Promise { const { organization } = await githubGraphQL( `{ organization(login: "cheeriojs") { sponsorshipsAsMaintainer(first: 100) { nodes { sponsor: sponsorEntity { ... on User { name login avatarUrl url websiteUrl isViewer } ... on Organization { name login avatarUrl url websiteUrl viewerCanAdminister } } tier { monthlyPriceInDollars } createdAt } } } } `, { headers: { authorization: `token ${CHEERIO_SPONSORS_GITHUB_TOKEN}`, }, }, ); // Return an array in the same format as Open Collective return organization.sponsorshipsAsMaintainer.nodes.map( ({ sponsor, tier, createdAt }: any): Sponsor => ({ createdAt, name: sponsor.name, image: `${sponsor.avatarUrl}&s=128`, url: sponsor.websiteUrl || sponsor.url, type: // Workaround to get the type — fetch a field that only exists on users. sponsor.isViewer === undefined ? 'ORGANIZATION' : 'INDIVIDUAL', monthlyDonation: (tier?.monthlyPriceInDollars ?? 0) * 100, totalDonations: getMonthsActive(createdAt) * tier?.monthlyPriceInDollars * 100, source: 'github', tier: getTierSlug(tier?.monthlyPriceInDollars ?? 0), }), ); } async function fetchSponsors(): Promise { const results = await Promise.all([ fetchOpenCollectiveSponsors(), fetchGitHubSponsors(), ]); return results.flat(); } /* * Remove sponsors from lower tiers that have individual accounts, * but are clearly orgs. */ const MISLABELED_ORGS = /[ck]as[iy]+no|bet$|poker|gambling|coffee|tuxedo|(?:ph|f)oto/i; const README_PATH = new URL('../Readme.md', import.meta.url); const JSON_PATH = new URL('../website/sponsors.json', import.meta.url); const SECTION_START_BEGINNING = ''; const SECTION_END = ''; const professionalToBackerOverrides = new Map([ ['Vasy Kafidoff', 'https://kafidoff.com'], ]); const sponsors = await fetchSponsors(); console.log('Received sponsors:', sponsors); // Remove sponsors that are already in the pre-populated headliners for (let i = 0; i < sponsors.length; i++) { if ( tierSponsors.headliner.some((sponsor) => sponsor.url === sponsors[i].url) ) { sponsors.splice(i, 1); i--; } } sponsors.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt)); // Process into a useful format for (const sponsor of sponsors) { if ( !sponsor.tier || // Always skip if sponsor has no tier (e.g., donation < $5) // OR if it's a 'professional' or 'backer' tier AND meets specific filtering criteria ((sponsor.tier === 'professional' || sponsor.tier === 'backer') && (sponsor.type === 'ORGANIZATION' || MISLABELED_ORGS.test(sponsor.name) || MISLABELED_ORGS.test(sponsor.url))) ) { continue; } if ( (sponsor.tier === 'professional' || sponsor.tier === 'backer') && professionalToBackerOverrides.has(sponsor.name) ) { sponsor.url = professionalToBackerOverrides.get(sponsor.name)!; } tierSponsors[sponsor.tier].push(sponsor); } for (const tier of Object.values(tierSponsors)) { // Sort order based on total donations tier.sort((a: Sponsor, b: Sponsor) => b.totalDonations - a.totalDonations); // Set all donations to 0 before writing to JSON for (const sponsor of tier) { sponsor.monthlyDonation = 0; sponsor.totalDonations = 0; } } // Write sponsors.json await fs.writeFile(JSON_PATH, JSON.stringify(tierSponsors, null, 2), 'utf8'); // Prepend professionals to backers for now tierSponsors.backer.unshift(...tierSponsors.professional); let readme = await fs.readFile(README_PATH, 'utf8'); const TIER_IMAGE_SIZES: Record = { headliner: 128, sponsor: 64, professional: 64, backer: 48, }; for (let sectionStartIndex = 0; ; ) { sectionStartIndex = readme.indexOf( SECTION_START_BEGINNING, sectionStartIndex, ); if (sectionStartIndex < 0) break; sectionStartIndex += SECTION_START_BEGINNING.length; const sectionStartEndIndex = readme.indexOf( SECTION_START_END, sectionStartIndex, ); const sectionName = readme .slice(sectionStartIndex, sectionStartEndIndex) .trim() as Tier; const sectionContentStart = sectionStartEndIndex + SECTION_START_END.length; const sectionEndIndex = readme.indexOf(SECTION_END, sectionContentStart); readme = `${readme.slice(0, sectionContentStart)}\n\n${tierSponsors[ sectionName ] .map((s: Sponsor) => { const size = TIER_IMAGE_SIZES[s.tier ?? sectionName]; // Display each sponsor's image in the README. return ` ${s.name} `; }) .join('\n')}\n\n${readme.slice(sectionEndIndex)}`; } await fs.writeFile(README_PATH, readme, { encoding: 'utf8', }); ================================================ FILE: src/__fixtures__/fixtures.ts ================================================ import type { CheerioAPI } from '../load.js'; import { load } from '../load-parse.js'; /** A Cheerio instance with no content. */ export const cheerio: CheerioAPI = load([]); export const fruits: string = [ '
      ', '
    • Apple
    • ', '
    • Orange
    • ', '
    • Pear
    • ', '
    ', ].join(''); export const vegetables: string = [ '
      ', '
    • Carrot
    • ', '
    • Sweetcorn
    • ', '
    ', ].join(''); export const divcontainers: string = [ '
    ', '
    First
    ', '
    Second
    ', '
    ', '
    ', '
    Third
    ', '
    Fourth
    ', '
    ', '
    ', '
    \n\n

    \n\n
    ', '
    ', ].join(''); export const chocolates: string = [ '
      ', '
    • Linth
    • ', '
    • Frey
    • ', '
    • Cailler
    • ', '
    ', ].join(''); export const drinks: string = [ '
      ', '
    • Beer
    • ', '
    • Juice
    • ', '
    • Milk
    • ', '
    • Water
    • ', '
    • Cider
    • ', '
    ', ].join(''); export const food: string = [ '
      ', fruits, vegetables, '
    ', ].join(''); export const eleven = `
    • One
    • Two
    • Three
    • Four
    • Five
    • Six
    • Seven
    • Eight
    • Nine
    • Ten
    • Eleven
    `; export const unwrapspans: string = [ '', ].join(''); export const inputs: string = [ '', '', '', '', '', '', '', '', '', '', '', '', '', '', ].join(''); export const text: string = [ '

    Apples, oranges and pears.

    ', '

    Carrots and

    ', ].join(''); export const forms: string = [ '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', ].join(''); export const noscript: string = [ '', '', '

    Rocks!

    ', '', ].join(''); export const script: string = [ '
    ', 'A', '', 'B', '
    ', ].join(''); export const mixedText = '1TEXT2'; ================================================ FILE: src/__tests__/deprecated.spec.ts ================================================ /** * This file includes tests for deprecated APIs. The methods are expected to be * removed in the next major release of Cheerio, but their stability should be * maintained until that time. */ import { beforeEach, describe, expect, it } from 'vitest'; import { cheerio, food, fruits } from '../__fixtures__/fixtures.js'; describe('deprecated APIs', () => { describe('cheerio module', () => { describe('.parseHTML', () => { it('(html) : should preserve content', () => { const html = '
    test div
    '; expect(cheerio(cheerio.parseHTML(html)[0]).html()).toBe('test div'); }); }); describe('.merge', () => { it('should be a function', () => { expect(typeof cheerio.merge).toBe('function'); }); // #1674 - merge, wont accept Cheerio object it('should be a able merge array and cheerio object', () => { const ret = cheerio.merge(cheerio(), ['elem1', 'elem2']); expect(typeof ret).toBe('object'); expect(ret).toHaveLength(2); }); it('(arraylike, arraylike) : should modify the first array, but not the second', () => { const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const ret = cheerio.merge(arr1, arr2); expect(typeof ret).toBe('object'); expect(Array.isArray(ret)).toBe(true); expect(ret).toBe(arr1); expect(arr1).toHaveLength(6); expect(arr2).toHaveLength(3); }); it('(arraylike, arraylike) : should handle objects that arent arrays, but are arraylike', () => { const arr1: ArrayLike = { length: 3, 0: 'a', 1: 'b', 2: 'c', }; const arr2 = { length: 3, 0: 'd', 1: 'e', 2: 'f', }; cheerio.merge(arr1, arr2); expect(arr1).toHaveLength(6); expect(arr1[3]).toBe('d'); expect(arr1[4]).toBe('e'); expect(arr1[5]).toBe('f'); expect(arr2).toHaveLength(3); }); it('(?, ?) : should gracefully reject invalid inputs', () => { expect(cheerio.merge([4], 3 as never)).toBeUndefined(); expect(cheerio.merge({} as never, {} as never)).toBeUndefined(); expect(cheerio.merge([], {} as never)).toBeUndefined(); expect(cheerio.merge({} as never, [])).toBeUndefined(); const fakeArray = { length: 3, 0: 'a', 1: 'b', 3: 'd' }; expect(cheerio.merge(fakeArray, [])).toBeUndefined(); expect(cheerio.merge([], fakeArray)).toBeUndefined(); expect(cheerio.merge({ length: '7' } as never, [])).toBeUndefined(); expect(cheerio.merge({ length: -1 }, [])).toBeUndefined(); }); it('(?, ?) : should no-op on invalid inputs', () => { const fakeArray1 = { length: 3, 0: 'a', 1: 'b', 3: 'd' }; cheerio.merge(fakeArray1, []); expect(fakeArray1).toHaveLength(3); expect(fakeArray1[0]).toBe('a'); expect(fakeArray1[1]).toBe('b'); expect(fakeArray1[3]).toBe('d'); cheerio.merge([], fakeArray1); expect(fakeArray1).toHaveLength(3); expect(fakeArray1[0]).toBe('a'); expect(fakeArray1[1]).toBe('b'); expect(fakeArray1[3]).toBe('d'); }); }); describe('.contains', () => { let $: typeof cheerio; beforeEach(() => { $ = cheerio.load(food); }); it('(container, contained) : should correctly detect the provided element', () => { const $food = $('#food'); const $fruits = $('#fruits'); const $apple = $('.apple'); expect(cheerio.contains($food[0], $fruits[0])).toBe(true); expect(cheerio.contains($food[0], $apple[0])).toBe(true); }); it('(container, other) : should not detect elements that are not contained', () => { const $fruits = $('#fruits'); const $vegetables = $('#vegetables'); const $apple = $('.apple'); expect(cheerio.contains($vegetables[0], $apple[0])).toBe(false); expect(cheerio.contains($fruits[0], $vegetables[0])).toBe(false); expect(cheerio.contains($vegetables[0], $fruits[0])).toBe(false); expect(cheerio.contains($fruits[0], $fruits[0])).toBe(false); expect(cheerio.contains($vegetables[0], $vegetables[0])).toBe(false); }); }); describe('.root', () => { it('returns an empty selection', () => { const $empty = cheerio.root(); expect($empty).toHaveLength(1); expect($empty[0].children).toHaveLength(0); }); }); }); describe('Cheerio function', () => { it('.load', () => { const $1 = cheerio.load(fruits); const $2 = $1.load('

    Some text.

    '); expect($2('a')).toHaveLength(1); }); /** * The `.html` static method defined on the "loaded" Cheerio factory * function is deprecated. * * In order to promote consistency with the jQuery library, users are * encouraged to instead use the instance method of the same name. * * @example * * ```js * const $ = cheerio.load('

    Hello, world.

    '); * * $('h1').html(); * //=> 'Hello, world.' * ``` * * @example To render the markup of an entire document, invoke the * `html` function exported by the Cheerio module with a "root" * selection. * * ```js * cheerio.html($.root()); * //=> '

    Hello, world.

    ' * ``` */ describe('.html - deprecated API', () => { it('() : of empty cheerio object should return null', () => { /* * Note: the direct invocation of the Cheerio constructor function is * also deprecated. */ const $ = cheerio(); expect($.html()).toBe(null); }); it('(selector) : should return the outerHTML of the selected element', () => { const $ = cheerio.load(fruits); expect($.html('.pear')).toBe('
  • Pear
  • '); }); }); /** * The `.xml` static method defined on the "loaded" Cheerio factory function * is deprecated. Users are encouraged to instead use the `xml` function * exported by the Cheerio module. * * @example * * ```js * cheerio.xml($.root()); * ``` */ describe('.xml - deprecated API', () => { it('() : renders XML', () => { const $ = cheerio.load('', { xmlMode: true }); expect($.xml()).toBe(''); }); }); /** * The `.text` static method defined on the "loaded" Cheerio factory * function is deprecated. * * In order to promote consistency with the jQuery library, users are * encouraged to instead use the instance method of the same name. * * @example * * ```js * const $ = cheerio.load('

    Hello, world.

    '); * $('h1').text(); * //=> 'Hello, world.' * ``` * * @example To render the text content of an entire document, * invoke the `text` function exported by the Cheerio module with a "root" * selection. * * ```js * cheerio.text($.root()); * //=> 'Hello, world.' * ``` */ describe('.text - deprecated API', () => { it('(cheerio object) : should return the text contents of the specified elements', () => { const $ = cheerio.load('This is content.'); expect($.text($('a'))).toBe('This is content.'); }); it('(cheerio object) : should omit comment nodes', () => { const $ = cheerio.load( 'This is not a comment.', ); expect($.text($('a'))).toBe('This is not a comment.'); }); it('(cheerio object) : should include text contents of children recursively', () => { const $ = cheerio.load( 'This is
    a child with another child and not a comment followed by one last child and some final
    text.
    ', ); expect($.text($('a'))).toBe( 'This is a child with another child and not a comment followed by one last child and some final text.', ); }); it('() : should return the rendered text content of the root', () => { const $ = cheerio.load( 'This is
    a child with another child and not a comment followed by one last child and some final
    text.
    ', ); expect($.text()).toBe( 'This is a child with another child and not a comment followed by one last child and some final text.', ); }); it('(cheerio object) : should not omit script tags', () => { const $ = cheerio.load(''); expect($.text()).toBe('console.log("test")'); }); it('(cheerio object) : should omit style tags', () => { const $ = cheerio.load( '', ); expect($.text()).toBe('.cf-hidden { display: none; }'); }); }); }); }); ================================================ FILE: src/__tests__/xml.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { load } from '../index.js'; import type { CheerioOptions } from '../options.js'; function xml(str: string, options?: CheerioOptions) { options = { xml: true, ...options }; const $ = load(str, options); return $.xml(); } function dom(str: string, options?: CheerioOptions) { const $ = load('', options); return $(str).html(); } describe('render', () => { describe('(xml)', () => { it('should render tags correctly', () => { const str = ''; expect(xml(str)).toBe( '', ); }); it('should render tags (RSS) correctly', () => { const str = 'http://www.github.com/'; expect(xml(str)).toBe('http://www.github.com/'); }); it('should escape entities', () => { const str = ''; expect(xml(str)).toBe(str); }); it('should render HTML as XML', () => { const $ = load('', null, false); expect($.xml()).toBe(''); }); }); describe('(dom)', () => { it('should not keep camelCase for new nodes', () => { const str = 'hello'; expect(dom(str, { xml: false })).toBe( 'hello', ); }); it('should keep camelCase for new nodes', () => { const str = 'hello'; expect(dom(str, { xml: true })).toBe( 'hello', ); }); it('should maintain the parsing options of distinct contexts independently', () => { const str = 'hello'; const $ = load('', { xml: false }); expect($(str).html()).toBe( 'hello', ); }); }); }); ================================================ FILE: src/api/attributes.spec.ts ================================================ import type { Element } from 'domhandler'; import { beforeEach, describe, expect, it } from 'vitest'; import { cheerio, chocolates, food, fruits, inputs, mixedText, script, vegetables, } from '../__fixtures__/fixtures.js'; import { type Cheerio, type CheerioAPI, load } from '../index.js'; function withClass(attr: string) { return cheerio(`
    `); } describe('$(...)', () => { describe('.attr', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('() : should get all the attributes', () => { const attrs = $('ul').attr(); expect(attrs).toHaveProperty('id', 'fruits'); }); it('(invalid key) : invalid attr should get undefined', () => { const attr = $('.apple').attr('lol'); expect(attr).toBeUndefined(); }); it('(valid key) : valid attr should get value', () => { const cls = $('.apple').attr('class'); expect(cls).toBe('apple'); }); it('(valid key) : valid attr should get name when boolean', () => { const attr = $('').attr('autofocus'); expect(attr).toBe('autofocus'); }); it('(key, value) : should set one attr', () => { const $pear = $('.pear').attr('id', 'pear'); expect($('#pear')).toHaveLength(1); expect($pear).toBeInstanceOf($); }); it('(key, value) : should set multiple attr', () => { const $el = cheerio('
    ').attr( 'class', 'pear', ) as Cheerio; expect($el[0].attribs).toHaveProperty('class', 'pear'); expect($el[1].attribs).toBeUndefined(); expect($el[2].attribs).toHaveProperty('class', 'pear'); }); it('(key, value) : should return an empty object for an empty object', () => { const $src = $().attr('key', 'value'); expect($src.length).toBe(0); expect($src[0]).toBeUndefined(); }); it('(map) : object map should set multiple attributes', () => { $('.apple').attr({ id: 'apple', style: 'color:red;', 'data-url': 'http://apple.com', }); const attrs = $('.apple').attr(); expect(attrs).toHaveProperty('id', 'apple'); expect(attrs).toHaveProperty('style', 'color:red;'); expect(attrs).toHaveProperty('data-url', 'http://apple.com'); }); it('(map, val) : should throw with wrong combination of arguments', () => { expect(() => $('.apple').attr( { id: 'apple', style: 'color:red;', 'data-url': 'http://apple.com', } as never, () => '', ), ).toThrow('Bad combination of arguments.'); }); it('(key, function) : should call the function and update the attribute with the return value', () => { const $fruits = $('#fruits'); $fruits.attr('id', (index, value) => { expect(index).toBe(0); expect(value).toBe('fruits'); return 'ninja'; }); const attrs = $fruits.attr(); expect(attrs).toHaveProperty('id', 'ninja'); }); it('(key, function) : should ignore text nodes', () => { const $text = $(mixedText); $text.attr('class', () => 'ninja'); const className = $text.attr('class'); expect(className).toBe('ninja'); }); it('(key, value) : should correctly encode then decode unsafe values', () => { const $apple = $('.apple'); $apple.attr( 'href', 'http://github.com/">alert("XSS!")'); }); it('(key, value) : should coerce values to a string', () => { const $apple = $('.apple'); $apple.attr('data-test', 1 as never); expect($apple[0].attribs['data-test']).toBe('1'); expect($apple.attr('data-test')).toBe('1'); }); it('(key, value) : handle removed boolean attributes', () => { const $apple = $('.apple'); $apple.attr('autofocus', 'autofocus'); expect($apple.attr('autofocus')).toBe('autofocus'); $apple.removeAttr('autofocus'); expect($apple.attr('autofocus')).toBeUndefined(); }); it('(key, value) : should remove non-boolean attributes with names or values similar to boolean ones', () => { const $apple = $('.apple'); $apple.attr('data-autofocus', 'autofocus'); expect($apple.attr('data-autofocus')).toBe('autofocus'); $apple.removeAttr('data-autofocus'); expect($apple.attr('data-autofocus')).toBeUndefined(); }); it('(key, value) : should remove attributes when called with null value', () => { const $pear = $('.pear').attr('autofocus', 'autofocus'); expect($pear.attr('autofocus')).toBe('autofocus'); $pear.attr('autofocus', null); expect($pear.attr('autofocus')).toBeUndefined(); }); it('(map) : should remove attributes with null values', () => { const $pear = $('.pear').attr({ autofocus: 'autofocus', style: 'color:red', }); expect($pear.attr('autofocus')).toBe('autofocus'); expect($pear.attr('style')).toBe('color:red'); $pear.attr({ autofocus: null, style: 'color:blue' }); expect($pear.attr('autofocus')).toBeUndefined(); expect($pear.attr('style')).toBe('color:blue'); }); it('(chaining) setting value and calling attr returns result', () => { const pearAttr = $('.pear').attr('foo', 'bar').attr('foo'); expect(pearAttr).toBe('bar'); }); it('(chaining) setting attr to null returns a $', () => { const $pear = $('.pear').attr('foo', null); expect($pear).toBeInstanceOf($); }); it('(chaining) setting attr to undefined returns a $', () => { const $pear = $('.pear').attr('foo', undefined); expect($('.pear')).toHaveLength(1); expect($('.pear').attr('foo')).toBeUndefined(); expect($pear).toBeInstanceOf($); }); it("(bool) shouldn't treat boolean attributes differently in XML mode", () => { const $xml = $.load('', { xml: true, })('input'); expect($xml.attr('checked')).toBe('checked'); expect($xml.attr('disabled')).toBe('yes'); }); }); describe('.prop', () => { let $: CheerioAPI; let checkbox: Cheerio; beforeEach(() => { $ = load(inputs); checkbox = $('input[name=checkbox_on]'); }); it('(valid key) : valid prop should get value', () => { expect(checkbox.prop('checked')).toBe(true); checkbox.css('display', 'none'); expect(checkbox.prop('style')).toHaveProperty('display', 'none'); expect(checkbox.prop('style')).toHaveLength(1); expect(checkbox.prop('style')).toContain('display'); expect(checkbox.prop('tagName')).toBe('INPUT'); expect(checkbox.prop('nodeName')).toBe('INPUT'); }); it('(valid key) : should return on empty collection', () => { expect($(undefined).prop('checked')).toBeUndefined(); expect($(undefined).prop('style')).toBeUndefined(); expect($(undefined).prop('tagName')).toBeUndefined(); expect($(undefined).prop('nodeName')).toBeUndefined(); }); it('(invalid key) : invalid prop should get undefined', () => { expect(checkbox.prop('lol')).toBeUndefined(); expect(checkbox.prop(4 as never)).toBeUndefined(); expect(checkbox.prop(true as never)).toBeUndefined(); }); it('(key, value) : should set prop', () => { expect(checkbox.prop('checked')).toBe(true); checkbox.prop('checked', false); expect(checkbox.prop('checked')).toBe(false); checkbox.prop('checked', true); expect(checkbox.prop('checked')).toBe(true); }); it('(key, value) : should update attribute', () => { expect(checkbox.prop('checked')).toBe(true); expect(checkbox.attr('checked')).toBe('checked'); checkbox.prop('checked', false); expect(checkbox.prop('checked')).toBe(false); expect(checkbox.attr('checked')).toBeUndefined(); checkbox.prop('checked', true); expect(checkbox.prop('checked')).toBe(true); expect(checkbox.attr('checked')).toBe('checked'); }); it('(key, value) : should update namespace', () => { const imgs = $('\n\n\n\n'); const nsHtml = 'http://www.w3.org/1999/xhtml'; imgs.prop('src', '#').prop('namespace', nsHtml); expect(imgs.prop('namespace')).toBe(nsHtml); imgs.prop('attribs', null); expect(imgs.prop('src')).toBeUndefined(); expect(imgs.prop('data-foo')).toBeUndefined(); }); it('(key, value) : should ignore empty collection', () => { expect($(undefined).prop('checked')).toBeUndefined(); $(undefined).prop('checked', true); expect($(undefined).prop('checked')).toBeUndefined(); }); it('(map) : object map should set multiple props', () => { checkbox.prop({ id: 'check', checked: false, }); expect(checkbox.prop('id')).toBe('check'); expect(checkbox.prop('checked')).toBe(false); }); it('(map, val) : should throw with wrong combination of arguments', () => { expect(() => $('.apple').prop( { id: 'check', checked: false, } as never, () => '', ), ).toThrow('Bad combination of arguments.'); }); it('(key, function) : should call the function and update the prop with the return value', () => { checkbox.prop('checked', (index, value) => { expect(index).toBe(0); expect(value).toBe(true); return false; }); expect(checkbox.prop('checked')).toBe(false); }); it('(key, value) : should support chaining after setting props', () => { expect(checkbox.prop('checked', false)).toBe(checkbox); }); it('(invalid element/tag) : prop should return undefined', () => { expect($(undefined).prop('prop')).toBeUndefined(); expect($(null as never).prop('prop')).toBeUndefined(); }); it('("href") : should resolve links with `baseURI`', () => { const $ = load( ` example1 example2 example3 example4 `, { baseURI: 'http://example.com/page/1' }, ); expect($('#1').prop('href')).toBe('http://example.org/'); expect($('#2').prop('href')).toBe('http://example.org/'); expect($('#3').prop('href')).toBe('http://example.com/example.org'); expect($('#4').prop('href')).toBe('http://example.com/page/example.org'); expect($(undefined).prop('href')).toBeUndefined(); }); it('("href") : should skip values without an href', () => { const $ = load('example1'); expect($('#1').prop('href')).toBeUndefined(); }); it('("src") : should resolve links with `baseURI`', () => { const $ = load( ` `, { baseURI: 'http://example.com/page/1' }, ); expect($('#1').prop('src')).toBe('http://example.org/image.png'); expect($('#2').prop('src')).toBe('http://example.org/page.html'); expect($('#3').prop('src')).toBe( 'http://example.com/example.org/song.mp3', ); expect($('#4').prop('src')).toBe( 'http://example.com/page/example.org/image.png', ); expect($(undefined).prop('src')).toBeUndefined(); }); it('("outerHTML") : should render properly', () => { const outerHtml = '
    '; const $a = $(outerHtml); expect($a.prop('outerHTML')).toBe(outerHtml); expect($(undefined).prop('outerHTML')).toBeUndefined(); }); it('("outerHTML") : should support root nodes', () => { const $ = load('
    '); expect($.root().prop('outerHTML')).toBe( '
    ', ); }); it('("innerHTML") : should render properly', () => { const $a = $('
    '); expect($a.prop('innerHTML')).toBe(''); expect($(undefined).prop('innerHTML')).toBeUndefined(); }); it('("textContent") : should render properly', () => { expect($('select').children().prop('textContent')).toBe( 'Option not selected', ); expect($(script).prop('textContent')).toBe('A var foo = "bar";B'); expect($(undefined).prop('textContent')).toBeUndefined(); }); it('("textContent") : should include style and script tags', () => { const $ = load( 'Welcome
    Hello, testing text function,
    End of message', ); expect($('body').prop('textContent')).toBe( 'Welcome Hello, testing text function,console.log("hello").cf-hidden { display: none; }End of message', ); expect($('style').prop('textContent')).toBe( '.cf-hidden { display: none; }', ); expect($('script').prop('textContent')).toBe('console.log("hello")'); }); it('("innerText") : should render properly', () => { expect($('select').children().prop('innerText')).toBe( 'Option not selected', ); expect($(script).prop('innerText')).toBe('AB'); expect($(undefined).prop('innerText')).toBeUndefined(); }); it('("innerText") : should omit style and script tags', () => { const $ = load( 'Welcome
    Hello, testing text function,
    End of message', ); expect($('body').prop('innerText')).toBe( 'Welcome Hello, testing text function,End of message', ); expect($('style').prop('innerText')).toBe(''); expect($('script').prop('innerText')).toBe(''); }); it('(inherited properties) : prop should support inherited properties', () => { expect($('select').prop('childNodes')).toBe($('select')[0].childNodes); }); it('(key) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); expect($text($body[1]).prop('tagName')).toBeUndefined(); $body.prop('test-name', () => 'tester'); expect($text('body').html()).toBe( '1TEXT2', ); }); it("(bool) shouldn't treat boolean attributes differently in XML mode", () => { const $xml = $.load('', { xml: true, })('input'); expect($xml.prop('checked')).toBe('checked'); expect($xml.prop('disabled')).toBe('yes'); }); }); describe('.data', () => { let $: CheerioAPI; beforeEach(() => { $ = load(chocolates); }); it('() : should get all data attributes initially declared in the markup', () => { const data = $('.linth').data(); expect(data).toStrictEqual({ highlight: 'Lindor', origin: 'swiss', }); }); it('() : should get all data set via `data`', () => { const $el = cheerio('
    '); $el.data('a', 1); $el.data('b', 2); expect($el.data()).toStrictEqual({ a: 1, b: 2, }); }); it('() : should get all data attributes initially declared in the markup merged with all data additionally set via `data`', () => { const $el = cheerio('
    '); $el.data('b', 'b-modified'); $el.data('c', 'c'); expect($el.data()).toStrictEqual({ a: 'a', b: 'b-modified', c: 'c', }); }); it('() : no data attribute should return an empty object', () => { const data = $('.cailler').data(); expect(Object.keys(data)).toHaveLength(0); expect($('.free').data()).toBeUndefined(); }); it('(invalid key) : invalid data attribute should return `undefined`', () => { const data = $('.frey').data('lol'); expect(data).toBeUndefined(); }); it('(valid key) : valid data attribute should get value', () => { const highlight = $('.linth').data('highlight'); const origin = $('.linth').data('origin'); expect(highlight).toBe('Lindor'); expect(origin).toBe('swiss'); }); it('(key) : should translate camel-cased key values to hyphen-separated versions', () => { const $el = cheerio( '
    ', ); expect($el.data('ThreeWordAttribute')).toBe('a'); expect($el.data('fooBar_baz-')).toBe('b'); }); it('(key) : should retrieve object values', () => { const data = {}; const $el = cheerio('
    '); $el.data('test', data); expect($el.data('test')).toBe(data); }); it('(key) : should parse JSON data derived from the markup', () => { const $el = cheerio('
    '); expect($el.data('json')).toStrictEqual([1, 2, 3]); }); it('(key) : should not parse JSON data set via the `data` API', () => { const $el = cheerio('
    '); $el.data('json', '[1, 2, 3]'); expect($el.data('json')).toBe('[1, 2, 3]'); }); // See https://api.jquery.com/data/ and https://bugs.jquery.com/ticket/14523 it('(key) : should ignore the markup value after the first access', () => { const $el = cheerio('
    '); expect($el.data('test')).toBe('a'); $el.attr('data-test', 'b'); expect($el.data('test')).toBe('a'); }); it('(key) : should recover from malformed JSON', () => { const $el = cheerio('
    '); expect($el.data('custom')).toBe('{{templatevar}}'); }); it('("") : should accept the empty string as a name', () => { const $el = cheerio('
    '); expect($el.data('')).toBe('a'); }); it('(hyphen key) : data addribute with hyphen should be camelized ;-)', () => { const data = $('.frey').data(); expect(data).toStrictEqual({ taste: 'sweet', bestCollection: 'Mahony', }); }); it('(key, value) : should set data attribute', () => { // Adding as object. const a = $('.frey').data({ balls: 'giandor', }); // Adding as string. const b = $('.linth').data('snack', 'chocoletti'); expect(() => { a.data(4 as never, 'throw'); }).not.toThrow(); expect(a.data('balls')).toStrictEqual('giandor'); expect(b.data('snack')).toStrictEqual('chocoletti'); }); it('(key, value) : should set data for all elements in the selection', () => { $('li').data('foo', 'bar'); expect($('li').eq(0).data('foo')).toStrictEqual('bar'); expect($('li').eq(1).data('foo')).toStrictEqual('bar'); expect($('li').eq(2).data('foo')).toStrictEqual('bar'); }); it('(map) : object map should set multiple data attributes', () => { const { data } = $('.linth').data({ id: 'Cailler', flop: 'Pippilotti Rist', top: 'Frigor', url: 'http://www.cailler.ch/', })[0] as never; expect(data).toHaveProperty('id', 'Cailler'); expect(data).toHaveProperty('flop', 'Pippilotti Rist'); expect(data).toHaveProperty('top', 'Frigor'); expect(data).toHaveProperty('url', 'http://www.cailler.ch/'); }); describe('(attr) : data-* attribute type coercion :', () => { it('boolean', () => { const $el = cheerio('
    '); expect($el.data('bool')).toBe(true); }); it('number', () => { const $el = cheerio('
    '); expect($el.data('number')).toBe(23); }); it('number (scientific notation is not coerced)', () => { const $el = cheerio('
    '); expect($el.data('sci')).toBe('1E10'); }); it('null', () => { const $el = cheerio('
    '); expect($el.data('null')).toBe(null); }); it('object', () => { const $el = cheerio('
    '); expect($el.data('obj')).toStrictEqual({ a: 45 }); }); it('array', () => { const $el = cheerio('
    '); expect($el.data('array')).toStrictEqual([1, 2, 3]); }); }); it('(key, value) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); $body.data('snack', 'chocoletti'); expect($text('b').data('snack')).toBe('chocoletti'); }); }); describe('.val', () => { let $: CheerioAPI; beforeEach(() => { $ = load(inputs); }); it('(): on div should get undefined', () => { expect($('
    ').val()).toBeUndefined(); }); it('(): on button should get value', () => { const val = $('#btn-value').val(); expect(val).toBe('button'); }); it('(): on button with no value should get undefined', () => { const val = $('#btn-valueless').val(); expect(val).toBeUndefined(); }); it('(): on select should get value', () => { const val = $('select#one').val(); expect(val).toBe('option_selected'); }); it('(): on select with no value should get text', () => { const val = $('select#one-valueless').val(); expect(val).toBe('Option selected'); }); it('(): on select with no value should get converted HTML', () => { const val = $('select#one-html-entity').val(); expect(val).toBe('Option '); }); it('(): on select with no value should get text content', () => { const val = $('select#one-nested').val(); expect(val).toBe('Option selected'); }); it('(): on option should get value', () => { const val = $('select#one option').eq(0).val(); expect(val).toBe('option_not_selected'); }); it('(): on text input should get value', () => { const val = $('input[type="text"]').val(); expect(val).toBe('input_text'); }); it('(): on checked checkbox should get value', () => { const val = $('input[name="checkbox_on"]').val(); expect(val).toBe('on'); }); it('(): on unchecked checkbox should get value', () => { const val = $('input[name="checkbox_off"]').val(); expect(val).toBe('off'); }); it('(): on valueless checkbox should get value', () => { const val = $('input[name="checkbox_valueless"]').val(); expect(val).toBe('on'); }); it('(): on radio should get value', () => { const val = $('input[type="radio"]').val(); expect(val).toBe('off'); }); it('(): on valueless radio should get value', () => { const val = $('input[name="radio_valueless"]').val(); expect(val).toBe('on'); }); it('(): on multiple select should get an array of values', () => { const val = $('select#multi').val(); expect(val).toStrictEqual(['2', '3']); }); it('(): on multiple select with no value attribute should get an array of text content', () => { const val = $('select#multi-valueless').val(); expect(val).toStrictEqual(['2', '3']); }); it('(): with no selector matches should return nothing', () => { const val = $('.nasty').val(); expect(val).toBeUndefined(); }); it('(invalid value): should only handle arrays when it has the attribute multiple', () => { const val = $('select#one').val([]); expect(val).not.toBeUndefined(); }); it('(value): on empty set should get `this`', () => { const $empty = $([]); expect($empty.val('test')).toBe($empty); }); it('(value): on input text should set value', () => { const element = $('input[type="text"]').val('test'); expect(element.val()).toBe('test'); }); it('(value): on select should set value', () => { const element = $('select#one').val('option_not_selected'); expect(element.val()).toBe('option_not_selected'); }); it('(value): on option should set value', () => { const element = $('select#one option').eq(0).val('option_changed'); expect(element.val()).toBe('option_changed'); }); it('(value): on radio should set value', () => { const element = $('input[name="radio"]').val('off'); expect(element.val()).toBe('off'); }); it('(value): on radio with special characters should set value', () => { const element = $('input[name="radio[brackets]"]').val('off'); expect(element.val()).toBe('off'); }); it('(values): on multiple select should set multiple values', () => { const element = $('select#multi').val(['1', '3', '4']); expect(element.val()).toHaveLength(3); }); }); describe('.removeAttr', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('(key) : should remove a single attr', () => { const $fruits = $('#fruits'); expect($fruits.attr('id')).not.toBeUndefined(); $fruits.removeAttr('id'); expect($fruits.attr('id')).toBeUndefined(); }); it('(key key) : should remove multiple attrs', () => { const $apple = $('.apple'); $apple.attr('id', 'favorite'); $apple.attr('size', 'small'); expect($apple.attr('id')).toBe('favorite'); expect($apple.attr('class')).toBe('apple'); expect($apple.attr('size')).toBe('small'); $apple.removeAttr('id class'); expect($apple.attr('id')).toBeUndefined(); expect($apple.attr('class')).toBeUndefined(); expect($apple.attr('size')).toBe('small'); }); it('(key) : should return cheerio object', () => { const obj = $('ul').removeAttr('id'); expect(obj).toBeInstanceOf($); }); it('(key) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); $body.addClass(() => 'test'); expect($text('body').html()).toBe( '1TEXT2', ); $body.removeAttr('class'); expect($text('body').html()).toBe(mixedText); }); }); describe('.hasClass', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('(valid class) : should return true', () => { const cls = $('.apple').hasClass('apple'); expect(cls).toBe(true); expect(withClass('foo').hasClass('foo')).toBe(true); expect(withClass('foo bar').hasClass('foo')).toBe(true); expect(withClass('bar foo').hasClass('foo')).toBe(true); expect(withClass('bar foo bar').hasClass('foo')).toBe(true); }); it('(invalid class) : should return false', () => { const cls = $('#fruits').hasClass('fruits'); expect(cls).toBe(false); expect(withClass('foo-bar').hasClass('foo')).toBe(false); expect(withClass('foo-bar').hasClass('foo')).toBe(false); expect(withClass('foo-bar').hasClass('foo-ba')).toBe(false); }); it('should check multiple classes', () => { // Add a class $('.apple').addClass('red'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('red')).toBe(true); // Remove one and test again $('.apple').removeClass('apple'); expect($('li').eq(0).hasClass('apple')).toBe(false); }); it('(empty string argument) : should return false', () => { expect(withClass('foo').hasClass('')).toBe(false); expect(withClass('foo bar').hasClass('')).toBe(false); expect(withClass('foo bar').removeClass('foo').hasClass('')).toBe(false); }); }); describe('.addClass', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('(first class) : should add the class to the element', () => { const $fruits = $('#fruits'); $fruits.addClass('fruits'); const cls = $fruits.hasClass('fruits'); expect(cls).toBe(true); }); it('(single class) : should add the class to the element', () => { $('.apple').addClass('fruit'); const cls = $('.apple').hasClass('fruit'); expect(cls).toBe(true); }); it('(class): adds classes to many selected items', () => { $('li').addClass('fruit'); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.orange').hasClass('fruit')).toBe(true); expect($('.pear').hasClass('fruit')).toBe(true); // Mixed with text nodes const $red = $('\n
      \n
    \t').addClass('red'); expect($red).toHaveLength(3); expect($red[0].type).toBe('text'); expect($red[1].type).toBe('tag'); expect($red[2].type).toBe('text'); expect($red.hasClass('red')).toBe(true); }); it('(class class class) : should add multiple classes to the element', () => { $('.apple').addClass('fruit red tasty'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('red')).toBe(true); expect($('.apple').hasClass('tasty')).toBe(true); }); it('(fn) : should add classes returned from the function', () => { const $fruits = $('#fruits').children().add($('#fruits')); const args: [i: number, className: string][] = []; const thisVals: Element[] = []; const toAdd = ['main', 'apple red', '', undefined]; $fruits.addClass(function (...myArgs) { args.push(myArgs); thisVals.push(this); return toAdd[myArgs[0]]; }); expect(args).toStrictEqual([ [0, ''], [1, 'apple'], [2, 'orange'], [3, 'pear'], ]); expect(thisVals).toStrictEqual([ $fruits[0], $fruits[1], $fruits[2], $fruits[3], ]); expect($fruits.eq(0).hasClass('main')).toBe(true); expect($fruits.eq(0).hasClass('apple')).toBe(false); expect($fruits.eq(1).hasClass('apple')).toBe(true); expect($fruits.eq(1).hasClass('red')).toBe(true); expect($fruits.eq(2).hasClass('orange')).toBe(true); expect($fruits.eq(3).hasClass('pear')).toBe(true); }); }); describe('.removeClass', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('() : should remove all the classes', () => { $('.pear').addClass('fruit'); $('.pear').removeClass(); expect($('.pear').attr('class')).toBeUndefined(); }); it('("") : should not modify class list', () => { const $fruits = $('#fruits'); $fruits.children().removeClass(''); expect($('.apple')).toHaveLength(1); }); it('(invalid class) : should not remove anything', () => { $('.pear').removeClass('fruit'); expect($('.pear').hasClass('pear')).toBe(true); }); it('(no class attribute) : should not throw an exception', () => { const $vegetables = cheerio(vegetables); expect(() => { $('li', $vegetables).removeClass('vegetable'); }).not.toThrow(); }); it('(single class) : should remove a single class from the element', () => { $('.pear').addClass('fruit'); expect($('.pear').hasClass('fruit')).toBe(true); $('.pear').removeClass('fruit'); expect($('.pear').hasClass('fruit')).toBe(false); expect($('.pear').hasClass('pear')).toBe(true); // Remove one class from set const $li = $('li').removeClass('orange'); expect($li.eq(0).attr('class')).toBe('apple'); expect($li.eq(1).attr('class')).toBe(''); expect($li.eq(2).attr('class')).toBe('pear'); // Mixed with text nodes const $red = $('\n
      \n
    \t').removeClass( 'one', ); expect($red).toHaveLength(3); expect($red[0].type).toBe('text'); expect($red[1].type).toBe('tag'); expect($red[2].type).toBe('text'); expect($red.eq(1).attr('class')).toBe(''); expect($red.eq(1).prop('tagName')).toBe('UL'); }); it('(single class) : should remove a single class from multiple classes on the element', () => { $('.pear').addClass('fruit green tasty'); expect($('.pear').hasClass('fruit')).toBe(true); expect($('.pear').hasClass('green')).toBe(true); expect($('.pear').hasClass('tasty')).toBe(true); $('.pear').removeClass('green'); expect($('.pear').hasClass('fruit')).toBe(true); expect($('.pear').hasClass('green')).toBe(false); expect($('.pear').hasClass('tasty')).toBe(true); }); it('(class class class) : should remove multiple classes from the element', () => { $('.apple').addClass('fruit red tasty'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('red')).toBe(true); expect($('.apple').hasClass('tasty')).toBe(true); $('.apple').removeClass('apple red tasty'); expect($('.fruit').hasClass('apple')).toBe(false); expect($('.fruit').hasClass('red')).toBe(false); expect($('.fruit').hasClass('tasty')).toBe(false); expect($('.fruit').hasClass('fruit')).toBe(true); }); it('(class) : should remove all occurrences of a class name', () => { const $div = cheerio('
    '); expect($div.removeClass('x').hasClass('x')).toBe(false); }); it('(fn) : should remove classes returned from the function', () => { const $fruits = $('#fruits').children(); const args: [number, string][] = []; const thisVals: Element[] = []; const toAdd = ['apple red', '', undefined]; $fruits.removeClass(function (...myArgs) { args.push(myArgs); thisVals.push(this); return toAdd[myArgs[0]]; }); expect(args).toStrictEqual([ [0, 'apple'], [1, 'orange'], [2, 'pear'], ]); expect(thisVals).toStrictEqual([$fruits[0], $fruits[1], $fruits[2]]); expect($fruits.eq(0).hasClass('apple')).toBe(false); expect($fruits.eq(0).hasClass('red')).toBe(false); expect($fruits.eq(1).hasClass('orange')).toBe(true); expect($fruits.eq(2).hasClass('pear')).toBe(true); }); it('(fn) : should no op elements without attributes', () => { const $inputs = $(inputs); const val = $inputs.removeClass(() => 'tasty'); expect(val).toHaveLength(17); }); it('(fn) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); $body.addClass(() => 'test'); expect($text('body').html()).toBe( '1TEXT2', ); $body.removeClass(() => 'test'); expect($text('body').html()).toBe( '1TEXT2', ); }); }); describe('.toggleClass', () => { let $: CheerioAPI; beforeEach(() => { $ = load(fruits); }); it('(class class) : should toggle multiple classes from the element', () => { $('.apple').addClass('fruit'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('red')).toBe(false); $('.apple').toggleClass('apple red'); expect($('.fruit').hasClass('apple')).toBe(false); expect($('.fruit').hasClass('red')).toBe(true); expect($('.fruit').hasClass('fruit')).toBe(true); // Mixed with text nodes const $red = $('\n
      \n
    \t').toggleClass( 'red', ); expect($red).toHaveLength(3); expect($red.hasClass('red')).toBe(true); expect($red.hasClass('one')).toBe(true); $red.toggleClass('one'); expect($red.hasClass('red')).toBe(true); expect($red.hasClass('one')).toBe(false); }); it('(class class, true) : should add multiple classes to the element', () => { $('.apple').addClass('fruit'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('red')).toBe(false); $('.apple').toggleClass('apple red', true); expect($('.fruit').hasClass('apple')).toBe(true); expect($('.fruit').hasClass('red')).toBe(true); expect($('.fruit').hasClass('fruit')).toBe(true); }); it('(class true) : should add only one instance of class', () => { $('.apple').toggleClass('tasty', true); $('.apple').toggleClass('tasty', true); expect($('.apple').attr('class')).toMatch(/tasty/g); }); it('(class class, false) : should remove multiple classes from the element', () => { $('.apple').addClass('fruit'); expect($('.apple').hasClass('apple')).toBe(true); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('red')).toBe(false); $('.apple').toggleClass('apple red', false); expect($('.fruit').hasClass('apple')).toBe(false); expect($('.fruit').hasClass('red')).toBe(false); expect($('.fruit').hasClass('fruit')).toBe(true); }); it('(fn) : should toggle classes returned from the function', () => { const $ = load(food); $('.apple').addClass('fruit'); $('.carrot').addClass('vegetable'); expect($('.apple').hasClass('fruit')).toBe(true); expect($('.apple').hasClass('vegetable')).toBe(false); expect($('.orange').hasClass('fruit')).toBe(false); expect($('.orange').hasClass('vegetable')).toBe(false); expect($('.carrot').hasClass('fruit')).toBe(false); expect($('.carrot').hasClass('vegetable')).toBe(true); expect($('.sweetcorn').hasClass('fruit')).toBe(false); expect($('.sweetcorn').hasClass('vegetable')).toBe(false); $('li').toggleClass(function () { return $(this).parent().is('#fruits') ? 'fruit' : 'vegetable'; }); expect($('.apple').hasClass('fruit')).toBe(false); expect($('.apple').hasClass('vegetable')).toBe(false); expect($('.orange').hasClass('fruit')).toBe(true); expect($('.orange').hasClass('vegetable')).toBe(false); expect($('.carrot').hasClass('fruit')).toBe(false); expect($('.carrot').hasClass('vegetable')).toBe(false); expect($('.sweetcorn').hasClass('fruit')).toBe(false); expect($('.sweetcorn').hasClass('vegetable')).toBe(true); }); it('(fn) : should work with no initial class attribute', () => { const $inputs = load(inputs); $inputs('input, select').toggleClass(function () { return $inputs(this).get(0)!.tagName === 'select' ? 'selectable' : 'inputable'; }); expect($inputs('.selectable')).toHaveLength(6); expect($inputs('.inputable')).toHaveLength(9); }); it('(fn) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); $body.toggleClass(() => 'test'); expect($text('body').html()).toBe( '1TEXT2', ); $body.toggleClass(() => 'test'); expect($text('body').html()).toBe( '1TEXT2', ); }); it('(invalid) : should be a no-op for invalid inputs', () => { const original = $('.apple'); const testAgainst = original.attr('class'); expect(original.toggleClass().attr('class')).toStrictEqual(testAgainst); for (const value of [undefined, true, false, null, 0, 1, {}]) { expect( original.toggleClass(value as never).attr('class'), ).toStrictEqual(testAgainst); } }); }); }); ================================================ FILE: src/api/attributes.ts ================================================ /** * Methods for getting and modifying attributes. * * @module cheerio/attributes */ import { type AnyNode, type Element, isTag } from 'domhandler'; import { innerText, textContent } from 'domutils'; import { ElementType } from 'htmlparser2'; import type { Cheerio } from '../cheerio.js'; import { text } from '../static.js'; import { camelCase, cssCase, domEach } from '../utils.js'; const rspace = /\s+/; const dataAttrPrefix = 'data-'; // Attributes that are booleans const 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 const rbrace = /^{[\s\S]*}$|^\[[\s\S]*]$/; /** * Gets a node's attribute. For boolean attributes, it will return the value's * name should it be set. * * Also supports getting the `value` of several form elements. * * @category Attributes * @param elem - Element to get the attribute of. * @param name - Name of the attribute. * @param xmlMode - Disable handling of special HTML attributes. * @returns The attribute's value. */ function getAttr( elem: AnyNode, name: undefined, xmlMode?: boolean, ): Record | undefined; function getAttr( elem: AnyNode, name: string, xmlMode?: boolean, ): string | undefined; function getAttr( elem: AnyNode, name: string | undefined, xmlMode?: boolean, ): Record | string | undefined; function getAttr( elem: AnyNode, name: string | undefined, xmlMode?: boolean, ): Record | string | undefined { if (!(elem && isTag(elem))) return; elem.attribs ??= {}; // Return the entire attribs object if no attribute specified if (!name) { return elem.attribs; } if (Object.hasOwn(elem.attribs, name)) { // Get the (decoded) attribute return !xmlMode && rboolean.test(name) ? name : elem.attribs[name]; } // Mimic the DOM and return text content as value for `option's` if (elem.name === 'option' && name === 'value') { 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' ) { return 'on'; } return; } /** * Sets the value of an attribute. The attribute will be deleted if the value is * `null`. * * @private * @param el - The element to set the attribute on. * @param name - The attribute's name. * @param value - The attribute's value. */ function setAttr(el: Element, name: string, value: string | null) { if (value === null) { removeAttribute(el, name); } else { el.attribs[name] = `${value}`; } } /** * Method for getting attributes. Gets the attribute value for only the first * element in the matched set. * * @category Attributes * @example * * ```js * $('ul').attr('id'); * //=> fruits * ``` * * @param name - Name of the attribute. * @returns The attribute's value. * @see {@link https://api.jquery.com/attr/} */ export function attr( this: Cheerio, name: string, ): string | undefined; /** * Method for getting all attributes and their values of the first element in * the matched set. * * @category Attributes * @example * * ```js * $('ul').attr(); * //=> { id: 'fruits' } * ``` * * @returns The attribute's values. * @see {@link https://api.jquery.com/attr/} */ export function attr( this: Cheerio, ): Record | undefined; /** * Method for setting attributes. Sets the attribute value for all elements 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`. * * @category Attributes * @example * * ```js * $('.apple').attr('id', 'favorite').prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param name - Name of the attribute. * @param value - The new value of the attribute. * @returns The instance itself. * @see {@link https://api.jquery.com/attr/} */ export function attr( this: Cheerio, name: string, value?: | string | null | ((this: Element, i: number, attrib: string) => string | null), ): Cheerio; /** * Method for setting multiple attributes at once. Sets the attribute value for * all elements in the matched set. If you set an attribute's value to `null`, * you remove that attribute. * * @category Attributes * @example * * ```js * $('.apple').attr({ id: 'favorite' }).prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param values - Map of attribute names and values. * @returns The instance itself. * @see {@link https://api.jquery.com/attr/} */ export function attr( this: Cheerio, values: Record, ): Cheerio; export function attr( this: Cheerio, name?: string | Record, value?: | string | null | ((this: Element, i: number, attrib: string) => string | null), ): string | Cheerio | undefined | Record { // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { if (typeof name !== 'string') { throw new TypeError('Bad combination of arguments.'); } return domEach(this, (el, i) => { if (isTag(el)) setAttr(el, name, value.call(el, i, el.attribs[name])); }); } return domEach(this, (el) => { if (!isTag(el)) return; if (typeof name === 'object') { for (const objName of Object.keys(name)) { const objValue = name[objName]; setAttr(el, objName, objValue); } } else { if (typeof name !== 'string') { throw new TypeError('Bad combination of arguments.'); } setAttr(el, name, value ?? null); } }); } return arguments.length > 1 ? this : getAttr(this[0], name, this.options.xmlMode); } /** * Gets a node's prop. * * @private * @category Attributes * @param el - Element to get the prop of. * @param name - Name of the prop. * @param xmlMode - Disable handling of special HTML attributes. * @returns The prop's value. */ function getProp( el: Element, name: string, xmlMode?: boolean, ): string | undefined | boolean | Element[keyof Element] { return name in el ? // @ts-expect-error TS doesn't like us accessing the value directly here. (el[name] as string | undefined) : !xmlMode && rboolean.test(name) ? getAttr(el, name, false) !== undefined : getAttr(el, name, xmlMode); } /** * Sets the value of a prop. * * @private * @param el - The element to set the prop on. * @param name - The prop's name. * @param value - The prop's value. * @param xmlMode - Disable handling of special HTML attributes. */ function setProp(el: Element, name: string, value: unknown, xmlMode?: boolean) { if (name in el) { // @ts-expect-error Overriding value el[name] = value; } else { setAttr( el, name, !xmlMode && rboolean.test(name) ? value ? '' : null : `${value as string}`, ); } } interface StyleProp { length: number; [key: string]: string | number; [index: number]: string; } /** * Get a string representation of the element. * * @param name Name of the property. */ export function prop( this: Cheerio, name: 'innerHTML' | 'outerHTML' | 'innerText' | 'textContent', ): string | null; /** * Get a parsed CSS style object. * * @param name - Name of the property. * @returns The style object, or `undefined` if the element has no `style` * attribute. */ export function prop( this: Cheerio, name: 'style', ): StyleProp | undefined; /** * Method for getting and setting properties. Gets the property value for only * the first element in the matched set. * * @category Attributes * @example * * ```js * $('input[type="checkbox"]').prop('checked'); * //=> false * * $('input[type="checkbox"]').prop('checked', true).val(); * //=> ok * ``` * * @param name - Name of the property. * @returns If `value` is specified the instance itself, otherwise the prop's * value. * @see {@link https://api.jquery.com/prop/} */ // biome-ignore lint/style/useUnifiedTypeSignatures: Separate overloads needed for accurate docs export function prop( this: Cheerio, name: 'tagName' | 'nodeName', ): string | undefined; /** * Resolve `href` or `src` of supported elements. Requires the `baseURI` option * to be set, and a global `URL` object to be part of the environment. * * @example With `baseURI` set to `'https://example.com'`: * * ```js * $('').prop('src'); * //=> 'https://example.com/image.png' * ``` * * @param name - Name of the property. * @returns The resolved URL, or `undefined` if the element is not supported. */ export function prop( this: Cheerio, name: 'href' | 'src', ): string | undefined; /** * Get a property of an element. * * @param name - Name of the property. * @returns The property's value. */ export function prop( this: Cheerio, name: K, ): Element[K]; /** * Set a property of an element. * * @param name - Name of the property. * @param value - Value to set the property to. * @returns The instance itself. */ export function prop( this: Cheerio, name: K, value: | Element[K] | ((this: Element, i: number, prop: K) => Element[keyof Element]), ): Cheerio; /** * Set multiple properties of an element. * * @example * * ```js * $('input[type="checkbox"]').prop({ * checked: true, * disabled: false, * }); * ``` * * @param map - Object of properties to set. * @returns The instance itself. */ export function prop( this: Cheerio, map: Record, ): Cheerio; /** * Set a property of an element. * * @param name - Name of the property. * @param value - Value to set the property to. * @returns The instance itself. */ export function prop( this: Cheerio, name: string, value: | string | boolean | null | ((this: Element, i: number, prop: string) => string | boolean), ): Cheerio; /** * Get a property of an element. * * @param name - The property's name. * @returns The property's value. */ export function prop(this: Cheerio, name: string): string; export function prop( this: Cheerio, name: string | Record, value?: unknown, ): | Cheerio | string | boolean | undefined | null | Element[keyof Element] | StyleProp { if (typeof name === 'string' && value === undefined) { const el = this[0]; if (!el) return; switch (name) { case 'style': { const property = this.css() as StyleProp; const keys = Object.keys(property); for (let i = 0; i < keys.length; i++) { property[i] = keys[i]; } property.length = keys.length; return property; } case 'tagName': case 'nodeName': { if (!isTag(el)) return; return el.name.toUpperCase(); } case 'href': case 'src': { if (!isTag(el)) return; const prop = el.attribs?.[name]; if ( typeof URL !== 'undefined' && ((name === 'href' && (el.tagName === 'a' || el.tagName === 'link')) || (name === 'src' && (el.tagName === 'img' || el.tagName === 'iframe' || el.tagName === 'audio' || el.tagName === 'video' || el.tagName === 'source'))) && prop !== undefined && this.options.baseURI ) { return new URL(prop, this.options.baseURI).href; } return prop; } case 'innerText': { return innerText(el); } case 'textContent': { return textContent(el); } case 'outerHTML': { if (el.type === ElementType.Root) return this.html(); return this.clone().wrap('').parent().html(); } case 'innerHTML': { return this.html(); } default: { if (!isTag(el)) return; return getProp(el, name, this.options.xmlMode); } } } if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { if (typeof name === 'object') { throw new TypeError('Bad combination of arguments.'); } return domEach(this, (el, i) => { if (isTag(el)) { setProp( el, name, value.call(el, i, getProp(el, name, this.options.xmlMode)), this.options.xmlMode, ); } }); } return domEach(this, (el) => { if (!isTag(el)) return; if (typeof name === 'object') { for (const key of Object.keys(name)) { const val = name[key]; setProp(el, key, val, this.options.xmlMode); } } else { setProp(el, name, value, this.options.xmlMode); } }); } return; } /** * An element with a data attribute. * * @private */ interface DataElement extends Element { /** The data attribute. */ data?: Record; } /** * Sets the value of a data attribute. * * @private * @param elem - The element to set the data attribute on. * @param name - The data attribute's name. * @param value - The data attribute's value. */ function setData( elem: DataElement, name: string | Record, value?: unknown, ) { elem.data ??= {}; if (typeof name === 'object') Object.assign(elem.data, name); else if (typeof name === 'string' && value !== undefined) { elem.data[name] = value; } } /** * Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*` * attribute, and cache the value in the node's internal data store. * * @private * @category Attributes * @param el - Element to get the data attribute of. * @returns A map with all of the data attributes. */ function readAllData(el: DataElement): unknown { const data = (el.data ??= {}); for (const domName of Object.keys(el.attribs)) { if (!domName.startsWith(dataAttrPrefix)) { continue; } const jsName = camelCase(domName.slice(dataAttrPrefix.length)); if (!Object.hasOwn(data, jsName)) { data[jsName] = parseDataValue(el.attribs[domName]); } } return data; } /** * Read the specified attribute from the equivalent HTML5 `data-*` attribute, * and (if present) cache the value in the node's internal data store. * * @category Attributes * @param el - Element to get the data attribute of. * @param name - Name of the data attribute. * @returns The data attribute's value. */ function readData(el: DataElement, name: string): unknown { const domName = dataAttrPrefix + cssCase(name); const data = (el.data ??= {}); if (Object.hasOwn(data, name)) { return data[name]; } if (Object.hasOwn(el.attribs, domName)) { data[name] = parseDataValue(el.attribs[domName]); return data[name]; } return; } /** * Coerce string data-* attributes to their corresponding JavaScript primitives. * * @category Attributes * @param value - The value to parse. * @returns The parsed value. */ function parseDataValue(value: string): unknown { if (value === 'null') return null; if (value === 'true') return true; if (value === 'false') return false; const num = Number(value); if (value === String(num)) return num; if (rbrace.test(value)) { try { return JSON.parse(value); } catch { /* Ignore */ } } return value; } /** * Method for getting data attributes, for only the first element in the matched * set. * * @category Attributes * @example * * ```js * $('
    ').data('apple-color'); * //=> 'red' * ``` * * @param name - Name of the data attribute. * @returns The data attribute's value, or `undefined` if the attribute does not * exist. * @see {@link https://api.jquery.com/data/} */ export function data( this: Cheerio, name: string, ): unknown; /** * Method for getting all of an element's data attributes, for only the first * element in the matched set. * * @category Attributes * @example * * ```js * $('
    ').data(); * //=> { appleColor: 'red' } * ``` * * @returns A map with all of the data attributes. * @see {@link https://api.jquery.com/data/} */ export function data( this: Cheerio, ): Record; /** * Method for setting data attributes, for only the first element in the matched * set. * * @category Attributes * @example * * ```js * const apple = $('.apple').data('kind', 'mac'); * * apple.data('kind'); * //=> 'mac' * ``` * * @param name - Name of the data attribute. * @param value - The new value. * @returns The instance itself. * @see {@link https://api.jquery.com/data/} */ export function data( this: Cheerio, name: string, value: unknown, ): Cheerio; /** * Method for setting multiple data attributes at once, for only the first * element in the matched set. * * @category Attributes * @example * * ```js * const apple = $('.apple').data({ kind: 'mac' }); * * apple.data('kind'); * //=> 'mac' * ``` * * @param values - Map of names to values. * @returns The instance itself. * @see {@link https://api.jquery.com/data/} */ export function data( this: Cheerio, values: Record, ): Cheerio; export function data( this: Cheerio, name?: string | Record, value?: unknown, ): unknown { const elem = this[0]; if (!(elem && isTag(elem))) return; const dataEl: DataElement = elem; dataEl.data ??= {}; // Return the entire data object if no data specified if (name == null) { return readAllData(dataEl); } // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { domEach(this, (el) => { if (isTag(el)) { if (typeof name === 'object') setData(el, name); else setData(el, name, value); } }); return this; } return readData(dataEl, name); } /** * Method for getting the value of input, select, and textarea. Note: Support * for `map`, and `function` has not been added yet. * * @category Attributes * @example * * ```js * $('input[type="text"]').val(); * //=> input_text * ``` * * @returns The value. * @see {@link https://api.jquery.com/val/} */ export function val( this: Cheerio, ): string | undefined | string[]; /** * Method for setting the value of input, select, and textarea. Note: Support * for `map`, and `function` has not been added yet. * * @category Attributes * @example * * ```js * $('input[type="text"]').val('test').prop('outerHTML'); * //=> * ``` * * @param value - The new value. * @returns The instance itself. * @see {@link https://api.jquery.com/val/} */ export function val( this: Cheerio, value: string | string[], ): Cheerio; export function val( this: Cheerio, value?: string | string[], ): string | string[] | Cheerio | undefined { const querying = arguments.length === 0; const element = this[0]; if (!(element && isTag(element))) return querying ? undefined : this; switch (element.name) { case 'textarea': { return this.text(value as string); } case 'select': { const option = this.find('option:selected'); if (!querying) { if (this.attr('multiple') == null && typeof value === 'object') { return this; } this.find('option').removeAttr('selected'); const values = typeof value === 'object' ? value : [value]; for (const val of values) { this.find(`option[value="${val}"]`).attr('selected', ''); } return this; } return this.attr('multiple') ? option.toArray().map((el) => text(el.children)) : option.attr('value'); } case 'button': case 'input': case 'option': { return querying ? this.attr('value') : this.attr('value', value as string); } } return; } /** * Remove an attribute. * * @param elem - Node to remove attribute from. * @param name - Name of the attribute to remove. */ function removeAttribute(elem: Element, name: string) { if (!(elem.attribs && Object.hasOwn(elem.attribs, name))) return; delete elem.attribs[name]; } /** * Splits a space-separated list of names to individual names. * * @category Attributes * @param names - Names to split. * @returns - Split names. */ function splitNames(names?: string): string[] { return names ? names.trim().split(rspace) : []; } /** * Method for removing attributes by `name`. * * @category Attributes * @example * * ```js * $('.pear').removeAttr('class').prop('outerHTML'); * //=>
  • Pear
  • * * $('.apple').attr('id', 'favorite'); * $('.apple').removeAttr('id class').prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param name - Name of the attribute. * @returns The instance itself. * @see {@link https://api.jquery.com/removeAttr/} */ export function removeAttr( this: Cheerio, name: string, ): Cheerio { const attrNames = splitNames(name); for (const attrName of attrNames) { domEach(this, (elem) => { if (isTag(elem)) removeAttribute(elem, attrName); }); } return this; } /** * Check to see if _any_ of the matched elements have the given `className`. * * @category Attributes * @example * * ```js * $('.pear').hasClass('pear'); * //=> true * * $('apple').hasClass('fruit'); * //=> false * * $('li').hasClass('pear'); * //=> true * ``` * * @param className - Name of the class. * @returns Indicates if an element has the given `className`. * @see {@link https://api.jquery.com/hasClass/} */ export function hasClass( this: Cheerio, className: string, ): boolean { return this.toArray().some((elem) => { const clazz = isTag(elem) && elem.attribs['class']; if (clazz && className.length > 0) { for ( let idx = clazz.indexOf(className); idx > -1; idx = clazz.indexOf(className, idx + 1) ) { const end = idx + className.length; if ( (idx === 0 || rspace.test(clazz[idx - 1])) && (end === clazz.length || rspace.test(clazz[end])) ) { return true; } } } return false; }); } /** * Adds class(es) to all of the matched elements. Also accepts a `function`. * * @category Attributes * @example * * ```js * $('.pear').addClass('fruit').prop('outerHTML'); * //=>
  • Pear
  • * * $('.apple').addClass('fruit red').prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param value - Name of new class. * @returns The instance itself. * @see {@link https://api.jquery.com/addClass/} */ export function addClass>( this: R, value?: | string | ((this: Element, i: number, className: string) => string | undefined), ): R { // Support functions if (typeof value === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { const className = el.attribs['class'] || ''; addClass.call([el], value.call(el, i, className)); } }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; const classNames = value.split(rspace); const numElements = this.length; for (let i = 0; i < numElements; i++) { const el = this[i]; // If selected element isn't a tag, move on if (!isTag(el)) continue; // If we don't already have classes — always set xmlMode to false here, as it doesn't matter for classes const className = getAttr(el, 'class', false); if (className) { let setClass = ` ${className} `; // Check if class already exists for (const cn of classNames) { const appendClass = `${cn} `; if (!setClass.includes(` ${appendClass}`)) setClass += appendClass; } setAttr(el, 'class', setClass.trim()); } else { setAttr(el, 'class', classNames.join(' ').trim()); } } return this; } /** * 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`. * * @category Attributes * @example * * ```js * $('.pear').removeClass('pear').prop('outerHTML'); * //=>
  • Pear
  • * * $('.apple').addClass('red').removeClass().prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param name - Name of the class. If not specified, removes all elements. * @returns The instance itself. * @see {@link https://api.jquery.com/removeClass/} */ export function removeClass>( this: R, name?: | string | ((this: Element, i: number, className: string) => string | undefined), ): R { // Handle if value is a function if (typeof name === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { removeClass.call([el], name.call(el, i, el.attribs['class'] || '')); } }); } const classes = splitNames(name); const numClasses = classes.length; const removeAll = arguments.length === 0; return domEach(this, (el) => { if (!isTag(el)) return; if (removeAll) { // Short circuit the remove all case as this is the nice one el.attribs['class'] = ''; } else { const elClasses = splitNames(el.attribs['class']); let changed = false; for (let j = 0; j < numClasses; j++) { const index = elClasses.indexOf(classes[j]); if (index !== -1) { elClasses.splice(index, 1); changed = true; /* * We have to do another pass to ensure that there are not duplicate * classes listed */ j--; } } if (changed) { el.attribs['class'] = elClasses.join(' '); } } }); } /** * 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`. * * @category Attributes * @example * * ```js * $('.apple.green').toggleClass('fruit green red').prop('outerHTML'); * //=>
  • Apple
  • * * $('.apple.green').toggleClass('fruit green red', true).prop('outerHTML'); * //=>
  • Apple
  • * ``` * * @param value - Name of the class. Can also be a function. * @param stateVal - If specified the state of the class. * @returns The instance itself. * @see {@link https://api.jquery.com/toggleClass/} */ export function toggleClass>( this: R, value?: | string | (( this: Element, i: number, className: string, stateVal?: boolean, ) => string), stateVal?: boolean, ): R { // Support functions if (typeof value === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { toggleClass.call( [el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal, ); } }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; const classNames = value.split(rspace); const numClasses = classNames.length; const state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0; const numElements = this.length; for (let i = 0; i < numElements; i++) { const el = this[i]; // If selected element isn't a tag, move on if (!isTag(el)) continue; const elementClasses = splitNames(el.attribs['class']); // Check if class already exists for (let j = 0; j < numClasses; j++) { // Check if the class name is currently defined const index = elementClasses.indexOf(classNames[j]); // Add if stateValue === true or we are toggling and there is no value if (state >= 0 && index === -1) { elementClasses.push(classNames[j]); } else if (state <= 0 && index !== -1) { // Otherwise remove but only if the item exists elementClasses.splice(index, 1); } } el.attribs['class'] = elementClasses.join(' '); } return this; } ================================================ FILE: src/api/css.spec.ts ================================================ import type { Element } from 'domhandler'; import { beforeEach, describe, expect, it } from 'vitest'; import { cheerio, mixedText } from '../__fixtures__/fixtures.js'; import { type Cheerio, load } from '../index.js'; describe('$(...)', () => { describe('.css', () => { it('(prop): should return a css property value', () => { const el = cheerio('
  • '); expect(el.css('hai')).toBe('there'); }); it('([prop1, prop2]): should return the specified property values as an object', () => { const el = cheerio( '
  • ', ); expect(el.css(['margin', 'color'])).toStrictEqual({ margin: '1px', color: 'blue', }); }); it('(prop, val): should set a css property', () => { const el = cheerio('
  • '); el.css('color', 'red'); expect(el.attr('style')).toBe('margin: 0; color: red;'); expect(el.eq(1).attr('style')).toBe('color: red;'); }); it('(prop, val) : should skip text nodes', () => { const $text = load(mixedText); const $body = $text($text('body')[0].children); $body.css('test', 'value'); expect($text('body').html()).toBe( '1TEXT2', ); }); it('(prop, ""): should unset a css property', () => { const el = cheerio('
  • '); el.css('padding', ''); expect(el.attr('style')).toBe('margin: 0;'); }); it('(any, val): should ignore unsupported prop types', () => { const el = cheerio('
  • '); el.css(123 as never, 'test'); expect(el.attr('style')).toBe('padding: 1px;'); }); it('(prop): should not mangle embedded urls', () => { const el = cheerio( '
  • ', ); expect(el.css('background-image')).toBe( 'url(http://example.com/img.png)', ); }); it('(prop): should ignore blank properties', () => { const el = cheerio('
  • '); expect(el.css()).toStrictEqual({ color: '#aaa' }); }); it('(prop): should ignore blank values', () => { const el = cheerio('
  • '); expect(el.css()).toStrictEqual({ position: 'absolute' }); }); it('(prop): should return undefined for unmatched elements', () => { const $ = load('
  • '); expect($('ul').css('background-image')).toBeUndefined(); }); it('(prop): should return undefined for unmatched styles', () => { const el = cheerio('
  • '); expect(el.css('margin')).toBeUndefined(); }); describe('(prop, function):', () => { let $el: Cheerio; beforeEach(() => { const $ = load( '
    ', ); $el = $('div'); }); it('should iterate over the selection', () => { let count = 0; $el.css('margin', function (idx, value) { expect(idx).toBe(count); expect(value).toBe(`${count}px`); expect(this).toBe($el[count]); count++; return; }); expect(count).toBe(3); }); it('should set each attribute independently', () => { const values = ['4px', '', undefined]; $el.css('margin', (idx) => values[idx]); expect($el.eq(0).attr('style')).toBe('margin: 4px;'); expect($el.eq(1).attr('style')).toBe(''); expect($el.eq(2).attr('style')).toBe('margin: 2px;'); }); }); it('(obj): should set each key and val', () => { const el = cheerio('
  • '); el.css({ foo: 0 } as never); expect(el.eq(0).attr('style')).toBe('padding: 0; foo: 0;'); expect(el.eq(1).attr('style')).toBe('foo: 0;'); }); describe('parser', () => { it('should allow any whitespace between declarations', () => { const el = cheerio('
  • '); expect(el.css(['one', 'two', 'five'])).toStrictEqual({ one: '0', two: '1', }); }); it('should add malformed values to previous field (#1134)', () => { const el = cheerio( '', ); expect(el.css('background-image')).toStrictEqual( 'url(data:image/png;base64,iVBORw0KGgo)', ); }); }); }); }); ================================================ FILE: src/api/css.ts ================================================ import { type AnyNode, type Element, isTag } from 'domhandler'; import type { Cheerio } from '../cheerio.js'; import { domEach } from '../utils.js'; /** * Get the value of a style property for the first element in the set of matched * elements. * * @category CSS * @param names - Optionally the names of the properties of interest. * @returns A map of all of the style properties. * @see {@link https://api.jquery.com/css/} */ export function css( this: Cheerio, names?: string[], ): Record | undefined; /** * Get the value of a style property for the first element in the set of matched * elements. * * @category CSS * @param name - The name of the property. * @returns The property value for the given name. * @see {@link https://api.jquery.com/css/} */ export function css( this: Cheerio, name: string, ): string | undefined; /** * Set one CSS property for every matched element. * * @category CSS * @param prop - The name of the property. * @param val - The new value. * @returns The instance itself. * @see {@link https://api.jquery.com/css/} */ export function css( this: Cheerio, prop: string, val: string | ((this: Element, i: number, style: string) => string | void), ): Cheerio; /** * Set multiple CSS properties for every matched element. * * @category CSS * @param map - A map of property names and values. * @returns The instance itself. * @see {@link https://api.jquery.com/css/} */ export function css( this: Cheerio, map: Record, ): Cheerio; /** * Set multiple CSS properties for every matched element. * * @category CSS * @param prop - The names of the properties. * @param val - The new values. * @returns The instance itself. * @see {@link https://api.jquery.com/css/} */ export function css( this: Cheerio, prop?: string | string[] | Record, val?: string | ((this: Element, i: number, style: string) => string | void), ): Cheerio | Record | string | undefined { if ( (prop != null && val != null) || // When `prop` is a "plain" object (typeof prop === 'object' && !Array.isArray(prop)) ) { return domEach(this, (el, i) => { if (isTag(el)) { // `prop` can't be an array here anymore. setCss(el, prop as string, val, i); } }); } if (this.length === 0) { return; } return getCss(this[0], prop as string); } /** * Set styles of all elements. * * @private * @param el - Element to set style of. * @param prop - Name of property. * @param value - Value to set property to. * @param idx - Optional index within the selection. */ function setCss( el: Element, prop: string | Record, value: | string | ((this: Element, i: number, style: string) => string | void) | undefined, idx: number, ) { if (typeof prop === 'string') { const styles = getCss(el); const val = typeof value === 'function' ? value.call(el, idx, styles[prop]) : value; if (val === '') { delete styles[prop]; } else if (val != null) { styles[prop] = val; } el.attribs['style'] = stringify(styles); } else if (typeof prop === 'object') { const keys = Object.keys(prop); for (let i = 0; i < keys.length; i++) { const k = keys[i]; setCss(el, k, prop[k], i); } } } /** * Get the parsed styles of the first element. * * @private * @category CSS * @param el - Element to get styles from. * @param props - Optionally the names of the properties of interest. * @returns The parsed styles. */ function getCss(el: AnyNode, props?: string[]): Record; /** * Get a property from the parsed styles of the first element. * * @private * @category CSS * @param el - Element to get styles from. * @param prop - Name of the prop. * @returns The value of the property. */ function getCss(el: AnyNode, prop: string): string | undefined; function getCss( el: AnyNode, prop?: string | string[], ): Record | string | undefined { if (!(el && isTag(el))) return; const styles = parse(el.attribs['style']); if (typeof prop === 'string') { return styles[prop]; } if (Array.isArray(prop)) { const newStyles: Record = {}; for (const item of prop) { if (styles[item] != null) { newStyles[item] = styles[item]; } } return newStyles; } return styles; } /** * Stringify `obj` to styles. * * @private * @category CSS * @param obj - Object to stringify. * @returns The serialized styles. */ function stringify(obj: Record): string { return Object.keys(obj).reduce( (str, prop) => `${str}${str ? ' ' : ''}${prop}: ${obj[prop]};`, '', ); } /** * Parse `styles`. * * @private * @category CSS * @param styles - Styles to be parsed. * @returns The parsed styles. */ function parse(styles: string): Record { styles = (styles || '').trim(); if (!styles) return {}; const obj: Record = {}; let key: string | undefined; for (const str of styles.split(';')) { const n = str.indexOf(':'); // If there is no :, or if it is the first/last character, add to the previous item's value if (n < 1 || n === str.length - 1) { const trimmed = str.trimEnd(); if (trimmed.length > 0 && key !== undefined) { obj[key] += `;${trimmed}`; } } else { key = str.slice(0, n).trim(); obj[key] = str.slice(n + 1).trim(); } } return obj; } ================================================ FILE: src/api/extract.spec.ts ================================================ import { describe, expect, expectTypeOf, it } from 'vitest'; import * as fixtures from '../__fixtures__/fixtures.js'; import { load } from '../load-parse.js'; interface RedSelObject { red: string | undefined; sel: string | undefined; } interface RedSelMultipleObject { red: string[]; sel: string[]; } describe('$.extract', () => { it('should return an empty object when no selectors are provided', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf($root.extract({})).toEqualTypeOf>(); const emptyExtract = $root.extract({}); expect(emptyExtract).toStrictEqual({}); }); it('should return undefined for selectors that do not match any elements', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf($root.extract({ foo: 'bar' })).toEqualTypeOf<{ foo: string | undefined; }>(); const simpleExtract = $root.extract({ foo: 'bar' }); expect(simpleExtract).toStrictEqual({ foo: undefined }); }); it('should extract values for existing selectors', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf($root.extract({ red: '.red' })).toEqualTypeOf<{ red: string | undefined; }>(); expect($root.extract({ red: '.red' })).toStrictEqual({ red: 'Four' }); expectTypeOf( $root.extract({ red: '.red', sel: '.sel' }), ).toEqualTypeOf(); expect($root.extract({ red: '.red', sel: '.sel' })).toStrictEqual({ red: 'Four', sel: 'Three', }); }); it('should extract values using descriptor objects', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: { selector: '.red' }, sel: { selector: '.sel' }, }), ).toEqualTypeOf(); expect( $root.extract({ red: { selector: '.red' }, sel: { selector: '.sel' }, }), ).toStrictEqual({ red: 'Four', sel: 'Three' }); }); it('should extract multiple values for selectors', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: ['.red'], sel: ['.sel'], }), ).toEqualTypeOf<{ red: string[]; sel: string[] }>(); const multipleExtract = $root.extract({ red: ['.red'], sel: ['.sel'], }); expectTypeOf(multipleExtract).toEqualTypeOf(); expect(multipleExtract).toStrictEqual({ red: ['Four', 'Five', 'Nine'], sel: ['Three', 'Nine', 'Eleven'], }); }); it('should extract custom properties specified by the user', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: { selector: '.red', value: 'outerHTML' }, sel: { selector: '.sel', value: 'tagName' }, }), ).toEqualTypeOf(); expect( $root.extract({ red: { selector: '.red', value: 'outerHTML' }, sel: { selector: '.sel', value: 'tagName' }, }), ).toStrictEqual({ red: '
  • Four
  • ', sel: 'LI' }); }); it('should extract multiple custom properties for selectors', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: [{ selector: '.red', value: 'outerHTML' }], }), ).toEqualTypeOf<{ red: string[] }>(); expect( $root.extract({ red: [{ selector: '.red', value: 'outerHTML' }], }), ).toStrictEqual({ red: [ '
  • Four
  • ', '
  • Five
  • ', '
  • Nine
  • ', ], }); }); it('should extract values using custom extraction functions', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: { selector: '.red', value: (el, key) => `${key}=${$(el).text()}`, }, }), ).toEqualTypeOf<{ red: string | undefined }>(); expect( $root.extract({ red: { selector: '.red', value: (el, key) => `${key}=${$(el).text()}`, }, }), ).toStrictEqual({ red: 'red=Four' }); }); it('should correctly type check custom extraction functions returning non-string values', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: { selector: '.red', value: (el) => $(el).text().length, }, }), ).toEqualTypeOf<{ red: number | undefined }>(); expect( $root.extract({ red: { selector: '.red', value: (el) => $(el).text().length, }, }), ).toStrictEqual({ red: 4 }); }); it('should extract multiple values using custom extraction functions', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ red: [ { selector: '.red', value: (el, key) => `${key}=${$(el).text()}`, }, ], }), ).toEqualTypeOf<{ red: string[] }>(); expect( $root.extract({ red: [ { selector: '.red', value: (el, key) => `${key}=${$(el).text()}`, }, ], }), ).toStrictEqual({ red: ['red=Four', 'red=Five', 'red=Nine'] }); }); it('should extract nested objects based on selectors', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ section: { selector: 'ul:nth(1)', value: { red: '.red', sel: '.blue', }, }, }), ).toEqualTypeOf<{ section: { red: string | undefined; sel: string | undefined } | undefined; }>(); const subExtractObject = $root.extract({ section: { selector: 'ul:nth(1)', value: { red: '.red', sel: '.blue', }, }, }); expectTypeOf(subExtractObject).toEqualTypeOf<{ section: RedSelObject | undefined; }>(); expect(subExtractObject).toStrictEqual({ section: { red: 'Five', sel: 'Seven', }, }); }); it('should correctly type check nested objects returning non-string values', () => { const $ = load(fixtures.eleven); const $root = $.root(); expectTypeOf( $root.extract({ section: { selector: 'ul:nth(1)', value: { red: { selector: '.red', value: (el) => $(el).text().length, }, }, }, }), ).toEqualTypeOf<{ section: { red: number | undefined } | undefined; }>(); expect( $root.extract({ section: { selector: 'ul:nth(1)', value: { red: { selector: '.red', value: (el) => $(el).text().length, }, }, }, }), ).toStrictEqual({ section: { red: 4, }, }); }); it('should handle missing href properties without errors (#4239)', () => { const $ = load(fixtures.eleven); expect<{ links: string[] }>( $.extract({ links: [{ selector: 'li', value: 'href' }] }), ).toStrictEqual({ links: [] }); }); }); ================================================ FILE: src/api/extract.ts ================================================ import type { AnyNode, Element } from 'domhandler'; import type { Cheerio } from '../cheerio.js'; import type { prop } from './attributes.js'; type ExtractDescriptorFn = ( el: Element, key: string, // TODO: This could be typed with ExtractedMap obj: Record, ) => unknown; interface ExtractDescriptor { selector: string; value?: string | ExtractDescriptorFn | ExtractMap; } type ExtractValue = string | ExtractDescriptor | [string | ExtractDescriptor]; /** Descriptor map used by {@link extract}. */ export type ExtractMap = Record; type ExtractedValue = V extends [ string | ExtractDescriptor, ] ? NonNullable>[] : V extends string ? string | undefined : V extends ExtractDescriptor ? V['value'] extends infer U ? U extends ExtractMap ? ExtractedMap | undefined : U extends ExtractDescriptorFn ? ReturnType | undefined : ReturnType | undefined : never : never; /** Result type computed from an {@link ExtractMap}. */ export type ExtractedMap = { [key in keyof M]: ExtractedValue; }; function getExtractDescr( descr: string | ExtractDescriptor, ): Required { if (typeof descr === 'string') { return { selector: descr, value: 'textContent' }; } return { selector: descr.selector, value: descr.value ?? 'textContent', }; } /** * Extract multiple values from a document, and store them in an object. * * @param map - An object containing key-value pairs. The keys are the names of * the properties to be created on the object, and the values are the * selectors to be used to extract the values. * @returns An object containing the extracted values. */ export function extract( this: Cheerio, map: M, ): ExtractedMap { const ret: Record = {}; for (const key in map) { const descr = map[key]; const isArray = Array.isArray(descr); const { selector, value } = getExtractDescr(isArray ? descr[0] : descr); const fn: ExtractDescriptorFn = typeof value === 'function' ? value : typeof value === 'string' ? (el: Element) => this._make(el).prop(value) : (el: Element) => this._make(el).extract(value); if (isArray) { ret[key] = this._findBySelector(selector, Number.POSITIVE_INFINITY) .map((_, el) => fn(el, key, ret)) .get(); } else { const $ = this._findBySelector(selector, 1); ret[key] = $.length > 0 ? fn($[0], key, ret) : undefined; } } return ret as ExtractedMap; } ================================================ FILE: src/api/forms.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { cheerio, forms } from '../__fixtures__/fixtures.js'; import type { CheerioAPI } from '../index.js'; describe('$(...)', () => { let $: CheerioAPI; beforeEach(() => { $ = cheerio.load(forms); }); describe('.serializeArray', () => { it('() : should get form controls', () => { expect($('form#simple').serializeArray()).toStrictEqual([ { name: 'fruit', value: 'Apple', }, ]); }); it('() : should get nested form controls', () => { expect($('form#nested').serializeArray()).toHaveLength(2); const data = $('form#nested').serializeArray(); data.sort((a, b) => (a.value > b.value ? 1 : -1)); expect(data).toStrictEqual([ { name: 'fruit', value: 'Apple', }, { name: 'vegetable', value: 'Carrot', }, ]); }); it('() : should not get disabled form controls', () => { expect($('form#disabled').serializeArray()).toStrictEqual([]); }); it('() : should not get form controls with the wrong type', () => { expect($('form#submit').serializeArray()).toStrictEqual([ { name: 'fruit', value: 'Apple', }, ]); }); it('() : should get selected options', () => { expect($('form#select').serializeArray()).toStrictEqual([ { name: 'fruit', value: 'Orange', }, ]); }); it('() : should not get unnamed form controls', () => { expect($('form#unnamed').serializeArray()).toStrictEqual([ { name: 'fruit', value: 'Apple', }, ]); }); it('() : should get multiple selected options', () => { expect($('form#multiple').serializeArray()).toHaveLength(2); const data = $('form#multiple').serializeArray(); data.sort((a, b) => (a.value > b.value ? 1 : -1)); expect(data).toStrictEqual([ { name: 'fruit', value: 'Apple', }, { name: 'fruit', value: 'Orange', }, ]); }); it('() : should get individually selected elements', () => { const data = $('form#nested input').serializeArray(); data.sort((a, b) => (a.value > b.value ? 1 : -1)); expect(data).toStrictEqual([ { name: 'fruit', value: 'Apple', }, { name: 'vegetable', value: 'Carrot', }, ]); }); it('() : should standardize line breaks', () => { expect($('form#textarea').serializeArray()).toStrictEqual([ { name: 'fruits', value: 'Apple\r\nOrange', }, ]); }); it("() : shouldn't serialize the empty string", () => { expect($('').serializeArray()).toStrictEqual([]); expect( $('').serializeArray(), ).toStrictEqual([]); expect( $('').serializeArray(), ).toStrictEqual([ { name: 'fruit', value: 'pineapple', }, ]); }); it('() : should serialize inputs without value attributes', () => { expect($('').serializeArray()).toStrictEqual([ { name: 'fruit', value: '', }, ]); }); }); describe('.serialize', () => { it('() : should get form controls', () => { expect($('form#simple').serialize()).toBe('fruit=Apple'); }); it('() : should get nested form controls', () => { expect($('form#nested').serialize()).toBe('fruit=Apple&vegetable=Carrot'); }); it('() : should not get disabled form controls', () => { expect($('form#disabled').serialize()).toBe(''); }); it('() : should get multiple selected options', () => { expect($('form#multiple').serialize()).toBe('fruit=Apple&fruit=Orange'); }); it("() : should encode spaces as +'s", () => { expect($('form#spaces').serialize()).toBe('fruit=Blood+orange'); }); }); }); ================================================ FILE: src/api/forms.ts ================================================ import { type AnyNode, isTag } from 'domhandler'; import type { Cheerio } from '../cheerio.js'; /* * 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 */ const submittableSelector = 'input,select,textarea,keygen'; const r20 = /%20/g; const rCRLF = /\r?\n/g; /** * Encode a set of form elements as a string for submission. * * @category Forms * @example * * ```js * $('
    ').serialize(); * //=> 'foo=bar' * ``` * * @returns The serialized form. * @see {@link https://api.jquery.com/serialize/} */ export function serialize(this: Cheerio): string { // Convert form elements into name/value objects const arr = this.serializeArray(); // Serialize each element into a key/value string const retArr = arr.map( (data) => `${encodeURIComponent(data.name)}=${encodeURIComponent(data.value)}`, ); // Return the resulting serialization return retArr.join('&').replace(r20, '+'); } /** * Encode a set of form elements as an array of names and values. * * @category Forms * @example * * ```js * $('
    ').serializeArray(); * //=> [ { name: 'foo', value: 'bar' } ] * ``` * * @returns The serialized form. * @see {@link https://api.jquery.com/serializeArray/} */ export function serializeArray( this: Cheerio, ): { name: string; value: string; }[] { // Resolve all form elements from either forms or collections of form elements return this.map((_, elem) => { const $elem = this._make(elem); if (isTag(elem) && elem.name === 'form') { return $elem.find(submittableSelector).toArray(); } return $elem.filter(submittableSelector).toArray(); }) .filter( // Verify elements have a name (`attr.name`) and are not disabled (`:enabled`) '[name!=""]:enabled' + // And cannot be clicked (`[type=submit]`) or are used in `x-www-form-urlencoded` (`[type=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< AnyNode, { name: string; value: string; } >((_, elem) => { const $elem = this._make(elem); const name = $elem.attr('name'); if (!name) return []; // If there is no value set (e.g. `undefined`, `null`), then default value to empty const value = $elem.val() ?? ''; // If we have an array of values (e.g. `'; // Comments const comment = ''; const conditional = ''; // Text const text = 'lorem ipsum'; // Script const script = ''; const scriptEmpty = ''; // Style const style = ''; const styleEmpty = ''; // Directives const directive = ''; function rootTest(root: Document) { expect(root).toHaveProperty('type', 'root'); expect(root.nextSibling).toBe(null); expect(root.previousSibling).toBe(null); expect(root.parentNode).toBe(null); const child = root.childNodes[0]; expect(child.parentNode).toBe(root); } describe('parse', () => { describe('evaluate', () => { it(`should parse basic empty tags: ${basic}`, () => { const [tag] = parse(basic, defaultOpts, true, null).children as Element[]; expect(tag.type).toBe('tag'); expect(tag.tagName).toBe('html'); expect(tag.childNodes).toHaveLength(2); }); it(`should handle sibling tags: ${siblings}`, () => { const dom = parse(siblings, defaultOpts, false, null) .children as Element[]; const [h2, p] = dom; expect(dom).toHaveLength(2); expect(h2.tagName).toBe('h2'); expect(p.tagName).toBe('p'); }); it(`should handle single tags: ${single}`, () => { const [tag] = parse(single, defaultOpts, false, null) .children as Element[]; expect(tag.type).toBe('tag'); expect(tag.tagName).toBe('br'); expect(tag.childNodes).toHaveLength(0); }); it(`should handle malformatted single tags: ${singleWrong}`, () => { const [tag] = parse(singleWrong, defaultOpts, false, null) .children as Element[]; expect(tag.type).toBe('tag'); expect(tag.tagName).toBe('br'); expect(tag.childNodes).toHaveLength(0); }); it(`should handle tags with children: ${children}`, () => { const [tag] = parse(children, defaultOpts, true, null) .children as Element[]; expect(tag.type).toBe('tag'); expect(tag.tagName).toBe('html'); expect(tag.childNodes).toBeTruthy(); expect(tag.childNodes[1]).toHaveProperty('tagName', 'body'); expect((tag.childNodes[1] as Element).childNodes).toHaveLength(1); }); it(`should handle tags with children: ${li}`, () => { const [tag] = parse(li, defaultOpts, false, null).children as Element[]; expect(tag.childNodes).toHaveLength(1); expect(tag.childNodes[0]).toHaveProperty('data', 'Durian'); }); it(`should handle tags with attributes: ${attributes}`, () => { const attrs = parse(attributes, defaultOpts, false, null) .children[0] as Element; expect(attrs.attribs).toBeTruthy(); expect(attrs.attribs).toHaveProperty('src', 'hello.png'); expect(attrs.attribs).toHaveProperty('alt', 'man waving'); }); it(`should handle value-less attributes: ${noValueAttribute}`, () => { const attrs = parse(noValueAttribute, defaultOpts, false, null) .children[0] as Element; expect(attrs.attribs).toBeTruthy(); expect(attrs.attribs).toHaveProperty('disabled', ''); }); it(`should handle comments: ${comment}`, () => { const elem = parse(comment, defaultOpts, false, null).children[0]; expect(elem.type).toBe('comment'); expect(elem).toHaveProperty('data', ' sexy '); }); it(`should handle conditional comments: ${conditional}`, () => { const elem = parse(conditional, defaultOpts, false, null).children[0]; expect(elem.type).toBe('comment'); expect(elem).toHaveProperty( 'data', conditional.replace('', ''), ); }); it(`should handle text: ${text}`, () => { const text_ = parse(text, defaultOpts, false, null).children[0]; expect(text_.type).toBe('text'); expect(text_).toHaveProperty('data', 'lorem ipsum'); }); it(`should handle script tags: ${script}`, () => { const script_ = parse(script, defaultOpts, false, null) .children[0] as Element; expect(script_.type).toBe('script'); expect(script_.tagName).toBe('script'); expect(script_.attribs).toHaveProperty('type', 'text/javascript'); expect(script_.childNodes).toHaveLength(1); expect(script_.childNodes[0].type).toBe('text'); expect(script_.childNodes[0]).toHaveProperty( 'data', 'alert("hi world!");', ); }); it(`should handle style tags: ${style}`, () => { const style_ = parse(style, defaultOpts, false, null) .children[0] as Element; expect(style_.type).toBe('style'); expect(style_.tagName).toBe('style'); expect(style_.attribs).toHaveProperty('type', 'text/css'); expect(style_.childNodes).toHaveLength(1); expect(style_.childNodes[0].type).toBe('text'); expect(style_.childNodes[0]).toHaveProperty( 'data', ' h2 { color:blue; } ', ); }); it(`should handle directives: ${directive}`, () => { const elem = parse(directive, defaultOpts, true, null).children[0]; expect(elem.type).toBe('directive'); expect(elem).toHaveProperty('data', '!DOCTYPE html'); expect(elem).toHaveProperty('name', '!doctype'); }); }); describe('.parse', () => { // Root test utility it(`should add root to: ${basic}`, () => { const root = parse(basic, defaultOpts, true, null); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0]).toHaveProperty('tagName', 'html'); }); it(`should add root to: ${siblings}`, () => { const root = parse(siblings, defaultOpts, false, null); rootTest(root); expect(root.childNodes).toHaveLength(2); expect(root.childNodes[0]).toHaveProperty('tagName', 'h2'); expect(root.childNodes[1]).toHaveProperty('tagName', 'p'); expect(root.childNodes[1].parent).toBe(root); }); it(`should add root to: ${comment}`, () => { const root = parse(comment, defaultOpts, false, null); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0].type).toBe('comment'); }); it(`should add root to: ${text}`, () => { const root = parse(text, defaultOpts, false, null); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0].type).toBe('text'); }); it(`should add root to: ${scriptEmpty}`, () => { const root = parse(scriptEmpty, defaultOpts, false, null); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0].type).toBe('script'); }); it(`should add root to: ${styleEmpty}`, () => { const root = parse(styleEmpty, defaultOpts, false, null); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0].type).toBe('style'); }); it(`should add root to: ${directive}`, () => { const root = parse(directive, defaultOpts, true, null); rootTest(root); expect(root.childNodes).toHaveLength(2); expect(root.childNodes[0].type).toBe('directive'); }); it('should simply return root', () => { const oldroot = parse(basic, defaultOpts, true, null); const root = parse(oldroot, defaultOpts, true, null); expect(root).toBe(oldroot); rootTest(root); expect(root.childNodes).toHaveLength(1); expect(root.childNodes[0]).toHaveProperty('tagName', 'html'); }); it('should expose the DOM level 1 API', () => { const root = parse( '

    ', defaultOpts, false, null, ).childNodes[0] as Element; const childNodes = root.childNodes as Element[]; expect(childNodes).toHaveLength(3); expect(root.tagName).toBe('div'); expect(root.firstChild).toBe(childNodes[0]); expect(root.lastChild).toBe(childNodes[2]); expect(childNodes[0].tagName).toBe('a'); expect(childNodes[0].previousSibling).toBe(null); expect(childNodes[0].nextSibling).toBe(childNodes[1]); expect(childNodes[0].parentNode).toBe(root); expect(childNodes[0].childNodes).toHaveLength(0); expect(childNodes[0].firstChild).toBe(null); expect(childNodes[0].lastChild).toBe(null); expect(childNodes[1].tagName).toBe('span'); expect(childNodes[1].previousSibling).toBe(childNodes[0]); expect(childNodes[1].nextSibling).toBe(childNodes[2]); expect(childNodes[1].parentNode).toBe(root); expect(childNodes[1].childNodes).toHaveLength(0); expect(childNodes[1].firstChild).toBe(null); expect(childNodes[1].lastChild).toBe(null); expect(childNodes[2].tagName).toBe('p'); expect(childNodes[2].previousSibling).toBe(childNodes[1]); expect(childNodes[2].nextSibling).toBe(null); expect(childNodes[2].parentNode).toBe(root); expect(childNodes[2].childNodes).toHaveLength(0); expect(childNodes[2].firstChild).toBe(null); expect(childNodes[2].lastChild).toBe(null); }); it('Should parse less than or equal sign sign', () => { const root = parse('A<=B', defaultOpts, false, null); const { childNodes } = root; expect(childNodes[0]).toHaveProperty('tagName', 'i'); expect((childNodes[0] as Element).childNodes[0]).toHaveProperty( 'data', 'A', ); expect(childNodes[1]).toHaveProperty('data', '<='); expect(childNodes[2]).toHaveProperty('tagName', 'i'); expect((childNodes[2] as Element).childNodes[0]).toHaveProperty( 'data', 'B', ); }); it('Should ignore unclosed CDATA', () => { const root = parse( '', defaultOpts, false, null, ); const childNodes = root.childNodes as Element[]; expect(childNodes[0].tagName).toBe('a'); expect(childNodes[1].tagName).toBe('script'); expect(childNodes[1].childNodes[0]).toHaveProperty( 'data', 'foo // to documents', () => { const root = parse('', defaultOpts, true, null); const childNodes = root.childNodes as Element[]; expect(childNodes[0].tagName).toBe('html'); expect(childNodes[0].childNodes[0]).toHaveProperty('tagName', 'head'); }); it('Should implicitly create around ', () => { const root = parse( '
    bar
    ', defaultOpts, false, null, ); const table = root.childNodes[0] as Element; expect(table.tagName).toBe('table'); expect(table.childNodes.length).toBe(1); const tbody = table.childNodes[0] as Element; expect(table.childNodes[0]).toHaveProperty('tagName', 'tbody'); const tr = tbody.childNodes[0] as Element; expect(tr).toHaveProperty('tagName', 'tr'); const td = tr.childNodes[0] as Element; expect(td).toHaveProperty('tagName', 'td'); expect(td.childNodes[0]).toHaveProperty('data', 'bar'); }); it('Should parse custom tag ', () => { const root = parse('test', defaultOpts, false, null); const childNodes = root.childNodes as Element[]; expect(childNodes.length).toBe(1); expect(childNodes[0].tagName).toBe('line'); expect(childNodes[0].childNodes[0]).toHaveProperty('data', 'test'); }); it('Should properly parse misnested table tags', () => { const root = parse( 'i1i2i3', defaultOpts, false, null, ); const childNodes = root.childNodes as Element[]; expect(childNodes.length).toBe(3); for (let i = 0; i < childNodes.length; i++) { const child = childNodes[i]; expect(child.tagName).toBe('tr'); expect(child.childNodes[0]).toHaveProperty('tagName', 'td'); expect((child.childNodes[0] as Element).childNodes[0]).toHaveProperty( 'data', `i${i + 1}`, ); } }); it('Should correctly parse data url attributes', () => { const html = '
    '; const expectedAttr = 'font-family:"butcherman-caps"; src:url(data:font/opentype;base64,AAEA...);'; const root = parse(html, defaultOpts, false, null); const childNodes = root.childNodes as Element[]; expect(childNodes[0].attribs).toHaveProperty('style', expectedAttr); }); it('Should treat tag content as text', () => { const root = parse('<xmp><h2>', defaultOpts, false, null); const childNodes = root.childNodes as Element[]; expect(childNodes[0].childNodes[0]).toHaveProperty('data', '

    '); }); it('Should correctly parse malformed numbered entities', () => { const root = parse('

    z&#

    ', defaultOpts, false, null); const childNodes = root.childNodes as Element[]; expect(childNodes[0].childNodes[0]).toHaveProperty('data', 'z&#'); }); it('Should correctly parse mismatched headings', () => { const root = parse('

    Test

    ', defaultOpts, false, null); const { childNodes } = root; expect(childNodes.length).toBe(2); expect(childNodes[0]).toHaveProperty('tagName', 'h2'); expect(childNodes[1]).toHaveProperty('tagName', 'div'); }); it('Should correctly parse tricky
     content', () => {
          const root = parse(
            '
    \nA <- factor(A, levels = c("c","a","b"))\n
    ', defaultOpts, false, null, ); const childNodes = root.childNodes as Element[]; expect(childNodes.length).toBe(1); expect(childNodes[0].tagName).toBe('pre'); expect(childNodes[0].childNodes[0]).toHaveProperty( 'data', 'A <- factor(A, levels = c("c","a","b"))\n', ); }); it('should pass the options for including the location info to parse5', () => { const root = parse( '

    Hello

    ', { ...defaultOpts, sourceCodeLocationInfo: true }, false, null, ); const location = root.children[0].sourceCodeLocation; expect(typeof location).toBe('object'); expect(location?.endOffset).toBe(12); }); }); }); ================================================ FILE: src/parse.ts ================================================ import { type AnyNode, isDocument as checkIsDocument, Document, type ParentNode, } from 'domhandler'; import { removeElement } from 'domutils'; import type { InternalOptions } from './options.js'; /** * Get the parse function with options. * * @param parser - The parser function. * @returns The parse function with options. */ export function getParse( parser: ( content: string, options: InternalOptions, isDocument: boolean, context: ParentNode | null, ) => Document, ) { /** * Parse a HTML string or a node. * * @param content - The HTML string or node. * @param options - The parser options. * @param isDocument - If `content` is a document. * @param context - The context node in the DOM tree. * @returns The parsed document node. */ return function parse( content: string | Document | AnyNode | AnyNode[] | Buffer, options: InternalOptions, isDocument: boolean, context: ParentNode | null, ): Document { if (typeof Buffer !== 'undefined' && Buffer.isBuffer(content)) { content = content.toString(); } if (typeof content === 'string') { return parser(content, options, isDocument, context); } const doc = content as AnyNode | AnyNode[] | Document; if (!Array.isArray(doc) && checkIsDocument(doc)) { // If `doc` is already a root, just return it return doc; } // Add content to new root element const root = new Document([]); // Update the DOM using the root update(doc, root); return root; }; } /** * Update the dom structure, for one changed layer. * * @param newChilds - The new children. * @param parent - The new parent. * @returns The parent node. */ export function update( newChilds: AnyNode[] | AnyNode, parent: ParentNode | null, ): ParentNode | null { // Normalize const arr = Array.isArray(newChilds) ? newChilds : [newChilds]; // Update parent if (parent) { parent.children = arr; } else { parent = null; } // Update neighbors for (let i = 0; i < arr.length; i++) { const node = arr[i]; // Cleanly remove existing nodes from their previous structures. if (node.parent && node.parent.children !== arr) { removeElement(node); } if (parent) { node.prev = arr[i - 1] || null; node.next = arr[i + 1] || null; } else { node.prev = node.next = null; } node.parent = parent; } return parent; } ================================================ FILE: src/parsers/parse5-adapter.ts ================================================ import { type AnyNode, type Document, isDocument, type ParentNode, } from 'domhandler'; import { parse as parseDocument, parseFragment, serializeOuter } from 'parse5'; import { adapter as htmlparser2Adapter } from 'parse5-htmlparser2-tree-adapter'; import type { InternalOptions } from '../options.js'; /** * Parse the content with `parse5` in the context of the given `ParentNode`. * * @param content - The content to parse. * @param options - A set of options to use to parse. * @param isDocument - Whether to parse the content as a full HTML document. * @param context - The context in which to parse the content. * @returns The parsed content. */ export function parseWithParse5( content: string, options: InternalOptions, isDocument: boolean, context: ParentNode | null, ): Document { options.treeAdapter ??= htmlparser2Adapter; if (options.scriptingEnabled !== false) { options.scriptingEnabled = true; } return isDocument ? parseDocument(content, options) : parseFragment(context, content, options); } const renderOpts = { treeAdapter: htmlparser2Adapter }; /** * Renders the given DOM tree with `parse5` and returns the result as a string. * * @param dom - The DOM tree to render. * @returns The rendered document. */ export function renderWithParse5(dom: AnyNode | ArrayLike): string { /* * `dom-serializer` passes over the special "root" node and renders the * node's children in its place. To mimic this behavior with `parse5`, an * equivalent operation must be applied to the input array. */ const nodes = 'length' in dom ? dom : [dom]; for (let index = 0; index < nodes.length; index += 1) { const node = nodes[index]; if (isDocument(node)) { Array.prototype.splice.call(nodes, index, 1, ...node.children); } } let result = ''; for (let index = 0; index < nodes.length; index += 1) { const node = nodes[index]; result += serializeOuter(node, renderOpts); } return result; } ================================================ FILE: src/slim.ts ================================================ /** * @file Alternative entry point for Cheerio that always uses htmlparser2. This * way, parse5 won't be loaded, saving some memory. */ import render from 'dom-serializer'; import type { AnyNode } from 'domhandler'; import { parseDocument } from 'htmlparser2'; import { type CheerioAPI, getLoad } from './load.js'; import type { CheerioOptions } from './options.js'; import { getParse } from './parse.js'; export type { Cheerio } from './cheerio.js'; export type { AnyNode, CheerioAPI, Document, Element, ParentNode, } from './load.js'; export type { CheerioOptions, HTMLParser2Options } from './options.js'; export { contains, merge } from './static.js'; export type * from './types.js'; /** * Create a querying function, bound to a document created from the provided * markup. * * @param content - Markup to be loaded. * @param options - Options for the created instance. * @param isDocument - Always `false` here, as we are always using * `htmlparser2`. * @returns The loaded document. * @see {@link https://cheerio.js.org#loading} for additional usage information. */ export const load: ( content: string | AnyNode | AnyNode[] | Buffer, options?: CheerioOptions | null, isDocument?: boolean, ) => CheerioAPI = getLoad(getParse(parseDocument), render); ================================================ FILE: src/static.spec.ts ================================================ import { beforeEach, describe, expect, it } from 'vitest'; import { cheerio, eleven, food } from './__fixtures__/fixtures.js'; import type { CheerioAPI } from './index.js'; describe('cheerio', () => { describe('.html', () => { it('() : should return innerHTML; $.html(obj) should return outerHTML', () => { const $div = cheerio( 'div', '
    foobar
    ', ); const span = $div.children()[1]; expect(cheerio(span).html()).toBe('bar'); expect(cheerio.html(span)).toBe('bar'); }); it('() : should accept an object, an array, or a cheerio object', () => { const $span = cheerio('foo'); expect(cheerio.html($span[0])).toBe('foo'); expect(cheerio.html($span)).toBe('foo'); }); it('() : should be able to set to an empty string', () => { const $elem = cheerio('foo').html(''); expect(cheerio.html($elem)).toBe(''); }); it('() : does not render the root element', () => { const $ = cheerio.load(''); expect(cheerio.html($.root())).toBe( '', ); }); it('(, , ) : does not render the root element', () => { const $ = cheerio.load('
    a div
    a span'); const $collection = $('div').add($.root()).add('span'); const expected = '
    a div
    a span
    a div
    a span'; expect(cheerio.html($collection)).toBe(expected); }); it('() : does not crash with `null` as `this` value', () => { const { html } = cheerio; expect(html.call(null as never)).toBe(''); expect(html.call(null as never, '#nothing')).toBe(''); }); }); describe('.text', () => { it('(cheerio object) : should return the text contents of the specified elements', () => { const $ = cheerio.load('This is content.'); expect(cheerio.text($('a'))).toBe('This is content.'); }); it('(cheerio object) : should omit comment nodes', () => { const $ = cheerio.load( 'This is not a comment.', ); expect(cheerio.text($('a'))).toBe('This is not a comment.'); }); it('(cheerio object) : should include text contents of children recursively', () => { const $ = cheerio.load( 'This is
    a child with another child and not a comment followed by one last child and some final
    text.
    ', ); expect(cheerio.text($('a'))).toBe( 'This is a child with another child and not a comment followed by one last child and some final text.', ); }); it('() : should return the rendered text content of the root', () => { const $ = cheerio.load( 'This is
    a child with another child and not a comment followed by one last child and some final
    text.
    ', ); expect(cheerio.text($.root())).toBe( 'This is a child with another child and not a comment followed by one last child and some final text.', ); }); it('(cheerio object) : should not omit script tags', () => { const $ = cheerio.load(''); expect(cheerio.text($.root())).toBe('console.log("test")'); }); it('(cheerio object) : should omit style tags', () => { const $ = cheerio.load( '', ); expect($.text()).toBe('.cf-hidden { display: none; }'); }); it('() : does not crash with `null` as `this` value', () => { const { text } = cheerio; expect(text.call(null as never)).toBe(''); }); }); describe('.parseHTML', () => { const $ = cheerio.load(''); it('() : returns null', () => { expect($.parseHTML()).toBe(null); }); it('(null) : returns null', () => { expect($.parseHTML(null)).toBe(null); }); it('("") : returns null', () => { expect($.parseHTML('')).toBe(null); }); it('(largeHtmlString) : parses large HTML strings', () => { const html = '
    '.repeat(10); const nodes = $.parseHTML(html); expect(nodes.length).toBe(10); expect(nodes).toBeInstanceOf(Array); }); it('("'; expect($.parseHTML(html)).toHaveLength(0); }); it('("'; expect($.parseHTML(html, true)[0]).toHaveProperty('tagName', 'script'); }); it('("scriptAndNonScript) : preserves non-script nodes', () => { const html = '
    '; expect($.parseHTML(html)[0]).toHaveProperty('tagName', 'div'); }); it('(scriptAndNonScript, true) : Preserves script position', () => { const html = '
    '; expect($.parseHTML(html, true)[0]).toHaveProperty('tagName', 'script'); }); it('(text) : returns a text node', () => { expect($.parseHTML('text')[0].type).toBe('text'); }); it('(>text) : preserves leading whitespace', () => { expect($.parseHTML('\t
    ')[0]).toHaveProperty('data', '\t'); }); it('( text) : Leading spaces are treated as text nodes', () => { expect($.parseHTML('
    ')[0].type).toBe('text'); }); it('(html) : should preserve content', () => { const html = '
    test div
    '; expect(cheerio($.parseHTML(html)[0]).html()).toBe('test div'); }); it('(malformedHtml) : should not break', () => { expect($.parseHTML('')).toHaveLength(1); }); it('(garbageInput) : should not cause an error', () => { expect( $.parseHTML('<#if>

    This is a test.

    <#/if>'), ).toBeTruthy(); }); it('(text) : should return an array that is not effected by DOM manipulation methods', () => { const $div = cheerio.load('
    '); const elems = $div.parseHTML(''); $div('div').append(elems); expect(elems).toHaveLength(2); }); it('(html, context) : should ignore context argument', () => { const $div = cheerio.load('
    '); const elems = $div.parseHTML('', { foo: 123 }); $div('div').append(elems); expect(elems).toHaveLength(1); }); it('(html, context, keepScripts) : should ignore context argument', () => { const $div = cheerio.load('
    '); const elems = $div.parseHTML( '', { foo: 123 }, true, ); $div('div').append(elems); expect(elems).toHaveLength(2); }); }); describe('.merge', () => { const $ = cheerio.load(''); it('should be a function', () => { expect(typeof $.merge).toBe('function'); }); it('(arraylike, arraylike) : should modify the first array, but not the second', () => { const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const ret = $.merge(arr1, arr2); expect(typeof ret).toBe('object'); expect(Array.isArray(ret)).toBe(true); expect(ret).toBe(arr1); expect(arr1).toHaveLength(6); expect(arr2).toHaveLength(3); }); it('(arraylike, arraylike) : should handle objects that arent arrays, but are arraylike', () => { const arr1: ArrayLike = { length: 3, 0: 'a', 1: 'b', 2: 'c', }; const arr2 = { length: 3, 0: 'd', 1: 'e', 2: 'f', }; $.merge(arr1, arr2); expect(arr1).toHaveLength(6); expect(arr1[3]).toBe('d'); expect(arr1[4]).toBe('e'); expect(arr1[5]).toBe('f'); expect(arr2).toHaveLength(3); }); it('(?, ?) : should gracefully reject invalid inputs', () => { expect($.merge([4], 3 as never)).toBeFalsy(); expect($.merge({} as never, {} as never)).toBeFalsy(); expect($.merge([], {} as never)).toBeFalsy(); expect($.merge({} as never, [])).toBeFalsy(); const fakeArray1 = { length: 3, 0: 'a', 1: 'b', 3: 'd' }; expect($.merge(fakeArray1, [])).toBeFalsy(); expect($.merge([], fakeArray1)).toBeFalsy(); expect($.merge({ length: '7' } as never, [])).toBeFalsy(); expect($.merge({ length: -1 }, [])).toBeFalsy(); }); it('(?, ?) : should no-op on invalid inputs', () => { const fakeArray1 = { length: 3, 0: 'a', 1: 'b', 3: 'd' }; $.merge(fakeArray1, []); expect(fakeArray1).toHaveLength(3); expect(fakeArray1[0]).toBe('a'); expect(fakeArray1[1]).toBe('b'); expect(fakeArray1[3]).toBe('d'); $.merge([], fakeArray1); expect(fakeArray1).toHaveLength(3); expect(fakeArray1[0]).toBe('a'); expect(fakeArray1[1]).toBe('b'); expect(fakeArray1[3]).toBe('d'); }); }); describe('.contains', () => { let $: CheerioAPI; beforeEach(() => { $ = cheerio.load(food); }); it('(container, contained) : should correctly detect the provided element', () => { const $food = $('#food'); const $fruits = $('#fruits'); const $apple = $('.apple'); expect($.contains($food[0], $fruits[0])).toBe(true); expect($.contains($food[0], $apple[0])).toBe(true); }); it('(container, other) : should not detect elements that are not contained', () => { const $fruits = $('#fruits'); const $vegetables = $('#vegetables'); const $apple = $('.apple'); expect($.contains($vegetables[0], $apple[0])).toBe(false); expect($.contains($fruits[0], $vegetables[0])).toBe(false); expect($.contains($vegetables[0], $fruits[0])).toBe(false); expect($.contains($fruits[0], $fruits[0])).toBe(false); expect($.contains($vegetables[0], $vegetables[0])).toBe(false); }); }); describe('.root', () => { it('() : should return a cheerio-wrapped root object', () => { const $ = cheerio.load('foo'); $.root().append('
    '); expect($.html()).toBe( 'foo
    ', ); }); }); describe('.extract', () => { it('() : should extract values for selectors', () => { const $ = cheerio.load(eleven); expect( $.extract({ red: [{ selector: '.red', value: 'outerHTML' }], }), ).toStrictEqual({ red: [ '
  • Four
  • ', '
  • Five
  • ', '
  • Nine
  • ', ], }); }); }); }); ================================================ FILE: src/static.ts ================================================ import type { AnyNode, Document } from 'domhandler'; import { textContent } from 'domutils'; import type { ExtractedMap, ExtractMap } from './api/extract.js'; import type { Cheerio } from './cheerio.js'; import type { CheerioAPI } from './load.js'; import { type CheerioOptions, flattenOptions, type InternalOptions, } from './options.js'; import type { BasicAcceptedElems } from './types.js'; /** * Helper function to render a DOM. * * @param that - Cheerio instance to render. * @param dom - The DOM to render. Defaults to `that`'s root. * @param options - Options for rendering. * @returns The rendered document. */ function render( that: CheerioAPI, dom: BasicAcceptedElems | undefined, options: InternalOptions, ): string { if (!that) return ''; return that(dom ?? that._root.children, null, undefined, options).toString(); } /** * Checks if a passed object is an options object. * * @param dom - Object to check if it is an options object. * @param options - Options object. * @returns Whether the object is an options object. */ function isOptions( dom?: BasicAcceptedElems | CheerioOptions | null, options?: CheerioOptions, ): dom is CheerioOptions { return ( !options && typeof dom === 'object' && dom != null && !('length' in dom) && !('type' in dom) ); } /** * Renders the document. * * @category Static * @param options - Options for the renderer. * @returns The rendered document. */ export function html(this: CheerioAPI, options?: CheerioOptions): string; /** * Renders the document. * * @category Static * @param dom - Element to render. * @param options - Options for the renderer. * @returns The rendered document. */ export function html( this: CheerioAPI, dom?: BasicAcceptedElems, options?: CheerioOptions, ): string; export function html( this: CheerioAPI, dom?: BasicAcceptedElems | CheerioOptions, options?: CheerioOptions, ): string { /* * Be flexible about parameters, sometimes we call html(), * with options as only parameter * check dom argument for dom element specific properties * assume there is no 'length' or 'type' properties in the options object */ const toRender = isOptions(dom) ? ((options = dom), undefined) : dom; /* * Sometimes `$.html()` is used without preloading html, * so fallback non-existing options to the default ones. */ const opts = { ...this?._options, ...flattenOptions(options), }; return render(this, toRender, opts); } /** * Render the document as XML. * * @category Static * @param dom - Element to render. * @returns THe rendered document. */ export function xml( this: CheerioAPI, dom?: BasicAcceptedElems, ): string { const options = { ...this._options, xmlMode: true }; return render(this, dom, options); } /** * Render the document as text. * * This returns the `textContent` of the passed elements. The result will * include the contents of ` ================================================ FILE: website/src/components/LiveEditor.astro ================================================ --- /** * Astro component that wraps LiveCode for use in markdown. * Usage in markdown: ... */ import { LiveCode } from './live-code'; ---
    ================================================ FILE: website/src/components/Navbar.astro ================================================ --- import { Image } from 'astro:assets'; import logo from '@/assets/orange-c.svg'; const navItems = [ { label: 'Learn', href: '/docs/intro', match: (p: string) => p.startsWith('/docs/') && !p.startsWith('/docs/api'), }, { label: 'API', href: '/docs/api', match: (p: string) => p.startsWith('/docs/api'), }, { label: 'Blog', href: '/blog', match: (p: string) => p.startsWith('/blog') }, ]; const currentPath = Astro.url.pathname; ---
    ================================================ FILE: website/src/components/Sidebar.astro ================================================ --- import { getCollection } from 'astro:content'; interface SidebarItem { label: string; href: string; } interface SidebarSection { title: string; items: SidebarItem[]; } interface ApiGroup { title: string; items: SidebarItem[]; } interface Props { currentPath: string; } const { currentPath } = Astro.props; const sidebar: SidebarSection[] = [ { title: 'Getting Started', items: [{ label: 'Introduction', href: '/docs/intro' }], }, { title: 'Basics', items: [ { label: 'Loading Documents', href: '/docs/basics/loading' }, { label: 'Selecting Elements', href: '/docs/basics/selecting' }, { label: 'Traversing the DOM', href: '/docs/basics/traversing' }, { label: 'Manipulating Elements', href: '/docs/basics/manipulation' }, ], }, { title: 'Advanced', items: [ { label: 'Configuring Cheerio', href: '/docs/advanced/configuring-cheerio', }, { label: 'Extending Cheerio', href: '/docs/advanced/extending-cheerio' }, { label: 'Extracting Data', href: '/docs/advanced/extract' }, ], }, ]; // Dynamically build API sub-pages from the content collection const allDocs = await getCollection('docs'); const apiDocs = allDocs.filter( (doc) => doc.id.startsWith('api/') && doc.id !== 'api/index.md' && doc.id !== 'api/index', ); // Group by kind (directory name) const kindLabels: Record = { classes: 'Classes', interfaces: 'Interfaces', functions: 'Functions', types: 'Types', variables: 'Variables', }; const kindOrder = ['classes', 'variables', 'functions', 'interfaces', 'types']; const apiGroups: ApiGroup[] = []; const groupedByKind = new Map(); for (const doc of apiDocs) { // id looks like "api/classes/Cheerio.md" or "api/functions/contains" const parts = doc.id .replace(/^api\//, '') .replace(/\.mdx?$/, '') .split('/'); if (parts.length !== 2) continue; const [kind, name] = parts; if (!groupedByKind.has(kind)) groupedByKind.set(kind, []); groupedByKind.get(kind)!.push({ label: name, href: `/docs/api/${kind}/${name}`, }); } // Sort items within each group alphabetically for (const items of groupedByKind.values()) { items.sort((a, b) => a.label.localeCompare(b.label)); } // Build ordered groups for (const kind of kindOrder) { const items = groupedByKind.get(kind); if (items && items.length > 0) { apiGroups.push({ title: kindLabels[kind] || kind, items }); } } const isApiPage = currentPath.startsWith('/docs/api'); const normalizedPath = currentPath.replace(/\/$/, ''); export { sidebar }; export type { SidebarItem, SidebarSection }; --- {/* ── Desktop sidebar ── */} {/* ── Mobile sidebar drawer ── */} ================================================ FILE: website/src/components/Sponsors.astro ================================================ --- import { Image } from 'astro:assets'; import { Heart } from '@lucide/astro'; import sponsorsData from '../../sponsors.json'; interface Sponsor { name: string; image: string; url: string; } const headliners = sponsorsData.headliner as Sponsor[]; ---

    Trusted by the best

    Supported and Backed by

    ================================================ FILE: website/src/components/TableOfContents.astro ================================================ --- interface Props { headings: Array<{ depth: number; slug: string; text: string; }>; } const { headings } = Astro.props; // Filter to only h2 and h3 headings const toc = headings.filter((h) => h.depth >= 2 && h.depth <= 3); --- {toc.length > 0 && ( )} ================================================ FILE: website/src/components/Testimonials.astro ================================================ --- import { Image } from 'astro:assets'; interface Tweet { id: string; name: string; user: string; github: string; tweet: string; tagline: string; url?: string; } const tweets: Tweet[] = [ { id: '628016191928446977', name: 'Axel Rauschmayer', user: 'rauschma', github: 'rauschma', tweet: "For transforming HTML via Node.js scripts, @mattmueller's cheerio works really well.", tagline: "Author of 'Exploring JavaScript', educator at 2ality.com", }, { id: '1616150822932385792', name: 'Valeri Karpov', user: 'code_barbarian', github: 'vkarpov15', tweet: 'Cheerio is a weird npm module: most devs have never heard of it, but I rarely build an app without it. So much utility for quick and easy HTML transformations.', tagline: 'Creator of Mongoose ODM', }, { id: '1186972238190403587', name: 'Matthew Phillips', user: 'matthewcp', github: 'matthewp', tweet: 'Cheerio is (still) such a useful tool for manipulating HTML. Shout to @MattMueller for saving me an untold amount of time over the years.', tagline: 'Co-Creator of Astro', }, { id: '1403139379757977602', name: 'Mike Pennisi', user: 'JugglinMike', github: 'jugglinmike', tweet: "Thank you @fb55 for tirelessly pushing Cheerio to version 1.0. That library helps so many developers expand their horizons beyond the browser, and you've been making it possible for a decade!", tagline: 'Open source developer at Bocoup', }, { id: '264033999272439809', name: 'Thomas Steiner', user: 'tomayac', github: 'tomayac', tweet: "npm install cheerio. That's the #jQuery DOM API for #nodeJS essentially. Thanks, @MattMueller", tagline: 'Developer Relations Engineer at Google Chrome', url: 'https://tomayac.com/tweets/264033999272439809/', }, { id: '1545481085865320449', name: 'Thomas Boutell', user: 'boutell', github: 'boutell', tweet: 'If you\'re great at jQuery, you\'re going to be really popular on server-side projects that need web scraping or HTML transformation. "npm install cheerio" ahoy!', tagline: 'Co-creator of the PNG format, CEO of ApostropheCMS', }, ]; // Split tweets into 3 columns for masonry-like layout const columns: Tweet[][] = [[], [], []]; tweets.forEach((tweet, i) => { columns[i % 3].push(tweet); }); ---

    Community

    What developers are saying

    { tweets.map((tweet, i) => (
    {`${tweet.name}'s
    {tweet.name}
    {tweet.tagline}

    {tweet.tweet}

    )) }
    ================================================ FILE: website/src/components/live-code.tsx ================================================ import { SandpackCodeEditor, SandpackConsole, SandpackProvider, useSandpack, } from '@codesandbox/sandpack-react'; import { useCallback } from 'react'; interface LiveCodeProps { code: string; } function ResetButton() { const { sandpack } = useSandpack(); const handleReset = useCallback(() => sandpack.resetAllFiles(), [sandpack]); return ( ); } function RunButton() { const { sandpack } = useSandpack(); const handleRun = () => { const { code } = sandpack.files['/index.js']; sandpack.updateFile('/index.js', code, true); }; return ( ); } function Toolbar() { return (
    Live Editor
    ); } export function LiveCode({ code }: LiveCodeProps) { // Wrap user code to run immediately and output via console.log const wrappedCode = `import * as cheerio from 'cheerio'; ${code} `; return (
    ); } ================================================ FILE: website/src/content/blog/2023-02-13-new-website.md ================================================ --- slug: new-website title: New Website, Who Dis? authors: fb55 tags: [website, intro] --- Cheerio has a new website, and it's looking pretty good! This has been in the works for a while, and we're excited to finally share it with you. Let's walk through all that's new. :::note Cheerio's website is still a work-in-progress, and covers Cheerio's next release that isn't available yet. [We would love your help in making this website better!](https://github.com/cheeriojs/cheerio/discussions/3002) ::: ## Guides The Cheerio website now features a set of guides, helping you get started with the project. Even if you've used the project for a while, you might still learn something new! Most of these guides were written with the help of [ChatGPT](https://chat.openai.com/), which made it possible to generate high-quality content in a reasonable amount of time. While contents have been checked for accuracy, we are happy to accept pull requests to improve the guides. Get started with the [intro guide](/docs/intro). ## API Docs API docs, the old focus of the website, have been moved to a subdirectory. They are available at [docs/api](/docs/api). ## Blog We're also starting a blog, where we'll share updates about the project. Feel free to subscribe [to the RSS feed](/blog/rss.xml)! ================================================ FILE: website/src/content/blog/2024-08-07-version-1.md ================================================ --- slug: cheerio-1.0 title: Cheerio 1.0 Released, Batteries Included 🔋 authors: fb55 tags: [release, announcement] --- Cheerio 1.0 is out! After 12 release candidates and just a short seven years after the initial 1.0 release candidate, it is finally time to call Cheerio 1.0 complete. The theme for this release is "batteries included", with common use cases now supported out of the box. So grab a pair of double-As, and read below for what's new, what's changed, and how to upgrade! ## New Website and Documentation Since the last release, we've published a new website and documentation for Cheerio. The new site features detailed guides and API documentation to get the most from Cheerio. Check it out at [cheerio.js.org](https://cheerio.js.org/). ## A new way to load documents Loading documents into Cheerio has been revamped. Cheerio now supports multiple loading methods, each tailored to different use cases: - `load`: The classic method for parsing HTML or XML strings. - `loadBuffer`: Works with binary data, automatically detecting the document encoding. - `stringStream` and `decodeStream`: Parse HTML directly from streams. - `fromURL`: Fetch and parse HTML from a URL in one go. Dive deeper into these methods in the [Loading Documents](/docs/basics/loading) tutorial. ## Simplified Data Extraction The new `extract` method allows you to extract data from an HTML document and store it in an object. To fetch the latest release of Cheerio from GitHub and extract the release date and the release notes from the release page is now as simple as: ```ts import * as cheerio from 'cheerio'; const $ = await cheerio.fromURL( 'https://github.com/cheeriojs/cheerio/releases', ); const data = $.extract({ releases: [ { // First, we select individual release sections. selector: 'section', // Then, we extract the release date, name, and notes from each section. value: { // Selectors are executed within the context of the selected element. name: 'h2', date: { selector: 'relative-time', // The actual release date is stored in the `datetime` attribute. value: 'datetime', }, notes: { selector: '.markdown-body', // We are looking for the HTML content of the element. value: 'innerHTML', }, }, }, ], }); ``` Read more about all of the available options in the [Extracting Data](/docs/advanced/extract) guide. ## Breaking Changes and Upgrade Guide Cheerio 1.0 introduces several breaking changes, most notably: - The minimum NodeJS version is now 18.17 or higher. - Import paths were simplified. For example, use `cheerio/slim` instead of `cheerio/lib/slim`. - The deprecated default Cheerio instance and static methods were removed. Before, it was possible to write code like this: ```ts import cheerio, { html } from 'cheerio'; html(cheerio('')); // ~ '' -- NO LONGER WORKS ``` Make sure to always load documents first: ```ts import * as cheerio from 'cheerio'; cheerio.load('').html(); ``` - htmlparser2 options now reside exclusively under the `xml` key: ```ts const $ = cheerio.load('', { xml: { withStartIndices: true, }, }); ``` - Node types previously re-exported by Cheerio must now be imported directly from [`domhandler`](https://github.com/fb55/domhandler). For a comprehensive list of changes, please consult [the changelog](https://github.com/cheeriojs/cheerio/releases). ## Upgrading to Cheerio 1.0 To upgrade to Cheerio 1.0, just run: ```bash npm2yarn npm install cheerio@latest ``` ## Get Involved Explore the new features and let us know what you think! Encounter an issue? Report it on our [GitHub issue tracker](https://github.com/cheeriojs/cheerio/issues). Have an idea for an improvement? Pull requests welcome :) ## Thank You Thanks to [@jugglinmike](https://github.com/jugglinmike) for kick-starting Cheerio 1.0, and to all the contributors who have helped shape this release. We couldn't have done it without you. Thanks to our [sponsors and backers](https://github.com/cheeriojs/cheerio?sponsor) for supporting Cheerio's development. If you use Cheerio at work, consider asking your company to support us! And finally, thank you for using Cheerio 🙇🙇‍♀️ ================================================ FILE: website/src/content/config.ts ================================================ import { defineCollection, z } from 'astro:content'; const docs = defineCollection({ type: 'content', schema: z.object({ title: z.string().optional(), description: z.string().optional(), sidebar_position: z.number().optional(), sidebar_label: z.string().optional(), }), }); const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), slug: z.string().optional(), authors: z.union([z.string(), z.array(z.string())]).optional(), tags: z.array(z.string()).optional(), date: z.date().optional(), }), }); export const collections = { docs, blog }; ================================================ FILE: website/src/content/docs/advanced/configuring-cheerio.md ================================================ --- sidebar_position: 2 description: Configure Cheerio to work with different documents. --- # Configuring Cheerio In this guide, we'll cover how to configure Cheerio to work with different types of documents, and how to use and configure the different parsers that ship with the library. ## Parsing HTML with parse5 By default, Cheerio uses the [`parse5`](https://parse5.js.org/) parser for HTML documents. `parse5` is an excellent project that rigorously conforms to the HTML standard. However, if you need to modify parsing options for HTML input, you may pass an extra object to `.load()`: ```js const cheerio = require('cheerio'); const $ = cheerio.load('', { scriptingEnabled: false, }); ``` For example, if you want the contents of `