@octanejs/cmdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/package.json +52 -0
- package/src/cmdk.tsrx +52 -0
- package/src/cmdk.tsrx.d.ts +49 -0
- package/src/command-score.ts +177 -0
- package/src/components/command.tsrx +729 -0
- package/src/components/dialog.tsrx +40 -0
- package/src/components/empty.tsrx +28 -0
- package/src/components/group.tsrx +52 -0
- package/src/components/input.tsrx +44 -0
- package/src/components/item.tsrx +95 -0
- package/src/components/list.tsrx +50 -0
- package/src/components/loading.tsrx +19 -0
- package/src/components/separator.tsrx +14 -0
- package/src/context.tsrx +12 -0
- package/src/filter.ts +14 -0
- package/src/hooks.tsrx +125 -0
- package/src/index.ts +31 -0
- package/src/lib/constants.ts +23 -0
- package/src/types.ts +171 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @octanejs/cmdk
|
|
2
|
+
|
|
3
|
+
[cmdk](https://github.com/dip/cmdk) — the fast, composable command menu — ported
|
|
4
|
+
to the [Octane](https://github.com/octanejs/octane) renderer.
|
|
5
|
+
|
|
6
|
+
This is an Octane port of `cmdk@1.1.1`. It preserves cmdk's public API (the
|
|
7
|
+
`Command` namespace, `useCommandState`, `defaultFilter`) and its
|
|
8
|
+
DOM-authoritative filter/selection model, adapting only the React-renderer
|
|
9
|
+
wiring to Octane (native events, ref-as-prop, `useId`), and building on
|
|
10
|
+
[`@octanejs/radix`](../radix) for the Radix primitives cmdk depends on.
|
|
11
|
+
|
|
12
|
+
> **Alpha.** The whole component set is implemented — `Command`, `Command.Input`,
|
|
13
|
+
> `Command.List`, `Command.Item`, `Command.Group`, `Command.Separator`,
|
|
14
|
+
> `Command.Dialog`, `Command.Empty`, `Command.Loading`, plus `useCommandState`
|
|
15
|
+
> and `defaultFilter` — with filtering, score sorting, keyboard navigation,
|
|
16
|
+
> controlled modes, and SSR + hydration. `asChild` is the one unsupported prop.
|
|
17
|
+
> A differential suite runs the same fixture through this port and the published
|
|
18
|
+
> `cmdk@1.1.1` on React and asserts byte-equal HTML. See
|
|
19
|
+
> [`docs/cmdk-port-plan.md`](../../docs/cmdk-port-plan.md) and the authoritative
|
|
20
|
+
> [`docs/bindings-status.md`](../../docs/bindings-status.md).
|
|
21
|
+
|
|
22
|
+
## Divergences from React cmdk
|
|
23
|
+
|
|
24
|
+
Standard Octane binding adaptations (see
|
|
25
|
+
[`docs/react-parity-migration-plan.md`](../../docs/react-parity-migration-plan.md)):
|
|
26
|
+
|
|
27
|
+
- `Command.Input` drives search from the native `onInput` event; the public
|
|
28
|
+
`onValueChange(search)` API is unchanged.
|
|
29
|
+
- Components take `ref` as a normal prop (Octane has no `forwardRef`).
|
|
30
|
+
- Callbacks (`onSelect`, pointer, keyboard) observe native DOM events.
|
|
31
|
+
- Item/group values come from the `value` prop or the rendered `textContent`;
|
|
32
|
+
cmdk's string-child inspection is dropped (Octane's compiled children are
|
|
33
|
+
opaque).
|
|
34
|
+
- `asChild` is **not** supported: cmdk clones the child element and re-parents
|
|
35
|
+
the component's own content into it, which has no faithful equivalent over
|
|
36
|
+
opaque compiled children.
|
|
37
|
+
- Group reordering resolves the group element by its registered value; upstream
|
|
38
|
+
looks it up by `[data-value="<groupId>"]`, which can never match.
|
|
39
|
+
- `Command.Dialog` is built on `@octanejs/radix`'s Dialog; ids come from
|
|
40
|
+
`octane`'s `useId` (prefixed to match upstream's `@radix-ui/react-id`). Radix
|
|
41
|
+
`Primitive`/`Slot` are not used — without `asChild` there is nothing to slot,
|
|
42
|
+
so every component renders its own host element.
|
|
43
|
+
- `Command.Empty` and a visible item are mutually exclusive, including for
|
|
44
|
+
`forceMount` items and during SSR. Upstream can render both at once.
|
|
45
|
+
- `forceMount` items are full citizens of registration: they release the value
|
|
46
|
+
they registered on unmount, and removing the selected one moves the selection
|
|
47
|
+
on. Upstream never registers them, so it does neither.
|
|
48
|
+
- `Command.Dialog` also accepts `defaultOpen` and `modal`; upstream forwards
|
|
49
|
+
only `open`/`onOpenChange`.
|
|
50
|
+
- After a search is cleared, items stay in the order `sort()` left them rather
|
|
51
|
+
than returning to source order — octane's reconciler does not reposition nodes
|
|
52
|
+
it did not move. Same items, same selection; only the residual order differs.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT. The vendored `command-score` implementation retains its upstream MIT
|
|
57
|
+
license.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/cmdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"description": "cmdk for the octane renderer — the cmdk 1.1.1 command-menu source ported onto octane's hooks and native events: the Command namespace, useCommandState, and the DOM-authoritative filter/selection model, built on @octanejs/radix primitives.",
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Dominic Gannaway",
|
|
12
|
+
"email": "dg@domgan.com"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
20
|
+
"directory": "packages/cmdk"
|
|
21
|
+
},
|
|
22
|
+
"main": "src/index.ts",
|
|
23
|
+
"module": "src/index.ts",
|
|
24
|
+
"types": "src/index.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./src/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@octanejs/radix": "0.1.16"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"octane": "0.1.17"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
40
|
+
"@tsrx/react": "^0.2.50",
|
|
41
|
+
"cmdk": "1.1.1",
|
|
42
|
+
"esbuild": "^0.28.1",
|
|
43
|
+
"react": "^19.2.7",
|
|
44
|
+
"react-dom": "^19.2.7",
|
|
45
|
+
"vitest": "^4.1.10",
|
|
46
|
+
"octane": "0.1.17",
|
|
47
|
+
"@octanejs/testing-library": "0.1.14"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cmdk.tsrx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @octanejs/cmdk — the Command menu, ported from cmdk@1.1.1 (dip/cmdk,
|
|
2
|
+
// cmdk/src/index.tsx) onto the octane renderer.
|
|
3
|
+
//
|
|
4
|
+
// This module is the barrel: it assembles the `Command` namespace from the
|
|
5
|
+
// per-component modules under ./components and re-exports the public surface.
|
|
6
|
+
// The store/registration hooks live in ./hooks.tsrx, the contexts in
|
|
7
|
+
// ./context.tsrx, and the DOM constants/helpers in ./lib.
|
|
8
|
+
//
|
|
9
|
+
// octane adaptations (see docs/react-parity-migration-plan.md):
|
|
10
|
+
// - No forwardRef: every component takes `ref` as a normal prop; composeRefs
|
|
11
|
+
// becomes octane's multi-ref `ref={[a, b]}`.
|
|
12
|
+
// - Input drives search from native `onInput`; the public `onValueChange` API
|
|
13
|
+
// is unchanged (there is no synthetic `onChange`).
|
|
14
|
+
// - Radix `useId` becomes octane's `useId`; the store subscribes through
|
|
15
|
+
// octane's `useSyncExternalStore`.
|
|
16
|
+
// - `useValue` derives an item's value from its provided `value` prop or the
|
|
17
|
+
// rendered `textContent`; cmdk's string-child inspection is dropped because
|
|
18
|
+
// octane's compiled children are opaque render bodies.
|
|
19
|
+
// - `sort()` reorders only the surviving (score > 0) items — moving a
|
|
20
|
+
// to-be-unmounted node orphans it in an octane hydrated tree (see
|
|
21
|
+
// ./components/command.tsrx).
|
|
22
|
+
// - `asChild` is NOT supported: cmdk's SlottableWithNestedChildren clones a child
|
|
23
|
+
// element via React.cloneElement/isValidElement and re-parents the component's
|
|
24
|
+
// own content into it. Octane's compiled children are opaque render bodies, so
|
|
25
|
+
// that introspect-and-clone has no faithful equivalent — components always
|
|
26
|
+
// render their own host element. `Command.Dialog` composes @octanejs/radix via
|
|
27
|
+
// createElement descriptors instead.
|
|
28
|
+
import { CommandRoot } from './components/command.tsrx';
|
|
29
|
+
import { Item } from './components/item.tsrx';
|
|
30
|
+
import { Input } from './components/input.tsrx';
|
|
31
|
+
import { List } from './components/list.tsrx';
|
|
32
|
+
import { Group } from './components/group.tsrx';
|
|
33
|
+
import { Separator } from './components/separator.tsrx';
|
|
34
|
+
import { Empty } from './components/empty.tsrx';
|
|
35
|
+
import { Loading } from './components/loading.tsrx';
|
|
36
|
+
import { Dialog } from './components/dialog.tsrx';
|
|
37
|
+
import { useCmdk } from './hooks.tsrx';
|
|
38
|
+
|
|
39
|
+
const Command = Object.assign(CommandRoot, {
|
|
40
|
+
List,
|
|
41
|
+
Item,
|
|
42
|
+
Input,
|
|
43
|
+
Group,
|
|
44
|
+
Separator,
|
|
45
|
+
Dialog,
|
|
46
|
+
Empty,
|
|
47
|
+
Loading,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export { Command, CommandRoot, Item, Input, List, Group, Separator, Dialog, Empty, Loading };
|
|
51
|
+
export { useCmdk as useCommandState };
|
|
52
|
+
export { Item as CommandItem, Input as CommandInput, List as CommandList, Group as CommandGroup, Separator as CommandSeparator, Dialog as CommandDialog, Empty as CommandEmpty, Loading as CommandLoading };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Ambient declaration for cmdk.tsrx so `.ts` consumers and tsgo see real types
|
|
2
|
+
// without parsing the .tsrx source (kept in sync with cmdk.tsrx's exports).
|
|
3
|
+
import type { OctaneNode } from 'octane';
|
|
4
|
+
import type {
|
|
5
|
+
CommandProps,
|
|
6
|
+
DialogProps,
|
|
7
|
+
EmptyProps,
|
|
8
|
+
GroupProps,
|
|
9
|
+
InputProps,
|
|
10
|
+
ItemProps,
|
|
11
|
+
ListProps,
|
|
12
|
+
LoadingProps,
|
|
13
|
+
SeparatorProps,
|
|
14
|
+
State,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export declare function CommandRoot(props: CommandProps): OctaneNode;
|
|
18
|
+
export declare function Item(props: ItemProps): OctaneNode;
|
|
19
|
+
export declare function Input(props: InputProps): OctaneNode;
|
|
20
|
+
export declare function List(props: ListProps): OctaneNode;
|
|
21
|
+
export declare function Group(props: GroupProps): OctaneNode;
|
|
22
|
+
export declare function Separator(props: SeparatorProps): OctaneNode;
|
|
23
|
+
export declare function Dialog(props: DialogProps): OctaneNode;
|
|
24
|
+
export declare function Empty(props: EmptyProps): OctaneNode;
|
|
25
|
+
export declare function Loading(props: LoadingProps): OctaneNode;
|
|
26
|
+
|
|
27
|
+
export declare const Command: typeof CommandRoot & {
|
|
28
|
+
List: typeof List;
|
|
29
|
+
Item: typeof Item;
|
|
30
|
+
Input: typeof Input;
|
|
31
|
+
Group: typeof Group;
|
|
32
|
+
Separator: typeof Separator;
|
|
33
|
+
Dialog: typeof Dialog;
|
|
34
|
+
Empty: typeof Empty;
|
|
35
|
+
Loading: typeof Loading;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export declare function useCommandState<T>(selector: (state: State) => T): T;
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
Item as CommandItem,
|
|
42
|
+
Input as CommandInput,
|
|
43
|
+
List as CommandList,
|
|
44
|
+
Group as CommandGroup,
|
|
45
|
+
Separator as CommandSeparator,
|
|
46
|
+
Dialog as CommandDialog,
|
|
47
|
+
Empty as CommandEmpty,
|
|
48
|
+
Loading as CommandLoading,
|
|
49
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Vendored from cmdk@1.1.1 (source: cmdk/src/command-score.ts, upstream repo
|
|
2
|
+
// dip/cmdk). The algorithm and every score constant are unchanged. The only
|
|
3
|
+
// edits are toolchain adaptations: the inner helpers gained explicit parameter
|
|
4
|
+
// type annotations for this package's `strict` typecheck (upstream ships them
|
|
5
|
+
// untyped), and the file is formatted to repo style. No logic was touched.
|
|
6
|
+
|
|
7
|
+
// The scores are arranged so that a continuous match of characters will
|
|
8
|
+
// result in a total score of 1.
|
|
9
|
+
//
|
|
10
|
+
// The best case, this character is a match, and either this is the start
|
|
11
|
+
// of the string, or the previous character was also a match.
|
|
12
|
+
var SCORE_CONTINUE_MATCH = 1,
|
|
13
|
+
// A new match at the start of a word scores better than a new match
|
|
14
|
+
// elsewhere as it's more likely that the user will type the starts
|
|
15
|
+
// of fragments.
|
|
16
|
+
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
|
|
17
|
+
// hyphens, etc.
|
|
18
|
+
SCORE_SPACE_WORD_JUMP = 0.9,
|
|
19
|
+
SCORE_NON_SPACE_WORD_JUMP = 0.8,
|
|
20
|
+
// Any other match isn't ideal, but we include it for completeness.
|
|
21
|
+
SCORE_CHARACTER_JUMP = 0.17,
|
|
22
|
+
// If the user transposed two letters, it should be significantly penalized.
|
|
23
|
+
//
|
|
24
|
+
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
|
|
25
|
+
SCORE_TRANSPOSITION = 0.1,
|
|
26
|
+
// The goodness of a match should decay slightly with each missing
|
|
27
|
+
// character.
|
|
28
|
+
//
|
|
29
|
+
// i.e. "bad" is more likely than "bard" when "bd" is typed.
|
|
30
|
+
//
|
|
31
|
+
// This will not change the order of suggestions based on SCORE_* until
|
|
32
|
+
// 100 characters are inserted between matches.
|
|
33
|
+
PENALTY_SKIPPED = 0.999,
|
|
34
|
+
// The goodness of an exact-case match should be higher than a
|
|
35
|
+
// case-insensitive match by a small amount.
|
|
36
|
+
//
|
|
37
|
+
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
|
|
38
|
+
//
|
|
39
|
+
// This will not change the order of suggestions based on SCORE_* until
|
|
40
|
+
// 1000 characters are inserted between matches.
|
|
41
|
+
PENALTY_CASE_MISMATCH = 0.9999,
|
|
42
|
+
// Match higher for letters closer to the beginning of the word
|
|
43
|
+
PENALTY_DISTANCE_FROM_START = 0.9,
|
|
44
|
+
// If the word has more characters than the user typed, it should
|
|
45
|
+
// be penalised slightly.
|
|
46
|
+
//
|
|
47
|
+
// i.e. "html" is more likely than "html5" if I type "html".
|
|
48
|
+
//
|
|
49
|
+
// However, it may well be the case that there's a sensible secondary
|
|
50
|
+
// ordering (like alphabetical) that it makes sense to rely on when
|
|
51
|
+
// there are many prefix matches, so we don't make the penalty increase
|
|
52
|
+
// with the number of tokens.
|
|
53
|
+
PENALTY_NOT_COMPLETE = 0.99;
|
|
54
|
+
|
|
55
|
+
var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/,
|
|
56
|
+
COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g,
|
|
57
|
+
IS_SPACE_REGEXP = /[\s-]/,
|
|
58
|
+
COUNT_SPACE_REGEXP = /[\s-]/g;
|
|
59
|
+
|
|
60
|
+
function commandScoreInner(
|
|
61
|
+
string: string,
|
|
62
|
+
abbreviation: string,
|
|
63
|
+
lowerString: string,
|
|
64
|
+
lowerAbbreviation: string,
|
|
65
|
+
stringIndex: number,
|
|
66
|
+
abbreviationIndex: number,
|
|
67
|
+
memoizedResults: Record<string, number>,
|
|
68
|
+
): number {
|
|
69
|
+
if (abbreviationIndex === abbreviation.length) {
|
|
70
|
+
if (stringIndex === string.length) {
|
|
71
|
+
return SCORE_CONTINUE_MATCH;
|
|
72
|
+
}
|
|
73
|
+
return PENALTY_NOT_COMPLETE;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
|
77
|
+
if (memoizedResults[memoizeKey] !== undefined) {
|
|
78
|
+
return memoizedResults[memoizeKey];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
|
82
|
+
var index = lowerString.indexOf(abbreviationChar, stringIndex);
|
|
83
|
+
var highScore = 0;
|
|
84
|
+
|
|
85
|
+
var score, transposedScore, wordBreaks, spaceBreaks;
|
|
86
|
+
|
|
87
|
+
while (index >= 0) {
|
|
88
|
+
score = commandScoreInner(
|
|
89
|
+
string,
|
|
90
|
+
abbreviation,
|
|
91
|
+
lowerString,
|
|
92
|
+
lowerAbbreviation,
|
|
93
|
+
index + 1,
|
|
94
|
+
abbreviationIndex + 1,
|
|
95
|
+
memoizedResults,
|
|
96
|
+
);
|
|
97
|
+
if (score > highScore) {
|
|
98
|
+
if (index === stringIndex) {
|
|
99
|
+
score *= SCORE_CONTINUE_MATCH;
|
|
100
|
+
} else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) {
|
|
101
|
+
score *= SCORE_NON_SPACE_WORD_JUMP;
|
|
102
|
+
wordBreaks = string.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP);
|
|
103
|
+
if (wordBreaks && stringIndex > 0) {
|
|
104
|
+
score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length);
|
|
105
|
+
}
|
|
106
|
+
} else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) {
|
|
107
|
+
score *= SCORE_SPACE_WORD_JUMP;
|
|
108
|
+
spaceBreaks = string.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP);
|
|
109
|
+
if (spaceBreaks && stringIndex > 0) {
|
|
110
|
+
score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
score *= SCORE_CHARACTER_JUMP;
|
|
114
|
+
if (stringIndex > 0) {
|
|
115
|
+
score *= Math.pow(PENALTY_SKIPPED, index - stringIndex);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
|
|
120
|
+
score *= PENALTY_CASE_MISMATCH;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
(score < SCORE_TRANSPOSITION &&
|
|
126
|
+
lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
|
|
127
|
+
(lowerAbbreviation.charAt(abbreviationIndex + 1) ===
|
|
128
|
+
lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428
|
|
129
|
+
lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex))
|
|
130
|
+
) {
|
|
131
|
+
transposedScore = commandScoreInner(
|
|
132
|
+
string,
|
|
133
|
+
abbreviation,
|
|
134
|
+
lowerString,
|
|
135
|
+
lowerAbbreviation,
|
|
136
|
+
index + 1,
|
|
137
|
+
abbreviationIndex + 2,
|
|
138
|
+
memoizedResults,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (transposedScore * SCORE_TRANSPOSITION > score) {
|
|
142
|
+
score = transposedScore * SCORE_TRANSPOSITION;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (score > highScore) {
|
|
147
|
+
highScore = score;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
index = lowerString.indexOf(abbreviationChar, index + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
memoizedResults[memoizeKey] = highScore;
|
|
154
|
+
return highScore;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatInput(string: string): string {
|
|
158
|
+
// convert all valid space characters to space so they match each other
|
|
159
|
+
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function commandScore(string: string, abbreviation: string, aliases: string[]): number {
|
|
163
|
+
/* NOTE:
|
|
164
|
+
* in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase()
|
|
165
|
+
* was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster.
|
|
166
|
+
*/
|
|
167
|
+
string = aliases && aliases.length > 0 ? `${string + ' ' + aliases.join(' ')}` : string;
|
|
168
|
+
return commandScoreInner(
|
|
169
|
+
string,
|
|
170
|
+
abbreviation,
|
|
171
|
+
formatInput(string),
|
|
172
|
+
formatInput(abbreviation),
|
|
173
|
+
0,
|
|
174
|
+
0,
|
|
175
|
+
{},
|
|
176
|
+
);
|
|
177
|
+
}
|