neonctl 2.22.0 → 2.23.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/README.md +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/connection_string.test.js +0 -196
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { formatNumericLocale } from './units.js';
|
|
2
|
+
/**
|
|
3
|
+
* HTML printer.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors print.c `print_html_text` and `print_html_vertical`.
|
|
6
|
+
*
|
|
7
|
+
* Output shape (flat):
|
|
8
|
+
* <table border="1">
|
|
9
|
+
* <caption>title</caption>
|
|
10
|
+
* <tr>
|
|
11
|
+
* <th align="center">col</th>
|
|
12
|
+
* ...
|
|
13
|
+
* </tr>
|
|
14
|
+
* <tr valign="top">
|
|
15
|
+
* <td align="left">val</td>
|
|
16
|
+
* ...
|
|
17
|
+
* </tr>
|
|
18
|
+
* ...
|
|
19
|
+
* </table>
|
|
20
|
+
* <p>(N rows)<br />
|
|
21
|
+
* footer<br />
|
|
22
|
+
* </p>
|
|
23
|
+
*
|
|
24
|
+
* Output shape (expanded, `\pset expanded on`):
|
|
25
|
+
* <table border="1">
|
|
26
|
+
* <caption>title</caption>
|
|
27
|
+
*
|
|
28
|
+
* <tr><td colspan="2" align="center">Record 1</td></tr>
|
|
29
|
+
* <tr valign="top">
|
|
30
|
+
* <th>col</th>
|
|
31
|
+
* <td align="left">val</td>
|
|
32
|
+
* </tr>
|
|
33
|
+
* ...
|
|
34
|
+
* </table>
|
|
35
|
+
* <p>user footer<br />
|
|
36
|
+
* </p>
|
|
37
|
+
*
|
|
38
|
+
* - `topt.tableAttr` is appended to the opening `<table>` tag.
|
|
39
|
+
* - `topt.border` becomes the `border="..."` value.
|
|
40
|
+
* - `topt.startTable` / `stopTable` gate the prologue/epilogue, allowing
|
|
41
|
+
* chunked streaming. For this WP both default to true, so the full
|
|
42
|
+
* document is emitted in one call.
|
|
43
|
+
* - Right-align numeric columns based on the PG type-OID heuristic
|
|
44
|
+
* (`NUMERIC_OIDS`), matching how `print_aligned_text` derives the
|
|
45
|
+
* `aligns` array upstream.
|
|
46
|
+
* - Cells that are whitespace-only render ` ` so the cell still
|
|
47
|
+
* takes layout space (verbatim upstream behavior).
|
|
48
|
+
* - Expanded mode renders a `<tr><td colspan="2" align="center">Record
|
|
49
|
+
* N</td></tr>` separator before each row (or ` ` placeholder
|
|
50
|
+
* when `tuplesOnly`); it never emits the default `(N rows)` footer.
|
|
51
|
+
*/
|
|
52
|
+
// INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, INTERVAL.
|
|
53
|
+
const NUMERIC_OIDS = new Set([21, 23, 20, 700, 701, 1700, 1186]);
|
|
54
|
+
export const htmlPrinter = {
|
|
55
|
+
format: 'html',
|
|
56
|
+
printQuery(rs, opts, out) {
|
|
57
|
+
const topt = opts.topt;
|
|
58
|
+
if (topt.expanded === 'on') {
|
|
59
|
+
return printExpanded(rs, opts, out);
|
|
60
|
+
}
|
|
61
|
+
return printFlat(rs, opts, out);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const printFlat = (rs, opts, out) => {
|
|
65
|
+
const topt = opts.topt;
|
|
66
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
67
|
+
const startTable = topt.startTable;
|
|
68
|
+
const stopTable = topt.stopTable;
|
|
69
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
70
|
+
const title = opts.title ?? topt.title;
|
|
71
|
+
const footers = opts.footers ?? topt.footers;
|
|
72
|
+
const headers = rs.fields.map((f) => f.name);
|
|
73
|
+
const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'right' : 'left');
|
|
74
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
75
|
+
let buf = '';
|
|
76
|
+
if (startTable) {
|
|
77
|
+
buf += `<table border="${String(topt.border)}"`;
|
|
78
|
+
if (topt.tableAttr)
|
|
79
|
+
buf += ` ${topt.tableAttr}`;
|
|
80
|
+
buf += '>\n';
|
|
81
|
+
if (!tuplesOnly && title) {
|
|
82
|
+
buf += ' <caption>' + escapeHtml(title) + '</caption>\n';
|
|
83
|
+
}
|
|
84
|
+
if (!tuplesOnly) {
|
|
85
|
+
buf += ' <tr>\n';
|
|
86
|
+
for (const h of headers) {
|
|
87
|
+
buf += ' <th align="center">' + escapeHtml(h) + '</th>\n';
|
|
88
|
+
}
|
|
89
|
+
buf += ' </tr>\n';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const row of cells) {
|
|
93
|
+
buf += ' <tr valign="top">\n';
|
|
94
|
+
row.forEach((value, idx) => {
|
|
95
|
+
buf += ` <td align="${aligns[idx]}">`;
|
|
96
|
+
if (isWhitespaceOnly(value)) {
|
|
97
|
+
buf += ' ';
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
buf += escapeHtml(value);
|
|
101
|
+
}
|
|
102
|
+
buf += '</td>\n';
|
|
103
|
+
});
|
|
104
|
+
buf += ' </tr>\n';
|
|
105
|
+
}
|
|
106
|
+
if (stopTable) {
|
|
107
|
+
buf += '</table>\n';
|
|
108
|
+
if (!tuplesOnly) {
|
|
109
|
+
const effective = effectiveFooters(rs, topt, footers);
|
|
110
|
+
if (effective.length > 0) {
|
|
111
|
+
buf += '<p>';
|
|
112
|
+
for (const f of effective) {
|
|
113
|
+
buf += escapeHtml(f) + '<br />\n';
|
|
114
|
+
}
|
|
115
|
+
buf += '</p>';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Upstream `print_html_text` finishes with an unconditional
|
|
119
|
+
// `fputc('\n', fout)` AFTER the (optional) `<p>...</p>` block. The
|
|
120
|
+
// trailing newline appears regardless of footers / tuplesOnly.
|
|
121
|
+
buf += '\n';
|
|
122
|
+
}
|
|
123
|
+
out.write(buf);
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
};
|
|
126
|
+
const printExpanded = (rs, opts, out) => {
|
|
127
|
+
const topt = opts.topt;
|
|
128
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
129
|
+
const startTable = topt.startTable;
|
|
130
|
+
const stopTable = topt.stopTable;
|
|
131
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
132
|
+
const title = opts.title ?? topt.title;
|
|
133
|
+
const footers = opts.footers ?? topt.footers;
|
|
134
|
+
const headers = rs.fields.map((f) => f.name);
|
|
135
|
+
const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'right' : 'left');
|
|
136
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
137
|
+
let buf = '';
|
|
138
|
+
if (startTable) {
|
|
139
|
+
buf += `<table border="${String(topt.border)}"`;
|
|
140
|
+
if (topt.tableAttr)
|
|
141
|
+
buf += ` ${topt.tableAttr}`;
|
|
142
|
+
buf += '>\n';
|
|
143
|
+
if (!tuplesOnly && title) {
|
|
144
|
+
buf += ' <caption>' + escapeHtml(title) + '</caption>\n';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let record = topt.prior + 1;
|
|
148
|
+
cells.forEach((row) => {
|
|
149
|
+
if (!tuplesOnly) {
|
|
150
|
+
buf += `\n <tr><td colspan="2" align="center">Record ${String(record)}</td></tr>\n`;
|
|
151
|
+
record += 1;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
buf += '\n <tr><td colspan="2"> </td></tr>\n';
|
|
155
|
+
}
|
|
156
|
+
row.forEach((value, idx) => {
|
|
157
|
+
buf += ' <tr valign="top">\n';
|
|
158
|
+
buf += ' <th>' + escapeHtml(headers[idx]) + '</th>\n';
|
|
159
|
+
buf += ` <td align="${aligns[idx]}">`;
|
|
160
|
+
if (isWhitespaceOnly(value)) {
|
|
161
|
+
buf += ' ';
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
buf += escapeHtml(value);
|
|
165
|
+
}
|
|
166
|
+
buf += '</td>\n';
|
|
167
|
+
buf += ' </tr>\n';
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
if (stopTable) {
|
|
171
|
+
buf += '</table>\n';
|
|
172
|
+
// Expanded mode never emits the default "(N rows)" footer; only
|
|
173
|
+
// user-supplied footers appear (matches print_html_vertical).
|
|
174
|
+
if (!tuplesOnly && footers && footers.length > 0) {
|
|
175
|
+
buf += '<p>';
|
|
176
|
+
for (const f of footers) {
|
|
177
|
+
buf += escapeHtml(f) + '<br />\n';
|
|
178
|
+
}
|
|
179
|
+
buf += '</p>';
|
|
180
|
+
}
|
|
181
|
+
buf += '\n';
|
|
182
|
+
}
|
|
183
|
+
out.write(buf);
|
|
184
|
+
return Promise.resolve();
|
|
185
|
+
};
|
|
186
|
+
const effectiveFooters = (rs, topt, footers) => {
|
|
187
|
+
if (footers && footers.length > 0)
|
|
188
|
+
return footers;
|
|
189
|
+
if (topt.defaultFooter) {
|
|
190
|
+
const n = rs.rows.length;
|
|
191
|
+
return [`(${String(n)} ${n === 1 ? 'row' : 'rows'})`];
|
|
192
|
+
}
|
|
193
|
+
return [];
|
|
194
|
+
};
|
|
195
|
+
const isWhitespaceOnly = (s) => {
|
|
196
|
+
if (s.length === 0)
|
|
197
|
+
return true;
|
|
198
|
+
for (const ch of s) {
|
|
199
|
+
if (ch !== ' ' && ch !== '\t')
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
const escapeHtml = (input) => {
|
|
205
|
+
// Upstream `html_escaped_print` walks the string byte-by-byte and
|
|
206
|
+
// converts leading spaces (per line) to ` ` so EXPLAIN output
|
|
207
|
+
// stays indented. We replicate that with a stateful pass.
|
|
208
|
+
let out = '';
|
|
209
|
+
let leadingSpace = true;
|
|
210
|
+
for (const ch of input) {
|
|
211
|
+
switch (ch) {
|
|
212
|
+
case '&':
|
|
213
|
+
out += '&';
|
|
214
|
+
break;
|
|
215
|
+
case '<':
|
|
216
|
+
out += '<';
|
|
217
|
+
break;
|
|
218
|
+
case '>':
|
|
219
|
+
out += '>';
|
|
220
|
+
break;
|
|
221
|
+
case '"':
|
|
222
|
+
out += '"';
|
|
223
|
+
break;
|
|
224
|
+
case '\n':
|
|
225
|
+
out += '<br />\n';
|
|
226
|
+
break;
|
|
227
|
+
case ' ':
|
|
228
|
+
out += leadingSpace ? ' ' : ' ';
|
|
229
|
+
break;
|
|
230
|
+
default:
|
|
231
|
+
out += ch;
|
|
232
|
+
}
|
|
233
|
+
if (ch !== ' ')
|
|
234
|
+
leadingSpace = false;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
const renderCell = (cell, nullPrint, numericLocale) => {
|
|
239
|
+
if (cell === null || cell === undefined)
|
|
240
|
+
return nullPrint;
|
|
241
|
+
if (typeof cell === 'string') {
|
|
242
|
+
return formatNumericLocale(cell, numericLocale);
|
|
243
|
+
}
|
|
244
|
+
if (typeof cell === 'number' || typeof cell === 'bigint') {
|
|
245
|
+
return formatNumericLocale(cell.toString(), numericLocale);
|
|
246
|
+
}
|
|
247
|
+
if (typeof cell === 'boolean')
|
|
248
|
+
return cell ? 't' : 'f';
|
|
249
|
+
if (cell instanceof Date)
|
|
250
|
+
return cell.toISOString();
|
|
251
|
+
if (cell instanceof Uint8Array) {
|
|
252
|
+
let hex = '\\x';
|
|
253
|
+
for (const b of cell)
|
|
254
|
+
hex += b.toString(16).padStart(2, '0');
|
|
255
|
+
return hex;
|
|
256
|
+
}
|
|
257
|
+
return JSON.stringify(cell);
|
|
258
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON printer (used by `\gset`, `\gdesc`, and `--json` callers later
|
|
3
|
+
* in the WP plan).
|
|
4
|
+
*
|
|
5
|
+
* Output shape:
|
|
6
|
+
* [{ "col": value, ... }, ...]
|
|
7
|
+
*
|
|
8
|
+
* - Default emits a single-line array; `topt.expanded === 'on'`
|
|
9
|
+
* pretty-prints with two-space indentation.
|
|
10
|
+
* - SQL NULL → JSON null.
|
|
11
|
+
* - Numeric column data types (INT2/4/8, FLOAT4/8, NUMERIC) are
|
|
12
|
+
* parsed to JSON numbers when the string is a finite JS number.
|
|
13
|
+
* Anything outside `Number.isFinite` (large NUMERIC, NaN, Inf) is
|
|
14
|
+
* preserved as a string so we never silently lose precision.
|
|
15
|
+
* - Booleans, dates, bytea, and unknown objects render as their
|
|
16
|
+
* natural JSON forms (boolean / ISO string / hex-prefixed string /
|
|
17
|
+
* stringified).
|
|
18
|
+
*
|
|
19
|
+
* Output is deterministic: column key order matches `rs.fields`; we
|
|
20
|
+
* never reorder rows.
|
|
21
|
+
*/
|
|
22
|
+
// PostgreSQL type OIDs for the numeric family that map cleanly to
|
|
23
|
+
// JSON numbers. NUMERIC is included but guarded by isFinite().
|
|
24
|
+
const NUMERIC_TYPE_OIDS = new Set([
|
|
25
|
+
21,
|
|
26
|
+
23,
|
|
27
|
+
20,
|
|
28
|
+
700,
|
|
29
|
+
701,
|
|
30
|
+
1700, // NUMERIC
|
|
31
|
+
]);
|
|
32
|
+
export const jsonPrinter = {
|
|
33
|
+
format: 'json',
|
|
34
|
+
printQuery(rs, opts, out) {
|
|
35
|
+
const pretty = opts.topt.expanded === 'on';
|
|
36
|
+
const objects = rs.rows.map((row) => {
|
|
37
|
+
const obj = {};
|
|
38
|
+
rs.fields.forEach((field, idx) => {
|
|
39
|
+
obj[field.name] = renderCell(row[idx], field.dataTypeID);
|
|
40
|
+
});
|
|
41
|
+
return obj;
|
|
42
|
+
});
|
|
43
|
+
const serialized = pretty
|
|
44
|
+
? JSON.stringify(objects, null, 2)
|
|
45
|
+
: JSON.stringify(objects);
|
|
46
|
+
out.write(serialized + '\n');
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const renderCell = (cell, dataTypeID) => {
|
|
51
|
+
if (cell === null || cell === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
if (typeof cell === 'number') {
|
|
54
|
+
return Number.isFinite(cell) ? cell : cell.toString();
|
|
55
|
+
}
|
|
56
|
+
if (typeof cell === 'bigint') {
|
|
57
|
+
// Preserve as string when outside safe integer range.
|
|
58
|
+
const asNum = Number(cell);
|
|
59
|
+
return BigInt(asNum) === cell ? asNum : cell.toString();
|
|
60
|
+
}
|
|
61
|
+
if (typeof cell === 'boolean')
|
|
62
|
+
return cell;
|
|
63
|
+
if (cell instanceof Date)
|
|
64
|
+
return cell.toISOString();
|
|
65
|
+
if (cell instanceof Uint8Array) {
|
|
66
|
+
let hex = '\\x';
|
|
67
|
+
for (const b of cell)
|
|
68
|
+
hex += b.toString(16).padStart(2, '0');
|
|
69
|
+
return hex;
|
|
70
|
+
}
|
|
71
|
+
if (typeof cell === 'string') {
|
|
72
|
+
if (NUMERIC_TYPE_OIDS.has(dataTypeID)) {
|
|
73
|
+
const parsed = Number(cell);
|
|
74
|
+
if (Number.isFinite(parsed) && String(parsed) === normalize(cell)) {
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return cell;
|
|
79
|
+
}
|
|
80
|
+
return cell;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Normalize a numeric string so the `String(parsed) === normalize(cell)`
|
|
84
|
+
* round-trip is stable: strip a single leading `+`, drop trailing zeros
|
|
85
|
+
* after a decimal point, drop a trailing bare decimal point, and drop a
|
|
86
|
+
* leading zero before a multi-digit integer.
|
|
87
|
+
*/
|
|
88
|
+
const normalize = (s) => {
|
|
89
|
+
let v = s;
|
|
90
|
+
if (v.startsWith('+'))
|
|
91
|
+
v = v.slice(1);
|
|
92
|
+
if (v.includes('.')) {
|
|
93
|
+
v = v.replace(/0+$/, '').replace(/\.$/, '');
|
|
94
|
+
}
|
|
95
|
+
return v;
|
|
96
|
+
};
|