xshell 1.1.9 → 1.1.10

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/prototype.d.ts CHANGED
@@ -71,8 +71,7 @@ declare global {
71
71
  surround_tag(this: string, tag_name: string): string;
72
72
  to_lf(this: string): string;
73
73
  /** 'xxx'.replace(/pattern/g, '')
74
- 如果 pattern 是 string 则在创建 RegExp 时自动加上 flags (默认 'g'), 否则忽略 flags
75
- */
74
+ 如果 pattern 是 string 则在创建 RegExp 时自动加上 flags (默认 'g'), 否则忽略 flags */
76
75
  rm(this: string, pattern: string | RegExp, flags?: string): string;
77
76
  readonly red: string;
78
77
  readonly red_: string;
@@ -127,17 +126,25 @@ declare global {
127
126
  slice_to(this: string, search: string, options?: SliceOptions): string;
128
127
  /** 等价于 .endsWith('/') */
129
128
  isdir: boolean;
130
- /** `/` 分割的路径,可能以 / 结尾 */
129
+ /** 转换为以 `/` 分割的路径,可能以 / 结尾 */
131
130
  fp: string;
132
- /** `/` 分割的文件夹路径,一定以 / 结尾 */
131
+ /** 转换为以 `/` 分割的文件夹路径,一定以 / 结尾 */
133
132
  fpd: string;
134
- /** 父文件夹路径 (path.dirname),一定以 `/` 结尾 */
133
+ /** 父文件夹路径,一定以 `/` 结尾,或为空
134
+ 特殊情况:
135
+ - '/'.fdir === ''
136
+ - 'D:/'.fdir === '' */
135
137
  fdir: string;
136
- /** 文件名 (path.basename 的结果), 保留结尾的 /,如:
138
+ /** 文件或文件夹名, 保留结尾的 /
139
+ 常规情况:
137
140
  - D:/0/aaa.txt -> aaa.txt
138
- - D:/aaa/ -> aaa/ */
141
+ - D:/aaa/ -> aaa/
142
+ 特殊情况:
143
+ - '/'.fname === '/'
144
+ - 'D:/'.fname === 'D:/' */
139
145
  fname: string;
140
- /** .txt */
146
+ /** 文件后缀名,不带点,如: txt, zip,没有后缀时返回空字符串
147
+ 特殊情况: .aaa 的 fext 为 '' */
141
148
  fext: string;
142
149
  to_backslash(this: string): string;
143
150
  }
@@ -173,15 +180,15 @@ declare global {
173
180
  log(this: string[], limit?: number): void;
174
181
  indent(this: string[], width?: number, c?: string): string[];
175
182
  indent2to4(this: string[]): string[];
176
- /** 对数组中所有元素求和 (+), 返回结果,可传入 selector 选择某个属性,或者计算出某个值,用作求和 */
177
- sum<TReturn = T>(this: T[], selector?: keyof T | KeySelector<T>): TReturn;
178
- /** 查找数组中最大的元素,可传入 selector 选择某个属性,或者计算出某个值,用作大小比较 */
179
- max(this: T[], selector?: keyof T | KeySelector<T>): T;
180
- /** 查找数组中最小的元素,可传入 selector 选择某个属性,或者计算出某个值,用作大小比较 */
181
- min(this: T[], selector?: keyof T | KeySelector<T>): T;
182
- /** 去除重复元素(可按 selector 去重),重复值保留最后出现的那个
183
- - selector?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
184
- unique(this: T[], selector?: keyof T | KeySelector<T>): T[];
183
+ /** 对数组中所有元素求和 (+), 返回结果,可传入 mapper 计算出某个值,用作求和 */
184
+ sum<TReturn = T>(this: T[], mapper?: keyof T | Mapper<T>): TReturn;
185
+ /** 查找数组中最大的元素,可传入 mapper 计算出某个值,用作大小比较 */
186
+ max(this: T[], mapper?: keyof T | Mapper<T>): T;
187
+ /** 查找数组中最小的元素,可传入 mapper 计算出某个值,用作大小比较 */
188
+ min(this: T[], mapper?: keyof T | Mapper<T>): T;
189
+ /** 去除重复元素(可按 mapper 选择或计算某个值来去重),重复值保留最后出现的那个
190
+ - mapper?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
191
+ unique(this: T[], mapper?: keyof T | Mapper<T>): T[];
185
192
  /**
186
193
  - trim_line?: `true`
187
194
  - rm_empty_lines?: `true`
@@ -209,7 +216,7 @@ declare global {
209
216
  toJSON(this: Error): string;
210
217
  }
211
218
  interface Set<T> {
212
- map<TResult>(mapfn: (value: T, index: number) => TResult): TResult[];
219
+ map<TResult>(mapper: (value: T, index: number) => TResult): TResult[];
213
220
  }
214
221
  }
215
222
  interface SliceOptions {
@@ -219,8 +226,8 @@ interface SliceOptions {
219
226
  export declare const emoji_regex: RegExp;
220
227
  export declare const noop: () => void;
221
228
  export declare const ident: <T>(x: T) => T;
222
- export declare const build_selector: <TObj>(key: keyof TObj) => (obj: TObj) => TObj[keyof TObj];
223
- export type KeySelector<TObj = any, TKey extends keyof TObj = keyof TObj> = (key: TObj) => TObj[TKey];
229
+ export declare const build_mapper: <TObj>(key: keyof TObj) => (obj: TObj) => TObj[keyof TObj];
230
+ export type Mapper<TObj = any, TKey extends keyof TObj = keyof TObj> = (obj: TObj) => TObj[TKey];
224
231
  /** value 不为 null 或 undefined */
225
232
  export declare const not_empty: (value: any) => boolean;
226
233
  export declare const empty: (value: any) => boolean;
package/prototype.js CHANGED
@@ -2,11 +2,10 @@ import util from 'util';
2
2
  import EmojiRegex from 'emoji-regex';
3
3
  export const emoji_regex = EmojiRegex();
4
4
  import strip_ansi from 'strip-ansi';
5
- import { to_fp, dirname, basename, extname } from "./path.js";
6
5
  import { t } from "./i18n/instance.js";
7
6
  export const noop = () => { };
8
7
  export const ident = (x) => x;
9
- export const build_selector = (key) => (obj) => obj[key];
8
+ export const build_mapper = (key) => (obj) => obj[key];
10
9
  /** value 不为 null 或 undefined */
11
10
  export const not_empty = (value) => value !== null && value !== undefined;
12
11
  export const empty = (value) => value === undefined || value === null;
@@ -247,9 +246,6 @@ if (!globalThis.my_prototype_defined) {
247
246
  to_lf() {
248
247
  return this.replace(/\r\n/g, '\n');
249
248
  },
250
- to_crlf() {
251
- return this.replace(/\n/g, '\r\n');
252
- },
253
249
  rm(pattern, flags = 'g') {
254
250
  if (typeof pattern === 'string')
255
251
  pattern = new RegExp(pattern, flags);
@@ -371,28 +367,47 @@ if (!globalThis.my_prototype_defined) {
371
367
  return this.endsWith('/');
372
368
  },
373
369
  fp() {
374
- return to_fp(this);
370
+ if (!this)
371
+ return this;
372
+ const fp = this.replaceAll('\\', '/');
373
+ // 转换小写盘符开头的路径
374
+ return fp[1] === ':' && 'a' <= fp[0] && fp[0] <= 'z'
375
+ ? fp[0].toUpperCase() + fp.slice(1)
376
+ : fp;
375
377
  },
376
378
  fpd() {
377
- const fp = to_fp(this);
379
+ const { fp } = this;
378
380
  return fp.endsWith('/') ? fp : `${fp}/`;
379
381
  },
380
382
  fdir() {
381
- const fpd = dirname(this);
382
- // 有可能 fpd 是 '/'
383
- return fpd.endsWith('/') ? fpd : `${fpd}/`;
383
+ return this.strip_end(this.fname);
384
384
  },
385
385
  fname() {
386
- return `${basename(this)}${this.endsWith('/') ? '/' : ''}`;
386
+ const ilast = this.lastIndexOf('/');
387
+ if (ilast === -1)
388
+ return this; // 没有斜杠时返回整个字符串
389
+ // 以斜杠结尾的情况
390
+ if (ilast === this.length - 1) {
391
+ const iprev = this.lastIndexOf('/', ilast - 1);
392
+ return iprev === -1
393
+ ? this // 只有一个斜杠且在末尾
394
+ : this.slice(iprev + 1);
395
+ }
396
+ // 返回最后一个斜杠后的内容
397
+ return this.slice(ilast + 1);
387
398
  },
388
399
  fext() {
389
- return extname(this);
390
- },
400
+ const { fname } = this;
401
+ const index = fname.lastIndexOf('.');
402
+ return index <= 0
403
+ ? ''
404
+ : fname.slice(index + 1);
405
+ }
391
406
  }),
392
407
  ...to_method_property_descriptors({
393
408
  to_backslash() {
394
409
  return this.replaceAll('/', '\\');
395
- },
410
+ }
396
411
  })
397
412
  });
398
413
  // ------------------------------------ Date.prototype
@@ -449,10 +464,9 @@ if (!globalThis.my_prototype_defined) {
449
464
  return String(hour).padStart(2, '0') + splitter +
450
465
  String(date.getMinutes()).padStart(2, '0') + splitter +
451
466
  String(date.getSeconds()).padStart(2, '0') +
452
- (ms ?
453
- '.' + String(date.getMilliseconds()).padStart(3, '0')
454
- :
455
- '');
467
+ (ms
468
+ ? '.' + String(date.getMilliseconds()).padStart(3, '0')
469
+ : '');
456
470
  }
457
471
  // ------------------------------------ Number.prototype
458
472
  Object.defineProperties(Number.prototype, to_method_property_descriptors({
@@ -528,27 +542,27 @@ if (!globalThis.my_prototype_defined) {
528
542
  return this.split_indents()
529
543
  .map(line => ' '.repeat(line.indent * 2) + line.text);
530
544
  },
531
- sum(selector) {
545
+ sum(mapper) {
532
546
  if (!this.length)
533
547
  return undefined;
534
548
  // 快捷路径
535
549
  const first = this[0];
536
- if ((typeof first === 'number' || typeof first === 'bigint') && !selector)
550
+ if ((typeof first === 'number' || typeof first === 'bigint') && !mapper)
537
551
  return this.reduce((acc, x) => acc + x, first);
538
- if (is_key_type(selector))
539
- selector = build_selector(selector);
540
- selector ??= ident;
541
- return this.reduce((acc, x) => acc + selector(x), selector(first));
552
+ if (is_key_type(mapper))
553
+ mapper = build_mapper(mapper);
554
+ mapper ??= ident;
555
+ return this.reduce((acc, x) => acc + mapper(x), mapper(first));
542
556
  },
543
- max(selector = ident) {
557
+ max(mapper = ident) {
544
558
  if (!this.length)
545
559
  return undefined;
546
- if (is_key_type(selector))
547
- selector = build_selector(selector);
548
- let max = selector(this[0]);
560
+ if (is_key_type(mapper))
561
+ mapper = build_mapper(mapper);
562
+ let max = mapper(this[0]);
549
563
  let imax = 0;
550
564
  for (let i = 0; i < this.length; i++) {
551
- const value = selector(this[i]);
565
+ const value = mapper(this[i]);
552
566
  if (value > max) {
553
567
  max = value;
554
568
  imax = i;
@@ -556,15 +570,15 @@ if (!globalThis.my_prototype_defined) {
556
570
  }
557
571
  return this[imax];
558
572
  },
559
- min(selector = ident) {
573
+ min(mapper = ident) {
560
574
  if (!this.length)
561
575
  return undefined;
562
- if (is_key_type(selector))
563
- selector = build_selector(selector);
564
- let min = selector(this[0]);
576
+ if (is_key_type(mapper))
577
+ mapper = build_mapper(mapper);
578
+ let min = mapper(this[0]);
565
579
  let imin = 0;
566
580
  for (let i = 0; i < this.length; i++) {
567
- const value = selector(this[i]);
581
+ const value = mapper(this[i]);
568
582
  if (value < min) {
569
583
  min = value;
570
584
  imin = i;
@@ -572,14 +586,14 @@ if (!globalThis.my_prototype_defined) {
572
586
  }
573
587
  return this[imin];
574
588
  },
575
- unique(selector) {
576
- if (!selector)
589
+ unique(mapper) {
590
+ if (!mapper)
577
591
  return [...new Set(this)];
578
- if (is_key_type(selector))
579
- selector = build_selector(selector);
592
+ if (is_key_type(mapper))
593
+ mapper = build_mapper(mapper);
580
594
  let map = new Map();
581
595
  for (const x of this)
582
- map.set(selector(x), x);
596
+ map.set(mapper(x), x);
583
597
  return [...map.values()];
584
598
  },
585
599
  join_lines(append = true) {
package/server.d.ts CHANGED
@@ -3,6 +3,15 @@ import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Header
3
3
  import type { Duplex } from 'stream';
4
4
  import type { WebSocketServer } from 'ws';
5
5
  import { default as Koa, type Context, type Next } from 'koa';
6
+ import { Remote, type RequestOptions, type RawResponse } from './net.ts';
7
+ declare module 'http' {
8
+ interface IncomingMessage {
9
+ body?: Buffer;
10
+ }
11
+ interface ServerResponse {
12
+ body?: Buffer;
13
+ }
14
+ }
6
15
  declare module 'koa' {
7
16
  interface Request {
8
17
  /** 经过 decodeURIComponent 后,在路径重写之前的路径 */
@@ -13,15 +22,6 @@ declare module 'koa' {
13
22
  compress: boolean;
14
23
  }
15
24
  }
16
- import { Remote, type RequestOptions, type RawResponse } from './net.ts';
17
- declare module 'http' {
18
- interface IncomingMessage {
19
- body?: Buffer;
20
- }
21
- interface ServerResponse {
22
- body?: Buffer;
23
- }
24
- }
25
25
  export declare class Server {
26
26
  /** proxy 时需要丢弃的 resposne headers */
27
27
  static drop_response_headers: Set<string>;
@@ -33,9 +33,10 @@ export declare class Server {
33
33
  };
34
34
  /** sea 下最后修改时间,用于 http 资源缓存 */
35
35
  last_modified_str?: string;
36
- js_exts: Set<string>;
37
- empty_body_statuses: Set<number>;
38
- empty_body_methods: Set<string>;
36
+ static js_exts: Set<string>;
37
+ static logger_ignore_fexts: Set<string>;
38
+ static empty_body_statuses: Set<number>;
39
+ static empty_body_methods: Set<string>;
39
40
  app: Koa;
40
41
  handler: ReturnType<Koa['callback']>;
41
42
  /** 启用 http server */
package/server.js CHANGED
@@ -37,9 +37,10 @@ export class Server {
37
37
  };
38
38
  /** sea 下最后修改时间,用于 http 资源缓存 */
39
39
  last_modified_str;
40
- js_exts = new Set(['.js', '.mjs', '.cjs']);
41
- empty_body_statuses = new Set([304, 204, 205]);
42
- empty_body_methods = new Set(['HEAD', 'OPTIONS']);
40
+ static js_exts = new Set(['.js', '.mjs', '.cjs']);
41
+ static logger_ignore_fexts = new Set(['.js', '.css', '.map', '.png', '.jpg', '.svg', '.ico', '.json', '.woff2', '.ttf', '.php']);
42
+ static empty_body_statuses = new Set([304, 204, 205]);
43
+ static empty_body_methods = new Set(['HEAD', 'OPTIONS']);
43
44
  app;
44
45
  handler;
45
46
  /** 启用 http server */
@@ -384,6 +385,8 @@ export class Server {
384
385
  const { request } = ctx;
385
386
  const { query: queries, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, headers, } = request;
386
387
  let { method } = request;
388
+ if (Server.logger_ignore_fexts.has(path.fext))
389
+ return;
387
390
  let s = '';
388
391
  // 时间
389
392
  s += `${this.log_date ? new Date().to_str() : new Date().to_time_str()} `;
@@ -522,7 +525,7 @@ export class Server {
522
525
  // 关联了 response_.body 到 res 的 finish 事件,(koa/response.js#set body() 中 onFinish(res, destroy.bind(null, response_.body))
523
526
  // 会让 response_.body 这个 UndiciBodyReadable 被提前 finish,而此时
524
527
  // 流还没 emit end, 会触发错误 RequestAbortedError [AbortError]: Request aborted
525
- if (this.empty_body_statuses.has(response_.status) || this.empty_body_methods.has(method))
528
+ if (Server.empty_body_statuses.has(response_.status) || Server.empty_body_methods.has(method))
526
529
  consume_stream(response_.body, true);
527
530
  else
528
531
  response.body = response_.body;
@@ -652,8 +655,9 @@ export class Server {
652
655
  response.set('content-disposition', `attachment; filename="${encodeURIComponent(fp.fname)}"`);
653
656
  if (!response.get('content-type')) {
654
657
  const { fext } = fp;
655
- response.set('content-type', (this.js_exts.has(fext) ? 'text/javascript; chatset=utf-8' : get_content_type(fext))
656
- || 'application/octet-stream');
658
+ response.set('content-type', (Server.js_exts.has(fext)
659
+ ? 'text/javascript; chatset=utf-8'
660
+ : get_content_type(`file.${fext}`) || 'application/octet-stream'));
657
661
  }
658
662
  const modified_since_str = request.get('if-modified-since');
659
663
  if ((method === 'GET' || method === 'HEAD') && modified_since_str === last_modified_str) {
package/storage.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export declare let storage: {
2
2
  /** 通过 key 获取字符串类型的值,不存在时返回空字符串 ('') */
3
- getstr(key: string): string;
3
+ getstr<TString extends string = string>(key: string): TString;
4
4
  /** 将字符串类型的值保存到 key */
5
5
  setstr(key: string, value?: string): void;
6
6
  /** 根据 key 获取 JSON 类型的值,不存在时返回通过第二个参数传入的默认值,默认为 null */
7
7
  get<TValue = any>(key: string, _default?: TValue): TValue;
8
- /** 保存 JSON 类型的值到 key */
9
- set(key: string, value: any): void;
8
+ /** 保存 JSON 类型的值到 key, 并返回 value */
9
+ set<TValue>(key: string, value: TValue): TValue;
10
10
  list(): string[];
11
11
  delete(key: string): void;
12
12
  };
package/storage.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export let storage = {
2
2
  /** 通过 key 获取字符串类型的值,不存在时返回空字符串 ('') */
3
3
  getstr(key) {
4
- return localStorage.getItem(key) || '';
4
+ return (localStorage.getItem(key) || '');
5
5
  },
6
6
  /** 将字符串类型的值保存到 key */
7
7
  setstr(key, value = '') {
@@ -14,9 +14,10 @@ export let storage = {
14
14
  ? _default
15
15
  : JSON.parse(strvalue);
16
16
  },
17
- /** 保存 JSON 类型的值到 key */
17
+ /** 保存 JSON 类型的值到 key, 并返回 value */
18
18
  set(key, value) {
19
19
  localStorage.setItem(key, JSON.stringify(value));
20
+ return value;
20
21
  },
21
22
  list() {
22
23
  return Object.keys(localStorage);
@@ -1,16 +1,16 @@
1
- import { type KeySelector } from './prototype.browser.ts';
2
- export declare function assert(assertion: any, message?: string): never | void;
1
+ import { type Mapper } from './prototype.browser.ts';
2
+ export declare function assert<T>(assertion: T, message?: string): T;
3
3
  /** 做参数校验,逻辑检查 */
4
- export declare function check(condition: any, message?: string): never | void;
4
+ export declare function check<T>(condition: T, message?: string): T;
5
5
  /** 通过 console.log 打印对象并返回
6
6
  @example
7
7
  log('label', obj)
8
8
  log(obj) */
9
9
  export declare function log<TObj>(obj: TObj): TObj;
10
10
  export declare function log<TObj>(label: string, obj: TObj): TObj;
11
- /** 数组或 iterable 去重(可按 selector 去重),重复值保留最后出现的那个
12
- - selector?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
13
- export declare function unique<TObj>(iterable: TObj[] | Iterable<TObj>, selector?: keyof TObj | KeySelector<TObj>): TObj[];
11
+ /** 数组或 iterable 去重(可按 mapper 选择或计算某个值来去重),重复值保留最后出现的那个
12
+ - mapper?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
13
+ export declare function unique<TObj>(iterable: TObj[] | Iterable<TObj>, mapper?: keyof TObj | Mapper<TObj>): TObj[];
14
14
  /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
15
15
  @example seq(10, i => `item-${i}`) */
16
16
  export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
@@ -79,11 +79,16 @@ export declare function encode(str: string): Uint8Array<ArrayBufferLike>;
79
79
  在流式处理 (buffer 可能不完整) 时,应使用独立的 TextDecoder 实例调用 decode(buffer, { stream: true }) */
80
80
  export declare function decode(buffer: Uint8Array): string;
81
81
  /** 字符串字典序比较 */
82
- export declare function strcmp(l: string, r: string): 0 | 1 | -1;
82
+ export declare function strcmp(l: string, r: string): 1 | 0 | -1;
83
83
  /** 比较 1.10.02 这种版本号
84
84
  - l, r: 两个版本号字符串
85
85
  - loose?: 宽松模式,允许两个版本号格式(位数)不一致 */
86
86
  export declare function vercmp(l: string, r: string, loose?: boolean): number;
87
+ /** 模糊过滤字符串列表或对象列表,常用于根据用户输入补全或搜索过滤
88
+ - query: 查询字符串,要求为全小写
89
+ - list: 要过滤的列表
90
+ - list_lower?: 要过滤的列表对应的全小写字符串列表形式,传入时可复用缓存,加快搜索速度 */
91
+ export declare function fuzzyfilter<TItem = string>(query: string, list: TItem[], mapper?: keyof TItem | Mapper<TItem>, list_lower?: string[], single_char_startswith?: boolean): TItem[];
87
92
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
88
93
  export declare function global_get<TReturn = any>(keypath: string): TReturn;
89
94
  export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
package/utils.browser.js CHANGED
@@ -1,17 +1,19 @@
1
- import { is_key_type, build_selector, not_empty } from "./prototype.browser.js";
1
+ import { is_key_type, build_mapper, not_empty, ident } from "./prototype.browser.js";
2
2
  import { t } from "./i18n/instance.js";
3
3
  export function assert(assertion, message) {
4
4
  if (!assertion) {
5
5
  debugger;
6
- throw new Error(`断言失败: ${message ? `${message}: ` : ''}`);
6
+ throw new Error(`${t('断言失败')}: ${message ? `${message}: ` : ''}`);
7
7
  }
8
+ return assertion;
8
9
  }
9
10
  /** 做参数校验,逻辑检查 */
10
11
  export function check(condition, message) {
11
12
  if (!condition) {
12
13
  debugger;
13
- throw new Error(message || '检查失败');
14
+ throw new Error(message || t('检查失败'));
14
15
  }
16
+ return condition;
15
17
  }
16
18
  export function log(...args) {
17
19
  if (args.length === 2) {
@@ -25,16 +27,16 @@ export function log(...args) {
25
27
  return obj;
26
28
  }
27
29
  }
28
- /** 数组或 iterable 去重(可按 selector 去重),重复值保留最后出现的那个
29
- - selector?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
30
- export function unique(iterable, selector) {
31
- if (!selector)
30
+ /** 数组或 iterable 去重(可按 mapper 选择或计算某个值来去重),重复值保留最后出现的那个
31
+ - mapper?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
32
+ export function unique(iterable, mapper) {
33
+ if (!mapper)
32
34
  return [...new Set(iterable)];
33
- if (is_key_type(selector))
34
- selector = build_selector(selector);
35
+ if (is_key_type(mapper))
36
+ mapper = build_mapper(mapper);
35
37
  let map = new Map();
36
38
  for (const x of iterable)
37
- map.set(selector(x), x);
39
+ map.set(mapper(x), x);
38
40
  return [...map.values()];
39
41
  }
40
42
  /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
@@ -260,6 +262,33 @@ export function vercmp(l, r, loose = false) {
260
262
  // loose 下按短的优先,否则应该一样,为 0
261
263
  return lparts.length - rparts.length;
262
264
  }
265
+ /** 模糊过滤字符串列表或对象列表,常用于根据用户输入补全或搜索过滤
266
+ - query: 查询字符串,要求为全小写
267
+ - list: 要过滤的列表
268
+ - list_lower?: 要过滤的列表对应的全小写字符串列表形式,传入时可复用缓存,加快搜索速度 */
269
+ export function fuzzyfilter(query, list, mapper = ident, list_lower, single_char_startswith = false) {
270
+ if (!query)
271
+ return list;
272
+ if (is_key_type(mapper))
273
+ mapper = build_mapper(mapper);
274
+ mapper ??= ident;
275
+ list_lower ??= list.map(item => mapper(item).toLowerCase());
276
+ if (single_char_startswith && query.length === 1) {
277
+ const c = query[0];
278
+ return list.filter((_, i) => list_lower[i].startsWith(c));
279
+ }
280
+ const query_lower = query.toLowerCase();
281
+ return list.filter((_, i) => {
282
+ const str_lower = list_lower[i];
283
+ let j = 0;
284
+ for (const c of query_lower) {
285
+ j = str_lower.indexOf(c, j) + 1;
286
+ if (!j) // 找不到则 j === 0
287
+ return false;
288
+ }
289
+ return true;
290
+ });
291
+ }
263
292
  export function get(obj, keypath) {
264
293
  let obj_ = obj;
265
294
  for (const key of keypath.split('.'))
package/utils.d.ts CHANGED
@@ -2,22 +2,22 @@ import { Writable, Transform, type Readable, type Duplex, type TransformCallback
2
2
  import util from 'util';
3
3
  import type { TimerOptions } from 'timers';
4
4
  import type Vinyl from 'vinyl';
5
- import { type KeySelector } from './prototype.ts';
5
+ import { type Mapper } from './prototype.ts';
6
6
  /** `230` term 字符宽度 (实际上有 240) */
7
7
  export declare const output_width = 230;
8
8
  export declare function set_inspect_options(colors?: boolean): void;
9
- export declare function assert(assertion: any, message?: string): never | void;
9
+ export declare function assert<T>(assertion: T, message?: string): T;
10
10
  /** 做参数校验,逻辑检查 */
11
- export declare function check(condition: any, message?: string): never | void;
11
+ export declare function check<T>(condition: T, message?: string): T;
12
12
  /** 通过 console.log 打印对象并返回
13
13
  @example
14
14
  log('label', obj)
15
15
  log(obj) */
16
16
  export declare function log<TObj>(obj: TObj): TObj;
17
17
  export declare function log<TObj>(label: string, obj: TObj): TObj;
18
- /** 数组或 iterable 去重(可按 selector 去重),重复值保留最后出现的那个
19
- - selector?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
20
- export declare function unique<TObj>(iterable: TObj[] | Iterable<TObj>, selector?: keyof TObj | KeySelector<TObj>): TObj[];
18
+ /** 数组或 iterable 去重(可按 mapper 选择或计算某个值来去重),重复值保留最后出现的那个
19
+ - mapper?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
20
+ export declare function unique<TObj>(iterable: TObj[] | Iterable<TObj>, mapper?: keyof TObj | Mapper<TObj>): TObj[];
21
21
  /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
22
22
  @example seq(10, i => `item-${i}`) */
23
23
  export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
@@ -40,11 +40,16 @@ export declare function filter_values<TObj extends Record<string, any>>(obj: TOb
40
40
  /** 忽略对象中的 keys, 返回新对象 */
41
41
  export declare function omit<TObj>(obj: TObj, omit_keys: string[]): TObj;
42
42
  /** 字符串字典序比较 */
43
- export declare function strcmp(l: string, r: string): 0 | 1 | -1;
43
+ export declare function strcmp(l: string, r: string): 1 | 0 | -1;
44
44
  /** 比较 1.10.02 这种版本号
45
45
  - l, r: 两个版本号字符串
46
46
  - loose?: 宽松模式,允许两个版本号格式(位数)不一致 */
47
47
  export declare function vercmp(l: string, r: string, loose?: boolean): number;
48
+ /** 模糊过滤字符串列表或对象列表,常用于根据用户输入补全或搜索过滤
49
+ - query: 查询字符串,要求为全小写
50
+ - list: 要过滤的列表
51
+ - list_lower?: 要过滤的列表对应的全小写字符串列表形式,传入时可复用缓存,加快搜索速度 */
52
+ export declare function fuzzyfilter<TItem = string>(query: string, list: TItem[], mapper?: keyof TItem | Mapper<TItem>, list_lower?: string[], single_char_startswith?: boolean): TItem[];
48
53
  export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
49
54
  export declare function global_get<TReturn = any>(keypath: string): TReturn;
50
55
  export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
package/utils.js CHANGED
@@ -2,7 +2,7 @@ import { Stream, Writable, Transform } from 'stream';
2
2
  import util from 'util';
3
3
  import timers from 'timers/promises';
4
4
  import { t } from "./i18n/instance.js";
5
- import { build_selector, not_empty, is_key_type, noop } from "./prototype.js";
5
+ import { build_mapper, not_empty, is_key_type, noop, ident } from "./prototype.js";
6
6
  /** `230` term 字符宽度 (实际上有 240) */
7
7
  export const output_width = 230;
8
8
  export function set_inspect_options(colors = true) {
@@ -26,6 +26,7 @@ export function assert(assertion, message) {
26
26
  debugger;
27
27
  throw new Error(`${t('断言失败')}: ${message ? `${message}: ` : ''}`);
28
28
  }
29
+ return assertion;
29
30
  }
30
31
  /** 做参数校验,逻辑检查 */
31
32
  export function check(condition, message) {
@@ -33,6 +34,7 @@ export function check(condition, message) {
33
34
  debugger;
34
35
  throw new Error(message || t('检查失败'));
35
36
  }
37
+ return condition;
36
38
  }
37
39
  export function log(...args) {
38
40
  if (args.length === 2) {
@@ -46,16 +48,16 @@ export function log(...args) {
46
48
  return obj;
47
49
  }
48
50
  }
49
- /** 数组或 iterable 去重(可按 selector 去重),重复值保留最后出现的那个
50
- - selector?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
51
- export function unique(iterable, selector) {
52
- if (!selector)
51
+ /** 数组或 iterable 去重(可按 mapper 选择或计算某个值来去重),重复值保留最后出现的那个
52
+ - mapper?: 可以是 key (string, number, symbol) 或 (obj: any) => any */
53
+ export function unique(iterable, mapper) {
54
+ if (!mapper)
53
55
  return [...new Set(iterable)];
54
- if (is_key_type(selector))
55
- selector = build_selector(selector);
56
+ if (is_key_type(mapper))
57
+ mapper = build_mapper(mapper);
56
58
  let map = new Map();
57
59
  for (const x of iterable)
58
- map.set(selector(x), x);
60
+ map.set(mapper(x), x);
59
61
  return [...map.values()];
60
62
  }
61
63
  /** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
@@ -130,6 +132,33 @@ export function vercmp(l, r, loose = false) {
130
132
  // loose 下按短的优先,否则应该一样,为 0
131
133
  return lparts.length - rparts.length;
132
134
  }
135
+ /** 模糊过滤字符串列表或对象列表,常用于根据用户输入补全或搜索过滤
136
+ - query: 查询字符串,要求为全小写
137
+ - list: 要过滤的列表
138
+ - list_lower?: 要过滤的列表对应的全小写字符串列表形式,传入时可复用缓存,加快搜索速度 */
139
+ export function fuzzyfilter(query, list, mapper = ident, list_lower, single_char_startswith = false) {
140
+ if (!query)
141
+ return list;
142
+ if (is_key_type(mapper))
143
+ mapper = build_mapper(mapper);
144
+ mapper ??= ident;
145
+ list_lower ??= list.map(item => mapper(item).toLowerCase());
146
+ if (single_char_startswith && query.length === 1) {
147
+ const c = query[0];
148
+ return list.filter((_, i) => list_lower[i].startsWith(c));
149
+ }
150
+ const query_lower = query.toLowerCase();
151
+ return list.filter((_, i) => {
152
+ const str_lower = list_lower[i];
153
+ let j = 0;
154
+ for (const c of query_lower) {
155
+ j = str_lower.indexOf(c, j) + 1;
156
+ if (!j) // 找不到则 j === 0
157
+ return false;
158
+ }
159
+ return true;
160
+ });
161
+ }
133
162
  export function get(obj, keypath) {
134
163
  let obj_ = obj;
135
164
  for (const key of keypath.split('.'))