| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 | // @ts-check/** * @typedef {string | number | null | undefined | bigint | boolean | symbol} Primitive * @typedef {(...args: any[]) => any} AnyFunction */class PrettyJSONError extends Error {  /**   *   * @param {string} message   */  constructor(message) {    super(message);    this.name = "PrettyJSONError";  }}class PrettyJSON extends HTMLElement {  /**   * @type {any}   */  #input;  /**   * @type {boolean}   */  #isExpanded;  static get observedAttributes() {    return ["expand", "key", "truncate-string"];  }  static styles = `/* css */    :host {      --key-color: #cc0000;      --arrow-color: #737373;      --brace-color: #0030f0;      --bracket-color: #0030f0;      --string-color: #009900;      --number-color: #0000ff;      --null-color: #666666;      --boolean-color: #d23c91;      --comma-color: #666666;      --ellipsis-color: #666666;      --indent: 2rem;    }    @media (prefers-color-scheme: dark) {      :host {        --key-color: #f73d3d;        --arrow-color: #6c6c6c;        --brace-color: #0690bc;        --bracket-color: #0690bc;        --string-color: #21c521;        --number-color: #0078b3;        --null-color: #8c8888;        --boolean-color: #c737b3;        --comma-color: #848181;        --ellipsis-color: #c2c2c2;      }    }    button {      border: none;      background: transparent;      cursor: pointer;      font-family: inherit;      font-size: 1rem;      vertical-align: text-bottom;    }    .container {      font-family: monospace;      font-size: 1rem;    }    .key {      color: var(--key-color);      margin-right: 0.5rem;      padding: 0;    }    .key .arrow {      width: 1rem;      height: 0.75rem;      margin-left: -1.25rem;      padding-right: 0.25rem;      vertical-align: baseline;    }    .arrow .triangle {      fill: var(--arrow-color);    }    .comma {      color: var(--comma-color);    }    .brace {      color: var(--brace-color);    }    .string,    .url {      color: var(--string-color);    }    .number,    .bigint {      color: var(--number-color);    }    .null {      color: var(--null-color);    }    .boolean {      color: var(--boolean-color);    }    .ellipsis {      width: 1rem;      padding: 0;      color: var(--ellipsis-color);    }    .ellipsis::after {      content: "…";    }    .string .ellipsis::after {      color: var(--string-color);    }    .triangle {      fill: black;      stroke: black;      stroke-width: 0;    }    .row {      padding-left: var(--indent);    }    .row .row {      display: block;    }    .row > div,    .row > span {      display: inline-block;    }  `;  constructor() {    super();    this.#isExpanded = true;    this.attachShadow({ mode: "open" });  }  get #expandAttributeValue() {    const expandAttribute = this.getAttribute("expand");    if (expandAttribute === null) {      return 1;    }    const expandValue = Number.parseInt(expandAttribute);    return isNaN(expandValue) || expandValue < 0 ? 0 : expandValue;  }  get #truncateStringAttributeValue() {    const DEFAULT_TRUNCATE_STRING = 500;    const truncateStringAttribute = this.getAttribute("truncate-string");    if (truncateStringAttribute === null) {      return DEFAULT_TRUNCATE_STRING;    }    const truncateStringValue = Number.parseInt(truncateStringAttribute);    return isNaN(truncateStringValue) || truncateStringValue < 0      ? 0      : truncateStringValue;  }  #toggle() {    this.#isExpanded = !this.#isExpanded;    this.setAttribute(      "expand",      this.#isExpanded ? String(this.#expandAttributeValue + 1) : "0"    );    this.#render();  }  /**   * @param {Record<any, any> | any[] | Primitive | AnyFunction} input   * @param {number} expand   * @param {string} [key]   * @returns {HTMLElement}   */  #createChild(input, expand, key) {    if (this.#isPrimitiveValue(input)) {      const container = this.#createContainer();      container.appendChild(this.#createPrimitiveValueElement(input));      return container;    }    return this.#createObjectOrArray(input);  }  /**   * @param {any} input   * @returns {input is Primitive}   */  #isPrimitiveValue(input) {    return typeof input !== "object" || input === null;  }  #isValidStringURL() {    try {      new URL(this.#input);      return true;    } catch (error) {      return false;    }  }  /**   * @param {Primitive} input   * @returns {HTMLElement}   */  #createPrimitiveValueElement(input) {    const container = document.createElement("div");    const type = typeof input === "object" ? "null" : typeof input;    container.className = `primitive value ${type}`;    if (typeof input === "string") {      if (this.#isValidStringURL()) {        const anchor = document.createElement("a");        anchor.className = "url";        anchor.href = this.#input;        anchor.target = "_blank";        anchor.textContent = input;        container.append('"', anchor, '"');      } else if (input.length > this.#truncateStringAttributeValue) {        container.appendChild(this.#createTruncatedStringElement(input));      } else {        container.textContent = JSON.stringify(input);      }    } else {      container.textContent = JSON.stringify(input);    }    return container;  }  /**   * @param {string} input   */  #createTruncatedStringElement(input) {    const container = document.createElement("div");    container.dataset.expandedTimes = "1";    container.className = "truncated string";    const ellipsis = document.createElement("button");    ellipsis.className = "ellipsis";    ellipsis.addEventListener("click", () => {      const expandedTimes = Number.parseInt(        container.dataset.expandedTimes ?? "1"      );      container.dataset.expandedTimes = String(expandedTimes + 1);      const expandedString = input.slice(        0,        (expandedTimes + 1) * this.#truncateStringAttributeValue      );      const textChild = container.childNodes[1];      container.replaceChild(        document.createTextNode(expandedString),        textChild      );    });    container.append(      '"',      input.slice(0, this.#truncateStringAttributeValue),      ellipsis,      '"'    );    return container;  }  /**   * @returns {HTMLElement}   */  #createContainer() {    const container = document.createElement("div");    container.className = "container";    return container;  }  /**   * @param {Record<any, any> | any[]} object   * @returns {HTMLElement}   */  #createObjectOrArray(object) {    const isArray = Array.isArray(object);    const objectKeyName = this.getAttribute("key");    const expand = this.#expandAttributeValue;    const container = this.#createContainer();    container.classList.add(isArray ? "array" : "object");    if (objectKeyName) {      // if objectKeyName is provided, then it is a row      container.classList.add("row");      const keyElement = this.#createKeyElement(objectKeyName, {        withArrow: true,        expanded: this.#isExpanded,      });      keyElement.addEventListener("click", this.#toggle.bind(this));      container.appendChild(keyElement);    }    const openingBrace = document.createElement("span");    openingBrace.className = "open brace";    openingBrace.textContent = isArray ? "[" : "{";    container.appendChild(openingBrace);    const closingBrace = document.createElement("span");    closingBrace.className = "close brace";    closingBrace.textContent = isArray ? "]" : "}";    if (!this.#isExpanded) {      const ellipsis = document.createElement("button");      ellipsis.className = "ellipsis";      container.appendChild(ellipsis);      ellipsis.addEventListener("click", this.#toggle.bind(this));      container.appendChild(closingBrace);      return container;    }    Object.entries(object).forEach(([key, value], index) => {      // for primitives we make a row here      if (this.#isPrimitiveValue(value)) {        const rowContainer = document.createElement("div");        rowContainer.className = "row";        if (!isArray) {          const keyElement = this.#createKeyElement(key);          rowContainer.appendChild(keyElement);        }        rowContainer.appendChild(this.#createPrimitiveValueElement(value));        container.appendChild(rowContainer);        const isLast = index === Object.keys(object).length - 1;        if (!isLast) {          const comma = document.createElement("span");          comma.className = "comma";          comma.textContent = ",";          rowContainer.appendChild(comma);        }        return;      }      // for objects and arrays we make a "container row"      const prettyJsonElement = document.createElement("pretty-json");      prettyJsonElement.textContent = JSON.stringify(value);      prettyJsonElement.setAttribute("expand", String(expand - 1));      prettyJsonElement.setAttribute("key", key);      container.appendChild(prettyJsonElement);    });    container.appendChild(closingBrace);    return container;  }  /**   * @param {{ expanded?: boolean }} [options]   * @returns {SVGElement}   */  #createArrowElement({ expanded = false } = {}) {    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");    svg.setAttribute("width", "100");    svg.setAttribute("height", "100");    svg.setAttribute("viewBox", "0 0 100 100");    svg.setAttribute("class", "arrow");    const polygon = document.createElementNS(      "http://www.w3.org/2000/svg",      "polygon"    );    polygon.setAttribute("class", "triangle");    polygon.setAttribute("points", "0,0 100,50 0,100");    if (expanded) {      polygon.setAttribute("transform", "rotate(90 50 50)");    }    svg.appendChild(polygon);    return svg;  }  /**   * @param {string} key   * @param {{ withArrow?: boolean, expanded?: boolean }} [options]   * @returns {HTMLElement}   */  #createKeyElement(key, { withArrow = false, expanded = false } = {}) {    const keyElement = document.createElement(withArrow ? "button" : "span");    keyElement.className = "key";    if (withArrow) {      const arrow = this.#createArrowElement({ expanded });      keyElement.appendChild(arrow);    }    const keyName = document.createElement("span");    keyName.className = "key-name";    keyName.textContent = JSON.stringify(key);    keyElement.appendChild(keyName);    const colon = document.createElement("span");    colon.className = "colon";    colon.textContent = ":";    keyElement.appendChild(colon);    return keyElement;  }  #render() {    if (!this.shadowRoot) {      throw new PrettyJSONError("Shadow root not available");    }    this.shadowRoot.innerHTML = "";    this.shadowRoot.appendChild(      this.#createChild(this.#input, this.#expandAttributeValue)    );    if (this.shadowRoot.querySelector("[data-pretty-json]")) {      return;    }    const styles = document.createElement("style");    styles.setAttribute("data-pretty-json", "");    styles.textContent = PrettyJSON.styles;    this.shadowRoot.appendChild(styles);  }  /**   * Handle when attributes change   * @param {string} name   * @param {string} _oldValue   * @param {string | null} newValue   */  attributeChangedCallback(name, _oldValue, newValue) {    if (name === "expand") {      if (newValue === null) {        this.#isExpanded = false;      } else {        const expandValue = Number.parseInt(newValue);        this.#isExpanded = !isNaN(expandValue) && expandValue > 0;      }      this.#render();    }  }  connectedCallback() {    try {      this.#input = JSON.parse(this.textContent ?? "");    } catch (jsonParseError) {      const message = `Error parsing JSON: ${jsonParseError instanceof Error ? jsonParseError.message : "Unknown error"}`;      throw new PrettyJSONError(message);    }    this.#render();  }}// Define pretty-json custom elementcustomElements.define("pretty-json", PrettyJSON);
 |