lc-to-markdown-txt-html

力扣题目描述,讨论发布内容复制 复制为 markdown、txt、html 等格式

  1. // ==UserScript==
  2. // @name lc-to-markdown-txt-html
  3. // @author wuxin0011
  4. // @version 0.0.5
  5. // @namespace https://github.com/wuxin0011/tampermonkey-script/tree/main/lc-to-markdown-txt-html
  6. // @description 力扣题目描述,讨论发布内容复制 复制为 markdown、txt、html 等格式
  7. // @icon data:image/png;base64,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
  8. // @match https://leetcode.cn/circle/discuss/*
  9. // @match https://leetcode.cn/problems/*
  10. // @match https://leetcode.cn/contest/weekly-contest-*/problems/*
  11. // @match https://leetcode.cn/contest/biweekly-contest-*/problems/*
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js
  13. // @require https://unpkg.com/turndown@7.2.0/dist/turndown.js
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22. const url = window.location.href
  23. const HTML_CONVERT = '__HTML_CONVERT__'
  24. const TXT_CONVERT = '__TXT_CONVERT__'
  25. const MARKDOWN_CONVERT = '__MARKDOWN_CONVERT__'
  26. const markdownURL = "https://stonehank.github.io/html-to-md/"
  27.  
  28.  
  29.  
  30. const isDiscuss = () => url.indexOf('https://leetcode.cn/circle/discuss') != -1
  31. const isProblem = () => url.indexOf('https://leetcode.cn/problems') != -1
  32. const isContest = () => url.indexOf('https://leetcode.cn/contest/weekly-contest') != -1 || url.indexOf('https://leetcode.cn/contest/biweekly-contest') != -1
  33.  
  34. const isAutoKey = '__auto_pluging_key' + (isDiscuss() ? '__Discuss__' : isProblem() ? '__Problem__' : '__Contest__')
  35. //
  36. const use = (key) => typeof GM_getValue(key) == 'undefined' ? true : GM_getValue(key)
  37. const isUseMarkDown = () => use(MARKDOWN_CONVERT)
  38. const isUseTxt = () => use(TXT_CONVERT)
  39. const isUseHTML = () => use(HTML_CONVERT)
  40. let timerId = null
  41. let loadOk = false
  42. console.log('markdown', isUseMarkDown(), 'txt', isUseTxt(), 'html', isUseHTML())
  43. const isUsePlugins = () => isUseHTML() || isUseMarkDown() || isUseTxt()
  44. const isUsePluginInThis = () => use(isAutoKey) // 当前页面是否使用该插件
  45. let isFindButtonContainer = false
  46.  
  47.  
  48.  
  49. const SUPPORT_TYPE = {
  50. 'md': 'md',
  51. 'txt': 'txt',
  52. 'html': 'html'
  53. }
  54.  
  55.  
  56.  
  57. const buttons = []
  58. const targetClass = 'my-button-target'
  59. const BUTTON_ID = `#${targetClass}`
  60. for (let i = 0; i < 3; i++) {
  61. const temp = document.createElement('button')
  62. temp.style.marginLeft = '10px'
  63. const type = i == 0 ? SUPPORT_TYPE['md'] : i == 1 ? SUPPORT_TYPE['txt'] : SUPPORT_TYPE['html']
  64. temp.title = `复制为 ${type == 'md' ? 'markdown' : type} 格式`
  65. temp.id = `${BUTTON_ID}-${type}`
  66. temp.textContent = type
  67. temp.copytype = type
  68. buttons.push(temp)
  69. }
  70.  
  71.  
  72. const updateDisplay = (element, u) => element && element instanceof HTMLElement ? (element.style.display = u ? 'inline-block' : 'none') : ''
  73. // markdown button
  74. const markdownButton = buttons[0]
  75. updateDisplay(markdownButton, isUseMarkDown())
  76.  
  77. // txt button
  78. const txtButton = buttons[1]
  79. updateDisplay(txtButton, isUseTxt())
  80.  
  81. // html button
  82. const htmlButton = buttons[2]
  83. updateDisplay(htmlButton, isUseHTML())
  84.  
  85. function getHtmlContent(className) {
  86. const htmlContent = document.querySelector(className)
  87. return htmlContent ? htmlContent.innerHTML : ''
  88. }
  89.  
  90. function updateElementShow(element) {
  91. if (!element instanceof HTMLElement) {
  92. return
  93. }
  94. element.style.display = element.style.display == 'none' ? 'inline-block' : 'none'
  95. }
  96.  
  97.  
  98. function runQuestionActionsContainer() {
  99. const className = '[class$=MarkdownContent]';
  100. const questionActionsContainer = document.querySelector('[class*=QuestionActionsContainer]')
  101. markdownButton.className = 'e11vgnte0 css-yf7o-BaseButtonComponent-ThemedButton ery7n2v0'
  102. htmlButton.className = 'e11vgnte0 css-yf7o-BaseButtonComponent-ThemedButton ery7n2v0'
  103. txtButton.className = 'e11vgnte0 css-yf7o-BaseButtonComponent-ThemedButton ery7n2v0'
  104. const htmlContent = getHtmlContent(className)
  105. runCopy(questionActionsContainer, markdownButton, htmlContent, SUPPORT_TYPE['md'])
  106. runCopy(questionActionsContainer, htmlButton, htmlContent, SUPPORT_TYPE['html'])
  107. }
  108.  
  109.  
  110.  
  111. const toMarkdown = (htmlContent) => {
  112. try {
  113. var turndownService = new TurndownService()
  114. var markdown = turndownService.turndown(htmlContent)
  115. return markdown
  116. } catch (e) {
  117. if (confirm('markdown转换失败,跳转到网站转换?')) {
  118. if (window?.navigator?.clipboard?.writeText) {
  119. window.navigator.clipboard.writeText(htmlContent).then(() => {
  120. window.open(markdownURL, '_blank')
  121. }, () => {
  122.  
  123. })
  124. }
  125. } else {
  126. console.error('convert markdown error default convert txt !', e)
  127. const d = document.createElement('div')
  128. d.innerHTML = content
  129. const txt = handlerText(d.textContent)
  130. return txt
  131. }
  132. }
  133. }
  134.  
  135. function runProblems() {
  136. console.log('~~~ run problem ~~~~', url)
  137. let buttonClassName = 'relative inline-flex items-center justify-center text-caption px-2 py-1 gap-1 rounded-full bg-fill-secondary text-difficulty-easy dark:text-difficulty-easy'
  138. let className = "[data-track-load=description_content]"
  139. let titleClassName = '#qd-content [class*=text-title]'
  140. const isFlexMode = !!document.querySelector('#__next')
  141. console.log('is find', !!document.querySelector(className))
  142. if (isContest()) {
  143. console.log('isFlexMode', isFlexMode)
  144. if (isFlexMode) {
  145. // className = ".FN9Jv"
  146. titleClassName = '#qd-content a'
  147. } else {
  148. className = '#base_content .question-content'
  149. titleClassName = '#base_content .question-title h3'
  150. }
  151.  
  152. } else {
  153.  
  154. // LCP 老版本的 容器 https://leetcode.cn/problems/1ybDKD/description/
  155. if (!document.querySelector(className)) {
  156. className = ".FN9Jv"
  157. titleClassName = '#qd-content a'
  158. }
  159. }
  160.  
  161. let title = document.querySelector(titleClassName)
  162. const titleTxt = title?.textContent
  163. title = title ? '<h2>' + (title?.textContent) + '</h2>' : ''
  164. let u = window.location.href
  165. let orginUrl = title ? `<a href="${u}">` + (u) + '</a>' : ''
  166. let htmlContent = title + getHtmlContent(className) + orginUrl
  167. let container = null
  168.  
  169. // https://leetcode.cn/contest/weekly-contest-312
  170. if (isContest() && !isFlexMode) {
  171. if (!isFindButtonContainer) {
  172. const c = document.querySelector('.contest-question-info')
  173. if (c && !c.querySelector('#lx-markdown-plugins')) {
  174. const str = `<li class="list-group-item lx-markdown-plugins" id="lx-markdown-plugins">
  175. <span>插件</span>
  176. </li>`
  177. c.innerHTML = c.innerHTML + str
  178. container = c.querySelector('.lx-markdown-plugins')
  179. if (container) {
  180. isFindButtonContainer = true
  181. }
  182. }
  183.  
  184. }
  185.  
  186. } else {
  187. container = document.querySelector(className)
  188. if (container) {
  189. container = container.previousElementSibling
  190. }
  191. }
  192. if (!container) {
  193. console.warn('找不到 容器!', url)
  194. urlChangeLoadOk = false
  195. return;
  196. }
  197. markdownButton.className = buttonClassName
  198. txtButton.className = buttonClassName
  199. htmlButton.className = buttonClassName
  200. runCopy(container, txtButton, htmlContent, SUPPORT_TYPE['txt'], titleTxt)
  201. runCopy(container, htmlButton, htmlContent, SUPPORT_TYPE['html'])
  202. runCopy(container, markdownButton, htmlContent, SUPPORT_TYPE['md'])
  203. urlChangeLoadOk = true
  204. }
  205.  
  206.  
  207. function copy(w, element) {
  208. if (!element || !(element instanceof HTMLElement)) {
  209. return
  210. }
  211.  
  212. try {
  213. let clipboard = element?.clipboardObject
  214. if (clipboard) {
  215. //console.log('clipboard destroy')
  216. clipboard.destroy();
  217. }
  218. clipboard = new ClipboardJS(element, {
  219. text: function () {
  220. return w;
  221. }
  222. })
  223. // console.log('update txt >>>>>>>>>')
  224. element.clipboardObject = clipboard
  225. clipboard.on('success', function (e) {
  226. updateButtonStatus(element)
  227. })
  228. clipboard.on('error', function (e) {
  229. updateButtonStatus(element, 'copy error!')
  230. })
  231.  
  232.  
  233. } catch (error) {
  234. // 如果 clipboardjs 引入失败 使用原生的
  235. // use navigator writeText
  236. element.onclick = () => {
  237. navigator.clipboard.writeText(w).then(() => {
  238. //updateButtonStatus(element)
  239. }, () => {
  240. updateButtonStatus(element, 'copy error!')
  241. })
  242. }
  243.  
  244. }
  245.  
  246. }
  247.  
  248.  
  249.  
  250.  
  251. function runCopy(container, ele, htmlContent, type = SUPPORT_TYPE['md'], title = '') {
  252.  
  253. if (!ele || !container || !htmlContent || !type) {
  254. return
  255. }
  256. if (!(container instanceof HTMLElement && ele instanceof HTMLElement)) {
  257. return;
  258. }
  259. // append
  260. if (!document.getElementById(ele.id)) {
  261. ele.originClass = ele.className
  262. container.appendChild(ele)
  263. } else {
  264. // 加载完成 初始化
  265. loadOk = true
  266. // initConmand()
  267. updateButtonStatus(ele, ele.copytype, '', 1000)
  268. clearTimeId()
  269. }
  270.  
  271. if (type == SUPPORT_TYPE['md']) {
  272. const markdown = toMarkdown(htmlContent)
  273. copy(markdown, ele)
  274. } else if (type == SUPPORT_TYPE['txt']) {
  275. const d = document.createElement('div')
  276. d.innerHTML = htmlContent
  277. const txt = handlerText(d.textContent, title)
  278. copy(txt, ele)
  279. } else if (type == SUPPORT_TYPE['html']) {
  280. // html
  281. copy(htmlContent, ele)
  282. } else {
  283. console.warn('no support format ' + type)
  284. }
  285.  
  286. }
  287.  
  288. const handlerText = (str, title = '') => {
  289. if (!str) return str
  290. // 移出空白字符
  291. str = str.replaceAll(' ', '')
  292. str = str.replaceAll('​​​​​​​​​​​​​​​​​​​​​​​', '')
  293. str = str.replaceAll('&nbsp;', '')
  294. str = str.replace('。', "。\n")
  295. str = str.replace(/\n{2,}/g, "\n")
  296. str = str.replace('http', '\n\nhttp')
  297. str = str.replaceAll('示例', "\n示例")
  298. str = str.replace('231', '2^31')
  299. str = str.replace(/10(?!0)(\d+)/g, '10^$1')
  300. str = str.replace('提示', "\n提示")
  301. if (title != '') {
  302. str = str.replace(title, title + "\n\n")
  303. }
  304. return str
  305. }
  306.  
  307.  
  308. const updateButtonStatus = (element, newText = 'copied!', newClass = '', timeout = 1000) => {
  309. if (!element) {
  310. return;
  311. }
  312. // console.log('update button status', element, newText)
  313. element.textContent = newText
  314. if (newClass) {
  315. element.className = newClass
  316. }
  317. setTimeout(() => {
  318. element.textContent = element.copytype
  319. element.className = element.originClass
  320. }, timeout)
  321. }
  322.  
  323. const cookieInfo = document.cookie
  324.  
  325.  
  326. const initConmand = () => {
  327. try {
  328. const isAutoPluginCommand = GM_registerMenuCommand(`当前页面 ${isUsePluginInThis() ? '关闭' : '启用'} 插件 `, () => {
  329. GM_setValue(isAutoKey, !isUsePluginInThis())
  330. window.location.reload()
  331. }, { title: `当前页面 ${isUseHTML() ? '关闭' : '启用'} 插件 ` })
  332.  
  333.  
  334.  
  335. if (!isUsePluginInThis()) {
  336. return;
  337. }
  338.  
  339. // const message = (u, type) => u ? '关闭' : '启用' + (type == 'md' ? ' markdown ' : ` ${type} `)
  340.  
  341. const html_to_markdown = GM_registerMenuCommand(`${isUseMarkDown() ? '关闭' : '启用'} markdown `, () => {
  342. GM_setValue(MARKDOWN_CONVERT, !isUseMarkDown())
  343. updateElementShow(markdownButton)
  344. }, { title: `点击 ${isUseMarkDown() ? '关闭' : '启用'} markdown ` })
  345.  
  346.  
  347. const html_to_txt = GM_registerMenuCommand(`${isUseTxt() ? '关闭' : '启用'} txt `, () => {
  348. GM_setValue(TXT_CONVERT, !isUseTxt())
  349. updateElementShow(txtButton)
  350. }, { title: `点击 ${isUseTxt() ? '关闭' : '启用'} txt ` })
  351.  
  352. const html_to_html = GM_registerMenuCommand(`${isUseHTML() ? '关闭' : '启用'} html `, () => {
  353. GM_setValue(HTML_CONVERT, !isUseHTML())
  354. updateElementShow(htmlButton)
  355. }, { title: `点击 ${isUseHTML() ? '关闭' : '启用'} html ` })
  356.  
  357.  
  358.  
  359. const html_to_markdown_web = GM_registerMenuCommand('html转换markdown网站', () => {
  360. window.open(markdownURL, '_blank')
  361. }, { title: '如果格式转换有问题,请复制为 html 然后用这个网站转换' })
  362.  
  363.  
  364.  
  365. // const copy_local_cookie = GM_registerMenuCommand('复制 cookie ', () => {
  366. // prompt('复制Cookie, Ctrl+A,Ctrl+C 😅', cookieInfo)
  367. // }, { title: '这个功能是本人某些地方需要,但是又不想打开浏览器控制台 。 如果不需要请忽略😅' })
  368.  
  369.  
  370.  
  371. } catch (e) {
  372. console.log('init command error', e)
  373. }
  374.  
  375. }
  376.  
  377.  
  378.  
  379. let times = 0
  380. const MAX_CNT = 10
  381. const TIME_OUT = 1500
  382. initConmand()
  383.  
  384. function clearTimeId() {
  385. if (timerId != null) {
  386. window.cancelIdleCallback(timerId)
  387. window.clearInterval(timerId)
  388. window.clearTimeout(timerId)
  389. timerId = null;
  390. }
  391. }
  392. let support = true
  393. const start = () => {
  394. times += 1
  395. if (times > MAX_CNT || !support) {
  396. // console.info('>>>>>>>>>>>>>>>>>>>clear<<<<<<<<<<<<<<<<<<<<<<<<<')
  397. clearTimeId()
  398. return
  399. }
  400. if (!isUsePlugins() || !isUsePluginInThis()) {
  401. clearTimeId()
  402. return;
  403. }
  404. if (loadOk) {
  405. console.log('load ok')
  406. return
  407. }
  408. timerId = setTimeout(() => {
  409. try {
  410. if (isDiscuss()) {
  411. runQuestionActionsContainer()
  412. } else if (isProblem() || isContest()) {
  413. runProblems()
  414. } else {
  415. support = false
  416. }
  417. } catch (e) {
  418. console.error('install fail ', e)
  419. }
  420. if (!loadOk) {
  421. start()
  422. }
  423. }, TIME_OUT)
  424.  
  425. }
  426.  
  427.  
  428.  
  429. const updateUrl = () => {
  430. updateTimes += 1
  431. if (updateTimes >= 10 || urlChangeLoadOk) {
  432. clearTimeId()
  433. return;
  434. }
  435. timerId = requestIdleCallback(() => {
  436. if (isDiscuss()) {
  437. runQuestionActionsContainer()
  438. } else if (isProblem() || isContest()) {
  439. runProblems()
  440. }
  441.  
  442. if (!urlChangeLoadOk) {
  443. updateUrl()
  444. }
  445.  
  446. }, { timeout: TIME_OUT })
  447. }
  448.  
  449.  
  450. window.onload = () => {
  451. times = 0
  452. start()
  453. }
  454.  
  455. // 监听地址改变
  456. // 重新修改描述
  457. let urlChangeLoadOk = false
  458. let updateTimes = 0
  459. window.addEventListener("urlchange", () => {
  460. updateTimes = 0
  461. urlChangeLoadOk = false
  462. updateUrl();
  463. })
  464.  
  465.  
  466. })();