编写一个浏览器文章目录提取插件

86 阅读6分钟

说在前面

不知道大家发布公众号文章用的是哪个排版工具呢?我这边使用的是墨滴,用起来体验感还是不错的,就是有一点让我觉得比较不习惯,正常写文章的时候应该有个目录可以查看会比较方便,比如掘金写文章的时候就可以查看文章目录,而墨滴是没有这个功能的,于是我便想着自己简单写一个插件来解决一下,给它加一个目录功能。

墨滴文章编写页面如下图,没有一个可以查看目录结构的功能。

掘金文章编写页面如下图,我们可以点击目录按钮来查看目录

插件效果展示

墨滴文章编写页面最右边有一栏操作按钮,我们可以在这里加上一个查看目录的按钮,点击查看按钮后显示当前文章的目录结构信息。

插件功能实现

配置文件

  • manifest.json 编写浏览器插件的第一步,首先我们要先编写配置文件manifest.json
{
  "manifest_version": 3,
  "name": "墨滴插件",
  "version": "1.0.0",
  "description": "墨滴插件",
  "icons": {
    "16": "目录.png",
    "48": "目录.png",
    "128": "目录.png"
  },
  "action": {
    "default_title": "墨滴插件",
    "default_icon": "目录.png",
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["https://editor.mdnice.com/*"],
      "js": ["bg.js"],
      "run_at": "document_end",
      "css": []
    }
  ],
  "web_accessible_resources": [
    {
      "resources": [],
      "matches": ["https://editor.mdnice.com/*"]
    }
  ],
  "permissions": []
}

各配置项说明如下:

  1. 基本信息

    • manifest_version 为 3,表明遵循 Manifest V3 规范。
    • name 是插件的名称“墨滴插件”。
    • version 是插件的版本号“1.0.0”。
    • description 对插件进行了简要描述。
  2. 图标设置

    • 通过 icons 对象指定了不同尺寸的图标为“目录.png”,分别对应 16、48、128 像素大小。
  3. 插件动作设置

    • action 部分定义了插件的动作。
    • default_title 是插件动作的默认标题“墨滴插件”。
    • default_icon 也是“目录.png”。
    • default_popup 指定了当用户点击插件图标时显示的弹出页面为“popup.html”。
  4. 内容脚本设置

    • content_scripts 数组中定义了要注入到网页中的脚本。
    • matches 指定了匹配的网址模式,这里只匹配“editor.mdnice.com/*”。
    • js 列出了要注入的脚本文件为“bg.js”。
    • run_at 设置为“document_end”,表示在文档加载结束时注入脚本。
  5. 可访问资源设置

    • web_accessible_resources 数组允许插件声明哪些资源可以被网页通过特定的方式访问。
    • 当前 resources 为空数组,可能表示暂时没有特定的资源需要被网页访问。
    • matches 同样匹配“editor.mdnice.com/*”,限制了资源可被访…
  6. 权限设置

    • permissions 数组目前为空,表示插件目前没有请求特定的权限。

注入脚本编写

(1)插入目录查看按钮

首先我们需要在页面右边的操作栏最下方加上一个目录查看按钮

const addBtn = () => {
  const btnBox = document.querySelector("#nice-sidebar");
  const svg = `<svg t="1730880340340" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4296" width="16" height="16"><path d="M106.666667 192a21.333333 21.333333 0 1 0 0 42.666667h85.333333a21.333333 21.333333 0 0 0 0-42.666667H106.666667z m0 298.666667a21.333333 21.333333 0 0 0 0 42.666666h85.333333a21.333333 21.333333 0 0 0 0-42.666666H106.666667z m0 298.666666a21.333333 21.333333 0 0 0 0 42.666667h85.333333a21.333333 21.333333 0 0 0 0-42.666667H106.666667zM320 192a21.333333 21.333333 0 0 0 0 42.666667h597.333333a21.333333 21.333333 0 0 0 0-42.666667H320z m0 298.666667a21.333333 21.333333 0 0 0 0 42.666666h597.333333a21.333333 21.333333 0 0 0 0-42.666666H320z m0 298.666666a21.333333 21.333333 0 0 0 0 42.666667h597.333333a21.333333 21.333333 0 0 0 0-42.666667H320z" fill="#1196db" p-id="4297"></path></svg>`;
  const menuBtn = document.createElement("a");
  menuBtn.id = "nice-sidebar-menu-type";
  menuBtn.classList.add("nice-btn-previewtype");
  menuBtn.innerHTML = svg;
  menuBtn.title = "查看目录";
  menuBtn.addEventListener("click", () => {
    const mdniceMenu = document.querySelector("#mdniceMenu");
    if (!mdniceMenu) return;
    const display = mdniceMenu.style.display;
    mdniceMenu.style.display = display === "none" ? "block" : "none";
  });
  btnBox.appendChild(menuBtn);
};

打开控制台,我们可以查看到右边操作栏元素具体selector,使用document.querySelector方法查找页面中具有idnice-sidebar的元素,该元素即为右边的操作栏实例;按钮图标的 SVG 可以自己绘制或者直接到iconfont上找。监听按钮点击事件,在点击按钮的时候对目录面板进行显示或隐藏。

(2)获取文章目录结构

文章标题基本都是h标签包裹的,所以我们可以直接找出页面上的文章容器中的所有h标签,最后将获取到的所有h标签按其offsetTop的高度进行排序即可,具体代码如下:

const getMenuItem = () => {
  const tmp = document.querySelector("#nice-rich-text-box");
  const titleTag = ["h1", "h2", "h3", "h4", "h5", "h6"];
  const menuList = [];
  titleTag.forEach((item) => {
    const list = tmp.querySelectorAll(item);
    menuList.push(...list);
  });
  return menuList.sort((a, b) => {
    return a.offsetTop - b.offsetTop;
  });
};

(3)插入目录面板

我们直接在页面上插入一个div作为目录面板容器,使用fixed定位将其固定到目录查看按钮左边即可。

const addMenuPanel = () => {
  const btnBox = document.querySelector("#nice-sidebar");
  const menuBtn = document.querySelector("#nice-sidebar-menu-type");
  const menuBox = document.createElement("div");
  menuBox.id = "mdniceMenu";
  menuBox.style.position = "fixed";
  menuBox.style.right = btnBox.offsetWidth + "px";
  menuBox.style.top = menuBtn.offsetTop + btnBox.offsetTop + "px";
  menuBox.style.background = "#c4dde9";
  menuBox.style.opacity = "0.7";
  menuBox.style.padding = "1em 0.5em";
  menuBox.style.lineHeight = "1.5em";
  menuBox.style.borderRadius = "1em";
  menuBox.style.height = "20em";
  menuBox.style.overflow = "scroll";
  menuBox.style.color = "#0335f3";
  menuBox.style.display = "none";
  menuBox.style.width = "15em";
  menuBox.style.wordBreak = "break-all";
  document.body.appendChild(menuBox);
  updateMenu();
};

(4)将目录结构插入到目录面板

  • 判断目录面板是否存在
const mdniceMenu = document.querySelector("#mdniceMenu");
if (!mdniceMenu) return;
  • 判断目录结构是否有更新
const menuList = getMenuItem();
const lastMenuList = originMenuList.join("");
originMenuList = [];
menuList.forEach((item) => {
  originMenuList.push(item.nodeName + item.innerText);
});
if (lastMenuList === originMenuList.join("")) return;
  • 获取最小标题

我们需要根据最小标题来计算标题的缩进。

const minNumber = Math.min(
  ...menuList.map((item) => {
    return item.nodeName.slice(1);
  })
);
  • 插入目录信息

使用mousemove+mouseleave可以实现一个hover效果。

mdniceMenu.innerHTML = "";
menuList.forEach((item) => {
  const div = document.createElement("div");
  div.style.paddingLeft = item.nodeName.slice(1) - minNumber + "em";
  div.innerText = item.innerText;
  div.style.cursor = "pointer";
  div.addEventListener("click", () => {
    const tmp = document.querySelector("#nice-rich-text-box");
    tmp.scroll(0, item.offsetTop);
  });
  div.addEventListener("mousemove", () => {
    div.style.textDecoration = "underline";
    div.style.backgroundColor = "#f0f0f0";
  });
  div.addEventListener("mouseleave", () => {
    div.style.textDecoration = "none";
    div.style.backgroundColor = "#c4dde9";
  });
  mdniceMenu.appendChild(div);
});
  • 完整代码
const updateMenu = () => {
  const mdniceMenu = document.querySelector("#mdniceMenu");
  if (!mdniceMenu) return;
  const menuList = getMenuItem();
  const lastMenuList = originMenuList.join("");
  originMenuList = [];
  menuList.forEach((item) => {
    originMenuList.push(item.nodeName + item.innerText);
  });
  if (lastMenuList === originMenuList.join("")) return;
  mdniceMenu.innerHTML = "";
  const minNumber = Math.min(
    ...menuList.map((item) => {
      return item.nodeName.slice(1);
    })
  );
  menuList.forEach((item) => {
    const div = document.createElement("div");
    div.style.paddingLeft = item.nodeName.slice(1) - minNumber + "em";
    div.innerText = item.innerText;
    div.style.cursor = "pointer";
    div.addEventListener("click", () => {
      const tmp = document.querySelector("#nice-rich-text-box");
      tmp.scroll(0, item.offsetTop);
    });
    div.addEventListener("mousemove", () => {
      div.style.textDecoration = "underline";
      div.style.backgroundColor = "#f0f0f0";
    });
    div.addEventListener("mouseleave", () => {
      div.style.textDecoration = "none";
      div.style.backgroundColor = "#c4dde9";
    });
    mdniceMenu.appendChild(div);
  });
};

源码

插件源码已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…

🌟觉得有帮助的可以点个star~

🖊有什么问题或错误可以指出,欢迎pr~

📬有什么想要实现的功能或想法可以联系我~

使用

将插件下载到本地,在扩展程序(chrome://extensions/)中加载已解压的扩展程序即可。

公众号

关注公众号『前端也能这么有趣』,获取更多有趣内容。

说在后面

🎉这里是JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打打羽毛球🏸 ,平时也喜欢写些东西,既为自己记录📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解🙇,写错的地方望指出,定会认真改进😊,在此谢谢大家的支持,我们下文再见🙌。