394 lines
10 KiB
JavaScript
394 lines
10 KiB
JavaScript
/* 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/>. */
|
|
|
|
/* eslint no-shadow: ["error", { "allow": ["length"] }] */
|
|
|
|
import {
|
|
a,
|
|
span,
|
|
} from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
|
|
import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
|
|
|
|
import {
|
|
appendRTLClassNameIfNeeded,
|
|
containsURL,
|
|
escapeString,
|
|
getGripType,
|
|
rawCropString,
|
|
sanitizeString,
|
|
wrapRender,
|
|
ELLIPSIS,
|
|
uneatLastUrlCharsRegex,
|
|
urlRegex,
|
|
} from "resource://devtools/client/shared/components/reps/reps/rep-utils.mjs";
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
const rep = wrapRender(StringRep);
|
|
|
|
// Exports from this module
|
|
|
|
export { rep, supportsObject, isLongString };
|