├── screen.png ├── error.svg ├── README.md └── Pinterest.com_Backup_Original_Files.user.js /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvzi/pinterest-Backup-Original-Files/HEAD/screen.png -------------------------------------------------------------------------------- /error.svg: -------------------------------------------------------------------------------- 1 | Error! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pinterest-Backup-Original-Files 2 | 3 | [**Click here for install**](https://github.com/cvzi/pinterest-Backup-Original-Files/raw/master/Pinterest.com_Backup_Original_Files.user.js) 4 | 5 | This is a [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) script. 6 | You can find information about Greasemonkey scripts over at [wiki.greasespot.net](https://wiki.greasespot.net/Greasemonkey_Manual:Installing_Scripts). 7 | 8 | The scripts lets you download all original images from your Pinterest.com profile. 9 | 10 | It adds a red download button at the top of your boards. The script also creates an entry in the Greasemonkey menu. 11 | 12 | Simply go to one of your boards, scroll down to the last image and click the download button or option in the menu. 13 | 14 | The images will be downloaded and packed into a single Zip file file for you. 15 | 16 | [![Say Thanks!](https://img.shields.io/badge/say-thanks-ff69b4.svg?style=for-the-badge)](https://saythanks.io/to/cvzi) 17 | 18 | 19 | ![Screenshot](screen.png) 20 | 21 | Works with Firefox and Chrome (via [Tampermonkey](http://tampermonkey.net/)) 22 | 23 | The Zip file contains all original photos. If there were errors, the thumbnails of the missing photos will be added to a special folder "error_thumbnails" in the Zip file. 24 | -------------------------------------------------------------------------------- /Pinterest.com_Backup_Original_Files.user.js: -------------------------------------------------------------------------------- 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.4 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 | let lastImageListLength = -1 52 | let noImageListLengthChangesFor = 0 53 | 54 | function prepareForDownloading () { 55 | if (scrollIV !== null) { 56 | return 57 | } 58 | 59 | document.scrollingElement.scrollTo(0, 0) 60 | collectActive = true 61 | scrollIV = true 62 | collectImages() 63 | 64 | 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')) { 65 | return 66 | } 67 | 68 | const div = document.querySelector('.downloadoriginal123button') 69 | div.style.position = 'fixed' 70 | div.style.top = '30%' 71 | div.style.zIndex = 100 72 | div.innerHTML = 'Collecting images... (keep this tab visible)
' 73 | 74 | const startDownloadButton = div.appendChild(document.createElement('button')) 75 | startDownloadButton.appendChild(document.createTextNode('Stop scrolling & start downloading')) 76 | startDownloadButton.addEventListener('click', function () { 77 | window.clearInterval(scrollIV) 78 | downloadOriginals() 79 | }) 80 | 81 | const statusImageCollector = div.appendChild(document.createElement('div')) 82 | statusImageCollector.setAttribute('id', 'statusImageCollector') 83 | 84 | document.scrollingElement.scrollTo(0, document.scrollingElement.scrollHeight) 85 | 86 | window.setTimeout(function () { 87 | scrollIV = window.setInterval(scrollDown, scrollPause) 88 | }, 1000) 89 | } 90 | 91 | function scrollDown () { 92 | if (document.hidden) { 93 | // Tab is hidden, don't do anyhting 94 | return 95 | } 96 | if (noChangesFor > 2) { 97 | console.log('noChangesFor > 2') 98 | window.clearInterval(scrollIV) 99 | window.setTimeout(downloadOriginals, 1000) 100 | } else { 101 | console.log('noChangesFor <= 2') 102 | document.scrollingElement.scrollTo(0, document.scrollingElement.scrollTop + 500) 103 | if (document.scrollingElement.scrollTop === lastScrollY) { 104 | noChangesFor++ 105 | console.log('noChangesFor++') 106 | } else { 107 | noChangesFor = 0 108 | console.log('noChangesFor = 0') 109 | } 110 | if (entryList.length !== lastImageListLength) { 111 | lastImageListLength = entryList.length 112 | noImageListLengthChangesFor = 0 113 | } else { 114 | console.log('noImageListLengthChangesFor =', noImageListLengthChangesFor) 115 | noImageListLengthChangesFor++ 116 | if (noImageListLengthChangesFor > 5) { 117 | window.clearInterval(scrollIV) 118 | window.setTimeout(downloadOriginals, 1000) 119 | } 120 | } 121 | } 122 | lastScrollY = document.scrollingElement.scrollTop 123 | } 124 | 125 | let entryList = [] 126 | let url = document.location.href 127 | let collectActive = false 128 | let boardName = '' 129 | let boardNameEscaped = '' 130 | let userName = '' 131 | let userNameEscaped = '' 132 | const startTime = new Date() 133 | const entryTemplate = { 134 | images: [], 135 | title: null, 136 | link: null, 137 | description: null, 138 | note: null, 139 | sourceLink: null 140 | } 141 | 142 | function collectImages () { 143 | if (!collectActive) return 144 | if (url !== document.location.href) { 145 | // Reset on new page 146 | url = document.location.href 147 | entryList = [] 148 | } 149 | 150 | const masonry = document.querySelector('[data-test-id="board-feed"] .masonryContainer') 151 | if (!masonry) { 152 | return 153 | } 154 | const imgs = masonry.querySelectorAll('a[href^="/pin/"] img') 155 | for (let i = 0; i < imgs.length; i++) { 156 | if (imgs[i].clientWidth < 100) { 157 | // Skip small images, these are user profile photos 158 | continue 159 | } 160 | if (!('mouseOver' in imgs[i].dataset)) { 161 | // Fake mouse over to load source link 162 | const mouseOverEvent = new MouseEvent('mouseover', { 163 | bubbles: true, 164 | cancelable: true 165 | }) 166 | 167 | imgs[i].dispatchEvent(mouseOverEvent) 168 | imgs[i].dataset.mouseOver = true 169 | } 170 | 171 | const entry = Object.assign({}, entryTemplate) 172 | entry.images = [imgs[i].src.replace(/\/\d+x\//, '/originals/'), imgs[i].src] 173 | 174 | if (imgs[i].alt) { 175 | entry.description = imgs[i].alt 176 | } 177 | 178 | const pinWrapper = parentQuery(imgs[i], '[data-test-id="pinWrapper"]') || parentQuery(imgs[i], '[role="listitem"]') || parentQuery(imgs[i], '[draggable="true"]') 179 | if (pinWrapper) { 180 | // find metadata 181 | const aText = Array.from(pinWrapper.querySelectorAll('a[href*="/pin/"]')).filter(a => a.firstChild.nodeType === a.TEXT_NODE) 182 | if (aText.length > 0 && aText[0]) { 183 | entry.title = aText[0].textContent.trim() 184 | entry.link = aText[0].href.toString() 185 | } else if (pinWrapper.querySelector('a[href*="/pin/"]')) { 186 | entry.link = pinWrapper.querySelector('a[href*="/pin/"]').href.toString() 187 | } 188 | const aNotes = Array.from(pinWrapper.querySelectorAll('a[href*="/pin/"]')).filter(a => a.querySelector('div[title]')) 189 | if (aNotes.length > 0 && aNotes[0]) { 190 | entry.note = aNotes[0].textContent.trim() 191 | } 192 | 193 | if (pinWrapper.querySelector('[data-test-id="pinrep-source-link"] a')) { 194 | entry.sourceLink = pinWrapper.querySelector('[data-test-id="pinrep-source-link"] a').href.toString() 195 | } 196 | } 197 | 198 | if (imgs[i].srcset) { 199 | // 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" 200 | 201 | let goodUrl = false 202 | let quality = -1 203 | const srcset = imgs[i].srcset.split(', ') 204 | for (let j = 0; j < srcset.length; j++) { 205 | const pair = srcset[j].split(' ') 206 | const q = parseInt(pair[1].replace('x')) 207 | if (q > quality) { 208 | goodUrl = pair[0] 209 | quality = q 210 | } 211 | if (pair[0].indexOf('/originals/') !== -1) { 212 | break 213 | } 214 | } 215 | if (goodUrl && quality !== -1) { 216 | entry.images[0] = goodUrl 217 | } 218 | } 219 | 220 | let exists = false 221 | for (let j = 0; j < entryList.length; j++) { 222 | if (entryList[j].images[0] === entry.images[0] && entryList[j].images[1] === entry.images[1]) { 223 | exists = true 224 | entryList[j] = entry // replace with newer entry 225 | break 226 | } 227 | } 228 | if (!exists) { 229 | entryList.push(entry) 230 | console.debug(imgs[i].parentNode) 231 | console.debug(entry) 232 | } 233 | } 234 | const statusImageCollector = document.getElementById('statusImageCollector') 235 | if (statusImageCollector) { 236 | statusImageCollector.innerHTML = `Collected ${entryList.length} images` 237 | } 238 | } 239 | 240 | function addButton () { 241 | if (document.querySelector('.downloadoriginal123button')) { 242 | return 243 | } 244 | 245 | if (document.querySelector('[data-test-id="board-tools"],[data-test-id="board-header"]') && document.querySelectorAll('[data-test-id="board-feed"] a[href^="/pin/"] img').length) { 246 | const button = document.createElement('div') 247 | button.type = 'button' 248 | button.classList.add('downloadoriginal123button') 249 | button.setAttribute('style', ` 250 | position: absolute; 251 | display: block; 252 | background: white; 253 | border: none; 254 | padding: 5px; 255 | text-align: center; 256 | cursor:pointer; 257 | `) 258 | button.innerHTML = ` 259 |
\u2B73
260 |
Download
originals
261 | ` 262 | button.addEventListener('click', prepareForDownloading) 263 | document.querySelector('[data-test-id="board-tools"],[data-test-id="board-header"]').appendChild(button) 264 | try { 265 | const buttons = document.querySelectorAll('[role="button"] a[href*="/more-ideas/"],[data-test-id="board-header"] [role="button"]') 266 | const rect = buttons[buttons.length - 1].getBoundingClientRect() 267 | button.style.top = rect.top - 2 + 'px' 268 | button.style.left = rect.left - rect.width + 300 + 'px' 269 | } catch (e) { 270 | console.warn(e) 271 | try { 272 | const title = document.querySelector('h1') 273 | const rect = title.getBoundingClientRect() 274 | button.style.top = rect.top - 2 + 'px' 275 | button.style.left = rect.left - 120 + 'px' 276 | } catch (e) { 277 | console.warn(e) 278 | } 279 | } 280 | } 281 | } 282 | 283 | GM.registerMenuCommand('Pinterest.com - backup originals', prepareForDownloading) 284 | addButton() 285 | window.setInterval(addButton, 1000) 286 | window.setInterval(collectImages, 400) 287 | 288 | function downloadOriginals () { 289 | try { 290 | boardName = document.querySelector('h1').textContent.trim() 291 | boardNameEscaped = boardName.replace(/[^a-z0-9]/gi, '_') 292 | } catch (e1) { 293 | try { 294 | boardName = document.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/').pop() 295 | boardNameEscaped = boardName.replace(/[^a-z0-9]/gi, '_') 296 | } catch (e2) { 297 | boardName = 'board-' + Math.random() 298 | boardNameEscaped = boardName 299 | } 300 | } 301 | try { 302 | userName = document.location.href.match(/\.(\w{2,3})\/(.*?)\//)[2] 303 | userNameEscaped = userName.replace(/[^a-z0-9]/gi, '_') 304 | } catch (e) { 305 | try { 306 | userName = document.location.pathname.replace(/^\//, '').replace(/\/$/, '').split('/').shift() 307 | userNameEscaped = userName.replace(/[^a-z0-9]/gi, '_') 308 | } catch (e2) { 309 | userName = 'user' 310 | userNameEscaped = userName 311 | } 312 | } 313 | 314 | collectImages() 315 | collectActive = false 316 | 317 | const lst = entryList.slice() 318 | 319 | const total = lst.length 320 | let zip = new JSZip() 321 | const fileNameSet = new Set() 322 | 323 | // Create folders 324 | const imagesFolder = zip.folder('images') 325 | const errorFolder = zip.folder('error_thumbnails') 326 | const markdownOut = [] 327 | const htmlOut = [] 328 | 329 | document.body.style.padding = '3%' 330 | document.body.innerHTML = '

' + (total - lst.length) + '/' + total + ' downloaded


(Keep this tab visible)
' + ' image download
total progress
'
331 |   document.scrollingElement.scrollTo(0, 0)
332 |   const pre = document.getElementById('statusmessage')
333 |   const statusbar = document.getElementById('status')
334 |   const totalbar = document.getElementById('total')
335 |   const h1 = document.getElementById('counter');
336 | 
337 |   (async function work () {
338 |     document.title = (total - lst.length) + '/' + total + ' downloaded'
339 |     h1.innerHTML = totalbar.value = total - lst.length
340 |     statusbar.removeAttribute('value')
341 |     statusbar.removeAttribute('max')
342 | 
343 |     if (lst.length === 0) {
344 |       document.title = 'Generating zip file...'
345 |       document.body.innerHTML = '

Generating zip file...

' 346 | } 347 | if (lst.length > 0) { 348 | const entry = lst.pop() 349 | const urls = entry.images 350 | let fileName = null 351 | const prettyFilename = (s) => safeFileName(s.substr(0, 200)).substr(0, 110).replace(/^[^\w]+/, '').replace(/[^\w]+$/, '') 352 | if (entry.title) { 353 | fileName = prettyFilename(entry.title) 354 | } else if (entry.description) { 355 | fileName = prettyFilename(entry.description) 356 | } else if (entry.note) { 357 | fileName = prettyFilename(entry.note) 358 | } else if (entry.sourceLink) { 359 | fileName = prettyFilename(entry.sourceLink.split('/').slice(3).join('-')) 360 | } 361 | 362 | if (!fileName) { 363 | fileName = urls[0].split('/').pop() 364 | } else { 365 | fileName = fileName + '.' + urls[0].split('/').pop().split('.').pop() 366 | } 367 | 368 | while (fileNameSet.has(fileName.toLowerCase())) { 369 | const parts = fileName.split('.') 370 | parts.splice(parts.length - 1, 0, parseInt(Math.random() * 10000).toString()) 371 | fileName = parts.join('.') 372 | } 373 | fileNameSet.add(fileName.toLowerCase()) 374 | 375 | pre.innerHTML = fileName 376 | GM.xmlHttpRequest({ 377 | method: 'GET', 378 | url: urls[0], 379 | responseType: 'arraybuffer', 380 | onload: async function (response) { 381 | const s = String.fromCharCode.apply(null, new Uint8Array(response.response.slice(0, 125))) 382 | if (s.indexOf('') !== -1) { 383 | // Download thumbnail to error folder 384 | if (!('isError' in entry) || !entry.isError) { 385 | const errorEntry = Object.assign({}, entry) 386 | errorEntry.images = [urls[1]] 387 | errorEntry.isError = true 388 | // TODO change title? of error entry 389 | lst.push(errorEntry) 390 | } 391 | } else { 392 | // Save file to zip 393 | entry.fileName = fileName 394 | entry.fileNameUrl = markdownEncodeURIComponent(fileName) 395 | if (!('isError' in entry) || !entry.isError) { 396 | imagesFolder.file(fileName, response.response) 397 | entry.filePath = 'images/' + fileName 398 | entry.fileUrl = 'images/' + entry.fileNameUrl 399 | await addMetadata('successful', entry, htmlOut, markdownOut) 400 | } else { 401 | errorFolder.file(fileName, response.response) 402 | entry.filePath = 'error_thumbnails/' + fileName 403 | entry.fileUrl = 'error_thumbnails/' + entry.fileNameUrl 404 | await addMetadata('error', entry, htmlOut, markdownOut) 405 | } 406 | } 407 | 408 | work() 409 | }, 410 | onprogress: function (progress) { 411 | try { 412 | statusbar.max = progress.total 413 | statusbar.value = progress.loaded 414 | } catch (e) { } 415 | }, 416 | onerror: async function (response) { 417 | console.error('Error downloading image:', response) 418 | entry.filePath = '' 419 | entry.fileUrl = 'https://github.com/cvzi/pinterest-Backup-Original-Files/blob/master/error.svg' 420 | entry.note = 'Failed to download from: \'' + urls[0] + '\': ' + ('error' in response ? response.error : response.toString()) 421 | await addMetadata('error', entry, htmlOut, markdownOut) 422 | 423 | work() 424 | } 425 | }) 426 | } else { 427 | // Create html and markdown overview 428 | htmlOut.unshift(` 429 | 438 | 439 |

${escapeXml(boardName)}

440 |

441 | ${escapeXml(userName)} 442 |
443 | : 446 | ${escapeXml(document.location.href)} 447 |

448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | `) 459 | htmlOut.push('
TitleImagePinterestSourceDescriptionNotes
') 460 | zip.file('index.html', htmlOut.join('\n')) 461 | markdownOut.unshift(` 462 | # ${escapeMD(boardName)} 463 | 464 | ### ${escapeXml(userName)} 465 | 466 | ${startTime.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}: ${document.location.href} 467 | 468 | | Title | Image | Pinterest | Source | Description | Notes | 469 | |---|---|---|---|---|---|`) 470 | 471 | zip.file('README.md', markdownOut.join('\n')) 472 | 473 | // Done. Open ZIP file 474 | let zipfilename 475 | try { 476 | const d = startTime || new Date() 477 | zipfilename = userNameEscaped + '_' + boardNameEscaped + '_' + d.getFullYear() + '-' + ((d.getMonth() + 1) > 9 ? '' : '0') + (d.getMonth() + 1) + '-' + (d.getDate() > 9 ? '' : '0') + d.getDate() + 478 | '_' + (d.getHours() > 9 ? '' : '0') + d.getHours() + '-' + (d.getMinutes() > 9 ? '' : '0') + d.getMinutes() 479 | } catch (e) { 480 | zipfilename = 'board' 481 | } 482 | zipfilename += '.zip' 483 | const content = await zip.generateAsync({ type: 'blob' }) // TODO catch errors 484 | zip = null 485 | const h = document.createElement('h1') 486 | h.appendChild(document.createTextNode('Click here to Download')) 487 | h.style = 'cursor:pointer; color:blue; background:white; text-decoration:underline' 488 | document.body.appendChild(h) 489 | const genZipProgress = document.getElementById('gen_zip_progress') 490 | if (genZipProgress) { 491 | genZipProgress.remove() 492 | } 493 | h.addEventListener('click', function () { 494 | saveAs(content, zipfilename) 495 | }) 496 | saveAs(content, zipfilename) 497 | } 498 | })() 499 | } 500 | 501 | function addMetadata (status, e, htmlOut, markdownOut) { 502 | return new Promise((resolve) => { 503 | writeMetadata(status, e, htmlOut, markdownOut) 504 | resolve() 505 | }) 506 | } 507 | 508 | function writeMetadata (status, entry, htmlOut, markdownOut) { 509 | // XML escape all values for html 510 | const entryEscaped = Object.fromEntries(Object.entries(entry).map(entry => { 511 | const escapedValue = escapeXml(entry[1]) 512 | return [entry[0], escapedValue] 513 | })) 514 | 515 | // Shorten source link title 516 | let sourceA = '' 517 | if (entry.sourceLink) { 518 | let sourceTitle = decodeURI(entry.sourceLink) 519 | if (sourceTitle.length > 160) { 520 | sourceTitle = sourceTitle.substring(0, 155) + '\u2026' 521 | } 522 | sourceA = `${escapeXml(sourceTitle)}` 523 | } 524 | 525 | // HTML table entry 526 | htmlOut.push(` 527 | 528 | ${entryEscaped.title || entryEscaped.description || entryEscaped.fileName} 530 | 531 | 532 | ${entryEscaped.description || entryEscaped.filePath} 533 | 534 | 535 | 536 | ${entryEscaped.link} 537 | 538 | 539 | ${sourceA} 540 | 541 | ${entryEscaped.description} 542 | ${entryEscaped.note} 543 | 544 | `) 545 | 546 | // Shorten source link title 547 | let sourceLink = entry.sourceLink || '' 548 | if (entry.sourceLink) { 549 | let sourceTitle = decodeURI(entry.sourceLink) 550 | if (sourceTitle.length > 160) { 551 | sourceTitle = sourceTitle.substring(0, 155) + '\u2026' 552 | } 553 | sourceLink = `[${escapeMD(sourceTitle)}](${entry.sourceLink})` 554 | } 555 | 556 | // Markdown 557 | markdownOut.push(`| ${escapeMD(entry.title || entry.description || entry.fileName)}` + 558 | ` | ![${escapeMD(entry.description || entry.fileName)}](${entry.fileUrl})` + 559 | ` | ${entry.link || ''}` + 560 | ` | ${sourceLink}` + 561 | ` | ${escapeMD(entry.description || '')}` + 562 | ` | ${escapeMD(entry.note || '')}` + ' |') 563 | } 564 | 565 | function parentQuery (node, q) { 566 | const parents = [node.parentElement] 567 | node = node.parentElement.parentElement 568 | while (node) { 569 | const lst = node.querySelectorAll(q) 570 | for (let i = 0; i < lst.length; i++) { 571 | if (parents.indexOf(lst[i]) !== -1) { 572 | return lst[i] 573 | } 574 | } 575 | parents.push(node) 576 | node = node.parentElement 577 | } 578 | return null 579 | } 580 | 581 | function safeFileName (s) { 582 | const blacklist = /[<>:'"/\\|?*\u0000\n\r\t]/g // eslint-disable-line no-control-regex 583 | s = s.replace(blacklist, ' ').trim().replace(/^\.+/, '').replace(/\.+$/, '') 584 | return s.replace(/\s+/g, ' ').trim() 585 | } 586 | 587 | function escapeXml (unsafe) { 588 | // https://stackoverflow.com/a/27979933/ 589 | const s = (unsafe || '').toString() 590 | return s.replace(/[<>&'"\n\t]/gim, function (c) { 591 | switch (c) { 592 | case '<': return '<' 593 | case '>': return '>' 594 | case '&': return '&' 595 | case '\'': return ''' 596 | case '"': return '"' 597 | case '\n': return '
' 598 | case '\t': return ' ' 599 | } 600 | }) 601 | } 602 | 603 | function escapeMD (unsafe) { 604 | // Markdown escape 605 | const s = (unsafe || '').toString() 606 | return s.replace(/\W/gim, function (c) { 607 | switch (c) { 608 | case '<': return '<' 609 | case '>': return '>' 610 | case '&': return '&' 611 | case '\'': return '\\\'' 612 | case '"': return '\\"' 613 | case '*': return '\\*' 614 | case '[': return '\\[' 615 | case ']': return '\\]' 616 | case '(': return '\\(' 617 | case ')': return '\\)' 618 | case '{': return '\\{' 619 | case '}': return '\\}' 620 | case '`': return '\\`' 621 | case '!': return '\\!' 622 | case '|': return '\\|' 623 | case '#': return '\\#' 624 | case '+': return '\\+' 625 | case '-': return '\\-' 626 | case '\r': return ' ' 627 | case '\n': return '
' 628 | default: return c 629 | } 630 | }).trim() 631 | } 632 | 633 | function markdownEncodeURIComponent (s) { 634 | return encodeURIComponent(s).replace(/[[\](){}`!]/g, function (c) { 635 | switch (c) { 636 | case '[': return '%5B' 637 | case ']': return '%5D' 638 | case '(': return '%28' 639 | case ')': return '%29' 640 | case '{': return '%7B' 641 | case '}': return '%7D' 642 | case '`': return '%60' 643 | case '!': return '%21' 644 | } 645 | }) 646 | } 647 | --------------------------------------------------------------------------------