乐于分享
好东西不私藏

浏览器插件:Html转Markdown,连图片都自动打包

浏览器插件:Html转Markdown,连图片都自动打包

文章背景

上篇文章我使用Python 开发了一款 桌面工具网页转Markdown 桌面工具,有网友反馈建议开发一个 浏览器插件 来使用。咱主打就是一个听话照做,经过几天的开发,今天终于给他开发完成。


先看效果

已关注
关注
重播 分享

这个插件解决了什么问题

HtmlToMD 当前支持:

  • • 将整个网页转换为 Markdown
  • • 只转换用户选中的内容
  • • 使用 Readability 提取正文,减少广告和无关区域
  • • 使用 Turndown 将 HTML 转成 Markdown
  • • 处理懒加载图片,例如 data-srcdata-original
  • • 自动提取 Markdown 中的图片链接
  • • 下载图片并和 Markdown 一起打包为 ZIP
  • • 支持右键菜单和快捷键
  • • 支持 Chrome、Edge、Brave 等 Chromium 浏览器
  • • 提供 Firefox 临时加载版本
  • • 接入匿名使用统计,便于了解插件是否真的有人用

插件界面非常克制,只保留两个核心选项:

  • • 提取正文
  • • 下载图片并打包为 ZIP

对我来说,这类工具最重要的是稳定和快速,而不是做一个复杂后台。


技术选型

这是一个 Manifest V3 浏览器扩展,核心结构如下:

HtmlToMD├── manifest.json├── popup.html├── popup.css├── popup.js├── background.js├── content.js├── icons/└── lib/    ├── readability.js    ├── turndown.js    ├── jszip.min.js    ├── download-utils.js    └── analytics.js

主要用到三个库:

  • • Readability:从网页里提取正文
  • • Turndown:把 HTML 转成 Markdown
  • • JSZip:把 Markdown 和图片打包成 ZIP

整体链路是:

用户点击插件  ↓popup.js 向当前页面发送消息  ↓content.js 提取 HTML / 选中内容  ↓Readability 清理正文  ↓Turndown 转 Markdown  ↓提取图片链接  ↓popup.js 展示结果  ↓download-utils.js 下载 Markdown 或 ZIP

Manifest V3 配置

插件的 manifest.json 里配置了 popup、后台脚本、内容脚本、权限和快捷键:

{  "manifest_version": 3,  "name": "HtmlToMD",  "version": "1.0.0",  "description": "将网页 HTML 转换为 Markdown 格式,支持正文提取、图片下载、ZIP 打包",  "permissions": [    "activeTab",    "contextMenus",    "downloads",    "storage"  ],  "host_permissions": [    "http://*/*",    "https://*/*"  ],  "action": {    "default_popup": "popup.html",    "default_title": "HtmlToMD - 转换为 Markdown"  },  "background": {    "service_worker": "background.js"  },  "content_scripts": [    {      "matches": ["<all_urls>"],      "js": [        "lib/readability.js",        "lib/turndown.js",        "content.js"      ],      "run_at": "document_idle"    }  ]}

这里有几个关键点。

activeTab 用来访问当前标签页。

contextMenus 用来支持右键转换。

downloads 用来下载 .md 或 .zip

storage 用来保存用户选项,以及生成匿名统计 ID。


核心:把网页正文变成 Markdown

真正做转换的是 content.js

第一步,先创建 Turndown 实例:

function getTurndownService() {  if (!turndownService) {    turndownService = new TurndownService({      headingStyle: 'atx',      hr: '---',      bulletListMarker: '-',      codeBlockStyle: 'fenced',      emDelimiter: '*',      strongDelimiter: '**',      linkStyle: 'inlined',    });  }  return turndownService;}

这里我选择了 fenced 代码块,因为它更适合技术文章:

```jsconsole.log('hello');```

为了避免公众号编辑器误识别,上面这一段在真实 Markdown 文件里需要注意转义。插件生成的 Markdown 会正常输出三反引号代码块。


处理懒加载图片

现在很多网站不会直接把图片地址放在 src 上,而是放在:

  • • data-src
  • • data-original
  • • data-original-src
  • • data-actualsrc

所以我给 Turndown 增加了一个图片规则:

turndownService.addRule('lazyImages', {  filter: ['img'],  replacement: function (content, node) {    var src = node.getAttribute('data-src')      || node.getAttribute('data-original')      || node.getAttribute('data-original-src')      || node.getAttribute('data-actualsrc')      || node.getAttribute('src')      || '';    if (!src) return '';    if (src.indexOf('//') === 0) src = 'https:' + src;    var alt = node.getAttribute('alt') || '';    var title = node.getAttribute('title');    return title      ? '![' + alt + '](' + src + ' "' + title + '")'      : '![' + alt + '](' + src + ')';  }});

这样即使网页使用懒加载,转换出来的 Markdown 里也尽量保留真实图片链接。


提取正文:Readability

如果直接转换整个 document.documentElement.outerHTML,结果通常会非常脏。

页面里会有:

  • • 顶部导航
  • • 侧边栏
  • • 推荐文章
  • • 评论区
  • • 登录弹窗
  • • 广告位

所以我使用 Readability 做正文提取:

function convertWithReadability(html, url) {  var doc = createDocFromHTML(html, url);  var reader = new Readability(doc);  var article = reader.parse();  if (!article) return null;  return {    title: article.title || extractTitle(),    content: article.content,    byline: article.byline || '',  };}

这一步的作用是把“网页”变成“文章主体”。

如果用户关闭「提取正文」,插件也可以直接转换整个页面 HTML。


为什么要单独处理代码块

技术文章最怕代码块格式乱。

默认 HTML 转 Markdown 有时能处理代码块,但语言标识可能丢失。于是我加了一条规则:

turndownService.addRule('fencedCodeBlock', {  filter: function (node, options) {    return (      options.codeBlockStyle === 'fenced' &&      node.nodeName === 'PRE' &&      node.firstChild &&      node.firstChild.nodeName === 'CODE'    );  },  replacement: function (content, node) {    var code = node.firstChild;    var className = code.getAttribute('class') || '';    var language = (className.match(/language-(\S+)/) || [null, ''])[1]      || (className.match(/lang-(\S+)/) || [null, ''])[1]      || (className.match(/brush:\s*(\S+)/) || [null, ''])[1];    return '\n\n```' + language + '\n'      + code.textContent.replace(/\n$/, '')      + '\n```\n\n';  }});

这样遇到类似:

<pre><code class="language-js">console.log('hello')</code></pre>

就能输出:

```jsconsole.log('hello')```

对技术文章归档来说,这个细节很重要。


图片打包:把 Markdown 和图片放进 ZIP

如果只下载 Markdown,图片依然依赖网络链接。

这就会遇到一个问题:过一段时间,图片链接可能失效。

所以插件支持下载图片并打包成 ZIP。

核心逻辑在 download-utils.js

async function downloadAsZip(result, onProgress) {  var zip = new JSZip();  var markdown = result.markdown;  var imageMap = {};  for (var i = 0; i < result.images.length; i++) {    imageMap[result.images[i].url] = result.images[i].filename;  }  markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (match, alt, url) {    return imageMap[url] ? '![' + alt + '](' + imageMap[url] + ')' : match;  });  zip.file(result.filename.replace(/\.md$/, '') + '.md', markdown);  var zipBlob = await zip.generateAsync({ type: 'blob' });  var zipUrl = URL.createObjectURL(zipBlob);  chrome.downloads.download({    url: zipUrl,    filename: result.filename.replace(/\.md$/, '') + '.zip',    saveAs: false,  });}

这里做了两件事:

第一,把 Markdown 里的远程图片链接替换成本地相对路径:

![cover](images/cover.png)

第二,把 .md 文件和 images/ 目录一起放进 ZIP。

最终下载出来的结构大概是:

article.zip├── article.md└── images/    ├── cover.png    ├── image_1.jpg    └── image_2.webp

这对长期归档很友好。


右键菜单和快捷键

除了 popup,插件也支持右键菜单和快捷键。

后台脚本里会创建两个菜单:

chrome.contextMenus.create({  id: 'convert-page',  title: '将当前页面转换为 Markdown',  contexts: ['page', 'action'],});chrome.contextMenus.create({  id: 'convert-selection',  title: '将选中内容转换为 Markdown',  contexts: ['selection'],});

快捷键配置在 manifest.json

{  "commands": {    "convert-page": {      "suggested_key": {        "default": "Alt+M",        "mac": "Alt+M"      },      "description": "转换当前页面为 Markdown"    }  }}

我个人很喜欢 Alt + M 这个交互。

它的含义很直接:Markdown。


打包时踩过的坑

这个插件打包时我踩了两个典型坑。

第一个是 service worker 注册失败:

Service worker registration failed. Status code: 2

解决方式是让后台脚本启动时保持轻量,把依赖改成懒加载:

function ensureDownloadUtils() {  if (typeof JSZip === 'undefined' && typeof importScripts === 'function') {    importScripts('lib/jszip.min.js');  }  if (typeof DownloadUtils === 'undefined' && typeof importScripts === 'function') {    importScripts('lib/download-utils.js');  }}

这样后台 service worker 注册时不会立刻加载所有依赖。

第二个是读取配置时,chrome.storage.sync.get 可能失败或返回空值。

如果直接写:

document.getElementById('extract-article').checked = items.extractArticle;

一旦 items 是 undefined,popup 就会直接报错。

所以我加了默认值兜底:

const DEFAULT_OPTIONS = {  extractArticle: true,  preserveImages: true,};chrome.storage.sync.get(DEFAULT_OPTIONS, (items) => {  const options = chrome.runtime.lastError || !items ? DEFAULT_OPTIONS : items;  document.getElementById('extract-article').checked = options.extractArticle;  document.getElementById('preserve-images').checked = options.preserveImages;});

浏览器扩展开发里,很多问题不是核心逻辑错了,而是浏览器运行环境比普通网页更严格。


多浏览器支持

目前我打了两个包:

HtmlToMD-v1.0.0-chromium.zipHtmlToMD-v1.0.0-firefox.zip

Chromium 系浏览器使用:

{  "background": {    "service_worker": "background.js"  }}

Firefox 临时加载版本使用:

{  "background": {    "scripts": [      "lib/analytics.js",      "lib/jszip.min.js",      "lib/download-utils.js",      "background.js"    ]  }}

这是因为不同浏览器对 Manifest V3 后台脚本的实现细节并不完全一致。

如果要做长期分发,Chrome Web Store 和 Mozilla Add-ons 仍然是更合适的方式。

网盘安装更适合内测。


如何安装

Chrome / Edge / Brave

  1. 1. 下载 HtmlToMD-v1.0.0-chromium.zip
  2. 2. 解压
  3. 3. 打开扩展管理页
    • • Chrome: chrome://extensions
    • • Edge: edge://extensions
    • • Brave: brave://extensions
  4. 4. 开启「开发者模式」
  5. 5. 点击「加载已解压的扩展程序」
  6. 6. 选择解压后的 HtmlToMD 文件夹

Firefox

  1. 1. 下载 HtmlToMD-v1.0.0-firefox.zip
  2. 2. 解压
  3. 3. 打开 about:debugging#/runtime/this-firefox
  4. 4. 点击「临时载入附加组件」
  5. 5. 选择解压目录里的 manifest.json

Firefox 这种方式是临时安装,重启后可能需要重新加载。


最后

HtmlToMD 不是一个很复杂的项目,但它很实用。

它把几个平时经常遇到的小痛点串起来:

  • • 网页正文提取
  • • HTML 转 Markdown
  • • 图片链接修复
  • • 图片批量下载
  • • ZIP 打包
  • • 浏览器扩展交互
  • • 匿名事件统计
  • • 多浏览器打包

这些点单独看都不大,但组合起来就是一个能每天节省时间的小工具。

如果你也经常整理网页资料、写技术博客、维护知识库,HtmlToMD 应该会很顺手。

后面我还计划继续优化:

  • • 支持更多网站的定制提取规则
  • • 优化微信公众号文章转换
  • • 增加批量页面转换
  • • 提供更友好的设置页
  • • 发布到 Chrome Web Store

欢迎试用,也欢迎反馈。


体验软件 私信回复 MD插件GitHub :https://github.com/zhugyGit/HtmlToMD-dis  

上期文章:

我用 Python 写了一个网页转 Markdown 工具:终于不用再手动复制排版了