├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Handler} Handler 3 | * @typedef {import('./lib/index.js').Options} Options 4 | */ 5 | 6 | export {search} from './lib/index.js' 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('nlcst').Nodes} Nodes 3 | * @typedef {import('nlcst').Root} Root 4 | * @typedef {import('nlcst').Sentence} Sentence 5 | * @typedef {import('nlcst').SentenceContent} SentenceContent 6 | * @typedef {import('nlcst-normalize').Options} NormalizeOptions 7 | */ 8 | 9 | /** 10 | * @callback Handler 11 | * Handle a match. 12 | * @param {Array} nodes 13 | * Match. 14 | * @param {number} index 15 | * Index of first node of `nodes` in `parent`. 16 | * @param {Root | Sentence} parent 17 | * Parent of `nodes`. 18 | * @param {string} phrase 19 | * The phrase that matched. 20 | * @returns {undefined | void} 21 | * Nothing. 22 | * 23 | * @typedef {NormalizeOptions & OptionsExtraFields} Options 24 | * Configuration (optional). 25 | * 26 | * @typedef OptionsExtraFields 27 | * Extra fields. 28 | * @property {boolean | null | undefined} [allowLiterals=false] 29 | * Include literal phrases (default: `false`). 30 | */ 31 | 32 | import {visit} from 'unist-util-visit' 33 | import {normalize} from 'nlcst-normalize' 34 | import {isLiteral} from 'nlcst-is-literal' 35 | 36 | const own = {}.hasOwnProperty 37 | 38 | /** 39 | * Search for phrases in a tree. 40 | * 41 | * Each phrase is a space-separated list of words, where each word will be 42 | * normalized to remove casing, apostrophes, and dashes. 43 | * Spaces in a pattern mean one or more whitespace nodes in the tree. 44 | * Instead of a word with letters, it’s also possible to use a wildcard 45 | * symbol (`*`, an asterisk) which will match any word in a pattern 46 | * (`alpha * charlie`). 47 | * 48 | * @param {Nodes} tree 49 | * Tree to search. 50 | * @param {Array} phrases 51 | * Phrases to search for. 52 | * @param {Handler} handler 53 | * Handle a match 54 | * @param {Options} [options] 55 | * Configuration (optional). 56 | * @returns {undefined} 57 | * Nothing. 58 | */ 59 | export function search(tree, phrases, handler, options) { 60 | const config = options || {} 61 | 62 | if (!tree || !tree.type) { 63 | throw new Error('Expected node') 64 | } 65 | 66 | if (typeof phrases !== 'object') { 67 | throw new TypeError('Expected object for phrases') 68 | } 69 | 70 | /** @type {Record>} */ 71 | const byWord = {'*': []} 72 | 73 | let index = -1 74 | 75 | while (++index < phrases.length) { 76 | const phrase = phrases[index] 77 | const firstWord = normalize(phrase.split(' ', 1)[0], config) 78 | 79 | if (own.call(byWord, firstWord)) { 80 | byWord[firstWord].push(phrase) 81 | } else { 82 | byWord[firstWord] = [phrase] 83 | } 84 | } 85 | 86 | // Search the tree. 87 | visit(tree, 'WordNode', (node, position, parent) => { 88 | if ( 89 | !parent || 90 | position === undefined || 91 | (!config.allowLiterals && isLiteral(parent, position)) 92 | ) { 93 | return 94 | } 95 | 96 | const word = normalize(node, config) 97 | const phrases = own.call(byWord, word) 98 | ? [...byWord['*'], ...byWord[word]] 99 | : byWord['*'] 100 | let index = -1 101 | 102 | while (++index < phrases.length) { 103 | const result = test(phrases[index], position, parent) 104 | 105 | if (result) { 106 | handler(result, position, parent, phrases[index]) 107 | } 108 | } 109 | }) 110 | 111 | /** 112 | * Test a phrase (the first word already matched). 113 | * 114 | * @param {string} phrase 115 | * Normalized phrase. 116 | * @param {number} position 117 | * Index in `parent`. 118 | * @param {Root | Sentence} parent 119 | * Parent node. 120 | * @returns {Array | undefined} 121 | * Match, if found. 122 | */ 123 | function test(phrase, position, parent) { 124 | /** @type {Array} */ 125 | // @ts-expect-error: content in a root must be of the same content type. 126 | const siblings = parent.children 127 | const start = position 128 | const expressions = phrase.split(' ').slice(1) 129 | let index = -1 130 | 131 | // Move one position forward. 132 | position++ 133 | 134 | // Iterate over `expressions`. 135 | while (++index < expressions.length) { 136 | // Allow joining white-space. 137 | while (position < siblings.length) { 138 | if (siblings[position].type !== 'WhiteSpaceNode') break 139 | position++ 140 | } 141 | 142 | // Exit if there are no nodes left, if the current node is not a word, or 143 | // if the current word does not match the search for value. 144 | if ( 145 | !siblings[position] || 146 | siblings[position].type !== 'WordNode' || 147 | (expressions[index] !== '*' && 148 | normalize(expressions[index], config) !== 149 | normalize(siblings[position], config)) 150 | ) { 151 | return 152 | } 153 | 154 | position++ 155 | } 156 | 157 | return siblings.slice(start, position) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nlcst-search", 3 | "version": "4.0.0", 4 | "description": "nlcst utility to search for patterns in a tree", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "nlcst", 9 | "nlcst-util", 10 | "util", 11 | "utility", 12 | "pattern", 13 | "search" 14 | ], 15 | "repository": "syntax-tree/nlcst-search", 16 | "bugs": "https://github.com/syntax-tree/nlcst-search/issues", 17 | "funding": { 18 | "type": "opencollective", 19 | "url": "https://opencollective.com/unified" 20 | }, 21 | "author": "Titus Wormer (https://wooorm.com)", 22 | "contributors": [ 23 | "Titus Wormer (https://wooorm.com)", 24 | "Dave Newton ", 25 | "Tom MacWright " 26 | ], 27 | "sideEffects": false, 28 | "type": "module", 29 | "exports": "./index.js", 30 | "files": [ 31 | "lib/", 32 | "index.d.ts", 33 | "index.js" 34 | ], 35 | "dependencies": { 36 | "@types/nlcst": "^2.0.0", 37 | "nlcst-is-literal": "^3.0.0", 38 | "nlcst-normalize": "^4.0.0", 39 | "unist-util-visit": "^5.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^20.0.0", 43 | "c8": "^8.0.0", 44 | "prettier": "^3.0.0", 45 | "remark-cli": "^11.0.0", 46 | "remark-preset-wooorm": "^9.0.0", 47 | "type-coverage": "^2.0.0", 48 | "typescript": "^5.0.0", 49 | "xo": "^0.55.0" 50 | }, 51 | "scripts": { 52 | "prepack": "npm run build && npm run format", 53 | "build": "tsc --build --clean && tsc --build && type-coverage", 54 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 55 | "test-api": "node --conditions development test.js", 56 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 57 | "test": "npm run build && npm run format && npm run test-coverage" 58 | }, 59 | "prettier": { 60 | "bracketSpacing": false, 61 | "semi": false, 62 | "singleQuote": true, 63 | "tabWidth": 2, 64 | "trailingComma": "none", 65 | "useTabs": false 66 | }, 67 | "remarkConfig": { 68 | "plugins": [ 69 | "remark-preset-wooorm" 70 | ] 71 | }, 72 | "typeCoverage": { 73 | "atLeast": 100, 74 | "detail": true, 75 | "ignoreCatch": true, 76 | "strict": true 77 | }, 78 | "xo": { 79 | "prettier": true 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # nlcst-search 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [nlcst][] utility to search for phrases in a tree. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`search(tree, phrases, handler[, options])`](#searchtree-phrases-handler-options) 21 | * [`Handler`](#handler) 22 | * [`Options`](#options) 23 | * [Types](#types) 24 | * [Compatibility](#compatibility) 25 | * [Related](#related) 26 | * [Contribute](#contribute) 27 | * [License](#license) 28 | 29 | ## What is this? 30 | 31 | This utility can search for phrases (words and phrases) in trees. 32 | 33 | ## When should I use this? 34 | 35 | This package is a tiny utility that helps when you’re searching for words 36 | and phrases. 37 | 38 | ## Install 39 | 40 | This package is [ESM only][esm]. 41 | In Node.js (version 16+), install with [npm][]: 42 | 43 | ```sh 44 | npm install nlcst-search 45 | ``` 46 | 47 | In Deno with [`esm.sh`][esmsh]: 48 | 49 | ```js 50 | import {search} from 'https://esm.sh/nlcst-search@4' 51 | ``` 52 | 53 | In browsers with [`esm.sh`][esmsh]: 54 | 55 | ```html 56 | 59 | ``` 60 | 61 | ## Use 62 | 63 | ```js 64 | import {search} from 'nlcst-search' 65 | import {toString} from 'nlcst-to-string' 66 | 67 | const tree = { 68 | type: 'SentenceNode', 69 | children: [ 70 | { 71 | type: 'WordNode', 72 | children: [ 73 | {type: 'TextNode', value: 'Don'}, 74 | {type: 'PunctuationNode', value: '’'}, 75 | {type: 'TextNode', value: 't'} 76 | ] 77 | }, 78 | {type: 'WhiteSpaceNode', value: ' '}, 79 | { 80 | type: 'WordNode', 81 | children: [{type: 'TextNode', value: 'do'}] 82 | }, 83 | {type: 'WhiteSpaceNode', value: ' '}, 84 | { 85 | type: 'WordNode', 86 | children: [ 87 | {type: 'TextNode', value: 'Block'}, 88 | {type: 'PunctuationNode', value: '-'}, 89 | {type: 'TextNode', value: 'level'} 90 | ] 91 | } 92 | ] 93 | } 94 | 95 | search(tree, ['dont'], function(nodes) { 96 | console.log(toString(nodes)) 97 | }) 98 | // `Don’t` 99 | 100 | search(tree, ['do blocklevel'], function(nodes) { 101 | console.log(toString(nodes)) 102 | }) 103 | // `do Block-level` 104 | ``` 105 | 106 | ## API 107 | 108 | This package exports the identifier [`search`][api-search]. 109 | There is no default export. 110 | 111 | ### `search(tree, phrases, handler[, options])` 112 | 113 | Search for phrases in a tree. 114 | 115 | Each phrase is a space-separated list of words, where each word will be 116 | [normalized][nlcst-normalize] to remove casing, apostrophes, and dashes. 117 | Spaces in a pattern mean one or more whitespace nodes in the tree. 118 | Instead of a word with letters, it’s also possible to use a wildcard symbol 119 | (`*`, an asterisk) which will match any word in a pattern (`alpha * charlie`). 120 | 121 | ##### Parameters 122 | 123 | * `tree` ([`Node`][node]) 124 | — tree to search 125 | * `phrases` (`Array`) 126 | — phrases to search for 127 | * `handler` ([`Handler`][api-handler]) 128 | — handle a match 129 | * `options` ([`Options`][api-options]) 130 | — configuration 131 | 132 | ###### Returns 133 | 134 | Nothing (`undefined`). 135 | 136 | ### `Handler` 137 | 138 | Handle a match (TypeScript type). 139 | 140 | ###### Parameters 141 | 142 | * `nodes` ([`Array`][node]) 143 | — match 144 | * `index` (`number`) 145 | — index of first node of `nodes` in `parent` 146 | * `parent` ([`Node`][node]) 147 | — parent of `nodes` 148 | * `phrase` (`string`) 149 | — the phrase that matched 150 | 151 | ###### Returns 152 | 153 | Nothing (`undefined`). 154 | 155 | ### `Options` 156 | 157 | Configuration (TypeScript type). 158 | 159 | ###### Fields 160 | 161 | * `allowApostrophes` (`boolean`, default: `false`) 162 | — passed to [`nlcst-normalize`][nlcst-normalize] 163 | * `allowDashes` (`boolean`, default: `false`) 164 | — passed to [`nlcst-normalize`][nlcst-normalize] 165 | * `allowLiterals` (`boolean`, default: `false`) 166 | — include [literal][] phrases 167 | 168 | ## Types 169 | 170 | This package is fully typed with [TypeScript][]. 171 | It exports the additional types [`Handler`][api-handler] and 172 | [`Options`][api-options]. 173 | 174 | ## Compatibility 175 | 176 | Projects maintained by the unified collective are compatible with maintained 177 | versions of Node.js. 178 | 179 | When we cut a new major release, we drop support for unmaintained versions of 180 | Node. 181 | This means we try to keep the current release line, `nlcst-search@^4`, 182 | compatible with Node.js 16. 183 | 184 | ## Related 185 | 186 | * [`nlcst-normalize`](https://github.com/syntax-tree/nlcst-normalize) 187 | — normalize a word for easier comparison 188 | * [`nlcst-is-literal`](https://github.com/syntax-tree/nlcst-is-literal) 189 | — check whether a node is meant literally 190 | 191 | ## Contribute 192 | 193 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 194 | ways to get started. 195 | See [`support.md`][support] for ways to get help. 196 | 197 | This project has a [code of conduct][coc]. 198 | By interacting with this repository, organization, or community you agree to 199 | abide by its terms. 200 | 201 | ## License 202 | 203 | [MIT][license] © [Titus Wormer][author] 204 | 205 | 206 | 207 | [build-badge]: https://github.com/syntax-tree/nlcst-search/workflows/main/badge.svg 208 | 209 | [build]: https://github.com/syntax-tree/nlcst-search/actions 210 | 211 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/nlcst-search.svg 212 | 213 | [coverage]: https://codecov.io/github/syntax-tree/nlcst-search 214 | 215 | [downloads-badge]: https://img.shields.io/npm/dm/nlcst-search.svg 216 | 217 | [downloads]: https://www.npmjs.com/package/nlcst-search 218 | 219 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=nlcst-search 220 | 221 | [size]: https://bundlejs.com/?q=nlcst-search 222 | 223 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 224 | 225 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 226 | 227 | [collective]: https://opencollective.com/unified 228 | 229 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 230 | 231 | [chat]: https://github.com/syntax-tree/unist/discussions 232 | 233 | [npm]: https://docs.npmjs.com/cli/install 234 | 235 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 236 | 237 | [esmsh]: https://esm.sh 238 | 239 | [typescript]: https://www.typescriptlang.org 240 | 241 | [license]: license 242 | 243 | [author]: https://wooorm.com 244 | 245 | [health]: https://github.com/syntax-tree/.github 246 | 247 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 248 | 249 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 250 | 251 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 252 | 253 | [nlcst]: https://github.com/syntax-tree/nlcst 254 | 255 | [node]: https://github.com/syntax-tree/unist#node 256 | 257 | [literal]: https://github.com/syntax-tree/nlcst-is-literal 258 | 259 | [nlcst-normalize]: https://github.com/syntax-tree/nlcst-normalize 260 | 261 | [api-search]: #searchtree-phrases-handler-options 262 | 263 | [api-handler]: #handler 264 | 265 | [api-options]: #options 266 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('nlcst').Sentence} Sentence 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import {search} from 'nlcst-search' 8 | 9 | test('search', async function (t) { 10 | /** @type {Sentence} */ 11 | const tree = { 12 | type: 'SentenceNode', 13 | children: [ 14 | { 15 | type: 'WordNode', 16 | children: [ 17 | {type: 'TextNode', value: 'Don'}, 18 | {type: 'PunctuationNode', value: '’'}, 19 | {type: 'TextNode', value: 't'} 20 | ] 21 | }, 22 | {type: 'WhiteSpaceNode', value: ' '}, 23 | { 24 | type: 'WordNode', 25 | children: [{type: 'TextNode', value: 'do'}] 26 | }, 27 | {type: 'WhiteSpaceNode', value: ' '}, 28 | { 29 | type: 'WordNode', 30 | children: [ 31 | {type: 'TextNode', value: 'Block'}, 32 | {type: 'PunctuationNode', value: '-'}, 33 | {type: 'TextNode', value: 'level'} 34 | ] 35 | }, 36 | {type: 'WhiteSpaceNode', value: ' '}, 37 | { 38 | type: 'WordNode', 39 | children: [{type: 'TextNode', value: 'or'}] 40 | }, 41 | {type: 'WhiteSpaceNode', value: ' '}, 42 | {type: 'PunctuationNode', value: '"'}, 43 | { 44 | type: 'WordNode', 45 | children: [{type: 'TextNode', value: 'do'}] 46 | }, 47 | {type: 'PunctuationNode', value: '"'}, 48 | { 49 | type: 'WordNode', 50 | children: [{type: 'TextNode', value: 'or'}] 51 | }, 52 | {type: 'PunctuationNode', value: ','}, 53 | {type: 'WhiteSpaceNode', value: ' '}, 54 | { 55 | type: 'WordNode', 56 | children: [{type: 'TextNode', value: 'mellow'}] 57 | }, 58 | {type: 'PunctuationNode', value: ','}, 59 | {type: 'WhiteSpaceNode', value: ' '}, 60 | { 61 | type: 'WordNode', 62 | children: [{type: 'TextNode', value: 'that'}] 63 | }, 64 | {type: 'WhiteSpaceNode', value: ' '}, 65 | { 66 | type: 'WordNode', 67 | children: [{type: 'TextNode', value: 'or'}] 68 | }, 69 | {type: 'WhiteSpaceNode', value: ' '}, 70 | { 71 | type: 'WordNode', 72 | children: [{type: 'TextNode', value: 'this'}] 73 | }, 74 | {type: 'WhiteSpaceNode', value: ' '}, 75 | { 76 | type: 'WordNode', 77 | children: [{type: 'TextNode', value: 'hell'}] 78 | }, 79 | {type: 'WhiteSpaceNode', value: ' '}, 80 | { 81 | type: 'WordNode', 82 | children: [{type: 'TextNode', value: 'selfservice'}] 83 | } 84 | ] 85 | } 86 | 87 | await t.test('should expose the public api', async function () { 88 | assert.deepEqual(Object.keys(await import('nlcst-search')).sort(), [ 89 | 'search' 90 | ]) 91 | }) 92 | 93 | await t.test('should throw when not given a tree', async function () { 94 | assert.throws(function () { 95 | // @ts-expect-error: check how the runtime handles a missing tree. 96 | search() 97 | }, /Error: Expected node/) 98 | }) 99 | 100 | await t.test('should throw when not given phrases', async function () { 101 | assert.throws(function () { 102 | // @ts-expect-error: check how the runtime handles a missing phrases. 103 | search(tree) 104 | }, /Error: Expected object for phrases/) 105 | }) 106 | 107 | await t.test('should search', async function () { 108 | search(tree, ['Don’t'], function (nodes, index, parent, phrase) { 109 | assert.deepEqual(nodes, [tree.children[0]], 'should pass nodes') 110 | assert.equal(index, 0, 'should pass the correct index') 111 | assert.equal(parent, tree, 'should pass the parent') 112 | assert.equal(phrase, 'Don’t', 'should pass the phrase') 113 | }) 114 | }) 115 | 116 | await t.test('should search (normalized)', async function () { 117 | search(tree, ['Dont'], function (nodes, index, parent, phrase) { 118 | const match = [tree.children[0]] 119 | assert.deepEqual(nodes, match, 'should pass nodes (normalized)') 120 | assert.equal(index, 0, 'should pass the correct index (normalized)') 121 | assert.equal(parent, tree, 'should pass the parent (normalized)') 122 | assert.equal(phrase, 'Dont', 'should pass the phrase') 123 | }) 124 | }) 125 | 126 | await t.test('should search (normalized, 2)', async function () { 127 | search(tree, ['blocklevel'], function (nodes, index, parent, phrase) { 128 | const match = [tree.children[4]] 129 | assert.deepEqual(nodes, match, 'should pass nodes (normalized 2)') 130 | assert.equal(index, 4, 'should pass the correct index (normalized 2)') 131 | assert.equal(parent, tree, 'should pass the parent (normalized 2)') 132 | assert.equal(phrase, 'blocklevel', 'should pass the phrase') 133 | }) 134 | }) 135 | 136 | await t.test('should search (list)', async function () { 137 | const results = [ 138 | [[tree.children[0]], 0, tree, 'dont'], 139 | [[tree.children[2]], 2, tree, 'do'] 140 | ] 141 | let position = -1 142 | 143 | search(tree, ['dont', 'do'], function (nodes, index, parent, phrase) { 144 | const match = results[++position] 145 | assert.deepEqual(nodes, match[0], 'should pass nodes (phrases)') 146 | assert.equal(index, match[1], 'should pass the correct index (phrases)') 147 | assert.equal(parent, match[2], 'should pass the parent (phrases)') 148 | assert.equal(phrase, match[3], 'should pass the phrase (phrases)') 149 | }) 150 | }) 151 | 152 | await t.test('should search (phrase)', async function () { 153 | search(tree, ['dont do'], function (nodes, index, parent, phrase) { 154 | const match = tree.children.slice(0, 3) 155 | assert.deepEqual(nodes, match, 'should pass nodes (phrase)') 156 | assert.equal(index, 0, 'should pass the correct index (phrase)') 157 | assert.equal(parent, tree, 'should pass the parent (phrase)') 158 | assert.equal(phrase, 'dont do', 'should pass the phrase (phrase)') 159 | }) 160 | }) 161 | 162 | await t.test( 163 | 'should not include non-word and non-white-space nodes', 164 | async function () { 165 | assert.doesNotThrow(function () { 166 | search(tree, ['or that'], throws) 167 | }) 168 | } 169 | ) 170 | 171 | await t.test('should search (phrases)', async function () { 172 | const phrases = ['that or this', 'that'] 173 | 174 | const results = [ 175 | [tree.children.slice(17, 22), 17, tree, phrases[0]], 176 | [[tree.children[17]], 17, tree, phrases[1]] 177 | ] 178 | let position = -1 179 | 180 | search(tree, phrases, function (nodes, index, parent, phrase) { 181 | const match = results[++position] 182 | assert.deepEqual(nodes, match[0], 'should pass nodes (phrases)') 183 | assert.equal(index, match[1], 'should pass the correct index (phrases)') 184 | assert.equal(parent, match[2], 'should pass the parent (phrases)') 185 | assert.equal(phrase, match[3], 'should pass the phrase (phrases)') 186 | }) 187 | }) 188 | 189 | await t.test( 190 | 'should find non-apostrophe words when `allowApostrophes` is absent', 191 | async function () { 192 | assert.throws(function () { 193 | search(tree, ['hell'], throws) 194 | }) 195 | } 196 | ) 197 | 198 | await t.test( 199 | 'should find smart apostrophe words when `allowApostrophes` is absent', 200 | async function () { 201 | assert.throws(function () { 202 | search(tree, ['he’ll'], throws) 203 | }) 204 | } 205 | ) 206 | 207 | await t.test( 208 | 'should find dumb apostrophe words when `allowApostrophes` is absent', 209 | async function () { 210 | assert.throws(function () { 211 | search(tree, ["he'll"], throws) 212 | }) 213 | } 214 | ) 215 | 216 | await t.test( 217 | 'should find non-apostrophe words when `allowApostrophes` is true', 218 | async function () { 219 | assert.throws(function () { 220 | search(tree, ['hell'], throws, {allowApostrophes: true}) 221 | }) 222 | } 223 | ) 224 | 225 | await t.test( 226 | 'should not find smart apostrophe words when `allowApostrophes` is true', 227 | async function () { 228 | assert.doesNotThrow(function () { 229 | search(tree, ['he’ll'], throws, {allowApostrophes: true}) 230 | }) 231 | } 232 | ) 233 | 234 | await t.test( 235 | 'should not find dumb apostrophe words when `allowApostrophes` is true', 236 | async function () { 237 | assert.doesNotThrow(function () { 238 | search(tree, ["he'll"], throws, {allowApostrophes: true}) 239 | }) 240 | } 241 | ) 242 | 243 | await t.test( 244 | 'should find non-apostrophe words when `allowApostrophes` is false', 245 | async function () { 246 | assert.throws(function () { 247 | search(tree, ['hell'], throws, {allowApostrophes: false}) 248 | }) 249 | } 250 | ) 251 | 252 | await t.test( 253 | 'should find smart apostrophe words when `allowApostrophes` is false', 254 | async function () { 255 | assert.throws(function () { 256 | search(tree, ['he’ll'], throws, {allowApostrophes: false}) 257 | }) 258 | } 259 | ) 260 | 261 | await t.test( 262 | 'should find dumb apostrophe words when `allowApostrophes` is false', 263 | async function () { 264 | assert.throws(function () { 265 | search(tree, ["he'll"], throws, {allowApostrophes: false}) 266 | }) 267 | } 268 | ) 269 | 270 | // The tree contains “selfservice” but not “self-service” 271 | await t.test( 272 | 'should find non-dash words when `allowDashes` is absent and `allowApostrophes` is absent', 273 | async function () { 274 | assert.throws(function () { 275 | search(tree, ['selfservice'], throws) 276 | }) 277 | } 278 | ) 279 | 280 | await t.test( 281 | 'should find dash words when `allowDashes` is absent and `allowApostrophes` is absent', 282 | async function () { 283 | assert.throws(function () { 284 | search(tree, ['self-service'], throws) 285 | }) 286 | } 287 | ) 288 | 289 | await t.test( 290 | 'should find non-dash words when `allowDashes` is absent and `allowApostrophes` is false', 291 | async function () { 292 | assert.throws(function () { 293 | search(tree, ['selfservice'], throws, {allowApostrophes: false}) 294 | }) 295 | } 296 | ) 297 | 298 | await t.test( 299 | 'should find dash words when `allowDashes` is absent and `allowApostrophes` is false', 300 | async function () { 301 | assert.throws(function () { 302 | search(tree, ['self-service'], throws, {allowApostrophes: false}) 303 | }) 304 | } 305 | ) 306 | 307 | await t.test( 308 | 'should find non-dash words when `allowDashes` is absent and `allowApostrophes` is true', 309 | async function () { 310 | assert.throws(function () { 311 | search(tree, ['selfservice'], throws, {allowApostrophes: true}) 312 | }) 313 | } 314 | ) 315 | 316 | await t.test( 317 | 'should find dash words when `allowDashes` is absent and `allowApostrophes` is true', 318 | async function () { 319 | assert.throws(function () { 320 | search(tree, ['self-service'], throws, {allowApostrophes: true}) 321 | }) 322 | } 323 | ) 324 | 325 | await t.test( 326 | 'should find non-dash words when `allowDashes` is true', 327 | async function () { 328 | assert.throws(function () { 329 | search(tree, ['selfservice'], throws, {allowDashes: true}) 330 | }) 331 | } 332 | ) 333 | 334 | await t.test( 335 | 'should not find dash words when `allowDashes` is true', 336 | async function () { 337 | assert.doesNotThrow(function () { 338 | search(tree, ['self-service'], throws, {allowDashes: true}) 339 | }) 340 | } 341 | ) 342 | 343 | await t.test( 344 | 'should find non-dash words when `allowDashes` is false', 345 | async function () { 346 | assert.throws(function () { 347 | search(tree, ['selfservice'], throws, {allowDashes: false}) 348 | }) 349 | } 350 | ) 351 | 352 | await t.test( 353 | 'should find dash words when `allowDashes` is false', 354 | async function () { 355 | assert.throws(function () { 356 | search(tree, ['self-service'], throws, {allowDashes: false}) 357 | }) 358 | } 359 | ) 360 | 361 | await t.test( 362 | 'should find non-dash words when `allowDashes` is true and `allowApostrophes` is false', 363 | async function () { 364 | assert.throws(function () { 365 | search(tree, ['selfservice'], throws, { 366 | allowApostrophes: false, 367 | allowDashes: true 368 | }) 369 | }) 370 | } 371 | ) 372 | 373 | await t.test( 374 | 'should not find dash words when `allowDashes` is true and `allowApostrophes` is false', 375 | async function () { 376 | assert.doesNotThrow(function () { 377 | search(tree, ['self-service'], throws, { 378 | allowApostrophes: false, 379 | allowDashes: true 380 | }) 381 | }) 382 | } 383 | ) 384 | 385 | await t.test( 386 | 'should find non-dash words when `allowDashes` is false and `allowApostrophes` is false', 387 | async function () { 388 | assert.throws(function () { 389 | search(tree, ['selfservice'], throws, { 390 | allowApostrophes: false, 391 | allowDashes: false 392 | }) 393 | }) 394 | } 395 | ) 396 | 397 | await t.test( 398 | 'should find dash words when `allowDashes` is false and `allowApostrophes` is false', 399 | async function () { 400 | assert.throws(function () { 401 | search(tree, ['self-service'], throws, { 402 | allowApostrophes: false, 403 | allowDashes: false 404 | }) 405 | }) 406 | } 407 | ) 408 | 409 | await t.test( 410 | 'should find non-dash words when `allowDashes` is true and `allowApostrophes` is true', 411 | async function () { 412 | assert.throws(function () { 413 | search(tree, ['selfservice'], throws, { 414 | allowApostrophes: true, 415 | allowDashes: true 416 | }) 417 | }) 418 | } 419 | ) 420 | 421 | await t.test( 422 | 'should not find dash words when `allowDashes` is true and `allowApostrophes` is true', 423 | async function () { 424 | assert.doesNotThrow(function () { 425 | search(tree, ['self-service'], throws, { 426 | allowApostrophes: true, 427 | allowDashes: true 428 | }) 429 | }) 430 | } 431 | ) 432 | 433 | await t.test( 434 | 'should find non-dash words when `allowDashes` is false and `allowApostrophes` is true', 435 | async function () { 436 | assert.throws(function () { 437 | search(tree, ['selfservice'], throws, { 438 | allowApostrophes: true, 439 | allowDashes: false 440 | }) 441 | }) 442 | } 443 | ) 444 | 445 | await t.test( 446 | 'should find dash words when `allowDashes` is false and `allowApostrophes` is true', 447 | async function () { 448 | assert.throws(function () { 449 | search(tree, ['self-service'], throws, { 450 | allowApostrophes: true, 451 | allowDashes: false 452 | }) 453 | }) 454 | } 455 | ) 456 | 457 | await t.test('should support wild cards (#1)', async function () { 458 | assert.throws(function () { 459 | search(tree, ['this * selfservice'], throws) 460 | }) 461 | }) 462 | 463 | await t.test('should support wild cards (#2)', async function () { 464 | assert.doesNotThrow(function () { 465 | search(tree, ['that * selfservice'], throws) 466 | }) 467 | }) 468 | 469 | await t.test('should support wild cards (#3)', async function () { 470 | assert.throws(function () { 471 | search(tree, ['* selfservice'], throws) 472 | }) 473 | }) 474 | 475 | await t.test('should support wild cards (#4)', async function () { 476 | assert.doesNotThrow(function () { 477 | search(tree, ['* zelfzervice'], throws) 478 | }) 479 | }) 480 | 481 | await t.test('should not find literals by default', async function () { 482 | assert.doesNotThrow(function () { 483 | search(tree, ['mellow'], throws) 484 | }) 485 | }) 486 | 487 | await t.test( 488 | 'should find literals when given `allowLiterals`', 489 | async function () { 490 | search( 491 | tree, 492 | ['mellow'], 493 | function () { 494 | assert.ok(true) 495 | }, 496 | {allowLiterals: true} 497 | ) 498 | } 499 | ) 500 | }) 501 | 502 | function throws() { 503 | throw new Error('Should not be called') 504 | } 505 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js"] 15 | } 16 | --------------------------------------------------------------------------------