UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

186 lines (185 loc) 7.32 kB
import { escapeHtml, numDelim } from "../../core/utils/html.js"; import { hasOwn, objectToString } from "../../core/utils/object-utils.js"; import { isArray } from "../../core/utils/is-type.js"; import { matchAll } from "../../core/utils/pattern.js"; const urlRx = /^(?:https?:)?\/\/(?:[a-z0-9\-]+(?:\.[a-z0-9\-]+)+|\d+(?:\.\d+){3})(?:\:\d+)?(?:\/\S*?)?$/i; function token(type, str) { return `<span class="${type}">${str}</span>`; } function more(num) { return token("more", `\u2026 ${numDelim(num)} more`); } function stringifySafe(str) { return escapeHtml(stringifyIfNeeded(str)); } export function stringifyIfNeeded(value) { return /[^\x20\x21\x23-\x5B\x5D-\uD799]/.test(value) ? JSON.stringify(value).slice(1, -1) : value; } export default function value2html(value, compact, options) { switch (typeof value) { case "boolean": case "undefined": return token("keyword", String(value)); case "number": case "bigint": return token("number", numDelim(value)); case "symbol": return token("symbol", String(value)); case "function": return "\u0192n"; case "string": { const valueLength = value.length; const maxLength = compact ? options.maxCompactStringLength : options.maxStringLength; const shortString = valueLength > maxLength + options.allowedExcessStringLength; let stringContent = ""; let stringPrefix = ""; let stringRest = ""; if (shortString) { if (options.match) { const matches = []; const gap = maxLength > 30 ? 10 : 5; const maxMatchLength = maxLength - gap; let offset = 0; let firstMatchOffset = -1; matchAll( value, options.match, (chunk) => { offset += chunk.length; }, (chunk, stop) => { if (firstMatchOffset === -1) { firstMatchOffset = offset + maxMatchLength; } if (offset < firstMatchOffset) { matches.push({ start: offset, end: offset + chunk.length }); } offset += chunk.length; if (offset > firstMatchOffset) { return stop; } } ); if (matches.length > 0) { const start = matches[0].start; let budget = maxLength; if (start !== 0) { const prefix = stringifyIfNeeded(value.slice(0, start)); if (start > gap) { const moreLength = 2; const prefixLength = gap - moreLength; stringPrefix = token("more prefix", ""); stringContent = prefix.slice(-prefixLength); budget -= gap; } else { stringContent = prefix; } } for (let i = 0; i < matches.length && budget > 0; i++) { const { start: start2, end } = matches[i]; const matchLength = Math.min(end - start2, budget); stringContent += token("match", stringifySafe(value.slice(start2, start2 + matchLength))); budget -= matchLength; if (budget > 0) { const isLast = i + 1 >= matches.length; const nextEnd = isLast ? value.length : matches[i + 1].start; const nextTextLength = Math.min(nextEnd - end, budget); if (isLast) { const rest = value.length - (end + nextTextLength); if (rest > 0) { stringRest = token("more suffix", numDelim(rest)); } } if (nextTextLength > 0) { stringContent += stringifySafe(value.slice(end, end + nextTextLength)); budget -= nextTextLength; } } } } } if (stringContent === "") { stringContent = stringifySafe(value.slice(0, maxLength)); stringRest = token("more suffix", numDelim(valueLength - maxLength)); } } else { if (options.match) { matchAll( value, options.match, (text) => { stringContent += stringifySafe(text); }, (text) => { stringContent += token("match", stringifySafe(text)); } ); } else { stringContent = stringifySafe(value); } } return token( "string", !compact && (value[0] === "h" || value[0] === "/") && urlRx.test(value) ? `"${stringPrefix}<a href="${escapeHtml(value)}" target="_blank">${stringContent}</a>${stringRest}"` : `"${stringPrefix}${stringContent}${stringRest}"` ); } case "object": { if (value === null) { return token("keyword", "null"); } if (isArray(value)) { const valueLength = value.length; const limitCollapsed = options.limitCollapsed === false || options.limitCollapsed > valueLength ? valueLength : options.limitCollapsed; const content2 = Array.from({ length: limitCollapsed }, (_, index) => value2html(value[index], true, options)); if (valueLength > limitCollapsed) { content2.push(`${more(valueLength - limitCollapsed)} `); } return `[${content2.join(", ")}]`; } switch (objectToString(value)) { case "[object Set]": { const valueSize = value.size; const limitCollapsed = options.limitCollapsed === false || options.limitCollapsed > valueSize ? valueSize : options.limitCollapsed; const iterator = value.values(); const content2 = Array.from({ length: limitCollapsed }, () => value2html(iterator.next().value, true, options)); if (valueSize > limitCollapsed) { content2.push(`${more(valueSize - limitCollapsed)} `); } return `[${content2.join(", ")}]`; } case "[object Date]": return token("date", String(value)); case "[object RegExp]": return token("regexp", String(value)); } if (compact && options.limitCompactObjectEntries === 0) { for (const key in value) { if (hasOwn(value, key)) { return "{\u2026}"; } } return "{}"; } const limitObjectEntries = compact ? options.limitCompactObjectEntries === false ? Infinity : options.limitCompactObjectEntries : options.limitCollapsed === false ? Infinity : options.limitCollapsed; const content = []; let count = 0; for (const key in value) { if (hasOwn(value, key)) { if (count < limitObjectEntries) { const property = escapeHtml( key.length > options.maxCompactPropertyLength ? key.slice(0, options.maxCompactPropertyLength) + "\u2026" : key ); content.push(`${token("property", property)}: ${value2html(value[key], true, options)}`); } count++; } } if (count > limitObjectEntries) { content.push(more(count - limitObjectEntries)); } return content.length ? `{ ${content.join(", ")} }` : "{}"; } default: return `unknown type "${typeof value}"`; } }