summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/reps
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/reps')
-rw-r--r--devtools/client/shared/components/reps/images/input.svg7
-rw-r--r--devtools/client/shared/components/reps/images/jump-definition.svg8
-rw-r--r--devtools/client/shared/components/reps/images/open-a11y.svg10
-rw-r--r--devtools/client/shared/components/reps/images/open-inspector.svg6
-rw-r--r--devtools/client/shared/components/reps/index.js32
-rw-r--r--devtools/client/shared/components/reps/moz.build14
-rw-r--r--devtools/client/shared/components/reps/reps.css400
-rw-r--r--devtools/client/shared/components/reps/reps/accessible.js197
-rw-r--r--devtools/client/shared/components/reps/reps/accessor.js106
-rw-r--r--devtools/client/shared/components/reps/reps/array.js170
-rw-r--r--devtools/client/shared/components/reps/reps/attribute.js80
-rw-r--r--devtools/client/shared/components/reps/reps/big-int.js57
-rw-r--r--devtools/client/shared/components/reps/reps/comment-node.js76
-rw-r--r--devtools/client/shared/components/reps/reps/constants.js18
-rw-r--r--devtools/client/shared/components/reps/reps/custom-formatter.js256
-rw-r--r--devtools/client/shared/components/reps/reps/date-time.js95
-rw-r--r--devtools/client/shared/components/reps/reps/document-type.js60
-rw-r--r--devtools/client/shared/components/reps/reps/document.js79
-rw-r--r--devtools/client/shared/components/reps/reps/element-node.js322
-rw-r--r--devtools/client/shared/components/reps/reps/error.js338
-rw-r--r--devtools/client/shared/components/reps/reps/event.js115
-rw-r--r--devtools/client/shared/components/reps/reps/function.js264
-rw-r--r--devtools/client/shared/components/reps/reps/grip-array.js263
-rw-r--r--devtools/client/shared/components/reps/reps/grip-entry.js78
-rw-r--r--devtools/client/shared/components/reps/reps/grip-map.js234
-rw-r--r--devtools/client/shared/components/reps/reps/grip.js401
-rw-r--r--devtools/client/shared/components/reps/reps/infinity.js52
-rw-r--r--devtools/client/shared/components/reps/reps/moz.build45
-rw-r--r--devtools/client/shared/components/reps/reps/nan.js51
-rw-r--r--devtools/client/shared/components/reps/reps/null.js59
-rw-r--r--devtools/client/shared/components/reps/reps/number.js63
-rw-r--r--devtools/client/shared/components/reps/reps/object-with-text.js70
-rw-r--r--devtools/client/shared/components/reps/reps/object-with-url.js73
-rw-r--r--devtools/client/shared/components/reps/reps/object.js207
-rw-r--r--devtools/client/shared/components/reps/reps/promise.js105
-rw-r--r--devtools/client/shared/components/reps/reps/prop-rep.js106
-rw-r--r--devtools/client/shared/components/reps/reps/regexp.js66
-rw-r--r--devtools/client/shared/components/reps/reps/rep-utils.js596
-rw-r--r--devtools/client/shared/components/reps/reps/rep.js205
-rw-r--r--devtools/client/shared/components/reps/reps/string.js410
-rw-r--r--devtools/client/shared/components/reps/reps/stylesheet.js78
-rw-r--r--devtools/client/shared/components/reps/reps/symbol.js82
-rw-r--r--devtools/client/shared/components/reps/reps/text-node.js141
-rw-r--r--devtools/client/shared/components/reps/reps/undefined.js59
-rw-r--r--devtools/client/shared/components/reps/reps/window.js102
-rw-r--r--devtools/client/shared/components/reps/shared/dom-node-constants.js31
-rw-r--r--devtools/client/shared/components/reps/shared/grip-length-bubble.js64
-rw-r--r--devtools/client/shared/components/reps/shared/moz.build10
48 files changed, 6361 insertions, 0 deletions
diff --git a/devtools/client/shared/components/reps/images/input.svg b/devtools/client/shared/components/reps/images/input.svg
new file mode 100644
index 0000000000..830b651e9e
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/input.svg
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="context-fill #0b0b0b">
+ <path d="M11.04 5.46L7.29 1.71a.75.75 0 0 0-1.06 1.06L9.45 6 6.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
+ <path d="M6.04 5.46L2.29 1.71a.75.75 0 0 0-1.06 1.06L4.45 6 1.23 9.21a.75.75 0 1 0 1.06 1.06l3.75-3.75c.3-.3.3-.77 0-1.06z"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/shared/components/reps/images/jump-definition.svg b/devtools/client/shared/components/reps/images/jump-definition.svg
new file mode 100644
index 0000000000..9ac071523d
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/jump-definition.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" stroke="context-stroke" fill="none" stroke-linecap="round">
+ <path d="M5.5 3.5l2 2M5.5 7.5l2-2"/>
+ <path d="M7 5.5H4.006c-1.012 0-1.995 1.017-2.011 2.024-.005.023-.005 1.347 0 3.971" stroke-linejoin="round"/>
+ <path d="M10.5 5.5h4M9.5 3.5h5M9.5 7.5h5"/>
+</svg> \ No newline at end of file
diff --git a/devtools/client/shared/components/reps/images/open-a11y.svg b/devtools/client/shared/components/reps/images/open-a11y.svg
new file mode 100644
index 0000000000..cba2ab93c9
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/open-a11y.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="context-fill #0C0C0D">
+ <path d="M9.5 2.5C9.5 3.60457 8.60457 4.5 7.5 4.5C6.39543 4.5 5.5 3.60457 5.5 2.5C5.5 1.39543 6.39543 0.5 7.5 0.5C8.60457 0.5 9.5 1.39543 9.5 2.5Z"/>
+ <path d="M1.5 6C1.5 5.44772 1.94772 5 2.5 5H12.5C13.0523 5 13.5 5.44772 13.5 6C13.5 6.55228 13.0523 7 12.5 7H2.5C1.94772 7 1.5 6.55228 1.5 6Z"/>
+ <path d="M6 5C6.55228 5 7 5.44772 7 6L7 13C7 13.5523 6.55228 14 6 14C5.44771 14 5 13.5523 5 13L5 6C5 5.44772 5.44772 5 6 5Z"/>
+ <path d="M9 5C9.55228 5 10 5.44772 10 6V13C10 13.5523 9.55228 14 9 14C8.44771 14 8 13.5523 8 13L8 6C8 5.44772 8.44772 5 9 5Z"/>
+ <path d="M5 7H10V10.03H5V7Z"/>
+</svg>
diff --git a/devtools/client/shared/components/reps/images/open-inspector.svg b/devtools/client/shared/components/reps/images/open-inspector.svg
new file mode 100644
index 0000000000..9e8a277e7c
--- /dev/null
+++ b/devtools/client/shared/components/reps/images/open-inspector.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="context-fill">
+ <path d="M7 3H5a2 2 0 0 0-2 2v2H1.5a.5.5 0 0 0 0 1H3v2c0 1.1.9 2 2 2h2v1.5a.5.5 0 0 0 1 0V12h2a2 2 0 0 0 2-2V8h1.5a.5.5 0 0 0 0-1H12V5a2 2 0 0 0-2-2H8V1.5a.5.5 0 0 0-1 0V3zM5 5h5v5H5V5z"/>
+</svg>
diff --git a/devtools/client/shared/components/reps/index.js b/devtools/client/shared/components/reps/index.js
new file mode 100644
index 0000000000..e99e642c38
--- /dev/null
+++ b/devtools/client/shared/components/reps/index.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const {
+ MODE,
+} = require("devtools/client/shared/components/reps/reps/constants");
+const {
+ REPS,
+ getRep,
+} = require("devtools/client/shared/components/reps/reps/rep");
+const objectInspector = require("devtools/client/shared/components/object-inspector/index");
+
+const {
+ parseURLEncodedText,
+ parseURLParams,
+ maybeEscapePropertyName,
+ getGripPreviewItems,
+} = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+module.exports = {
+ REPS,
+ getRep,
+ MODE,
+ maybeEscapePropertyName,
+ parseURLEncodedText,
+ parseURLParams,
+ getGripPreviewItems,
+ objectInspector,
+};
diff --git a/devtools/client/shared/components/reps/moz.build b/devtools/client/shared/components/reps/moz.build
new file mode 100644
index 0000000000..058e8046a7
--- /dev/null
+++ b/devtools/client/shared/components/reps/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "reps",
+ "shared",
+]
+
+DevToolsModules(
+ "index.js",
+)
diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css
new file mode 100644
index 0000000000..68eee2e78b
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,400 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-red);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-highlight-blue);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-comment);
+ --source-link-color: var(--theme-link-color);
+ --node-color: var(--theme-highlight-purple);
+ --reference-color: var(--theme-highlight-blue);
+ --comment-node-color: var(--theme-comment);
+}
+
+/******************************************************************************/
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-symbol,
+.objectBox-text,
+.objectBox-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+:is(
+ .objectBox-string,
+ .objectBox-textNode,
+ .objectBox > .nodeName,
+ .objectBox-node .tag-name,
+ .objectBox-node .attrName
+).has-rtl-char {
+ unicode-bidi: isolate;
+}
+
+.objectBox-number,
+.objectBox-styleRule,
+.objectBox-element,
+.objectBox-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-textNode,
+.objectBox-string,
+.objectBox-symbol {
+ color: var(--string-color);
+}
+
+.objectBox-empty-string {
+ font-style: italic;
+}
+
+.objectBox-string a {
+ word-break: break-all;
+}
+
+.objectBox-string a,
+.objectBox-string a:visited {
+ color: currentColor;
+ text-decoration: underline;
+ text-decoration-skip-ink: none;
+ font-style: italic;
+ cursor: pointer;
+}
+
+/* Visually hide the middle of "cropped" url */
+.objectBox-string a .cropped-url-middle {
+ max-width: 0;
+ max-height: 0;
+ display: inline-block;
+ overflow: hidden;
+ vertical-align: bottom;
+}
+
+.objectBox-string a .cropped-url-end::before {
+ content: "…";
+}
+
+
+.objectBox-function,
+.objectBox-profile {
+ color: var(--object-color);
+}
+
+.objectBox-stackTrace.reps-custom-format,
+.objectBox-stackTrace.reps-custom-format > .objectBox-string {
+ color: var(--error-color);
+}
+
+.objectBox-stackTrace-grid {
+ display: inline-grid;
+ grid-template-columns: auto auto;
+ margin-top: 3px;
+}
+
+.objectBox-stackTrace-fn {
+ color: var(--console-output-color);
+ padding-inline-start: 17px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-inline-end: 5px;
+}
+
+.objectBox-stackTrace-location {
+ color: var(--frame-link-source, currentColor);
+ direction: rtl;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: end;
+}
+
+.objectBox-stackTrace-location:hover {
+ text-decoration: underline;
+}
+
+.objectBox-stackTrace-location {
+ cursor: pointer;
+}
+
+.objectBox-Location,
+.location {
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.objectBox-nan,
+.logRowHint {
+ color: var(--null-color);
+}
+
+.objectBox-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+.objectBox-failure {
+ color: var(--string-color);
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 2px;
+ font-size: 0.8em;
+ padding: 0 2px;
+}
+
+.objectBox-accessible.clickable,
+.objectBox-node.clickable {
+ cursor: pointer;
+}
+
+/* JsonML reps can be nested, though only the top-level rep needs layout
+ * adjustments to align it with the toggle arrow and fit its width to its
+ * contents. */
+.objectBox-jsonml-wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ width: fit-content;
+ word-break: break-word;
+ line-height: normal;
+}
+
+.objectBox-jsonml-wrapper[data-expandable="true"] {
+ cursor: default;
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button {
+ margin: 0 4px 2px 0;
+ padding: 0;
+ vertical-align: middle;
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 10px;
+ background: url("chrome://devtools/skin/images/arrow.svg") no-repeat center;
+ background-size: 10px;
+ transform: rotate(-90deg);
+ transition: transform 125ms ease;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+}
+
+.objectBox-jsonml-wrapper .jsonml-header-collapse-button[aria-expanded="true"]::before {
+ transform: rotate(0deg);
+}
+
+/******************************************************************************/
+
+.objectBox-event,
+.objectBox-eventLog,
+.objectBox-regexp,
+.objectBox-object {
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox .Date {
+ color: var(--string-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectBox.theme-comment {
+ color: var(--comment-node-color);
+}
+
+.accessible-role,
+.tag-name {
+ color: var(--object-color);
+}
+
+.attrName {
+ color: var(--string-color);
+}
+
+.attrEqual,
+.objectEqual {
+ color: var(--comment-node-color);
+}
+
+.attrValue,
+.attrValue.objectBox-string {
+ color: var(--node-color);
+}
+
+.angleBracket {
+ color: var(--theme-body-color);
+}
+
+/******************************************************************************/
+/* Length bubble for arraylikes and maplikes */
+
+.objectLengthBubble {
+ color: var(--null-color);
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ color: var(--object-color);
+}
+
+/******************************************************************************/
+/* Cycle reference */
+
+.objectBox-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+[class*="objectBox"] > .objectTitle {
+ color: var(--object-color);
+}
+
+.caption {
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+/* Open DOMNode in inspector or Accessible in accessibility inspector button */
+
+:is(button, [role="button"]).open-accessibility-inspector {
+ background: url("chrome://devtools/content/shared/components/reps/images/open-a11y.svg")
+ no-repeat;
+}
+
+:is(button, [role="button"]).open-inspector {
+ background: url("chrome://devtools/content/shared/components/reps/images/open-inspector.svg")
+ no-repeat;
+}
+
+:is(button, [role="button"]).highlight-node {
+ background: url("chrome://devtools/skin/images/highlight-selector.svg")
+ no-repeat;
+}
+
+
+:is(button, [role="button"]):is(.open-accessibility-inspector, .open-inspector, .highlight-node) {
+ display: inline-block;
+ vertical-align: top;
+ height: 15px;
+ width: 15px;
+ margin: 0 4px;
+ padding: 0;
+ border: none;
+ fill: var(--theme-icon-color);
+ cursor: pointer;
+ -moz-context-properties: fill;
+}
+
+.objectBox-accessible:hover .open-accessibility-inspector,
+.objectBox-node:hover .open-inspector,
+.objectBox-textNode:hover .open-inspector,
+.open-accessibility-inspector:hover,
+.highlight-node:hover,
+.open-inspector:hover {
+ fill: var(--theme-icon-checked-color);
+}
+
+/******************************************************************************/
+/* Jump to definition button */
+
+button.jump-definition {
+ display: inline-block;
+ height: 16px;
+ margin-left: 0.25em;
+ vertical-align: middle;
+ background: 0% 50%
+ url("chrome://devtools/content/shared/components/reps/images/jump-definition.svg")
+ no-repeat;
+ border-color: transparent;
+ stroke: var(--theme-icon-color);
+ -moz-context-properties: stroke;
+ cursor: pointer;
+}
+
+.jump-definition:hover {
+ stroke: var(--theme-icon-checked-color);
+}
+
+.tree-node.focused .jump-definition {
+ stroke: currentColor;
+}
+
+/******************************************************************************/
+/* Invoke getter button */
+
+button.invoke-getter {
+ mask: url(chrome://devtools/content/shared/components/reps/images/input.svg)
+ no-repeat;
+ display: inline-block;
+ background-color: var(--theme-icon-color);
+ height: 10px;
+ vertical-align: middle;
+ border: none;
+}
+
+.invoke-getter:hover {
+ background-color: var(--theme-icon-checked-color);
+}
+
+/******************************************************************************/
+/* "more…" ellipsis */
+.more-ellipsis {
+ color: var(--comment-node-color);
+}
+
+/* function parameters */
+.objectBox-function .param {
+ color: var(--theme-highlight-red);
+}
diff --git a/devtools/client/shared/components/reps/reps/accessible.js b/devtools/client/shared/components/reps/reps/accessible.js
new file mode 100644
index 0000000000..796a850161
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/accessible.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders Accessible object.
+ */
+
+ Accessible.propTypes = {
+ object: PropTypes.object.isRequired,
+ inspectIconTitle: PropTypes.string,
+ nameMaxLength: PropTypes.number,
+ onAccessibleClick: PropTypes.func,
+ onAccessibleMouseOver: PropTypes.func,
+ onAccessibleMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ roleFirst: PropTypes.bool,
+ separatorText: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Accessible(props) {
+ const {
+ object,
+ inspectIconTitle,
+ nameMaxLength,
+ onAccessibleClick,
+ onInspectIconClick,
+ roleFirst,
+ separatorText,
+ } = props;
+
+ const isInTree = object.preview && object.preview.isConnected === true;
+
+ const config = getElementConfig({ ...props, isInTree });
+ const elements = getElements(
+ object,
+ nameMaxLength,
+ roleFirst,
+ separatorText
+ );
+ const inspectIcon = getInspectIcon({
+ object,
+ onInspectIconClick,
+ inspectIconTitle,
+ onAccessibleClick,
+ isInTree,
+ });
+
+ return span(config, ...elements, inspectIcon);
+ }
+
+ // Get React Config Obj
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onAccessibleClick,
+ onAccessibleMouseOver,
+ onAccessibleMouseOut,
+ shouldRenderTooltip,
+ roleFirst,
+ } = opts;
+ const { name, role } = object.preview;
+
+ // Initiate config
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-accessible",
+ };
+
+ if (isInTree) {
+ if (onAccessibleClick) {
+ Object.assign(config, {
+ onClick: _ => onAccessibleClick(object),
+ className: `${config.className} clickable`,
+ });
+ }
+
+ if (onAccessibleMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onAccessibleMouseOver(object),
+ });
+ }
+
+ if (onAccessibleMouseOut) {
+ Object.assign(config, {
+ onMouseOut: onAccessibleMouseOut,
+ });
+ }
+ }
+
+ // If tooltip, build tooltip
+ if (shouldRenderTooltip) {
+ let tooltip;
+ if (!name) {
+ tooltip = role;
+ } else {
+ const quotedName = `"${name}"`;
+ tooltip = `${roleFirst ? role : quotedName}: ${
+ roleFirst ? quotedName : role
+ }`;
+ }
+
+ config.title = tooltip;
+ }
+
+ // Return config obj
+ return config;
+ }
+
+ // Get Content Elements
+ function getElements(
+ grip,
+ nameMaxLength,
+ roleFirst = false,
+ separatorText = ": "
+ ) {
+ const { name, role } = grip.preview;
+ const elements = [];
+
+ // If there's a `name` value in `grip.preview`, render it with the
+ // StringRep and push element into Elements array
+
+ if (name) {
+ elements.push(
+ StringRep({
+ className: "accessible-name",
+ object: name,
+ cropLimit: nameMaxLength,
+ }),
+ span({ className: "separator" }, separatorText)
+ );
+ }
+
+ elements.push(span({ className: "accessible-role" }, role));
+ return roleFirst ? elements.reverse() : elements;
+ }
+
+ // Get Icon
+ function getInspectIcon(opts) {
+ const {
+ object,
+ onInspectIconClick,
+ inspectIconTitle,
+ onAccessibleClick,
+ isInTree,
+ } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: "open-accessibility-inspector",
+ title: inspectIconTitle,
+ onClick: e => {
+ if (onAccessibleClick) {
+ e.stopPropagation();
+ }
+
+ onInspectIconClick(object, e);
+ },
+ });
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return (
+ object?.preview && object.typeName && object.typeName === "accessible"
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Accessible),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/accessor.js b/devtools/client/shared/components/reps/reps/accessor.js
new file mode 100644
index 0000000000..b234d814b3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/accessor.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+
+ Accessor.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Accessor(props) {
+ const {
+ object,
+ evaluation,
+ onInvokeGetterButtonClick,
+ shouldRenderTooltip,
+ } = props;
+
+ if (evaluation) {
+ const {
+ Rep,
+ Grip,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+ return span(
+ {
+ className: "objectBox objectBox-accessor objectTitle",
+ },
+ Rep({
+ ...props,
+ object: evaluation.getterValue,
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ })
+ );
+ }
+
+ if (hasGetter(object) && onInvokeGetterButtonClick) {
+ return button({
+ className: "invoke-getter",
+ title: "Invoke getter",
+ onClick: event => {
+ onInvokeGetterButtonClick();
+ event.stopPropagation();
+ },
+ });
+ }
+
+ const accessors = [];
+ if (hasGetter(object)) {
+ accessors.push("Getter");
+ }
+
+ if (hasSetter(object)) {
+ accessors.push("Setter");
+ }
+
+ const accessorsString = accessors.join(" & ");
+
+ return span(
+ {
+ className: "objectBox objectBox-accessor objectTitle",
+ title: shouldRenderTooltip ? accessorsString : null,
+ },
+ accessorsString
+ );
+ }
+
+ function hasGetter(object) {
+ return object && object.get && object.get.type !== "undefined";
+ }
+
+ function hasSetter(object) {
+ return object && object.set && object.set.type !== "undefined";
+ }
+
+ function supportsObject(object) {
+ return hasGetter(object) || hasSetter(object);
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Accessor),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/array.js b/devtools/client/shared/components/reps/reps/array.js
new file mode 100644
index 0000000000..f8797fa4d3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/array.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const ModePropType = PropTypes.oneOf(Object.values(MODE));
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+
+ ArrayRep.propTypes = {
+ mode: ModePropType,
+ object: PropTypes.array.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ArrayRep(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip = true } = props;
+
+ let brackets;
+ let items;
+ const needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ if (mode === MODE.TINY) {
+ const isEmpty = object.length === 0;
+ if (isEmpty) {
+ items = [];
+ } else {
+ items = [
+ span(
+ {
+ className: "more-ellipsis",
+ },
+ "…"
+ ),
+ ];
+ }
+ brackets = needSpace(false);
+ } else {
+ items = arrayIterator(props, object, maxLengthMap.get(mode));
+ brackets = needSpace(!!items.length);
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-array",
+ title: shouldRenderTooltip ? "Array" : null,
+ },
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ ...items,
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ )
+ );
+ }
+
+ function arrayIterator(props, array, max) {
+ const items = [];
+
+ for (let i = 0; i < array.length && i < max; i++) {
+ const config = {
+ mode: MODE.TINY,
+ delim: i == array.length - 1 ? "" : ", ",
+ };
+ let item;
+
+ try {
+ item = ItemRep({
+ ...props,
+ ...config,
+ object: array[i],
+ });
+ } catch (exc) {
+ item = ItemRep({
+ ...props,
+ ...config,
+ object: exc,
+ });
+ }
+ items.push(item);
+ }
+
+ if (array.length > max) {
+ items.push(
+ span(
+ {
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ );
+ }
+
+ return items;
+ }
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+
+ ItemRep.propTypes = {
+ object: PropTypes.any.isRequired,
+ delim: PropTypes.string.isRequired,
+ mode: ModePropType,
+ };
+
+ function ItemRep(props) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ const { object, delim, mode } = props;
+ return span(
+ {},
+ Rep({
+ ...props,
+ object,
+ mode,
+ }),
+ delim
+ );
+ }
+
+ function getLength(object) {
+ return object.length;
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return (
+ noGrip &&
+ (Array.isArray(object) ||
+ Object.prototype.toString.call(object) === "[object Arguments]")
+ );
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ArrayRep),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/attribute.js b/devtools/client/shared/components/reps/reps/attribute.js
new file mode 100644
index 0000000000..d9c4e7c237
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/attribute.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ appendRTLClassNameIfNeeded,
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders DOM attribute
+ */
+
+ Attribute.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Attribute(props) {
+ const { object, shouldRenderTooltip } = props;
+ const value = object.preview.value;
+ const attrName = getTitle(object);
+
+ const config = getElementConfig({
+ attrName,
+ shouldRenderTooltip,
+ value,
+ object,
+ });
+
+ return span(
+ config,
+ span(
+ {
+ className: appendRTLClassNameIfNeeded("attrName", attrName),
+ },
+ attrName
+ ),
+ span({ className: "attrEqual" }, "="),
+ StringRep({ className: "attrValue", object: value })
+ );
+ }
+
+ function getTitle(grip) {
+ return grip.preview.nodeName;
+ }
+
+ function getElementConfig(opts) {
+ const { attrName, shouldRenderTooltip, value, object } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox-Attr",
+ title: shouldRenderTooltip ? `${attrName}="${value}"` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) == "Attr" && grip?.preview;
+ }
+
+ module.exports = {
+ rep: wrapRender(Attribute),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/big-int.js b/devtools/client/shared/components/reps/reps/big-int.js
new file mode 100644
index 0000000000..4bb7db507f
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/big-int.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a BigInt Number
+ */
+
+ BigInt.propTypes = {
+ object: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.number,
+ PropTypes.bool,
+ ]).isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function BigInt(props) {
+ const { object, shouldRenderTooltip } = props;
+ const text = object.text;
+ const config = getElementConfig({ text, shouldRenderTooltip });
+
+ return span(config, `${text}n`);
+ }
+
+ function getElementConfig(opts) {
+ const { text, shouldRenderTooltip } = opts;
+
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? `${text}n` : null,
+ };
+ }
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) === "BigInt";
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(BigInt),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/comment-node.js b/devtools/client/shared/components/reps/reps/comment-node.js
new file mode 100644
index 0000000000..5d4ce49e3d
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/comment-node.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ const {
+ cropString,
+ cropMultipleLines,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const nodeConstants = require("devtools/client/shared/components/reps/shared/dom-node-constants");
+
+ /**
+ * Renders DOM comment node.
+ */
+
+ CommentNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function CommentNode(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip } = props;
+
+ let { textContent } = object.preview;
+ if (mode === MODE.TINY || mode === MODE.HEADER) {
+ textContent = cropMultipleLines(textContent, 30);
+ } else if (mode === MODE.SHORT) {
+ textContent = cropString(textContent, 50);
+ }
+
+ const config = getElementConfig({
+ object,
+ textContent,
+ shouldRenderTooltip,
+ });
+
+ return span(config, `<!-- ${textContent} -->`);
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip } = opts;
+
+ // Run textContent through cropString to sanitize
+ const uncroppedText = shouldRenderTooltip
+ ? cropString(object.preview.textContent)
+ : null;
+
+ return {
+ className: "objectBox theme-comment",
+ "data-link-actor-id": object.actor,
+ title: shouldRenderTooltip ? `<!-- ${uncroppedText} -->` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return object?.preview?.nodeType === nodeConstants.COMMENT_NODE;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(CommentNode),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/constants.js b/devtools/client/shared/components/reps/reps/constants.js
new file mode 100644
index 0000000000..2599ccab75
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/constants.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ module.exports = {
+ MODE: {
+ TINY: Symbol("TINY"),
+ SHORT: Symbol("SHORT"),
+ LONG: Symbol("LONG"),
+ // Used by Debugger Preview popup
+ HEADER: Symbol("HEADER"),
+ },
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/custom-formatter.js b/devtools/client/shared/components/reps/reps/custom-formatter.js
new file mode 100644
index 0000000000..469866f790
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/custom-formatter.js
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ // Dependencies
+ const {
+ Component,
+ createElement,
+ createFactory,
+ } = require("devtools/client/shared/vendor/react");
+ const {
+ cleanupStyle,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const flags = require("resource://devtools/shared/flags.js");
+
+ const ALLOWED_TAGS = new Set([
+ "span",
+ "div",
+ "ol",
+ "ul",
+ "li",
+ "table",
+ "tr",
+ "td",
+ ]);
+
+ class CustomFormatter extends Component {
+ static get propTypes() {
+ return {
+ autoExpandDepth: PropTypes.number,
+ client: PropTypes.object,
+ createElement: PropTypes.func,
+ frame: PropTypes.object,
+ front: PropTypes.object,
+ object: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = { open: false };
+ this.toggleBody = this.toggleBody.bind(this);
+ }
+
+ componentDidThrow(e) {
+ console.error("Error in CustomFormatter", e);
+ this.setState(state => ({ ...state, hasError: true }));
+ }
+
+ async toggleBody(evt) {
+ evt.stopPropagation();
+
+ const open = !this.state.open;
+ if (open && !this.state.bodyJsonMl) {
+ let front = this.props.front;
+ if (!front && this.props.client?.createObjectFront) {
+ if (flags.testing && !this.props.frame) {
+ throw new Error("props.frame is mandatory");
+ }
+ front = this.props.client.createObjectFront(
+ this.props.object,
+ this.props.frame
+ );
+ }
+ if (!front) {
+ return;
+ }
+
+ const response = await front.customFormatterBody();
+
+ const bodyJsonMl = renderJsonMl(response.customFormatterBody, {
+ ...this.props,
+ autoExpandDepth: this.props.autoExpandDepth
+ ? this.props.autoExpandDepth - 1
+ : 0,
+ object: null,
+ });
+
+ this.setState(state => ({
+ ...state,
+ bodyJsonMl,
+ open,
+ }));
+ } else {
+ this.setState(state => ({
+ ...state,
+ bodyJsonMl: null,
+ open,
+ }));
+ }
+ }
+
+ render() {
+ if (this.state && this.state.hasError) {
+ return createElement(
+ "span",
+ {
+ className: "objectBox objectBox-failure",
+ title:
+ "This object could not be rendered, " +
+ "please file a bug on bugzilla.mozilla.org",
+ },
+ "Invalid custom formatter object"
+ );
+ }
+
+ const headerJsonMl = renderJsonMl(this.props.object.header, {
+ ...this.props,
+ open: this.state?.open,
+ });
+
+ return createElement(
+ "span",
+ {
+ className: "objectBox-jsonml-wrapper",
+ "data-expandable": this.props.object.hasBody,
+ "aria-expanded": this.state.open,
+ onClick: this.props.object.hasBody ? this.toggleBody : null,
+ },
+ headerJsonMl,
+ this.state.bodyJsonMl
+ ? createElement(
+ "div",
+ { className: "objectBox-jsonml-body-wrapper" },
+ this.state.bodyJsonMl
+ )
+ : null
+ );
+ }
+ }
+
+ function renderJsonMl(jsonMl, props, index = 0) {
+ // The second item of the array can either be an object holding the attributes
+ // for the element or the first child element. Therefore, all array items after the
+ // first one are fetched together and split afterwards if needed.
+ let [tagName, ...attributesAndChildren] = jsonMl ?? [];
+
+ if (!ALLOWED_TAGS.has(tagName)) {
+ tagName = "div";
+ }
+
+ const attributes = attributesAndChildren[0];
+ const hasAttributes =
+ Object(attributes) === attributes && !Array.isArray(attributes);
+ const style =
+ hasAttributes && attributes?.style && props.createElement
+ ? cleanupStyle(attributes.style, props.createElement)
+ : null;
+ const children = attributesAndChildren;
+ if (hasAttributes) {
+ children.shift();
+ }
+
+ const childElements = [];
+
+ if (props.object?.hasBody) {
+ childElements.push(
+ createElement("button", {
+ "aria-expanded": props.open,
+ className: `collapse-button jsonml-header-collapse-button${
+ props.open ? " expanded" : ""
+ }`,
+ })
+ );
+ }
+
+ if (Array.isArray(children)) {
+ children.forEach((child, childIndex) => {
+ let childElement;
+ // If the child is an array, it should be a JsonML item, so use this function to
+ // render them.
+ if (Array.isArray(child)) {
+ childElement = renderJsonMl(
+ child,
+ { ...props, object: null },
+ childIndex
+ );
+ } else if (typeof child === "object" && child !== null) {
+ // If we don't have an array, this means that we're probably dealing with
+ // a front or a grip. If the object has a `getGrip` function, call it to get the
+ // actual grip.
+ const gripOrPrimitive =
+ (child.typeName == "obj" || child.typeName == "string") &&
+ typeof child?.getGrip == "function"
+ ? child.getGrip()
+ : child;
+
+ // If the grip represents an object that was custom formatted, we should render
+ // it using this component.
+ if (supportsObject(gripOrPrimitive)) {
+ childElement = createElement(CustomFormatter, {
+ ...props,
+ object: gripOrPrimitive,
+ front: child && !!child.typeName ? child : null,
+ });
+ } else {
+ // Here we have a non custom-formatted grip, so we let the ObjectInspector
+ // handles it.
+ const {
+ objectInspector,
+ MODE,
+ } = require("devtools/client/shared/components/reps/index");
+ childElement = createElement(objectInspector.ObjectInspector, {
+ ...props,
+ mode: props.mode == MODE.LONG ? MODE.SHORT : MODE.TINY,
+ roots: [
+ {
+ path: `${
+ gripOrPrimitive?.actorID ?? gripOrPrimitive?.actor ?? null
+ }`,
+ contents: {
+ value: gripOrPrimitive,
+ front: child && !!child.typeName ? child : null,
+ },
+ },
+ ],
+ });
+ }
+ } else {
+ // Here we have a primitive. We don't want to use Rep to render them as reps come
+ // with their own styling which might clash with the style defined in the JsonMl.
+ childElement = child;
+ }
+ childElements.push(childElement);
+ });
+ } else {
+ childElements.push(children);
+ }
+
+ return createElement(
+ tagName,
+ {
+ className: `objectBox objectBox-jsonml`,
+ key: `jsonml-${tagName}-${index}`,
+ style,
+ },
+ childElements
+ );
+ }
+
+ function supportsObject(grip) {
+ return grip?.useCustomFormatter === true && Array.isArray(grip?.header);
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: createFactory(CustomFormatter),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/date-time.js b/devtools/client/shared/components/reps/reps/date-time.js
new file mode 100644
index 0000000000..36d15fcbaa
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/date-time.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+
+ DateTime.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function DateTime(props) {
+ const { object: grip, shouldRenderTooltip } = props;
+ let date;
+ try {
+ const dateObject = new Date(grip.preview.timestamp);
+ // Calling `toISOString` will throw if the date is invalid,
+ // so we can render an `Invalid Date` element.
+ dateObject.toISOString();
+
+ const dateObjectString = dateObject.toString();
+
+ const config = getElementConfig({
+ grip,
+ dateObjectString,
+ shouldRenderTooltip,
+ });
+
+ date = span(
+ config,
+ getTitle(grip),
+ span({ className: "Date" }, dateObjectString)
+ );
+ } catch (e) {
+ date = span(
+ {
+ className: "objectBox",
+ title: shouldRenderTooltip ? "Invalid Date" : null,
+ },
+ "Invalid Date"
+ );
+ }
+
+ return date;
+ }
+
+ function getElementConfig(opts) {
+ const { grip, dateObjectString, shouldRenderTooltip } = opts;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox",
+ title: shouldRenderTooltip ? `${grip.class} ${dateObjectString}` : null,
+ };
+ }
+
+ // getTitle() is used to render the `Date ` before the stringified date object,
+ // not to render the actual span "title".
+
+ function getTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ `${grip.class} `
+ );
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) == "Date" && grip?.preview;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(DateTime),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/document-type.js b/devtools/client/shared/components/reps/reps/document-type.js
new file mode 100644
index 0000000000..36442858fc
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/document-type.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders DOM documentType object.
+ */
+
+ DocumentType.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function DocumentType(props) {
+ const { object, shouldRenderTooltip } = props;
+ const name =
+ object && object.preview && object.preview.nodeName
+ ? ` ${object.preview.nodeName}`
+ : "";
+
+ const config = getElementConfig({ object, shouldRenderTooltip, name });
+
+ return span(config, `<!DOCTYPE${name}>`);
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip, name } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-document",
+ title: shouldRenderTooltip ? `<!DOCTYPE${name}>` : null,
+ };
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) === "DocumentType";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(DocumentType),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/document.js b/devtools/client/shared/components/reps/reps/document.js
new file mode 100644
index 0000000000..1ee4eeb9a7
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/document.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders DOM document object.
+ */
+
+ Document.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Document(props) {
+ const grip = props.object;
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const location = getLocation(grip);
+ const config = getElementConfig({ grip, location, shouldRenderTooltip });
+ return span(
+ config,
+ getTitle(grip),
+ location ? span({ className: "location" }, ` ${location}`) : null
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { grip, location, shouldRenderTooltip } = opts;
+ const config = {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-document",
+ };
+
+ if (!shouldRenderTooltip || !location) {
+ return config;
+ }
+ config.title = `${grip.class} ${location}`;
+ return config;
+ }
+
+ function getLocation(grip) {
+ const location = grip.preview.location;
+ return location ? getURLDisplayString(location) : null;
+ }
+
+ function getTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ grip.class
+ );
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) === "HTMLDocument";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Document),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/element-node.js b/devtools/client/shared/components/reps/reps/element-node.js
new file mode 100644
index 0000000000..a31fb4225b
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/element-node.js
@@ -0,0 +1,322 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Utils
+ const {
+ appendRTLClassNameIfNeeded,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ rep: StringRep,
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const nodeConstants = require("devtools/client/shared/components/reps/shared/dom-node-constants");
+
+ const MAX_ATTRIBUTE_LENGTH = 50;
+
+ /**
+ * Renders DOM element node.
+ */
+
+ ElementNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ // The class should be in reps.css
+ inspectIconTitle: PropTypes.oneOf(["open-inspector", "highlight-node"]),
+ inspectIconClassName: PropTypes.string,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeClick: PropTypes.func,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ElementNode(props) {
+ const { object, mode, shouldRenderTooltip } = props;
+
+ const {
+ isAfterPseudoElement,
+ isBeforePseudoElement,
+ isMarkerPseudoElement,
+ } = object.preview;
+
+ let renderElements = [];
+ const isInTree = object.preview && object.preview.isConnected === true;
+ let config = getElementConfig({ ...props, isInTree });
+ const inspectIcon = getInspectIcon({ ...props, isInTree });
+
+ // Elements Case 1: Pseudo Element
+ if (
+ isAfterPseudoElement ||
+ isBeforePseudoElement ||
+ isMarkerPseudoElement
+ ) {
+ const pseudoNodeElement = getPseudoNodeElement(object);
+
+ // Regenerate config if shouldRenderTooltip
+ if (shouldRenderTooltip) {
+ const tooltipString = pseudoNodeElement.content;
+ config = getElementConfig({ ...props, tooltipString, isInTree });
+ }
+
+ // Return ONLY pseudo node element as array[0]
+ renderElements = [
+ span(pseudoNodeElement.config, pseudoNodeElement.content),
+ ];
+ } else if (mode === MODE.TINY) {
+ // Elements Case 2: MODE.TINY
+ const tinyElements = getTinyElements(object);
+
+ // Regenerate config to include tooltip title
+ if (shouldRenderTooltip) {
+ // Reduce for plaintext
+ const tooltipString = tinyElements.reduce(function (acc, cur) {
+ return acc.concat(cur.content);
+ }, "");
+
+ config = getElementConfig({ ...props, tooltipString, isInTree });
+ }
+
+ // Reduce for React elements
+ const tinyElementsRender = tinyElements.reduce(function (acc, cur) {
+ acc.push(span(cur.config, cur.content));
+ return acc;
+ }, []);
+
+ // Render array of React spans
+ renderElements = tinyElementsRender;
+ } else {
+ // Elements Default case
+ renderElements = getElements(props);
+ }
+
+ return span(config, ...renderElements, inspectIcon ? inspectIcon : null);
+ }
+
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onDOMNodeClick,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ shouldRenderTooltip,
+ tooltipString,
+ } = opts;
+
+ // Initiate config
+ const config = {
+ "data-link-actor-id": object.actor,
+ "data-link-content-dom-reference": JSON.stringify(
+ object.contentDomReference
+ ),
+ className: "objectBox objectBox-node",
+ };
+
+ // Triage event handlers
+ if (isInTree) {
+ if (onDOMNodeClick) {
+ Object.assign(config, {
+ onClick: _ => onDOMNodeClick(object),
+ className: `${config.className} clickable`,
+ });
+ }
+
+ if (onDOMNodeMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onDOMNodeMouseOver(object),
+ });
+ }
+
+ if (onDOMNodeMouseOut) {
+ Object.assign(config, {
+ onMouseOut: _ => onDOMNodeMouseOut(object),
+ });
+ }
+ }
+
+ // If tooltip, build tooltip
+ if (tooltipString && shouldRenderTooltip) {
+ config.title = tooltipString;
+ }
+
+ // Return config obj
+ return config;
+ }
+
+ function getElements(opts) {
+ const { object: grip } = opts;
+
+ const { attributes, nodeName } = grip.preview;
+
+ const nodeNameElement = span(
+ {
+ className: appendRTLClassNameIfNeeded("tag-name", nodeName),
+ },
+ nodeName
+ );
+
+ const attributeKeys = Object.keys(attributes);
+ if (attributeKeys.includes("class")) {
+ attributeKeys.splice(attributeKeys.indexOf("class"), 1);
+ attributeKeys.unshift("class");
+ }
+ if (attributeKeys.includes("id")) {
+ attributeKeys.splice(attributeKeys.indexOf("id"), 1);
+ attributeKeys.unshift("id");
+ }
+ const attributeElements = attributeKeys.reduce((arr, name, i, keys) => {
+ const value = attributes[name];
+
+ let title = isLongString(value) ? value.initial : value;
+ if (title.length < MAX_ATTRIBUTE_LENGTH) {
+ title = null;
+ }
+
+ const attribute = span(
+ {},
+ span(
+ {
+ className: appendRTLClassNameIfNeeded("attrName", name),
+ },
+ name
+ ),
+ span({ className: "attrEqual" }, "="),
+ StringRep({
+ className: "attrValue",
+ object: value,
+ cropLimit: MAX_ATTRIBUTE_LENGTH,
+ title,
+ })
+ );
+
+ return arr.concat([" ", attribute]);
+ }, []);
+
+ return [
+ span({ className: "angleBracket" }, "<"),
+ nodeNameElement,
+ ...attributeElements,
+ span({ className: "angleBracket" }, ">"),
+ ];
+ }
+
+ function getTinyElements(grip) {
+ const { attributes, nodeName } = grip.preview;
+
+ // Initialize elements array
+ const elements = [
+ {
+ config: {
+ className: appendRTLClassNameIfNeeded("tag-name", nodeName),
+ },
+ content: nodeName,
+ },
+ ];
+
+ // Push ID element
+ if (attributes.id) {
+ elements.push({
+ config: {
+ className: appendRTLClassNameIfNeeded("attrName", attributes.id),
+ },
+ content: `#${attributes.id}`,
+ });
+ }
+
+ // Push Classes
+ if (attributes.class) {
+ const elementClasses = attributes.class
+ .trim()
+ .split(/\s+/)
+ .map(cls => `.${cls}`)
+ .join("");
+ elements.push({
+ config: {
+ className: appendRTLClassNameIfNeeded("attrName", elementClasses),
+ },
+ content: elementClasses,
+ });
+ }
+
+ return elements;
+ }
+
+ function getPseudoNodeElement(grip) {
+ const {
+ isAfterPseudoElement,
+ isBeforePseudoElement,
+ isMarkerPseudoElement,
+ } = grip.preview;
+
+ let pseudoNodeName;
+
+ if (isAfterPseudoElement) {
+ pseudoNodeName = "after";
+ } else if (isBeforePseudoElement) {
+ pseudoNodeName = "before";
+ } else if (isMarkerPseudoElement) {
+ pseudoNodeName = "marker";
+ }
+
+ return {
+ config: { className: "attrName" },
+ content: `::${pseudoNodeName}`,
+ };
+ }
+
+ function getInspectIcon(opts) {
+ const {
+ object,
+ isInTree,
+ onInspectIconClick,
+ inspectIconTitle,
+ inspectIconClassName,
+ onDOMNodeClick,
+ } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: inspectIconClassName || "open-inspector",
+ // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+ title: inspectIconTitle || "Click to select the node in the inspector",
+ onClick: e => {
+ if (onDOMNodeClick) {
+ e.stopPropagation();
+ }
+
+ onInspectIconClick(object, e);
+ },
+ });
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return object?.preview?.nodeType === nodeConstants.ELEMENT_NODE;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ElementNode),
+ supportsObject,
+ MAX_ATTRIBUTE_LENGTH,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/error.js b/devtools/client/shared/components/reps/reps/error.js
new file mode 100644
index 0000000000..617bf8c8a2
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/error.js
@@ -0,0 +1,338 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ div,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ cleanFunctionName,
+ } = require("devtools/client/shared/components/reps/reps/function");
+ const {
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+ /**
+ * Renders Error objects.
+ */
+ ErrorRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ // An optional function that will be used to render the Error stacktrace.
+ renderStacktrace: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ /**
+ * Render an Error object.
+ * The customFormat prop allows to print a simplified view of the object, with only the
+ * message and the stacktrace, e.g.:
+ * Error: "blah"
+ * <anonymous> debugger eval code:1
+ *
+ * The customFormat prop will only be taken into account if the mode isn't tiny and the
+ * depth is 0. This is because we don't want error in previews or in object to be
+ * displayed unlike other objects:
+ * - Object { err: Error }
+ * - â–¼ {
+ * err: Error: "blah"
+ * }
+ */
+ function ErrorRep(props) {
+ const { object, mode, shouldRenderTooltip, depth } = props;
+ const preview = object.preview;
+ const customFormat =
+ props.customFormat &&
+ mode !== MODE.TINY &&
+ mode !== MODE.HEADER &&
+ !depth;
+
+ const name = getErrorName(props);
+ const errorTitle =
+ mode === MODE.TINY || mode === MODE.HEADER ? name : `${name}: `;
+ const content = [];
+
+ if (customFormat) {
+ content.push(errorTitle);
+ } else {
+ content.push(
+ span({ className: "objectTitle", key: "title" }, errorTitle)
+ );
+ }
+
+ if (mode !== MODE.TINY && mode !== MODE.HEADER) {
+ const {
+ Rep,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+ content.push(
+ Rep({
+ ...props,
+ key: "message",
+ object: preview.message,
+ mode: props.mode || MODE.TINY,
+ useQuotes: false,
+ })
+ );
+ }
+ const renderStack = preview.stack && customFormat;
+ if (renderStack) {
+ const stacktrace = props.renderStacktrace
+ ? props.renderStacktrace(parseStackString(preview.stack))
+ : getStacktraceElements(props, preview);
+ content.push(stacktrace);
+ }
+
+ const renderCause = customFormat && preview.hasOwnProperty("cause");
+ if (renderCause) {
+ content.push(getCauseElement(props, preview));
+ }
+
+ return span(
+ {
+ "data-link-actor-id": object.actor,
+ className: `objectBox-stackTrace ${
+ customFormat ? "reps-custom-format" : ""
+ }`,
+ title: shouldRenderTooltip ? `${name}: "${preview.message}"` : null,
+ },
+ ...content
+ );
+ }
+
+ function getErrorName(props) {
+ const { object } = props;
+ const preview = object.preview;
+
+ let name;
+ if (typeof preview?.name === "string" && preview.kind) {
+ switch (preview.kind) {
+ case "Error":
+ name = preview.name;
+ break;
+ case "DOMException":
+ name = preview.kind;
+ break;
+ default:
+ throw new Error("Unknown preview kind for the Error rep.");
+ }
+ } else {
+ name = "Error";
+ }
+
+ return name;
+ }
+
+ /**
+ * Returns a React element reprensenting the Error stacktrace, i.e.
+ * transform error.stack from:
+ *
+ * semicolon@debugger eval code:1:109
+ * jkl@debugger eval code:1:63
+ * asdf@debugger eval code:1:28
+ * @debugger eval code:1:227
+ *
+ * Into a column layout:
+ *
+ * semicolon (<anonymous>:8:10)
+ * jkl (<anonymous>:5:10)
+ * asdf (<anonymous>:2:10)
+ * (<anonymous>:11:1)
+ */
+ function getStacktraceElements(props, preview) {
+ const stack = [];
+ if (!preview.stack) {
+ return stack;
+ }
+
+ parseStackString(preview.stack).forEach((frame, index, frames) => {
+ let onLocationClick;
+ const { filename, lineNumber, columnNumber, functionName, location } =
+ frame;
+
+ if (
+ props.onViewSourceInDebugger &&
+ !IGNORED_SOURCE_URLS.includes(filename)
+ ) {
+ onLocationClick = e => {
+ // Don't trigger ObjectInspector expand/collapse.
+ e.stopPropagation();
+ props.onViewSourceInDebugger({
+ url: filename,
+ line: lineNumber,
+ column: columnNumber,
+ });
+ };
+ }
+
+ stack.push(
+ "\t",
+ span(
+ {
+ key: `fn${index}`,
+ className: "objectBox-stackTrace-fn",
+ },
+ cleanFunctionName(functionName)
+ ),
+ " ",
+ span(
+ {
+ key: `location${index}`,
+ className: "objectBox-stackTrace-location",
+ onClick: onLocationClick,
+ title: onLocationClick
+ ? `View source in debugger → ${location}`
+ : undefined,
+ },
+ location
+ ),
+ "\n"
+ );
+ });
+
+ return span(
+ {
+ key: "stack",
+ className: "objectBox-stackTrace-grid",
+ },
+ stack
+ );
+ }
+
+ /**
+ * Returns a React element representing the cause of the Error i.e. the `cause`
+ * property in the second parameter of the Error constructor (`new Error("message", { cause })`)
+ *
+ * Example:
+ * Caused by: Error: original error
+ */
+ function getCauseElement(props, preview) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+ return div(
+ {
+ key: "cause-container",
+ className: "error-rep-cause",
+ },
+ "Caused by: ",
+ Rep({
+ ...props,
+ key: "cause",
+ object: preview.cause,
+ mode: props.mode || MODE.TINY,
+ })
+ );
+ }
+
+ /**
+ * Parse a string that should represent a stack trace and returns an array of
+ * the frames. The shape of the frames are extremely important as they can then
+ * be processed here or in the toolbox by other components.
+ * @param {String} stack
+ * @returns {Array} Array of frames, which are object with the following shape:
+ * - {String} filename
+ * - {String} functionName
+ * - {String} location
+ * - {Number} columnNumber
+ * - {Number} lineNumber
+ */
+ function parseStackString(stack) {
+ if (!stack) {
+ return [];
+ }
+
+ const isStacktraceALongString = isLongString(stack);
+ const stackString = isStacktraceALongString ? stack.initial : stack;
+
+ if (typeof stackString !== "string") {
+ return [];
+ }
+
+ const res = [];
+ stackString.split("\n").forEach((frame, index, frames) => {
+ if (!frame) {
+ // Skip any blank lines
+ return;
+ }
+
+ // If the stacktrace is a longString, don't include the last frame in the
+ // array, since it is certainly incomplete.
+ // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833
+ // is fixed.
+ if (isStacktraceALongString && index === frames.length - 1) {
+ return;
+ }
+
+ let functionName;
+ let location;
+
+ // Retrieve the index of the first @ to split the frame string.
+ const atCharIndex = frame.indexOf("@");
+ if (atCharIndex > -1) {
+ functionName = frame.slice(0, atCharIndex);
+ location = frame.slice(atCharIndex + 1);
+ }
+
+ if (location && location.includes(" -> ")) {
+ // If the resource was loaded by base-loader.sys.mjs, the location looks like:
+ // resource://devtools/shared/base-loader.sys.mjs -> resource://path/to/file.js .
+ // What's needed is only the last part after " -> ".
+ location = location.split(" -> ").pop();
+ }
+
+ if (!functionName) {
+ functionName = "<anonymous>";
+ }
+
+ // Given the input: "scriptLocation:2:100"
+ // Result:
+ // ["scriptLocation:2:100", "scriptLocation", "2", "100"]
+ const locationParts = location
+ ? location.match(/^(.*):(\d+):(\d+)$/)
+ : null;
+
+ if (location && locationParts) {
+ const [, filename, line, column] = locationParts;
+ res.push({
+ filename,
+ functionName,
+ location,
+ columnNumber: Number(column),
+ lineNumber: Number(line),
+ });
+ }
+ });
+
+ return res;
+ }
+
+ // Registration
+ function supportsObject(object) {
+ return (
+ object?.isError ||
+ object?.class === "DOMException" ||
+ object?.class === "Exception"
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ErrorRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/event.js b/devtools/client/shared/components/reps/reps/event.js
new file mode 100644
index 0000000000..460f7e8c31
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/event.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Reps
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const { rep } = require("devtools/client/shared/components/reps/reps/grip");
+
+ /**
+ * Renders DOM event objects.
+ */
+ Event.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ };
+
+ function Event(props) {
+ const gripProps = {
+ ...props,
+ title: getTitle(props),
+ object: {
+ ...props.object,
+ preview: {
+ ...props.object.preview,
+ ownProperties: {},
+ },
+ },
+ };
+
+ if (gripProps.object.preview.target) {
+ Object.assign(gripProps.object.preview.ownProperties, {
+ target: gripProps.object.preview.target,
+ });
+ }
+ Object.assign(
+ gripProps.object.preview.ownProperties,
+ gripProps.object.preview.properties
+ );
+
+ delete gripProps.object.preview.properties;
+ gripProps.object.ownPropertyLength = Object.keys(
+ gripProps.object.preview.ownProperties
+ ).length;
+
+ switch (gripProps.object.class) {
+ case "MouseEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "clientX", "clientY", "layerX", "layerY"].includes(
+ name
+ );
+ };
+ break;
+ case "KeyboardEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "key", "charCode", "keyCode"].includes(name);
+ };
+ break;
+ case "MessageEvent":
+ gripProps.isInterestingProp = (type, value, name) => {
+ return ["target", "isTrusted", "data"].includes(name);
+ };
+ break;
+ default:
+ gripProps.isInterestingProp = (type, value, name) => {
+ // We want to show the properties in the order they are declared.
+ return Object.keys(gripProps.object.preview.ownProperties).includes(
+ name
+ );
+ };
+ }
+
+ return rep(gripProps);
+ }
+
+ function getTitle(props) {
+ const preview = props.object.preview;
+ let title = preview.type;
+
+ if (
+ preview.eventKind == "key" &&
+ preview.modifiers &&
+ preview.modifiers.length
+ ) {
+ title = `${title} ${preview.modifiers.join("-")}`;
+ }
+ return title;
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "DOMEvent";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(Event),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/function.js b/devtools/client/shared/components/reps/reps/function.js
new file mode 100644
index 0000000000..54d8905c20
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/function.js
@@ -0,0 +1,264 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ cropString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+ /**
+ * This component represents a template for Function objects.
+ */
+
+ FunctionRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ onViewSourceInDebugger: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function FunctionRep(props) {
+ const {
+ object: grip,
+ onViewSourceInDebugger,
+ recordTelemetryEvent,
+ shouldRenderTooltip,
+ } = props;
+
+ let jumpToDefinitionButton;
+
+ // Test to see if we should display the link back to the original function definition
+ if (
+ onViewSourceInDebugger &&
+ grip.location &&
+ grip.location.url &&
+ !IGNORED_SOURCE_URLS.includes(grip.location.url)
+ ) {
+ jumpToDefinitionButton = button({
+ className: "jump-definition",
+ draggable: false,
+ title: "Jump to definition",
+ onClick: async e => {
+ // Stop the event propagation so we don't trigger ObjectInspector
+ // expand/collapse.
+ e.stopPropagation();
+ if (recordTelemetryEvent) {
+ recordTelemetryEvent("jump_to_definition");
+ }
+
+ onViewSourceInDebugger(grip.location);
+ },
+ });
+ }
+
+ const elProps = {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-function",
+ // Set dir="ltr" to prevent parentheses from
+ // appearing in the wrong direction
+ dir: "ltr",
+ };
+
+ const parameterNames = (grip.parameterNames || []).filter(Boolean);
+ const fnTitle = getFunctionTitle(grip, props);
+ const fnName = getFunctionName(grip, props);
+
+ if (grip.isClassConstructor) {
+ const classTitle = getClassTitle(grip, props);
+ const classBodyTooltip = getClassBody(parameterNames, true, props);
+ const classTooltip = `${classTitle ? classTitle.props.children : ""}${
+ fnName ? fnName : ""
+ }${classBodyTooltip.join("")}`;
+
+ elProps.title = shouldRenderTooltip ? classTooltip : null;
+
+ return span(
+ elProps,
+ classTitle,
+ fnName,
+ ...getClassBody(parameterNames, false, props),
+ jumpToDefinitionButton
+ );
+ }
+
+ const fnTooltip = `${fnTitle ? fnTitle.props.children : ""}${
+ fnName ? fnName : ""
+ }(${parameterNames.join(", ")})`;
+
+ elProps.title = shouldRenderTooltip ? fnTooltip : null;
+
+ const returnSpan = span(
+ elProps,
+ fnTitle,
+ fnName,
+ "(",
+ ...getParams(parameterNames),
+ ")",
+ jumpToDefinitionButton
+ );
+
+ return returnSpan;
+ }
+
+ function getClassTitle(grip) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ "class "
+ );
+ }
+
+ function getFunctionTitle(grip, props) {
+ const { mode } = props;
+
+ if (mode === MODE.TINY && !grip.isGenerator && !grip.isAsync) {
+ return null;
+ }
+
+ let title = mode === MODE.TINY ? "" : "function ";
+
+ if (grip.isGenerator) {
+ title = mode === MODE.TINY ? "* " : "function* ";
+ }
+
+ if (grip.isAsync) {
+ title = `${"async" + " "}${title}`;
+ }
+
+ return span(
+ {
+ className: "objectTitle",
+ },
+ title
+ );
+ }
+
+ /**
+ * Returns a ReactElement representing the function name.
+ *
+ * @param {Object} grip : Function grip
+ * @param {Object} props: Function rep props
+ */
+ function getFunctionName(grip, props = {}) {
+ let { functionName } = props;
+ let name;
+
+ if (functionName) {
+ const end = functionName.length - 1;
+ functionName =
+ functionName.startsWith('"') && functionName.endsWith('"')
+ ? functionName.substring(1, end)
+ : functionName;
+ }
+
+ if (
+ grip.displayName != undefined &&
+ functionName != undefined &&
+ grip.displayName != functionName
+ ) {
+ name = `${functionName}:${grip.displayName}`;
+ } else {
+ name = cleanFunctionName(
+ grip.userDisplayName ||
+ grip.displayName ||
+ grip.name ||
+ props.functionName ||
+ ""
+ );
+ }
+
+ return cropString(name, 100);
+ }
+
+ const objectProperty = /([\w\d\$]+)$/;
+ const arrayProperty = /\[(.*?)\]$/;
+ const functionProperty = /([\w\d]+)[\/\.<]*?$/;
+ const annonymousProperty = /([\w\d]+)\(\^\)$/;
+
+ /**
+ * Decodes an anonymous naming scheme that
+ * spider monkey implements based on "Naming Anonymous JavaScript Functions"
+ * http://johnjbarton.github.io/nonymous/index.html
+ *
+ * @param {String} name : Function name to clean up
+ * @returns String
+ */
+ function cleanFunctionName(name) {
+ for (const reg of [
+ objectProperty,
+ arrayProperty,
+ functionProperty,
+ annonymousProperty,
+ ]) {
+ const match = reg.exec(name);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return name;
+ }
+
+ function getClassBody(constructorParams, textOnly = false, props) {
+ const { mode } = props;
+
+ if (mode === MODE.TINY) {
+ return [];
+ }
+
+ return [" {", ...getClassConstructor(textOnly, constructorParams), "}"];
+ }
+
+ function getClassConstructor(textOnly = false, parameterNames) {
+ if (parameterNames.length === 0) {
+ return [];
+ }
+
+ if (textOnly) {
+ return [` constructor(${parameterNames.join(", ")}) `];
+ }
+ return [" constructor(", ...getParams(parameterNames), ") "];
+ }
+
+ function getParams(parameterNames) {
+ return parameterNames.flatMap((param, index, arr) => {
+ return [
+ span({ className: "param" }, param),
+ index === arr.length - 1 ? "" : span({ className: "delimiter" }, ", "),
+ ];
+ });
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return getGripType(grip, noGrip) === "Function";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(FunctionRep),
+ supportsObject,
+ cleanFunctionName,
+ // exported for testing purpose.
+ getFunctionName,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-array.js b/devtools/client/shared/components/reps/reps/grip-array.js
new file mode 100644
index 0000000000..e9c67039a4
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-array.js
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ lengthBubble,
+ } = require("devtools/client/shared/components/reps/shared/grip-length-bubble");
+ const {
+ interleave,
+ getGripType,
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const DEFAULT_TITLE = "Array";
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+
+ GripArray.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ provider: PropTypes.object,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function GripArray(props) {
+ const { object, mode = MODE.SHORT, shouldRenderTooltip } = props;
+
+ let brackets;
+ const needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+ };
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-array",
+ title: shouldRenderTooltip ? "Array" : null,
+ };
+
+ const title = getTitle(props, object);
+
+ if (mode === MODE.TINY) {
+ const isEmpty = getLength(object) === 0;
+
+ // Omit bracketed ellipsis for non-empty non-Array arraylikes (f.e: Sets).
+ if (!isEmpty && object.class !== "Array") {
+ return span(config, title);
+ }
+
+ brackets = needSpace(false);
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ isEmpty ? null : ellipsisElement,
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ )
+ );
+ }
+
+ if (mode === MODE.HEADER) {
+ return span(config, title);
+ }
+
+ const max = maxLengthMap.get(mode);
+ const items = arrayIterator(props, object, max);
+ brackets = needSpace(!!items.length);
+
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "arrayLeftBracket",
+ },
+ brackets.left
+ ),
+ ...interleave(items, ", "),
+ span(
+ {
+ className: "arrayRightBracket",
+ },
+ brackets.right
+ ),
+ span({
+ className: "arrayProperties",
+ role: "group",
+ })
+ );
+ }
+
+ function getLength(grip) {
+ if (!grip.preview) {
+ return 0;
+ }
+
+ return grip.preview.length || grip.preview.childNodesLength || 0;
+ }
+
+ function getTitle(props, object) {
+ const objectLength = getLength(object);
+ const isEmpty = objectLength === 0;
+
+ let title = props.title || object.class || DEFAULT_TITLE;
+
+ const length = lengthBubble({
+ object,
+ mode: props.mode,
+ maxLengthMap,
+ getLength,
+ });
+
+ if (props.mode === MODE.TINY) {
+ if (isEmpty) {
+ if (object.class === DEFAULT_TITLE) {
+ return null;
+ }
+
+ return span({ className: "objectTitle" }, `${title} `);
+ }
+
+ let trailingSpace;
+ if (object.class === DEFAULT_TITLE) {
+ title = null;
+ trailingSpace = " ";
+ }
+
+ return span({ className: "objectTitle" }, title, length, trailingSpace);
+ }
+
+ if (props.mode === MODE.HEADER) {
+ return span({ className: "objectTitle" }, title, length);
+ }
+
+ return span({ className: "objectTitle" }, title, length, " ");
+ }
+
+ function getPreviewItems(grip) {
+ if (!grip.preview) {
+ return null;
+ }
+
+ return grip.preview.items || grip.preview.childNodes || [];
+ }
+
+ function arrayIterator(props, grip, max) {
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ let items = [];
+ const gripLength = getLength(grip);
+
+ if (!gripLength) {
+ return items;
+ }
+
+ const previewItems = getPreviewItems(grip);
+ const provider = props.provider;
+
+ let emptySlots = 0;
+ let foldedEmptySlots = 0;
+ items = previewItems.reduce((res, itemGrip) => {
+ if (res.length >= max) {
+ return res;
+ }
+
+ let object;
+ try {
+ if (!provider && itemGrip === null) {
+ emptySlots++;
+ return res;
+ }
+
+ object = provider ? provider.getValue(itemGrip) : itemGrip;
+ } catch (exc) {
+ object = exc;
+ }
+
+ if (emptySlots > 0) {
+ res.push(getEmptySlotsElement(emptySlots));
+ foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+ emptySlots = 0;
+ }
+
+ if (res.length < max) {
+ res.push(
+ Rep({
+ ...props,
+ object,
+ mode: MODE.TINY,
+ // Do not propagate title to array items reps
+ title: undefined,
+ })
+ );
+ }
+
+ return res;
+ }, []);
+
+ // Handle trailing empty slots if there are some.
+ if (items.length < max && emptySlots > 0) {
+ items.push(getEmptySlotsElement(emptySlots));
+ foldedEmptySlots = foldedEmptySlots + emptySlots - 1;
+ }
+
+ const itemsShown = items.length + foldedEmptySlots;
+ if (gripLength > itemsShown) {
+ items.push(ellipsisElement);
+ }
+
+ return items;
+ }
+
+ function getEmptySlotsElement(number) {
+ // TODO: Use l10N - See https://github.com/firefox-devtools/reps/issues/141
+ return `<${number} empty slot${number > 1 ? "s" : ""}>`;
+ }
+
+ function supportsObject(grip, noGrip = false) {
+ return (
+ grip?.preview &&
+ (grip.preview.kind == "ArrayLike" ||
+ getGripType(grip, noGrip) === "DocumentFragment")
+ );
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripArray),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-entry.js b/devtools/client/shared/components/reps/reps/grip-entry.js
new file mode 100644
index 0000000000..6c08ca5d70
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-entry.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+ // Utils
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an entry of a Map, (Local|Session)Storage, Header or FormData entry.
+ */
+ GripEntry.propTypes = {
+ object: PropTypes.object,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ };
+
+ function GripEntry(props) {
+ const { object } = props;
+
+ let { key, value } = object.preview;
+ if (key && key.getGrip) {
+ key = key.getGrip();
+ }
+ if (value && value.getGrip) {
+ value = value.getGrip();
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-map-entry",
+ },
+ PropRep({
+ ...props,
+ name: key,
+ object: value,
+ equal: " \u2192 ",
+ title: null,
+ suppressQuotes: false,
+ })
+ );
+ }
+
+ function supportsObject(grip, noGrip = false) {
+ if (noGrip === true) {
+ return false;
+ }
+ return (
+ grip &&
+ (grip.type === "formDataEntry" ||
+ grip.type === "highlightRegistryEntry" ||
+ grip.type === "mapEntry" ||
+ grip.type === "storageEntry" ||
+ grip.type === "urlSearchParamsEntry") &&
+ grip.preview
+ );
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripEntry),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip-map.js b/devtools/client/shared/components/reps/reps/grip-map.js
new file mode 100644
index 0000000000..dcb7c50972
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip-map.js
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ lengthBubble,
+ } = require("devtools/client/shared/components/reps/shared/grip-length-bubble");
+ const {
+ interleave,
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+
+ GripMap.propTypes = {
+ object: PropTypes.object,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ isInterestingEntry: PropTypes.func,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ title: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function GripMap(props) {
+ const { mode, object, shouldRenderTooltip } = props;
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTooltip(object, props) : null,
+ };
+
+ const title = getTitle(props, object);
+ const isEmpty = getLength(object) === 0;
+
+ if (isEmpty || mode === MODE.TINY || mode === MODE.HEADER) {
+ return span(config, title);
+ }
+
+ const propsArray = safeEntriesIterator(
+ props,
+ object,
+ maxLengthMap.get(mode)
+ );
+
+ return span(
+ config,
+ title,
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...interleave(propsArray, ", "),
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitle(props, object) {
+ const title =
+ props.title || (object && object.class ? object.class : "Map");
+ return span(
+ {
+ className: "objectTitle",
+ },
+ title,
+ lengthBubble({
+ object,
+ mode: props.mode,
+ maxLengthMap,
+ getLength,
+ showZeroLength: true,
+ })
+ );
+ }
+
+ function getTooltip(object, props) {
+ const tooltip =
+ props.title || (object && object.class ? object.class : "Map");
+ return `${tooltip}(${getLength(object)})`;
+ }
+
+ function safeEntriesIterator(props, object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return entriesIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function entriesIterator(props, object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ const isInterestingEntry =
+ props.isInterestingEntry ||
+ ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && !!value.length)
+ );
+ });
+
+ const mapEntries =
+ object.preview && object.preview.entries ? object.preview.entries : [];
+
+ let indexes = getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ getEntriesIndexes(
+ mapEntries,
+ max - indexes.length,
+ (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ }
+ )
+ );
+ }
+
+ const entries = getEntries(props, mapEntries, indexes);
+ if (entries.length < getLength(object)) {
+ // There are some undisplayed entries. Then display "…".
+ entries.push(ellipsisElement);
+ }
+
+ return entries;
+ }
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Object} props Component props.
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ function getEntries(props, entries, indexes) {
+ const { onDOMNodeMouseOver, onDOMNodeMouseOut, onInspectIconClick } = props;
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ const [key, entryValue] = entries[index];
+ const value =
+ entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ name: key && key.getGrip ? key.getGrip() : key,
+ equal: " \u2192 ",
+ object: value && value.getGrip ? value.getGrip() : value,
+ mode: MODE.TINY,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ onInspectIconClick,
+ });
+ });
+ }
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ function getEntriesIndexes(entries, max, filter) {
+ return entries.reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ const value = entry && entry.value !== undefined ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ const type = (
+ value && value.class ? value.class : typeof value
+ ).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ }
+
+ function getLength(grip) {
+ return grip.preview.size || 0;
+ }
+
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "MapLike";
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(GripMap),
+ supportsObject,
+ maxLengthMap,
+ getLength,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/grip.js b/devtools/client/shared/components/reps/reps/grip.js
new file mode 100644
index 0000000000..68f356858a
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/grip.js
@@ -0,0 +1,401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Dependencies
+ const {
+ interleave,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+
+ GripRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ isInterestingProp: PropTypes.func,
+ title: PropTypes.string,
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ noGrip: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ const DEFAULT_TITLE = "Object";
+
+ function GripRep(props) {
+ const { mode = MODE.SHORT, object, shouldRenderTooltip } = props;
+
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ };
+
+ if (mode === MODE.TINY) {
+ const propertiesLength = getPropertiesLength(object);
+
+ const tinyModeItems = [];
+ if (getTitle(props, object) !== DEFAULT_TITLE) {
+ tinyModeItems.push(getTitleElement(props, object));
+ } else {
+ tinyModeItems.push(
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ "{"
+ ),
+ propertiesLength > 0
+ ? span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ : null,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ "}"
+ )
+ );
+ }
+
+ config.title = shouldRenderTooltip ? getTitle(props, object) : null;
+
+ return span(config, ...tinyModeItems);
+ }
+
+ if (mode === MODE.HEADER) {
+ config.title = shouldRenderTooltip ? getTitle(props, object) : null;
+ return span(config, getTitleElement(props, object));
+ }
+
+ const propsArray = safePropIterator(props, object, maxLengthMap.get(mode));
+
+ config.title = shouldRenderTooltip ? getTitle(props, object) : null;
+
+ return span(
+ config,
+ getTitleElement(props, object),
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...interleave(propsArray, ", "),
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitleElement(props, object) {
+ return span(
+ {
+ className: "objectTitle",
+ },
+ getTitle(props, object)
+ );
+ }
+
+ function getTitle(props, object) {
+ return props.title || object.class || DEFAULT_TITLE;
+ }
+
+ function getPropertiesLength(object) {
+ let propertiesLength =
+ object.preview && object.preview.ownPropertiesLength
+ ? object.preview.ownPropertiesLength
+ : object.ownPropertyLength;
+
+ if (object.preview && object.preview.safeGetterValues) {
+ propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+ }
+
+ if (object.preview && object.preview.ownSymbols) {
+ propertiesLength += object.preview.ownSymbolsLength;
+ }
+
+ if (object.preview && object.preview.privateProperties) {
+ propertiesLength += object.preview.privatePropertiesLength;
+ }
+
+ return propertiesLength;
+ }
+
+ function safePropIterator(props, object, max) {
+ max = typeof max === "undefined" ? maxLengthMap.get(MODE.SHORT) : max;
+ try {
+ return propIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function propIterator(props, object, max) {
+ if (
+ object.preview &&
+ Object.keys(object.preview).includes("wrappedValue")
+ ) {
+ const {
+ Rep,
+ } = require("devtools/client/shared/components/reps/reps/rep");
+
+ return [
+ Rep({
+ object: object.preview.wrappedValue,
+ mode: props.mode || MODE.TINY,
+ defaultRep: Grip,
+ }),
+ ];
+ }
+
+ // Property filter. Show only interesting properties to the user.
+ const isInterestingProp =
+ props.isInterestingProp ||
+ ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && !!value.length)
+ );
+ });
+
+ let properties = object.preview ? object.preview.ownProperties || {} : {};
+
+ const propertiesLength = getPropertiesLength(object);
+
+ if (object.preview && object.preview.safeGetterValues) {
+ properties = { ...properties, ...object.preview.safeGetterValues };
+ }
+
+ let indexes = getPropIndexes(properties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < propertiesLength) {
+ // There are not enough props yet.
+ // Then add uninteresting props to display them.
+ indexes = indexes.concat(
+ getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ })
+ );
+ }
+
+ // The server synthesizes some property names for a Proxy, like
+ // <target> and <handler>; we don't want to quote these because,
+ // as synthetic properties, they appear more natural when
+ // unquoted. Analogous for a Promise.
+ const suppressQuotes = ["Proxy", "Promise"].includes(object.class);
+ const propsArray = getProps(props, properties, indexes, suppressQuotes);
+
+ // Show private properties
+ if (object.preview && object.preview.privateProperties) {
+ const { privateProperties } = object.preview;
+ const length = max - indexes.length;
+
+ const privateProps = privateProperties.slice(0, length).map(item => {
+ const value = item.descriptor.value;
+ const grip = value && value.getGrip ? value.getGrip() : value;
+
+ return PropRep({
+ ...props,
+ keyClassName: "private",
+ mode: MODE.TINY,
+ name: item.name,
+ object: grip,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes: true,
+ });
+ });
+
+ propsArray.push(...privateProps);
+ }
+
+ // Show symbols.
+ if (object.preview && object.preview.ownSymbols) {
+ const { ownSymbols } = object.preview;
+ const length = max - indexes.length;
+
+ const symbolsProps = ownSymbols.slice(0, length).map(symbolItem => {
+ const symbolValue = symbolItem.descriptor.value;
+ const symbolGrip =
+ symbolValue && symbolValue.getGrip
+ ? symbolValue.getGrip()
+ : symbolValue;
+
+ return PropRep({
+ ...props,
+ mode: MODE.TINY,
+ name: symbolItem,
+ object: symbolGrip,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes,
+ });
+ });
+
+ propsArray.push(...symbolsProps);
+ }
+
+ if (
+ Object.keys(properties).length > max ||
+ propertiesLength > max ||
+ // When the object has non-enumerable properties, we don't have them in the
+ // packet, but we might want to show there's something in the object.
+ propertiesLength > propsArray.length
+ ) {
+ // There are some undisplayed props. Then display "more...".
+ propsArray.push(
+ span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ },
+ "…"
+ )
+ );
+ }
+
+ return propsArray;
+ }
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} componentProps Grip Component props.
+ * @param {Object} properties Properties of the object the Grip describes.
+ * @param {Array} indexes Indexes of properties.
+ * @param {Boolean} suppressQuotes true if we should suppress quotes
+ * on property names.
+ * @return {Array} Props.
+ */
+ function getProps(componentProps, properties, indexes, suppressQuotes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ const propertiesKeys = Object.keys(properties);
+ return indexes.map(i => {
+ const name = propertiesKeys[i];
+ const value = getPropValue(properties[name]);
+
+ return PropRep({
+ ...componentProps,
+ mode: MODE.TINY,
+ name,
+ object: value,
+ equal: ": ",
+ defaultRep: Grip,
+ title: null,
+ suppressQuotes,
+ });
+ });
+ }
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} properties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ function getPropIndexes(properties, max, filter) {
+ const indexes = [];
+
+ try {
+ let i = 0;
+ for (const name in properties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ const value = getPropValue(properties[name]);
+ let type = value.class || typeof value;
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ return indexes;
+ }
+
+ /**
+ * Get the actual value of a property.
+ *
+ * @param {Object} property
+ * @return {Object} Value of the property.
+ */
+ function getPropValue(property) {
+ let value = property;
+ if (typeof property === "object") {
+ const keys = Object.keys(property);
+ if (keys.includes("value")) {
+ value = property.value;
+ } else if (keys.includes("getterValue")) {
+ value = property.getterValue;
+ }
+ }
+ return value;
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ if (object?.class === "DeadObject") {
+ return true;
+ }
+
+ return object?.preview
+ ? typeof object.preview.ownProperties !== "undefined"
+ : typeof object?.ownPropertyLength !== "undefined";
+ }
+
+ const maxLengthMap = new Map();
+ maxLengthMap.set(MODE.SHORT, 3);
+ maxLengthMap.set(MODE.LONG, 10);
+
+ // Grip is used in propIterator and has to be defined here.
+ const Grip = {
+ rep: wrapRender(GripRep),
+ supportsObject,
+ maxLengthMap,
+ };
+
+ // Exports from this module
+ module.exports = Grip;
+});
diff --git a/devtools/client/shared/components/reps/reps/infinity.js b/devtools/client/shared/components/reps/reps/infinity.js
new file mode 100644
index 0000000000..2a36698611
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/infinity.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a Infinity object
+ */
+
+ InfinityRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function InfinityRep(props) {
+ const { object, shouldRenderTooltip } = props;
+
+ const config = getElementConfig(shouldRenderTooltip, object);
+
+ return span(config, object.type);
+ }
+
+ function getElementConfig(shouldRenderTooltip, object) {
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? object.type : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ const type = getGripType(object, noGrip);
+ return type == "Infinity" || type == "-Infinity";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(InfinityRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/moz.build b/devtools/client/shared/components/reps/reps/moz.build
new file mode 100644
index 0000000000..30b7e72a73
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/moz.build
@@ -0,0 +1,45 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "accessible.js",
+ "accessor.js",
+ "array.js",
+ "attribute.js",
+ "big-int.js",
+ "comment-node.js",
+ "constants.js",
+ "custom-formatter.js",
+ "date-time.js",
+ "document-type.js",
+ "document.js",
+ "element-node.js",
+ "error.js",
+ "event.js",
+ "function.js",
+ "grip-array.js",
+ "grip-entry.js",
+ "grip-map.js",
+ "grip.js",
+ "infinity.js",
+ "nan.js",
+ "null.js",
+ "number.js",
+ "object-with-text.js",
+ "object-with-url.js",
+ "object.js",
+ "promise.js",
+ "prop-rep.js",
+ "regexp.js",
+ "rep-utils.js",
+ "rep.js",
+ "string.js",
+ "stylesheet.js",
+ "symbol.js",
+ "text-node.js",
+ "undefined.js",
+ "window.js",
+)
diff --git a/devtools/client/shared/components/reps/reps/nan.js b/devtools/client/shared/components/reps/reps/nan.js
new file mode 100644
index 0000000000..d8b022177c
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/nan.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a NaN object
+ */
+
+ NaNRep.PropTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function NaNRep(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "NaN");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-nan",
+ title: shouldRenderTooltip ? "NaN" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "NaN";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(NaNRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/null.js b/devtools/client/shared/components/reps/reps/null.js
new file mode 100644
index 0000000000..53dca6b8f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/null.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders null value
+ */
+
+ Null.PropTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Null(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "null");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-null",
+ title: shouldRenderTooltip ? "null" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ if (noGrip === true) {
+ return object === null;
+ }
+
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return object == null;
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Null),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/number.js b/devtools/client/shared/components/reps/reps/number.js
new file mode 100644
index 0000000000..455cb971e7
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/number.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a number
+ */
+
+ Number.propTypes = {
+ object: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.number,
+ PropTypes.bool,
+ ]).isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Number(props) {
+ const value = stringify(props.object);
+ const config = getElementConfig(props.shouldRenderTooltip, value);
+
+ return span(config, value);
+ }
+
+ function stringify(object) {
+ const isNegativeZero =
+ Object.is(object, -0) || (object.type && object.type == "-0");
+
+ return isNegativeZero ? "-0" : String(object);
+ }
+
+ function getElementConfig(shouldRenderTooltip, value) {
+ return {
+ className: "objectBox objectBox-number",
+ title: shouldRenderTooltip ? value : null,
+ };
+ }
+
+ const SUPPORTED_TYPES = new Set(["boolean", "number", "-0"]);
+ function supportsObject(object, noGrip = false) {
+ return SUPPORTED_TYPES.has(getGripType(object, noGrip));
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Number),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object-with-text.js b/devtools/client/shared/components/reps/reps/object-with-text.js
new file mode 100644
index 0000000000..5dc3f27ae8
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object-with-text.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const String =
+ require("devtools/client/shared/components/reps/reps/string").rep;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+
+ ObjectWithText.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectWithText(props) {
+ const grip = props.object;
+ const config = getElementConfig(props);
+
+ return span(config, `${getType(grip)} `, getDescription(grip));
+ }
+
+ function getElementConfig(opts) {
+ const shouldRenderTooltip = opts.shouldRenderTooltip;
+ const grip = opts.object;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: `objectTitle objectBox objectBox-${getType(grip)}`,
+ title: shouldRenderTooltip
+ ? `${getType(grip)} "${grip.preview.text}"`
+ : null,
+ };
+ }
+
+ function getType(grip) {
+ return grip.class;
+ }
+
+ function getDescription(grip) {
+ return String({
+ object: grip.preview.text,
+ });
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "ObjectWithText";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectWithText),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object-with-url.js b/devtools/client/shared/components/reps/reps/object-with-url.js
new file mode 100644
index 0000000000..0d0660cacf
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object-with-url.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip object with URL data.
+ */
+
+ ObjectWithURL.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectWithURL(props) {
+ const grip = props.object;
+ const config = getElementConfig(props);
+
+ return span(
+ config,
+ getTitle(grip),
+ span({ className: "objectPropValue" }, getDescription(grip))
+ );
+ }
+
+ function getElementConfig(opts) {
+ const grip = opts.object;
+ const shouldRenderTooltip = opts.shouldRenderTooltip;
+ const tooltip = `${getType(grip)} ${getDescription(grip)}`;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: `objectBox objectBox-${getType(grip)}`,
+ title: shouldRenderTooltip ? tooltip : null,
+ };
+ }
+
+ function getTitle(grip) {
+ return span({ className: "objectTitle" }, `${getType(grip)} `);
+ }
+
+ function getType(grip) {
+ return grip.class;
+ }
+
+ function getDescription(grip) {
+ return getURLDisplayString(grip.preview.url);
+ }
+
+ // Registration
+ function supportsObject(grip) {
+ return grip?.preview?.kind == "ObjectWithURL";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectWithURL),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/object.js b/devtools/client/shared/components/reps/reps/object.js
new file mode 100644
index 0000000000..bdd98cfd04
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/object.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ wrapRender,
+ ellipsisElement,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ const DEFAULT_TITLE = "Object";
+
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+
+ ObjectRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ title: PropTypes.string,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function ObjectRep(props) {
+ const object = props.object;
+ const { shouldRenderTooltip = true } = props;
+
+ if (props.mode === MODE.TINY) {
+ const tinyModeItems = [];
+ if (getTitle(props) !== DEFAULT_TITLE) {
+ tinyModeItems.push(getTitleElement(props));
+ } else {
+ tinyModeItems.push(
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ "{"
+ ),
+ Object.keys(object).length ? ellipsisElement : null,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ "}"
+ )
+ );
+ }
+
+ return span(
+ {
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTitle(props) : null,
+ },
+ ...tinyModeItems
+ );
+ }
+
+ const propsArray = safePropIterator(props, object);
+
+ return span(
+ {
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? getTitle(props) : null,
+ },
+ getTitleElement(props),
+ span(
+ {
+ className: "objectLeftBrace",
+ },
+ " { "
+ ),
+ ...propsArray,
+ span(
+ {
+ className: "objectRightBrace",
+ },
+ " }"
+ )
+ );
+ }
+
+ function getTitleElement(props) {
+ return span({ className: "objectTitle" }, getTitle(props));
+ }
+
+ function getTitle(props) {
+ return props.title || DEFAULT_TITLE;
+ }
+
+ function safePropIterator(props, object, max) {
+ max = typeof max === "undefined" ? 3 : max;
+ try {
+ return propIterator(props, object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ }
+
+ function propIterator(props, object, max) {
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ const elements = [];
+ const unimportantProperties = [];
+ let propertiesNumber = 0;
+ const propertiesNames = Object.keys(object);
+
+ const pushPropRep = (name, value) => {
+ elements.push(
+ PropRep({
+ ...props,
+ key: name,
+ mode: MODE.TINY,
+ name,
+ object: value,
+ equal: ": ",
+ })
+ );
+ propertiesNumber++;
+
+ if (propertiesNumber < propertiesNames.length) {
+ elements.push(", ");
+ }
+ };
+
+ try {
+ for (const name of propertiesNames) {
+ if (propertiesNumber >= max) {
+ break;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ if (isInterestingProp(value)) {
+ pushPropRep(name, value);
+ } else {
+ // If the property is not important, put its name on an array for later
+ // use.
+ unimportantProperties.push(name);
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ if (propertiesNumber < max) {
+ for (const name of unimportantProperties) {
+ if (propertiesNumber >= max) {
+ break;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ pushPropRep(name, value);
+ }
+ }
+
+ if (propertiesNumber < propertiesNames.length) {
+ elements.push(ellipsisElement);
+ }
+
+ return elements;
+ }
+
+ function isInterestingProp(value) {
+ const type = typeof value;
+ return type == "boolean" || type == "number" || (type == "string" && value);
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return noGrip;
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(ObjectRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/promise.js b/devtools/client/shared/components/reps/reps/promise.js
new file mode 100644
index 0000000000..186f4201eb
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/promise.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Dependencies
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders a DOM Promise object.
+ */
+
+ PromiseRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function PromiseRep(props) {
+ const object = props.object;
+
+ // @backward-compat { version 85 } On older servers, the preview of a promise was
+ // useless and didn't include the internal promise state, which was directly exposed
+ // in the grip.
+ if (object.promiseState) {
+ const { state, value, reason } = object.promiseState;
+ const ownProperties = Object.create(null);
+ ownProperties["<state>"] = { value: state };
+ let ownPropertiesLength = 1;
+ if (state == "fulfilled") {
+ ownProperties["<value>"] = { value };
+ ++ownPropertiesLength;
+ } else if (state == "rejected") {
+ ownProperties["<reason>"] = { value: reason };
+ ++ownPropertiesLength;
+ }
+ object.preview = {
+ kind: "Object",
+ ownProperties,
+ ownPropertiesLength,
+ };
+ }
+
+ if (props.mode !== MODE.TINY && props.mode !== MODE.HEADER) {
+ return Grip.rep(props);
+ }
+
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const config = {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip ? "Promise" : null,
+ };
+
+ if (props.mode === MODE.HEADER) {
+ return span(config, getTitle(object));
+ }
+
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ return span(
+ config,
+ getTitle(object),
+ span({ className: "objectLeftBrace" }, " { "),
+ Rep({ object: object.preview.ownProperties["<state>"].value }),
+ span({ className: "objectRightBrace" }, " }")
+ );
+ }
+
+ function getTitle(object) {
+ return span({ className: "objectTitle" }, object.class);
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ if (!Grip.supportsObject(object, noGrip)) {
+ return false;
+ }
+ return getGripType(object, noGrip) == "Promise";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(PromiseRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/prop-rep.js b/devtools/client/shared/components/reps/reps/prop-rep.js
new file mode 100644
index 0000000000..271aea1e21
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/prop-rep.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ appendRTLClassNameIfNeeded,
+ maybeEscapePropertyName,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+ PropRep.propTypes = {
+ // Additional class to set on the key element
+ keyClassName: PropTypes.string,
+ // Property name.
+ name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: PropTypes.string,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ // Normally a PropRep will quote a property name that isn't valid
+ // when unquoted; but this flag can be used to suppress the
+ // quoting.
+ suppressQuotes: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ /**
+ * Function that given a name, a delimiter and an object returns an array
+ * of React elements representing an object property (e.g. `name: value`)
+ *
+ * @param {Object} props
+ * @return {Array} Array of React elements.
+ */
+
+ function PropRep(props) {
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+ const { Rep } = require("devtools/client/shared/components/reps/reps/rep");
+
+ let {
+ equal,
+ keyClassName,
+ mode,
+ name,
+ shouldRenderTooltip,
+ suppressQuotes,
+ } = props;
+
+ const className = `nodeName${keyClassName ? " " + keyClassName : ""}`;
+
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof name === "string") {
+ if (!suppressQuotes) {
+ name = maybeEscapePropertyName(name);
+ }
+ key = span(
+ {
+ className: appendRTLClassNameIfNeeded(className, name),
+ title: shouldRenderTooltip ? name : null,
+ },
+ name
+ );
+ } else {
+ key = Rep({
+ ...props,
+ className,
+ object: name,
+ mode: mode || MODE.TINY,
+ defaultRep: Grip,
+ });
+ }
+
+ return [
+ key,
+ span(
+ {
+ className: "objectEqual",
+ },
+ equal
+ ),
+ Rep({ ...props }),
+ ];
+ }
+
+ // Exports from this module
+ module.exports = wrapRender(PropRep);
+});
diff --git a/devtools/client/shared/components/reps/reps/regexp.js b/devtools/client/shared/components/reps/reps/regexp.js
new file mode 100644
index 0000000000..fc753e0fca
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/regexp.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ wrapRender,
+ ELLIPSIS,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+
+ RegExp.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function RegExp(props) {
+ const { object } = props;
+ const config = getElementConfig(props);
+
+ return span(config, getSource(object));
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip } = opts;
+ const text = getSource(object);
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-regexp regexpSource",
+ title: shouldRenderTooltip ? text : null,
+ };
+ }
+
+ function getSource(grip) {
+ const { displayString } = grip;
+ if (displayString?.type === "longString") {
+ return `${displayString.initial}${ELLIPSIS}`;
+ }
+
+ return displayString;
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "RegExp";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(RegExp),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/rep-utils.js b/devtools/client/shared/components/reps/reps/rep-utils.js
new file mode 100644
index 0000000000..6d148bb06b
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/rep-utils.js
@@ -0,0 +1,596 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const validProtocols = /(http|https|ftp|data|resource|chrome):/i;
+
+ // URL Regex, common idioms:
+ //
+ // Lead-in (URL):
+ // ( Capture because we need to know if there was a lead-in
+ // character so we can include it as part of the text
+ // preceding the match. We lack look-behind matching.
+ // ^| The URL can start at the beginning of the string.
+ // [\s(,;'"`“] Or whitespace or some punctuation that does not imply
+ // a context which would preclude a URL.
+ // )
+ //
+ // We do not need a trailing look-ahead because our regex's will terminate
+ // because they run out of characters they can eat.
+
+ // What we do not attempt to have the regexp do:
+ // - Avoid trailing '.' and ')' characters. We let our greedy match absorb
+ // these, but have a separate regex for extra characters to leave off at the
+ // end.
+ //
+ // The Regex (apart from lead-in/lead-out):
+ // ( Begin capture of the URL
+ // (?: (potential detect beginnings)
+ // https?:\/\/| Start with "http" or "https"
+ // www\d{0,3}[.][a-z0-9.\-]{2,249}|
+ // Start with "www", up to 3 numbers, then "." then
+ // something that looks domain-namey. We differ from the
+ // next case in that we do not constrain the top-level
+ // domain as tightly and do not require a trailing path
+ // indicator of "/". This is IDN root compatible.
+ // [a-z0-9.\-]{2,250}[.][a-z]{2,4}\/
+ // Detect a non-www domain, but requiring a trailing "/"
+ // to indicate a path. This only detects IDN domains
+ // with a non-IDN root. This is reasonable in cases where
+ // there is no explicit http/https start us out, but
+ // unreasonable where there is. Our real fix is the bug
+ // to port the Thunderbird/gecko linkification logic.
+ //
+ // Domain names can be up to 253 characters long, and are
+ // limited to a-zA-Z0-9 and '-'. The roots don't have
+ // hyphens unless they are IDN roots. Root zones can be
+ // found here: http://www.iana.org/domains/root/db
+ // )
+ // [-\w.!~*'();,/?:@&=+$#%]*
+ // path onwards. We allow the set of characters that
+ // encodeURI does not escape plus the result of escaping
+ // (so also '%')
+ // )
+ // eslint-disable-next-line max-len
+ const urlRegex =
+ /(^|[\s(,;'"`“])((?:https?:\/(\/)?|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
+
+ // Set of terminators that are likely to have been part of the context rather
+ // than part of the URL and so should be uneaten. This is '(', ',', ';', plus
+ // quotes and question end-ing punctuation and the potential permutations with
+ // parentheses (english-specific).
+ const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
+
+ const ELLIPSIS = "\u2026";
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { span } = dom;
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ // Map from character code to the corresponding escape sequence. \0
+ // isn't here because it would require special treatment in some
+ // situations. \b, \f, and \v aren't here because they aren't very
+ // common. \' isn't here because there's no need, we only
+ // double-quote strings.
+ const escapeMap = {
+ // Tab.
+ 9: "\\t",
+ // Newline.
+ 0xa: "\\n",
+ // Carriage return.
+ 0xd: "\\r",
+ // Quote.
+ 0x22: '\\"',
+ // Backslash.
+ 0x5c: "\\\\",
+ };
+
+ // All characters we might possibly want to escape, excluding quotes.
+ // Note that we over-match here, because it's difficult to, say, match
+ // an unpaired surrogate with a regexp. The details are worked out by
+ // the replacement function; see |escapeString|.
+ const commonEscapes =
+ // Backslash.
+ "\\\\" +
+ // Controls.
+ "\x00-\x1f" +
+ // More controls.
+ "\x7f-\x9f" +
+ // BOM
+ "\ufeff" +
+ // Specials, except for the replacement character.
+ "\ufff0-\ufffc\ufffe\uffff" +
+ // Surrogates.
+ "\ud800-\udfff" +
+ // Mathematical invisibles.
+ "\u2061-\u2064" +
+ // Line and paragraph separators.
+ "\u2028-\u2029" +
+ // Private use area.
+ "\ue000-\uf8ff";
+ const escapeRegexp = new RegExp(`[${commonEscapes}]`, "g");
+ const escapeRegexpIncludingDoubleQuote = new RegExp(
+ `[${commonEscapes}"]`,
+ "g"
+ );
+
+ /**
+ * Escape a string so that the result is viewable and valid JS.
+ * Control characters, other invisibles, invalid characters, and backslash
+ * are escaped. The resulting string is quoted with either double quotes,
+ * single quotes, or backticks. The preference is for a quote that doesn't
+ * require escaping, falling back to double quotes if that's not possible
+ * (and then escaping them in the string).
+ *
+ * @param {String} str
+ * the input
+ * @param {Boolean} escapeWhitespace
+ * if true, TAB, CR, and NL characters will be escaped
+ * @return {String} the escaped string
+ */
+ function escapeString(str, escapeWhitespace) {
+ let quote = '"';
+ let regexp = escapeRegexp;
+ if (str.includes('"')) {
+ if (!str.includes("'")) {
+ quote = "'";
+ } else if (!str.includes("`") && !str.includes("${")) {
+ quote = "`";
+ } else {
+ regexp = escapeRegexpIncludingDoubleQuote;
+ }
+ }
+ return `${quote}${str.replace(regexp, (match, offset) => {
+ const c = match.charCodeAt(0);
+ if (c in escapeMap) {
+ if (!escapeWhitespace && (c === 9 || c === 0xa || c === 0xd)) {
+ return match[0];
+ }
+ return escapeMap[c];
+ }
+ if (c >= 0xd800 && c <= 0xdfff) {
+ // Find the full code point containing the surrogate, with a
+ // special case for a trailing surrogate at the start of the
+ // string.
+ if (c >= 0xdc00 && offset > 0) {
+ --offset;
+ }
+ const codePoint = str.codePointAt(offset);
+ if (codePoint >= 0xd800 && codePoint <= 0xdfff) {
+ // Unpaired surrogate.
+ return `\\u${codePoint.toString(16)}`;
+ } else if (codePoint >= 0xf0000 && codePoint <= 0x10fffd) {
+ // Private use area. Because we visit each pair of a such a
+ // character, return the empty string for one half and the
+ // real result for the other, to avoid duplication.
+ if (c <= 0xdbff) {
+ return `\\u{${codePoint.toString(16)}}`;
+ }
+ return "";
+ }
+ // Other surrogate characters are passed through.
+ return match;
+ }
+ return `\\u${`0000${c.toString(16)}`.substr(-4)}`;
+ })}${quote}`;
+ }
+
+ /**
+ * Escape a property name, if needed. "Escaping" in this context
+ * means surrounding the property name with quotes.
+ *
+ * @param {String}
+ * name the property name
+ * @return {String} either the input, or the input surrounded by
+ * quotes, properly quoted in JS syntax.
+ */
+ function maybeEscapePropertyName(name) {
+ // Quote the property name if it needs quoting. This particular
+ // test is an approximation; see
+ // https://mathiasbynens.be/notes/javascript-properties. However,
+ // the full solution requires a fair amount of Unicode data, and so
+ // let's defer that until either it's important, or the \p regexp
+ // syntax lands, see
+ // https://github.com/tc39/proposal-regexp-unicode-property-escapes.
+ if (!/^\w+$/.test(name)) {
+ name = escapeString(name);
+ }
+ return name;
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function rawCropString(text, limit, alternativeText = ELLIPSIS) {
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ const halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return (
+ text.substr(0, Math.ceil(halfLimit)) +
+ alternativeText +
+ text.substr(text.length - Math.floor(halfLimit))
+ );
+ }
+
+ return text;
+ }
+
+ function cropString(text, limit, alternativeText) {
+ return rawCropString(sanitizeString(`${text}`), limit, alternativeText);
+ }
+
+ function sanitizeString(text) {
+ // Replace all non-printable characters, except of
+ // (horizontal) tab (HT: \x09) and newline (LF: \x0A, CR: \x0D),
+ // with unicode replacement character (u+fffd).
+ // eslint-disable-next-line no-control-regex
+ const re = new RegExp("[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]", "g");
+ return text.replace(re, "\ufffd");
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ const params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ const searchParams = new URLSearchParams(text);
+ const entries = [...searchParams.entries()];
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1],
+ };
+ });
+ }
+
+ function getFileName(url) {
+ const split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return url && url.substr(0, 5) == "data:";
+ }
+
+ function splitURLTrue(url) {
+ const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ const m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url,
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2],
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5],
+ };
+ }
+
+ /**
+ * Wrap the provided render() method of a rep in a try/catch block that will
+ * render a fallback rep if the render fails.
+ */
+ function wrapRender(renderMethod) {
+ const wrappedFunction = function (props) {
+ try {
+ return renderMethod.call(this, props);
+ } catch (e) {
+ console.error(e);
+ return span(
+ {
+ className: "objectBox objectBox-failure",
+ title:
+ "This object could not be rendered, " +
+ "please file a bug on bugzilla.mozilla.org",
+ },
+ /* Labels have to be hardcoded for reps, see Bug 1317038. */
+ "Invalid object"
+ );
+ }
+ };
+ wrappedFunction.propTypes = renderMethod.propTypes;
+ return wrappedFunction;
+ }
+
+ /**
+ * Get preview items from a Grip.
+ *
+ * @param {Object} Grip from which we want the preview items
+ * @return {Array} Array of the preview items of the grip, or an empty array
+ * if the grip does not have preview items
+ */
+ function getGripPreviewItems(grip) {
+ if (!grip) {
+ return [];
+ }
+
+ // Array Grip
+ if (grip.preview && grip.preview.items) {
+ return grip.preview.items;
+ }
+
+ // Node Grip
+ if (grip.preview && grip.preview.childNodes) {
+ return grip.preview.childNodes;
+ }
+
+ // Set or Map Grip
+ if (grip.preview && grip.preview.entries) {
+ return grip.preview.entries.reduce((res, entry) => res.concat(entry), []);
+ }
+
+ // Event Grip
+ if (grip.preview && grip.preview.target) {
+ const keys = Object.keys(grip.preview.properties);
+ const values = Object.values(grip.preview.properties);
+ return [grip.preview.target, ...keys, ...values];
+ }
+
+ // RegEx Grip
+ if (grip.displayString) {
+ return [grip.displayString];
+ }
+
+ // Generic Grip
+ if (grip.preview && grip.preview.ownProperties) {
+ let propertiesValues = Object.values(grip.preview.ownProperties).map(
+ property => property.value || property
+ );
+
+ const propertyKeys = Object.keys(grip.preview.ownProperties);
+ propertiesValues = propertiesValues.concat(propertyKeys);
+
+ // ArrayBuffer Grip
+ if (grip.preview.safeGetterValues) {
+ propertiesValues = propertiesValues.concat(
+ Object.values(grip.preview.safeGetterValues).map(
+ property => property.getterValue || property
+ )
+ );
+ }
+
+ return propertiesValues;
+ }
+
+ return [];
+ }
+
+ /**
+ * Get the type of an object.
+ *
+ * @param {Object} Grip from which we want the type.
+ * @param {boolean} noGrip true if the object is not a grip.
+ * @return {boolean}
+ */
+ function getGripType(object, noGrip) {
+ if (noGrip || Object(object) !== object) {
+ return typeof object;
+ }
+ if (object.type === "object") {
+ return object.class;
+ }
+ return object.type;
+ }
+
+ /**
+ * Determines whether a grip is a string containing a URL.
+ *
+ * @param string grip
+ * The grip, which may contain a URL.
+ * @return boolean
+ * Whether the grip is a string containing a URL.
+ */
+ function containsURL(grip) {
+ // An URL can't be shorter than 5 char (e.g. "ftp:").
+ if (typeof grip !== "string" || grip.length < 5) {
+ return false;
+ }
+
+ return validProtocols.test(grip);
+ }
+
+ /**
+ * Determines whether a string token is a valid URL.
+ *
+ * @param string token
+ * The token.
+ * @return boolean
+ * Whether the token is a URL.
+ */
+ function isURL(token) {
+ try {
+ if (!validProtocols.test(token)) {
+ return false;
+ }
+ new URL(token);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns new array in which `char` are interleaved between the original items.
+ *
+ * @param {Array} items
+ * @param {String} char
+ * @returns Array
+ */
+ function interleave(items, char) {
+ return items.reduce((res, item, index) => {
+ if (index !== items.length - 1) {
+ return res.concat(item, char);
+ }
+ return res.concat(item);
+ }, []);
+ }
+
+ const ellipsisElement = span(
+ {
+ key: "more",
+ className: "more-ellipsis",
+ title: `more${ELLIPSIS}`,
+ },
+ ELLIPSIS
+ );
+
+ /**
+ * Removes any unallowed CSS properties from a string of CSS declarations
+ *
+ * @param {String} userProvidedStyle CSS declarations
+ * @param {Function} createElement Method to create a dummy element the styles get applied to
+ * @returns {Object} Filtered CSS properties as JavaScript object in camelCase notation
+ */
+ function cleanupStyle(userProvidedStyle, createElement) {
+ // Regular expression that matches the allowed CSS property names.
+ const allowedStylesRegex = new RegExp(
+ "^(?:-moz-)?(?:align|background|border|box|clear|color|cursor|display|" +
+ "float|font|justify|line|margin|padding|position|text|transition" +
+ "|outline|vertical-align|white-space|word|writing|" +
+ "(?:min-|max-)?width|(?:min-|max-)?height)"
+ );
+
+ const mozElementRegex = /\b((?:-moz-)?element)[\s('"]+/gi;
+
+ // Regex to retrieve usages of `url(*)` in property value
+ const cssUrlRegex = /url\([\'\"]?([^\)]*)/g;
+
+ // Use a dummy element to parse the style string.
+ const dummy = createElement("div");
+ dummy.style = userProvidedStyle;
+
+ // Return a style object as expected by React DOM components, e.g.
+ // {color: "red"}
+ // without forbidden properties and values.
+ return Array.from(dummy.style)
+ .filter(name => {
+ if (!allowedStylesRegex.test(name)) {
+ return false;
+ }
+
+ if (mozElementRegex.test(name)) {
+ return false;
+ }
+
+ if (name === "position") {
+ return ["static", "relative"].includes(
+ dummy.style.getPropertyValue(name)
+ );
+ }
+ // There can be multiple call to `url()` (e.g.` background: url("path/to/image"), url("data:image/png,…");`);
+ // filter out the property if the url function is called with anything that is not
+ // a data URL.
+ return Array.from(dummy.style[name].matchAll(cssUrlRegex))
+ .map(match => match[1])
+ .every(potentialUrl => potentialUrl.startsWith("data:"));
+ })
+ .reduce((object, name) => {
+ // React requires CSS properties to be provided in JavaScript form, i.e. camelCased.
+ const jsName = name.replace(/-([a-z])/g, (_, char) =>
+ char.toUpperCase()
+ );
+ return Object.assign(
+ {
+ [jsName]: dummy.style.getPropertyValue(name),
+ },
+ object
+ );
+ }, {});
+ }
+
+ /**
+ * Append has-rtl-char to className if passed string has RTL chars.
+ * has-rtl-char is used in reps.css to set `unicode-bidi: isolate` on the element.
+ * It's important to only apply it when needed as this CSS property can have an
+ * important impact on performance (See Bug 1879806)
+ *
+ * @param {String} className: The className want to set on an element
+ * @param {String} strToCheck: The string for which we want to check if it has RTL chars
+ * @returns {String}
+ */
+ function appendRTLClassNameIfNeeded(className = "", strToCheck) {
+ if (
+ // The JSONViewer, which uses some Reps component, doesn't have access to Services.
+ typeof Services == "undefined" ||
+ !Services?.intl?.stringHasRTLChars(strToCheck)
+ ) {
+ return className;
+ }
+ return `${className} has-rtl-char`;
+ }
+
+ module.exports = {
+ interleave,
+ isURL,
+ cropString,
+ containsURL,
+ rawCropString,
+ appendRTLClassNameIfNeeded,
+ sanitizeString,
+ escapeString,
+ wrapRender,
+ cropMultipleLines,
+ parseURLParams,
+ parseURLEncodedText,
+ getFileName,
+ getURLDisplayString,
+ maybeEscapePropertyName,
+ getGripPreviewItems,
+ getGripType,
+ ellipsisElement,
+ ELLIPSIS,
+ uneatLastUrlCharsRegex,
+ urlRegex,
+ cleanupStyle,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/rep.js b/devtools/client/shared/components/reps/reps/rep.js
new file mode 100644
index 0000000000..10d719c42c
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/rep.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Load all existing rep templates
+ const Undefined = require("devtools/client/shared/components/reps/reps/undefined");
+ const Null = require("devtools/client/shared/components/reps/reps/null");
+ const StringRep = require("devtools/client/shared/components/reps/reps/string");
+ const Number = require("devtools/client/shared/components/reps/reps/number");
+ const ArrayRep = require("devtools/client/shared/components/reps/reps/array");
+ const Obj = require("devtools/client/shared/components/reps/reps/object");
+ const SymbolRep = require("devtools/client/shared/components/reps/reps/symbol");
+ const InfinityRep = require("devtools/client/shared/components/reps/reps/infinity");
+ const NaNRep = require("devtools/client/shared/components/reps/reps/nan");
+ const Accessor = require("devtools/client/shared/components/reps/reps/accessor");
+
+ // DOM types (grips)
+ const Accessible = require("devtools/client/shared/components/reps/reps/accessible");
+ const Attribute = require("devtools/client/shared/components/reps/reps/attribute");
+ const BigInt = require("devtools/client/shared/components/reps/reps/big-int");
+ const DateTime = require("devtools/client/shared/components/reps/reps/date-time");
+ const Document = require("devtools/client/shared/components/reps/reps/document");
+ const DocumentType = require("devtools/client/shared/components/reps/reps/document-type");
+ const Event = require("devtools/client/shared/components/reps/reps/event");
+ const Func = require("devtools/client/shared/components/reps/reps/function");
+ const PromiseRep = require("devtools/client/shared/components/reps/reps/promise");
+ const RegExp = require("devtools/client/shared/components/reps/reps/regexp");
+ const StyleSheet = require("devtools/client/shared/components/reps/reps/stylesheet");
+ const CommentNode = require("devtools/client/shared/components/reps/reps/comment-node");
+ const ElementNode = require("devtools/client/shared/components/reps/reps/element-node");
+ const TextNode = require("devtools/client/shared/components/reps/reps/text-node");
+ const ErrorRep = require("devtools/client/shared/components/reps/reps/error");
+ const Window = require("devtools/client/shared/components/reps/reps/window");
+ const ObjectWithText = require("devtools/client/shared/components/reps/reps/object-with-text");
+ const ObjectWithURL = require("devtools/client/shared/components/reps/reps/object-with-url");
+ const GripArray = require("devtools/client/shared/components/reps/reps/grip-array");
+ const GripEntry = require("devtools/client/shared/components/reps/reps/grip-entry");
+ const GripMap = require("devtools/client/shared/components/reps/reps/grip-map");
+ const Grip = require("devtools/client/shared/components/reps/reps/grip");
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+ const reps = [
+ RegExp,
+ StyleSheet,
+ Event,
+ DateTime,
+ CommentNode,
+ Accessible,
+ ElementNode,
+ TextNode,
+ Attribute,
+ Func,
+ PromiseRep,
+ Document,
+ DocumentType,
+ Window,
+ ObjectWithText,
+ ObjectWithURL,
+ ErrorRep,
+ GripArray,
+ GripMap,
+ GripEntry,
+ Grip,
+ Undefined,
+ Null,
+ StringRep,
+ Number,
+ BigInt,
+ SymbolRep,
+ InfinityRep,
+ NaNRep,
+ Accessor,
+ ];
+
+ // Reps for rendering of native object reference (e.g. used from the JSONViewer, Netmonitor, …)
+ const noGripReps = [StringRep, Number, ArrayRep, Undefined, Null, Obj];
+
+ /**
+ * Generic rep that is used for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed in as the 'object'
+ * property.
+ */
+ const Rep = function (props) {
+ const { object, defaultRep } = props;
+ const rep = getRep(
+ object,
+ defaultRep,
+ props.noGrip,
+ props.mayUseCustomFormatter
+ );
+ return rep(props);
+ };
+
+ const exportedReps = {
+ Accessible,
+ Accessor,
+ ArrayRep,
+ Attribute,
+ BigInt,
+ CommentNode,
+ DateTime,
+ Document,
+ DocumentType,
+ ElementNode,
+ ErrorRep,
+ Event,
+ Func,
+ Grip,
+ GripArray,
+ GripMap,
+ GripEntry,
+ InfinityRep,
+ NaNRep,
+ Null,
+ Number,
+ Obj,
+ ObjectWithText,
+ ObjectWithURL,
+ PromiseRep,
+ RegExp,
+ Rep,
+ StringRep,
+ StyleSheet,
+ SymbolRep,
+ TextNode,
+ Undefined,
+ Window,
+ };
+
+ // Custom Formatters
+ // Services.prefs isn't available in jsonviewer. It doesn't matter as we don't want to use
+ // custom formatters there
+ if (typeof Services == "object" && Services?.prefs) {
+ const useCustomFormatters = Services.prefs.getBoolPref(
+ "devtools.custom-formatters.enabled",
+ false
+ );
+
+ if (useCustomFormatters) {
+ const CustomFormatter = require("devtools/client/shared/components/reps/reps/custom-formatter");
+ reps.unshift(CustomFormatter);
+ exportedReps.CustomFormatter = CustomFormatter;
+ }
+ }
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultRep {React.Component} The default template
+ * that should be used to render given object if none is found.
+ *
+ * @param noGrip {Boolean} If true, will only check reps not made for remote
+ * objects.
+ *
+ * @param mayUseCustomFormatter {Boolean} If true, custom formatters are
+ * allowed to be used as rep.
+ */
+ function getRep(
+ object,
+ defaultRep = Grip,
+ noGrip = false,
+ mayUseCustomFormatter = false
+ ) {
+ const repsList = noGrip ? noGripReps : reps;
+ for (const rep of repsList) {
+ if (rep === exportedReps.CustomFormatter && !mayUseCustomFormatter) {
+ continue;
+ }
+
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, noGrip)) {
+ return rep.rep;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return defaultRep.rep;
+ }
+
+ module.exports = {
+ Rep,
+ REPS: exportedReps,
+ // Exporting for tests
+ getRep,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/string.js b/devtools/client/shared/components/reps/reps/string.js
new file mode 100644
index 0000000000..3149f4a51b
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/string.js
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const {
+ a,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ appendRTLClassNameIfNeeded,
+ containsURL,
+ escapeString,
+ getGripType,
+ rawCropString,
+ sanitizeString,
+ wrapRender,
+ ELLIPSIS,
+ uneatLastUrlCharsRegex,
+ urlRegex,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+
+ StringRep.propTypes = {
+ useQuotes: PropTypes.bool,
+ escapeWhitespace: PropTypes.bool,
+ style: PropTypes.object,
+ cropLimit: PropTypes.number.isRequired,
+ urlCropLimit: PropTypes.number,
+ member: PropTypes.object,
+ object: PropTypes.object.isRequired,
+ openLink: PropTypes.func,
+ className: PropTypes.string,
+ title: PropTypes.string,
+ isInContentPage: PropTypes.bool,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function StringRep(props) {
+ const {
+ className,
+ style,
+ cropLimit,
+ urlCropLimit,
+ object,
+ useQuotes = true,
+ escapeWhitespace = true,
+ member,
+ openLink,
+ title,
+ isInContentPage,
+ transformEmptyString = false,
+ shouldRenderTooltip,
+ } = props;
+
+ let text = object;
+ const config = getElementConfig({
+ className,
+ style,
+ actor: object.actor,
+ title,
+ });
+
+ if (text == "" && transformEmptyString && !useQuotes) {
+ return span(
+ {
+ ...config,
+ title: "<empty string>",
+ className: `${config.className} objectBox-empty-string`,
+ },
+ "<empty string>"
+ );
+ }
+
+ const isLong = isLongString(object);
+ const isOpen = member && member.open;
+ const shouldCrop = !isOpen && cropLimit && text.length > cropLimit;
+
+ if (isLong) {
+ text = maybeCropLongString(
+ {
+ shouldCrop,
+ cropLimit,
+ },
+ text
+ );
+
+ const { fullText } = object;
+ if (isOpen && fullText) {
+ text = fullText;
+ }
+ }
+
+ text = formatText(
+ {
+ useQuotes,
+ escapeWhitespace,
+ },
+ text
+ );
+
+ if (shouldRenderTooltip) {
+ config.title = text;
+ }
+
+ if (!isLong) {
+ if (containsURL(text)) {
+ return span(
+ config,
+ getLinkifiedElements({
+ text,
+ cropLimit: shouldCrop ? cropLimit : null,
+ urlCropLimit,
+ openLink,
+ isInContentPage,
+ })
+ );
+ }
+
+ // Cropping of longString has been handled before formatting.
+ text = maybeCropString(
+ {
+ isLong,
+ shouldCrop,
+ cropLimit,
+ },
+ text
+ );
+ }
+
+ config.className = appendRTLClassNameIfNeeded(config.className, text);
+
+ return span(config, text);
+ }
+
+ function maybeCropLongString(opts, object) {
+ const { shouldCrop, cropLimit } = opts;
+
+ const grip = object && object.getGrip ? object.getGrip() : object;
+ const { initial, length } = grip;
+
+ let text = shouldCrop ? initial.substring(0, cropLimit) : initial;
+
+ if (text.length < length) {
+ text += ELLIPSIS;
+ }
+
+ return text;
+ }
+
+ function formatText(opts, text) {
+ const { useQuotes, escapeWhitespace } = opts;
+
+ return useQuotes
+ ? escapeString(text, escapeWhitespace)
+ : sanitizeString(text);
+ }
+
+ function getElementConfig(opts) {
+ const { className, style, actor, title } = opts;
+
+ const config = {};
+
+ if (actor) {
+ config["data-link-actor-id"] = actor;
+ }
+
+ if (title) {
+ config.title = title;
+ }
+
+ const classNames = ["objectBox", "objectBox-string"];
+ if (className) {
+ classNames.push(className);
+ }
+ config.className = classNames.join(" ");
+
+ if (style) {
+ config.style = style;
+ }
+
+ return config;
+ }
+
+ function maybeCropString(opts, text) {
+ const { shouldCrop, cropLimit } = opts;
+
+ return shouldCrop ? rawCropString(text, cropLimit) : text;
+ }
+
+ /**
+ * Get an array of the elements representing the string, cropped if needed,
+ * with actual links.
+ *
+ * @param {Object} An options object of the following shape:
+ * - text {String}: The actual string to linkify.
+ * - cropLimit {Integer}: The limit to apply on the whole text.
+ * - urlCropLimit {Integer}: The limit to apply on each URL.
+ * - openLink {Function} openLink: Function handling the link
+ * opening.
+ * - isInContentPage {Boolean}: pass true if the reps is
+ * rendered in the content page
+ * (e.g. in JSONViewer).
+ * @returns {Array<String|ReactElement>}
+ */
+ function getLinkifiedElements({
+ text,
+ cropLimit,
+ urlCropLimit,
+ openLink,
+ isInContentPage,
+ }) {
+ const halfLimit = Math.ceil((cropLimit - ELLIPSIS.length) / 2);
+ const startCropIndex = cropLimit ? halfLimit : null;
+ const endCropIndex = cropLimit ? text.length - halfLimit : null;
+
+ const items = [];
+ let currentIndex = 0;
+ let contentStart;
+ while (true) {
+ const url = urlRegex.exec(text);
+ // Pick the regexp with the earlier content; index will always be zero.
+ if (!url) {
+ break;
+ }
+ contentStart = url.index + url[1].length;
+ if (contentStart > 0) {
+ const nonUrlText = text.substring(0, contentStart);
+ items.push(
+ getCroppedString(
+ nonUrlText,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ )
+ );
+ }
+
+ // There are some final characters for a URL that are much more likely
+ // to have been part of the enclosing text rather than the end of the
+ // URL.
+ let useUrl = url[2];
+ const uneat = uneatLastUrlCharsRegex.exec(useUrl);
+ if (uneat) {
+ useUrl = useUrl.substring(0, uneat.index);
+ }
+
+ currentIndex = currentIndex + contentStart;
+ const linkText = getCroppedString(
+ useUrl,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ );
+
+ if (linkText) {
+ const linkItems = [];
+ const shouldCrop = urlCropLimit && useUrl.length > urlCropLimit;
+ if (shouldCrop) {
+ const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2);
+ // We cut the string into 3 elements and we'll visually hide the second one
+ // in CSS. This way people can still copy the full link.
+ linkItems.push(
+ span(
+ { className: "cropped-url-start" },
+ useUrl.substring(0, urlCropHalf)
+ ),
+ span(
+ { className: "cropped-url-middle" },
+ useUrl.substring(urlCropHalf, useUrl.length - urlCropHalf)
+ ),
+ span(
+ { className: "cropped-url-end" },
+ useUrl.substring(useUrl.length - urlCropHalf)
+ )
+ );
+ } else {
+ linkItems.push(linkText);
+ }
+
+ items.push(
+ a(
+ {
+ key: `${useUrl}-${currentIndex}`,
+ className: "url" + (shouldCrop ? " cropped-url" : ""),
+ title: useUrl,
+ draggable: false,
+ // Because we don't want the link to be open in the current
+ // panel's frame, we only render the href attribute if `openLink`
+ // exists (so we can preventDefault) or if the reps will be
+ // displayed in content page (e.g. in the JSONViewer).
+ href: openLink || isInContentPage ? useUrl : null,
+ target: "_blank",
+ rel: "noopener noreferrer",
+ onClick: openLink
+ ? e => {
+ e.preventDefault();
+ openLink(useUrl, e);
+ }
+ : null,
+ },
+ linkItems
+ )
+ );
+ }
+
+ currentIndex = currentIndex + useUrl.length;
+ text = text.substring(url.index + url[1].length + useUrl.length);
+ }
+
+ // Clean up any non-URL text at the end of the source string,
+ // i.e. not handled in the loop.
+ if (text.length) {
+ if (currentIndex < endCropIndex) {
+ text = getCroppedString(
+ text,
+ currentIndex,
+ startCropIndex,
+ endCropIndex
+ );
+ }
+ items.push(text);
+ }
+
+ return items;
+ }
+
+ /**
+ * Returns a cropped substring given an offset, start and end crop indices in a
+ * parent string.
+ *
+ * @param {String} text: The substring to crop.
+ * @param {Integer} offset: The offset corresponding to the index at which
+ * the substring is in the parent string.
+ * @param {Integer|null} startCropIndex: the index where the start of the crop
+ * should happen in the parent string.
+ * @param {Integer|null} endCropIndex: the index where the end of the crop
+ * should happen in the parent string
+ * @returns {String|null} The cropped substring, or null if the text is
+ * completly cropped.
+ */
+ function getCroppedString(text, offset = 0, startCropIndex, endCropIndex) {
+ if (!startCropIndex) {
+ return text;
+ }
+
+ const start = offset;
+ const end = offset + text.length;
+
+ const shouldBeVisible = !(start >= startCropIndex && end <= endCropIndex);
+ if (!shouldBeVisible) {
+ return null;
+ }
+
+ const shouldCropEnd = start < startCropIndex && end > startCropIndex;
+ const shouldCropStart = start < endCropIndex && end > endCropIndex;
+ if (shouldCropEnd) {
+ const cutIndex = startCropIndex - start;
+ return (
+ text.substring(0, cutIndex) +
+ ELLIPSIS +
+ (shouldCropStart ? text.substring(endCropIndex - start) : "")
+ );
+ }
+
+ if (shouldCropStart) {
+ // The string should be cropped at the beginning.
+ const cutIndex = endCropIndex - start;
+ return text.substring(cutIndex);
+ }
+
+ return text;
+ }
+
+ function isLongString(object) {
+ const grip = object && object.getGrip ? object.getGrip() : object;
+ return grip && grip.type === "longString";
+ }
+
+ function supportsObject(object, noGrip = false) {
+ // Accept the object if the grip-type (or type for noGrip objects) is "string"
+ if (getGripType(object, noGrip) == "string") {
+ return true;
+ }
+
+ // Also accept longString objects if we're expecting grip
+ if (!noGrip) {
+ return isLongString(object);
+ }
+
+ return false;
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(StringRep),
+ supportsObject,
+ isLongString,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/stylesheet.js b/devtools/client/shared/components/reps/reps/stylesheet.js
new file mode 100644
index 0000000000..591442ebbf
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/stylesheet.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+
+ StyleSheet.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function StyleSheet(props) {
+ const grip = props.object;
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+ const location = getLocation(grip);
+ const config = getElementConfig({ grip, shouldRenderTooltip, location });
+
+ return span(
+ config,
+ getTitle(grip),
+ span({ className: "objectPropValue" }, location)
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { grip, shouldRenderTooltip, location } = opts;
+
+ return {
+ "data-link-actor-id": grip.actor,
+ className: "objectBox objectBox-object",
+ title: shouldRenderTooltip
+ ? `${getGripType(grip, false)} ${location}`
+ : null,
+ };
+ }
+
+ function getTitle(grip) {
+ return span(
+ { className: "objectBoxTitle" },
+ `${getGripType(grip, false)} `
+ );
+ }
+
+ function getLocation(grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ const url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "CSSStyleSheet";
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(StyleSheet),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/symbol.js b/devtools/client/shared/components/reps/reps/symbol.js
new file mode 100644
index 0000000000..89c76cdbb2
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/symbol.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ rep: StringRep,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ const MAX_STRING_LENGTH = 50;
+
+ /**
+ * Renders a symbol.
+ */
+
+ SymbolRep.propTypes = {
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function SymbolRep(props) {
+ const {
+ className = "objectBox objectBox-symbol",
+ object,
+ shouldRenderTooltip,
+ } = props;
+ const { name } = object;
+
+ let symbolText = name || "";
+ if (name && name !== "Symbol.iterator" && name !== "Symbol.asyncIterator") {
+ symbolText = StringRep({
+ object: symbolText,
+ shouldCrop: true,
+ cropLimit: MAX_STRING_LENGTH,
+ useQuotes: true,
+ });
+ }
+
+ const config = getElementConfig(
+ {
+ shouldRenderTooltip,
+ className,
+ name,
+ },
+ object
+ );
+
+ return span(config, "Symbol(", symbolText, ")");
+ }
+
+ function getElementConfig(opts, object) {
+ const { shouldRenderTooltip, className, name } = opts;
+
+ return {
+ "data-link-actor-id": object.actor,
+ className,
+ title: shouldRenderTooltip ? `Symbol(${name})` : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ return getGripType(object, noGrip) == "symbol";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(SymbolRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/text-node.js b/devtools/client/shared/components/reps/reps/text-node.js
new file mode 100644
index 0000000000..ae9a7bb109
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/text-node.js
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const {
+ button,
+ span,
+ } = require("devtools/client/shared/vendor/react-dom-factories");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ // Reps
+ const {
+ appendRTLClassNameIfNeeded,
+ cropString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const {
+ rep: StringRep,
+ isLongString,
+ } = require("devtools/client/shared/components/reps/reps/string");
+
+ /**
+ * Renders DOM #text node.
+ */
+
+ TextNode.propTypes = {
+ object: PropTypes.object.isRequired,
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ onDOMNodeMouseOver: PropTypes.func,
+ onDOMNodeMouseOut: PropTypes.func,
+ onInspectIconClick: PropTypes.func,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function TextNode(props) {
+ const { object: grip, mode = MODE.SHORT } = props;
+
+ const isInTree = grip.preview && grip.preview.isConnected === true;
+ const config = getElementConfig({ ...props, isInTree });
+ const inspectIcon = getInspectIcon({ ...props, isInTree });
+
+ if (mode === MODE.TINY || mode === MODE.HEADER) {
+ return span(config, getTitle(grip), inspectIcon);
+ }
+
+ return span(
+ config,
+ getTitle(grip),
+ " ",
+ StringRep({
+ className: "nodeValue",
+ object: grip.preview.textContent,
+ }),
+ inspectIcon ? inspectIcon : null
+ );
+ }
+
+ function getElementConfig(opts) {
+ const {
+ object,
+ isInTree,
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ shouldRenderTooltip,
+ } = opts;
+
+ const text = getTextContent(object);
+ const config = {
+ "data-link-actor-id": object.actor,
+ "data-link-content-dom-reference": JSON.stringify(
+ object.contentDomReference
+ ),
+ className: appendRTLClassNameIfNeeded(
+ "objectBox objectBox-textNode",
+ text
+ ),
+ title: shouldRenderTooltip ? `#text "${text}"` : null,
+ };
+
+ if (isInTree) {
+ if (onDOMNodeMouseOver) {
+ Object.assign(config, {
+ onMouseOver: _ => onDOMNodeMouseOver(object),
+ });
+ }
+
+ if (onDOMNodeMouseOut) {
+ Object.assign(config, {
+ onMouseOut: _ => onDOMNodeMouseOut(object),
+ });
+ }
+ }
+
+ return config;
+ }
+
+ function getTextContent(grip) {
+ const text = grip.preview.textContent;
+ return cropString(isLongString(text) ? text.initial : text);
+ }
+
+ function getInspectIcon(opts) {
+ const { object, isInTree, onInspectIconClick } = opts;
+
+ if (!isInTree || !onInspectIconClick) {
+ return null;
+ }
+
+ return button({
+ className: "open-inspector",
+ draggable: false,
+ // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+ title: "Click to select the node in the inspector",
+ onClick: e => onInspectIconClick(object, e),
+ });
+ }
+
+ function getTitle(grip) {
+ const title = "#text";
+ return span({}, title);
+ }
+
+ // Registration
+ function supportsObject(grip, noGrip = false) {
+ return grip?.preview && grip?.class == "Text";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(TextNode),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/undefined.js b/devtools/client/shared/components/reps/reps/undefined.js
new file mode 100644
index 0000000000..7e0ac4c786
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/undefined.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ getGripType,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ /**
+ * Renders undefined value
+ */
+
+ Undefined.propTypes = {
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function Undefined(props) {
+ const shouldRenderTooltip = props.shouldRenderTooltip;
+
+ const config = getElementConfig(shouldRenderTooltip);
+
+ return span(config, "undefined");
+ }
+
+ function getElementConfig(shouldRenderTooltip) {
+ return {
+ className: "objectBox objectBox-undefined",
+ title: shouldRenderTooltip ? "undefined" : null,
+ };
+ }
+
+ function supportsObject(object, noGrip = false) {
+ if (noGrip === true) {
+ return object === undefined;
+ }
+
+ return (
+ (object && object.type && object.type == "undefined") ||
+ getGripType(object, noGrip) == "undefined"
+ );
+ }
+
+ // Exports from this module
+
+ module.exports = {
+ rep: wrapRender(Undefined),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/reps/window.js b/devtools/client/shared/components/reps/reps/window.js
new file mode 100644
index 0000000000..2c420c2b30
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps/window.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const { span } = require("devtools/client/shared/vendor/react-dom-factories");
+
+ // Reps
+ const {
+ getGripType,
+ getURLDisplayString,
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+
+ /**
+ * Renders a grip representing a window.
+ */
+
+ WindowRep.propTypes = {
+ mode: PropTypes.oneOf(Object.values(MODE)),
+ object: PropTypes.object.isRequired,
+ shouldRenderTooltip: PropTypes.bool,
+ };
+
+ function WindowRep(props) {
+ const { mode, object } = props;
+
+ if (mode === MODE.TINY) {
+ const tinyTitle = getTitle(object);
+ const title = getTitle(object, true);
+ const location = getLocation(object);
+ const config = getElementConfig({ ...props, title, location });
+
+ return span(
+ config,
+ span({ className: tinyTitle.className }, tinyTitle.content)
+ );
+ }
+
+ const title = getTitle(object, true);
+ const location = getLocation(object);
+ const config = getElementConfig({ ...props, title, location });
+
+ return span(
+ config,
+ span({ className: title.className }, title.content),
+ span({ className: "location" }, location)
+ );
+ }
+
+ function getElementConfig(opts) {
+ const { object, shouldRenderTooltip, title, location } = opts;
+ let tooltip;
+
+ if (location) {
+ tooltip = `${title.content}${location}`;
+ } else {
+ tooltip = `${title.content}`;
+ }
+
+ return {
+ "data-link-actor-id": object.actor,
+ className: "objectBox objectBox-Window",
+ title: shouldRenderTooltip ? tooltip : null,
+ };
+ }
+
+ function getTitle(object, trailingSpace) {
+ let title = object.displayClass || object.class || "Window";
+ if (trailingSpace === true) {
+ title = `${title} `;
+ }
+ return {
+ className: "objectTitle",
+ content: title,
+ };
+ }
+
+ function getLocation(object) {
+ return getURLDisplayString(object.preview.url);
+ }
+
+ // Registration
+ function supportsObject(object, noGrip = false) {
+ return object?.preview && getGripType(object, noGrip) == "Window";
+ }
+
+ // Exports from this module
+ module.exports = {
+ rep: wrapRender(WindowRep),
+ supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/dom-node-constants.js b/devtools/client/shared/components/reps/shared/dom-node-constants.js
new file mode 100644
index 0000000000..ecc1861d65
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/dom-node-constants.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ module.exports = {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12,
+
+ // DocumentPosition
+ DOCUMENT_POSITION_DISCONNECTED: 0x01,
+ DOCUMENT_POSITION_PRECEDING: 0x02,
+ DOCUMENT_POSITION_FOLLOWING: 0x04,
+ DOCUMENT_POSITION_CONTAINS: 0x08,
+ DOCUMENT_POSITION_CONTAINED_BY: 0x10,
+ DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20,
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/grip-length-bubble.js b/devtools/client/shared/components/reps/shared/grip-length-bubble.js
new file mode 100644
index 0000000000..8d0dcb8bb8
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/grip-length-bubble.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+ const {
+ wrapRender,
+ } = require("devtools/client/shared/components/reps/reps/rep-utils");
+ const {
+ MODE,
+ } = require("devtools/client/shared/components/reps/reps/constants");
+ const {
+ ModePropType,
+ } = require("devtools/client/shared/components/reps/reps/array");
+
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { span } = dom;
+
+ GripLengthBubble.propTypes = {
+ object: PropTypes.object.isRequired,
+ maxLengthMap: PropTypes.instanceOf(Map).isRequired,
+ getLength: PropTypes.func.isRequired,
+ mode: ModePropType,
+ visibilityThreshold: PropTypes.number,
+ };
+
+ function GripLengthBubble(props) {
+ const {
+ object,
+ mode = MODE.SHORT,
+ visibilityThreshold = 2,
+ maxLengthMap,
+ getLength,
+ showZeroLength = false,
+ } = props;
+
+ const length = getLength(object);
+ const isEmpty = length === 0;
+ const isObvious =
+ [MODE.SHORT, MODE.LONG].includes(mode) &&
+ length > 0 &&
+ length <= maxLengthMap.get(mode) &&
+ length <= visibilityThreshold;
+ if ((isEmpty && !showZeroLength) || isObvious) {
+ return "";
+ }
+
+ return span(
+ {
+ className: "objectLengthBubble",
+ },
+ `(${length})`
+ );
+ }
+
+ module.exports = {
+ lengthBubble: wrapRender(GripLengthBubble),
+ };
+});
diff --git a/devtools/client/shared/components/reps/shared/moz.build b/devtools/client/shared/components/reps/shared/moz.build
new file mode 100644
index 0000000000..6704491b97
--- /dev/null
+++ b/devtools/client/shared/components/reps/shared/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "dom-node-constants.js",
+ "grip-length-bubble.js",
+)