@isrd-isi-edu/ermrestjs 2.9.1 → 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/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.9.1",
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": "12.3.2",
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",
@@ -400,7 +400,16 @@ export default class HandlebarsService {
400
400
  precision = options.hash.precision;
401
401
  tooltip = options.hash.tooltip;
402
402
  }
403
- return _formatUtils.humanizeBytes(value, mode, precision, tooltip);
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
- return _formatUtils.datetimeDuration(start, end, unit, fraction, direction, tooltip);
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
  /**
@@ -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
- if (contentType && contentType.indexOf('html') > -1) {
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
  }
@@ -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
- if (inline) {
37
- return MarkdownIt.renderInline(value);
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 MarkdownIt.render(value);
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
- posTop = true;
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 class that we should add
922
- const className = _classNames.postLoad;
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', className]);
974
+ token.attrPush(['class', imgClassName]);
929
975
  } else {
930
- token.attrs![cIndex][1] += ' ' + className;
976
+ token.attrs![cIndex][1] += ' ' + imgClassName;
931
977
  }
932
978
 
933
979
  return _markdownItDefaultImageRenderer(tokens, idx, options, env, self);