B漫接口文档类型自动获取

吧啦吧啦

// ==UserScript==
// @name         B漫接口文档类型自动获取
// @namespace    mimiko/bilibili-manga-api-doc
// @version      0.0.12
// @description  吧啦吧啦
// @author       Mimiko
// @license      MIT
// @match        *://comic.bilibili.co/api-doc/*
// @grant        GM.addStyle
// @run-at      document-start
// ==/UserScript==
// https://greasyfork.org/zh-CN/scripts/470030-b%E6%BC%AB%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E7%B1%BB%E5%9E%8B%E8%87%AA%E5%8A%A8%E8%8E%B7%E5%8F%96
"use strict";
(() => {
  if (window.top !== window.self) return;
  // variables
  const regexOptional = new RegExp(
    ["不传", "不填", "可选", "选传", "选填", "非必传", "非必填"].join("|"),
  );
  // functions
  /** 将字符串转为小驼峰 */ const camelCase = (input) =>
    input
      .replace(/[^a-zA-Z0-9]/g, " ")
      .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
        index === 0 ? word.toLowerCase() : word.toUpperCase(),
      )
      .replace(/\s+/g, "");
  /** 复制到剪贴板 */ const copy = (content) => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(content);
      return;
    }
    const textarea = document.createElement("textarea");
    textarea.value = content;
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand("copy");
    document.body.removeChild(textarea);
  };
  /** 格式化;核心方法 */ const format = (input) =>
    input // [{ ... }] -> { ... }[]
      .replace(/: \[{/g, ": {")
      .replace(/}],/g, "}[],") // {} -> Record<string, never>
      .replace(/{}/g, "Record<string, never>") // bool -> boolean
      .replace(/false, \/\/ type<bool>/g, "boolean //") // i32 -> number
      .replace(/0, \/\/ type<int32>/g, "number //") // i64 -> `${bigint}`
      .replace(/"0", \/\/ type<int64>/g, "`${bigint}` //") // double -> number
      .replace(/0.0, \/\/ type<double>/g, "number //") // float -> number
      .replace(/0.0, \/\/ type<float>/g, "number //") // string -> string
      .replace(/"", \/\/ type<string>/g, "string //") // i32[] -> number[]
      .replace(/\[0], \/\/ list<int32>/g, "number[] //") // i64[] -> `${bigint}`[]
      .replace(/\["0"], \/\/ list<int64>/g, "`${bigint}`[] //") // string[] -> string[]
      .replace(/\[""], \/\/ list<string>/g, "string[] //") // UNKNOWN -> unknown
      .replace(/UNKNOWN, \/\/ type<>/g, "unknown //") // {"0": false} -> Record<string, boolean>
      .replace(
        /: {(.*?)\n(\s*)"0": false\n\s*}/g,
        ": $1\n$2Record<string, boolean>",
      ) // {"": ""} -> Record<string, string>
      .replace(/: {(.*?)\n(\s*)"": ""\n\s*}/g, ": $1\n$2Record<string, string>") // {"0": {}} -> Record<string, {}>
      .replace(/: {(.*?)\n(\s*)"0": {/g, ": $1\n$2Record<string, {") // {"0": 0} -> Record<string, number>
      .replace(/: {(.*?)\n(\s*)"0": 0\n\s*}/g, ": $1\n$2Record<string, number>")
      .replace(/}\n\s*},/g, "}>") // remove useless comma
      .replace(/},\n/g, "}\n")
      .replace(/>,\n/g, ">\n") // remove useless comment
      .replace(/\/\/ list<.*/g, "")
      .replace(/\/\/ map<.*/g, "")
      .replace(/}, \/\/ type<.*/g, "},") // remove empty inline comment
      .replace(/\s*\/\/\s*$/gm, "") // '// xxx' -> /** xxx */
      .replace(/\/\/\s*(.*?)\n/g, "/** $1 */\n") // */\n/** -> *
      .replace(/\*\/\n(\s*)\/\*\*/g, "\n$1 *") // replace 4 spaces with 2 spaces
      .replace(/ {4}/g, "  ") // optional parameters
      .replace(/\/\*\*.+?\*\/\n.+?:/g, (text) => {
        if (regexOptional.test(text)) text = text.replace(/:$/, "?:");
        return text;
      });
  /** 生成代码 */ const getContent = (btn) => {
    const data = pick(btn);
    if (!data) return;
    const name = upperFirst(camelCase(data.name.split("/").pop() ?? ""));
    if (!name) return;
    const { description, url } = data;
    const request = format(data.request);
    const response = format(data.response);
    const head = `
/**
 * ${data.name}
 * @description ${description}
 * @see ${url}
 */`;
    return `
${head}
export type Request${name} = ${request}
${head}
export type Response${name} = ${response}
    `;
  };
  /** 注入按钮 */ const inject = (callback) => {
    // h1
    document.querySelectorAll("h1").forEach((h1) => {
      const btn = document.createElement("button");
      btn.addEventListener("click", onClickAll);
      btn.classList.add("btn", "btn-primary");
      btn.innerText = "复制全部类型";
      btn.style.marginLeft = "10px";
      h1.appendChild(btn);
    });
    // h2
    document.querySelectorAll("h2").forEach((h2, i) => {
      h2.dataset.name = h2.textContent?.trim() ?? "";
      const btn = document.createElement("button");
      btn.addEventListener("click", () => onClick(btn));
      btn.classList.add("btn", "btn-primary");
      btn.innerText = "复制类型";
      btn.style.marginLeft = "10px";
      btn.dataset.index = i.toString();
      h2.appendChild(btn);
    });
    // .sourceCode
    document.querySelectorAll(".sourceCode").forEach((div) => {
      div.dataset.code = div.textContent?.trim() ?? "";
    });
    // callback
    callback();
  };
  /** 主函数 */ const main = () => {
    GM.addStyle(".sourceCode { pointer-events: none; }");
    window.addEventListener("load", () =>
      inject(() => GM.addStyle(".sourceCode { pointer-events: auto; }")),
    );
  };
  /** “复制类型” */ const onClick = (btn) => {
    const content = getContent(btn);
    if (!content) {
      alert("获取类型失败");
      return;
    }
    copy(content);
  };
  /** “复制全部类型” */ const onClickAll = () => {
    const content = [...document.querySelectorAll("h2 button")]
      .map(getContent)
      .join("");
    copy(content);
  };
  /** 获取页面文本 */ const pick = (btn) => {
    const h2 = btn.parentElement;
    if (!h2) return;
    const { name } = h2.dataset;
    if (!name) return;
    const url = [
      window.location.origin,
      window.location.pathname,
      "#",
      name.toLowerCase().replace(/\./g, "").replace(/\//g, ""),
    ].join("");
    const p = h2.nextElementSibling;
    if (!p) return;
    const description = p.textContent?.trim();
    if (!description) return;
    const { index } = btn.dataset;
    if (!index) return;
    const i = parseInt(index);
    const divReq = document.getElementById(`cb${1 + i * 2}`);
    if (!divReq) return;
    const request = divReq.dataset.code;
    if (!request) return;
    const divRes = document.getElementById(`cb${2 + i * 2}`);
    if (!divRes) return;
    const response = divRes.dataset.code;
    if (!response) return;
    return {
      description,
      name,
      request,
      response,
      url,
    };
  };
  /** 首字母大写 */ const upperFirst = (input) =>
    input.charAt(0).toUpperCase() + input.slice(1);
  // execute
  main();
})();