Markdown Web Clipper

将网页内容转换为 Markdown 格式的工具,支持复制、下载、发送到 GitHub 和 Obsidian 等功能。

目前为 2025-03-18 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Markdown Web Clipper
  3. // @name:zh-CN Markdown Web Clipper
  4. // @description A tool to convert web content into Markdown format, supporting features like copy, download, and sending to GitHub and Obsidian.
  5. // @description:zh-CN 将网页内容转换为 Markdown 格式的工具,支持复制、下载、发送到 GitHub 和 Obsidian 等功能。
  6. // @author shiquda,人民的勤务员 <china.qinwuyuan@gmail.com>
  7. // @namespace https://github.com/ChinaGodMan/UserScripts
  8. // @supportURL https://github.com/ChinaGodMan/UserScripts/issues
  9. // @homepageURL https://github.com/ChinaGodMan/UserScripts
  10. // @license MIT
  11. // @grant GM_addStyle
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setClipboard
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @require https://code.jquery.com/jquery-3.6.0.min.js
  17. // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
  18. // @require https://unpkg.com/turndown/dist/turndown.js
  19. // @require https://unpkg.com/@guyplusplus/turndown-plugin-gfm/dist/turndown-plugin-gfm.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
  21. // @match *://*/*
  22. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAABHNCSVQICAgIfAhkiAAABBdJREFUaIHtmluIVlUUx39pMup4Q1FrRi1CrMAaREUS75KlBhKKSGAKzkPRQ0lBPngjkIZeBMWHAccH8VIKJQTSIAgWVBQKglco7YYiitrYjONtjg/7G2bPPmt/nvOdtc8ndn5wQM/+77X+i73PnrPP/qCgoKCg4P/Bi8CGAHFfAj4JEDcTc4GodM1Ujt1aintcOW4mTtFT8GnFuAusuBGwSjF2xdQCdwlj7LwTt1kpbiYGAh30NtauEHe1EzMCtivEVeEEcXObM8bsFGIuyxhTjY3EzUXA4ArjNQmxHmSIp86zwH3iJvdWEGuoECcC9qs4VWQ3stGXU8b52hNnoppTJcYjG/05RYxJnhiHVZ0qcgjZ8OKE/X/x9J+t7lQJ3wj9lqDvUk/fNDOkKhxDNv5+mT5PAb97+r0d0qwG9nu1fV3DvKRIrPX0+SOwVzVOIxfQJGgHA20e/eocvKrwFnIBETDa0W716P7Ny6wWl5AL+cLS1Hs0EfBpnmY1eId4EfeBkZbmsKCJgHu5OlXkBr0Lsb+ITME/uo352tTjA3qKuATUWG1nkYv9M2ePqvSjZwV+17q/HP/ozsvZozpbgAvOvevIxR7N11oYRgLTrP+vxz+6r+TuLjBDkffNEdBSRV/B2IVcbCe9/1w9ETTgn8obq+grGEeQi70MPF1FX0FYiH90V5Y09Zht4hPBGeRi7VOKc8CM/K3pswb/6L7maB6r86NK6APcIl7oAWB6SdMPuGK1rcjfpg79MdNUGlmbz4X2N0OZ0lgkhmCOSF8FxgF1mDPdCR79JuCz0r/7Yz791Aq6m5gp/jdm43EO+J4qbi4WAD/ifz6lqwMY4MRpThnjH2Ad5lHIhT7Alx4zt/FvDHwvGGMw50aSvgv5UK278IYA9cU4KCQ/CyzBTOfhwPOY00Nb04aZ/hLuKF8HFgEvAGMxq/leIe8N4t/IVJkjJD3k0W5zdJvLxB2H+axj6/sKusVC/j3pSkjHN06yyx5dnaO7hdkplWMnyQr50NHdIdAo12CeGzvZex7tt44uyRfIsZhn1u5X79G6W8zXE1WQkjriC4hkaKqjuYZZ6JLgjnKrR/edo/soYfxUPEd8BKRpetzRrE2R4xmnb0TP66fNV47m4xQ5EjMC8zJgJ5ruaJY47TcryNPixDgmaH51NOUO6ipmIPHThIOOxv2ZUZrR7WY48VF+w2qvFdqDvYoeFZLNKrU1Ovc7MuRxPwWdsNpanbYuzIIXhFXEC76D2d795dzPspAME/I0In8T+yFDnkdS7uDavm4r5NqRII873YMwHv/7bfc1VynX1UfkCfHrXZFBwD7BQAfmtxpajAJOCnkuAPMrCZh1P1wDTMZsCv4DfsLsfLSZiNlItGOKvRggR0FBQcHjx0Ng/VhKUt8K3AAAAABJRU5ErkJggg==
  23. // @compatible chrome
  24. // @compatible firefox
  25. // @compatible edge
  26. // @compatible opera
  27. // @compatible safari
  28. // @compatible kiwi
  29. // @compatible qq
  30. // @compatible via
  31. // @compatible brave
  32. // @version 2025.03.18.0713
  33. // @created 2025-03-18 07:13:13
  34. // @modified 2025-03-18 07:13:13
  35. // ==/UserScript==
  36. /**
  37. * File: web-clipper.user.js
  38. * Project: UserScripts
  39. * File Created: 2025/03/18,Tuesday 07:13:13
  40. * Author: 人民的勤务员@ChinaGodMan (china.qinwuyuan@gmail.com)
  41. * -----
  42. * Last Modified: 2025/03/18,Tuesday 08:48:06
  43. * Modified By: 人民的勤务员@ChinaGodMan (china.qinwuyuan@gmail.com)
  44. * -----
  45. * License: MIT License
  46. * Copyright © 2024 - 2025 ChinaGodMan,Inc
  47. */
  48. //! https://greasyfork.org/zh-CN/scripts/486888
  49. (function () {
  50. 'use strict'
  51. // GitHub 信息设置, GitHub settings
  52.  
  53. // 替换为你的GitHub令牌,Replace with your GitHub Token
  54. const github_token = ''
  55.  
  56. // 替换为你的GitHub仓库所有者,Replace with your GitHub repository owner
  57. const github_owner = '' // 比如 ChinaGodMan,Example: ChinaGodMan
  58.  
  59. // 替换为你的GitHub仓库名称,Replace with your GitHub repository name
  60. const github_repo = '' // 比如 UserScripts,Example: UserScripts
  61.  
  62. // User Config
  63. // Short cut
  64.  
  65. const shortCutUserConfig = {
  66. /* Example:
  67. "Shift": false,
  68. "Ctrl": true,
  69. "Alt": false,
  70. "Key": "m"
  71. */
  72. }
  73.  
  74. // Obsidian
  75. const obsidianUserConfig = {
  76. /* Example:
  77. "my note": [
  78. "Inbox/Web/",
  79. "Collection/Web/Reading/"
  80. ]
  81. */
  82. }
  83. const userLang =
  84. (navigator.languages && navigator.languages[0]) ||
  85. navigator.language ||
  86. 'en'
  87. const translations = {
  88. en: {
  89. copy: 'Copy to clipboard',
  90. copied: 'Copied successfully!',
  91. download_md: 'Download as MD',
  92. send_to_github: 'Send to Github',
  93. send_to_obsidian: 'Send to Obsidian',
  94. github_failed: 'Creation failed:',
  95. github_success: 'Creation succeeded:',
  96. configure: 'Please configure your GitHub information first',
  97. menu: 'Convert to Markdown',
  98. guide: `
  99. - Use **arrow keys** to select elements:
  100. - Up: Select parent element
  101. - Down: Select the first child element
  102. - Left: Select the previous sibling element
  103. - Right: Select the next sibling element
  104. - Use **scroll wheel** to zoom in and out:
  105. * Up: Select parent element
  106. - Down: Select the first child element
  107. - Click to select an element
  108. - Press \`Esc\` key to cancel selection
  109. `
  110. },
  111. 'zh-CN,zh,zh-SG': {
  112. copy: '复制到剪辑版',
  113. copied: '复制成功!',
  114. download_md: '下载为MD',
  115. send_to_github: '保存到GitHub',
  116. send_to_obsidian: '发送到Obsidian',
  117. github_failed: '创建失败:',
  118. github_success: '创建成功:',
  119. configure: '请先配置你的GitHub 信息',
  120. menu: '转换为Markdown',
  121. guide: `
  122. - 使用**方向键**选择元素
  123. - 上:选择父元素
  124. - 下:选择第一个子元素
  125. - 左:选择上一个兄弟元素
  126. - 右:选择下一个兄弟元素
  127. - 使用**滚轮**放大缩小
  128. - 上:选择父元素
  129. - 下:选择第一个子元素
  130. - 点击元素选择
  131. - 按下 \`Esc\` 键取消选择
  132. `
  133. },
  134. 'zh-TW,zh-HK,zh-MO': {
  135. copy: '複製到剪貼簿',
  136. copied: '複製成功!',
  137. download_md: '下載為MD',
  138. send_to_github: '保存到GitHub',
  139. send_to_obsidian: '發送到Obsidian',
  140. github_failed: '創建失敗:',
  141. github_success: '創建成功:',
  142. configure: '請先配置你的GitHub 信息',
  143. menu: '轉換為Markdown',
  144. guide: `
  145. - 使用**方向鍵**選擇元素
  146. - 上:選擇父元素
  147. - 下:選擇第一個子元素
  148. - 左:選擇上一個兄弟元素
  149. - 右:選擇下一個兄弟元素
  150. - 使用**滾輪**放大縮小
  151. - 上:選擇父元素
  152. - 下:選擇第一個子元素
  153. - 點擊元素選擇
  154. - 按下 \`Esc\` 鍵取消選擇
  155. `
  156. },
  157. vi: {
  158. copy: 'Sao chép vào clipboard',
  159. copied: 'Sao chép thành công!',
  160. download_md: 'Tải xuống dưới dạng MD',
  161. send_to_github: 'Gửi đến Github',
  162. send_to_obsidian: 'Gửi đến Obsidian',
  163. github_failed: 'Tạo thất bại:',
  164. github_success: 'Tạo thành công:',
  165. configure: 'Vui lòng cấu hình thông tin GitHub của bạn trước',
  166. menu: 'Chuyển đổi sang Markdown',
  167. guide: `
  168. - S dng **phím mũi tên** để chn các phn tử:
  169. - Lên: Chn phn t cha
  170. - Xung: Chn phn t con đầu tiên
  171. - Trái: Chn phn t anh em trước
  172. - Phi: Chn phn t anh em sau
  173. - S dng **bánh xe cun** để phóng to và thu nhỏ:
  174. - Lên: Chn phn t cha
  175. - Xung: Chn phn t con đầu tiên
  176. - Nhp để chn mt phn t
  177. - Nhn phím \`Esc\` để hy chn
  178. `
  179. },
  180. ja: {
  181. copy: 'クリップボードにコピー',
  182. copied: 'コピー成功!',
  183. download_md: 'MDとしてダウンロード',
  184. send_to_github: 'Githubに送信',
  185. send_to_obsidian: 'Obsidianに送信',
  186. github_failed: '作成失敗:',
  187. github_success: '作成成功:',
  188. configure: 'まずGitHub情報を設定してください',
  189. menu: 'Markdownに変換',
  190. guide: `
  191. - **矢印キー**を使用して要素を選択します:
  192. - 上: 親要素を選択
  193. - 下: 最初の子要素を選択
  194. - 左: 前の兄弟要素を選択
  195. - 右: 次の兄弟要素を選択
  196. - **スクロールホイール**を使用してズームインおよびズームアウトします:
  197. - 上: 親要素を選択
  198. - 下: 最初の子要素を選択
  199. - 要素をクリックして選択
  200. - \`Esc\`キーを押して選択をキャンセル
  201. `
  202. },
  203. ko: {
  204. copy: '클립보드에 복사',
  205. copied: '복사 성공!',
  206. download_md: 'MD로 다운로드',
  207. send_to_github: 'Github에 보내기',
  208. send_to_obsidian: 'Obsidian에 보내기',
  209. github_failed: '생성 실패:',
  210. github_success: '생성 성공:',
  211. configure: '먼저 GitHub 정보를 구성하십시오',
  212. menu: 'Markdown으로 변환',
  213. guide: `
  214. - **화살표 키**를 사용하여 요소를 선택합니다:
  215. - 위: 부모 요소 선택
  216. - 아래: 번째 자식 요소 선택
  217. - 왼쪽: 이전 형제 요소 선택
  218. - 오른쪽: 다음 형제 요소 선택
  219. - **스크롤 휠**을 사용하여 확대 축소합니다:
  220. - 위: 부모 요소 선택
  221. - 아래: 번째 자식 요소 선택
  222. - 요소를 선택하려면 클릭
  223. - 선택을 취소하려면 \`Esc\` 키를 누르십시오
  224. `
  225. },
  226. fr: {
  227. copy: 'Copier dans le presse-papiers',
  228. copied: 'Copié avec succès!',
  229. download_md: 'Télécharger en tant que MD',
  230. send_to_github: 'Envoyer à Github',
  231. send_to_obsidian: 'Envoyer à Obsidian',
  232. github_failed: 'Échec de la création:',
  233. github_success: 'Création réussie:',
  234. configure: 'Veuillez d\'abord configurer vos informations GitHub',
  235. menu: 'Convertir en Markdown',
  236. guide: `
  237. - Utilisez les **touches fléchées** pour sélectionner les éléments:
  238. - Haut: Sélectionner llément parent
  239. - Bas: Sélectionner le premier élément enfant
  240. - Gauche: Sélectionner llément frère précédent
  241. - Droite: Sélectionner llément frère suivant
  242. - Utilisez la **molette de défilement** pour zoomer et dézoomer:
  243. - Haut: Sélectionner llément parent
  244. - Bas: Sélectionner le premier élément enfant
  245. - Cliquez pour sélectionner un élément
  246. - Appuyez sur la touche \`Esc\` pour annuler la sélection
  247. `
  248. },
  249. it: {
  250. copy: 'Copia negli appunti',
  251. copied: 'Copiato con successo!',
  252. download_md: 'Scarica come MD',
  253. send_to_github: 'Invia a Github',
  254. send_to_obsidian: 'Invia a Obsidian',
  255. github_failed: 'Creazione fallita:',
  256. github_success: 'Creazione riuscita:',
  257. configure: 'Si prega di configurare prima le informazioni di GitHub',
  258. menu: 'Converti in Markdown',
  259. guide: `
  260. - Usa i **tasti freccia** per selezionare gli elementi:
  261. - Su: Seleziona l'elemento padre
  262. - Giù: Seleziona il primo elemento figlio
  263. - Sinistra: Seleziona l'elemento fratello precedente
  264. - Destra: Seleziona l'elemento fratello successivo
  265. - Usa la **rotella di scorrimento** per ingrandire e ridurre:
  266. - Su: Seleziona l'elemento padre
  267. - Giù: Seleziona il primo elemento figlio
  268. - Clicca per selezionare un elemento
  269. - Premi il tasto \`Esc\` per annullare la selezione
  270. `
  271. },
  272. de: {
  273. copy: 'In die Zwischenablage kopieren',
  274. copied: 'Erfolgreich kopiert!',
  275. download_md: 'Als MD herunterladen',
  276. send_to_github: 'An Github senden',
  277. send_to_obsidian: 'An Obsidian senden',
  278. github_failed: 'Erstellung fehlgeschlagen:',
  279. github_success: 'Erstellung erfolgreich:',
  280. configure: 'Bitte konfigurieren Sie zuerst Ihre GitHub-Informationen',
  281. menu: 'In Markdown konvertieren',
  282. guide: `
  283. - Verwenden Sie die **Pfeiltasten**, um Elemente auszuwählen:
  284. - Oben: Elternelement auswählen
  285. - Unten: Erstes Kindelement auswählen
  286. - Links: Vorheriges Geschwisterelement auswählen
  287. - Rechts: Nächstes Geschwisterelement auswählen
  288. - Verwenden Sie das **Scrollrad**, um hinein- und herauszuzoomen:
  289. - Oben: Elternelement auswählen
  290. - Unten: Erstes Kindelement auswählen
  291. - Klicken Sie, um ein Element auszuwählen
  292. - Drücken Sie die \`Esc\`-Taste, um die Auswahl abzubrechen
  293. `
  294. }
  295. }
  296. const getTranslations = (lang) => {
  297. for (const key in translations) {
  298. if (key === lang || key.split(',').includes(lang)) {
  299. return translations[key]
  300. }
  301. }
  302. return translations['en']
  303. }
  304. const translate = new Proxy(
  305. function (key) {
  306. const lang = userLang
  307. const strings = getTranslations(lang)
  308. return strings[key] || translations['en'][key]
  309. },
  310. {
  311. get(target, prop) {
  312. const lang = userLang
  313. const strings = getTranslations(lang)
  314. return strings[prop] || translations['en'][prop]
  315. }
  316. }
  317. )
  318.  
  319. // 全局变量
  320. var isSelecting = false
  321. var selectedElement = null
  322. let shortCutConfig, obsidianConfig
  323. // 读取配置
  324. // 初始化快捷键配置
  325. let storedShortCutConfig = GM_getValue('shortCutConfig')
  326. if (Object.keys(shortCutUserConfig).length !== 0) {
  327. GM_setValue('shortCutConfig', JSON.stringify(shortCutUserConfig))
  328. shortCutConfig = shortCutUserConfig
  329. } else if (storedShortCutConfig) {
  330. shortCutConfig = JSON.parse(storedShortCutConfig)
  331. }
  332.  
  333. // 初始化Obsidian配置
  334. let storedObsidianConfig = GM_getValue('obsidianConfig')
  335. if (Object.keys(obsidianUserConfig).length !== 0) {
  336. GM_setValue('obsidianConfig', JSON.stringify(obsidianUserConfig))
  337. obsidianConfig = obsidianUserConfig
  338. } else if (storedObsidianConfig) {
  339. obsidianConfig = JSON.parse(storedObsidianConfig)
  340. }
  341.  
  342. // HTML2Markdown
  343. function convertToMarkdown(element) {
  344. var html = element.outerHTML
  345. let turndownMd = turndownService.turndown(html)
  346. turndownMd = turndownMd.replaceAll('[\n\n]', '[]') // 防止 <a> 元素嵌套的暂时方法,并不完善
  347. return turndownMd
  348. }
  349.  
  350. // 预览
  351. function showMarkdownModal(markdown) {
  352. var $modal = $(`
  353. <div class="h2m-modal-overlay">
  354. <div class="h2m-modal">
  355. <textarea>${markdown}</textarea>
  356. <div class="h2m-preview">${marked.parse(markdown)}</div>
  357. <div class="h2m-buttons">
  358. <button class="h2m-copy">${translate.copy}</button>
  359. <button class="h2m-download">${translate.download_md}</button>
  360. <button class="h2m-github">${translate.send_to_github}</button>
  361. <select class="h2m-obsidian-select">${translate.send_to_obsidian}</select>
  362. </div>
  363. <button class="h2m-close">X</button>
  364. </div>
  365. </div>
  366. `)
  367.  
  368. $modal.find('.h2m-obsidian-select').append($('<option>').val('').text(`${translate.send_to_obsidian}`))
  369. for (const vault in obsidianConfig) {
  370. for (const path of obsidianConfig[vault]) {
  371. // 插入元素
  372. const $option = $('<option>')
  373. .val(`obsidian://advanced-uri?vault=${vault}&filepath=${path}`)
  374. .text(`${vault}: ${path}`)
  375. $modal.find('.h2m-obsidian-select').append($option)
  376. }
  377. }
  378.  
  379. $modal.find('textarea').on('input', function () {
  380. // console.log("Input event triggered");
  381. var markdown = $(this).val()
  382. var html = marked.parse(markdown)
  383. // console.log("Markdown:", markdown);
  384. // console.log("HTML:", html);
  385. $modal.find('.h2m-preview').html(html)
  386. })
  387.  
  388. $modal.on('keydown', function (e) {
  389. if (e.key === 'Escape') {
  390. $modal.remove()
  391. }
  392. })
  393.  
  394. $modal.find('.h2m-copy').on('click', function () { // 复制到剪贴板
  395. GM_setClipboard($modal.find('textarea').val())
  396. $modal.find('.h2m-copy').text(`${translate.copied}`)
  397. setTimeout(() => {
  398. $modal.find('.h2m-copy').text(`${translate.copy}`)
  399. }, 1000)
  400. })
  401. $modal.find('.h2m-github').on('click', function () {
  402. if (!github_token || !github_owner || !github_repo) {
  403. alert(`${translate.configure}`)
  404. return
  405. }
  406. const labels = ['web-clipper']//标签,可多项
  407. const markdown = $modal.find('textarea').val()
  408. const title = markdown.split('\n')[0]
  409. const body = markdown
  410. createIssue(github_token, github_owner, github_repo, title, body, labels)
  411. })
  412. $modal.find('.h2m-download').on('click', function () { // 下载
  413. var markdown = $modal.find('textarea').val()
  414. var blob = new Blob([markdown], { type: 'text/markdown' })
  415. var url = URL.createObjectURL(blob)
  416. var a = document.createElement('a')
  417. a.href = url
  418. // 当前页面标题 + 时间
  419. a.download = `${document.title}-${new Date().toISOString().replace(/:/g, '-')}.md`
  420. a.click()
  421. })
  422.  
  423. $modal.find('.h2m-obsidian-select').on('change', function () { // 发送到 Obsidian
  424. const val = $(this).val()
  425. if (!val) return
  426. const markdown = $modal.find('textarea').val()
  427. GM_setClipboard(markdown)
  428. const title = document.title.replaceAll(/[\\/:*?"<>|]/g, '_') // File name cannot contain any of the following characters: * " \ / < > : | ?
  429. const url = `${val}${title}.md&clipboard=true`
  430. window.open(url)
  431. })
  432.  
  433. $modal.find('.h2m-close').on('click', function () { // 关闭按钮 X
  434. $modal.remove()
  435. })
  436.  
  437. // 同步滚动
  438. // 获取两个元素
  439. var $textarea = $modal.find('textarea')
  440. var $preview = $modal.find('.h2m-preview')
  441. var isScrolling = false
  442.  
  443. // 当 textarea 滚动时,设置 preview 的滚动位置
  444. $textarea.on('scroll', function () {
  445. if (isScrolling) {
  446. isScrolling = false
  447. return
  448. }
  449. var scrollPercentage = this.scrollTop / (this.scrollHeight - this.offsetHeight)
  450. $preview[0].scrollTop = scrollPercentage * ($preview[0].scrollHeight - $preview[0].offsetHeight)
  451. isScrolling = true
  452. })
  453.  
  454. // 当 preview 滚动时,设置 textarea 的滚动位置
  455. $preview.on('scroll', function () {
  456. if (isScrolling) {
  457. isScrolling = false
  458. return
  459. }
  460. var scrollPercentage = this.scrollTop / (this.scrollHeight - this.offsetHeight)
  461. $textarea[0].scrollTop = scrollPercentage * ($textarea[0].scrollHeight - $textarea[0].offsetHeight)
  462. isScrolling = true
  463. })
  464.  
  465. $(document).on('keydown', function (e) {
  466. if (e.key === 'Escape' && $('.h2m-modal-overlay').length > 0) {
  467. $('.h2m-modal-overlay').remove()
  468. }
  469. })
  470.  
  471. $('body').append($modal)
  472. }
  473.  
  474. // 开始选择
  475. function startSelecting() {
  476. $('body').addClass('h2m-no-scroll') // 防止页面滚动
  477. isSelecting = true
  478. // 操作指南
  479. tip(marked.parse(translate.guide))
  480. }
  481.  
  482. // 结束选择
  483. function endSelecting() {
  484. isSelecting = false
  485. $('.h2m-selection-box').removeClass('h2m-selection-box')
  486. $('body').removeClass('h2m-no-scroll')
  487. $('.h2m-tip').remove()
  488. }
  489.  
  490. function tip(message, timeout = null) {
  491. var $tipElement = $('<div>')
  492. .addClass('h2m-tip')
  493. .html(message)
  494. .appendTo('body')
  495. .hide()
  496. .fadeIn(200)
  497. if (timeout === null) {
  498. return
  499. }
  500. setTimeout(function () {
  501. $tipElement.fadeOut(200, function () {
  502. $tipElement.remove()
  503. })
  504. }, timeout)
  505. }
  506.  
  507. // Turndown 配置
  508. var turndownPluginGfm = TurndownPluginGfmService
  509. var turndownService = new TurndownService({ codeBlockStyle: 'fenced' })
  510.  
  511. turndownPluginGfm.gfm(turndownService) // 引入全部插件
  512. // turndownService.addRule('strikethrough', {
  513. // filter: ['del', 's', 'strike'],
  514. // replacement: function (content) {
  515. // return '~' + content + '~'
  516. // }
  517. // });
  518.  
  519. // turndownService.addRule('latex', {
  520. // filter: ['mjx-container'],
  521. // replacement: function (content, node) {
  522. // const text = node.querySelector('img')?.title;
  523. // const isInline = !node.getAttribute('display');
  524. // if (text) {
  525. // if (isInline) {
  526. // return '$' + text + '$'
  527. // }
  528. // else {
  529. // return '$$' + text + '$$'
  530. // }
  531. // }
  532. // return '';
  533. // }
  534. // });
  535.  
  536. // 添加CSS样式
  537. GM_addStyle(`
  538. .h2m-selection-box {
  539. border: 2px dashed #f00;
  540. background-color: rgba(255, 0, 0, 0.2);
  541. }
  542. .h2m-no-scroll {
  543. overflow: hidden;
  544. z-index: 9997;
  545. }
  546. .h2m-modal {
  547. position: fixed;
  548. top: 50%;
  549. left: 50%;
  550. transform: translate(-50%, -50%);
  551. width: 80%;
  552. height: 80%;
  553. background: white;
  554. border-radius: 10px;
  555. display: flex;
  556. flex-direction: row;
  557. z-index: 9999;
  558. }
  559. .h2m-modal-overlay {
  560. position: fixed;
  561. top: 0;
  562. left: 0;
  563. width: 100%;
  564. height: 100%;
  565. background: rgba(0, 0, 0, 0.5);
  566. z-index: 9998;
  567. }
  568. .h2m-modal textarea,
  569. .h2m-modal .h2m-preview {
  570. width: 50%;
  571. height: 100%;
  572. padding: 20px;
  573. box-sizing: border-box;
  574. overflow-y: auto;
  575. }
  576. .h2m-modal .h2m-buttons {
  577. position: absolute;
  578. bottom: 10px;
  579. right: 10px;
  580. }
  581. .h2m-modal .h2m-buttons button,
  582. .h2m-modal .h2m-obsidian-select {
  583. margin-left: 10px;
  584. background-color: #4CAF50; /* Green */
  585. border: none;
  586. color: white;
  587. padding: 13px 16px;
  588. border-radius: 10px;
  589. text-align: center;
  590. text-decoration: none;
  591. display: inline-block;
  592. font-size: 16px;
  593. transition-duration: 0.4s;
  594. cursor: pointer;
  595. }
  596. .h2m-modal .h2m-buttons button:hover,
  597. .h2m-modal .h2m-obsidian-select:hover {
  598. background-color: #45a049;
  599. }
  600. .h2m-modal .h2m-close {
  601. position: absolute;
  602. top: 10px;
  603. right: 10px;
  604. cursor: pointer;
  605. width: 25px;
  606. height: 25px;
  607. background-color: #f44336;
  608. color: white;
  609. font-size: 16px;
  610. border-radius: 50%;
  611. display: flex;
  612. justify-content: center;
  613. align-items: center;
  614. }
  615. .h2m-tip {
  616. position: fixed;
  617. top: 22%;
  618. left: 82%;
  619. transform: translate(-50%, -50%);
  620. background-color: white;
  621. border: 1px solid black;
  622. padding: 8px;
  623. z-index: 9999;
  624. border-radius: 10px;
  625. box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
  626. background-color: rgba(255, 255, 255, 0.7);
  627. }
  628. `)
  629.  
  630. // 注册触发器
  631. shortCutConfig = shortCutConfig ? shortCutConfig : {
  632. 'Shift': false,
  633. 'Ctrl': true,
  634. 'Alt': false,
  635. 'Key': 'm'
  636. }
  637. $(document).on('keydown', function (e) {
  638. if (e.ctrlKey === shortCutConfig['Ctrl'] &&
  639. e.altKey === shortCutConfig['Alt'] &&
  640. e.shiftKey === shortCutConfig['Shift'] &&
  641. e.key.toUpperCase() === shortCutConfig['Key'].toUpperCase()) {
  642. e.preventDefault()
  643. startSelecting()
  644. }
  645. // else {
  646. // console.log(e.ctrlKey, e.altKey, e.shiftKey, e.key.toUpperCase());
  647. // }
  648. })
  649. // $(document).on('keydown', function (e) {
  650. // if (e.ctrlKey && e.key === 'm') {
  651. // e.preventDefault();
  652. // startSelecting()
  653. // }
  654. // });
  655.  
  656. GM_registerMenuCommand(`${translate.menu}`, function () {
  657. startSelecting()
  658. })
  659.  
  660. $(document).on('mouseover', function (e) { // 开始选择
  661. if (isSelecting) {
  662. $(selectedElement).removeClass('h2m-selection-box')
  663. selectedElement = e.target
  664. $(selectedElement).addClass('h2m-selection-box')
  665. }
  666. }).on('wheel', function (e) { // 滚轮事件
  667. if (isSelecting) {
  668. e.preventDefault()
  669. if (e.originalEvent.deltaY < 0) {
  670. selectedElement = selectedElement.parentElement ? selectedElement.parentElement : selectedElement // 扩大
  671. if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') {
  672. selectedElement = selectedElement.firstElementChild
  673. }
  674. } else {
  675. selectedElement = selectedElement.firstElementChild ? selectedElement.firstElementChild : selectedElement // 缩小
  676. }
  677. $('.h2m-selection-box').removeClass('h2m-selection-box')
  678. $(selectedElement).addClass('h2m-selection-box')
  679. }
  680. }).on('keydown', function (e) { // 键盘事件
  681. if (isSelecting) {
  682. e.preventDefault()
  683. if (e.key === 'Escape') {
  684. endSelecting()
  685. return
  686. }
  687. switch (e.key) { // 方向键:上下左右
  688. case 'ArrowUp':
  689. selectedElement = selectedElement.parentElement ? selectedElement.parentElement : selectedElement // 扩大
  690. if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') { // 排除HTML 和 BODY
  691. selectedElement = selectedElement.firstElementChild
  692. }
  693. break
  694. case 'ArrowDown':
  695. selectedElement = selectedElement.firstElementChild ? selectedElement.firstElementChild : selectedElement // 缩小
  696. break
  697. case 'ArrowLeft': // 寻找上一个元素,若是最后一个子元素则选择父元素的下一个兄弟元素,直到找到一个元素
  698. var prev = selectedElement.previousElementSibling
  699. while (prev === null && selectedElement.parentElement !== null) {
  700. selectedElement = selectedElement.parentElement
  701. prev = selectedElement.previousElementSibling ? selectedElement.previousElementSibling.lastChild : null
  702. }
  703. if (prev !== null) {
  704. if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') {
  705. selectedElement = selectedElement.firstElementChild
  706. }
  707. selectedElement = prev
  708. }
  709. break
  710. case 'ArrowRight':
  711. var next = selectedElement.nextElementSibling
  712. while (next === null && selectedElement.parentElement !== null) {
  713. selectedElement = selectedElement.parentElement
  714. next = selectedElement.nextElementSibling ? selectedElement.nextElementSibling.firstElementChild : null
  715. }
  716. if (next !== null) {
  717. if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') {
  718. selectedElement = selectedElement.firstElementChild
  719. }
  720. selectedElement = next
  721. }
  722. break
  723. }
  724.  
  725. $('.h2m-selection-box').removeClass('h2m-selection-box')
  726. $(selectedElement).addClass('h2m-selection-box') // 更新选中元素的样式
  727. }
  728. }
  729. ).on('mousedown', function (e) { // 鼠标事件,选择 mousedown 是因为防止点击元素后触发其他事件
  730. if (isSelecting) {
  731. e.preventDefault()
  732. var markdown = convertToMarkdown(selectedElement)
  733. showMarkdownModal(markdown)
  734. endSelecting()
  735. }
  736. })
  737. function createIssue(token, owner, repo, title, body, labels = []) {
  738. const url = `https://api.github.com/repos/${owner}/${repo}/issues`
  739. const issueData = {
  740. title: title,
  741. body: body,
  742. labels: labels
  743. }
  744. const xhr = new XMLHttpRequest()
  745. xhr.open('POST', url, true)
  746. xhr.setRequestHeader('Authorization', `token ${token}`)
  747. xhr.setRequestHeader('Accept', 'application/vnd.github+json')
  748. xhr.setRequestHeader('Content-Type', 'application/json')
  749. xhr.onreadystatechange = function () {
  750. if (xhr.readyState === 4) {
  751. if (xhr.status >= 200 && xhr.status < 300) {
  752. const response = JSON.parse(xhr.responseText)
  753. alert(`${translate.github_success}\n ${response.html_url}`)
  754.  
  755. } else {
  756. alert(`${translate.github_failed}\n ${xhr.status}\n ${xhr.statusText}\n ${xhr.responseText}`)
  757. console.error(`${translate.github_failed}`, xhr.status, xhr.statusText, xhr.responseText)
  758. }
  759. }
  760. }
  761. xhr.send(JSON.stringify(issueData))
  762. }
  763. })()