@astryxdesign/cli 0.7.11 → 0.7.12-canary.ccb4a6d
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/build/kit/rank.mjs +21 -4
- package/api/build/kit/words.d.mts +1 -0
- package/api/build/kit/words.mjs +1 -1
- package/api/icon/_adapter.d.mts +3 -2
- package/api/icon/_adapter.mjs +8 -2
- package/api/icon/_authoring.d.mts +1 -0
- package/api/icon/_authoring.mjs +586 -66
- package/api/icon/_default-look.d.mts +155 -0
- package/api/icon/_default-look.mjs +474 -0
- package/api/icon/_records.d.mts +1 -1
- package/api/icon/_records.mjs +1 -0
- package/api/icon/add/add.mjs +224 -38
- package/api/icon/icon.type.d.mts +22 -2
- package/api/icon/icon.type.mjs +6 -2
- package/api/icon/iconAdd.doc.mjs +2 -2
- package/api/icon/iconNewDefinition.doc.mjs +3 -2
- package/api/icon/iconNewSet.doc.mjs +4 -4
- package/api/icon/search/search.mjs +10 -9
- package/api/integration/add-helpers.d.mts +11 -1
- package/api/integration/add-helpers.mjs +5 -2
- package/api/integration/authoring-checks.d.mts +0 -2
- package/api/integration/authoring-checks.mjs +1 -6
- package/api/logger.mjs +2 -1
- package/api/swizzle/_adapter.d.mts +1 -0
- package/api/swizzle/_adapter.mjs +4 -1
- package/api/swizzle/_moduleGraph.d.mts +171 -0
- package/api/swizzle/_moduleGraph.mjs +795 -0
- package/api/swizzle/copy/copy.d.mts +2 -43
- package/api/swizzle/copy/copy.mjs +195 -314
- package/api/swizzle/swizzle.d.mts +0 -2
- package/api/swizzle/swizzle.doc.mjs +9 -4
- package/api/swizzle/swizzle.mjs +3 -7
- package/api/swizzle/swizzle.type.d.mts +2 -1
- package/api/swizzle/swizzle.type.mjs +3 -2
- package/assets/discover/official-packages.json +20 -0
- package/assets/docs/tree/icon-sets.doc.mjs +11 -5
- package/assets/templates/blocks/components/Table/TableRowIndexTable.doc.mjs +1 -1
- package/assets/templates/blocks/components/Table/TableRowIndexTable.tsx +2 -2
- package/assets/templates/pages/course-detail/page.tsx +699 -0
- package/assets/templates/pages/course-detail/template.doc.mjs +37 -0
- package/assets/templates/pages/file-explorer/page.tsx +103 -79
- package/assets/templates/pages/table-grouped/page.tsx +160 -232
- package/assets/templates/pages/table-grouped/template.doc.mjs +0 -2
- package/assets/templates/pages/theme-showcase/page.tsx +724 -792
- package/assets/templates/pages/theme-showcase/template.doc.mjs +4 -5
- package/authoring/doctypes/template/type.ts +1 -0
- package/clients/cli/commands/experimental/icon.mjs +31 -10
- package/clients/cli/commands/experimental-icon-add.doc.mjs +1 -1
- package/clients/cli/commands/experimental-icon-new-definition.doc.mjs +2 -1
- package/clients/cli/commands/experimental-icon-new-set.doc.mjs +2 -2
- package/clients/cli/commands/swizzle.doc.mjs +8 -3
- package/clients/cli/commands/swizzle.mjs +21 -9
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/doc-types.mjs +9 -2
- package/foundation/discovery/icon-ranking.d.mts +27 -3
- package/foundation/discovery/icon-ranking.mjs +125 -8
- package/foundation/discovery/icon-set-checks.mjs +44 -35
- package/foundation/discovery/icon-set-discovery.d.mts +13 -0
- package/foundation/discovery/icon-set-discovery.mjs +25 -0
- package/foundation/discovery/icon-set-writer.d.mts +238 -16
- package/foundation/discovery/icon-set-writer.mjs +676 -79
- package/foundation/discovery/module-exports.d.mts +3 -2
- package/foundation/discovery/module-exports.mjs +167 -37
- package/foundation/fs/paths.d.mts +2 -1
- package/foundation/fs/paths.mjs +3 -4
- package/foundation/text/shell-arg.d.mts +14 -0
- package/foundation/text/shell-arg.mjs +17 -0
- package/package.json +6 -6
|
@@ -6,7 +6,8 @@ export const doc = {
|
|
|
6
6
|
name: 'Theme Showcase',
|
|
7
7
|
displayName: 'Theme Showcase',
|
|
8
8
|
description:
|
|
9
|
-
'
|
|
9
|
+
'Multi-surface comparison shell: preview palettes, typography, icons and control states across a storefront, checkout, chat and inventory on one responsive canvas. A theme preview and design-system showcase, with an embeddable content view for an existing app shell.',
|
|
10
|
+
category: 'Shell - Top Nav',
|
|
10
11
|
keywords: [
|
|
11
12
|
'theme',
|
|
12
13
|
'tokens',
|
|
@@ -16,10 +17,8 @@ export const doc = {
|
|
|
16
17
|
'design system preview',
|
|
17
18
|
],
|
|
18
19
|
isReady: true,
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// No `category` — it's hidden from the overview gallery (the only consumer
|
|
22
|
-
// of `category`), and 'Showcase' isn't part of the TemplateCategory taxonomy.
|
|
20
|
+
// The standalone entry demonstrates an application shell; the named preview
|
|
21
|
+
// composes into its host without nesting AppShell. Keep it out of the gallery.
|
|
23
22
|
isHiddenFromOverview: true,
|
|
24
23
|
icons: {
|
|
25
24
|
Banknote: 'money',
|
|
@@ -186,6 +186,7 @@ export type TemplateCategory =
|
|
|
186
186
|
| 'Content - Order Detail'
|
|
187
187
|
| 'Content - Product Detail'
|
|
188
188
|
| 'Content - Work Item Detail'
|
|
189
|
+
| 'Content - Course Detail'
|
|
189
190
|
| 'Content - Product List'
|
|
190
191
|
| 'Content - Documentation Catalog'
|
|
191
192
|
| 'Content - Documentation Design'
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getCliInvocation,
|
|
21
21
|
} from '../../../../foundation/env/package-manager.mjs';
|
|
22
22
|
import {jsonOut} from '../../../../foundation/response/json.mjs';
|
|
23
|
+
import {logger} from '../../../../api/logger.mjs';
|
|
23
24
|
import {resultSet} from '../../../../foundation/debug/index.mjs';
|
|
24
25
|
import {
|
|
25
26
|
emit,
|
|
@@ -140,7 +141,7 @@ export function registerExperimentalIcon(group, program) {
|
|
|
140
141
|
),
|
|
141
142
|
),
|
|
142
143
|
text(
|
|
143
|
-
`Built-in definitions are the ones every set maps. Map one: ${run} experimental icon map <definition> <icon id>`,
|
|
144
|
+
`Built-in definitions are the ones every set maps. Map one: ${run} experimental icon map <definition> <icon id> --set <set>`,
|
|
144
145
|
),
|
|
145
146
|
);
|
|
146
147
|
return answered;
|
|
@@ -157,7 +158,9 @@ export function registerExperimentalIcon(group, program) {
|
|
|
157
158
|
`${each.id}: ${each.words.join(', ')}${each.definitions.length > 0 ? ` = ${each.definitions.join(', ')}` : ''}`,
|
|
158
159
|
),
|
|
159
160
|
),
|
|
160
|
-
text(
|
|
161
|
+
text(
|
|
162
|
+
`Map one: ${run} experimental icon map <definition> <id> --set ${set}`,
|
|
163
|
+
),
|
|
161
164
|
);
|
|
162
165
|
return answered;
|
|
163
166
|
}
|
|
@@ -308,6 +311,9 @@ export function registerExperimentalIcon(group, program) {
|
|
|
308
311
|
* @param {(result: any) => void} print
|
|
309
312
|
*/
|
|
310
313
|
const run = async (call, print) => {
|
|
314
|
+
// Reading a large library reports its progress through the shared
|
|
315
|
+
// logger: on stderr in text mode, never under --json.
|
|
316
|
+
logger.setSilent(Boolean(program.opts().json));
|
|
311
317
|
let result;
|
|
312
318
|
try {
|
|
313
319
|
result = await call();
|
|
@@ -436,7 +442,7 @@ export function registerExperimentalIcon(group, program) {
|
|
|
436
442
|
return;
|
|
437
443
|
}
|
|
438
444
|
if (result.type === 'icon.definitionMatches') {
|
|
439
|
-
const {matches, newDefinition} = result.data;
|
|
445
|
+
const {matches, newDefinition, variant} = result.data;
|
|
440
446
|
emit(
|
|
441
447
|
section(
|
|
442
448
|
`Icon definitions for ${result.data.export} (${matches.length})`,
|
|
@@ -466,6 +472,13 @@ export function registerExperimentalIcon(group, program) {
|
|
|
466
472
|
),
|
|
467
473
|
]
|
|
468
474
|
: []),
|
|
475
|
+
...(variant
|
|
476
|
+
? [
|
|
477
|
+
text(
|
|
478
|
+
`The set already has this glyph in another look. Add this one as its variant: ${formatCliCommand(variant)}`,
|
|
479
|
+
),
|
|
480
|
+
]
|
|
481
|
+
: []),
|
|
469
482
|
text(
|
|
470
483
|
`All icon definitions: ${cli} docs cli/experimental/icon-definitions`,
|
|
471
484
|
),
|
|
@@ -500,17 +513,25 @@ export function registerExperimentalIcon(group, program) {
|
|
|
500
513
|
const {root, manifest, files, written, dryRun} = data;
|
|
501
514
|
const state = dryRun ? 'plan' : written ? 'ok' : 'unchanged';
|
|
502
515
|
const refreshed = data.refreshed === true;
|
|
516
|
+
const updated = data.updated === true;
|
|
517
|
+
const What = `${what[0].toUpperCase()}${what.slice(1)}`;
|
|
503
518
|
emit(
|
|
504
519
|
section(
|
|
505
520
|
refreshed
|
|
506
521
|
? dryRun
|
|
507
522
|
? `Refresh ${what} plan`
|
|
508
|
-
: `${
|
|
509
|
-
:
|
|
510
|
-
?
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
523
|
+
: `${What} refreshed`
|
|
524
|
+
: updated
|
|
525
|
+
? dryRun
|
|
526
|
+
? `${What} update plan`
|
|
527
|
+
: written && files.length > 0
|
|
528
|
+
? `${What} updated`
|
|
529
|
+
: `${What} unchanged`
|
|
530
|
+
: dryRun
|
|
531
|
+
? `New ${what} plan`
|
|
532
|
+
: written
|
|
533
|
+
? `New ${what} added`
|
|
534
|
+
: `New ${what} unchanged`,
|
|
514
535
|
),
|
|
515
536
|
text(`[${state}] ${data.name}`),
|
|
516
537
|
// What the command did (or, in a dry run, would do): never a step
|
|
@@ -542,7 +563,7 @@ export function registerExperimentalIcon(group, program) {
|
|
|
542
563
|
list(
|
|
543
564
|
data.suggestions.map(
|
|
544
565
|
each =>
|
|
545
|
-
`${each.definition} -> ${each.icon} (${each.word}): ${formatCliCommand(`astryx experimental icon map ${each.definition} ${each.icon}`)}`,
|
|
566
|
+
`${each.definition} -> ${each.icon} (${each.word}): ${formatCliCommand(`astryx experimental icon map ${each.definition} ${each.icon} --set ${data.name}`)}`,
|
|
546
567
|
),
|
|
547
568
|
),
|
|
548
569
|
]
|
|
@@ -30,7 +30,7 @@ export const doc = {
|
|
|
30
30
|
flag: '--new-definition <name>',
|
|
31
31
|
param: 'options.newDefinition',
|
|
32
32
|
description:
|
|
33
|
-
"Add your set's own definition, then map the icon to it; needs --description and --words",
|
|
33
|
+
"Add your set's own definition, named in camelCase (aiHelper) or kebab-case (ai-helper), then map the icon to it; needs --description and --words",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
flag: '--description <text>',
|
|
@@ -18,7 +18,8 @@ export const doc = {
|
|
|
18
18
|
name: 'name',
|
|
19
19
|
param: 'name',
|
|
20
20
|
required: true,
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
'The definition name, in camelCase (rocketLaunch) or kebab-case (rocket-launch)',
|
|
22
23
|
},
|
|
23
24
|
],
|
|
24
25
|
options: [
|
|
@@ -11,7 +11,7 @@ export const doc = {
|
|
|
11
11
|
stability: 'experimental',
|
|
12
12
|
summary: 'Start an icon set in your integration package',
|
|
13
13
|
description:
|
|
14
|
-
"Run inside an integration package. Writes the set's folder with its descriptor, its data file, and its generated module, and declares the manifest's `experimental.iconSets` root on first use. With --from it wraps every icon component of a library, records the library and its version as the set's source, copies its license notice, and maps each Astryx icon definition whose name is an icon's id; definitions an icon only shares a word with are listed as suggestions, never written. Run it again to refresh. --variants and --default-variant declare the set's variants. Experimental: it may change or be removed in a patch release.",
|
|
14
|
+
"Run inside an integration package. Writes the set's folder with its descriptor, its data file, and its generated module, and declares the manifest's `experimental.iconSets` root on first use. With --from it wraps every icon component of a library, records the library and its version as the set's source, copies its license notice, and maps each Astryx icon definition whose name is an icon's id; definitions an icon only shares a word with are listed as suggestions, never written. A library whose export names end in looks (XOutline24WGHT500Icon) gives one icon per glyph, by its glyph id (x), in the look the default variant names, else the library's usual default. Run it again with --from to refresh. --variants and --default-variant declare the set's variants; over an existing set without --from, they, --display-name and --description update the set in place and leave its icons as they are; a set with icons changes its default variant only with --from, and only in a library whose export names encode looks. Experimental: it may change or be removed in a patch release.",
|
|
15
15
|
fn: 'iconNewSet',
|
|
16
16
|
args: [
|
|
17
17
|
{
|
|
@@ -49,7 +49,7 @@ export const doc = {
|
|
|
49
49
|
flag: '--default-variant <variant>',
|
|
50
50
|
param: 'options.defaultVariant',
|
|
51
51
|
description:
|
|
52
|
-
"With --variants: the variant each icon's own export draws (the first variant
|
|
52
|
+
"With --variants: the variant each icon's own export draws. When omitted: in a library whose export names encode looks, the look the icons draw (the library's usual default for a new set), which --variants must list; otherwise the set's current default, which --variants must list when the set has icons, else, on a set without icons, the first variant. A set with icons changes it only with --from, and only in a library whose export names encode looks",
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
flag: '--display-name <text>',
|
|
@@ -16,8 +16,13 @@ export const doc = {
|
|
|
16
16
|
summary: 'Copy component source for customization',
|
|
17
17
|
description:
|
|
18
18
|
"Ejects a component's source from the resolved @astryxdesign/core (or its owning " +
|
|
19
|
-
'integration) into your project for deep customization
|
|
20
|
-
'
|
|
19
|
+
'integration) into your project for deep customization. Every import in the copy ' +
|
|
20
|
+
'names a file the copy holds or a path the package exports: names the package ' +
|
|
21
|
+
'exports come from the package, and every internal module the component needs ' +
|
|
22
|
+
"is copied beside it in the package's folder layout. " +
|
|
23
|
+
'Tests, stories and docs are not copied. It lists the npm packages the copy imports, ' +
|
|
24
|
+
'with the ranges the owner package declares, to install if your project lacks them. ' +
|
|
25
|
+
'With no name it lists the swizzlable components. ' +
|
|
21
26
|
'An integration component that replaces a Core component is what that Core name ' +
|
|
22
27
|
'copies; --package @astryxdesign/core copies the original. --list lists Core ' +
|
|
23
28
|
'components, including one an integration replaces.',
|
|
@@ -46,7 +51,7 @@ export const doc = {
|
|
|
46
51
|
flag: '-f, --overwrite',
|
|
47
52
|
param: 'options.overwrite',
|
|
48
53
|
description:
|
|
49
|
-
|
|
54
|
+
"Replace the component's existing files. Without it, existing files fail the command with ERR_FILE_EXISTS and nothing is written. A helper an earlier swizzle wrote is reused when it is the same and is never replaced by a copy that carries it: a different one stays, and this copy writes its own under a version-suffixed name. Swizzling the component whose folder holds that helper counts it as one of that component's own files. A changed private attribute owner module, or a style module it imports, fails the command without --overwrite and is replaced with it",
|
|
50
55
|
},
|
|
51
56
|
],
|
|
52
57
|
examples: [
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file swizzle command — thin wrapper around api/swizzle.
|
|
5
5
|
*
|
|
6
|
-
* Copies a component's source into the consumer project for customization
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* commands for the caller's package manager.
|
|
6
|
+
* Copies a component's source into the consumer project for customization,
|
|
7
|
+
* with the internal modules it needs. All logic — owner resolution, the copy,
|
|
8
|
+
* StyleX detection, maintainer feedback — lives in api/swizzle; this file only
|
|
9
|
+
* parses flags, prints, and prefixes commands for the caller's package manager.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import {jsonOut} from '../../../foundation/response/json.mjs';
|
|
@@ -85,18 +84,31 @@ export function registerSwizzle(program) {
|
|
|
85
84
|
text(`[ok] Copied ${filesCopied} files to ${outputDir}/`),
|
|
86
85
|
text(
|
|
87
86
|
[
|
|
88
|
-
`
|
|
87
|
+
`What ${ownerPackage} exports is imported from ${ownerPackage}; the internal modules the component needs are copied beside it.`,
|
|
89
88
|
'You can now customize the component source freely.',
|
|
90
89
|
].join('\n'),
|
|
91
90
|
),
|
|
92
91
|
];
|
|
93
|
-
// Files the copy wrote outside the component folder: the
|
|
94
|
-
//
|
|
92
|
+
// Files the copy wrote outside the component folder: the internal
|
|
93
|
+
// modules it needs, in the package's folder layout.
|
|
95
94
|
const shared = (result.data.files ?? []).filter(f => f.startsWith('../'));
|
|
96
95
|
if (shared.length > 0) {
|
|
97
96
|
out.push(
|
|
98
97
|
text(
|
|
99
|
-
`
|
|
98
|
+
`Copied beside it: ${shared.join(', ')} (in the package's folder layout; imported by relative path)`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// npm packages the copy imports, each range quoted so the list pastes
|
|
104
|
+
// safely after an install command.
|
|
105
|
+
const dependencies = Object.entries(result.data.dependencies ?? {});
|
|
106
|
+
if (dependencies.length > 0) {
|
|
107
|
+
out.push(
|
|
108
|
+
text(
|
|
109
|
+
`It imports these npm packages; install any your project lacks: ${dependencies
|
|
110
|
+
.map(([name, range]) => (range === '*' ? name : `${name}@"${range}"`))
|
|
111
|
+
.join(' ')}`,
|
|
100
112
|
),
|
|
101
113
|
);
|
|
102
114
|
}
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* none), and a `<topic>.doc.<lang>.mjs` overlay against
|
|
28
28
|
* `ReferenceTranslationDoc`;
|
|
29
29
|
* - themes root: `ThemeDoc`;
|
|
30
|
-
* - iconSets root: `IconSetDoc
|
|
30
|
+
* - iconSets root: `IconSetDoc`, and a set's own definition (stamped
|
|
31
|
+
* `type: 'icon-definition'`) `IconDefinitionDoc`.
|
|
31
32
|
* Every finding is a warning, names the field, and says how to fix it.
|
|
32
33
|
*
|
|
33
34
|
* @input Doc files with the root each was found in, the package root, and the
|
|
@@ -312,7 +313,13 @@ function expectedType(ts, root, exportName, node, core = false) {
|
|
|
312
313
|
return {typeExpr: authoring('ThemeDoc'), typeName: 'ThemeDoc'};
|
|
313
314
|
}
|
|
314
315
|
if (root === 'icon-sets') {
|
|
315
|
-
|
|
316
|
+
// A set's own definitions live in its folder too.
|
|
317
|
+
return shape?.stamp === 'icon-definition'
|
|
318
|
+
? {
|
|
319
|
+
typeExpr: authoring('IconDefinitionDoc'),
|
|
320
|
+
typeName: 'IconDefinitionDoc',
|
|
321
|
+
}
|
|
322
|
+
: {typeExpr: authoring('IconSetDoc'), typeName: 'IconSetDoc'};
|
|
316
323
|
}
|
|
317
324
|
if (translation) {
|
|
318
325
|
return {
|
|
@@ -30,7 +30,9 @@ export function iconRecord(set: DiscoveredIconSet, icon: IconSetIcon, definition
|
|
|
30
30
|
* @property {boolean} meaning the query is one of the icon's tags (never
|
|
31
31
|
* both: a tag is a word that is not the id or one of its parts)
|
|
32
32
|
* @property {number} rank within-tier key, lower first: for a name match 0
|
|
33
|
-
* (whole id)
|
|
33
|
+
* (whole id), 1 (the whole id's plural or singular), 2 (an id part), or 3
|
|
34
|
+
* (an id part's plural or singular); for a meaning match the tag's
|
|
35
|
+
* position
|
|
34
36
|
* @property {string} reason
|
|
35
37
|
*/
|
|
36
38
|
/**
|
|
@@ -41,6 +43,24 @@ export function iconRecord(set: DiscoveredIconSet, icon: IconSetIcon, definition
|
|
|
41
43
|
* @returns {IconMatch | null}
|
|
42
44
|
*/
|
|
43
45
|
export function matchIcon(query: string, icon: IconSetIcon, definitions?: string[]): IconMatch | null;
|
|
46
|
+
/**
|
|
47
|
+
* The query without the words that only phrase it, when it has several
|
|
48
|
+
* words and some word names something; else the query as typed.
|
|
49
|
+
* @param {string} query
|
|
50
|
+
*/
|
|
51
|
+
export function iconQuery(query: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* The matches that answer a query: the whole-word ones (a definition or an
|
|
54
|
+
* exact match, 80 and up) when there are any, else word starts (60) when
|
|
55
|
+
* there are any, else partial matches. A longer word that only starts with
|
|
56
|
+
* the query (`start` for `star`) never stands beside a whole-word answer.
|
|
57
|
+
* @template {{score: number}} T
|
|
58
|
+
* @param {T[]} ranked best first
|
|
59
|
+
* @returns {T[]}
|
|
60
|
+
*/
|
|
61
|
+
export function answeringMatches<T extends {
|
|
62
|
+
score: number;
|
|
63
|
+
}>(ranked: T[]): T[];
|
|
44
64
|
/**
|
|
45
65
|
* Which exact reading leads for a set of matches: meaning, unless more icons
|
|
46
66
|
* carry the query as a name than as a tag.
|
|
@@ -78,7 +98,9 @@ export function withDefinitionWords(icon: IconSetIcon, definitions: string[], lo
|
|
|
78
98
|
words: string[];
|
|
79
99
|
} | undefined): IconSetIcon;
|
|
80
100
|
/**
|
|
81
|
-
* Every icon in `sets` that answers the query, ranked
|
|
101
|
+
* Every icon in `sets` that answers the query, ranked. The query is
|
|
102
|
+
* scored as typed; when nothing answers it with a whole word (80 and up),
|
|
103
|
+
* it is scored again without its filler words ({@link iconQuery}).
|
|
82
104
|
* @param {DiscoveredIconSet[]} sets in tie order
|
|
83
105
|
* @param {string} query
|
|
84
106
|
* @returns {IconMatchEntry[]}
|
|
@@ -106,7 +128,9 @@ export type IconMatch = {
|
|
|
106
128
|
meaning: boolean;
|
|
107
129
|
/**
|
|
108
130
|
* within-tier key, lower first: for a name match 0
|
|
109
|
-
* (whole id)
|
|
131
|
+
* (whole id), 1 (the whole id's plural or singular), 2 (an id part), or 3
|
|
132
|
+
* (an id part's plural or singular); for a meaning match the tag's
|
|
133
|
+
* position
|
|
110
134
|
*/
|
|
111
135
|
rank: number;
|
|
112
136
|
reason: string;
|
|
@@ -9,8 +9,18 @@
|
|
|
9
9
|
* 90 an exact match in the reading the searched sets favor (below)
|
|
10
10
|
* 80 an exact match in the other reading
|
|
11
11
|
* 60 one of its words starts with the query
|
|
12
|
-
* 40 a partial match: the id or a word contains the query
|
|
13
|
-
* word of the query starts one of the
|
|
12
|
+
* 40 a partial match: the id or a word contains the query (three
|
|
13
|
+
* letters or more), or every word of the query starts one of the
|
|
14
|
+
* icon's words or id parts
|
|
15
|
+
*
|
|
16
|
+
* A word matches a whole word: a word's plural or singular (`stars` for
|
|
17
|
+
* `star`) is an exact match, while a longer word that only starts with it
|
|
18
|
+
* (`start`) is a word-start match, and {@link answeringMatches} keeps
|
|
19
|
+
* word-start and partial matches only when nothing matches a whole word.
|
|
20
|
+
* A query is scored as typed first. Only when nothing answers it with a
|
|
21
|
+
* whole word are the words that only phrase it (`go`, `the`, `icon`) left
|
|
22
|
+
* out of a query of several words, so `go home` asks for `home` while
|
|
23
|
+
* `arrow up to line` still names `arrow-up-to-line`.
|
|
14
24
|
*
|
|
15
25
|
* An exact match is a NAME match when the query is the icon's id, or one of
|
|
16
26
|
* its words that is also a part of its id (`trash` for `trash-2`); it is a
|
|
@@ -26,7 +36,8 @@
|
|
|
26
36
|
*
|
|
27
37
|
* Equal scores keep the set order first (the project's set, then load
|
|
28
38
|
* order), so the project's set always leads at a score. Within one set, a
|
|
29
|
-
* name match on the whole id
|
|
39
|
+
* name match on the whole id (or its plural or singular) comes before one on
|
|
40
|
+
* a part of it, and a meaning
|
|
30
41
|
* match whose query sits earlier among the icon's tags comes first (sets list
|
|
31
42
|
* an icon's most telling tags first). Remaining ties keep id order, so the
|
|
32
43
|
* same installation always answers the same way.
|
|
@@ -93,7 +104,9 @@ const squash = value => value.toLowerCase().replace(/[\s_-]+/g, '');
|
|
|
93
104
|
* @property {boolean} meaning the query is one of the icon's tags (never
|
|
94
105
|
* both: a tag is a word that is not the id or one of its parts)
|
|
95
106
|
* @property {number} rank within-tier key, lower first: for a name match 0
|
|
96
|
-
* (whole id)
|
|
107
|
+
* (whole id), 1 (the whole id's plural or singular), 2 (an id part), or 3
|
|
108
|
+
* (an id part's plural or singular); for a meaning match the tag's
|
|
109
|
+
* position
|
|
97
110
|
* @property {string} reason
|
|
98
111
|
*/
|
|
99
112
|
|
|
@@ -127,10 +140,27 @@ export function matchIcon(query, icon, definitions = []) {
|
|
|
127
140
|
tier: 'exact',
|
|
128
141
|
name: wholeId || partWord,
|
|
129
142
|
meaning: !wholeId && !partWord,
|
|
130
|
-
rank: wholeId ? 0 : partWord ?
|
|
143
|
+
rank: wholeId ? 0 : partWord ? 2 : tagAt,
|
|
131
144
|
reason: wholeId ? `id "${icon.id}"` : `word "${query}"`,
|
|
132
145
|
};
|
|
133
146
|
}
|
|
147
|
+
const plural = icon.words.find(word => sameWord(word, query));
|
|
148
|
+
if (plural) {
|
|
149
|
+
const named = parts.has(plural);
|
|
150
|
+
return {
|
|
151
|
+
tier: 'exact',
|
|
152
|
+
name: named,
|
|
153
|
+
meaning: !named,
|
|
154
|
+
// The whole id's plural or singular right after the whole id, then
|
|
155
|
+
// after the word itself, wherever it sits.
|
|
156
|
+
rank: named
|
|
157
|
+
? plural === icon.id
|
|
158
|
+
? 1
|
|
159
|
+
: 3
|
|
160
|
+
: tags.length + tags.indexOf(plural),
|
|
161
|
+
reason: `word "${plural}"`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
134
164
|
const prefix = icon.words.find(word => word.startsWith(query));
|
|
135
165
|
if (prefix) {
|
|
136
166
|
return {
|
|
@@ -141,7 +171,11 @@ export function matchIcon(query, icon, definitions = []) {
|
|
|
141
171
|
reason: `word "${prefix}" starts with "${query}"`,
|
|
142
172
|
};
|
|
143
173
|
}
|
|
144
|
-
|
|
174
|
+
// A letter or two inside a word (`x` in `box`) says nothing about it.
|
|
175
|
+
const contains =
|
|
176
|
+
query.replace(/\s+/gu, '').length >= 3
|
|
177
|
+
? [icon.id, ...icon.words].find(text => text.includes(query))
|
|
178
|
+
: undefined;
|
|
145
179
|
if (contains) {
|
|
146
180
|
return {
|
|
147
181
|
tier: 'partial',
|
|
@@ -170,6 +204,72 @@ export function matchIcon(query, icon, definitions = []) {
|
|
|
170
204
|
return null;
|
|
171
205
|
}
|
|
172
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Whether two words are one word's singular and plural (`star`, `stars`;
|
|
209
|
+
* `box`, `boxes`), never the same word.
|
|
210
|
+
* @param {string} a
|
|
211
|
+
* @param {string} b
|
|
212
|
+
*/
|
|
213
|
+
function sameWord(a, b) {
|
|
214
|
+
if (a === b || /\s/u.test(a) || /\s/u.test(b)) return false;
|
|
215
|
+
const [short, long] = a.length < b.length ? [a, b] : [b, a];
|
|
216
|
+
return short.length >= 2 && (long === `${short}s` || long === `${short}es`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Words that phrase a query without naming anything: left out of a query of
|
|
221
|
+
* several words that nothing answers as typed (`go home` asks for `home`).
|
|
222
|
+
*/
|
|
223
|
+
const FILLER_WORDS = new Set([
|
|
224
|
+
'a',
|
|
225
|
+
'an',
|
|
226
|
+
'the',
|
|
227
|
+
'go',
|
|
228
|
+
'to',
|
|
229
|
+
'my',
|
|
230
|
+
'of',
|
|
231
|
+
'for',
|
|
232
|
+
'icon',
|
|
233
|
+
'icons',
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A query as typed: trimmed and lower-case, one space between words.
|
|
238
|
+
* @param {string} query
|
|
239
|
+
*/
|
|
240
|
+
function typedQuery(query) {
|
|
241
|
+
return query.trim().toLowerCase().split(/\s+/u).filter(Boolean).join(' ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The query without the words that only phrase it, when it has several
|
|
246
|
+
* words and some word names something; else the query as typed.
|
|
247
|
+
* @param {string} query
|
|
248
|
+
*/
|
|
249
|
+
export function iconQuery(query) {
|
|
250
|
+
const words = typedQuery(query).split(' ').filter(Boolean);
|
|
251
|
+
const named = words.filter(word => !FILLER_WORDS.has(word));
|
|
252
|
+
return (words.length > 1 && named.length > 0 ? named : words).join(' ');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The matches that answer a query: the whole-word ones (a definition or an
|
|
257
|
+
* exact match, 80 and up) when there are any, else word starts (60) when
|
|
258
|
+
* there are any, else partial matches. A longer word that only starts with
|
|
259
|
+
* the query (`start` for `star`) never stands beside a whole-word answer.
|
|
260
|
+
* @template {{score: number}} T
|
|
261
|
+
* @param {T[]} ranked best first
|
|
262
|
+
* @returns {T[]}
|
|
263
|
+
*/
|
|
264
|
+
export function answeringMatches(ranked) {
|
|
265
|
+
for (const floor of [80, 60]) {
|
|
266
|
+
if (ranked.some(each => each.score >= floor)) {
|
|
267
|
+
return ranked.filter(each => each.score >= floor);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return ranked;
|
|
271
|
+
}
|
|
272
|
+
|
|
173
273
|
/**
|
|
174
274
|
* The score of one match, given which exact reading leads.
|
|
175
275
|
* @param {IconMatch} match
|
|
@@ -249,13 +349,30 @@ export function withDefinitionWords(icon, definitions, lookup) {
|
|
|
249
349
|
}
|
|
250
350
|
|
|
251
351
|
/**
|
|
252
|
-
* Every icon in `sets` that answers the query, ranked
|
|
352
|
+
* Every icon in `sets` that answers the query, ranked. The query is
|
|
353
|
+
* scored as typed; when nothing answers it with a whole word (80 and up),
|
|
354
|
+
* it is scored again without its filler words ({@link iconQuery}).
|
|
253
355
|
* @param {DiscoveredIconSet[]} sets in tie order
|
|
254
356
|
* @param {string} query
|
|
255
357
|
* @returns {IconMatchEntry[]}
|
|
256
358
|
*/
|
|
257
359
|
export function rankIcons(sets, query) {
|
|
258
|
-
const
|
|
360
|
+
const typed = typedQuery(query);
|
|
361
|
+
const asTyped = rankTerm(sets, typed);
|
|
362
|
+
if (asTyped.some(each => each.score >= 80)) return asTyped;
|
|
363
|
+
const named = iconQuery(query);
|
|
364
|
+
if (named === typed) return asTyped;
|
|
365
|
+
const withoutFiller = rankTerm(sets, named);
|
|
366
|
+
return withoutFiller.length > 0 ? withoutFiller : asTyped;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* {@link rankIcons} for one normalized query.
|
|
371
|
+
* @param {DiscoveredIconSet[]} sets
|
|
372
|
+
* @param {string} term
|
|
373
|
+
* @returns {IconMatchEntry[]}
|
|
374
|
+
*/
|
|
375
|
+
function rankTerm(sets, term) {
|
|
259
376
|
const astryx = astryxDefinitionsByName();
|
|
260
377
|
/** @type {Array<{set: DiscoveredIconSet, icon: IconSetIcon, definitions: string[], match: IconMatch, setIndex: number}>} */
|
|
261
378
|
const hits = [];
|