├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── mocha-setup.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── get-path.ts └── index.ts ├── test ├── get-path.ts └── index.ts ├── tsconfig.decl.json └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | [ 5 | "@babel/env", 6 | { 7 | "modules": false 8 | } 9 | ] 10 | ], 11 | "env": { 12 | "test": { 13 | "presets": ["@babel/preset-typescript", "@babel/env"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.travis.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@aduth/eslint-config", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "no-redeclare": "off", 10 | "@typescript-eslint/no-redeclare": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: "16.x" 13 | - run: npm ci 14 | - run: npm test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | es/ 2 | node_modules/ 3 | *.log 4 | dist/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true, "useTabs": true, "printWidth": 100 } 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.0 (2023-03-25) 2 | 3 | - Improvement: Add TypeScript type definitions ([#18](https://github.com/aduth/hpq/pull/18), thanks @johnhooks!) 4 | 5 | ## 1.3.0 (2018-10-31) 6 | 7 | - Improvement: Improves performance of markup string parsing by ~4x. 8 | 9 | ## 1.2.0 (2017-04-06) 10 | 11 | - Add: Support for deep path lookup on `prop` and `attr` (example: `prop( 'p', 'style.textAlign' )`) 12 | 13 | ## 1.1.1 (2017-03-24) 14 | 15 | - Fix: `package.module` is now transpiled except ES2015 modules 16 | 17 | ## 1.1.0 (2017-03-23) 18 | 19 | - New: Optional selector argument to attr, prop can be omitted. Optional initial arguments are an awkward syntax supported here for convenience's sake. This feature may be removed, or arguments reversed, in future major versions. 20 | - General: Include minified distributable 21 | - General: Added unit tests 22 | 23 | ## 1.0.1 (2017-03-23) 24 | 25 | - Fix: Broken "main" `package.json` field 26 | - General: Include repository details in `package.json` 27 | 28 | ## 1.0.0 (2017-03-23) 29 | 30 | - Initial release 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hpq 2 | 3 | A utility to parse and query HTML into an object shape. Heavily inspired by [gdom](https://github.com/syrusakbary/gdom). 4 | 5 | [Try the Explorer Demo](https://aduth.github.io/hpq/) 6 | 7 | [![Build Status](https://travis-ci.org/aduth/hpq.svg?branch=master)](https://travis-ci.org/aduth/hpq) 8 | 9 | ## Example 10 | 11 | ```js 12 | hpq.parse( '
Image
An Image
', { 13 | src: hpq.attr( 'img', 'src' ), 14 | alt: hpq.attr( 'img', 'alt' ), 15 | caption: hpq.text( 'figcaption' ) 16 | } ); 17 | 18 | // { src: "img.png", alt: "Image", caption: "An Image" } 19 | 20 | hpq.parse( '

...

...

Andrew
', { 21 | text: hpq.query( 'p', hpq.text() ), 22 | cite: hpq.text( 'cite' ) 23 | } ); 24 | 25 | // { text: [ "...", "..." ], cite: "Andrew" } 26 | ``` 27 | 28 | ## Getting Started 29 | 30 | [Download the generated script file](https://unpkg.com/hpq/dist/hpq.min.js) or install via NPM if you have a front-end build process: 31 | 32 | ``` 33 | npm install hpq 34 | ``` 35 | 36 | `hpq` assumes that it's being run in a browser environment. If you need to simulate this in Node, consider [jsdom](https://www.npmjs.com/package/jsdom). 37 | 38 | ## Usage 39 | 40 | Pass a markup string or DOM element to the top-level `parse` function, along with the object shape you'd like to match. Keys of the matcher object will align with the returned object shape, where values are matcher functions; one of the many included matchers or your own accepting the node under test. 41 | 42 | ## API 43 | 44 | `parse( source: string | Element, matchers: Object | Function ): Object | mixed` 45 | 46 | Given a markup string or DOM element, creates an object aligning with the shape of the `matchers` object, or the value returned by the matcher function. If `matchers` is an object, its keys are the desired parameter names and its values are matcher functions that will each extract the value of their desired parameter. 47 | 48 | Matcher functions accept a single parameter `node` (a DOM element) and return the requested value. 49 | 50 | `attr( selector: ?string, name: string ): Function` 51 | 52 | Generates a function which matches node of type selector, returning an attribute by name if the attribute exists. If no selector is passed, returns attribute of the query element. 53 | 54 | `prop( selector: ?string, name: string ): Function` 55 | 56 | Generates a function which matches node of type selector, returning an attribute by property if the attribute exists. If no selector is passed, returns property of the query element. 57 | 58 | `html( selector: ?string ): Function` 59 | 60 | Convenience for `prop( selector, 'innerHTML' )` . 61 | 62 | `text( selector: ?string ): Function` 63 | 64 | Convenience for `prop( selector, 'textContent' )` . 65 | 66 | `query( selector: string, matchers: Object | Function )` 67 | 68 | Creates a new matching context by first finding elements matching selector using [`querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) before then running another `parse` on `matchers` scoped to the matched elements. 69 | 70 | ## License 71 | 72 | Copyright (c) 2018 Andrew Duthie 73 | 74 | [The MIT License (MIT)](https://opensource.org/licenses/MIT) 75 | 76 | 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: 77 | 78 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 79 | 80 | 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. 81 | -------------------------------------------------------------------------------- /mocha-setup.js: -------------------------------------------------------------------------------- 1 | import register from '@babel/register'; 2 | 3 | register({ extensions: ['.ts'] }); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hpq", 3 | "version": "1.4.0", 4 | "description": "Utility to parse and query HTML into an object shape", 5 | "homepage": "https://github.com/aduth/hpq", 6 | "bugs": { 7 | "url": "https://github.com/aduth/hpq/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/aduth/hpq.git" 12 | }, 13 | "main": "dist/hpq.js", 14 | "module": "es/index.js", 15 | "types": "es/index.d.ts", 16 | "files": [ 17 | "dist", 18 | "es", 19 | "src" 20 | ], 21 | "scripts": { 22 | "build:es": "babel src/ --extensions '.ts' --out-dir es", 23 | "build:umd": "rollup -c", 24 | "build:types": "tsc -b tsconfig.decl.json", 25 | "build": "npm run build:es && npm run build:umd && npm run build:types", 26 | "dev": "rollup -c -w", 27 | "lint": "eslint . --ignore-pattern dist --ignore-pattern es", 28 | "unit-test": "NODE_ENV=test mocha -r jsdom-global/register -r @babel/register -r ./mocha-setup.js --extension ts", 29 | "typecheck": "tsc", 30 | "test": "npm run unit-test && npm run lint && npm run typecheck", 31 | "prepublishOnly": "npm run build" 32 | }, 33 | "author": { 34 | "name": "Andrew Duthie", 35 | "email": "andrew@andrewduthie.com", 36 | "url": "http://andrewduthie.com" 37 | }, 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@aduth/eslint-config": "^4.4.1", 41 | "@babel/cli": "^7.21.0", 42 | "@babel/core": "^7.21.3", 43 | "@babel/preset-env": "^7.20.2", 44 | "@babel/preset-typescript": "^7.21.0", 45 | "@babel/register": "^7.21.0", 46 | "@rollup/plugin-babel": "^6.0.3", 47 | "@rollup/plugin-node-resolve": "^15.0.1", 48 | "@rollup/plugin-terser": "^0.4.0", 49 | "@types/chai": "^4.3.4", 50 | "@types/mocha": "^10.0.1", 51 | "@typescript-eslint/eslint-plugin": "^5.56.0", 52 | "@typescript-eslint/parser": "^5.56.0", 53 | "chai": "^4.3.7", 54 | "eslint": "^8.36.0", 55 | "eslint-config-prettier": "^8.8.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "jsdom": "^21.1.1", 58 | "jsdom-global": "^3.0.2", 59 | "mocha": "^10.2.0", 60 | "prettier": "^2.8.7", 61 | "rollup": "^3.20.2", 62 | "typescript": "^5.0.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import terser from '@rollup/plugin-terser'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | output: { 9 | format: 'umd', 10 | name: 'hpq', 11 | file: 'dist/hpq.js', 12 | }, 13 | plugins: [ 14 | nodeResolve({ 15 | extensions: ['.ts'], 16 | }), 17 | babel({ 18 | extensions: ['.ts'], 19 | babelHelpers: 'bundled', 20 | exclude: 'node_modules/**', 21 | presets: ['@babel/preset-typescript'], 22 | }), 23 | ], 24 | }, 25 | { 26 | input: 'src/index.ts', 27 | output: { 28 | format: 'umd', 29 | name: 'hpq', 30 | file: 'dist/hpq.min.js', 31 | }, 32 | plugins: [ 33 | nodeResolve({ 34 | extensions: ['.ts'], 35 | }), 36 | babel({ 37 | extensions: ['.ts'], 38 | babelHelpers: 'bundled', 39 | exclude: 'node_modules/**', 40 | presets: ['@babel/preset-typescript'], 41 | }), 42 | terser(), 43 | ], 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/get-path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Given object and string of dot-delimited path segments, returns value at 3 | * path or undefined if path cannot be resolved. 4 | * 5 | * @param object Lookup object 6 | * @param path Path to resolve 7 | * @return Resolved value 8 | */ 9 | export default function getPath(object: Record, path: string): any | undefined { 10 | const segments = path.split('.'); 11 | 12 | let segment; 13 | while ((segment = segments.shift())) { 14 | if (!(segment in object)) { 15 | return; 16 | } 17 | 18 | object = object[segment]; 19 | } 20 | 21 | return object; 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import getPath from './get-path'; 5 | 6 | export type MatcherFn = (node: Element) => T | undefined; 7 | 8 | export type MatcherObj = { [key: string]: MatcherObj | MatcherFn }; 9 | 10 | export type MatcherObjResult = { 11 | [K in keyof O]: O[K] extends F 12 | ? ReturnType 13 | : O[K] extends MatcherObj 14 | ? MatcherObjResult 15 | : never; 16 | }; 17 | 18 | /** 19 | * Function returning a DOM document created by `createHTMLDocument`. The same 20 | * document is returned between invocations. 21 | * 22 | * @return DOM document. 23 | */ 24 | const getDocument = (() => { 25 | let doc: Document | undefined; 26 | return () => { 27 | if (!doc) { 28 | doc = document.implementation.createHTMLDocument(''); 29 | } 30 | 31 | return doc; 32 | }; 33 | })(); 34 | 35 | /** 36 | * Given a markup string or DOM element, creates an object aligning with the 37 | * shape of the matchers object, or the value returned by the matcher. 38 | * 39 | * @param source Source content 40 | * @param matchers Matcher function or object of matchers 41 | */ 42 | export function parse(source: string | Element, matchers?: undefined): undefined; 43 | 44 | /** 45 | * Given a markup string or DOM element, creates an object aligning with the 46 | * shape of the matchers object, or the value returned by the matcher. 47 | * 48 | * @param source Source content 49 | * @param matchers Object of matchers 50 | * @return Matched values, shaped by object 51 | */ 52 | export function parse( 53 | source: string | Element, 54 | matchers: O 55 | ): MatcherObjResult; 56 | 57 | /** 58 | * Given a markup string or DOM element, creates an object aligning with the 59 | * shape of the matchers object, or the value returned by the matcher. 60 | * 61 | * @param source Source content 62 | * @param matcher Matcher function 63 | * @return Matched value 64 | */ 65 | export function parse(source: string | Element, matchers: F): ReturnType; 66 | 67 | /** 68 | * Given a markup string or DOM element, creates an object aligning with the 69 | * shape of the matchers object, or the value returned by the matcher. 70 | * 71 | * @param source Source content 72 | * @param matchers Matcher function or object of matchers 73 | */ 74 | export function parse( 75 | source: string | Element, 76 | matchers: O | F 77 | ): MatcherObjResult | ReturnType; 78 | 79 | /** 80 | * Given a markup string or DOM element, creates an object aligning with the 81 | * shape of the matchers object, or the value returned by the matcher. 82 | * 83 | * @param source Source content 84 | * @param matchers Matcher function or object of matchers 85 | */ 86 | export function parse( 87 | source: string | Element, 88 | matchers?: O | F 89 | ) { 90 | if (!matchers) { 91 | return; 92 | } 93 | 94 | // Coerce to element 95 | if ('string' === typeof source) { 96 | const doc = getDocument(); 97 | doc.body.innerHTML = source; 98 | source = doc.body; 99 | } 100 | 101 | // Return singular value 102 | if (typeof matchers === 'function') { 103 | return matchers(source); 104 | } 105 | 106 | // Bail if we can't handle matchers 107 | if (Object !== matchers.constructor) { 108 | return; 109 | } 110 | 111 | // Shape result by matcher object 112 | return Object.keys(matchers).reduce((memo, key: keyof MatcherObjResult) => { 113 | const inner = matchers[key]; 114 | memo[key] = parse(source, inner); 115 | return memo; 116 | }, {} as MatcherObjResult); 117 | } 118 | 119 | /** 120 | * Generates a function which matches node of type selector, returning an 121 | * attribute by property if the attribute exists. If no selector is passed, 122 | * returns property of the query element. 123 | * 124 | * @param name Property name 125 | * @return Property value 126 | */ 127 | export function prop(name: string): MatcherFn; 128 | 129 | /** 130 | * Generates a function which matches node of type selector, returning an 131 | * attribute by property if the attribute exists. If no selector is passed, 132 | * returns property of the query element. 133 | * 134 | * @param selector Optional selector 135 | * @param name Property name 136 | * @return Property value 137 | */ 138 | export function prop( 139 | selector: string | undefined, 140 | name: N 141 | ): MatcherFn; 142 | 143 | /** 144 | * Generates a function which matches node of type selector, returning an 145 | * attribute by property if the attribute exists. If no selector is passed, 146 | * returns property of the query element. 147 | * 148 | * @param selector Optional selector 149 | * @param name Property name 150 | * @return Property value 151 | */ 152 | export function prop( 153 | arg1: string | undefined, 154 | arg2?: string 155 | ): MatcherFn { 156 | let name: string; 157 | let selector: string | undefined; 158 | if (1 === arguments.length) { 159 | name = arg1 as string; 160 | selector = undefined; 161 | } else { 162 | name = arg2 as string; 163 | selector = arg1; 164 | } 165 | return function (node: Element): Element[N] | undefined { 166 | let match: Element | null = node; 167 | if (selector) { 168 | match = node.querySelector(selector); 169 | } 170 | if (match) { 171 | return getPath(match, name); 172 | } 173 | } as MatcherFn; 174 | } 175 | 176 | /** 177 | * Generates a function which matches node of type selector, returning an 178 | * attribute by name if the attribute exists. If no selector is passed, 179 | * returns attribute of the query element. 180 | * 181 | * @param name Attribute name 182 | * @return Attribute value 183 | */ 184 | export function attr(name: string): MatcherFn; 185 | 186 | /** 187 | * Generates a function which matches node of type selector, returning an 188 | * attribute by name if the attribute exists. If no selector is passed, 189 | * returns attribute of the query element. 190 | * 191 | * @param selector Optional selector 192 | * @param name Attribute name 193 | * @return Attribute value 194 | */ 195 | export function attr(selector: string | undefined, name: string): MatcherFn; 196 | 197 | /** 198 | * Generates a function which matches node of type selector, returning an 199 | * attribute by name if the attribute exists. If no selector is passed, 200 | * returns attribute of the query element. 201 | * 202 | * @param selector Optional selector 203 | * @param name Attribute name 204 | * @return Attribute value 205 | */ 206 | export function attr(arg1: string | undefined, arg2?: string): MatcherFn { 207 | let name: string; 208 | let selector: string | undefined; 209 | if (1 === arguments.length) { 210 | name = arg1 as string; 211 | selector = undefined; 212 | } else { 213 | name = arg2 as string; 214 | selector = arg1; 215 | } 216 | return function (node: Element): string | undefined { 217 | const attributes = prop(selector, 'attributes')(node); 218 | if (attributes && Object.prototype.hasOwnProperty.call(attributes, name)) { 219 | return attributes[name as any].value; 220 | } 221 | }; 222 | } 223 | 224 | /** 225 | * Convenience for `prop( selector, 'innerHTML' )`. 226 | * 227 | * @see prop() 228 | * 229 | * @param selector Optional selector 230 | * @return Inner HTML 231 | */ 232 | export function html(selector?: string) { 233 | return prop(selector, 'innerHTML') as MatcherFn; 234 | } 235 | 236 | /** 237 | * Convenience for `prop( selector, 'textContent' )`. 238 | * 239 | * @see prop() 240 | * 241 | * @param selector Optional selector 242 | * @return Text content 243 | */ 244 | export function text(selector?: string) { 245 | return prop(selector, 'textContent') as MatcherFn; 246 | } 247 | 248 | /** 249 | * Creates a new matching context by first finding elements matching selector 250 | * using querySelectorAll before then running another `parse` on `matchers` 251 | * scoped to the matched elements. 252 | * 253 | * @see parse() 254 | * 255 | * @param selector Selector to match 256 | * @param matchers Matcher function or object of matchers 257 | * @return Matcher function which returns an array of matched value(s) 258 | */ 259 | export function query( 260 | selector: string, 261 | matchers?: F | O 262 | ): MatcherFn[]> { 263 | return function (node: Element) { 264 | const matches = node.querySelectorAll(selector); 265 | return [].map.call(matches, (match) => parse(match, matchers!)) as MatcherObjResult[]; 266 | }; 267 | } 268 | -------------------------------------------------------------------------------- /test/get-path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { expect } from 'chai'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import getPath from '../src/get-path'; 10 | 11 | describe('getPath()', () => { 12 | it('should return simple path value', () => { 13 | const value = getPath({ a: 1 }, 'a'); 14 | 15 | expect(value).to.equal(1); 16 | }); 17 | 18 | it('should return deep value', () => { 19 | const value = getPath({ a: { b: 1 } }, 'a.b'); 20 | 21 | expect(value).to.equal(1); 22 | }); 23 | 24 | it('should return undefined on missing simple path value', () => { 25 | const value = getPath({}, 'a'); 26 | 27 | expect(value).to.be.undefined; 28 | }); 29 | 30 | it('should return undefined on missing deep path value', () => { 31 | const value = getPath({}, 'a.b'); 32 | 33 | expect(value).to.be.undefined; 34 | }); 35 | 36 | it('should allow retrieving by prototype', () => { 37 | const value = getPath({}, 'valueOf'); 38 | 39 | expect(value).to.be.a('function'); 40 | }); 41 | 42 | it('should allow deep retrieving by prototype', () => { 43 | const value = getPath({ a: {} }, 'a.valueOf'); 44 | 45 | expect(value).to.be.a('function'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { expect } from 'chai'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { parse, attr, prop, html, text, query } from '../src'; 10 | 11 | describe('hpq', () => { 12 | // Markup 13 | const markup = 14 | '

Andrew
'; 15 | 16 | // Element 17 | let element: HTMLElement; 18 | 19 | before(() => { 20 | element = document.createElement('div'); 21 | element.innerHTML = markup; 22 | element = element.firstChild as HTMLElement; 23 | }); 24 | 25 | describe('parse()', () => { 26 | it('should return undefined if passed no matchers', () => { 27 | const result = parse(markup); 28 | 29 | expect(result).to.be.undefined; 30 | }); 31 | 32 | it('should accept a string of markup as source', () => { 33 | const result = parse(markup, text('cite')); 34 | 35 | expect(result).to.equal('— Andrew'); 36 | }); 37 | 38 | it('should return matcher value if passed function matcher', () => { 39 | const result = parse(element, text('cite')); 40 | 41 | expect(result).to.equal('— Andrew'); 42 | }); 43 | 44 | it('should return undefined if passed matchers other than object, function', () => { 45 | // @ts-ignore 46 | const result = parse(element, 2); 47 | 48 | expect(result).to.be.undefined; 49 | }); 50 | 51 | it('should return parsed matches in shape of matcher object', () => { 52 | const result = parse(element, { 53 | author: text('cite'), 54 | deep: { value: text('cite') }, 55 | }); 56 | 57 | expect(result).to.eql({ 58 | author: '— Andrew', 59 | deep: { value: '— Andrew' }, 60 | }); 61 | }); 62 | }); 63 | 64 | describe('prop()', () => { 65 | it('should return a matcher function', () => { 66 | // @ts-ignore 67 | const matcher = prop(); 68 | 69 | expect(matcher).to.be.a('function'); 70 | }); 71 | 72 | it('should return property of current top node if undefined selector', () => { 73 | const result = parse(element, prop(undefined, 'nodeName')); 74 | 75 | expect(result).to.equal('BLOCKQUOTE'); 76 | }); 77 | 78 | it('should return property of current top node if omitted selector', () => { 79 | const result = parse(element, prop('nodeName')); 80 | 81 | expect(result).to.equal('BLOCKQUOTE'); 82 | }); 83 | 84 | it('should return undefined if selector does not match', () => { 85 | const result = parse(element, prop('strong', 'nodeName')); 86 | 87 | expect(result).to.be.undefined; 88 | }); 89 | 90 | it('should return property of selector match by property', () => { 91 | const result = parse(element, prop('cite', 'nodeName')); 92 | 93 | expect(result).to.equal('CITE'); 94 | }); 95 | }); 96 | 97 | describe('attr()', () => { 98 | it('should return a matcher function', () => { 99 | // @ts-ignore 100 | const matcher = attr(); 101 | 102 | expect(matcher).to.be.a('function'); 103 | }); 104 | 105 | it('should return attribute of current top node if undefined selector', () => { 106 | const result = parse(element, query('cite', attr(undefined, 'class'))); 107 | 108 | expect(result).to.eql(['large']); 109 | }); 110 | 111 | it('should return attribute of current top node if omitted selector', () => { 112 | const result = parse(element, query('cite', attr('class'))); 113 | 114 | expect(result).to.eql(['large']); 115 | }); 116 | 117 | it('should return undefined if selector does not match', () => { 118 | const result = parse(element, attr('strong', 'class')); 119 | 120 | expect(result).to.be.undefined; 121 | }); 122 | 123 | it('should return undefined if match does not have attribute', () => { 124 | const result = parse(element, attr('cite', 'data-unknown')); 125 | 126 | expect(result).to.be.undefined; 127 | }); 128 | 129 | it('should return attribute value of match', () => { 130 | const result = parse(element, attr('cite', 'class')); 131 | 132 | expect(result).to.equal('large'); 133 | }); 134 | }); 135 | 136 | describe('html()', () => { 137 | it('should return a matcher function', () => { 138 | const matcher = html(); 139 | 140 | expect(matcher).to.be.a('function'); 141 | }); 142 | 143 | it('should return inner HTML of top node if no selector', () => { 144 | const result = parse(element, html()); 145 | 146 | expect(result).to.match(/^

/); 147 | }); 148 | 149 | it('should return inner HTML of selector match', () => { 150 | const result = parse(element, html('cite')); 151 | 152 | expect(result).to.equal(' Andrew'); 153 | }); 154 | }); 155 | 156 | describe('text()', () => { 157 | it('should return a matcher function', () => { 158 | const matcher = text(); 159 | 160 | expect(matcher).to.be.a('function'); 161 | }); 162 | 163 | it('should return text content of top node if no selector', () => { 164 | const result = parse(element, text()); 165 | 166 | expect(result).to.match(/^…/); 167 | }); 168 | 169 | it('should return text content of selector match', () => { 170 | const result = parse(element, text('cite')); 171 | 172 | expect(result).to.equal('— Andrew'); 173 | }); 174 | }); 175 | 176 | describe('query()', () => { 177 | it('should return array of parse on matched nodes', () => { 178 | const result = parse(element, { text: query('p', text()) }); 179 | 180 | expect(result).to.eql({ 181 | text: ['…', '…'], 182 | }); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /tsconfig.decl.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./es", 7 | "rootDir": "src", 8 | "noEmit": false 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["ES2017", "DOM", "DOM.Iterable"], 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noEmit": true 8 | }, 9 | "include": ["src", "test"] 10 | } 11 | --------------------------------------------------------------------------------