MAL - Unretard Related Entries

Reverts retarded changes from 2024-05-24 to related entries back to the sane list.

// ==UserScript==
// @name         MAL - Unretard Related Entries
// @description  Reverts retarded changes from 2024-05-24 to related entries back to the sane list.
// @version      1.0.3
// @author       tariel36
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @namespace    https://github.com/tariel36/MAL-Unretard-Related-Entries
// @match        https://myanimelist.net/*
// @license      MIT
// ==/UserScript==

const first = () => true;

function unfold() {
  Array.from(document.getElementsByClassName("hide-entry")).forEach((el) => {
    el.removeAttribute("style");
    el.classList.remove("hide-entry");
  });
}

function getGroupedEntries() {
  return Array.from(document.getElementsByClassName("entry"))
    .map((entry) => ({
      entry,
      key: entry
        .querySelector(".content")
        ?.querySelector(".relation")
        ?.innerText?.trim()
        ?.split("(")
        .find(first)
        ?.trim(),
    }))
    .filter((x) => !!x.key)
    .reduce((prev, curr) => {
      if (!prev[curr.key]) {
        prev[curr.key] = [];
      }
      prev[curr.key].push(curr.entry);
      return prev;
    }, {});
}

function getTableParent() {
  const foo = document.querySelector(".related-entries");

  return foo;
}

function getOrCreateEntriesTable(relatedEntriesContainer) {
  return (
    [...relatedEntriesContainer.getElementsByClassName("entries-table")]
      .find(first)
      ?.querySelector("tbody") ??
    (() => {
      const table = document.createElement("table");
      table.className = "entries-table";

      const tbody = document.createElement("tbody");

      // Append the tbody to the table
      table.appendChild(tbody);
      relatedEntriesContainer.appendChild(table);

      return tbody;
    })()
  );
}

function extractSectionsHeaders() {
  return [
    ...([...document.getElementsByClassName("entries-table")]
      .find(first)
      ?.querySelector("tbody")
      ?.querySelectorAll("td") ?? []),
  ].filter((x) => !!x && x.classList.contains("ar"));
}

function addEntriesToTable(table, retardedEntries, existingSections) {
  Object.keys(retardedEntries).forEach((y) => {
    const entriesCollection = retardedEntries[y];
    const key = y;

    const section =
      existingSections.find((z) =>
        (z.innerText ?? "").toUpperCase().includes(key.toUpperCase())
      ) ??
      (() => {
        const row = document.createElement("tr");

        const cell1 = document.createElement("td");
        cell1.textContent = ` ${key}: `;
        cell1.setAttribute("valign", "top");
        cell1.className = "ar fw-n borderClass nowrap";

        const cell2 = document.createElement("td");
        cell2.setAttribute("width", "100%");
        cell2.className = "borderClass";

        const ul = document.createElement("ul");
        ul.className = "entries";
        cell2.appendChild(ul);

        row.appendChild(cell1);
        row.appendChild(cell2);
        table.appendChild(row);

        return cell1;
      })();

    entriesCollection.forEach((z) => {
      const relationTypeRaw = (
        (z.querySelector(".relation").innerText.split("(") ?? []).at(-1) ?? ""
      ).trim();
      const actualRelationType = relationTypeRaw ? ` (${relationTypeRaw}` : "";

      const list = section.nextElementSibling.querySelector("ul");

      const li = document.createElement("li");
      const a = z.querySelector(".title").querySelector("a");
      a.innerText = (a.innerText ?? "").trim();

      li.appendChild(a);
      li.appendChild(document.createTextNode(` ${actualRelationType}`));

      list.appendChild(li);
    });
  });
}

function removeRetardedNodes(autoUnfold) {
  document.querySelector(".entries-tile")?.remove();

  if (autoUnfold) {
    document.querySelector(".related-entries > div:first-of-type")?.remove();
  }
}

function sortEntries(orderingArray) {
  const result = [
    ...([...document.getElementsByClassName("entries-table")]
      .find(first)
      ?.querySelector("tbody")
      ?.querySelectorAll("td") ?? []),
  ]
    .filter((x) => !!x && x.classList.contains("ar"))
    .map((x) => ({
      child: x,
      parent: x.parentNode,
      key: x.innerText.trim().slice(0, -1),
    }))
    .sort(
      (a, b) => orderingArray.indexOf(a.key) - orderingArray.indexOf(b.key)
    );

  return result;
}

function removeOldEntries(table) {
  while (table.firstChild) {
    table.removeChild(table.firstChild);
  }
}

function addNewEntries(table, sortedArray) {
  sortedArray.forEach((x) => {
    table.appendChild(x.parent);
  });
}

function setSetting(key, value) {
  GM_setValue(key, value);
}

function registerOptions() {
  GM_registerMenuCommand("Set ordering list", () => {
    const color = prompt("Enter ordering list, separated by commas:");

    if (color) {
      setSetting("orderingArray", color);
    }
  });

  GM_registerMenuCommand("Set auto-unfold", () => {
    const text = prompt(
      "Provide value (true = auto-unfold, any other value is false):"
    );

    if (text) {
      setSetting("autoUnfold", text);
    }
  });
}

function getOptions() {
  const defaultOrdering =
    "Adaptation, Prequel, Sequel, Summary, Parent Story, Side Story, Alternative Version, Alternative Setting, Spin-Off, Other, Character";
  const orderingArray = (
    GM_getValue("orderingArray", defaultOrdering) ?? defaultOrdering
  )
    .split(",")
    .map((x) => (x ?? "").trim())
    .filter((x) => x.length > 0);

  const autoUnfold =
    (GM_getValue("autoUnfold", "true") ?? "true").toLowerCase() === "true";

  return { orderingArray, autoUnfold };
}

function unretard() {
  // Variables etc.
  registerOptions();
  const options = getOptions();

  // Unfold
  if (options.autoUnfold) {
    unfold();
  }

  // Get aggregated entries from retarded area
  const retardedEntries = getGroupedEntries();

  // Get table's parent
  const relatedEntriesContainer = getTableParent();

  // On FireFox, for some reason the script is run multiple times
  // and every time after the first one, container is not found,
  // so we just escape early.
  if (!relatedEntriesContainer) {
    return;
  }

  // Get table
  const table = getOrCreateEntriesTable(relatedEntriesContainer);

  // Get all sections headers
  const existingSections = extractSectionsHeaders();

  // Add entries
  addEntriesToTable(table, retardedEntries, existingSections);

  // Remove useless nodes
  removeRetardedNodes(options.autoUnfold);

  // Sort related entries in sane order
  const sortedArray = sortEntries(options.orderingArray);

  removeOldEntries(table);

  addNewEntries(table, sortedArray);
}

unretard();