知乎重排for印象笔记

重新排版知乎的网页,使例如"印象笔记·剪藏"的web clippers只保存需要的内容。

스크립트 설치?
개발자의 추천 스크립트

Stack Exchange Formatter는 어떤가요?

스크립트 설치
// ==UserScript==
// @name         知乎重排for印象笔记
// @name:en      Zhihu Formatter
// @namespace    http://tampermonkey.net/
// @version      1.3.3
// @description  重新排版知乎的网页,使例如"印象笔记·剪藏"的web clippers只保存需要的内容。
// @description:en  Format a webpage on zhihu.com so that web clippers such as Evernote Web Clipper can save only useful information.
// @author       twchen
// @match        https://www.zhihu.com/question/*/answer/*
// @match        https://zhuanlan.zhihu.com/p/*
// @match        https://www.zhihu.com/pin/*
// @run-at       document-idle
// @inject-into  auto
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @grant        GM.getValue
// @grant        GM_getValue
// @grant        GM.setValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @connect      lens.zhihu.com
// @connect      api.zhihu.com
// @connect      www.zhihu.com
// @connect      unpkg.zhimg.com
// @supportURL   https://github.com/twchen/zhihu-formatter/issues
// ==/UserScript==

// GM 4 API polyfill
if (typeof GM_addStyle == "undefined") {
  this.GM_addStyle = css => {
    const style = document.createElement("style");
    style.textContent = css;
    document.documentElement.appendChild(style);
    return style;
  };
}

if (typeof GM == "undefined") {
  this.GM = {};
  [
    ["getValue", GM_getValue],
    ["setValue", GM_setValue],
  ].forEach(([newFunc, oldFunc]) => {
    GM[newFunc] = (...args) => {
      return new Promise((resolve, reject) => {
        try {
          resolve(oldFunc(...args));
        } catch (error) {
          reject(error);
        }
      });
    };
  });
  GM.xmlHttpRequest = GM_xmlhttpRequest;
}

GM.asyncHttpRequest = args => {
  return new Promise((resolve, reject) => {
    GM.xmlHttpRequest({
      ...args,
      onload: resolve,
      onerror: response => {
        reject({
          message: `Status:${response.status}. StatusText: ${response.statusText}`,
        });
      },
    });
  });
};

function createElement(type, props, ...children) {
  const element = document.createElement(type);
  Object.entries(props || {}).forEach(([name, value]) => {
    if (name.startsWith("on")) {
      const eventName = name.slice(2);
      element.addEventListener(eventName, value);
    } else if (name == "style" && typeof value !== "string") {
      Object.assign(element.style, value);
    } else {
      element.setAttribute(name, value);
    }
  });
  children
    .map(child =>
      typeof child === "string" ? document.createTextNode(child) : child
    )
    .forEach(child => element.appendChild(child));
  return element;
}

// for debugging
// unsafeWindow.GM = GM;

(async function () {
  "use strict";

  GM_addStyle(`
  .fmt-comments-container {
    box-sizing: border-box;
    border: 1px solid rgb(235, 235, 235);
    border-radius: 4px;
    align-items: stretch;
    margin-top: 1em;
    padding: 1.2em;
    display: flex;
    flex-direction: column;
  }
  .fmt-comments-count {
    font-size: 15px;
    font-weight: 600;
    padding-bottom: 1em;
    border-bottom: 1px solid rgb(235, 235, 235);
  }
  .fmt-root-comment-container {
    display: flex;
    flex-direction: column;
    padding-bottom: 1em;
    border-top: 1px solid rgb(235, 235, 235);
  }
  .fmt-comment {
    padding-top: 1em;
    display: flex;
    flex-direction: row;
  }
  .fmt-content-column {
    display: flex;
    flex-direction: column;
    margin-left: 0.5em;
  }
  .fmt-author-row {
    font-weight: 600;
  }
  .fmt-comment-content {
    margin: 0.5em 0;
    color: rgb(68, 68, 68);
  }
  .fmt-comment-info {
    color: rgb(153, 153, 153);
  }
  .fmt-emoticon {
    width: 1.4em;
    height: 1.4em;
    margin-bottom: -0.3em;
  }
  .fmt-comment-img {
    display: block;
    max-width: 10em;
    max-height: 20em;
  }
`);

  function addLinkToNav(onclick) {
    const nav = document.querySelector(".AppHeader-Tabs");
    const li = nav.querySelector("li").cloneNode(true);
    const a = li.querySelector("a");
    a.href = "#";
    a.text = "重排";
    a.onclick = event => {
      event.preventDefault();
      onclick();
    };
    nav.appendChild(li);
  }

  function addBtnToNav() {
    const pageHeader = document.querySelector("div.ColumnPageHeader-Button");
    const button = createElement("button", {
      type: "button",
      class: "Button Button--blue",
      style: {
        "margin-right": "1rem",
      },
      onclick: formatZhuanlan,
    });
    button.innerText = "重新排版";
    pageHeader.prepend(button);
  }

  function formatAnswer() {
    root.style.display = "none";
    let div = document.querySelector("#formatted");
    if (div !== null) {
      div.remove();
    }

    const showMoreBtn = document.querySelector(
      "button.Button.QuestionRichText-more"
    );
    if (showMoreBtn !== null) showMoreBtn.click();
    const title = document
      .querySelector(".QuestionHeader-title")
      .cloneNode(true);
    const question = createElement(
      "div",
      {
        style: {
          backgroundColor: "white",
          margin: "0.8rem 0",
          padding: "0.2rem 1rem 1rem",
          borderRadius: "2px",
          boxShadow: "0 1px 3px rgba(26,26,26,.1)",
        },
      },
      title
    );
    const detail = document.querySelector(
      ".QuestionHeader-main .QuestionRichText"
    );
    if (detail) question.appendChild(detail.cloneNode(true));
    const answer = document
      .querySelector("div.Card.AnswerCard")
      .cloneNode(true);
    // remove non-working actions
    const actions = answer.querySelector(".ContentItem-actions");
    actions.style.display = "none";

    div = createElement(
      "div",
      {
        id: "formatted",
      },
      question,
      answer
    );
    const answerContent = answer.querySelector(".RichContent");
    const answerId = window.location.href.substring(
      window.location.href.lastIndexOf("/") + 1
    );
    const commentsUrl = `https://www.zhihu.com/api/v4/comment_v5/answers/${answerId}/root_comment?order_by=score&limit=20&offset=`;
    renderComments(commentsUrl, answerContent);
    root.after(div);
    window.history.pushState("formatted", "");
    postprocess(div);
  }

  async function formatZhuanlan() {
    root.style.display = "none";
    let div = document.querySelector("#formatted");
    if (div !== null) {
      div.remove();
    }

    const header = document.querySelector("header.Post-Header").cloneNode(true);
    const title = header.querySelector(".Post-Title");
    Object.assign(title.style, {
      fontSize: "1.5rem",
      fontWeight: "bold",
      marginBottom: "1rem",
    });
    const post = document.querySelector("div.Post-RichText").cloneNode(true);
    const time = document.querySelector("div.ContentItem-time").cloneNode(true);
    const topics = document
      .querySelector("div.Post-topicsAndReviewer")
      .cloneNode(true);
    const titleImage = document.querySelector(".TitleImage");

    div = createElement("div", {
      id: "formatted",
      style: {
        padding: "1rem",
        "background-color": "white",
      },
    });
    if (titleImage) {
      const img =
        (await getRealImage(titleImage)) || titleImage.cloneNode(true);
      div.appendChild(img);
    }
    div.append(header, post, time, topics);
    const articleId = window.location.href.substring(
      window.location.href.lastIndexOf("/") + 1
    );
    const commentsUrl = `https://www.zhihu.com/api/v4/comment_v5/articles/${articleId}/root_comment?order_by=score&limit=20&offset=`;
    renderComments(commentsUrl, div);
    root.after(div);
    window.history.pushState("formatted", "");
    postprocess(div);
  }

  async function formatPin() {
    let div = document.querySelector("#formatted");
    if (div !== null) {
      div.remove();
    }

    const pinItem = document.querySelector(".PinItem");
    div = pinItem.cloneNode(true);
    div.id = "formatted";
    div.style.margin = "1rem";

    const remainContents = div.querySelectorAll(
      ".PinItem-remainContentRichText"
    );
    remainContents.forEach(remainContent => {
      // assume either the original pin or the repost pin has non-text content (video/image), not both.
      // otherwise the code may not run correctly.
      if (remainContent.querySelector(".RichText-video")) {
        // show video
        replaceVideosByLinks(remainContent);
      }
      const preview = remainContent.querySelector(".Image-Wrapper-Preview");
      if (preview) {
        // show all images
        replaceThumbnailsByRealImages(preview);
      }
    });

    const comments = div.querySelector(".Comments-container");
    comments.style.display = "none";

    const pinId = window.location.href.substring(
      window.location.href.lastIndexOf("/") + 1
    );
    const commentsUrl = `https://www.zhihu.com/api/v4/comment_v5/pins/${pinId}/root_comment?order_by=score&limit=20&offset=`;
    renderComments(commentsUrl, div);

    root.after(div);
    root.style.display = "none";
    window.history.pushState("formatted", "");
    fixLinks(div);
    convertEquations(div);
  }

  async function replaceThumbnailsByRealImages(preview) {
    try {
      const groups = /^\/pin\/(\d+)/.exec(window.location.pathname);
      const pinId = groups[1];
      const response = await GM.asyncHttpRequest({
        method: "GET",
        url: "https://api.zhihu.com/pins/" + pinId,
      });
      const pinInfo = JSON.parse(response.responseText);
      const content = (pinInfo.origin_pin || pinInfo).content;
      const images = await Promise.all(
        content
          .filter(item => item.type === "image")
          .map(item => createImgFromURL(item.url))
      );
      const div = createElement("div", {}, ...images);
      preview.replaceWith(div);
    } catch (error) {
      console.error(`Error getting all images: ${error.message}`);
    }
  }

  function replaceVideosByLinks(el) {
    let videoDivs = el.querySelectorAll(".RichText-video");
    if (el.classList.contains("RichText-video")) {
      videoDivs = [...videoDivs, el];
    }
    const newTitle = createElement("div", { style: { margin: "0.5rem auto" } });
    newTitle.innerText = "视频";
    videoDivs.forEach(async div => {
      try {
        const attr = div.attributes["data-za-extra-module"];
        const videoId = JSON.parse(attr.value).card.content.video_id;
        const href = "https://www.zhihu.com/video/" + videoId;
        const response = await GM.asyncHttpRequest({
          method: "GET",
          url: "https://lens.zhihu.com/api/videos/" + videoId,
          headers: {
            "Content-Type": "application/json",
            Origin: "https://v.vzuu.com",
            Referer: "https://v.vzuu.com/video/" + videoId,
          },
        });
        const videoInfo = JSON.parse(response.responseText);
        const thumbnail = videoInfo.cover_info.thumbnail;

        const layout = div.querySelector(".VideoCard-layout");
        layout.style.textAlign = "center";
        layout.prepend(newTitle.cloneNode(true));

        const title = layout.querySelector(".VideoCard-title");
        if (title) {
          layout.children[0].innerText = "视频:" + title.innerText;
          title.parentNode.remove();
        }
        const video = layout.querySelector(".VideoCard-video");
        const a = createElement(
          "a",
          { href: href, style: { width: "100%" } },
          createElement("img", {
            src: thumbnail,
            style: { "max-width": "100%" },
          })
        );
        video.replaceWith(a);
      } catch (error) {
        console.error(`Error getting video info: ${error.message}`);
      }
    });
  }

  function enableGIF(div) {
    try {
      const src = div.querySelector("img").src;
      const i = src.lastIndexOf(".");
      const img = createElement("img", {
        src: src.slice(0, i + 1) + GIF_EXT,
        style: {
          maxWidth: "100%",
          display: "block",
          margin: "auto",
        },
      });
      div.replaceWith(img);
    } catch (error) {
      console.error(`Error enabling gif: ${error.message}`);
    }
  }

  function getAttrValOfAnyDOM(el, attr) {
    const res = el.querySelector(`*[${attr}]`) || el;
    return res.getAttribute(attr);
  }

  function getAttrValFromNoscript(el, attr) {
    let noscripts = el.querySelectorAll("noscript");
    const re = new RegExp(`${attr}="(.*?)"`);
    if (el.tagName === "NOSCRIPT") {
      noscripts = [...noscripts, el];
    }
    for (let i = 0; i < noscripts.length; ++i) {
      const nos = noscripts[i];
      const content = nos.textContent || nos.innerText || nos.innerHTML;
      if (content) {
        const groups = re.exec(content);
        if (groups) {
          return groups[1];
        }
      }
    }
    return null;
  }

  function getAttrVal(el, attr) {
    return getAttrValOfAnyDOM(el, attr) || getAttrValFromNoscript(el, attr);
  }

  async function getRealImage(el) {
    const imgSrcAttrs = ["data-original", "data-actualsrc", "data-src", "src"];
    let imgSrcs = imgSrcAttrs
      .map(attr => getAttrVal(el, attr))
      .filter(src => src != null && IMG_SRC_REG_EX.test(src));

    return imgSrcs.length > 0 ? await createImgFromURL(imgSrcs[0]) : null;
  }

  async function createImgFromURL(url) {
    const suffix = QUALITY_TO_SUFFIX[await settings.get("imageQuality")];
    const image = new ZhihuImage(url, suffix);
    const img = createElement("img", {
      src: image.next(),
      style: {
        maxWidth: "100%",
        display: "block",
        margin: "1rem auto",
        cursor: "pointer",
      },
      onclick: () => {
        img.src = image.next();
      },
      onmouseover: hint.show,
      onmouseleave: hint.hide,
    });
    return img;
  }

  // enable all gifs and load images
  function loadAllFigures(el) {
    const figures = el.querySelectorAll("figure");
    figures.forEach(async figure => {
      const gifDiv = figure.querySelector("div.RichText-gifPlaceholder");
      if (gifDiv !== null) {
        enableGIF(gifDiv);
      } else {
        const img = await getRealImage(figure);
        if (img) {
          const el =
            figure.querySelector("img") || figure.querySelector("noscript");
          el ? el.replaceWith(img) : figure.prepend(img);
        }
      }
    });
  }

  function fixLinks(el) {
    el.querySelectorAll("a").forEach(a => {
      // fix redirect links
      const groups = REDIRECT_LINK_REG_EX.exec(a.href);
      if (groups) {
        a.href = decodeURIComponent(groups[1]);
      }

      // fix links with hidden texts
      const spans = a.querySelectorAll(
        ":scope > span.invisible, :scope > span.visible, :scope > span.ellipsis"
      );
      if (spans.length === a.children.length) {
        a.innerHTML =
          a.innerText.length > LINK_TEXT_MAX_LEN
            ? a.innerText.slice(0, LINK_TEXT_MAX_LEN) + "..."
            : a.innerText;
      }
    });
  }

  async function convertEquation(img) {
    const canvas = createElement("canvas", {
      width: EQ_IMG_SCALING_FACTOR * img.width,
      height: EQ_IMG_SCALING_FACTOR * img.height,
    });
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    Object.assign(img.style, {
      width: img.width + "px",
      height: img.height + "px",
    });
    // 直接用img会出现因为cross origin而导致的"Tainted canvases may not be exported"错误
    // 如果window.location.href不是www.zhihu.com/*的话才会出现
    const response = await GM.asyncHttpRequest({
      method: "GET",
      url: img.src,
    });
    const svgXML = response.responseText;
    const svgImg = createElement("img", {
      src: "data:image/svg+xml," + encodeURIComponent(svgXML),
      onload: () => {
        ctx.drawImage(svgImg, 0, 0, canvas.width, canvas.height);
        img.src = canvas.toDataURL("image/png");
      },
    });
  }

  // Equations are converted to PNG images by the clipper, but the images have low resolutions
  // This function converts equations to PNG images in higher resolutions.
  function convertEquations(el) {
    const equationImgs = el.querySelectorAll(
      'img[src^="https://www.zhihu.com/equation"]'
    );
    equationImgs.forEach(img => {
      const id = setInterval(() => {
        if (img.complete) {
          clearInterval(id);
          convertEquation(img);
        }
      }, 100);
    });
  }

  function postprocess(el) {
    replaceVideosByLinks(el);
    loadAllFigures(el);
    fixLinks(el);
    convertEquations(el);
  }

  class ZhihuImage {
    constructor(src, defaultSuffix) {
      const groups = IMG_SRC_REG_EX.exec(src);
      this.prefix = groups[1];
      this.ext = groups[3];
      this.i = SUFFIX.indexOf(defaultSuffix);
      if (this.i === -1) this.i = 0;
    }

    next() {
      const src = `${this.prefix}_${SUFFIX[this.i]}.${this.ext}`;
      if (++this.i === SUFFIX.length) this.i = 0;
      return src;
    }
  }

  async function getComments(url, numPages) {
    let comments = [];
    let currURL = url;
    for (let i = 0; numPages < 0 || i < numPages; ++i) {
      const obj = await getJson(currURL);
      comments = comments.concat(obj.data);
      if (obj.paging.is_end) break;
      currURL = obj.paging.next;
    }
    return comments;
  }

  async function getRootComments(url) {
    const rootCommentsObjs = await getComments(
      url,
      parseInt(await settings.get("numRootCommentPages"))
    );
    for (const obj of rootCommentsObjs) {
      if (obj.child_comment_count > obj.child_comments.length) {
        const childrenURL = `https://www.zhihu.com/api/v4/comment_v5/comment/${obj.id}/child_comment?order_by=ts&limit=20&offset=`;
        obj.child_comments = await getComments(
          childrenURL,
          parseInt(await settings.get("numChildCommentPages"))
        );
      }
    }
    return rootCommentsObjs;
  }

  function formatDate(date) {
    const str = date.toISOString();
    return str.substring(0, 10) + " " + str.substring(11, 16);
  }

  function createCommentNode(comment, isChild, stickers) {
    const authorURL = `https://www.zhihu.com/people/${comment.author.id}`;
    const authorAvatar = createElement(
      "div",
      { class: "fmt-author-avatar" },
      createElement(
        "a",
        { href: authorURL },
        createElement("img", {
          src: comment.author.avatar_url,
          style: {
            width: "1.5em",
            height: "1.5em",
          },
        })
      )
    );
    const authorRowNodes = [
      createElement("a", { href: authorURL }, comment.author.name),
    ];
    if ("reply_to_author" in comment) {
      authorRowNodes.push(
        document.createTextNode(" > "),
        createElement(
          "a",
          {
            href: `https://www.zhihu.com/people/${comment.reply_to_author.id}`,
          },
          comment.reply_to_author.name
        )
      );
    }
    const authorRow = createElement(
      "div",
      { class: "fmt-author-row" },
      ...authorRowNodes
    );
    const content = createElement("div", { class: "fmt-comment-content" });
    content.innerHTML = comment.content.replace(
      /\[.*?\]/g,
      (match, offset, string) => {
        if (match in stickers) {
          return `<img src="${stickers[match]}" class="fmt-emoticon">`;
        } else {
          return match;
        }
      }
    );
    content
      .querySelectorAll(".comment_sticker, .comment_img")
      .forEach(stickerLink => {
        const stickerImg = createElement("img", {
          class: "fmt-comment-img",
          src: stickerLink.href,
        });
        stickerLink.innerText = "";
        stickerLink.appendChild(stickerImg);
      });

    const formattedDate = formatDate(new Date(comment.created_time * 1000));
    const infoStrings = [formattedDate];
    for (const tag of comment.comment_tag) {
      if (tag.type === "ip_info") {
        infoStrings.push(tag.text);
        break;
      }
    }
    infoStrings.push(`${comment.like_count}个赞同`);
    const info = createElement(
      "div",
      { class: "fmt-comment-info" },
      infoStrings.join(" · ")
    );
    const contentColumn = createElement(
      "div",
      { class: "fmt-content-column" },
      authorRow,
      content,
      info
    );
    const node = createElement(
      "div",
      {
        class: isChild ? "fmt-comment" : "fmt-root-comment fmt-comment",
        style: {
          marginLeft: isChild ? "2em" : "0",
        },
      },
      authorAvatar,
      contentColumn
    );

    return node;
  }

  async function renderComments(commentsUrl, parent) {
    if (parseInt(await settings.get("numRootCommentPages")) == 0) return;
    const rootComments = await getRootComments(commentsUrl);
    const stickers = await getStickers();
    let numComments = 0;
    let commentNodes = [];
    for (const comment of rootComments) {
      numComments += 1 + comment.child_comments.length;
      const rootCommentNode = createCommentNode(comment, false, stickers);
      const childCommentNodes = comment.child_comments.map(childComment =>
        createCommentNode(childComment, true, stickers)
      );
      const rootCommentContainer = createElement(
        "div",
        {
          class: "fmt-root-comment-container",
        },
        rootCommentNode,
        ...childCommentNodes
      );
      commentNodes.push(rootCommentContainer);
    }
    const commentsContainer = createElement(
      "div",
      {
        class: "fmt-comments-container",
      },
      createElement(
        "div",
        { class: "fmt-comments-count" },
        `${numComments} 条评论`
      ),
      ...commentNodes
    );
    parent.appendChild(commentsContainer);
  }

  async function getJson(url) {
    const resp = await GM.asyncHttpRequest({
      method: "GET",
      url: url,
    });
    return JSON.parse(resp.responseText);
  }

  const getStickers = (() => {
    let stickers = null;
    return async () => {
      if (stickers !== null) return stickers;
      if (window.zh_emoticon === undefined) {
        const resp = await GM.asyncHttpRequest({
          method: "GET",
          url: "https://unpkg.zhimg.com/@cfe/emoticon/lib/emoticon.js",
        });
        eval(resp.responseText);
      }
      stickers = {};
      for (const group of window.zh_emoticon) {
        for (const sticker of group.stickers) {
          if (sticker.placeholder in stickers) continue;
          stickers[sticker.placeholder] = sticker.static_image_url;
        }
      }
      return stickers;
    };
  })();

  class Settings {
    constructor() {
      this.settings = {};
      this.div = null;
      const cornerButtons = document.querySelector(".CornerButtons");
      if (cornerButtons) {
        const button = createElement("button", {
          type: "button",
          class: "Button CornerButton Button--plain",
          "data-tooltip": "设置知乎重排",
          "data-tooltip-position": "left",
        });
        button.innerHTML = SETTING_ICON_HTML;
        button.onclick = this.show.bind(this);
        const div = createElement(
          "div",
          { class: "CornerAnimayedFlex" },
          button
        );
        cornerButtons.prepend(div);
      }
    }

    async get(key) {
      return await GM.getValue(key, this.settings[key].defaultOption);
    }

    async set(key, value) {
      return await GM.setValue(key, value);
    }

    add_setting(key, desc, options, defaultOption) {
      this.settings[key] = {
        desc,
        options,
        defaultOption,
      };
    }

    async show() {
      this.close();
      this.div = createElement("div", {
        style: {
          position: "fixed",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -50%)",
          backgroundColor: "white",
          border: "1px solid black",
          borderRadius: "5px",
          padding: "0.8rem",
          width: "24rem",
          zIndex: 999,
        },
      });

      for (let key in this.settings) {
        if (this.div.children.length > 0) {
          this.div.appendChild(document.createElement("br"));
        }
        const { desc, options } = this.settings[key];
        const descSpan = document.createElement("span");
        descSpan.innerText = `${desc}: `;
        this.div.appendChild(descSpan);
        if (options.length == 0) {
          const input = createElement("input", {
            id: key,
            type: "text",
            name: key,
            onchange: event => {
              this.set(key, event.target.value);
            },
          });
          input.value = await this.get(key);
          this.div.appendChild(input);
        } else if (
          options.length === 2 &&
          options.includes(true) &&
          options.includes(false)
        ) {
          // the setting is binary
          const checkbox = createElement("input", {
            type: "checkbox",
            onchange: event => {
              this.set(key, event.target.checked);
            },
          });
          if (await this.get(key)) {
            checkbox.setAttribute("checked", "checked");
          }
          this.div.appendChild(checkbox);
        } else {
          const savedOption = await this.get(key);
          for (let option of options) {
            const radio = createElement("input", {
              id: `${key}-${option}`,
              type: "radio",
              name: key,
              value: option,
              onchange: () => {
                this.set(key, option);
              },
            });
            if (option === savedOption) {
              radio.setAttribute("checked", "checked");
            }
            this.div.appendChild(radio);
            const label = createElement("label", { for: `${key}-${option}` });
            label.innerText = option;
            this.div.appendChild(label);
          }
        }
      }
      if (this.div.children.length > 0) {
        const closeBtn = createElement("span", {
          style: {
            position: "absolute",
            top: "0.5rem",
            right: "0.5rem",
            cursor: "pointer",
          },
          onclick: this.close.bind(this),
        });
        closeBtn.innerText = "X";
        this.div.appendChild(closeBtn);
        document.body.appendChild(this.div);
      }
    }

    close() {
      if (this.div !== null) {
        this.div.remove();
        this.div = null;
      }
    }
  }

  // global constants
  const QUALITY_TO_SUFFIX = {
    原始: "r",
    高清: "hd",
    缩略: "b",
  };
  const EQ_IMG_SCALING_FACTOR = 2; // scaling factor for equation images
  const IMG_SRC_REG_EX = /^(https?:\/\/.+[a-z0-9]{32})(_\w+)?\.(\w+)/;
  const REDIRECT_LINK_REG_EX = /https?:\/\/link\.zhihu\.com\/\?target=(.*)/i;
  const SUFFIX = ["r", "hd", "b"];
  //const SUFFIX = ["r", "hd", "b", "xl", "t", "l", "m", "s"];
  const GIF_EXT = "gif"; // can be changed to .webp, but Evernote does not support it.
  const LINK_TEXT_MAX_LEN = 50;
  const SETTING_ICON_HTML = `<svg t="1567388644978" viewBox="0 0 1024 1024" version="1.1" p-id="1116" width="24" height="24" fill="currentColor">
    <defs><style type="text/css"></style></defs>
    <path d="M1020.1856 443.045888c-4.01408-21.634048-25.494528-43.657216-47.776768-48.529408l-16.662528-3.702784c-39.144448-11.49952-73.873408-36.640768-95.955968-73.670656-22.081536-37.225472-27.300864-79.517696-17.665024-118.301696l5.219328-15.20128c6.62528-21.049344-2.00704-50.087936-19.472384-64.510976 0 0-15.657984-12.862464-59.82208-37.614592-44.164096-24.556544-63.235072-31.378432-63.235072-31.378432-21.479424-7.600128-51.591168-0.38912-67.249152 15.787008l-11.64288 12.0832c-29.710336 27.285504-69.658624 43.851776-113.82272 43.851776-44.164096 0-84.513792-16.760832-114.224128-44.240896l-11.241472-11.69408C371.177472 49.74592 340.865024 42.534912 319.3856 50.13504c0 0-19.27168 6.821888-63.435776 31.378432-44.164096 24.946688-59.621376 37.810176-59.621376 37.810176-17.46432 14.227456-26.09664 43.071488-19.472384 64.315392l4.81792 15.396864c9.435136 38.784 4.416512 80.88064-17.665024 118.106112-22.08256 37.225472-57.212928 62.56128-96.559104 73.865216l-16.059392 3.508224C29.308928 399.388672 7.6288 421.21728 3.61472 443.045888c0 0-3.613696 19.488768-3.613696 68.992 0 49.504256 3.613696 68.993024 3.613696 68.993024 4.01408 21.828608 25.494528 43.657216 47.776768 48.529408l15.657984 3.508224c39.346176 11.303936 74.677248 36.639744 96.759808 74.059776 22.081536 37.225472 27.300864 79.517696 17.665024 118.301696l-4.617216 15.00672c-6.62528 21.049344 2.00704 50.087936 19.472384 64.510976 0 0 15.657984 12.862464 59.82208 37.614592 44.164096 24.751104 63.235072 31.377408 63.235072 31.377408 21.479424 7.601152 51.591168 0.390144 67.249152-15.785984l11.040768-11.49952c29.91104-27.480064 70.060032-44.240896 114.424832-44.240896 44.3648 0 84.714496 16.956416 114.424832 44.43648l11.040768 11.49952c15.45728 16.175104 45.769728 23.387136 67.249152 15.785984 0 0 19.27168-6.821888 63.435776-31.378432 44.164096-24.751104 59.621376-37.614592 59.621376-37.614592 17.46432-14.227456 26.09664-43.267072 19.472384-64.509952l-4.81792-15.592448c-9.435136-38.58944-4.416512-80.68608 17.665024-117.715968 22.08256-37.225472 57.413632-62.756864 96.759808-74.0608l15.65696-3.508224c22.08256-4.872192 43.762688-26.7008 47.777792-48.528384 0 0 3.613696-19.489792 3.613696-68.993024-0.200704-49.698816-3.8144-69.187584-3.8144-69.187584zM512.100352 710.2464c-112.617472 0-204.157952-88.677376-204.157952-198.208512 0-109.335552 91.339776-198.012928 204.157952-198.012928 112.617472 0 204.157952 88.677376 204.157952 198.208512C716.0576 621.568 624.717824 710.2464 512.100352 710.2464z" p-id="1117"></path>
  </svg>`;

  // global variables
  const root = document.querySelector("#root");
  const hint = createElement("div", {
    style: {
      display: "none",
      position: "fixed",
      backgroundColor: "white",
      border: "1px solid black",
    },
  });
  hint.innerText = "点击图片更换分辨率(如有)";
  hint.show = event => {
    hint.style.display = "block";
    hint.style.top = event.clientY + "px";
    hint.style.left = event.clientX + 3 + "px";
  };
  hint.hide = () => {
    hint.style.display = "none";
  };
  let settings;

  function main() {
    // inject format button/link
    const url = window.location.href;
    if (url.includes("zhuanlan")) addBtnToNav();
    else if (url.includes("answer")) addLinkToNav(formatAnswer);
    else addLinkToNav(formatPin);

    settings = new Settings();
    settings.add_setting(
      "imageQuality",
      "默认图片质量",
      ["原始", "高清", "缩略"],
      "原始"
    );
    settings.add_setting(
      "numRootCommentPages",
      "最大评论页数 (每页20个评论, -1表示无限制, 0表示不存评论)",
      [],
      "0"
    );
    settings.add_setting(
      "numChildCommentPages",
      "最大子评论页数 (每页20个评论, -1表示无限制, 0表示不存评论)",
      [],
      "0"
    );

    // handle backward/forward events
    window.addEventListener("popstate", function (event) {
      const div = document.querySelector("#formatted");
      if (event.state === "formatted") {
        root.style.display = "none";
        div.style.display = "block";
      } else {
        root.style.display = "block";
        div.style.display = "none";
      }
    });

    document.body.append(hint);
  }

  setTimeout(main, 1500);
})();