├── .DS_Store ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── .gitignore ├── README.md ├── assets ├── .DS_Store └── css │ ├── element-variables.scss │ └── reset.css ├── daitu.jpg ├── demo.png ├── layouts └── default.vue ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages └── index.vue ├── plugins ├── axios.js ├── element-ui.js └── hw-editor.js ├── pm2system.config.js ├── server └── index.js ├── static ├── .DS_Store ├── favicon.ico └── img │ ├── 404-full.png │ ├── github.svg │ └── logo.svg ├── wx-group.png └── wx.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | charset = utf-8 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | components/hw-editor/** 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | '@nuxtjs', 12 | 'plugin:nuxt/recommended' 13 | ], 14 | plugins: [ 15 | 16 | ], 17 | // add your custom rules here 18 | rules: { 19 | 'nuxt/no-cjs-in-config': 'off', 20 | 'generator-star-spacing': 'off', 21 | 'no-mixed-operators': 0, 22 | 'vue/max-attributes-per-line': [ 23 | 2, 24 | { 25 | 'singleline': 5, 26 | 'multiline': { 27 | 'max': 1, 28 | 'allowFirstLine': false 29 | } 30 | } 31 | ], 32 | 'vue/attribute-hyphenation': 0, 33 | 'vue/html-self-closing': 0, 34 | 'vue/component-name-in-template-casing': 0, 35 | 'vue/html-closing-bracket-spacing': 0, 36 | 'vue/singleline-html-element-content-newline': 0, 37 | 'vue/no-unused-components': 0, 38 | 'vue/multiline-html-element-content-newline': 0, 39 | 'vue/no-use-v-if-with-v-for': 0, 40 | 'vue/html-closing-bracket-newline': 0, 41 | 'vue/no-parsing-error': 0, 42 | 'vue/comment-directive': 'off', 43 | 'no-console': 0, 44 | 'no-tabs': 0, 45 | 'quotes': [ 46 | 2, 47 | 'single', 48 | { 49 | 'avoidEscape': true, 50 | 'allowTemplateLiterals': true 51 | } 52 | ], 53 | 'no-delete-var': 2, 54 | 'prefer-const': [ 55 | 2, 56 | { 57 | 'ignoreReadBeforeAssign': false 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report(报告问题) 3 | about: Create a report to help us improve 4 | --- 5 | 10 | 11 | 12 | ## Bug report(问题描述) 13 | 14 | #### Steps to reproduce(问题复现步骤) 15 | 20 | 21 | #### Screenshot or Gif(截图或动态图) 22 | 23 | 24 | #### Other relevant information(格外信息) 25 | - Your OS: 26 | - Your broswer version: 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request(新功能建议) 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## Feature request(新功能建议) 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question(提问) 3 | about: Asking questions about use 4 | --- 5 | 6 | ## Question(提问) 7 | 8 | 15 | 16 | #### Steps to reproduce(问题复现步骤) 17 | 22 | 23 | #### Screenshot or Gif(截图或动态图) 24 | 25 | 26 | #### Other relevant information(格外信息) 27 | - Your OS: 28 | - Your broswer version: 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | /node_modules 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | *.tar.gz 87 | 88 | .vscode 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🎉 Html2md 🥳

2 | 3 |
4 | 5 | `html2md` 是由 [helloworld开发者社区](https://www.helloworld.net) 开源的一款轻量级功能强大的html转md工具💪🏻,纯前端开发,不需要后端接口( Node是我们前端的😗!),支持多平台,一键将文章链接转换为md,方便大家收藏和保存文章🤪。请勿做其它用途 6 | 7 | [html2md地址](https://www.helloworld.net/html2md) 👈🏻 8 | 9 | ### 教程,如下图 10 | ![html2md教程](./demo.png) 11 | 12 | 13 |
14 |
15 | 16 | ## 技术栈 17 | 18 | - [vue](https://cn.vuejs.org/) 前端三剑客之一,主张最少,具有高度灵活性的渐进式框架 19 | - [nuxt](https://zh.nuxtjs.org/) 通过利用 Vue.js 和 Node.js最佳实践来构建高性能应用程序 20 | - [express](https://www.expressjs.com.cn/) 基于 Node.js 平台,快速、开放、极简的 Web 开发框架 21 | - [element-ui](https://element.eleme.cn/#/zh-CN) 宇宙第一 Vue 第三方组件库,有不服? 22 | - [js-dom](https://github.com/jsdom/jsdom) 一款可在 Node 环境下模拟浏览器的 API 的库 23 | - [turndown](https://github.com/domchristie/turndown) 使用 JavaScript 将 HTML 转换为 Markdown 24 | - [axios](http://www.axios-js.com/) 易用、简洁且高效的 http库,支持浏览器和 Node 环境。 25 | - [mavon-editor](https://github.com/hinesboy/mavonEditor) 一款基于 Vue 的 markdown 编辑器,支持所见即所得 26 | - [sass](https://www.sass.hk/) 强大的 Css 预处理器之一 27 | ## 使用 28 | 29 | > 注意:node 版本要求: v14.18.0 30 | 31 | ### 第一步:下载 32 | ```bash 33 | git clone git@github.com:helloworld-Co/html2md.git 34 | cd ./html2md 35 | ``` 36 | ### 第二步:安装 37 | ```bash 38 | npm install 39 | 或者 40 | yarn install 41 | ``` 42 | ### 第三步:启动 43 | ```bash 44 | npm run dev 45 | 或者 46 | yarn dev 47 | ``` 48 | 49 | ## 使用备注 50 | - **防盗链**:由于各大网站的图片基本都会有防盗链机制,转换出来的 md 里的图片会无法正常预览,这个不慌 😳,把 md 内容复制到 [helloworld开发者社区](https://www.helloworld.net) 的写作文本框中就能正常显示了👌🏻。 51 | - **微信文章**:微信文章内容格式差异性比较大,不能保证 💯% 好使,如果一篇不能解决,那就换下一篇吧😏。 52 | 53 | ## 更新日志 54 | - **2021.02.07** 55 | 1. 新增 `a` 标签和 `img` 标签中的`相对路径`转换成`绝对路径`功能,方便以后查找源路径。 56 | - **2021.03.27** 57 | 1. 底部添加转载来源声明。 58 | - **2021.04.20** 59 | 1. 添加对知乎专栏文章的支持👌🏻。 60 | - **2021.05.01** 61 | 1. 添加 Markdown 内容的文件保存和下载⬇️。 62 | - **2021.06.27** 63 | 1. 添加 HTML 直接转 Markdown 功能,和之前的 URL 转 Markdown 区分开来,可切换使用。 64 | - **2021.07.10** 65 | 1. 添加对 learnku 文章的支持👌🏻。 66 | 67 | 68 | ## 技术交流群 / 官方公众号 69 | 微信号 **daitukeji** 70 |
71 | 官方微信群 72 | 官方公众号 73 | 74 | ## 支持环境 75 | 76 | 现代浏览器及 IE11。 77 | 78 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | 79 | | --------- | --------- | --------- | --------- | --------- | 80 | | IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions 81 | 82 | ## 参与贡献 83 | 84 | 我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 😃 85 | 86 | - 通过 [Issue](https://github.com/helloworld-Co/html2md/issues) 报告 bug。 87 | - 提交 [Pull Request](https://github.com/helloworld-Co/html2md/pulls) 一起改进。 88 | 89 | ## 技术交流 90 | 91 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/assets/.DS_Store -------------------------------------------------------------------------------- /assets/css/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量 */ 2 | $--color-primary: #00A6F4; 3 | 4 | /* 改变 icon 字体路径变量,必需 */ 5 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 6 | 7 | @import "~element-ui/packages/theme-chalk/src/index"; 8 | -------------------------------------------------------------------------------- /assets/css/reset.css: -------------------------------------------------------------------------------- 1 | /** 2 | * 重置样式 3 | * */ 4 | html, 5 | body, 6 | div, 7 | span, 8 | applet, 9 | object, 10 | iframe, 11 | p, 12 | blockquote, 13 | pre, 14 | a, 15 | abbr, 16 | acronym, 17 | address, 18 | big, 19 | cite, 20 | code, 21 | del, 22 | dfn, 23 | em, 24 | img, 25 | ins, 26 | kbd, 27 | q, 28 | s, 29 | samp, 30 | small, 31 | strike, 32 | sub, 33 | sup, 34 | tt, 35 | var, 36 | b, 37 | u, 38 | i, 39 | center, 40 | dl, 41 | dt, 42 | dd, 43 | ol, 44 | ul, 45 | li, 46 | fieldset, 47 | form, 48 | label, 49 | legend, 50 | table, 51 | caption, 52 | tbody, 53 | tfoot, 54 | thead, 55 | tr, 56 | th, 57 | td, 58 | article, 59 | aside, 60 | canvas, 61 | details, 62 | embed, 63 | figure, 64 | figcaption, 65 | footer, 66 | header, 67 | hgroup, 68 | menu, 69 | nav, 70 | output, 71 | ruby, 72 | section, 73 | summary, 74 | time, 75 | mark, 76 | audio, 77 | video { 78 | margin: 0; 79 | padding: 0; 80 | border: 0; 81 | font-size: 100%; 82 | -webkit-margin-before: 0; 83 | -webkit-margin-after: 0; 84 | -webkit-margin-start: 0; 85 | -webkit-margin-end: 0; 86 | font-weight: normal; 87 | } 88 | 89 | h1, 90 | h2, 91 | h3, 92 | h4, 93 | h5, 94 | h6, 95 | strong { 96 | margin: 0; 97 | padding: 0; 98 | border: 0; 99 | font-size: 100%; 100 | -webkit-margin-before: 0; 101 | -webkit-margin-after: 0; 102 | -webkit-margin-start: 0; 103 | -webkit-margin-end: 0; 104 | } 105 | /* HTML5 display-role reset for older browsers */ 106 | 107 | article, 108 | aside, 109 | details, 110 | figcaption, 111 | figure, 112 | footer, 113 | header, 114 | hgroup, 115 | menu, 116 | nav, 117 | section { 118 | display: block; 119 | } 120 | 121 | ol, 122 | ul, 123 | li { 124 | list-style: none; 125 | } 126 | 127 | blockquote, 128 | q { 129 | quotes: none; 130 | } 131 | 132 | blockquote:before, 133 | blockquote:after, 134 | q:before, 135 | q:after { 136 | content: ""; 137 | content: none; 138 | } 139 | 140 | table { 141 | border-collapse: collapse; 142 | border-spacing: 0; 143 | } 144 | 145 | html, 146 | body { 147 | -webkit-tap-hightlight-color: transparent; 148 | font-family: -apple-system, SF UI Text, Arial, PingFang SC, Hiragino Sans GB, 149 | Microsoft YaHei, WenQuanYi Micro Hei, sans-serif; 150 | width: 100%; 151 | height: 100%; 152 | padding: 0; 153 | margin: 0; 154 | background: #f5f5f5; 155 | } 156 | 157 | * { 158 | -moz-box-sizing: border-box; 159 | -webkit-box-sizing: border-box; 160 | box-sizing: border-box; 161 | } 162 | 163 | ol, 164 | ul, 165 | li { 166 | margin: 0; 167 | padding: 0; 168 | list-style-type: none; 169 | } 170 | 171 | img { 172 | border: 0; 173 | vertical-align: middle; 174 | } 175 | 176 | a, 177 | a:hover, 178 | a:focus, 179 | a:active { 180 | text-decoration: none; 181 | outline: none; 182 | } 183 | 184 | p { 185 | font-size: 16px; 186 | } 187 | 188 | /* 文本被选择的背景色以及文本颜色 */ 189 | ::selection { 190 | color: #fff; 191 | background-color: #00A6F4; 192 | } 193 | 194 | /* 文本被选择的背景色以及文本颜色 */ 195 | ::-moz-selection { 196 | color: #fff; 197 | background-color: #00A6F4; 198 | } 199 | 200 | img { 201 | object-fit: cover; 202 | } 203 | 204 | /* 滚动条 */ 205 | 206 | /*滚动条宽度*/ 207 | ::-webkit-scrollbar { 208 | -webkit-appearance: none; 209 | width: 6px; 210 | height: 6px; 211 | } 212 | 213 | /* 轨道样式 */ 214 | ::-webkit-scrollbar-track {} 215 | 216 | 217 | /* Handle样式 */ 218 | ::-webkit-scrollbar-thumb { 219 | border-radius: 6px; 220 | background: rgba(194,194,194,0.4); 221 | box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); 222 | } 223 | 224 | ::-webkit-scrollbar-thumb:hover { 225 | border-radius: 6px; 226 | background: rgba(194,194,194,0.8); 227 | } 228 | 229 | ::-webkit-scrollbar-thumb:vertical { 230 | background-color: rgb(194,194,194); 231 | } 232 | 233 | /*hover到滚动条上*/ 234 | ::-webkit-scrollbar-thumb:vertical:hover { 235 | background-color: rgba(0, 0, 0, 0.3); 236 | } 237 | 238 | /*滚动条按下*/ 239 | ::-webkit-scrollbar-thumb:vertical:active { 240 | background-color: rgba(0, 0, 0, 0.6); 241 | } 242 | 243 | .github-markdown-body ul li, 244 | .vuepress-markdown-body ul li{ 245 | list-style: disc!important; 246 | margin-left: 30px!important; 247 | } 248 | 249 | .github-markdown-body ol li, 250 | .vuepress-markdown-body ol li{ 251 | list-style: decimal!important; 252 | margin-left: 30px!important; 253 | } 254 | -------------------------------------------------------------------------------- /daitu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/daitu.jpg -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/demo.png -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | telemetry: false, 3 | /* 4 | ** Headers of the page 5 | */ 6 | head: { 7 | title: 'hello-html2md', 8 | meta: [ 9 | { charset: 'utf-8' }, 10 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 11 | { hid: 'description', name: 'description', content: 'helloworld社区开发的轻量强大的html一键转md工具,支持多平台一键转换。' }, 12 | { hid: 'keywords', name: 'keywords', content: 'javscript,html,markdown,vue,html-to-markdown,md,helloworld,helloworld开发者社区' }, 13 | { name: 'renderer', content: 'webkit' }, 14 | { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge,chrome=1' } 15 | ], 16 | link: [ 17 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 18 | ] 19 | }, 20 | 21 | /* 22 | ** Customize the progress-bar color 23 | */ 24 | loading: { color: '#00A6F4' }, 25 | 26 | /* 27 | ** Global CSS 28 | */ 29 | css: [ 30 | 'assets/css/reset.css' 31 | ], 32 | 33 | /* 34 | ** Plugins to load before mounting the App 35 | */ 36 | plugins: [ 37 | '@/plugins/axios', 38 | '@/plugins/element-ui', 39 | { src: '@/plugins/hw-editor', ssr: false } 40 | ], 41 | /* 42 | ** Nuxt.js dev-modules 43 | */ 44 | buildModules: [ 45 | // Doc: https://github.com/nuxt-community/eslint-module 46 | '@nuxtjs/eslint-module' 47 | ], 48 | /* 49 | ** Nuxt.js modules 50 | */ 51 | modules: [ 52 | '@nuxtjs/axios', 53 | '@nuxtjs/component-cache' 54 | ], 55 | /* 56 | ** axios modules 57 | ** https://axios.nuxtjs.org/options 58 | */ 59 | axios: { 60 | proxy: true, // Can be also an object with default options 61 | credentials: true 62 | }, 63 | /* 64 | ** Build configuration 65 | */ 66 | build: { 67 | // analyze: true, // 打包时用来分析依赖包包大小 68 | transpile: [], 69 | extractCSS: true, // 单独提取 css 70 | babel: { 71 | 'plugins': [ 72 | [ 73 | 'component', 74 | { 75 | 'libraryName': 'element-ui', 76 | 'styleLibraryName': 'theme-chalk' 77 | } 78 | ] 79 | ], 80 | 'comments': true 81 | }, 82 | optimization: { 83 | splitChunks: { 84 | chunks: 'all', 85 | minSize: 30000, // 模块的最小体积 86 | // maxSize: 360000, // 模块的最大体积 87 | automaticNameDelimiter: '~', // 文件名的连接符 88 | name: true, 89 | cacheGroups: { // 缓存组 90 | styles: { 91 | test: /\.[css|less|scss|sass]/, 92 | chunks: 'all', 93 | priority: 30, 94 | enforce: true 95 | }, 96 | 'element-ui': { 97 | test: /node_modules[\\/]element-ui/, 98 | chunks: 'initial', 99 | priority: 20, 100 | name: true, 101 | enforce: true 102 | }, 103 | vendors: { 104 | test: /[\\/]node_modules[\\/]/, 105 | chunks: 'initial', 106 | priority: 10, 107 | enforce: true 108 | }, 109 | default: { 110 | minChunks: 2, 111 | priority: -10, 112 | reuseExistingChunk: true 113 | } 114 | } 115 | } 116 | }, 117 | /* 118 | ** You can extend webpack config here 119 | */ 120 | extend (config, { isDev, isClient }) { 121 | if (isClient && !isDev) { 122 | config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true 123 | } 124 | } 125 | }, 126 | server: { 127 | port: 3031, // default: 3000 128 | host: 'localhost' // default: localhost 129 | }, 130 | env: { 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-html2md", 3 | "version": "1.0.0", 4 | "description": "轻量强大的html一键转md工具,支持多平台一键转换", 5 | "author": "洪少利", 6 | "private": true, 7 | "scripts": { 8 | "dev": "cross-env ENV=dev node server --watch server", 9 | "build": "cross-env ACTION=build node server", 10 | "start": "node server", 11 | "lint": "eslint --fix --ext .js,.vue --ignore-path .gitignore ." 12 | }, 13 | "lint-staged": { 14 | "*.{js,vue}": "eslint" 15 | }, 16 | "dependencies": { 17 | "body-parser": "^1.19.0", 18 | "copy-to-clipboard": "^3.3.1", 19 | "cross-env": "^7.0.3", 20 | "element-ui": "^2.13.0", 21 | "jsdom": "^16.4.0", 22 | "mavon-editor": "^2.9.1", 23 | "nuxt": "2.14.12", 24 | "request": "^2.88.2", 25 | "turndown": "^7.0.0", 26 | "turndown-plugin-gfm": "^1.0.2" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "7.9.0", 30 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 31 | "@nuxtjs/axios": "^5.9.7", 32 | "@nuxtjs/component-cache": "^1.1.5", 33 | "@nuxtjs/eslint-config": "^1.0.1", 34 | "@nuxtjs/eslint-module": "^1.0.0", 35 | "babel-eslint": "^10.0.1", 36 | "babel-plugin-component": "^1.1.1", 37 | "eslint": "^6.1.0", 38 | "eslint-plugin-html": "^6.0.0", 39 | "eslint-plugin-nuxt": ">=0.4.2", 40 | "eslint-plugin-vue": "^5.2.3", 41 | "husky": "^4.2.3", 42 | "node-sass": "^4.14.1", 43 | "sass-loader": "^7.3.1", 44 | "vuepress": "^1.5.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 126 | 127 | 170 | -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | const codes = { 2 | success: 1 3 | } 4 | 5 | export default function ({ $axios, store, redirect, req }) { 6 | // 添加请求拦截器 7 | $axios.onRequest((req) => { 8 | 9 | }) 10 | 11 | // 添加响应拦截器 12 | $axios.onResponse((res) => { 13 | return new Promise((resolve, reject) => { 14 | const { data = {} } = res 15 | if (data.code === codes.success) { // 成功 16 | resolve(data) 17 | return 18 | } 19 | 20 | reject(res) 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /plugins/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import '@/assets/css/element-variables.scss' 3 | 4 | // 按需加载组件 5 | import { 6 | Button, 7 | Input 8 | } from 'element-ui' 9 | Vue.use(Button) 10 | Vue.use(Input) 11 | -------------------------------------------------------------------------------- /plugins/hw-editor.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import mavonEditor from 'mavon-editor' 3 | import 'mavon-editor/dist/css/index.css' 4 | 5 | Vue.use(mavonEditor) 6 | -------------------------------------------------------------------------------- /pm2system.config.js: -------------------------------------------------------------------------------- 1 | /* pm2system.config.js 2 | * pm2 start pm2system.config.js --only hello-html2md 3 | */ 4 | module.exports = { 5 | apps: [ 6 | { 7 | name: 'hello-html2md', // 应用名 8 | cwd: './', // 当前工作路径 9 | script: 'npm', // 实际启动脚本 10 | args: 'run start', // 参数 11 | autorestart: true, 12 | watch: true, // 监控变化的目录,一旦变化,自动重启 13 | watch_delay: 10000, 14 | ignore_watch: ['node_modules', 'static'], // 从监控目录中排除 15 | watch_options: { 16 | 'followSymlinks': false, 17 | 'usePolling': true 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const bodyParser = require('body-parser') 4 | const express = require('express') 5 | const consola = require('consola') 6 | const request = require('request') 7 | const jsdom = require('jsdom') 8 | const { JSDOM } = jsdom 9 | 10 | const { Nuxt, Builder } = require('nuxt') 11 | const app = express() 12 | 13 | const isDev = process.env.ENV === 'dev' 14 | const isBuild = process.env.ACTION === 'build' 15 | 16 | // Import and Set Nuxt.js options 17 | const config = require('../nuxt.config.js') 18 | Object.assign(config, { 19 | dev: isDev 20 | }) 21 | 22 | app.use(bodyParser.json({ limit: '50mb' })) 23 | app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })) 24 | 25 | // 设置应用静态目录 26 | app.use(express.static(path.join(__dirname, 'static'))) 27 | 28 | const html2md = { 29 | dom: '', 30 | qUrl: '', 31 | lazyAttrs: ['data-src', 'data-original-src', 'data-original', 'src'], 32 | dealLazyImg (img) { 33 | /** 34 | * 处理懒加载路径 35 | * 简书:data-original-src 36 | * 掘金:data-src 37 | * segmentfault:data-src 38 | * 知乎专栏:data-original 39 | **/ 40 | for (let i = 0, len = this.lazyAttrs.length; i < len; i++) { 41 | const src = img.getAttribute(this.lazyAttrs[i]) 42 | if (src) { return src } 43 | } 44 | return '' 45 | }, 46 | getAbsoluteUrl (p) { 47 | // 获取图片、链接的绝对路径 48 | const qOrigin = new URL(this.qUrl).origin || '' 49 | return new URL(p, qOrigin).href 50 | }, 51 | changeRelativeUrl () { 52 | // 转换图片、链接的相对路径 53 | if (!this.dom) { return '
内容出错~
' } 54 | const copyDom = this.dom 55 | const imgs = copyDom.querySelectorAll('img') 56 | const links = copyDom.querySelectorAll('a') 57 | imgs.length > 0 && imgs.forEach((v) => { 58 | const src = this.dealLazyImg(v) 59 | v.src = this.getAbsoluteUrl(src) 60 | }) 61 | links.length > 0 && links.forEach((v) => { 62 | const href = v.href || this.qUrl 63 | v.href = this.getAbsoluteUrl(href) 64 | }) 65 | 66 | this.dom = copyDom 67 | return this 68 | }, 69 | addOriginText () { 70 | // 底部添加转载来源声明 71 | const originDom = new JSDOM('').window.document.createElement('div') 72 | originDom.innerHTML = `
本文转自 ${this.qUrl},如有侵权,请联系删除。
` 73 | this.dom.appendChild(originDom) 74 | return this 75 | }, 76 | getInnerHtml () { 77 | return this.dom.innerHTML 78 | }, 79 | returnFinalHtml () { 80 | return this.changeRelativeUrl().addOriginText().getInnerHtml() 81 | }, 82 | getDom (html, selector) { 83 | // 获取准确的文章内容 84 | const dom = new JSDOM(html) 85 | const htmlContent = dom.window.document.querySelector(selector) 86 | return htmlContent 87 | }, 88 | getTitle (content) { 89 | // 获取文章的 title 90 | const title = this.getDom(content, 'title') 91 | if (title) { return title.textContent } 92 | return '获取标题失败~' 93 | }, 94 | getBody (content) { 95 | // 获取不同平台的文章内容 96 | const getBySelector = selector => this.getDom(content, selector) 97 | 98 | // 掘金 99 | if (this.qUrl.includes('juejin.cn')) { 100 | const htmlContent = getBySelector('.markdown-body') 101 | const extraDom = htmlContent.querySelector('style') 102 | const extraDomArr = htmlContent.querySelectorAll('.copy-code-btn') 103 | extraDom && extraDom.remove() 104 | extraDomArr.length > 0 && extraDomArr.forEach((v) => { v.remove() }) 105 | html2md.dom = htmlContent 106 | return this.returnFinalHtml() 107 | } 108 | // oschina 109 | if (this.qUrl.includes('oschina.net')) { 110 | const htmlContent = getBySelector('.article-detail') 111 | const extraDom = htmlContent.querySelector('.ad-wrap') 112 | extraDom && extraDom.remove() 113 | html2md.dom = htmlContent 114 | return this.returnFinalHtml() 115 | } 116 | // cnblogs 117 | if (this.qUrl.includes('cnblogs.com')) { 118 | html2md.dom = getBySelector('#cnblogs_post_body') 119 | return this.returnFinalHtml() 120 | } 121 | // weixin 122 | if (this.qUrl.includes('weixin.qq.com')) { 123 | html2md.dom = getBySelector('#js_content') 124 | return this.returnFinalHtml() 125 | } 126 | // 知乎专栏 127 | if (this.qUrl.includes('zhuanlan.zhihu.com')) { 128 | const htmlContent = getBySelector('.RichText') 129 | const extraScript = htmlContent.querySelectorAll('noscript') 130 | extraScript.length > 0 && extraScript.forEach((v) => { v.remove() }) 131 | html2md.dom = htmlContent 132 | return this.returnFinalHtml() 133 | } 134 | // 慕课专栏 135 | if (this.qUrl.includes('www.imooc.com')) { 136 | const htmlContent = getBySelector('.article-con') 137 | html2md.dom = htmlContent 138 | return this.returnFinalHtml() 139 | } 140 | // learnku 141 | if (this.qUrl.includes('learnku.com')) { 142 | const htmlContent = getBySelector('.markdown-body') 143 | const extraScript = htmlContent.querySelectorAll('.toc-wraper') 144 | extraScript.length > 0 && extraScript.forEach((v) => { v.remove() }) 145 | const extraToc = htmlContent.querySelectorAll('.markdown-toc') 146 | extraToc.length > 0 && extraToc.forEach((v) => { v.remove() }) 147 | const extraLink = htmlContent.querySelectorAll('.reference-link') 148 | extraLink.length > 0 && extraLink.forEach((v) => { v.remove() }) 149 | html2md.dom = htmlContent 150 | return this.returnFinalHtml() 151 | } 152 | 153 | // 优先适配 article 标签,没有再用 body 标签 154 | const htmlArticle = getBySelector('article') 155 | if (htmlArticle) { 156 | html2md.dom = htmlArticle 157 | return this.returnFinalHtml() 158 | } 159 | 160 | const htmlBody = getBySelector('body') 161 | if (htmlBody) { 162 | html2md.dom = htmlBody 163 | return this.returnFinalHtml() 164 | } 165 | 166 | return content 167 | } 168 | } 169 | 170 | // 需要模拟请求头的网站 171 | const mockSiteUa = ['juejin.cn'] 172 | // 通过文章地址获取文章 html内容 173 | app.all('/getUrlHtml', function (req, res, next) { 174 | const qUrl = req.query.url || '' 175 | html2md.qUrl = qUrl 176 | 177 | const isMock = mockSiteUa.find(item => qUrl.includes(item)) 178 | const headers = isMock ? { 179 | 'User-Agent': 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)' 180 | } : {} 181 | 182 | // 通过请求获取链接的 html 183 | request({ 184 | url: qUrl, 185 | headers 186 | }, (error, response, body) => { 187 | if (error) { 188 | res.status(404).send('Url Error') 189 | return 190 | } 191 | res.type('text/json') 192 | try { 193 | const json = { 194 | code: 1, 195 | title: html2md.getTitle(body), 196 | html: html2md.getBody(body) 197 | } 198 | res.status(200).send(json) 199 | } catch (error) { 200 | res.status(200).send({ 201 | code: 0, 202 | msg: '程序异常了~' 203 | }) 204 | } 205 | }) 206 | }) 207 | 208 | // 下载路径 209 | const folderName = 'download' 210 | const downLoadPath = path.join('./static', folderName) 211 | // 下载 md 文件 212 | app.post('/getMdFile', function (req, res, next) { 213 | const qMd = req.body.md || '## 空空如也' 214 | const qUrl = req.body.url || 'https://www.helloworld.net' 215 | 216 | // 写入md文件 217 | function writeFile () { 218 | const mdName = `${Date.now()}.md` 219 | try { 220 | fs.writeFileSync(`${downLoadPath}/${mdName}`, qMd) 221 | res.status(200).send({ 222 | code: 1, 223 | path: `${qUrl}/${folderName}/${mdName}` 224 | }) 225 | } catch (error) { 226 | res.status(200).send({ 227 | code: 0, 228 | msg: '程序异常了~' 229 | }) 230 | } 231 | } 232 | 233 | // 判断目录是否存在 234 | const isExist = fs.existsSync(downLoadPath) 235 | if (isExist) { 236 | // 文件夹存在 237 | writeFile() 238 | return 239 | } 240 | 241 | // 文件夹不存在,创建一个 242 | fs.mkdirSync(downLoadPath) 243 | writeFile() 244 | }) 245 | 246 | // 全局错误抛出 247 | app.use((error, req, res, next) => { 248 | if (error) { 249 | console.log('全局错误抛出:', error) 250 | } 251 | }) 252 | 253 | async function start () { 254 | // Init Nuxt.js 255 | const nuxt = new Nuxt(config) 256 | 257 | const { host, port } = nuxt.options.server 258 | 259 | if (isBuild) { 260 | const builder = new Builder(nuxt) 261 | await builder.build() 262 | consola.success({ 263 | message: `🚀 Packaged! 🚀`, 264 | badge: true 265 | }) 266 | return 267 | } 268 | 269 | // Build only in dev mode 270 | if (isDev) { 271 | const builder = new Builder(nuxt) 272 | await builder.build() 273 | } else { 274 | await nuxt.ready() 275 | } 276 | 277 | // Give nuxt middleware to express 278 | app.use(nuxt.render) 279 | 280 | // Listen the server 281 | app.listen(port, host) 282 | 283 | // 拦截“未捕获的异常” 284 | process.on('uncaughtException', function (err) { 285 | console.log('未捕获的异常:', err) 286 | }) 287 | 288 | consola.ready({ 289 | message: `Server listening on http://${host}:${port}`, 290 | badge: true 291 | }) 292 | } 293 | start() 294 | -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/static/.DS_Store -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/static/favicon.ico -------------------------------------------------------------------------------- /static/img/404-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/static/img/404-full.png -------------------------------------------------------------------------------- /static/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /wx-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/wx-group.png -------------------------------------------------------------------------------- /wx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloworld-Co/html2md/ca08965af93e6565806a79087868daa439565ffc/wx.png --------------------------------------------------------------------------------