Pinterest.com Backup Original Files

Download all original images from your Pinterest.com profile. Creates an entry in the Greasemonkey menu, just go to one of your boards, scroll down to the last image and click the option in the menu.

当前为 2022-12-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Pinterest.com Backup Original Files
  3. // @description Download all original images from your Pinterest.com profile. Creates an entry in the Greasemonkey menu, just go to one of your boards, scroll down to the last image and click the option in the menu.
  4. // @namespace cuzi
  5. // @license MIT
  6. // @version 19.0.2
  7. // @match https://*.pinterest.com/*
  8. // @match https://*.pinterest.at/*
  9. // @match https://*.pinterest.ca/*
  10. // @match https://*.pinterest.ch/*
  11. // @match https://*.pinterest.cl/*
  12. // @match https://*.pinterest.co.kr/*
  13. // @match https://*.pinterest.co.uk/*
  14. // @match https://*.pinterest.com.au/*
  15. // @match https://*.pinterest.com.mx/*
  16. // @match https://*.pinterest.de/*
  17. // @match https://*.pinterest.dk/*
  18. // @match https://*.pinterest.es/*
  19. // @match https://*.pinterest.fr/*
  20. // @match https://*.pinterest.ie/*
  21. // @match https://*.pinterest.info/*
  22. // @match https://*.pinterest.it/*
  23. // @match https://*.pinterest.jp/*
  24. // @match https://*.pinterest.net/*
  25. // @match https://*.pinterest.nz/*
  26. // @match https://*.pinterest.ph/*
  27. // @match https://*.pinterest.pt/*
  28. // @match https://*.pinterest.ru/*
  29. // @match https://*.pinterest.se/*
  30. // @grant GM_xmlhttpRequest
  31. // @grant GM_registerMenuCommand
  32. // @grant GM.xmlHttpRequest
  33. // @grant GM.registerMenuCommand
  34. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  35. // @require https://cdn.jsdelivr.net/npm/jszip@3.9.1/dist/jszip.min.js
  36. // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js
  37. // @connect pinterest.com
  38. // @connect pinterest.de
  39. // @connect pinimg.com
  40. // @icon https://s.pinimg.com/webapp/logo_trans_144x144-5e37c0c6.png
  41. // ==/UserScript==
  42.  
  43. /* globals JSZip, saveAs, GM, MouseEvent */
  44.  
  45. // Time to wait between every scroll to the bottom (in milliseconds)
  46. const scrollPause = 1000
  47.  
  48. let scrollIV = null
  49. let lastScrollY = null
  50. let noChangesFor = 0
  51.  
  52. function prepareForDownloading () {
  53. if (scrollIV !== null) {
  54. return
  55. }
  56.  
  57. document.scrollingElement.scrollTo(0, 0)
  58. collectActive = true
  59. scrollIV = true
  60. collectImages()
  61.  
  62. if (!window.confirm('The script needs to scroll down to the end of the page. It will start downloading once the end is reached.\n\nOnly images that are already visible can be downloaded.\n\n\u2757 Keep this tab open (visible) \u2757')) {
  63. return
  64. }
  65.  
  66. const div = document.querySelector('.downloadoriginal123button')
  67. div.style.position = 'fixed'
  68. div.style.top = '30%'
  69. div.style.zIndex = 100
  70. div.innerHTML = 'Collecting images... (keep this tab visible)<br>'
  71.  
  72. const startDownloadButton = div.appendChild(document.createElement('button'))
  73. startDownloadButton.appendChild(document.createTextNode('Stop scrolling & start downloading'))
  74. startDownloadButton.addEventListener('click', function () {
  75. window.clearInterval(scrollIV)
  76. downloadOriginals()
  77. })
  78.  
  79. const statusImageCollector = div.appendChild(document.createElement('div'))
  80. statusImageCollector.setAttribute('id', 'statusImageCollector')
  81.  
  82. document.scrollingElement.scrollTo(0, document.scrollingElement.scrollHeight)
  83.  
  84. window.setTimeout(function () {
  85. scrollIV = window.setInterval(scrollDown, scrollPause)
  86. }, 1000)
  87. }
  88.  
  89. function scrollDown () {
  90. if (document.hidden) {
  91. // Tab is hidden, don't do anyhting
  92. return
  93. }
  94. if (noChangesFor > 2) {
  95. console.log('noChangesFor > 2')
  96. window.clearInterval(scrollIV)
  97. window.setTimeout(downloadOriginals, 1000)
  98. } else {
  99. console.log('noChangesFor <= 2')
  100. document.scrollingElement.scrollTo(0, document.scrollingElement.scrollTop + 500)
  101. if (document.scrollingElement.scrollTop === lastScrollY) {
  102. noChangesFor++
  103. console.log('noChangesFor++')
  104. } else {
  105. noChangesFor = 0
  106. console.log('noChangesFor = 0')
  107. }
  108. }
  109. lastScrollY = document.scrollingElement.scrollTop
  110. }
  111.  
  112. let entryList = []
  113. let url = document.location.href
  114. let collectActive = false
  115. let boardName = ''
  116. let boardNameEscaped = ''
  117. let userName = ''
  118. let userNameEscaped = ''
  119. const startTime = new Date()
  120. const entryTemplate = {
  121. images: [],
  122. title: null,
  123. link: null,
  124. description: null,
  125. note: null,
  126. sourceLink: null
  127. }
  128.  
  129. function collectImages () {
  130. if (!collectActive) return
  131. if (url !== document.location.href) {
  132. // Reset on new page
  133. url = document.location.href
  134. entryList = []
  135. }
  136.  
  137. const imgs = document.querySelectorAll('.gridCentered a[href^="/pin/"] img')
  138. for (let i = 0; i < imgs.length; i++) {
  139. if (imgs[i].clientWidth < 100) {
  140. // Skip small images, these are user profile photos
  141. continue
  142. }
  143. if (!('mouseOver' in imgs[i].dataset)) {
  144. // Fake mouse over to load source link
  145. const mouseOverEvent = new MouseEvent('mouseover', {
  146. bubbles: true,
  147. cancelable: true
  148. })
  149.  
  150. imgs[i].dispatchEvent(mouseOverEvent)
  151. imgs[i].dataset.mouseOver = true
  152. }
  153.  
  154. const entry = Object.assign({}, entryTemplate)
  155. entry.images = [imgs[i].src.replace(/\/\d+x\//, '/originals/'), imgs[i].src]
  156.  
  157. if (imgs[i].alt) {
  158. entry.description = imgs[i].alt
  159. }
  160.  
  161. const pinWrapper = parentQuery(imgs[i], '[data-test-id="pinWrapper"]') || parentQuery(imgs[i], '[role="listitem"]') || parentQuery(imgs[i], '[draggable="true"]')
  162. if (pinWrapper) {
  163. // find metadata
  164. const aText = Array.from(pinWrapper.querySelectorAll('a[href*="/pin/"]')).filter(a => a.firstChild.nodeType === a.TEXT_NODE)
  165. if (aText.length > 0 && aText[0]) {
  166. entry.title = aText[0].textContent.trim()
  167. entry.link = aText[0].href.toString()
  168. } else if (pinWrapper.querySelector('a[href*="/pin/"]')) {
  169. entry.link = pinWrapper.querySelector('a[href*="/pin/"]').href.toString()
  170. }
  171. const aNotes = Array.from(pinWrapper.querySelectorAll('a[href*="/pin/"]')).filter(a => a.querySelector('div[title]'))
  172. if (aNotes.length > 0 && aNotes[0]) {
  173. entry.note = aNotes[0].textContent.trim()
  174. }
  175.  
  176. if (pinWrapper.querySelector('[data-test-id="pinrep-source-link"] a')) {
  177. entry.sourceLink = pinWrapper.querySelector('[data-test-id="pinrep-source-link"] a').href.toString()
  178. }
  179. }
  180.  
  181. if (imgs[i].srcset) {
  182. // e.g. srcset="https://i-h2.pinimg.com/236x/15/87/ae/abcdefg1234.jpg 1x, https://i-h2.pinimg.com/474x/15/87/ae/abcdefg1234.jpg 2x, https://i-h2.pinimg.com/736x/15/87/ae/abcdefg1234.jpg 3x, https://i-h2.pinimg.com/originals/15/87/ae/abcdefg1234.png 4x"
  183.  
  184. let goodUrl = false
  185. let quality = -1
  186. const srcset = imgs[i].srcset.split(', ')
  187. for (let j = 0; j < srcset.length; j++) {
  188. const pair = srcset[j].split(' ')
  189. const q = parseInt(pair[1].replace('x'))
  190. if (q > quality) {
  191. goodUrl = pair[0]
  192. quality = q
  193. }
  194. if (pair[0].indexOf('/originals/') !== -1) {
  195. break
  196. }
  197. }
  198. if (goodUrl && quality !== -1) {
  199. entry.images[0] = goodUrl
  200. }
  201. }
  202.  
  203. let exists = false
  204. for (let j = 0; j < entryList.length; j++) {
  205. if (entryList[j].images[0] === entry.images[0] && entryList[j].images[1] === entry.images[1]) {
  206. exists = true
  207. entryList[j] = entry // replace with newer entry
  208. break
  209. }
  210. }
  211. if (!exists) {
  212. entryList.push(entry)
  213. console.debug(imgs[i].parentNode)
  214. console.debug(entry)
  215. }
  216. }
  217. const statusImageCollector = document.getElementById('statusImageCollector')
  218. if (statusImageCollector) {
  219. statusImageCollector.innerHTML = `Collected ${entryList.length} images`
  220. }
  221. }
  222.  
  223. function addButton () {
  224. if (document.querySelector('.downloadoriginal123button')) {
  225. return
  226. }
  227.  
  228. if (document.querySelector('[data-test-id="board-header"]') && document.querySelectorAll('.gridCentered a[href^="/pin/"] img').length) {
  229. const button = document.createElement('div')
  230. button.type = 'button'
  231. button.classList.add('downloadoriginal123button')
  232. button.setAttribute('style', `
  233. position: absolute;
  234. display: block;
  235. background: white;
  236. border: none;
  237. padding: 5px;
  238. text-align: center;
  239. cursor:pointer;
  240. `)
  241. button.innerHTML = `
  242. <div class="buttonText" style="background: #efefef;border: #efefef 1px solid;border-radius: 24px;padding: 5px;font-size: xx-large;color: #111;width: 62px; height: 58px;">\u2B73</div>
  243. <div style="font-weight: 700;color: #111;font-size: 12px;">Download<br>originals</div>
  244. `
  245. button.addEventListener('click', prepareForDownloading)
  246. document.querySelector('[data-test-id="board-header"]').appendChild(button)
  247. try {
  248. const buttons = document.querySelectorAll('[role="button"] a[href*="/more-ideas/"],[data-test-id="board-header"] [role="button"]')
  249. const rect = buttons[buttons.length - 1].getBoundingClientRect()
  250. button.style.top = rect.top - 2 + 'px'
  251. button.style.left = rect.left - rect.width + 300 + 'px'
  252. } catch (e) {
  253. console.warn(e)
  254. try {
  255. const title = document.querySelector('h1')
  256. const rect = title.getBoundingClientRect()
  257. button.style.top = rect.top - 2 + 'px'
  258. button.style.left = rect.left - 120 + 'px'
  259. } catch (e) {
  260. console.warn(e)
  261. }
  262. }
  263. }
  264. }
  265.  
  266. GM.registerMenuCommand('Pinterest.com - backup originals', prepareForDownloading)
  267. addButton()
  268. window.setInterval(addButton, 1000)
  269. window.setInterval(collectImages, 400)
  270.  
  271. function downloadOriginals () {
  272. try {
  273. boardName = document.querySelector('h1').textContent.trim()
  274. boardNameEscaped = boardName.replace(/[^a-z0-9]/gi, '_')
  275. } catch (e1) {
  276. try {
  277. boardName = document.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/').pop()
  278. boardNameEscaped = boardName.replace(/[^a-z0-9]/gi, '_')
  279. } catch (e2) {
  280. boardName = 'board-' + Math.random()
  281. boardNameEscaped = boardName
  282. }
  283. }
  284. try {
  285. userName = document.location.href.match(/\.(\w{2,3})\/(.*?)\//)[2]
  286. userNameEscaped = userName.replace(/[^a-z0-9]/gi, '_')
  287. } catch (e) {
  288. try {
  289. userName = document.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/').shift()
  290. userNameEscaped = userName.replace(/[^a-z0-9]/gi, '_')
  291. } catch (e2) {
  292. userName = 'user'
  293. userNameEscaped = userName
  294. }
  295. }
  296.  
  297. collectImages()
  298. collectActive = false
  299.  
  300. const lst = entryList.slice()
  301.  
  302. const total = lst.length
  303. let zip = new JSZip()
  304. const fileNameSet = new Set()
  305.  
  306. // Create folders
  307. const imagesFolder = zip.folder('images')
  308. const errorFolder = zip.folder('error_thumbnails')
  309. const markdownOut = []
  310. const htmlOut = []
  311.  
  312. document.body.style.padding = '3%'
  313. document.body.innerHTML = '<h1><span id="counter">' + (total - lst.length) + '</span>/' + total + ' downloaded</h1><br>(Keep this tab visible)<br>' + '</div><progress id="status"></progress> image download<br><progress id="total" value="0" max="' + total + '"></progress> total progress<pre id="statusmessage"></pre>'
  314. document.scrollingElement.scrollTo(0, 0)
  315. const pre = document.getElementById('statusmessage')
  316. const statusbar = document.getElementById('status')
  317. const totalbar = document.getElementById('total')
  318. const h1 = document.getElementById('counter');
  319.  
  320. (async function work () {
  321. document.title = (total - lst.length) + '/' + total + ' downloaded'
  322. h1.innerHTML = totalbar.value = total - lst.length
  323. statusbar.removeAttribute('value')
  324. statusbar.removeAttribute('max')
  325.  
  326. if (lst.length === 0) {
  327. document.title = 'Generating zip file...'
  328. document.body.innerHTML = '<h1>Generating zip file...</h1><progress id="gen_zip_progress"></progress>'
  329. }
  330. if (lst.length > 0) {
  331. const entry = lst.pop()
  332. const urls = entry.images
  333. let fileName = null
  334. const prettyFilename = (s) => safeFileName(s.substr(0, 200)).substr(0, 110).replace(/^[^\w]+/, '').replace(/[^\w]+$/, '')
  335. if (entry.title) {
  336. fileName = prettyFilename(entry.title)
  337. } else if (entry.description) {
  338. fileName = prettyFilename(entry.description)
  339. } else if (entry.note) {
  340. fileName = prettyFilename(entry.note)
  341. } else if (entry.sourceLink) {
  342. fileName = prettyFilename(entry.sourceLink.split('/').slice(3).join('-'))
  343. }
  344.  
  345. if (!fileName) {
  346. fileName = urls[0].split('/').pop()
  347. } else {
  348. fileName = fileName + '.' + urls[0].split('/').pop().split('.').pop()
  349. }
  350.  
  351. while (fileNameSet.has(fileName.toLowerCase())) {
  352. const parts = fileName.split('.')
  353. parts.splice(parts.length - 1, 0, parseInt(Math.random() * 10000).toString())
  354. fileName = parts.join('.')
  355. }
  356. fileNameSet.add(fileName.toLowerCase())
  357.  
  358. pre.innerHTML = fileName
  359. GM.xmlHttpRequest({
  360. method: 'GET',
  361. url: urls[0],
  362. responseType: 'arraybuffer',
  363. onload: async function (response) {
  364. const s = String.fromCharCode.apply(null, new Uint8Array(response.response.slice(0, 125)))
  365. if (s.indexOf('<Error>') !== -1) {
  366. // Download thumbnail to error folder
  367. if (!('isError' in entry) || !entry.isError) {
  368. const errorEntry = Object.assign({}, entry)
  369. errorEntry.images = [urls[1]]
  370. errorEntry.isError = true
  371. // TODO change title? of error entry
  372. lst.push(errorEntry)
  373. }
  374. } else {
  375. // Save file to zip
  376. entry.fileName = fileName
  377. entry.fileNameUrl = markdownEncodeURIComponent(fileName)
  378. if (!('isError' in entry) || !entry.isError) {
  379. imagesFolder.file(fileName, response.response)
  380. entry.filePath = 'images/' + fileName
  381. entry.fileUrl = 'images/' + entry.fileNameUrl
  382. await addMetadata('successful', entry, htmlOut, markdownOut)
  383. } else {
  384. errorFolder.file(fileName, response.response)
  385. entry.filePath = 'error_thumbnails/' + fileName
  386. entry.fileUrl = 'error_thumbnails/' + entry.fileNameUrl
  387. await addMetadata('error', entry, htmlOut, markdownOut)
  388. }
  389. }
  390.  
  391. work()
  392. },
  393. onprogress: function (progress) {
  394. try {
  395. statusbar.max = progress.total
  396. statusbar.value = progress.loaded
  397. } catch (e) { }
  398. }
  399. })
  400. } else {
  401. // Create html and markdown overview
  402. htmlOut.unshift(`
  403. <style>
  404. th,td {
  405. word-wrap: break-word;
  406. max-width: 25em
  407. }
  408. tr:nth-child(2n+2){
  409. background-color:#f0f0f0
  410. }
  411. </style>
  412.  
  413. <h1>${escapeXml(boardName)}</h1>
  414. <h3>
  415. ${escapeXml(userName)}
  416. <br>
  417. <time datetime="${startTime.toISOString()}" title=""${startTime.toString()}">
  418. ${startTime.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
  419. </time>:
  420. <a href="${escapeXml(document.location.href)}">${escapeXml(document.location.href)}</a>
  421. </h3>
  422.  
  423. <table border="1">
  424. <tr>
  425. <th>Title</th>
  426. <th>Image</th>
  427. <th>Pinterest</th>
  428. <th>Source</th>
  429. <th>Description</th>
  430. <th>Notes</th>
  431. </tr>
  432. `)
  433. htmlOut.push('</table>')
  434. zip.file('index.html', htmlOut.join('\n'))
  435. markdownOut.unshift(`
  436. # ${escapeMD(boardName)}
  437.  
  438. ### ${escapeXml(userName)}
  439.  
  440. ${startTime.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}: ${document.location.href}
  441.  
  442. | Title | Image | Pinterest | Source | Description | Notes |
  443. |---|---|---|---|---|---|`)
  444.  
  445. zip.file('README.md', markdownOut.join('\n'))
  446.  
  447. // Done. Open ZIP file
  448. let zipfilename
  449. try {
  450. const d = startTime || new Date()
  451. zipfilename = userNameEscaped + '_' + boardNameEscaped + '_' + d.getFullYear() + '-' + ((d.getMonth() + 1) > 9 ? '' : '0') + (d.getMonth() + 1) + '-' + (d.getDate() > 9 ? '' : '0') + d.getDate() +
  452. '_' + (d.getHours() > 9 ? '' : '0') + d.getHours() + '-' + (d.getMinutes() > 9 ? '' : '0') + d.getMinutes()
  453. } catch (e) {
  454. zipfilename = 'board'
  455. }
  456. zipfilename += '.zip'
  457. const content = await zip.generateAsync({ type: 'blob' }) // TODO catch errors
  458. zip = null
  459. const h = document.createElement('h1')
  460. h.appendChild(document.createTextNode('Click here to Download'))
  461. h.style = 'cursor:pointer; color:blue; background:white; text-decoration:underline'
  462. document.body.appendChild(h)
  463. const genZipProgress = document.getElementById('gen_zip_progress')
  464. if (genZipProgress) {
  465. genZipProgress.remove()
  466. }
  467. h.addEventListener('click', function () {
  468. saveAs(content, zipfilename)
  469. })
  470. saveAs(content, zipfilename)
  471. }
  472. })()
  473. }
  474.  
  475. function addMetadata (status, e, htmlOut, markdownOut) {
  476. return new Promise((resolve) => {
  477. writeMetadata(status, e, htmlOut, markdownOut)
  478. resolve()
  479. })
  480. }
  481.  
  482. function writeMetadata (status, entry, htmlOut, markdownOut) {
  483. // XML escape all values for html
  484. const entryEscaped = Object.fromEntries(Object.entries(entry).map(entry => {
  485. const escapedValue = escapeXml(entry[1])
  486. return [entry[0], escapedValue]
  487. }))
  488.  
  489. // Shorten source link title
  490. let sourceA = ''
  491. if (entry.sourceLink) {
  492. let sourceTitle = decodeURI(entry.sourceLink)
  493. if (sourceTitle.length > 160) {
  494. sourceTitle = sourceTitle.substring(0, 155) + '\u2026'
  495. }
  496. sourceA = `<a href="${entryEscaped.sourceLink}">${escapeXml(sourceTitle)}</a>`
  497. }
  498.  
  499. // HTML table entry
  500. htmlOut.push(` <tr>
  501. <th id="${entryEscaped.fileNameUrl}">
  502. <a href="#${entryEscaped.fileNameUrl}">${entryEscaped.title || entryEscaped.description || entryEscaped.fileName}</a
  503. </th>
  504. <td>
  505. <a href="${entryEscaped.fileUrl}">
  506. <img style="max-width:250px; max-height:250px" src="${entryEscaped.fileUrl}" alt="${entryEscaped.description || entryEscaped.filePath}">
  507. </a>
  508. </td>
  509. <td>
  510. <a href="${entryEscaped.link}">${entryEscaped.link}</a>
  511. </td>
  512. <td>
  513. ${sourceA}
  514. </td>
  515. <td>${entryEscaped.description}</td>
  516. <td>${entryEscaped.note}</td>
  517. </tr>
  518. `)
  519.  
  520. // Shorten source link title
  521. let sourceLink = entry.sourceLink || ''
  522. if (entry.sourceLink) {
  523. let sourceTitle = decodeURI(entry.sourceLink)
  524. if (sourceTitle.length > 160) {
  525. sourceTitle = sourceTitle.substring(0, 155) + '\u2026'
  526. }
  527. sourceLink = `[${escapeMD(sourceTitle)}](${entry.sourceLink})`
  528. }
  529.  
  530. // Markdown
  531. markdownOut.push(`| ${escapeMD(entry.title || entry.description || entry.fileName)}` +
  532. ` | ![${escapeMD(entry.description || entry.fileName)}](${entry.fileUrl})` +
  533. ` | ${entry.link || ''}` +
  534. ` | ${sourceLink}` +
  535. ` | ${escapeMD(entry.description || '')}` +
  536. ` | ${escapeMD(entry.note || '')}` + ' |')
  537. }
  538.  
  539. function parentQuery (node, q) {
  540. const parents = [node.parentElement]
  541. node = node.parentElement.parentElement
  542. while (node) {
  543. const lst = node.querySelectorAll(q)
  544. for (let i = 0; i < lst.length; i++) {
  545. if (parents.indexOf(lst[i]) !== -1) {
  546. return lst[i]
  547. }
  548. }
  549. parents.push(node)
  550. node = node.parentElement
  551. }
  552. return null
  553. }
  554.  
  555. function safeFileName (s) {
  556. const blacklist = /[<>:'"/\\|?*\u0000\n\r\t]/g // eslint-disable-line no-control-regex
  557. s = s.replace(blacklist, ' ').trim().replace(/^\.+/, '').replace(/\.+$/, '')
  558. return s.replace(/\s+/g, ' ').trim()
  559. }
  560.  
  561. function escapeXml (unsafe) {
  562. // https://stackoverflow.com/a/27979933/
  563. const s = (unsafe || '').toString()
  564. return s.replace(/[<>&'"\n\t]/gim, function (c) {
  565. switch (c) {
  566. case '<': return '&lt;'
  567. case '>': return '&gt;'
  568. case '&': return '&amp;'
  569. case '\'': return '&apos;'
  570. case '"': return '&quot;'
  571. case '\n': return '<br>'
  572. case '\t': return ' '
  573. }
  574. })
  575. }
  576.  
  577. function escapeMD (unsafe) {
  578. // Markdown escape
  579. const s = (unsafe || '').toString()
  580. return s.replace(/\W/gim, function (c) {
  581. switch (c) {
  582. case '<': return '&lt;'
  583. case '>': return '&gt;'
  584. case '&': return '&amp;'
  585. case '\'': return '\\\''
  586. case '"': return '\\"'
  587. case '*': return '\\*'
  588. case '[': return '\\['
  589. case ']': return '\\]'
  590. case '(': return '\\('
  591. case ')': return '\\)'
  592. case '{': return '\\{'
  593. case '}': return '\\}'
  594. case '`': return '\\`'
  595. case '!': return '\\!'
  596. case '|': return '\\|'
  597. case '#': return '\\#'
  598. case '+': return '\\+'
  599. case '-': return '\\-'
  600. case '\r': return ' '
  601. case '\n': return '<br>'
  602. default: return c
  603. }
  604. }).trim()
  605. }
  606.  
  607. function markdownEncodeURIComponent (s) {
  608. return encodeURIComponent(s).replace(/[[\](){}`!]/g, function (c) {
  609. switch (c) {
  610. case '[': return '%5B'
  611. case ']': return '%5D'
  612. case '(': return '%28'
  613. case ')': return '%29'
  614. case '{': return '%7B'
  615. case '}': return '%7D'
  616. case '`': return '%60'
  617. case '!': return '%21'
  618. }
  619. })
  620. }