@bhsd/common 1.1.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  declare interface JsonError {
2
+ severity: 'error' | 'warning';
2
3
  message: string;
3
- line: number | null;
4
- column: number | null;
5
- position: number | null;
4
+ line: number;
5
+ column: number;
6
+ endLine?: number;
7
+ endColumn?: number;
8
+ from: number;
9
+ to?: number;
10
+ /** @deprecated */
11
+ position: number;
6
12
  }
7
13
  export type RegexGetter<T = string> = (s: T) => RegExp;
8
14
  export declare const wmf = "wiktionary|wiki(?:pedia|books|news|quote|source|versity|voyage)";
@@ -39,6 +45,17 @@ export declare function getRegex<T extends object>(f: RegexGetter<T>): RegexGett
39
45
  * @deprecated 需要改为使用`getRegex`
40
46
  */
41
47
  export declare const getObjRegex: typeof getRegex;
48
+ /**
49
+ * 按行分割字符串并记录每行的起止位置
50
+ * @param str 字符串
51
+ */
52
+ export declare const splitLines: (str: string) => [string, number, number][];
53
+ /**
54
+ * 使用`JSON.parse()`诊断JSON字符串中的语法错误
55
+ * @param str JSON字符串
56
+ * @param force 是否强制诊断
57
+ */
58
+ export declare const lintJSONNative: (str: string, force?: boolean) => JsonError[];
42
59
  /**
43
60
  * 诊断JSON字符串中的语法错误
44
61
  * @param str JSON字符串
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lintJSON = exports.getObjRegex = exports.sanitizeInlineStyle = exports.splitColors = exports.numToHex = exports.rawurldecode = exports.wmf = void 0;
3
+ exports.lintJSON = exports.lintJSONNative = exports.splitLines = exports.getObjRegex = exports.sanitizeInlineStyle = exports.splitColors = exports.numToHex = exports.rawurldecode = exports.wmf = void 0;
4
4
  exports.getRegex = getRegex;
5
+ const json_parse_js_1 = require("./json_parse.js");
5
6
  exports.wmf = 'wiktionary|wiki(?:pedia|books|news|quote|source|versity|voyage)';
6
7
  /**
7
8
  * PHP的`rawurldecode`函数的JavaScript实现
@@ -72,23 +73,99 @@ function getRegex(f) {
72
73
  * @deprecated 需要改为使用`getRegex`
73
74
  */
74
75
  exports.getObjRegex = getRegex;
76
+ /**
77
+ * 按行分割字符串并记录每行的起止位置
78
+ * @param str 字符串
79
+ */
80
+ const splitLines = (str) => {
81
+ const lines = [];
82
+ let start = 0;
83
+ for (const line of str.split('\n')) {
84
+ const end = start + line.length;
85
+ lines.push([line, start, end]);
86
+ start = end + 1;
87
+ }
88
+ return lines;
89
+ };
90
+ exports.splitLines = splitLines;
75
91
  const mt2num = (mt) => mt && Number(mt[1]);
92
+ const formatJsonError = (str, errors) => {
93
+ let lines;
94
+ const offsetToPosition = (offset) => {
95
+ lines ??= (0, exports.splitLines)(str);
96
+ const line = lines.findIndex(([, , end]) => offset <= end) + 1;
97
+ return {
98
+ line,
99
+ column: offset - lines[line - 1][1] + 1,
100
+ };
101
+ };
102
+ for (const error of errors) {
103
+ const { line, column, from, to } = error;
104
+ if (from === null || from === undefined) {
105
+ if (line) {
106
+ lines ??= (0, exports.splitLines)(str);
107
+ error.column ??= 1;
108
+ error.from = lines[line - 1][1] + (error.column - 1);
109
+ }
110
+ else {
111
+ error.from = 0;
112
+ error.line = 1;
113
+ error.column = 1;
114
+ }
115
+ }
116
+ else if (!line || !column) {
117
+ Object.assign(error, offsetToPosition(from));
118
+ }
119
+ if (to !== undefined) {
120
+ ({ line: error.endLine, column: error.endColumn } = offsetToPosition(to));
121
+ }
122
+ error.position = error.from;
123
+ }
124
+ return errors;
125
+ };
126
+ /**
127
+ * 使用`JSON.parse()`诊断JSON字符串中的语法错误
128
+ * @param str JSON字符串
129
+ * @param force 是否强制诊断
130
+ */
131
+ const lintJSONNative = (str, force) => {
132
+ if (force || str.trim()) {
133
+ try {
134
+ JSON.parse(str);
135
+ }
136
+ catch (e) {
137
+ const { message } = e, line = mt2num(/\bline (\d+)/u.exec(message)), column = mt2num(/\bcolumn (\d+)/u.exec(message)), from = mt2num(/\bposition (\d+)/u.exec(message));
138
+ return formatJsonError(str, [{ message, line, column, from, severity: 'error' }]);
139
+ }
140
+ }
141
+ return [];
142
+ };
143
+ exports.lintJSONNative = lintJSONNative;
76
144
  /**
77
145
  * 诊断JSON字符串中的语法错误
78
146
  * @param str JSON字符串
79
147
  */
80
148
  const lintJSON = (str) => {
149
+ if (!str.trim()) {
150
+ return [];
151
+ }
152
+ let errors;
81
153
  try {
82
- if (str.trim()) {
83
- JSON.parse(str);
84
- }
154
+ (0, json_parse_js_1.json_parse)(str);
85
155
  }
86
156
  catch (e) {
87
- if (e instanceof SyntaxError) {
88
- const { message } = e, line = mt2num(/\bline (\d+)/u.exec(message)), column = mt2num(/\bcolumn (\d+)/u.exec(message)), position = mt2num(/\bposition (\d+)/u.exec(message));
89
- return [{ message, line, column, position }];
157
+ if (!(e instanceof Error)) {
158
+ const { warnings, ...error } = e;
159
+ if (error.message) {
160
+ warnings.push(error);
161
+ }
162
+ errors = formatJsonError(str, warnings);
163
+ if (error.message) {
164
+ return errors;
165
+ }
90
166
  }
91
167
  }
92
- return [];
168
+ const nativeErrors = (0, exports.lintJSONNative)(str);
169
+ return errors ? [...nativeErrors, ...errors] : nativeErrors;
93
170
  };
94
171
  exports.lintJSON = lintJSON;
package/dist/index.mjs CHANGED
@@ -1,14 +1,293 @@
1
- const wmf = "wiktionary|wiki(?:pedia|books|news|quote|source|versity|voyage)";
2
- const rawurldecode = (str) => decodeURIComponent(str.replace(/%(?![\da-f]{2})/giu, "%25"));
3
- const numToHex = (d) => Math.round(d * 255).toString(16).padStart(2, "0");
4
- const regex = /* @__PURE__ */ (() => {
1
+ // src/json_parse.ts
2
+ var escapee = {
3
+ '"': '"',
4
+ "\\": "\\",
5
+ "/": "/",
6
+ b: "\b",
7
+ f: "\f",
8
+ n: "\n",
9
+ r: "\r",
10
+ t: " "
11
+ };
12
+ var spaces = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
13
+ var stringify = (c) => {
14
+ if (c === "") {
15
+ return "end of input";
16
+ }
17
+ return c === '"' ? `'"'` : JSON.stringify(c);
18
+ };
19
+ var json_parse = /* @__PURE__ */ (() => {
20
+ let at;
21
+ let ch;
22
+ let text;
23
+ let warnings;
24
+ const prepareError = (e, from, to) => {
25
+ if (from === void 0) {
26
+ e.from = at - 1;
27
+ } else {
28
+ e.from = from;
29
+ e.to = to ?? at - 1;
30
+ }
31
+ };
32
+ const warn = (message, from, to) => {
33
+ const warning = {
34
+ message,
35
+ severity: "warning"
36
+ };
37
+ prepareError(warning, from, to);
38
+ warnings.push(warning);
39
+ };
40
+ const error = (message, from, to) => {
41
+ const e = {
42
+ warnings,
43
+ message,
44
+ severity: "error"
45
+ };
46
+ prepareError(e, from, to);
47
+ throw e;
48
+ };
49
+ const next = (c) => {
50
+ if (c && c !== ch) {
51
+ error(`Expected ${stringify(c)} instead of ${stringify(ch)}`);
52
+ }
53
+ ch = text.charAt(at);
54
+ at++;
55
+ return ch;
56
+ };
57
+ const number = () => {
58
+ let string2 = "";
59
+ const from = at - 1;
60
+ if (ch === "-") {
61
+ string2 = "-";
62
+ next();
63
+ }
64
+ if (ch === "0") {
65
+ string2 += ch;
66
+ next();
67
+ if (ch >= "0" && ch <= "9") {
68
+ error("Bad number");
69
+ }
70
+ } else if (ch >= "1" && ch <= "9") {
71
+ while (ch >= "0" && ch <= "9") {
72
+ string2 += ch;
73
+ next();
74
+ }
75
+ } else {
76
+ error("No number after minus sign");
77
+ }
78
+ if (ch !== "." && ch !== "e" && ch !== "E") {
79
+ const value3 = Number(string2);
80
+ if (!Number.isSafeInteger(value3)) {
81
+ warn("Not a safe integer", from);
82
+ }
83
+ return;
84
+ }
85
+ if (ch === ".") {
86
+ string2 += ".";
87
+ next();
88
+ if (ch < "0" || ch > "9") {
89
+ error("Unterminated fractional number");
90
+ }
91
+ while (ch >= "0" && ch <= "9") {
92
+ string2 += ch;
93
+ next();
94
+ }
95
+ }
96
+ if (ch === "e" || ch === "E") {
97
+ string2 += ch;
98
+ next();
99
+ if (ch === "-" || ch === "+") {
100
+ string2 += ch;
101
+ next();
102
+ }
103
+ while (ch >= "0" && ch <= "9") {
104
+ string2 += ch;
105
+ next();
106
+ }
107
+ }
108
+ const value2 = Number(string2);
109
+ if (!Number.isFinite(value2)) {
110
+ error("Bad number");
111
+ }
112
+ };
113
+ const string = () => {
114
+ let value2 = "";
115
+ if (ch === '"') {
116
+ while (next()) {
117
+ if (ch === '"') {
118
+ next();
119
+ return value2;
120
+ }
121
+ const from = at - 1;
122
+ if (ch === "\\") {
123
+ next();
124
+ if (ch === "u") {
125
+ let i = 0;
126
+ let uffff = 0;
127
+ for (; i < 4; i++) {
128
+ const hex = parseInt(next(), 16);
129
+ if (!isFinite(hex)) {
130
+ break;
131
+ }
132
+ uffff = uffff * 16 + hex;
133
+ }
134
+ if (i < 4) {
135
+ error("Bad unicode escape", from);
136
+ }
137
+ value2 += String.fromCharCode(uffff);
138
+ } else if (typeof escapee[ch] === "string") {
139
+ value2 += escapee[ch];
140
+ } else {
141
+ error("Bad escaped character", from, at);
142
+ }
143
+ } else if (ch < " ") {
144
+ error("Bad control character", from, at);
145
+ } else {
146
+ value2 += ch;
147
+ }
148
+ }
149
+ } else {
150
+ error(`Expected '"' instead of ${JSON.stringify(ch)}`);
151
+ }
152
+ return error("Unterminated string");
153
+ };
154
+ const white = () => {
155
+ while (ch && spaces.has(ch)) {
156
+ next();
157
+ }
158
+ };
159
+ const word = () => {
160
+ switch (ch) {
161
+ case "t":
162
+ next();
163
+ next("r");
164
+ next("u");
165
+ next("e");
166
+ return;
167
+ case "f":
168
+ next();
169
+ next("a");
170
+ next("l");
171
+ next("s");
172
+ next("e");
173
+ return;
174
+ case "n":
175
+ next();
176
+ next("u");
177
+ next("l");
178
+ next("l");
179
+ return;
180
+ default:
181
+ error(`Unexpected ${JSON.stringify(ch)}`);
182
+ }
183
+ };
184
+ const array = () => {
185
+ next();
186
+ white();
187
+ if (ch === "]") {
188
+ next();
189
+ return;
190
+ }
191
+ let from;
192
+ while (ch) {
193
+ if (ch === "]") {
194
+ error("Trailing comma in array", from, from + 1);
195
+ }
196
+ value();
197
+ white();
198
+ if (ch === "]") {
199
+ next();
200
+ return;
201
+ } else if (ch === ",") {
202
+ from = at - 1;
203
+ next();
204
+ white();
205
+ } else {
206
+ error(`Expected "," or "]" instead of ${stringify(ch)}`);
207
+ }
208
+ }
209
+ error("Unterminated array");
210
+ };
211
+ const object = () => {
212
+ const keys = /* @__PURE__ */ new Set();
213
+ next();
214
+ white();
215
+ if (ch === "}") {
216
+ next();
217
+ return;
218
+ }
219
+ let from;
220
+ while (ch) {
221
+ if (ch === "}") {
222
+ error("Trailing comma in object", from, from + 1);
223
+ }
224
+ from = at;
225
+ const key = string();
226
+ const to = at - 2;
227
+ white();
228
+ next(":");
229
+ if (keys.has(key)) {
230
+ warn(`Duplicate key ${stringify(key)}`, from, to);
231
+ } else {
232
+ keys.add(key);
233
+ }
234
+ value();
235
+ white();
236
+ if (ch === "}") {
237
+ next();
238
+ return;
239
+ } else if (ch === ",") {
240
+ from = at - 1;
241
+ next();
242
+ white();
243
+ } else {
244
+ error(`Expected "," or "}" instead of ${stringify(ch)}`);
245
+ }
246
+ }
247
+ error(`Expected '"'`);
248
+ };
249
+ const value = () => {
250
+ white();
251
+ switch (ch) {
252
+ case "{":
253
+ return object();
254
+ case "[":
255
+ return array();
256
+ case '"':
257
+ return string();
258
+ case "-":
259
+ return number();
260
+ default:
261
+ return ch >= "0" && ch <= "9" ? number() : word();
262
+ }
263
+ };
264
+ return (source) => {
265
+ text = source;
266
+ warnings = [];
267
+ at = 0;
268
+ ch = " ";
269
+ value();
270
+ white();
271
+ if (ch) {
272
+ error("Syntax error");
273
+ } else if (warnings.length > 0) {
274
+ throw { warnings };
275
+ }
276
+ };
277
+ })();
278
+
279
+ // src/index.ts
280
+ var wmf = "wiktionary|wiki(?:pedia|books|news|quote|source|versity|voyage)";
281
+ var rawurldecode = (str) => decodeURIComponent(str.replace(/%(?![\da-f]{2})/giu, "%25"));
282
+ var numToHex = (d) => Math.round(d * 255).toString(16).padStart(2, "0");
283
+ var regex = /* @__PURE__ */ (() => {
5
284
  const hexColor = String.raw`#(?:[\da-f]{3,4}|(?:[\da-f]{2}){3,4})(?![\p{L}\p{N}_])`, rgbValue = String.raw`(?:\d*\.)?\d+%?`, hue = String.raw`(?:\d*\.)?\d+(?:deg|grad|rad|turn)?`, rgbColor = String.raw`rgba?\(\s*(?:${String.raw`${new Array(3).fill(rgbValue).join(String.raw`\s+`)}(?:\s*\/\s*${rgbValue})?`}|${String.raw`${new Array(3).fill(rgbValue).join(String.raw`\s*,\s*`)}(?:\s*,\s*${rgbValue})?`})\s*\)`, hslColor = String.raw`hsla?\(\s*(?:${String.raw`${hue}\s+${rgbValue}\s+${rgbValue}(?:\s*\/\s*${rgbValue})?`}|${String.raw`${hue}${String.raw`\s*,\s*(?:\d*\.)?\d+%`.repeat(2)}(?:\s*,\s*${rgbValue})?`})\s*\)`;
6
285
  return {
7
286
  full: new RegExp(String.raw`(^|[^\p{L}\p{N}_])(${hexColor}|${rgbColor}|${hslColor})`, "giu"),
8
287
  rgb: new RegExp(String.raw`(^|[^\p{L}\p{N}_])(${hexColor}|${rgbColor})`, "giu")
9
288
  };
10
289
  })();
11
- const splitColors = (str, hsl = true) => {
290
+ var splitColors = (str, hsl = true) => {
12
291
  const pieces = [], re = regex[hsl ? "full" : "rgb"];
13
292
  re.lastIndex = 0;
14
293
  let mt = re.exec(str), lastIndex = 0;
@@ -26,7 +305,7 @@ const splitColors = (str, hsl = true) => {
26
305
  }
27
306
  return pieces;
28
307
  };
29
- const sanitizeInlineStyle = (style) => style.replace(/[{}]/gu, (p) => p === "{" ? "{" : "}");
308
+ var sanitizeInlineStyle = (style) => style.replace(/[{}]/gu, (p) => p === "{" ? "{" : "}");
30
309
  function getRegex(f) {
31
310
  const map = /* @__PURE__ */ new Map(), weakMap = /* @__PURE__ */ new WeakMap();
32
311
  return (s) => {
@@ -41,28 +320,92 @@ function getRegex(f) {
41
320
  return re;
42
321
  };
43
322
  }
44
- const getObjRegex = getRegex;
45
- const mt2num = (mt) => mt && Number(mt[1]);
46
- const lintJSON = (str) => {
47
- try {
48
- if (str.trim()) {
323
+ var getObjRegex = getRegex;
324
+ var splitLines = (str) => {
325
+ const lines = [];
326
+ let start = 0;
327
+ for (const line of str.split("\n")) {
328
+ const end = start + line.length;
329
+ lines.push([line, start, end]);
330
+ start = end + 1;
331
+ }
332
+ return lines;
333
+ };
334
+ var mt2num = (mt) => mt && Number(mt[1]);
335
+ var formatJsonError = (str, errors) => {
336
+ let lines;
337
+ const offsetToPosition = (offset) => {
338
+ lines ??= splitLines(str);
339
+ const line = lines.findIndex(([, , end]) => offset <= end) + 1;
340
+ return {
341
+ line,
342
+ column: offset - lines[line - 1][1] + 1
343
+ };
344
+ };
345
+ for (const error of errors) {
346
+ const { line, column, from, to } = error;
347
+ if (from === null || from === void 0) {
348
+ if (line) {
349
+ lines ??= splitLines(str);
350
+ error.column ??= 1;
351
+ error.from = lines[line - 1][1] + (error.column - 1);
352
+ } else {
353
+ error.from = 0;
354
+ error.line = 1;
355
+ error.column = 1;
356
+ }
357
+ } else if (!line || !column) {
358
+ Object.assign(error, offsetToPosition(from));
359
+ }
360
+ if (to !== void 0) {
361
+ ({ line: error.endLine, column: error.endColumn } = offsetToPosition(to));
362
+ }
363
+ error.position = error.from;
364
+ }
365
+ return errors;
366
+ };
367
+ var lintJSONNative = (str, force) => {
368
+ if (force || str.trim()) {
369
+ try {
49
370
  JSON.parse(str);
371
+ } catch (e) {
372
+ const { message } = e, line = mt2num(/\bline (\d+)/u.exec(message)), column = mt2num(/\bcolumn (\d+)/u.exec(message)), from = mt2num(/\bposition (\d+)/u.exec(message));
373
+ return formatJsonError(str, [{ message, line, column, from, severity: "error" }]);
50
374
  }
375
+ }
376
+ return [];
377
+ };
378
+ var lintJSON = (str) => {
379
+ if (!str.trim()) {
380
+ return [];
381
+ }
382
+ let errors;
383
+ try {
384
+ json_parse(str);
51
385
  } catch (e) {
52
- if (e instanceof SyntaxError) {
53
- const { message } = e, line = mt2num(/\bline (\d+)/u.exec(message)), column = mt2num(/\bcolumn (\d+)/u.exec(message)), position = mt2num(/\bposition (\d+)/u.exec(message));
54
- return [{ message, line, column, position }];
386
+ if (!(e instanceof Error)) {
387
+ const { warnings, ...error } = e;
388
+ if (error.message) {
389
+ warnings.push(error);
390
+ }
391
+ errors = formatJsonError(str, warnings);
392
+ if (error.message) {
393
+ return errors;
394
+ }
55
395
  }
56
396
  }
57
- return [];
397
+ const nativeErrors = lintJSONNative(str);
398
+ return errors ? [...nativeErrors, ...errors] : nativeErrors;
58
399
  };
59
400
  export {
60
401
  getObjRegex,
61
402
  getRegex,
62
403
  lintJSON,
404
+ lintJSONNative,
63
405
  numToHex,
64
406
  rawurldecode,
65
407
  sanitizeInlineStyle,
66
408
  splitColors,
409
+ splitLines,
67
410
  wmf
68
411
  };
@@ -0,0 +1,344 @@
1
+ "use strict";
2
+ /*
3
+ json_parse.js
4
+ 2016-05-02
5
+
6
+ Public Domain.
7
+
8
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
9
+
10
+ This file creates a json_parse function.
11
+
12
+ json_parse(text)
13
+ This method parses a JSON text and throws a SyntaxError exception.
14
+
15
+ This is a reference implementation. You are free to copy, modify, or redistribute.
16
+
17
+ Original source:
18
+ https://github.com/douglascrockford/JSON-js/blob/107fc93c94aa3a9c7b48548631593ecf3aac60d2/json_parse.js
19
+
20
+ Modifications:
21
+ - Only returns errors and warnings instead of the parsed value.
22
+ - Better agreement with JSON specification.
23
+ - Warnings for duplicate object keys and unsafe integers.
24
+
25
+ This code should be minified before deployment.
26
+ See http://javascript.crockford.com/jsmin.html
27
+
28
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.json_parse = void 0;
32
+ const escapee = {
33
+ '"': '"',
34
+ "\\": "\\",
35
+ "/": "/",
36
+ b: "\b",
37
+ f: "\f",
38
+ n: "\n",
39
+ r: "\r",
40
+ t: "\t",
41
+ };
42
+ const spaces = new Set([" ", "\t", "\n", "\r"]);
43
+ const stringify = (c) => {
44
+ if (c === "") {
45
+ return "end of input";
46
+ }
47
+ return c === '"' ? `'"'` : JSON.stringify(c);
48
+ };
49
+ exports.json_parse = (() => {
50
+ // This is a function that can parse a JSON text.
51
+ // It is a simple, recursive descent parser.
52
+ // It does not use eval or regular expressions,
53
+ // so it can be used as a model for implementing a JSON parser in other languages.
54
+ // We are defining the function inside of another function to avoid creating global variables.
55
+ let at; // The index of the current character
56
+ let ch; // The current character
57
+ let text;
58
+ let warnings;
59
+ const prepareError = (e, from, to) => {
60
+ if (from === undefined) {
61
+ e.from = at - 1;
62
+ }
63
+ else {
64
+ e.from = from;
65
+ e.to = to ?? at - 1;
66
+ }
67
+ };
68
+ const warn = (message, from, to) => {
69
+ // Log warning when something is wrong.
70
+ const warning = {
71
+ message,
72
+ severity: "warning",
73
+ };
74
+ prepareError(warning, from, to);
75
+ warnings.push(warning);
76
+ };
77
+ const error = (message, from, to) => {
78
+ // Call error when something is wrong.
79
+ const e = {
80
+ warnings,
81
+ message,
82
+ severity: "error",
83
+ };
84
+ prepareError(e, from, to);
85
+ throw e;
86
+ };
87
+ const next = (c) => {
88
+ // If a c parameter is provided, verify that it matches the current character.
89
+ if (c && c !== ch) {
90
+ error(`Expected ${stringify(c)} instead of ${stringify(ch)}`);
91
+ }
92
+ // Get the next character. When there are no more characters, return the empty string.
93
+ ch = text.charAt(at);
94
+ at++;
95
+ return ch;
96
+ };
97
+ const number = () => {
98
+ // Parse a number value.
99
+ let string = "";
100
+ const from = at - 1;
101
+ if (ch === "-") {
102
+ string = "-";
103
+ next();
104
+ }
105
+ if (ch === "0") {
106
+ string += ch;
107
+ next();
108
+ if (ch >= "0" && ch <= "9") {
109
+ error("Bad number");
110
+ }
111
+ }
112
+ else if (ch >= "1" && ch <= "9") {
113
+ while (ch >= "0" && ch <= "9") {
114
+ string += ch;
115
+ next();
116
+ }
117
+ }
118
+ else {
119
+ error("No number after minus sign");
120
+ }
121
+ if (ch !== "." && ch !== "e" && ch !== "E") {
122
+ const value = Number(string);
123
+ if (!Number.isSafeInteger(value)) {
124
+ warn("Not a safe integer", from);
125
+ }
126
+ return;
127
+ }
128
+ if (ch === ".") {
129
+ string += ".";
130
+ next();
131
+ if (ch < "0" || ch > "9") {
132
+ error("Unterminated fractional number");
133
+ }
134
+ while (ch >= "0" && ch <= "9") {
135
+ string += ch;
136
+ next();
137
+ }
138
+ }
139
+ if (ch === "e" || ch === "E") {
140
+ string += ch;
141
+ next();
142
+ // @ts-expect-error `ch` modified
143
+ if (ch === "-" || ch === "+") {
144
+ string += ch;
145
+ next();
146
+ }
147
+ while (ch >= "0" && ch <= "9") {
148
+ string += ch;
149
+ next();
150
+ }
151
+ }
152
+ const value = Number(string);
153
+ if (!Number.isFinite(value)) {
154
+ error("Bad number");
155
+ }
156
+ };
157
+ const string = () => {
158
+ // Parse a string value.
159
+ let value = "";
160
+ // When parsing for string values, we must look for " and \ characters.
161
+ if (ch === '"') {
162
+ while (next()) {
163
+ if (ch === '"') {
164
+ next();
165
+ return value;
166
+ }
167
+ const from = at - 1;
168
+ if (ch === "\\") {
169
+ next();
170
+ if (ch === "u") {
171
+ let i = 0;
172
+ let uffff = 0;
173
+ for (; i < 4; i++) {
174
+ const hex = parseInt(next(), 16);
175
+ if (!isFinite(hex)) {
176
+ break;
177
+ }
178
+ uffff = uffff * 16 + hex;
179
+ }
180
+ if (i < 4) {
181
+ error("Bad unicode escape", from);
182
+ }
183
+ value += String.fromCharCode(uffff);
184
+ }
185
+ else if (typeof escapee[ch] === "string") {
186
+ value += escapee[ch];
187
+ }
188
+ else {
189
+ error("Bad escaped character", from, at);
190
+ }
191
+ }
192
+ else if (ch < " ") {
193
+ error("Bad control character", from, at);
194
+ }
195
+ else {
196
+ value += ch;
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ error(`Expected '"' instead of ${JSON.stringify(ch)}`);
202
+ }
203
+ return error("Unterminated string");
204
+ };
205
+ const white = () => {
206
+ // Skip whitespace.
207
+ while (ch && spaces.has(ch)) {
208
+ next();
209
+ }
210
+ };
211
+ const word = () => {
212
+ // true, false, or null.
213
+ switch (ch) {
214
+ case "t":
215
+ next();
216
+ next("r");
217
+ next("u");
218
+ next("e");
219
+ return;
220
+ case "f":
221
+ next();
222
+ next("a");
223
+ next("l");
224
+ next("s");
225
+ next("e");
226
+ return;
227
+ case "n":
228
+ next();
229
+ next("u");
230
+ next("l");
231
+ next("l");
232
+ return;
233
+ default:
234
+ error(`Unexpected ${JSON.stringify(ch)}`);
235
+ }
236
+ };
237
+ const array = () => {
238
+ // Parse an array value.
239
+ next();
240
+ white();
241
+ if (ch === "]") {
242
+ next();
243
+ return; // empty array
244
+ }
245
+ let from;
246
+ while (ch) {
247
+ if (ch === "]") {
248
+ error("Trailing comma in array", from, from + 1);
249
+ }
250
+ value();
251
+ white();
252
+ if (ch === "]") {
253
+ next();
254
+ return;
255
+ }
256
+ else if (ch === ",") {
257
+ from = at - 1;
258
+ next();
259
+ white();
260
+ }
261
+ else {
262
+ error(`Expected "," or "]" instead of ${stringify(ch)}`);
263
+ }
264
+ }
265
+ error("Unterminated array");
266
+ };
267
+ const object = () => {
268
+ // Parse an object value.
269
+ const keys = new Set();
270
+ next();
271
+ white();
272
+ if (ch === "}") {
273
+ next();
274
+ return; // empty object
275
+ }
276
+ let from;
277
+ while (ch) {
278
+ if (ch === "}") {
279
+ error("Trailing comma in object", from, from + 1);
280
+ }
281
+ from = at;
282
+ const key = string();
283
+ const to = at - 2;
284
+ white();
285
+ next(":");
286
+ if (keys.has(key)) {
287
+ warn(`Duplicate key ${stringify(key)}`, from, to);
288
+ }
289
+ else {
290
+ keys.add(key);
291
+ }
292
+ value();
293
+ white();
294
+ if (ch === "}") {
295
+ next();
296
+ return;
297
+ }
298
+ else if (ch === ",") {
299
+ from = at - 1;
300
+ next();
301
+ white();
302
+ }
303
+ else {
304
+ error(`Expected "," or "}" instead of ${stringify(ch)}`);
305
+ }
306
+ }
307
+ error(`Expected '"'`);
308
+ };
309
+ const value = () => {
310
+ // Parse a JSON value. It could be an object, an array, a string, a number,
311
+ // or a word.
312
+ white();
313
+ switch (ch) {
314
+ case "{":
315
+ return object();
316
+ case "[":
317
+ return array();
318
+ case '"':
319
+ return string();
320
+ case "-":
321
+ return number();
322
+ default:
323
+ return ch >= "0" && ch <= "9"
324
+ ? number()
325
+ : word();
326
+ }
327
+ };
328
+ // Return the json_parse function. It will have access to all of the above
329
+ // functions and variables.
330
+ return (source) => {
331
+ text = source;
332
+ warnings = [];
333
+ at = 0;
334
+ ch = " ";
335
+ value();
336
+ white();
337
+ if (ch) {
338
+ error("Syntax error");
339
+ }
340
+ else if (warnings.length > 0) {
341
+ throw { warnings };
342
+ }
343
+ };
344
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhsd/common",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "homepage": "https://github.com/bhsd-harry/common#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/bhsd-harry/common/issues"
@@ -8,7 +8,9 @@
8
8
  "license": "MIT",
9
9
  "author": "Bhsd",
10
10
  "files": [
11
- "/dist/"
11
+ "/dist/*.js",
12
+ "/dist/index.mjs",
13
+ "/dist/index.d.ts"
12
14
  ],
13
15
  "exports": {
14
16
  ".": {
@@ -21,13 +23,15 @@
21
23
  "sideEffects": false,
22
24
  "scripts": {
23
25
  "prepublishOnly": "npm run build",
24
- "build": "tsc && esbuild src/index.ts --charset=utf8 --target=es2024 --format=esm --outfile=dist/index.mjs",
26
+ "build": "tsc && esbuild src/index.ts --charset=utf8 --target=es2024 --bundle --format=esm --outfile=dist/index.mjs && cp dist/index.d.ts dist/index.d.mts",
25
27
  "lint:ts": "tsc --noEmit && eslint --cache .",
26
- "lint": "npm run lint:ts"
28
+ "lint": "npm run lint:ts",
29
+ "test": "mocha"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@bhsd/code-standard": "^1.3.2",
30
33
  "@stylistic/eslint-plugin": "^5.5.0",
34
+ "@types/mocha": "^10.0.10",
31
35
  "@typescript-eslint/eslint-plugin": "^8.46.4",
32
36
  "@typescript-eslint/parser": "^8.46.4",
33
37
  "esbuild": "^0.25.12",
@@ -39,6 +43,7 @@
39
43
  "eslint-plugin-promise": "^7.2.1",
40
44
  "eslint-plugin-regexp": "^2.10.0",
41
45
  "eslint-plugin-unicorn": "^62.0.0",
46
+ "mocha": "^11.7.5",
42
47
  "typescript": "^5.9.3"
43
48
  },
44
49
  "engines": {