@astryxdesign/cli 0.7.12-canary.ccb4a6d → 0.7.12-canary.da3dbdc
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/api/component/__fixtures__/resolutions/modules.txt +5 -5
- package/api/docs/_adapter.d.mts +22 -0
- package/api/docs/_adapter.mjs +81 -3
- package/api/docs/docs.doc.mjs +5 -5
- package/api/docs/docs.type.d.mts +21 -0
- package/api/docs/docs.type.mjs +11 -0
- package/api/docs/list/list.d.mts +5 -1
- package/api/docs/list/list.mjs +72 -6
- package/api/docs/node/node.mjs +2 -40
- package/api/index.d.mts +1 -0
- package/api/index.mjs +1 -0
- package/api/render/_adapter.mjs +9 -0
- package/api/render/render.doc.mjs +2 -2
- package/api/search/_adapter.d.mts +22 -1
- package/api/search/_adapter.mjs +37 -20
- package/api/search/exportSearchIndex.doc.d.mts +10 -0
- package/api/search/exportSearchIndex.doc.mjs +58 -0
- package/api/search/rankSearchIndex.doc.d.mts +10 -0
- package/api/search/rankSearchIndex.doc.mjs +79 -0
- package/api/search/search-index.d.mts +44 -0
- package/api/search/search-index.mjs +270 -0
- package/api/search/search.type.d.mts +28 -0
- package/api/search/search.type.mjs +17 -0
- package/api/swizzle/_moduleGraph.d.mts +7 -0
- package/api/swizzle/_moduleGraph.mjs +191 -83
- package/api/swizzle/copy/copy.mjs +100 -9
- package/api/swizzle/swizzle.doc.mjs +7 -3
- package/api/swizzle/swizzle.type.d.mts +5 -0
- package/api/swizzle/swizzle.type.mjs +1 -0
- package/api/template/template.mjs +1 -0
- package/assets/discover/official-packages.json +16 -0
- package/assets/docs/browser-support.doc.mjs +2 -2
- package/assets/docs/styling-overview.doc.mjs +6 -2
- package/assets/docs/tree/render-pages.doc.mjs +2 -2
- package/assets/docs/tree/tailwind.doc.mjs +41 -1
- package/assets/docs/tree/type-scale.doc.mjs +19 -2
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.tsx +146 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.tsx +173 -0
- package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
- package/assets/templates/pages/table-filter/page.tsx +15 -23
- package/assets/templates/pages/table-inbox/page.tsx +3 -6
- package/assets/templates/themes/butter/butterTheme.ts +2 -2
- package/assets/templates/themes/stone/stoneTheme.ts +1 -1
- package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
- package/clients/cli/commands/docs.doc.mjs +5 -1
- package/clients/cli/commands/docs.mjs +30 -2
- package/clients/cli/commands/swizzle.doc.mjs +4 -0
- package/clients/cli/commands/swizzle.mjs +12 -0
- package/clients/cli/lib/resolve-theme.mjs +16 -7
- package/foundation/discovery/hook-discovery.mjs +31 -8
- package/foundation/doc-compiler/import.d.mts +5 -1
- package/foundation/doc-compiler/import.mjs +40 -6
- package/foundation/fs/import-url.d.mts +20 -0
- package/foundation/fs/import-url.mjs +22 -0
- package/foundation/fs/module-loader.d.mts +24 -3
- package/foundation/fs/module-loader.mjs +69 -47
- package/foundation/render/charts.d.mts +59 -0
- package/foundation/render/charts.mjs +216 -0
- package/foundation/render/embedded-imports.d.mts +7 -7
- package/foundation/render/embedded-imports.mjs +63 -54
- package/foundation/render/page-worker.mjs +339 -52
- package/foundation/render/render-page.mjs +26 -10
- package/foundation/render/ts-paths.d.mts +63 -0
- package/foundation/render/ts-paths.mjs +138 -0
- package/foundation/response/error-codes.doc.mjs +2 -2
- package/foundation/response/error-codes.mjs +3 -2
- package/foundation/text/levenshtein.d.mts +6 -1
- package/foundation/text/levenshtein.mjs +25 -9
- package/foundation/text/suggest-order.d.mts +8 -1
- package/foundation/text/suggest-order.mjs +72 -6
- package/package.json +7 -6
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A project's `compilerOptions.paths` (tsconfig.json or jsconfig.json),
|
|
5
|
+
* read and matched by TypeScript's rules, for `astryx experimental render`:
|
|
6
|
+
* an exact key matches only itself; a key may hold one `*` anywhere, and the
|
|
7
|
+
* longest matching prefix wins; a target may hold one `*`, which takes what
|
|
8
|
+
* the key's `*` matched; targets are tried in order, and when none exists the
|
|
9
|
+
* import resolves as a package. Only bare specifiers are matched. Targets
|
|
10
|
+
* resolve from `baseUrl` (default: the config's folder).
|
|
11
|
+
*
|
|
12
|
+
* @position foundation/render — used by page-worker.mjs and embedded-imports.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} PathPattern
|
|
20
|
+
* @property {string} key - The key as written.
|
|
21
|
+
* @property {string} prefix - What comes before the key's `*` (all of it, for an exact key).
|
|
22
|
+
* @property {string} suffix - What comes after the key's `*`.
|
|
23
|
+
* @property {boolean} wildcard - Whether the key holds a `*`.
|
|
24
|
+
* @property {string[]} targets - Absolute target patterns, in order.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Extensions an extensionless candidate may stand for, in resolution order. */
|
|
28
|
+
const EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.mts', '.cts', '.json'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The path patterns of the config in `dir` (tsconfig.json, else
|
|
32
|
+
* jsconfig.json), or none. Comments and trailing commas are allowed.
|
|
33
|
+
* @param {string} dir
|
|
34
|
+
* @returns {PathPattern[] | null} null when neither file is there
|
|
35
|
+
*/
|
|
36
|
+
export function readPathsIn(dir) {
|
|
37
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
38
|
+
const file = path.join(dir, name);
|
|
39
|
+
if (!fs.existsSync(file)) continue;
|
|
40
|
+
try {
|
|
41
|
+
const text = fs
|
|
42
|
+
.readFileSync(file, 'utf8')
|
|
43
|
+
.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? '')
|
|
44
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
45
|
+
const options = JSON.parse(text)?.compilerOptions ?? {};
|
|
46
|
+
const base = path.resolve(dir, typeof options.baseUrl === 'string' ? options.baseUrl : '.');
|
|
47
|
+
/** @type {PathPattern[]} */
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const [key, targets] of Object.entries(options.paths ?? {})) {
|
|
50
|
+
if (!Array.isArray(targets)) continue;
|
|
51
|
+
const star = key.indexOf('*');
|
|
52
|
+
if (star !== key.lastIndexOf('*')) continue;
|
|
53
|
+
out.push({
|
|
54
|
+
key,
|
|
55
|
+
prefix: star === -1 ? key : key.slice(0, star),
|
|
56
|
+
suffix: star === -1 ? '' : key.slice(star + 1),
|
|
57
|
+
wildcard: star !== -1,
|
|
58
|
+
targets: targets
|
|
59
|
+
.filter(t => typeof t === 'string' && t.indexOf('*') === t.lastIndexOf('*'))
|
|
60
|
+
.map(t => path.resolve(base, t)),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The pattern a specifier matches by TypeScript's rules, and what its `*`
|
|
73
|
+
* matched: an exact key first, else the wildcard key with the longest prefix.
|
|
74
|
+
* @param {PathPattern[]} patterns
|
|
75
|
+
* @param {string} spec
|
|
76
|
+
* @returns {{pattern: PathPattern, capture: string} | null}
|
|
77
|
+
*/
|
|
78
|
+
export function matchPath(patterns, spec) {
|
|
79
|
+
// TypeScript applies `paths` only to bare specifiers.
|
|
80
|
+
if (/^\.{1,2}(\/|$)|^\//.test(spec)) return null;
|
|
81
|
+
const exact = patterns.find(p => !p.wildcard && p.key === spec);
|
|
82
|
+
if (exact) return {pattern: exact, capture: ''};
|
|
83
|
+
/** @type {{pattern: PathPattern, capture: string} | null} */
|
|
84
|
+
let best = null;
|
|
85
|
+
for (const p of patterns) {
|
|
86
|
+
if (!p.wildcard) continue;
|
|
87
|
+
if (spec.length < p.prefix.length + p.suffix.length) continue;
|
|
88
|
+
if (!spec.startsWith(p.prefix) || !spec.endsWith(p.suffix)) continue;
|
|
89
|
+
if (best && best.pattern.prefix.length >= p.prefix.length) continue;
|
|
90
|
+
best = {pattern: p, capture: spec.slice(p.prefix.length, spec.length - p.suffix.length)};
|
|
91
|
+
}
|
|
92
|
+
return best;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The absolute paths a match names, one per target, in order.
|
|
97
|
+
* @param {{pattern: PathPattern, capture: string}} match
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export const candidatesOf = match => match.pattern.targets.map(t => t.replace('*', match.capture));
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The file a candidate path stands for: itself, with an extension, or its
|
|
104
|
+
* folder's index; null when none exists.
|
|
105
|
+
* @param {string} candidate
|
|
106
|
+
* @returns {string | null}
|
|
107
|
+
*/
|
|
108
|
+
export function existingFile(candidate) {
|
|
109
|
+
const isFile = (/** @type {string} */ f) => {
|
|
110
|
+
try {
|
|
111
|
+
return fs.statSync(f).isFile();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (isFile(candidate)) return candidate;
|
|
117
|
+
for (const ext of EXTENSIONS) if (isFile(candidate + ext)) return candidate + ext;
|
|
118
|
+
for (const ext of EXTENSIONS) if (isFile(path.join(candidate, `index${ext}`))) return path.join(candidate, `index${ext}`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a specifier through path patterns: the first target that names an
|
|
124
|
+
* existing file wins.
|
|
125
|
+
* @param {PathPattern[]} patterns
|
|
126
|
+
* @param {string} spec
|
|
127
|
+
* @returns {string | null} the file, or null when the specifier matches no
|
|
128
|
+
* pattern or no target exists
|
|
129
|
+
*/
|
|
130
|
+
export function resolvePath(patterns, spec) {
|
|
131
|
+
const match = matchPath(patterns, spec);
|
|
132
|
+
if (!match) return null;
|
|
133
|
+
for (const candidate of candidatesOf(match)) {
|
|
134
|
+
const file = existingFile(candidate);
|
|
135
|
+
if (file) return file;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -338,13 +338,13 @@ export const doc = {
|
|
|
338
338
|
value: 'ERR_RENDER_NOT_ASTRYX',
|
|
339
339
|
stability: 'experimental',
|
|
340
340
|
description:
|
|
341
|
-
'`render --reverse` was given a file with no embedded source and no Astryx markup. Nothing was written. Raised only by the experimental render command.',
|
|
341
|
+
'`render --reverse` was given a file that a render did not write, with no embedded source and no Astryx markup. Nothing was written. Raised only by the experimental render command.',
|
|
342
342
|
},
|
|
343
343
|
{
|
|
344
344
|
value: 'ERR_RENDER_SOURCE_DAMAGED',
|
|
345
345
|
stability: 'experimental',
|
|
346
346
|
description:
|
|
347
|
-
'`render --reverse` refused the received file, and nothing was written: its embedded source is damaged (not valid, or a file does not match its checksum), lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its own imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds: a dotfile or dot-folder, anything under node_modules, a package manifest, lockfile or tool configuration, a file a test, story or benchmark runner would run, or any kind but a page module, stylesheet, JSON file or image. Raised only by the experimental render command.',
|
|
347
|
+
'`render --reverse` refused the received file, and nothing was written: its embedded source was removed (a file a render wrote, with no Astryx components left to rebuild from) or is damaged (not valid, or a file does not match its checksum), lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its own imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds: a dotfile or dot-folder, anything under node_modules, a package manifest, lockfile or tool configuration, a file a test, story or benchmark runner would run, or any kind but a page module, stylesheet, JSON file or image. Raised only by the experimental render command.',
|
|
348
348
|
},
|
|
349
349
|
],
|
|
350
350
|
};
|
|
@@ -298,8 +298,9 @@ export const ERROR_CODES = Object.freeze({
|
|
|
298
298
|
*/
|
|
299
299
|
ERR_RENDER_FAILED: 'ERR_RENDER_FAILED',
|
|
300
300
|
/**
|
|
301
|
-
* `render --reverse` was given an HTML file that
|
|
302
|
-
* no embedded source, so there is nothing
|
|
301
|
+
* `render --reverse` was given an HTML file that a render did not write and
|
|
302
|
+
* that holds no Astryx markup and no embedded source, so there is nothing
|
|
303
|
+
* to rebuild. Nothing was written.
|
|
303
304
|
*/
|
|
304
305
|
ERR_RENDER_NOT_ASTRYX: 'ERR_RENDER_NOT_ASTRYX',
|
|
305
306
|
/**
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a: string, b: string, max?: number
|
|
27
|
+
export function levenshteinDistance(a: string, b: string, max?: number, { swaps }?: {
|
|
28
|
+
swaps?: boolean;
|
|
29
|
+
}): number;
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a, b, max) {
|
|
25
|
-
if (max !== undefined) return distanceWithin(a, b, max);
|
|
26
|
-
const m = a.length,
|
|
27
|
+
export function levenshteinDistance(a, b, max, {swaps = false} = {}) {
|
|
28
|
+
if (max !== undefined) return distanceWithin(a, b, max, swaps);
|
|
29
|
+
const m = a.length,
|
|
30
|
+
n = b.length;
|
|
27
31
|
// Early exit: the distance is always ≥ |m − n|, so once the length gap
|
|
28
32
|
// exceeds the loosest fuzzy-match threshold any caller uses (the hook
|
|
29
33
|
// suggester keeps distance ≤ 5) the pair can never match — skip the O(m·n)
|
|
@@ -36,9 +40,10 @@ export function levenshteinDistance(a, b, max) {
|
|
|
36
40
|
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
37
41
|
for (let i = 1; i <= m; i++) {
|
|
38
42
|
for (let j = 1; j <= n; j++) {
|
|
39
|
-
dp[i][j] =
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
dp[i][j] =
|
|
44
|
+
a[i - 1] === b[j - 1]
|
|
45
|
+
? dp[i - 1][j - 1]
|
|
46
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
return dp[m][n];
|
|
@@ -51,11 +56,13 @@ export function levenshteinDistance(a, b, max) {
|
|
|
51
56
|
* @param {string} a
|
|
52
57
|
* @param {string} b
|
|
53
58
|
* @param {number} max
|
|
59
|
+
* @param {boolean} swaps - two adjacent letters swapped cost one edit
|
|
54
60
|
* @returns {number}
|
|
55
61
|
*/
|
|
56
|
-
function distanceWithin(a, b, max) {
|
|
62
|
+
function distanceWithin(a, b, max, swaps) {
|
|
57
63
|
const over = max + 1;
|
|
58
64
|
if (Math.abs(a.length - b.length) > max) return over;
|
|
65
|
+
let before = new Array(b.length + 1).fill(over);
|
|
59
66
|
let previous = new Array(b.length + 1).fill(over);
|
|
60
67
|
let current = new Array(b.length + 1).fill(over);
|
|
61
68
|
for (let j = 0; j <= Math.min(b.length, max); j++) previous[j] = j;
|
|
@@ -67,18 +74,27 @@ function distanceWithin(a, b, max) {
|
|
|
67
74
|
let rowMin = current[0];
|
|
68
75
|
for (let j = from; j <= to; j++) {
|
|
69
76
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
70
|
-
|
|
77
|
+
let value = Math.min(
|
|
71
78
|
previous[j - 1] + cost,
|
|
72
79
|
previous[j] + 1,
|
|
73
80
|
current[j - 1] + 1,
|
|
74
81
|
over,
|
|
75
82
|
);
|
|
83
|
+
if (
|
|
84
|
+
swaps &&
|
|
85
|
+
i > 1 &&
|
|
86
|
+
j > 1 &&
|
|
87
|
+
a[i - 1] === b[j - 2] &&
|
|
88
|
+
a[i - 2] === b[j - 1]
|
|
89
|
+
) {
|
|
90
|
+
value = Math.min(value, before[j - 2] + 1);
|
|
91
|
+
}
|
|
76
92
|
current[j] = value;
|
|
77
93
|
if (value < rowMin) rowMin = value;
|
|
78
94
|
}
|
|
79
95
|
// Every later cell costs at least this row's least.
|
|
80
96
|
if (rowMin >= over) return over;
|
|
81
|
-
[previous, current] = [current,
|
|
97
|
+
[before, previous, current] = [previous, current, before];
|
|
82
98
|
}
|
|
83
99
|
return Math.min(previous[b.length], over);
|
|
84
100
|
}
|
|
@@ -18,15 +18,22 @@ export function nameWords(text: string): string[];
|
|
|
18
18
|
* when it carries some of them, the items that carry none are left out. When
|
|
19
19
|
* no item carries one, nothing is left out. The list only ever gets shorter,
|
|
20
20
|
* and its order does not change.
|
|
21
|
+
*
|
|
22
|
+
* With `typos`, a near name (one or two slips from the typed name, as
|
|
23
|
+
* `ButonGroup` or `ButtonGorup` for ButtonGroup) ranks just below the names
|
|
24
|
+
* that carry every typed word and above the rest. With `closest` too, near
|
|
25
|
+
* names stay in the list whatever the cut leaves out, and the list is never
|
|
26
|
+
* longer than it is without `typos`.
|
|
21
27
|
* @template T
|
|
22
28
|
* @param {string} typedName
|
|
23
29
|
* @param {T[]} items
|
|
24
30
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
25
|
-
* @param {{closest?: boolean}} [options]
|
|
31
|
+
* @param {{closest?: boolean, typos?: boolean}} [options]
|
|
26
32
|
* @returns {T[]}
|
|
27
33
|
*/
|
|
28
34
|
export function orderByAffinity<T>(typedName: string, items: T[], namesOf: (item: T) => Array<string | undefined>, options?: {
|
|
29
35
|
closest?: boolean;
|
|
36
|
+
typos?: boolean;
|
|
30
37
|
}): T[];
|
|
31
38
|
/**
|
|
32
39
|
* Docs route suggestions, closest to the typed route first. When every
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
|
|
20
20
|
import {levenshteinDistance} from './levenshtein.mjs';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The score of a near name (nearNameSlips), less a little per slip: above any
|
|
24
|
+
* name that carries only some of the typed words, below one that carries all
|
|
25
|
+
* of them.
|
|
26
|
+
*/
|
|
27
|
+
const NEAR_NAME_SCORE = 19.6;
|
|
22
28
|
/** The score a synonym match gets: an exact keyword's. */
|
|
23
29
|
const SYNONYM_SCORE = 90;
|
|
24
30
|
/** The score a name with exactly the typed words gets: an exact name's. */
|
|
@@ -59,17 +65,22 @@ export function nameWords(text) {
|
|
|
59
65
|
* closer. Tiers, best first: the same words; every typed word present; some
|
|
60
66
|
* typed word present; none. Within a tier, more of the typed words and more of
|
|
61
67
|
* the candidate's own words matched rank higher, and a name that starts with
|
|
62
|
-
* the typed name (or the reverse) gets a small lift.
|
|
68
|
+
* the typed name (or the reverse) gets a small lift. With `typos`, a near name
|
|
69
|
+
* (one or two slips from the typed name) ranks just below the names that
|
|
70
|
+
* carry every typed word.
|
|
63
71
|
* @param {string[]} typed - {@link nameWords} of the typed name
|
|
64
72
|
* @param {string} name
|
|
73
|
+
* @param {{typos?: boolean}} [options]
|
|
65
74
|
* @returns {number}
|
|
66
75
|
*/
|
|
67
|
-
function affinityOf(typed, name) {
|
|
76
|
+
function affinityOf(typed, name, {typos = false} = {}) {
|
|
68
77
|
const words = nameWords(name);
|
|
69
78
|
if (typed.length === 0 || words.length === 0) return 0;
|
|
70
79
|
const typedJoined = typed.join('');
|
|
71
80
|
const joined = words.join('');
|
|
72
81
|
if (joined === typedJoined) return 40;
|
|
82
|
+
const slips = typos ? nearNameSlips(typedJoined, joined) : 0;
|
|
83
|
+
const near = slips > 0 ? NEAR_NAME_SCORE - slips * 0.2 : 0;
|
|
73
84
|
const have = new Set(words);
|
|
74
85
|
const want = new Set(typed);
|
|
75
86
|
const covered = typed.filter(w => have.has(w)).length / typed.length;
|
|
@@ -77,7 +88,29 @@ function affinityOf(typed, name) {
|
|
|
77
88
|
const prefix =
|
|
78
89
|
joined.startsWith(typedJoined) || typedJoined.startsWith(joined) ? 1 : 0;
|
|
79
90
|
const tier = covered === 1 ? 2 : covered > 0 ? 1 : 0;
|
|
80
|
-
|
|
91
|
+
// A near name keeps its word score when that is higher: a name that also
|
|
92
|
+
// carries every typed word ranks among the other full matches.
|
|
93
|
+
return Math.max(
|
|
94
|
+
near,
|
|
95
|
+
tier * 10 + covered * 4 + precise * 2 + prefix + precise * 0.01,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How many slips separate a typed name from a candidate, both with their
|
|
101
|
+
* words joined, when it is a near name: an edit or two adjacent letters
|
|
102
|
+
* swapped count as one slip, one slip is allowed from five letters and two from
|
|
103
|
+
* eight. Zero when it is not a near name.
|
|
104
|
+
* @param {string} typed
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
function nearNameSlips(typed, name) {
|
|
109
|
+
const shorter = Math.min(typed.length, name.length);
|
|
110
|
+
const cap = shorter >= 8 ? 2 : shorter >= 5 ? 1 : 0;
|
|
111
|
+
if (cap === 0 || Math.abs(typed.length - name.length) > cap) return 0;
|
|
112
|
+
const slips = levenshteinDistance(typed, name, cap, {swaps: true});
|
|
113
|
+
return slips <= cap ? slips : 0;
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
/**
|
|
@@ -87,14 +120,15 @@ function affinityOf(typed, name) {
|
|
|
87
120
|
* @param {string} typedName
|
|
88
121
|
* @param {T[]} items
|
|
89
122
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
123
|
+
* @param {{typos?: boolean}} [options]
|
|
90
124
|
* @returns {Array<{item: T, best: number}>}
|
|
91
125
|
*/
|
|
92
|
-
function scoreByAffinity(typedName, items, namesOf) {
|
|
126
|
+
function scoreByAffinity(typedName, items, namesOf, options = {}) {
|
|
93
127
|
const typed = nameWords(typedName);
|
|
94
128
|
const scored = items.map((item, index) => {
|
|
95
129
|
let best = 0;
|
|
96
130
|
for (const name of namesOf(item)) {
|
|
97
|
-
if (name) best = Math.max(best, affinityOf(typed, name));
|
|
131
|
+
if (name) best = Math.max(best, affinityOf(typed, name, options));
|
|
98
132
|
}
|
|
99
133
|
return {item, index, best};
|
|
100
134
|
});
|
|
@@ -112,14 +146,46 @@ function scoreByAffinity(typedName, items, namesOf) {
|
|
|
112
146
|
* when it carries some of them, the items that carry none are left out. When
|
|
113
147
|
* no item carries one, nothing is left out. The list only ever gets shorter,
|
|
114
148
|
* and its order does not change.
|
|
149
|
+
*
|
|
150
|
+
* With `typos`, a near name (one or two slips from the typed name, as
|
|
151
|
+
* `ButonGroup` or `ButtonGorup` for ButtonGroup) ranks just below the names
|
|
152
|
+
* that carry every typed word and above the rest. With `closest` too, near
|
|
153
|
+
* names stay in the list whatever the cut leaves out, and the list is never
|
|
154
|
+
* longer than it is without `typos`.
|
|
115
155
|
* @template T
|
|
116
156
|
* @param {string} typedName
|
|
117
157
|
* @param {T[]} items
|
|
118
158
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
119
|
-
* @param {{closest?: boolean}} [options]
|
|
159
|
+
* @param {{closest?: boolean, typos?: boolean}} [options]
|
|
120
160
|
* @returns {T[]}
|
|
121
161
|
*/
|
|
122
162
|
export function orderByAffinity(typedName, items, namesOf, options = {}) {
|
|
163
|
+
if (!options.typos) return orderByWords(typedName, items, namesOf, options);
|
|
164
|
+
const scored = scoreByAffinity(typedName, items, namesOf, {typos: true});
|
|
165
|
+
if (!options.closest) return scored.map(s => s.item);
|
|
166
|
+
const kept = new Set(orderByWords(typedName, items, namesOf, options));
|
|
167
|
+
return scored
|
|
168
|
+
.filter(s => kept.has(s.item) || isNearScore(s.best))
|
|
169
|
+
.slice(0, kept.size)
|
|
170
|
+
.map(s => s.item);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether a score is a near name's.
|
|
175
|
+
* @param {number} score
|
|
176
|
+
*/
|
|
177
|
+
const isNearScore = score => score < 20 && score > NEAR_NAME_SCORE - 1;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* {@link orderByAffinity} without `typos`.
|
|
181
|
+
* @template T
|
|
182
|
+
* @param {string} typedName
|
|
183
|
+
* @param {T[]} items
|
|
184
|
+
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
185
|
+
* @param {{closest?: boolean}} options
|
|
186
|
+
* @returns {T[]}
|
|
187
|
+
*/
|
|
188
|
+
function orderByWords(typedName, items, namesOf, options) {
|
|
123
189
|
const scored = scoreByAffinity(typedName, items, namesOf);
|
|
124
190
|
if (!options.closest) return scored.map(s => s.item);
|
|
125
191
|
// One-letter words (`a` in `a11y`, `use-a-theme`) say nothing about which
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.7.12-canary.
|
|
3
|
+
"version": "0.7.12-canary.da3dbdc",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"ignore": "^7.0.5",
|
|
118
118
|
"jiti": "^2.7.0",
|
|
119
119
|
"jscodeshift": "^17.3.0",
|
|
120
|
+
"linkedom": "^0.18.14",
|
|
120
121
|
"node-diff3": "3.2.1",
|
|
121
122
|
"postcss": "^8.5.23",
|
|
122
123
|
"postcss-selector-parser": "^7.1.6",
|
|
@@ -124,7 +125,7 @@
|
|
|
124
125
|
"zod": "^4.4.3"
|
|
125
126
|
},
|
|
126
127
|
"peerDependencies": {
|
|
127
|
-
"@astryxdesign/core": "0.7.12-canary.
|
|
128
|
+
"@astryxdesign/core": "0.7.12-canary.da3dbdc"
|
|
128
129
|
},
|
|
129
130
|
"peerDependenciesMeta": {
|
|
130
131
|
"@astryxdesign/core": {
|
|
@@ -132,10 +133,10 @@
|
|
|
132
133
|
}
|
|
133
134
|
},
|
|
134
135
|
"devDependencies": {
|
|
135
|
-
"@astryxdesign/charts": "0.7.12-canary.
|
|
136
|
-
"@astryxdesign/core": "0.7.12-canary.
|
|
137
|
-
"@astryxdesign/lab": "0.7.12-canary.
|
|
138
|
-
"@astryxdesign/theme-neutral": "0.7.12-canary.
|
|
136
|
+
"@astryxdesign/charts": "0.7.12-canary.da3dbdc",
|
|
137
|
+
"@astryxdesign/core": "0.7.12-canary.da3dbdc",
|
|
138
|
+
"@astryxdesign/lab": "0.7.12-canary.da3dbdc",
|
|
139
|
+
"@astryxdesign/theme-neutral": "0.7.12-canary.da3dbdc",
|
|
139
140
|
"@heroicons/react": "^2.2.0",
|
|
140
141
|
"@stylexjs/stylex": "^0.19.0",
|
|
141
142
|
"@types/babel__core": "^7.20.5",
|