@isrd-isi-edu/ermrestjs 2.7.0 → 2.9.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/js/ag_reference.js +2 -1
- package/js/core.js +19 -16
- package/js/utils/helpers.js +2 -460
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/models/reference/tuple.ts +14 -1
- package/src/models/reference-column/foreign-key-pseudo-column.ts +10 -1
- package/src/models/reference-column/key-pseudo-column.ts +5 -1
- package/src/models/reference-column/reference-column.ts +10 -1
- package/src/services/handlebars.ts +23 -1
- package/src/utils/column-utils.ts +1 -1
- package/src/utils/constants.ts +78 -0
- package/src/utils/format-utils.ts +755 -0
- package/src/utils/type-utils.ts +7 -0
- package/tsconfig.json +1 -2
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3
|
+
|
|
4
|
+
import moment from 'moment-timezone';
|
|
5
|
+
|
|
6
|
+
// services
|
|
7
|
+
import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
8
|
+
|
|
9
|
+
// utils
|
|
10
|
+
import {
|
|
11
|
+
_classNames,
|
|
12
|
+
_dataFormats,
|
|
13
|
+
_datetimeDuration,
|
|
14
|
+
_specialPresentation,
|
|
15
|
+
DURATION_DIRECTION,
|
|
16
|
+
DURATION_UNIT,
|
|
17
|
+
type DurationDirection,
|
|
18
|
+
type DurationRealUnit,
|
|
19
|
+
type DurationUnit,
|
|
20
|
+
} from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
21
|
+
import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
|
|
22
|
+
import { isValidColorRGBHex } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
23
|
+
|
|
24
|
+
// legacy
|
|
25
|
+
import { _escapeMarkdownCharacters } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Truncate a number to a given number of significant digits.
|
|
29
|
+
*
|
|
30
|
+
* Note this **truncates** (`Math.floor`) rather than rounding, and operates on
|
|
31
|
+
* significant digits rather than decimal places — unlike `_toFixed`
|
|
32
|
+
* below. Used by `humanizeBytes` to keep byte counts from being padded.
|
|
33
|
+
*
|
|
34
|
+
* @param num the number to truncate
|
|
35
|
+
* @param precision target number of significant digits; coerced via `parseInt`
|
|
36
|
+
* and clamped to `minAllowedPrecision`
|
|
37
|
+
* @param minAllowedPrecision floor for `precision` (e.g. 3 for `si`, 4 for `binary`)
|
|
38
|
+
* @returns the truncated value as a string (sign-prefixed when negative). Falls
|
|
39
|
+
* back to the un-truncated number when the calculation overflows to NaN.
|
|
40
|
+
*/
|
|
41
|
+
export function _toPrecision(num: number, precision: any, minAllowedPrecision: number): string | number {
|
|
42
|
+
precision = parseInt(precision);
|
|
43
|
+
precision = isNaN(precision) || precision < minAllowedPrecision ? minAllowedPrecision : precision;
|
|
44
|
+
|
|
45
|
+
const isNegative = num < 0;
|
|
46
|
+
if (isNegative) num = num * -1;
|
|
47
|
+
|
|
48
|
+
// Truncation here depends on `minAllowedPrecision`; relaxing the floor needs new math.
|
|
49
|
+
let displayedNum: any = num.toString();
|
|
50
|
+
const f = displayedNum.indexOf('.');
|
|
51
|
+
if (f !== -1) {
|
|
52
|
+
// find the number of digits after decimal point
|
|
53
|
+
const decimalPlaces = Math.pow(10, precision - f);
|
|
54
|
+
|
|
55
|
+
// truncate the value
|
|
56
|
+
displayedNum = Math.floor(num * decimalPlaces) / decimalPlaces;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// if precision is too large, the calculation might return NaN.
|
|
60
|
+
if (isNaN(displayedNum)) {
|
|
61
|
+
return (isNegative ? '-' : '') + num;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (isNegative ? '-' : '') + displayedNum;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Round `value` to `fraction` decimal places and emit it as a string with
|
|
69
|
+
* trailing zeros preserved. Decimal-places + rounding semantics — NOT
|
|
70
|
+
* `_toPrecision`, which truncates on significant digits.
|
|
71
|
+
*
|
|
72
|
+
* Distinct from `Number.prototype.toFixed` in that the value is explicitly
|
|
73
|
+
* rounded via `Math.round` before formatting, avoiding the platform-specific
|
|
74
|
+
* banker's-rounding edge cases that `toFixed` exhibits.
|
|
75
|
+
*
|
|
76
|
+
* @param value the number to format
|
|
77
|
+
* @param fraction number of decimal places to retain
|
|
78
|
+
* @returns the rounded number as a fixed-precision string
|
|
79
|
+
*/
|
|
80
|
+
function _toFixed(value: number, fraction: number): string {
|
|
81
|
+
const m = Math.pow(10, fraction);
|
|
82
|
+
return (Math.round(value * m) / m).toFixed(fraction);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pick the largest unit (year > month > ... > millisecond) for which the
|
|
87
|
+
* given duration is at least one whole unit. Used by `unit="auto"`.
|
|
88
|
+
*
|
|
89
|
+
* @param absMs the absolute duration in milliseconds
|
|
90
|
+
* @returns the chosen unit name (e.g. `'year'`, `'second'`)
|
|
91
|
+
*/
|
|
92
|
+
function _durationPickAutoUnit(absMs: number): DurationRealUnit {
|
|
93
|
+
for (let i = 0; i < _datetimeDuration.UNIT_ORDER.length; i++) {
|
|
94
|
+
const u = _datetimeDuration.UNIT_ORDER[i];
|
|
95
|
+
if (absMs / _datetimeDuration.CONVERSION_RATES[u] >= 1) return u;
|
|
96
|
+
}
|
|
97
|
+
return DURATION_UNIT.MILLISECOND;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Format a duration in a single named unit (e.g. `"1.5 days"`). When
|
|
102
|
+
* `unit === 'auto'`, the unit is picked via `_durationPickAutoUnit`. Always
|
|
103
|
+
* uses fixed Julian conversion rates (1Y = 365.25d, 1M = 30.4375d).
|
|
104
|
+
*
|
|
105
|
+
* @param absMs the absolute duration in milliseconds
|
|
106
|
+
* @param unit one of the unit names in `_datetimeDuration.UNIT_ORDER`, or `'auto'`
|
|
107
|
+
* @param fraction number of decimal places to show
|
|
108
|
+
* @returns the formatted duration with a pluralized unit suffix
|
|
109
|
+
*/
|
|
110
|
+
function _durationFormatSingleUnit(absMs: number, unit: DurationUnit, fraction: number): string {
|
|
111
|
+
const realUnit: DurationRealUnit = unit === DURATION_UNIT.AUTO ? _durationPickAutoUnit(absMs) : (unit as DurationRealUnit);
|
|
112
|
+
const value = absMs / _datetimeDuration.CONVERSION_RATES[realUnit];
|
|
113
|
+
return _toFixed(value, fraction) + ' ' + realUnit + 's';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format a duration as a chain of fixed-math unit components
|
|
118
|
+
* (e.g. `"1M 4D 13h 30m 0s"`). Walks year > month > ... > minute consuming
|
|
119
|
+
* whole units, then puts any remainder on seconds at the configured precision.
|
|
120
|
+
*
|
|
121
|
+
* Because the math is fixed-Julian rather than calendar-aware, round calendar
|
|
122
|
+
* diffs will show "lopover": e.g. 365 days renders as `"11M 30D 4h 30m"`
|
|
123
|
+
* since 1M = 30.4375d. For clean calendar output use `_durationFormatCalendarWalk`.
|
|
124
|
+
*
|
|
125
|
+
* @param absMs the absolute duration in milliseconds
|
|
126
|
+
* @param fraction decimal places shown on the seconds component
|
|
127
|
+
* @returns space-separated multi-unit string
|
|
128
|
+
*/
|
|
129
|
+
function _durationFormatMultiFixed(absMs: number, fraction: number): string {
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
let rest = absMs;
|
|
132
|
+
const midUnits: Array<keyof typeof _datetimeDuration.CONVERSION_RATES> = ['year', 'month', 'day', 'hour', 'minute'];
|
|
133
|
+
for (let i = 0; i < midUnits.length; i++) {
|
|
134
|
+
const u = midUnits[i];
|
|
135
|
+
const whole = Math.floor(rest / _datetimeDuration.CONVERSION_RATES[u]);
|
|
136
|
+
if (whole > 0) parts.push(whole + _datetimeDuration.ABBREVIATIONS[u]);
|
|
137
|
+
rest -= whole * _datetimeDuration.CONVERSION_RATES[u];
|
|
138
|
+
}
|
|
139
|
+
const sec = rest / _datetimeDuration.CONVERSION_RATES.second;
|
|
140
|
+
if (sec > 0 || parts.length === 0) {
|
|
141
|
+
parts.push(_toFixed(sec, fraction) + 's');
|
|
142
|
+
}
|
|
143
|
+
return parts.join(' ');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format a duration as a chain of calendar-aware unit components using
|
|
148
|
+
* `moment.diff` + `moment.add`. Yields clean breakdowns for round calendar
|
|
149
|
+
* diffs (1 year → `"1Y"`, 1 month + 4 days → `"1M 4D"`), unlike the
|
|
150
|
+
* fixed-math variant.
|
|
151
|
+
*
|
|
152
|
+
* Two quirks to know about:
|
|
153
|
+
* - Operates in UTC so calendar boundaries don't shift with the host's local
|
|
154
|
+
* time zone (without this, "Jan 31Z → Feb 28Z" on an EST host would walk
|
|
155
|
+
* through "Jan 30 19:00 → Feb 27 19:00" and report 28 days instead of 1 month).
|
|
156
|
+
* - Inherits moment's month-end clamping: `Jan 31 → Feb 28` consumes a whole
|
|
157
|
+
* month, producing `"1M 0D"`. This is moment's documented behavior.
|
|
158
|
+
*
|
|
159
|
+
* @param startM the start moment (sign re-applied by the caller)
|
|
160
|
+
* @param endM the end moment
|
|
161
|
+
* @param fraction decimal places shown on the seconds component
|
|
162
|
+
* @returns space-separated multi-unit string
|
|
163
|
+
*/
|
|
164
|
+
function _durationFormatCalendarWalk(startM: moment.Moment, endM: moment.Moment, fraction: number): string {
|
|
165
|
+
let a: moment.Moment, b: moment.Moment;
|
|
166
|
+
if (endM.isBefore(startM)) {
|
|
167
|
+
a = endM.clone().utc();
|
|
168
|
+
b = startM.clone().utc();
|
|
169
|
+
} else {
|
|
170
|
+
a = startM.clone().utc();
|
|
171
|
+
b = endM.clone().utc();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const cursor = a.clone();
|
|
175
|
+
const parts: string[] = [];
|
|
176
|
+
const midUnits: Array<'year' | 'month' | 'day' | 'hour' | 'minute'> = ['year', 'month', 'day', 'hour', 'minute'];
|
|
177
|
+
for (let i = 0; i < midUnits.length; i++) {
|
|
178
|
+
const u = midUnits[i];
|
|
179
|
+
const n = b.diff(cursor, u);
|
|
180
|
+
if (n > 0) parts.push(n + _datetimeDuration.ABBREVIATIONS[u]);
|
|
181
|
+
cursor.add(n, u);
|
|
182
|
+
}
|
|
183
|
+
const sec = b.diff(cursor) / 1000;
|
|
184
|
+
if (sec > 0 || parts.length === 0) {
|
|
185
|
+
parts.push(_toFixed(sec, fraction) + 's');
|
|
186
|
+
}
|
|
187
|
+
return parts.join(' ');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Apply the requested direction modifier to a formatted duration.
|
|
192
|
+
*
|
|
193
|
+
* Behavior:
|
|
194
|
+
* - `unsigned` returns the value unchanged.
|
|
195
|
+
* - `before/after` appends `"before"` or `"after"`.
|
|
196
|
+
* - `earlier/later` appends `"earlier"` or `"later"`.
|
|
197
|
+
* - `sign` (default) prepends `+` or `-`.
|
|
198
|
+
*
|
|
199
|
+
* @param visible the duration string to decorate
|
|
200
|
+
* @param signSign +1 when end is after start, -1 otherwise
|
|
201
|
+
* @param direction one of the values in `_datetimeDuration.VALID_DIRECTIONS`
|
|
202
|
+
* @returns the directionally-adorned duration string
|
|
203
|
+
*/
|
|
204
|
+
function _durationApplyDirection(visible: string, signSign: number, direction: DurationDirection): string {
|
|
205
|
+
if (direction === DURATION_DIRECTION.UNSIGNED) return visible;
|
|
206
|
+
if (direction === DURATION_DIRECTION.BEFORE_AFTER) return visible + ' ' + (signSign > 0 ? 'after' : 'before');
|
|
207
|
+
if (direction === DURATION_DIRECTION.EARLIER_LATER) return visible + ' ' + (signSign > 0 ? 'later' : 'earlier');
|
|
208
|
+
return (signSign > 0 ? '+' : '-') + visible;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Wrap a formatted duration in a Chaise tooltip span. The tooltip body shows
|
|
213
|
+
* the fixed-math multi-unit form on line 1 and the Julian conversion rates
|
|
214
|
+
* for only the units that appear in line 1 on a second line.
|
|
215
|
+
*
|
|
216
|
+
* @param visible the visible (signed) duration text
|
|
217
|
+
* @param absMs the absolute duration in ms (used to derive the tooltip body)
|
|
218
|
+
* @returns the wrapped Chaise tooltip span markup
|
|
219
|
+
*/
|
|
220
|
+
function _durationWrapTooltip(visible: string, absMs: number): string {
|
|
221
|
+
const multi = _durationFormatMultiFixed(absMs, 2);
|
|
222
|
+
|
|
223
|
+
const used = new Set<string>();
|
|
224
|
+
for (const tok of multi.split(' ')) {
|
|
225
|
+
const m = tok.match(/[a-zA-Z]+$/);
|
|
226
|
+
if (m) used.add(m[0]);
|
|
227
|
+
}
|
|
228
|
+
const ratesByAbbrev: ReadonlyArray<[string, string]> = [
|
|
229
|
+
['Y', 'Y = 365.25 days'],
|
|
230
|
+
['M', 'M = 30.4375 days'],
|
|
231
|
+
['D', 'D = 24 hours'],
|
|
232
|
+
['h', 'h = 60 minutes'],
|
|
233
|
+
['m', 'm = 60 seconds'],
|
|
234
|
+
];
|
|
235
|
+
const usedRates = ratesByAbbrev.filter(([abbrev]) => used.has(abbrev)).map(([, text]) => text);
|
|
236
|
+
// Two rates per line to keep the tooltip from getting too wide.
|
|
237
|
+
const rateLines: string[] = [];
|
|
238
|
+
for (let i = 0; i < usedRates.length; i += 2) {
|
|
239
|
+
rateLines.push(usedRates.slice(i, i + 2).join(', '));
|
|
240
|
+
}
|
|
241
|
+
const rates = rateLines.join(' ');
|
|
242
|
+
|
|
243
|
+
// ` ` instead of literal `\n`: a real newline inside `{...}` breaks
|
|
244
|
+
// markdown-it-attrs's parsing of the attribute block.
|
|
245
|
+
const tooltip = rates ? multi + ' ' + rates : multi;
|
|
246
|
+
return ':span:' + visible + ':/span:{data-chaise-tooltip="' + tooltip + '"}';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Printable formatters (the contents of _formatUtils) ---
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Format a boolean value for display.
|
|
253
|
+
*
|
|
254
|
+
* @param value the value to format; `null` returns an empty string
|
|
255
|
+
* @param options unused (kept for signature parity with the other formatters)
|
|
256
|
+
* @returns `"true"`, `"false"`, or `""` for null
|
|
257
|
+
*/
|
|
258
|
+
export function printBoolean(value: any, options?: any): string {
|
|
259
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
260
|
+
if (value === null) {
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
return Boolean(value).toString();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Format an integer value with thousands separators (e.g. `1234567` -> `"1,234,567"`).
|
|
268
|
+
* Any fractional part is rounded off via `Math.round`.
|
|
269
|
+
*
|
|
270
|
+
* @param value the value to format; `null` returns an empty string
|
|
271
|
+
* @param options unused (kept for signature parity with the other formatters)
|
|
272
|
+
* @returns the comma-separated integer string
|
|
273
|
+
*/
|
|
274
|
+
export function printInteger(value: any, options?: any): string {
|
|
275
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
276
|
+
if (value === null) {
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Remove fractional digits
|
|
281
|
+
value = Math.round(value);
|
|
282
|
+
|
|
283
|
+
// Add comma separators
|
|
284
|
+
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Format a timestamp value into the project's display format
|
|
289
|
+
* (`_dataFormats.DATETIME.display`, e.g. `"2017-01-08 15:06:02"`).
|
|
290
|
+
*
|
|
291
|
+
* @param value any value moment() can parse (also `.toString()`-able)
|
|
292
|
+
* @param options unused (kept for signature parity)
|
|
293
|
+
* @returns the formatted timestamp, or `""` when the input is invalid/null
|
|
294
|
+
*/
|
|
295
|
+
export function printTimestamp(value: any, options?: any): string {
|
|
296
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
297
|
+
if (value === null) {
|
|
298
|
+
return '';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
value = value.toString();
|
|
303
|
+
} catch (exception) {
|
|
304
|
+
$log.error("Couldn't extract timestamp from input: " + value);
|
|
305
|
+
$log.error(exception);
|
|
306
|
+
return '';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!moment(value).isValid()) {
|
|
310
|
+
$log.error("Couldn't transform input to a valid timestamp: " + value);
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return moment(value).format(_dataFormats.DATETIME.display);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Format a date (or date-time) value into the project's date display format
|
|
319
|
+
* (`_dataFormats.DATE`). Any time component is dropped.
|
|
320
|
+
*
|
|
321
|
+
* @param value any value moment() can parse (also `.toString()`-able)
|
|
322
|
+
* @param options unused (kept for signature parity)
|
|
323
|
+
* @returns the formatted date, or `""` when the input is invalid/null
|
|
324
|
+
*/
|
|
325
|
+
export function printDate(value: any, options?: any): string {
|
|
326
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
327
|
+
if (value === null) {
|
|
328
|
+
return '';
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
value = value.toString();
|
|
332
|
+
} catch (exception) {
|
|
333
|
+
$log.error("Couldn't extract date info from input: " + value);
|
|
334
|
+
$log.error(exception);
|
|
335
|
+
return '';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!moment(value).isValid()) {
|
|
339
|
+
$log.error("Couldn't transform input to a valid date: " + value);
|
|
340
|
+
return '';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return moment(value).format(_dataFormats.DATE);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Format a float for display: leading zeros stripped, thousands separators
|
|
348
|
+
* added. When no explicit precision is given, values >= 1e12 or < 1e-6 fall
|
|
349
|
+
* back to scientific notation; otherwise four fractional digits are shown.
|
|
350
|
+
*
|
|
351
|
+
* @param value the value to format; `null` returns an empty string
|
|
352
|
+
* @param options optional config:
|
|
353
|
+
* - `numFracDigits` — number of fractional digits to keep (`toFixed` rounds)
|
|
354
|
+
* @returns the formatted float string
|
|
355
|
+
*/
|
|
356
|
+
export function printFloat(value: any, options?: any): string {
|
|
357
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
358
|
+
|
|
359
|
+
if (value === null) {
|
|
360
|
+
return '';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
value = parseFloat(value);
|
|
364
|
+
if (options.numFracDigits) {
|
|
365
|
+
value = value.toFixed(options.numFracDigits); // toFixed() rounds the value, is ok?
|
|
366
|
+
} else {
|
|
367
|
+
// >= 1e12 or < 1e-6: switch to scientific notation. toPrecision(5) ensures
|
|
368
|
+
// enough digits that the result formats as exponential rather than padded zeros.
|
|
369
|
+
if (Math.abs(value) >= 1000000000000 || Math.abs(value) < 0.000001) {
|
|
370
|
+
value = value.toPrecision(5);
|
|
371
|
+
} else {
|
|
372
|
+
value = value.toFixed(4);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Remove leading zeroes
|
|
377
|
+
value = value.toString().replace(/^0+(?!\.|$)/, '');
|
|
378
|
+
|
|
379
|
+
// Add comma separators
|
|
380
|
+
const parts = value.split('.');
|
|
381
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
382
|
+
return parts.join('.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Format a text value for display. Objects are JSON-stringified; everything
|
|
387
|
+
* else goes through `.toString()`.
|
|
388
|
+
*
|
|
389
|
+
* @param value the value to format; `null` returns an empty string
|
|
390
|
+
* @param options unused (kept for signature parity)
|
|
391
|
+
* @returns the value as a string
|
|
392
|
+
*/
|
|
393
|
+
export function printText(value: any, options?: any): string {
|
|
394
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
395
|
+
if (value === null) {
|
|
396
|
+
return '';
|
|
397
|
+
}
|
|
398
|
+
if (typeof value === 'object') {
|
|
399
|
+
return JSON.stringify(value);
|
|
400
|
+
}
|
|
401
|
+
return value.toString();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Render a markdown string to HTML for display.
|
|
406
|
+
*
|
|
407
|
+
* @param value the markdown source; `null` returns an empty string
|
|
408
|
+
* @param options optional config:
|
|
409
|
+
* - `inline` — render in inline mode (no block wrapping)
|
|
410
|
+
* @returns the rendered HTML
|
|
411
|
+
*/
|
|
412
|
+
export function printMarkdown(value: any, options?: any): string {
|
|
413
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
414
|
+
if (value === null) {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return renderMarkdown(value, options.inline);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Format a JSON value as a pretty-printed string for display. An empty-string
|
|
423
|
+
* input is treated as `null` (so `""` renders as `"null"`).
|
|
424
|
+
*
|
|
425
|
+
* @param value any JSON-serializable value
|
|
426
|
+
* @param _options unused (kept for signature parity)
|
|
427
|
+
* @returns the JSON-stringified value (2-space indent)
|
|
428
|
+
*/
|
|
429
|
+
export function printJSON(value: any, _options?: any): string {
|
|
430
|
+
return value === '' ? JSON.stringify(null) : JSON.stringify(value, undefined, 2);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Format a gene sequence in chunks separated by a delimiter, then render the
|
|
435
|
+
* result through markdown so the output appears in a fixed-width font.
|
|
436
|
+
* By default the sequence is split into 10-character chunks separated by a space.
|
|
437
|
+
*
|
|
438
|
+
* @param value the raw gene sequence string; `null` returns an empty string
|
|
439
|
+
* @param options optional config:
|
|
440
|
+
* - `increment` — number of characters per chunk (default 10, negatives clamp to 1, 0 disables chunking)
|
|
441
|
+
* - `separator` — chunk delimiter (default `' '`)
|
|
442
|
+
* @returns the chunked sequence rendered as inline markdown, or the original
|
|
443
|
+
* value when an exception bubbles out of the rendering layer
|
|
444
|
+
*/
|
|
445
|
+
export function printGeneSeq(value: any, options?: any): string {
|
|
446
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
447
|
+
|
|
448
|
+
if (value === null) {
|
|
449
|
+
return '';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
// Default separator is a space.
|
|
454
|
+
if (!options.separator) {
|
|
455
|
+
options.separator = ' ';
|
|
456
|
+
}
|
|
457
|
+
// Default increment is 10
|
|
458
|
+
if (!options.increment) {
|
|
459
|
+
options.increment = 10;
|
|
460
|
+
}
|
|
461
|
+
let inc = parseInt(options.increment, 10);
|
|
462
|
+
|
|
463
|
+
if (inc === 0) {
|
|
464
|
+
return value.toString();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Reset the increment if it's negative
|
|
468
|
+
if (inc <= -1) {
|
|
469
|
+
inc = 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let formattedSeq = '`';
|
|
473
|
+
const separator = options.separator;
|
|
474
|
+
while (value.length >= inc) {
|
|
475
|
+
// Get the first inc number of chars
|
|
476
|
+
const chunk = value.slice(0, inc);
|
|
477
|
+
// Append the chunk and separator
|
|
478
|
+
formattedSeq += chunk + separator;
|
|
479
|
+
// Remove this chunk from value
|
|
480
|
+
value = value.slice(inc);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Append any remaining chars from value that was too small to form an increment
|
|
484
|
+
formattedSeq += value;
|
|
485
|
+
|
|
486
|
+
// Slice off separator at the end
|
|
487
|
+
if (formattedSeq.slice(-1) == separator) {
|
|
488
|
+
formattedSeq = formattedSeq.slice(0, -1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Add the ending backtick at the end
|
|
492
|
+
formattedSeq += '`';
|
|
493
|
+
|
|
494
|
+
// Run it through renderMarkdown to get the sequence in a fixed-width font
|
|
495
|
+
return renderMarkdown(formattedSeq, true);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
$log.error("Couldn't parse the given markdown value: " + value);
|
|
498
|
+
$log.error(e);
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Format an array into a comma-separated string (or an array of formatted
|
|
505
|
+
* strings). `null` and `""` entries are replaced with their special markdown
|
|
506
|
+
* presentation tokens (`_specialPresentation.NULL` and `EMPTY_STR`).
|
|
507
|
+
*
|
|
508
|
+
* @param value the array to format; non-arrays / empty arrays return `""`
|
|
509
|
+
* @param options optional config:
|
|
510
|
+
* - `isMarkdown` — when true, do NOT markdown-escape the entries
|
|
511
|
+
* - `returnArray` — when true, return the formatted entries as an array
|
|
512
|
+
* instead of joining with `", "`
|
|
513
|
+
* @returns the joined string by default, or `string[]` when `returnArray` is set
|
|
514
|
+
*/
|
|
515
|
+
export function printArray(value: any, options?: any): any {
|
|
516
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
517
|
+
|
|
518
|
+
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
519
|
+
return '';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const arr = value.map(function (v) {
|
|
523
|
+
let isMarkdown = options.isMarkdown === true;
|
|
524
|
+
let pv = v;
|
|
525
|
+
if (v === '') {
|
|
526
|
+
pv = _specialPresentation.EMPTY_STR;
|
|
527
|
+
isMarkdown = true;
|
|
528
|
+
} else if (v == null) {
|
|
529
|
+
pv = _specialPresentation.NULL;
|
|
530
|
+
isMarkdown = true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!isMarkdown) pv = _escapeMarkdownCharacters(pv);
|
|
534
|
+
return pv;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
if (options.returnArray) return arr;
|
|
538
|
+
return arr.join(', ');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Format a hex color string as a Chaise markdown span with a colored swatch
|
|
543
|
+
* followed by the uppercase hex code. Invalid colors return an empty string.
|
|
544
|
+
*
|
|
545
|
+
* @param value the hex color string (e.g. `"#ff8800"`)
|
|
546
|
+
* @param options unused (kept for signature parity)
|
|
547
|
+
* @returns the markdown swatch + label, or `""` when the input is not a valid hex color
|
|
548
|
+
*/
|
|
549
|
+
export function printColor(value: any, options?: any): string {
|
|
550
|
+
options = typeof options === 'undefined' ? {} : options;
|
|
551
|
+
|
|
552
|
+
if (!isValidColorRGBHex(value)) {
|
|
553
|
+
return '';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
value = value.toUpperCase();
|
|
557
|
+
return ':span: :/span:{.' + _classNames.colorPreview + ' style=background-color:' + value + '} ' + value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Format a byte count in a human-readable unit (e.g. `1234567` -> `"1.23 MB"`).
|
|
562
|
+
*
|
|
563
|
+
* The value is **truncated** (never rounded up) to honor the requested
|
|
564
|
+
* precision via `_toPrecision`. The precision floor is 3 in `si` mode and 4
|
|
565
|
+
* in `binary` mode; smaller values are clamped. `raw` mode bypasses the unit
|
|
566
|
+
* conversion and returns the comma-separated integer.
|
|
567
|
+
*
|
|
568
|
+
* @param value the byte count (anything `parseFloat` accepts; `NaN` returns `""`)
|
|
569
|
+
* @param mode `'raw'`, `'si'`, or `'binary'` (default `'si'` for invalid/missing)
|
|
570
|
+
* @param precision number of significant digits to display (default 3, see
|
|
571
|
+
* the per-mode floor above)
|
|
572
|
+
* @param withTooltip when true and a unit conversion occurred, wrap the
|
|
573
|
+
* output in a Chaise tooltip span showing the raw byte
|
|
574
|
+
* count and the unit conversion factor
|
|
575
|
+
* @returns the humanized byte string
|
|
576
|
+
*/
|
|
577
|
+
export function humanizeBytes(value: any, mode?: any, precision?: any, withTooltip?: any): string {
|
|
578
|
+
// we cannot use parseInt here since it won't allow larger numbers.
|
|
579
|
+
let v = parseFloat(value);
|
|
580
|
+
mode = ['raw', 'si', 'binary'].indexOf(mode) === -1 ? 'si' : mode;
|
|
581
|
+
|
|
582
|
+
if (isNaN(v)) return '';
|
|
583
|
+
if (v === 0 || mode === 'raw') {
|
|
584
|
+
return printInteger(value);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let divisor = 1000;
|
|
588
|
+
let units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
589
|
+
if (mode === 'binary') {
|
|
590
|
+
divisor = 1024;
|
|
591
|
+
units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// find the closest power of the divisor to the given number ('u').
|
|
595
|
+
// in the end, 'v' will be the number that we should display.
|
|
596
|
+
let u = 0;
|
|
597
|
+
while (v >= divisor || -v >= divisor) {
|
|
598
|
+
v /= divisor;
|
|
599
|
+
u++;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// our units don't support this, so just return the "raw" mode value.
|
|
603
|
+
if (u >= units.length) {
|
|
604
|
+
return printInteger(value);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// we don't want to truncate the value, so we should set a minimum
|
|
608
|
+
const minP = mode === 'si' ? 3 : 4;
|
|
609
|
+
|
|
610
|
+
let res = (u ? _toPrecision(v, precision, minP) : v) + ' ' + units[u];
|
|
611
|
+
if (typeof withTooltip === 'boolean' && withTooltip && u > 0) {
|
|
612
|
+
const numBytes = printInteger(Math.pow(divisor, u));
|
|
613
|
+
let tooltip = printInteger(value);
|
|
614
|
+
tooltip += ' bytes (1 ' + units[u] + ' = ' + numBytes + ' bytes)';
|
|
615
|
+
res = ':span:' + res + ':/span:{data-chaise-tooltip="' + tooltip + '"}';
|
|
616
|
+
}
|
|
617
|
+
return res;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Format the duration between two datetimes for display
|
|
622
|
+
* (e.g. `"+1.1 months"`, `"35 days before"`, `"1Y 4D"`).
|
|
623
|
+
*
|
|
624
|
+
* Math is fixed-Julian (1Y = 365.25d, 1M = 30.4375d) for every single-unit
|
|
625
|
+
* mode and for `unit="multi"`. Calendar-aware walking via `moment.diff`/
|
|
626
|
+
* `moment.add` is used only for `unit="calendar"`. See the per-helper JSDocs
|
|
627
|
+
* above for the quirks of each mode (multi lopover, calendar month-end clamping).
|
|
628
|
+
*
|
|
629
|
+
* Edge cases:
|
|
630
|
+
* - `start === end` returns `"0 seconds"` regardless of direction.
|
|
631
|
+
* - Either side invalid (not moment-parseable) returns `""`.
|
|
632
|
+
*
|
|
633
|
+
* @param start any value moment() can parse
|
|
634
|
+
* @param end any value moment() can parse
|
|
635
|
+
* @param unit one of `"auto"` (default), `"year"`, `"month"`, `"day"`,
|
|
636
|
+
* `"hour"`, `"minute"`, `"second"`, `"millisecond"`, `"multi"`, `"calendar"`
|
|
637
|
+
* @param fraction number of decimal places (default 1; invalid -> 1)
|
|
638
|
+
* @param direction one of `"sign"` (default, prepends `+`/`-`),
|
|
639
|
+
* `"before/after"`, `"earlier/later"`, `"unsigned"`
|
|
640
|
+
* @param withTooltip when true, wrap the output in a tooltip span; suppressed
|
|
641
|
+
* only for `unit="calendar"` (the calendar walk in the
|
|
642
|
+
* tooltip body would conflict with the calendar walk
|
|
643
|
+
* already visible in line 1)
|
|
644
|
+
* @returns the formatted duration string
|
|
645
|
+
*/
|
|
646
|
+
export function datetimeDuration(start: any, end: any, unit?: any, fraction?: any, direction?: any, withTooltip?: any): string {
|
|
647
|
+
const startM = moment(start);
|
|
648
|
+
const endM = moment(end);
|
|
649
|
+
if (!startM.isValid() || !endM.isValid()) return '';
|
|
650
|
+
|
|
651
|
+
// normalize args
|
|
652
|
+
const normUnit: DurationUnit = _datetimeDuration.VALID_UNITS.indexOf(unit) === -1 ? DURATION_UNIT.AUTO : unit;
|
|
653
|
+
const normDirection: DurationDirection = _datetimeDuration.VALID_DIRECTIONS.indexOf(direction) === -1 ? DURATION_DIRECTION.SIGN : direction;
|
|
654
|
+
const f = parseInt(fraction);
|
|
655
|
+
fraction = isNaN(f) || f < 0 ? 1 : f;
|
|
656
|
+
|
|
657
|
+
const ms = endM.diff(startM);
|
|
658
|
+
if (ms === 0) return '0 seconds';
|
|
659
|
+
|
|
660
|
+
const absMs = Math.abs(ms);
|
|
661
|
+
const signSign = ms > 0 ? 1 : -1;
|
|
662
|
+
|
|
663
|
+
let res: string;
|
|
664
|
+
if (normUnit === DURATION_UNIT.MULTI) {
|
|
665
|
+
res = _durationFormatMultiFixed(absMs, fraction);
|
|
666
|
+
} else if (normUnit === DURATION_UNIT.CALENDAR) {
|
|
667
|
+
res = _durationFormatCalendarWalk(startM, endM, fraction);
|
|
668
|
+
} else {
|
|
669
|
+
res = _durationFormatSingleUnit(absMs, normUnit, fraction);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
res = _durationApplyDirection(res, signSign, normDirection);
|
|
673
|
+
|
|
674
|
+
if (withTooltip === true && normUnit !== DURATION_UNIT.CALENDAR) {
|
|
675
|
+
res = _durationWrapTooltip(res, absMs);
|
|
676
|
+
}
|
|
677
|
+
return res;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Pretty-print utility functions, exposed as the public `_formatUtils` object
|
|
682
|
+
* on the `ERMrest` global (`ERMrest._formatUtils.printX(...)`). Each method
|
|
683
|
+
* is also exported by name above for direct in-codebase imports.
|
|
684
|
+
*/
|
|
685
|
+
export const _formatUtils = {
|
|
686
|
+
printBoolean,
|
|
687
|
+
printInteger,
|
|
688
|
+
printTimestamp,
|
|
689
|
+
printDate,
|
|
690
|
+
printFloat,
|
|
691
|
+
printText,
|
|
692
|
+
printMarkdown,
|
|
693
|
+
printJSON,
|
|
694
|
+
printGeneSeq,
|
|
695
|
+
printArray,
|
|
696
|
+
printColor,
|
|
697
|
+
humanizeBytes,
|
|
698
|
+
datetimeDuration,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Dispatch to the appropriate `print*` formatter based on a column type
|
|
703
|
+
* definition. Recurses into `type.baseType` when no case matches (and falls
|
|
704
|
+
* back to `printText` when even the base type is unknown). Markdown columns
|
|
705
|
+
* are passed through with `toString()` because final markdown rendering
|
|
706
|
+
* happens later in the pipeline.
|
|
707
|
+
*
|
|
708
|
+
* @param type the column type descriptor (uses `type.name` and `type.baseType`)
|
|
709
|
+
* @param data the raw value to format
|
|
710
|
+
* @param options forwarded to the underlying `print*` formatter
|
|
711
|
+
* @returns the formatted value (typically a string, but the type matches the underlying formatter)
|
|
712
|
+
*/
|
|
713
|
+
export function _formatValueByType(type: any, data: any, options?: any): any {
|
|
714
|
+
switch (type.name) {
|
|
715
|
+
case 'timestamp':
|
|
716
|
+
case 'timestamptz':
|
|
717
|
+
data = printTimestamp(data, options);
|
|
718
|
+
break;
|
|
719
|
+
case 'date':
|
|
720
|
+
data = printDate(data, options);
|
|
721
|
+
break;
|
|
722
|
+
case 'numeric':
|
|
723
|
+
case 'float4':
|
|
724
|
+
case 'float8':
|
|
725
|
+
data = printFloat(data, options);
|
|
726
|
+
break;
|
|
727
|
+
case 'int2':
|
|
728
|
+
case 'int4':
|
|
729
|
+
case 'int8':
|
|
730
|
+
data = printInteger(data, options);
|
|
731
|
+
break;
|
|
732
|
+
case 'boolean':
|
|
733
|
+
data = printBoolean(data, options);
|
|
734
|
+
break;
|
|
735
|
+
case 'markdown':
|
|
736
|
+
// Do nothing as we will format markdown at the end of format
|
|
737
|
+
data = data.toString();
|
|
738
|
+
break;
|
|
739
|
+
case 'gene_sequence':
|
|
740
|
+
data = printGeneSeq(data, options);
|
|
741
|
+
break;
|
|
742
|
+
//Cases to support json and jsonb columns
|
|
743
|
+
case 'json':
|
|
744
|
+
case 'jsonb':
|
|
745
|
+
data = printJSON(data, options);
|
|
746
|
+
break;
|
|
747
|
+
case 'color_rgb_hex':
|
|
748
|
+
data = printColor(data, options);
|
|
749
|
+
break;
|
|
750
|
+
default: // includes 'text' and 'longtext' cases
|
|
751
|
+
data = type.baseType ? _formatValueByType(type.baseType, data, options) : printText(data, options);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
return data;
|
|
755
|
+
}
|