@anjianshi/utils 1.0.9 → 1.2.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/.eslintrc.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-env node */
2
2
  module.exports = {
3
3
  root: true,
4
- extends: ['./node_modules/@anjianshi/presets/eslint-base.js'],
4
+ extends: ['./node_modules/@anjianshi/presets-eslint-base/index.js'],
5
5
  rules: {},
6
6
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 通过此模块注册全局变量以便于调试
3
+ * 变量会注册到 window.app = {} 下
3
4
  */
4
5
  declare global {
5
6
  interface Window {
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 通过此模块注册全局变量以便于调试
3
+ * 变量会注册到 window.app = {} 下
3
4
  */
4
5
  // web-worker 环境下不存在 window 变量,无法执行注册
5
6
  let hasWindow = false;
@@ -1,3 +1,4 @@
1
+ import chalk from 'chalk';
1
2
  import { type Logger, type LogInfo, LogHandler } from '../logging/index.js';
2
3
  export * from '../logging/index.js';
3
4
  /**
@@ -24,14 +25,14 @@ export declare class ConsoleHandler extends LogHandler {
24
25
  };
25
26
  };
26
27
  static readonly levelColors: {
27
- 1: import("chalk").ChalkInstance;
28
- 2: import("chalk").ChalkInstance;
29
- 3: import("chalk").ChalkInstance;
30
- 4: import("chalk").ChalkInstance;
28
+ 1: chalk.Chalk;
29
+ 2: chalk.Chalk;
30
+ 3: chalk.Chalk;
31
+ 4: chalk.Chalk;
31
32
  };
32
33
  private static readonly loggerColors;
33
34
  private static readonly loggerColorMap;
34
- static getLoggerColor(logger: string): "green" | "yellow" | "blue" | "cyan" | "magenta" | "greenBright" | "yellowBright" | "blueBright" | "cyanBright" | "magentaBright";
35
+ static getLoggerColor(logger: string): "green" | "yellow" | "blue" | "magenta" | "cyan" | "greenBright" | "yellowBright" | "blueBright" | "magentaBright" | "cyanBright";
35
36
  }
36
37
  /**
37
38
  * 写入文件日志
@@ -47,7 +48,7 @@ export declare class FileHandler extends LogHandler {
47
48
  readonly options: FileHandlerOptions;
48
49
  constructor(options?: Partial<FileHandlerOptions>);
49
50
  log(info: LogInfo): void;
50
- protected formatDataItem(item: unknown): string;
51
+ protected stringifyDataItem(item: unknown): string;
51
52
  private buffer;
52
53
  private bufferSize;
53
54
  protected pushBuffer(...strings: string[]): void;
@@ -7,7 +7,6 @@ import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import chalk from 'chalk';
9
9
  import dayjs from 'dayjs';
10
- import { removeANSIColor } from '../lang/index.js';
11
10
  import { logger as defaultLogger, LogLevel, LogHandler, formatters, } from '../logging/index.js';
12
11
  export * from '../logging/index.js';
13
12
  /**
@@ -21,9 +20,9 @@ export class ConsoleHandler extends LogHandler {
21
20
  const levelName = formatters.level(info);
22
21
  const loggerColor = chalk[ConsoleHandler.getLoggerColor(logger)];
23
22
  const prefix = [
24
- chalk.white(`[${formatters.datetime(info)}]`),
23
+ chalk.white(`[${formatters.time(info)}]`),
25
24
  levelColor(`[${levelName}]`),
26
- logger ? loggerColor(`[${logger}]`) : '',
25
+ loggerColor(`[${logger}]`),
27
26
  ].join('');
28
27
  method(prefix, ...args);
29
28
  }
@@ -88,7 +87,7 @@ export class FileHandler extends LogHandler {
88
87
  const itemStrings = [];
89
88
  let totalLength = prefix.length;
90
89
  for (const item of args) {
91
- const itemString = this.formatDataItem(item);
90
+ const itemString = this.stringifyDataItem(item);
92
91
  // 截断过长的日志内容 Truncate overly long log messages
93
92
  if (totalLength + itemString.length < this.options.maxLength) {
94
93
  itemStrings.push((totalLength === prefix.length ? '' : ' ') + itemString);
@@ -101,9 +100,9 @@ export class FileHandler extends LogHandler {
101
100
  }
102
101
  this.pushBuffer(prefix, ...itemStrings, '\n');
103
102
  }
104
- formatDataItem(item) {
103
+ stringifyDataItem(item) {
105
104
  if (typeof item === 'string')
106
- return removeANSIColor(item);
105
+ return item;
107
106
  if (item instanceof Error)
108
107
  return item.stack ?? String(item);
109
108
  try {
package/init-dayjs.js CHANGED
@@ -1,8 +1,7 @@
1
- import dayjs from 'dayjs';
1
+ import { extend, locale } from 'dayjs';
2
2
  import objectSupport from 'dayjs/plugin/objectSupport.js';
3
3
  import 'dayjs/locale/zh-cn.js';
4
4
  export function initDayJs() {
5
- // dayjs 的类型标记错误,这两个方法只能通过 dayjs 对象访问,并不能直接引入
6
- dayjs.extend(objectSupport);
7
- dayjs.locale('zh-cn');
5
+ extend(objectSupport);
6
+ locale('zh-cn');
8
7
  }
package/lang/string.d.ts CHANGED
@@ -16,15 +16,10 @@ export declare function safeParseInt(value: string | number, fallback?: number,
16
16
  */
17
17
  export declare function safeParseFloat(value: string | number, fallback: number): number;
18
18
  /**
19
- * Return file size for human reading.
20
- * From:https://stackoverflow.com/a/14919494
19
+ * 返回人类可读的文件尺寸
20
+ * 来自:https://stackoverflow.com/a/14919494
21
21
  *
22
- * si: true for radix of 1024;false for radix of 1000(default: false)
23
- * dp: how many decimal places to keep(保留几位小数)
22
+ * si: true 则使用 1000 进制,否则 1024 进制(默认 false)
23
+ * dp: 保留几位小数
24
24
  */
25
25
  export declare function readableSize(bytes: number, si?: boolean, dp?: number): string;
26
- /**
27
- * 移除字符串中的 ASNI Color 修饰符
28
- * https://stackoverflow.com/a/7150870/2815178
29
- */
30
- export declare function removeANSIColor(string: string): string;
package/lang/string.js CHANGED
@@ -30,7 +30,6 @@ export function keywordCompare(keyword, target) {
30
30
  *
31
31
  * 以 0 开头是特例,例如 019 和 12 两个字符串,按直觉还是应该 019 在前面,毕竟本质还是字符串排序。
32
32
  *
33
- *
34
33
  * "123" "456" 数字排序
35
34
  * "123你好" "133我好" 字符串排序
36
35
  * "019" "12" 字符串排序
@@ -58,11 +57,11 @@ export function safeParseFloat(value, fallback) {
58
57
  return isFinite(raw) ? raw : fallback;
59
58
  }
60
59
  /**
61
- * Return file size for human reading.
62
- * From:https://stackoverflow.com/a/14919494
60
+ * 返回人类可读的文件尺寸
61
+ * 来自:https://stackoverflow.com/a/14919494
63
62
  *
64
- * si: true for radix of 1024;false for radix of 1000(default: false)
65
- * dp: how many decimal places to keep(保留几位小数)
63
+ * si: true 则使用 1000 进制,否则 1024 进制(默认 false)
64
+ * dp: 保留几位小数
66
65
  */
67
66
  export function readableSize(bytes, si = false, dp = 1) {
68
67
  const thresh = si ? 1000 : 1024;
@@ -79,12 +78,3 @@ export function readableSize(bytes, si = false, dp = 1) {
79
78
  } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
80
79
  return `${bytes.toFixed(dp)} ${units[u]}`;
81
80
  }
82
- /**
83
- * 移除字符串中的 ASNI Color 修饰符
84
- * https://stackoverflow.com/a/7150870/2815178
85
- */
86
- export function removeANSIColor(string) {
87
- return string.replace(
88
- // eslint-disable-next-line no-control-regex
89
- /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
90
- }
package/lang/types.d.ts CHANGED
@@ -62,12 +62,13 @@ export interface Success<T = void> {
62
62
  export interface Failed<ET = string> {
63
63
  success: false;
64
64
  error: ET;
65
+ code?: number | string;
65
66
  }
66
67
  export type MaySuccess<T = void, ET = string> = Success<T> | Failed<ET>;
67
68
  declare function success(): Success;
68
69
  declare function success<T>(data: T): Success<T>;
69
70
  export { success };
70
- export declare function failed<ET>(error: ET): Failed<ET>;
71
+ export declare function failed<ET>(error: ET, code?: number | string): Failed<ET>;
71
72
  /**
72
73
  * 若传入值为 success,格式化其 data;否则原样返回错误
73
74
  * 支持传入会返回 MaySuccess 的 Promise
package/lang/types.js CHANGED
@@ -18,8 +18,8 @@ function success(data) {
18
18
  return { success: true, data };
19
19
  }
20
20
  export { success };
21
- export function failed(error) {
22
- return { success: false, error };
21
+ export function failed(error, code) {
22
+ return { success: false, error, code };
23
23
  }
24
24
  function formatSuccess(item, formatter) {
25
25
  if ('then' in item)
@@ -2,4 +2,4 @@ import type { Debug } from 'debug';
2
2
  /**
3
3
  * 适配 debug package
4
4
  */
5
- export declare function adaptDebugLib(debugLib: Debug, enable?: string, logger?: import("./index.js").Logger): void;
5
+ export declare function adaptDebugLib(debugLib: Debug, enable?: string): void;
package/logging/adapt.js CHANGED
@@ -2,7 +2,7 @@ import { getLogger } from './index.js';
2
2
  /**
3
3
  * 适配 debug package
4
4
  */
5
- export function adaptDebugLib(debugLib, enable = '', logger = getLogger('3rd-library')) {
5
+ export function adaptDebugLib(debugLib, enable = '') {
6
6
  // 不在 localStorage 里记录 debugLib enable 状态,
7
7
  // 以解决 web worker 里读不到 localStorage 而无法启用 debugLib 日志的问题
8
8
  const emulate = {
@@ -30,12 +30,8 @@ export function adaptDebugLib(debugLib, enable = '', logger = getLogger('3rd-lib
30
30
  };
31
31
  Object.assign(debugLib, emulate);
32
32
  // 将 debugLib 日志转发给 logger
33
- debugLib.log = (initial, ...rest) => {
34
- // 移除开头的额外空格
35
- if (typeof initial === 'string')
36
- initial = initial.trimStart();
37
- logger.debug(initial, ...rest);
38
- };
33
+ const logger = getLogger('3rd-library');
34
+ debugLib.log = logger.debug.bind(logger);
39
35
  if (enable) {
40
36
  debugLib.enable(enable);
41
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anjianshi/utils",
3
- "version": "1.0.9",
3
+ "version": "1.2.0",
4
4
  "description": "Common JavaScript Utils",
5
5
  "homepage": "https://github.com/anjianshi/js-utils",
6
6
  "bugs": {
@@ -25,20 +25,19 @@
25
25
  },
26
26
  "main": "index.js",
27
27
  "dependencies": {
28
- "chalk": "^5.3.0",
29
28
  "dayjs": "^1.11.10",
30
29
  "lodash": "^4.17.21"
31
30
  },
32
31
  "devDependencies": {
33
- "@anjianshi/presets": "1.4.0",
32
+ "@anjianshi/presets-eslint-node": "^1.0.3",
33
+ "@anjianshi/presets-eslint-typescript": "^1.0.3",
34
+ "@anjianshi/presets-prettier": "^1.0.0",
35
+ "@anjianshi/presets-typescript": "^1.0.1",
34
36
  "@types/debug": "^4.1.9",
35
37
  "@types/lodash": "^4.14.199",
36
38
  "@types/node": "^20.8.6",
37
- "eslint": "^8.51.0",
38
- "prettier-not-stubborn": "^3.0.3",
39
- "typescript": "^5.3.2",
40
39
  "vconsole": "^3.15.1"
41
40
  },
42
41
  "eslintIgnore": [],
43
- "prettier": "@anjianshi/presets/prettierrc"
42
+ "prettier": "@anjianshi/presets-prettier/prettierrc"
44
43
  }
package/src/.eslintrc.cjs CHANGED
@@ -1,8 +1,4 @@
1
1
  /* eslint-env node */
2
2
  module.exports = {
3
- root: true,
4
- extends: [
5
- '../node_modules/@anjianshi/presets/eslint-base.js',
6
- '../node_modules/@anjianshi/presets/eslint-typescript.js',
7
- ],
3
+ extends: ['../node_modules/@anjianshi/presets-eslint-typescript/exclusive.js'],
8
4
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 通过此模块注册全局变量以便于调试
3
+ * 变量会注册到 window.app = {} 下
3
4
  */
4
5
 
5
6
  declare global {
@@ -1,4 +1,4 @@
1
1
  /* eslint-env node */
2
2
  module.exports = {
3
- extends: ['../../node_modules/@anjianshi/presets/eslint-node.js'],
3
+ extends: ['../../node_modules/@anjianshi/presets-eslint-node/exclusive.js'],
4
4
  }
@@ -7,7 +7,6 @@ import path from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import chalk from 'chalk'
9
9
  import dayjs from 'dayjs'
10
- import { removeANSIColor } from '../lang/index.js'
11
10
  import {
12
11
  logger as defaultLogger,
13
12
  type Logger,
@@ -30,9 +29,9 @@ export class ConsoleHandler extends LogHandler {
30
29
  const levelName = formatters.level(info)
31
30
  const loggerColor = chalk[ConsoleHandler.getLoggerColor(logger)]
32
31
  const prefix = [
33
- chalk.white(`[${formatters.datetime(info)}]`),
32
+ chalk.white(`[${formatters.time(info)}]`),
34
33
  levelColor(`[${levelName}]`),
35
- logger ? loggerColor(`[${logger}]`) : '',
34
+ loggerColor(`[${logger}]`),
36
35
  ].join('')
37
36
  method(prefix, ...args)
38
37
  }
@@ -121,7 +120,7 @@ export class FileHandler extends LogHandler {
121
120
  const itemStrings: string[] = []
122
121
  let totalLength = prefix.length
123
122
  for (const item of args) {
124
- const itemString = this.formatDataItem(item)
123
+ const itemString = this.stringifyDataItem(item)
125
124
 
126
125
  // 截断过长的日志内容 Truncate overly long log messages
127
126
  if (totalLength + itemString.length < this.options.maxLength) {
@@ -138,8 +137,8 @@ export class FileHandler extends LogHandler {
138
137
  this.pushBuffer(prefix, ...itemStrings, '\n')
139
138
  }
140
139
 
141
- protected formatDataItem(item: unknown) {
142
- if (typeof item === 'string') return removeANSIColor(item)
140
+ protected stringifyDataItem(item: unknown) {
141
+ if (typeof item === 'string') return item
143
142
  if (item instanceof Error) return item.stack ?? String(item)
144
143
  try {
145
144
  const json = JSON.stringify(item) as string | undefined
package/src/init-dayjs.ts CHANGED
@@ -1,9 +1,8 @@
1
- import dayjs from 'dayjs'
1
+ import { extend, locale } from 'dayjs'
2
2
  import objectSupport from 'dayjs/plugin/objectSupport.js'
3
3
  import 'dayjs/locale/zh-cn.js'
4
4
 
5
5
  export function initDayJs() {
6
- // dayjs 的类型标记错误,这两个方法只能通过 dayjs 对象访问,并不能直接引入
7
- dayjs.extend(objectSupport)
8
- dayjs.locale('zh-cn')
6
+ extend(objectSupport)
7
+ locale('zh-cn')
9
8
  }
@@ -32,7 +32,6 @@ export function keywordCompare(keyword: string, target: string) {
32
32
  *
33
33
  * 以 0 开头是特例,例如 019 和 12 两个字符串,按直觉还是应该 019 在前面,毕竟本质还是字符串排序。
34
34
  *
35
- *
36
35
  * "123" "456" 数字排序
37
36
  * "123你好" "133我好" 字符串排序
38
37
  * "019" "12" 字符串排序
@@ -62,11 +61,11 @@ export function safeParseFloat(value: string | number, fallback: number) {
62
61
  }
63
62
 
64
63
  /**
65
- * Return file size for human reading.
66
- * From:https://stackoverflow.com/a/14919494
64
+ * 返回人类可读的文件尺寸
65
+ * 来自:https://stackoverflow.com/a/14919494
67
66
  *
68
- * si: true for radix of 1024;false for radix of 1000(default: false)
69
- * dp: how many decimal places to keep(保留几位小数)
67
+ * si: true 则使用 1000 进制,否则 1024 进制(默认 false)
68
+ * dp: 保留几位小数
70
69
  */
71
70
  export function readableSize(bytes: number, si = false, dp = 1) {
72
71
  const thresh = si ? 1000 : 1024
@@ -82,15 +81,3 @@ export function readableSize(bytes: number, si = false, dp = 1) {
82
81
  } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
83
82
  return `${bytes.toFixed(dp)} ${units[u]!}`
84
83
  }
85
-
86
- /**
87
- * 移除字符串中的 ASNI Color 修饰符
88
- * https://stackoverflow.com/a/7150870/2815178
89
- */
90
- export function removeANSIColor(string: string) {
91
- return string.replace(
92
- // eslint-disable-next-line no-control-regex
93
- /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
94
- '',
95
- )
96
- }
package/src/lang/types.ts CHANGED
@@ -73,6 +73,7 @@ export interface Success<T = void> {
73
73
  export interface Failed<ET = string> {
74
74
  success: false
75
75
  error: ET
76
+ code?: number | string
76
77
  }
77
78
  export type MaySuccess<T = void, ET = string> = Success<T> | Failed<ET>
78
79
 
@@ -83,25 +84,19 @@ function success<T = void>(data?: T) {
83
84
  }
84
85
  export { success }
85
86
 
86
- export function failed<ET>(error: ET): Failed<ET> {
87
- return { success: false, error }
87
+ export function failed<ET>(error: ET, code?: number | string): Failed<ET> {
88
+ return { success: false, error, code }
88
89
  }
89
90
 
90
91
  /**
91
92
  * 若传入值为 success,格式化其 data;否则原样返回错误
92
93
  * 支持传入会返回 MaySuccess 的 Promise
93
94
  */
94
- function formatSuccess<T, ET, FT>(
95
- item: MaySuccess<T, ET>,
96
- formatter: (data: T) => FT,
97
- ): MaySuccess<FT, ET>
98
- function formatSuccess<T, ET, FT>(
99
- item: Promise<MaySuccess<T, ET>>,
100
- formatter: (data: T) => FT,
101
- ): Promise<MaySuccess<FT, ET>>
95
+ function formatSuccess<T, ET, FT>(item: MaySuccess<T, ET>, formatter: (data: T) => FT): MaySuccess<FT, ET> // prettier-ignore
96
+ function formatSuccess<T, ET, FT>(item: Promise<MaySuccess<T, ET>>, formatter: (data: T) => FT): Promise<MaySuccess<FT, ET>> // prettier-ignore
102
97
  function formatSuccess<T, ET, FT>(
103
98
  item: MaySuccess<T, ET> | Promise<MaySuccess<T, ET>>,
104
- formatter: (data: T) => FT,
99
+ formatter: (data: T) => FT
105
100
  ) {
106
101
  if ('then' in item) return item.then(finalItem => formatSuccess(finalItem, formatter))
107
102
  return item.success ? { ...item, data: formatter(item.data) } : item
@@ -113,10 +108,10 @@ export { formatSuccess }
113
108
  * 注意:空字符串和数字 0 也会判定为没有值
114
109
  */
115
110
  function truthy(
116
- value: string | number | boolean | null | undefined,
111
+ value: string | number | boolean | null | undefined
117
112
  ): value is string | number | true
118
113
  function truthy<T>(
119
- value: T | string | number | boolean | null | undefined,
114
+ value: T | string | number | boolean | null | undefined
120
115
  ): value is T | string | number | true
121
116
  function truthy<T>(value: T | string | number | boolean | null | undefined) {
122
117
  return value !== null && value !== undefined && value !== '' && value !== 0 && value !== false
@@ -4,7 +4,7 @@ import { getLogger } from './index.js'
4
4
  /**
5
5
  * 适配 debug package
6
6
  */
7
- export function adaptDebugLib(debugLib: Debug, enable = '', logger = getLogger('3rd-library')) {
7
+ export function adaptDebugLib(debugLib: Debug, enable = '') {
8
8
  // 不在 localStorage 里记录 debugLib enable 状态,
9
9
  // 以解决 web worker 里读不到 localStorage 而无法启用 debugLib 日志的问题
10
10
  const emulate = {
@@ -31,11 +31,8 @@ export function adaptDebugLib(debugLib: Debug, enable = '', logger = getLogger('
31
31
  Object.assign(debugLib, emulate)
32
32
 
33
33
  // 将 debugLib 日志转发给 logger
34
- debugLib.log = (initial?: unknown, ...rest: unknown[]) => {
35
- // 移除开头的额外空格
36
- if (typeof initial === 'string') initial = initial.trimStart()
37
- logger.debug(initial, ...rest)
38
- }
34
+ const logger = getLogger('3rd-library')
35
+ debugLib.log = logger.debug.bind(logger)
39
36
 
40
37
  if (enable) {
41
38
  debugLib.enable(enable)
@@ -14,7 +14,7 @@ export enum LogLevel {
14
14
  }
15
15
 
16
16
  export interface LogInfo {
17
- logger: string // 生成此条日志的 logger name;有多级 logger 的情况下,这是最初的 logger 名称
17
+ logger: string // logger name;有多级 logger 的情况下,这是最初的 logger 名称
18
18
  level: LogLevel
19
19
  time: Dayjs
20
20
  args: unknown[] // log content
package/src/url.ts CHANGED
@@ -10,20 +10,21 @@
10
10
  import isPlainObject from 'lodash/isPlainObject.js'
11
11
 
12
12
  /*
13
- URL 里的 query 内容解析成对象
13
+ 从字符串中解析出 query 对象
14
14
 
15
15
  array:
16
16
  - 若开启,支持解析 a[]=1&a[]=2 格式的参数,会解析成一个数组 { a: ['1', '2'] }
17
17
  - 否则把 `a[]` 整体当做一个参数名 { 'a[]': '2' }
18
18
 
19
19
  strict:
20
- 是否开启“严格模式”(默认不开启)。在非严格模式下,会做很多兼容处理:
20
+ 是否开启“严格模式”(默认不开启)。
21
+ 在非严格模式下,会做很多兼容处理:
21
22
  1. 支持直接传入 query string(a=1&b=2);严格模式下,则需在开头补充一个 ?(?a=1&b=2)
22
23
  2. hash 里的内容也会被解析,以兼容拼接错误的 URL(把 query 拼到了 hash 后面)。
23
24
  3. 出现多个 ? 符号时,会把 ? 也当做 & 分隔符(index.html?a=1&b=2?c=3)
24
25
 
25
26
  小技巧:
26
- 如果明确只想解析 hash 或 search 里的内容,可以传入 location.search / hash 而不是传入整个 location.href
27
+ 在非严格模式下,如果明确只想解析 hash 或 search 里的内容,可以传入 location.search / hash 而不是传入整个 location.href
27
28
 
28
29
  此函数不会对 query 值进行 decode,需自定处理
29
30
  */
@@ -31,7 +32,7 @@ function parseQuery(url: string, options?: { array?: false, strict?: boolean }):
31
32
  function parseQuery(url: string, options: { array: true, strict?: boolean }): Record<string, string | string[]> // prettier-ignore
32
33
  function parseQuery(
33
34
  url: string,
34
- options?: { array?: boolean; strict?: boolean },
35
+ options?: { array?: boolean; strict?: boolean }
35
36
  ): Record<string, string | string[]> {
36
37
  if (!url) return {}
37
38
  const { array = false, strict = false } = options ?? {}
@@ -58,21 +59,21 @@ function parseQuery(
58
59
  }
59
60
  export { parseQuery }
60
61
 
61
- /*
62
- 取 query 中指定参数的值
63
- - 参数存在,返回参数值(可能是空字符串);不存在返回 null
64
- - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
65
- - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
66
- */
62
+ /**
63
+ * 取 query 中指定参数的值
64
+ * - 参数存在,返回参数值(可能是空字符串);不存在返回 null
65
+ * - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
66
+ * - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
67
+ */
67
68
  export function getQueryParam(name: string, url: string): string | null {
68
69
  const query = parseQuery(url)
69
70
  return typeof query[name] === 'string' ? query[name]! : null
70
71
  }
71
72
 
72
- /*
73
- 把对象合并成 query string。
74
- 支持字符串、数值、布尔值(不建议,考虑用 0 和 1 代替),数组会转换成 name[]=value 的格式
75
- */
73
+ /**
74
+ * 把对象合并成 query string。
75
+ * 支持字符串、数值、布尔值(不建议,考虑用 0 和 1 代替),数组会转换成 name[]=value 的格式
76
+ */
76
77
  type StringifyVal = string | number | boolean
77
78
  export function stringifyQuery(obj: { [key: string]: StringifyVal | StringifyVal[] | undefined }) {
78
79
  if (!isPlainObject(obj)) return ''
@@ -91,52 +92,56 @@ function stringifyQueryItem(name: string, value: StringifyVal | StringifyVal[]):
91
92
  }
92
93
 
93
94
  /**
94
- * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
95
+ * 拆分 URL 的各个部分
96
+ *
97
+ * bare 为 true,则 search 不带 '?',hash 不带 '#'
98
+ * 否则和 location.search / hash 一样
95
99
  */
96
- export function splitUrl(url: string): { base: string; search: string; hash: string } {
100
+ export function splitUrl(url: string, bare = true): { base: string; search: string; hash: string } {
97
101
  let hashIndex = url.indexOf('#')
98
102
  if (hashIndex === -1) hashIndex = url.length
103
+ const bareHash = url.slice(hashIndex + 1)
99
104
 
100
105
  let searchIndex = url.slice(0, hashIndex).indexOf('?')
101
106
  if (searchIndex === -1) searchIndex = hashIndex
107
+ const bareSearch = url.slice(searchIndex + 1, hashIndex)
102
108
 
103
109
  return {
104
110
  base: url.slice(0, searchIndex),
105
- search: url.slice(searchIndex + 1, hashIndex),
106
- hash: url.slice(hashIndex + 1),
111
+ search: bare ? bareSearch : bareSearch ? '?' + bareSearch : '',
112
+ hash: bare ? bareHash : bareHash ? '#' + bareHash : '',
107
113
  }
108
114
  }
109
115
 
110
- /*
111
- 把指定 search 和 hash 内容合并到 url 上
112
-
113
- search object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
114
- hash string 不带 # 号;若 url 已有 hash,会用此值代替
115
- */
116
+ /**
117
+ * 把指定 query 和 hash 内容合并到 url 上
118
+ *
119
+ * query object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
120
+ * hash string url 已有 hash,会用此值代替。带不带开头的 '?' 皆可。
121
+ */
116
122
  export function combineUrl(
117
123
  origUrl: string,
118
- search?: Record<string, string | string[]>,
119
- hash?: string,
124
+ query: Record<string, string | string[]> = {},
125
+ hash: string = ''
120
126
  ) {
121
- search = search ?? {}
122
- hash = hash ?? ''
127
+ if (hash.startsWith('#')) hash = hash.slice(1)
123
128
 
124
129
  // 拆分原 url 的 search、hash
125
130
  const { base, search: origSearch, hash: origHash } = splitUrl(origUrl)
126
131
 
127
132
  // 拼接新 URL
128
133
  let newUrl = base
129
- const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...search })
134
+ const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...query })
130
135
  const newHash = hash || origHash
131
136
  if (newSearch) newUrl += `?${newSearch}`
132
137
  if (newHash) newUrl += `#${newHash}`
133
138
  return newUrl
134
139
  }
135
140
 
136
- /*
137
- decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
138
- 用此函数代替可避免此问题
139
- */
141
+ /**
142
+ * decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
143
+ * 用此函数代替可避免此问题
144
+ */
140
145
  export function safeDecode(content: string) {
141
146
  try {
142
147
  return decodeURIComponent(content)
package/url.d.ts CHANGED
@@ -7,20 +7,43 @@ declare function parseQuery(url: string, options: {
7
7
  strict?: boolean;
8
8
  }): Record<string, string | string[]>;
9
9
  export { parseQuery };
10
+ /**
11
+ * 取 query 中指定参数的值
12
+ * - 参数存在,返回参数值(可能是空字符串);不存在返回 null
13
+ * - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
14
+ * - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
15
+ */
10
16
  export declare function getQueryParam(name: string, url: string): string | null;
17
+ /**
18
+ * 把对象合并成 query string。
19
+ * 支持字符串、数值、布尔值(不建议,考虑用 0 和 1 代替),数组会转换成 name[]=value 的格式
20
+ */
11
21
  type StringifyVal = string | number | boolean;
12
22
  export declare function stringifyQuery(obj: {
13
23
  [key: string]: StringifyVal | StringifyVal[] | undefined;
14
24
  }): string;
15
25
  /**
16
- * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
26
+ * 拆分 URL 的各个部分
27
+ *
28
+ * bare 为 true,则 search 不带 '?',hash 不带 '#'
29
+ * 否则和 location.search / hash 一样
17
30
  */
18
- export declare function splitUrl(url: string): {
31
+ export declare function splitUrl(url: string, bare?: boolean): {
19
32
  base: string;
20
33
  search: string;
21
34
  hash: string;
22
35
  };
23
- export declare function combineUrl(origUrl: string, search?: Record<string, string | string[]>, hash?: string): string;
36
+ /**
37
+ * 把指定 query 和 hash 内容合并到 url 上
38
+ *
39
+ * query object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
40
+ * hash string 若 url 已有 hash,会用此值代替。带不带开头的 '?' 皆可。
41
+ */
42
+ export declare function combineUrl(origUrl: string, query?: Record<string, string | string[]>, hash?: string): string;
43
+ /**
44
+ * decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
45
+ * 用此函数代替可避免此问题
46
+ */
24
47
  export declare function safeDecode(content: string): string;
25
48
  /**
26
49
  * 将 URL 中的 http:// 协议改成 https://
package/url.js CHANGED
@@ -35,12 +35,12 @@ function parseQuery(url, options) {
35
35
  return query;
36
36
  }
37
37
  export { parseQuery };
38
- /*
39
- 取 query 中指定参数的值
40
- - 参数存在,返回参数值(可能是空字符串);不存在返回 null
41
- - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
42
- - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
43
- */
38
+ /**
39
+ * 取 query 中指定参数的值
40
+ * - 参数存在,返回参数值(可能是空字符串);不存在返回 null
41
+ * - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
42
+ * - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
43
+ */
44
44
  export function getQueryParam(name, url) {
45
45
  const query = parseQuery(url);
46
46
  return typeof query[name] === 'string' ? query[name] : null;
@@ -60,35 +60,40 @@ function stringifyQueryItem(name, value) {
60
60
  return `${name}=${value}`;
61
61
  }
62
62
  /**
63
- * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
63
+ * 拆分 URL 的各个部分
64
+ *
65
+ * bare 为 true,则 search 不带 '?',hash 不带 '#'
66
+ * 否则和 location.search / hash 一样
64
67
  */
65
- export function splitUrl(url) {
68
+ export function splitUrl(url, bare = true) {
66
69
  let hashIndex = url.indexOf('#');
67
70
  if (hashIndex === -1)
68
71
  hashIndex = url.length;
72
+ const bareHash = url.slice(hashIndex + 1);
69
73
  let searchIndex = url.slice(0, hashIndex).indexOf('?');
70
74
  if (searchIndex === -1)
71
75
  searchIndex = hashIndex;
76
+ const bareSearch = url.slice(searchIndex + 1, hashIndex);
72
77
  return {
73
78
  base: url.slice(0, searchIndex),
74
- search: url.slice(searchIndex + 1, hashIndex),
75
- hash: url.slice(hashIndex + 1),
79
+ search: bare ? bareSearch : bareSearch ? '?' + bareSearch : '',
80
+ hash: bare ? bareHash : bareHash ? '#' + bareHash : '',
76
81
  };
77
82
  }
78
- /*
79
- 把指定 search 和 hash 内容合并到 url 上
80
-
81
- search object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
82
- hash string 不带 # 号;若 url 已有 hash,会用此值代替
83
- */
84
- export function combineUrl(origUrl, search, hash) {
85
- search = search ?? {};
86
- hash = hash ?? '';
83
+ /**
84
+ * 把指定 query 和 hash 内容合并到 url 上
85
+ *
86
+ * query object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
87
+ * hash string url 已有 hash,会用此值代替。带不带开头的 '?' 皆可。
88
+ */
89
+ export function combineUrl(origUrl, query = {}, hash = '') {
90
+ if (hash.startsWith('#'))
91
+ hash = hash.slice(1);
87
92
  // 拆分原 url 的 search、hash
88
93
  const { base, search: origSearch, hash: origHash } = splitUrl(origUrl);
89
94
  // 拼接新 URL
90
95
  let newUrl = base;
91
- const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...search });
96
+ const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...query });
92
97
  const newHash = hash || origHash;
93
98
  if (newSearch)
94
99
  newUrl += `?${newSearch}`;
@@ -96,10 +101,10 @@ export function combineUrl(origUrl, search, hash) {
96
101
  newUrl += `#${newHash}`;
97
102
  return newUrl;
98
103
  }
99
- /*
100
- decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
101
- 用此函数代替可避免此问题
102
- */
104
+ /**
105
+ * decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
106
+ * 用此函数代替可避免此问题
107
+ */
103
108
  export function safeDecode(content) {
104
109
  try {
105
110
  return decodeURIComponent(content);