# path-search-sort

Search, filter, and sort file paths, ranked the way a human expects.

[![npm version](https://badge.fury.io/js/path-search-sort.svg)](https://www.npmjs.com/package/path-search-sort)
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/path-search-sort/badge)](https://www.jsdelivr.com/package/npm/path-search-sort)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)

## Features

* Exact names first, then starts-with, then contains, in natural order
* Directory and RegExp queries: `block/stone`, `/shot_\d+/`
* Scope by root, folders, extensions, or your own filter
* Forgiving: case, pasted paths, and either slash direction all just work

## Install

For Node.js, or the browser through a bundler:

```bash
npm install path-search-sort
```

Or in the browser, import it straight from a [CDN](https://www.jsdelivr.com/package/npm/path-search-sort):

```js
import pathSearch from "https://cdn.jsdelivr.net/npm/path-search-sort/+esm"
```

## Examples

```js
import pathSearch from "path-search-sort"

const paths = [
  "img/photo.png",
  "img/old/photo.png",
  "img/photo_edit.jpg",
  "screenshots/2024/shot_2.png",
  "screenshots/2024/shot_10.png",
  "screenshots/2025/my_shot.png",
  "backup/2024/screenshot.jpg",
  "backup/Shot.png"
]

pathSearch(paths, "photo")       // every file with photo in its name, best matches first
pathSearch(paths, "img/photo")   // files starting with photo under an img/ folder only
pathSearch(paths, /shot_\d+/)    // a RegExp works too, ranked by where it hits

// where to look
pathSearch(paths, "shot", { root: "screenshots" })  // only from the top of the tree
pathSearch(paths, "shot", { path: "2024" })         // only through a 2024/ folder, wherever it sits

// what to keep
pathSearch(paths, "shot", { extension: ["png", "jpg"] })         // only those extensions
pathSearch(paths, "shot", { filter: p => !p.endsWith(".jpg") })  // your own custom filter
pathSearch(paths, "shot", { limit: 25 })                         // top 25 after sorting

// how to match
pathSearch(paths, "screenshot", { folders: false })  // the file, not the screenshots/ folder
pathSearch(paths, "Shot", { caseSensitive: true })   // case matters
pathSearch(paths, "my shot", { spaces: "_" })        // typed spaces mean underscores

// items can be strings or objects with a .path, with either slash direction;
[
  "img\\photo.png",
  { path: "img/old/photo.png" }
]
// whatever went in comes back untouched
```

## Options

`pathSearch(items, query, options)`, and all options are optional:

| Option | Default | Description |
|---|---|---|
| `root` | *(everywhere)* | Only paths starting with this prefix |
| `path` | *(everywhere)* | Only paths passing through these folders, wherever they sit |
| `extension` | *(all)* | Only these file types: one extension or several, leading dot optional |
| `filter` | *(none)* | Your own predicate over the item, only called for what already matched |
| `limit` | *(all)* | Cap on results, applied after sorting |
| `folders` | `true` | Also match folder names: files inside a matching folder rank after name matches |
| `caseSensitive` | `false` | Case matters, except for the `extension` filter; RegExp queries ignore it |
| `spaces` | *(kept)* | What a typed space becomes, e.g. `"_"` for underscore-named files; RegExp queries ignore it |

## How it matches

Matching is on the full filename, extension included, and case insensitive. A directory in the query constrains it: `block/stone` means a stone under a `block/` folder wherever that sits. Folder names are matched too, so a file inside a matching folder is found even when its own name does not match, and `block/` on its own lists everything under block folders. `folders: false` narrows all of that to file names alone, where a bare `block/` then matches nothing.

RegExp queries are also supported and run globally over the path: every hit is found and the best-placed one does the ranking. The casing is case sensitive in RegEx mode: the `caseSensitive` and `spaces` options are ignored.

## How it sorts

```js
const results = pathSearch(paths, "stone")

// results
[
  "stone.png",                          // exact name first, the extension not counting
  "ore/stone.png",                      // equal names: the shallower path wins
  "stone_axe.png",                      // then names starting with the query
  "stone_axe_2.png",                    // in natural order within every tier
  "stone_axe_10.png",                   // so 10 after 2
  "stonework.png",                      // but whole words go first, so this one waits
  "smooth_stone.png",                   // then names containing the query
  "redstone.png",                       // buried in a word, so last of those too
  "stone/gravel.png",                   // folder matches last: an exact stone folder
  "mine/stone/ores/coal.png",           // the earlier the matching segment the better
  "mine/stone/deep/dark/caves/gem.png", // no matter how deep the path goes on
  "mine/ores/stone/iron.png",           // a later stone comes after
  "stonework/gravel.png",               // then a folder starting with stone
  "old_stone/gravel.png"                // then a folder containing it
]
```

## Also exported

```js
import { naturalCompare } from "path-search-sort"

names.sort(naturalCompare)
```

`naturalCompare` is the comparator on its own, for sorting anything.

## License

[MPL-2.0](LICENSE) © [Ewan Howell](https://ewanhowell.com/)
