@isrd-isi-edu/ermrestjs 2.9.0 → 2.10.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/hatrac.js +2 -2
- package/package.json +4 -3
- package/src/models/reference/reference.ts +2 -2
- package/src/services/handlebars.ts +20 -2
- package/src/services/http.ts +4 -2
- package/src/utils/constants.ts +2 -0
- package/src/utils/format-utils.ts +6 -0
- package/src/utils/markdown-utils.ts +57 -11
- package/vite.config.mts +8 -3
package/js/hatrac.js
CHANGED
|
@@ -171,8 +171,8 @@ export function Upload(file, otherInfo) {
|
|
|
171
171
|
if (!this.file) throw new Error('No file provided while creating hatrac file object');
|
|
172
172
|
|
|
173
173
|
this.storedFilename = file.name; // the name that will be used for content-disposition and filename column
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
/* In Node (unit tests) the caller provides `file.buffer`; the browser path reads via FileReader.
|
|
175
|
+
We avoid importing `fs` here so the browser bundle has no Node built-ins to stub. */
|
|
176
176
|
|
|
177
177
|
this.column = otherInfo.column;
|
|
178
178
|
if (!this.column) throw new Error('No column provided while creating hatrac file object');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isrd-isi-edu/ermrestjs",
|
|
3
3
|
"description": "ERMrest client library in JavaScript",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.10.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">= 20.0.0",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"handlebars": "4.7.9",
|
|
60
60
|
"lodash-es": "^4.18.1",
|
|
61
61
|
"lz-string": "^1.5.0",
|
|
62
|
-
"markdown-it": "
|
|
62
|
+
"markdown-it": "14.2.0",
|
|
63
|
+
"markdown-it-attrs": "5.0.0",
|
|
63
64
|
"moment": "2.29.4",
|
|
64
65
|
"moment-timezone": "0.5.48",
|
|
65
66
|
"mustache": "x",
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
"spark-md5": "^3.0.0",
|
|
68
69
|
"terser": "^5.44.1",
|
|
69
70
|
"typescript": "~5.9.3",
|
|
70
|
-
"vite": "^
|
|
71
|
+
"vite": "^8.0.16",
|
|
71
72
|
"vite-plugin-compression2": "^2.5.3"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
@@ -1688,7 +1688,7 @@ export class Reference {
|
|
|
1688
1688
|
/**
|
|
1689
1689
|
* Returns true if
|
|
1690
1690
|
* - ermrest supports tcrs, and
|
|
1691
|
-
* - table
|
|
1691
|
+
* - table or any of the columns of the table have dynamic acls, and
|
|
1692
1692
|
* - table has RID column, and
|
|
1693
1693
|
* - table is not marked non-updatable by annotation
|
|
1694
1694
|
*/
|
|
@@ -1698,7 +1698,7 @@ export class Reference {
|
|
|
1698
1698
|
this._canUseTCRS =
|
|
1699
1699
|
this.table.schema.catalog.features[rightKey] === true &&
|
|
1700
1700
|
// eslint-disable-next-line eqeqeq
|
|
1701
|
-
this.table.rights[_ERMrestACLs.UPDATE] == null &&
|
|
1701
|
+
(this.table.rights[_ERMrestACLs.UPDATE] == null || this.table.columns.all().some((col) => col.rights[_ERMrestACLs.UPDATE] == null)) &&
|
|
1702
1702
|
this.table.columns.has('RID') &&
|
|
1703
1703
|
this.canUpdate;
|
|
1704
1704
|
}
|
|
@@ -400,7 +400,16 @@ export default class HandlebarsService {
|
|
|
400
400
|
precision = options.hash.precision;
|
|
401
401
|
tooltip = options.hash.tooltip;
|
|
402
402
|
}
|
|
403
|
-
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* humanizeBytes generates markup like this:
|
|
406
|
+
* :span:1.23 KB:/span:{data-chaise-tooltip="1.23 KB"}
|
|
407
|
+
* So we have to make sure handlebars is not HTML-escaping it. Doing so breaks the attribute markup.
|
|
408
|
+
* NOTE: this is safe only because humanizeBytes derives its output entirely from the numeric byte value
|
|
409
|
+
* (no user-provided text). If we ever let users supply their own tooltip text, the SafeString would expose
|
|
410
|
+
* an HTML-injection vector and we'd need a different solution (escape the user portion before returning).
|
|
411
|
+
*/
|
|
412
|
+
return new Handlebars.SafeString(_formatUtils.humanizeBytes(value, mode, precision, tooltip));
|
|
404
413
|
},
|
|
405
414
|
|
|
406
415
|
/**
|
|
@@ -422,7 +431,16 @@ export default class HandlebarsService {
|
|
|
422
431
|
direction = options.hash.direction;
|
|
423
432
|
tooltip = options.hash.tooltip;
|
|
424
433
|
}
|
|
425
|
-
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* datetimeDuration generates markup like this:
|
|
437
|
+
* :span:+1.1 months:/span:{data-chaise-tooltip="1.1 months"}
|
|
438
|
+
* So we have to make sure handlebars is not HTML-escaping it. Doing so breaks the attribute markup.
|
|
439
|
+
* NOTE: this is safe only because datetimeDuration derives its output entirely from the start/end values
|
|
440
|
+
* (no user-provided text). If we ever let users supply their own tooltip text, the SafeString would expose
|
|
441
|
+
* an HTML-injection vector and we'd need a different solution (escape the user portion before returning).
|
|
442
|
+
*/
|
|
443
|
+
return new Handlebars.SafeString(_formatUtils.datetimeDuration(start, end, unit, fraction, direction, tooltip));
|
|
426
444
|
},
|
|
427
445
|
|
|
428
446
|
/**
|
package/src/services/http.ts
CHANGED
|
@@ -304,8 +304,10 @@ export default class HTTPService {
|
|
|
304
304
|
}
|
|
305
305
|
} else {
|
|
306
306
|
// If we get an HTTP error with HTML in it, this means something the server returned as an error.
|
|
307
|
-
// Ermrest never produces HTML errors, so this was produced by the server itself
|
|
308
|
-
|
|
307
|
+
// Ermrest never produces HTML errors, so this was produced by the server itself.
|
|
308
|
+
// Callers that hit non-ermrest endpoints (e.g. checking an arbitrary image URL) can set
|
|
309
|
+
// `skipHTTPErrorStatusReplacement` to keep the raw status instead of masking it as 500.
|
|
310
|
+
if (contentType && contentType.indexOf('html') > -1 && !config.skipHTTPErrorStatusReplacement) {
|
|
309
311
|
response.status = _http_status_codes.internal_server_error;
|
|
310
312
|
// keep response.data the way it is, so client can provide more info to users
|
|
311
313
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -480,6 +480,8 @@ export const _classNames = Object.freeze({
|
|
|
480
480
|
showInPrintMode: 'video-info-in-print',
|
|
481
481
|
colorPreview: 'chaise-color-preview',
|
|
482
482
|
imagePreview: 'chaise-image-preview',
|
|
483
|
+
// chaise attaches an onerror handler to images with this class to show a fallback
|
|
484
|
+
imageFallback: 'chaise-image-fallback',
|
|
483
485
|
});
|
|
484
486
|
|
|
485
487
|
export const _specialSourceDefinitions = Object.freeze({
|
|
@@ -573,6 +573,9 @@ export function printColor(value: any, options?: any): string {
|
|
|
573
573
|
* output in a Chaise tooltip span showing the raw byte
|
|
574
574
|
* count and the unit conversion factor
|
|
575
575
|
* @returns the humanized byte string
|
|
576
|
+
*
|
|
577
|
+
* SECURITY: Do NOT interpolate raw, user-controlled strings into the output without escaping/validating them first.
|
|
578
|
+
* The handlebars `humanizeBytes` helper returns this output as a Handlebars.SafeString (unescaped).
|
|
576
579
|
*/
|
|
577
580
|
export function humanizeBytes(value: any, mode?: any, precision?: any, withTooltip?: any): string {
|
|
578
581
|
// we cannot use parseInt here since it won't allow larger numbers.
|
|
@@ -642,6 +645,9 @@ export function humanizeBytes(value: any, mode?: any, precision?: any, withToolt
|
|
|
642
645
|
* tooltip body would conflict with the calendar walk
|
|
643
646
|
* already visible in line 1)
|
|
644
647
|
* @returns the formatted duration string
|
|
648
|
+
*
|
|
649
|
+
* SECURITY: Do NOT interpolate raw, user-controlled strings into the output without escaping/validating them first.
|
|
650
|
+
* The handlebars `datetimeDuration` helper returns this output as a Handlebars.SafeString (unescaped).
|
|
645
651
|
*/
|
|
646
652
|
export function datetimeDuration(start: any, end: any, unit?: any, fraction?: any, direction?: any, withTooltip?: any): string {
|
|
647
653
|
const startM = moment(start);
|
|
@@ -12,9 +12,11 @@ import markdownItSub from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-sub.min';
|
|
|
12
12
|
import markdownItSup from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-sup.min';
|
|
13
13
|
import markdownItSpan from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-span';
|
|
14
14
|
import markdownItEscape from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-escape';
|
|
15
|
-
import markdownItAttrs from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-attrs';
|
|
16
15
|
import MarkdownItContainer from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-container.min';
|
|
17
16
|
|
|
17
|
+
// npm package (replaces the previously vendored markdown-it-attrs)
|
|
18
|
+
import markdownItAttrs from 'markdown-it-attrs';
|
|
19
|
+
|
|
18
20
|
let _markdownItDefaultImageRenderer: any = null;
|
|
19
21
|
export const MarkdownIt = markdownit({ typographer: true, breaks: true })
|
|
20
22
|
.use(markdownItSub)
|
|
@@ -32,11 +34,48 @@ _bindCustomMarkdownTags(MarkdownIt);
|
|
|
32
34
|
* @param throwError if true, it will throw the error instead of swalloing it
|
|
33
35
|
*/
|
|
34
36
|
export function renderMarkdown(value: string, inline?: boolean, throwError?: boolean) {
|
|
37
|
+
/*
|
|
38
|
+
* markdown-it-attrs 5.x catches errors thrown while applying an attribute
|
|
39
|
+
* block and only reports them via `console.error` (it has no error callback
|
|
40
|
+
* or opt-out). To preserve the old "if anything fails, show the raw value"
|
|
41
|
+
* behavior, trap that signal during the render and treat it as a failure so
|
|
42
|
+
* we fall back to the raw value below instead of returning a partial render.
|
|
43
|
+
*/
|
|
44
|
+
let attrsError: string | null = null;
|
|
45
|
+
// message of the failure we just captured; used to recognize its stack-trace follow-up
|
|
46
|
+
let pendingMessage: string | null = null;
|
|
47
|
+
const consoleError = console.error;
|
|
48
|
+
console.error = (...args: any[]) => {
|
|
49
|
+
if (typeof args[0] === 'string' && args[0].indexOf('markdown-it-attrs:') === 0) {
|
|
50
|
+
attrsError = args[0];
|
|
51
|
+
/*
|
|
52
|
+
* markdown-it-attrs logs the message above and then `console.error(error.stack)`
|
|
53
|
+
* on the very next call (see its index.js: the two calls are consecutive and
|
|
54
|
+
* synchronous). error.stack is a string, not an Error, so we match it by content:
|
|
55
|
+
* the stack embeds the same `error.message` that trails this message.
|
|
56
|
+
*/
|
|
57
|
+
pendingMessage = args[0].replace(/^markdown-it-attrs: Error in pattern '.*?': /, '');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// swallow only that stack-trace follow-up; anything else is unrelated and must pass through
|
|
61
|
+
if (pendingMessage !== null) {
|
|
62
|
+
const message = pendingMessage;
|
|
63
|
+
pendingMessage = null;
|
|
64
|
+
if (message.length > 0 && typeof args[0] === 'string' && args[0].indexOf(message) !== -1) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
consoleError.apply(console, args);
|
|
69
|
+
};
|
|
70
|
+
|
|
35
71
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
const rendered = inline ? MarkdownIt.renderInline(value) : MarkdownIt.render(value);
|
|
73
|
+
pendingMessage = null; // render is done; don't let a dangling flag swallow our own catch logging
|
|
74
|
+
// markdown-it-attrs only logs (never throws), so re-throw here to fall back to the raw value
|
|
75
|
+
if (attrsError !== null) {
|
|
76
|
+
throw new Error(attrsError);
|
|
38
77
|
}
|
|
39
|
-
return
|
|
78
|
+
return rendered;
|
|
40
79
|
} catch (e) {
|
|
41
80
|
if (throwError) {
|
|
42
81
|
throw e;
|
|
@@ -45,6 +84,8 @@ export function renderMarkdown(value: string, inline?: boolean, throwError?: boo
|
|
|
45
84
|
$log.error(`Couldn't parse the given markdown value: ${value}`);
|
|
46
85
|
$log.error(e);
|
|
47
86
|
return value;
|
|
87
|
+
} finally {
|
|
88
|
+
console.error = consoleError;
|
|
48
89
|
}
|
|
49
90
|
}
|
|
50
91
|
|
|
@@ -476,8 +517,9 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
476
517
|
if (attrs[0].children[0].type == 'link_open') {
|
|
477
518
|
let imageHTML = '<img ';
|
|
478
519
|
const openingLink = attrs[0].children[0];
|
|
479
|
-
let enlargeLink
|
|
480
|
-
|
|
520
|
+
let enlargeLink;
|
|
521
|
+
let posTop = true;
|
|
522
|
+
let imageClass = _classNames.imageFallback;
|
|
481
523
|
|
|
482
524
|
// Add all attributes to the image
|
|
483
525
|
openingLink.attrs!.forEach(function (attr) {
|
|
@@ -497,12 +539,15 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
497
539
|
case 'pos':
|
|
498
540
|
posTop = attr[1].toLowerCase() == 'bottom' ? false : true;
|
|
499
541
|
break;
|
|
542
|
+
case 'class':
|
|
543
|
+
imageClass += ' ' + attr[1];
|
|
544
|
+
break;
|
|
500
545
|
default:
|
|
501
546
|
imageHTML += attr[0] + '="' + attr[1] + '" ';
|
|
502
547
|
}
|
|
503
548
|
});
|
|
504
549
|
|
|
505
|
-
html += imageHTML + '/>';
|
|
550
|
+
html += imageHTML + 'class="' + imageClass + '" />';
|
|
506
551
|
|
|
507
552
|
let captionHTML = '';
|
|
508
553
|
|
|
@@ -918,16 +963,17 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
918
963
|
};
|
|
919
964
|
}
|
|
920
965
|
|
|
921
|
-
// the
|
|
922
|
-
|
|
966
|
+
// the classes that we should add (post-load for layout, image-fallback so chaise
|
|
967
|
+
// can swap in a fallback image when the image fails to load)
|
|
968
|
+
const imgClassName = _classNames.postLoad + ' ' + _classNames.imageFallback;
|
|
923
969
|
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
|
924
970
|
const token = tokens[idx];
|
|
925
971
|
|
|
926
972
|
const cIndex = token.attrIndex('class');
|
|
927
973
|
if (cIndex < 0) {
|
|
928
|
-
token.attrPush(['class',
|
|
974
|
+
token.attrPush(['class', imgClassName]);
|
|
929
975
|
} else {
|
|
930
|
-
token.attrs![cIndex][1] += ' ' +
|
|
976
|
+
token.attrs![cIndex][1] += ' ' + imgClassName;
|
|
931
977
|
}
|
|
932
978
|
|
|
933
979
|
return _markdownItDefaultImageRenderer(tokens, idx, options, env, self);
|
package/vite.config.mts
CHANGED
|
@@ -24,7 +24,7 @@ export default defineConfig(async (): Promise<UserConfig> => {
|
|
|
24
24
|
// dev related plugins
|
|
25
25
|
if (isDev) {
|
|
26
26
|
// visualize the bundle size
|
|
27
|
-
const { visualizer } = await import('rollup-plugin-visualizer');
|
|
27
|
+
const { visualizer } = await import(/* @vite-ignore */ 'rollup-plugin-visualizer');
|
|
28
28
|
plugins.push(
|
|
29
29
|
visualizer({
|
|
30
30
|
filename: resolve(__dirname, 'dist', 'stats.html'),
|
|
@@ -61,14 +61,12 @@ export default defineConfig(async (): Promise<UserConfig> => {
|
|
|
61
61
|
name: 'ERMrest',
|
|
62
62
|
format: 'umd',
|
|
63
63
|
entryFileNames: 'ermrest.js',
|
|
64
|
-
inlineDynamicImports: true,
|
|
65
64
|
},
|
|
66
65
|
// the following is the main build that chaise uses:
|
|
67
66
|
{
|
|
68
67
|
name: 'ERMrest',
|
|
69
68
|
format: 'umd',
|
|
70
69
|
entryFileNames: 'ermrest.min.js',
|
|
71
|
-
inlineDynamicImports: true,
|
|
72
70
|
},
|
|
73
71
|
],
|
|
74
72
|
},
|
|
@@ -84,6 +82,13 @@ export default defineConfig(async (): Promise<UserConfig> => {
|
|
|
84
82
|
resolve: {
|
|
85
83
|
alias: {
|
|
86
84
|
'@isrd-isi-edu/ermrestjs': resolve(__dirname),
|
|
85
|
+
/**
|
|
86
|
+
* markdown-it imports the deprecated Node `punycode` builtin. In a browser
|
|
87
|
+
* build that gets externalized to a stub and logs a warning. markdown-it only
|
|
88
|
+
* calls it inside try/catch for hostname normalization, so an empty shim is
|
|
89
|
+
* safe (ASCII URLs unaffected) and silences the warning.
|
|
90
|
+
*/
|
|
91
|
+
punycode: resolve(__dirname, 'vendor/punycode-shim.js'),
|
|
87
92
|
},
|
|
88
93
|
},
|
|
89
94
|
plugins,
|