SWDD - Steam Workshop Description Downloader

Adds buttons to download Steam Workshop descriptions in .MD and .BBCode format.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==

// @name                SWDD - Steam Workshop Description Downloader
// @namespace           https://criskkky.carrd.co/
// @version             1.0.3
// @description         Adds buttons to download Steam Workshop descriptions in .MD and .BBCode format.
// @description:en      Adds buttons to download Steam Workshop descriptions in .MD and .BBCode format.
// @description:es      Añade botones para descargar descripciones de la Workshop de Steam en formato .MD y .BBCode.
// @description:pt      Adiciona botões para baixar descrições da Workshop do Steam em formato .MD e .BBCode.
// @description:fr      Ajoute des boutons pour télécharger les descriptions de la Workshop Steam au format .MD et .BBCode.
// @description:it      Aggiunge pulsanti per scaricare le descrizioni della Workshop di Steam in formato .MD e .BBCode.
// @description:uk      Додає кнопки для завантаження описів Workshop Steam у форматі .MD та .BBCode.
// @description:ru      Добавляет кнопки для загрузки описаний Workshop Steam в формате .MD и .BBCode.
// @description:ro      Adaugă butoane pentru a descărca descrierile Workshop-ului Steam în format .MD și .BBCode.

// @author              https://criskkky.carrd.co/
// @supportURL          https://github.com/criskkky/SWDD/issues
// @homepageURL         https://github.com/criskkky/SWDD/
// @icon                https://raw.githubusercontent.com/criskkky/SWDD/stable/static/icons/swdd.png
// @copyright           https://github.com/criskkky/SWDD/tree/stable?tab=readme-ov-file#license
// @license             https://github.com/criskkky/SWDD/tree/stable?tab=readme-ov-file#license

// @grant               none
// @match               https://steamcommunity.com/sharedfiles/filedetails/*
// ==/UserScript==

/*
I RECOMMEND TAKE A LOOK TO SUPPORTED SYNTAX CONVERSIONS HERE:
https://github.com/criskkky/SWDD?tab=readme-ov-file#supported-conversions

ANYWAYS, YOU CAN HELP ME TO IMPROVE THIS SCRIPT
BY DOING A PULL REQUEST OR OPENING AN ISSUE ON GITHUB :D
https://github.com/criskkky/SWDD

AVOID USING OFUSCATED CODE OR LIBS, PLEASE,
OR YOUR PULL REQUEST WILL BE REJECTED. THANKS!
*/

// Function to download content as a file
function downloadContent(content, fileName) {
  var blob = new Blob([content], { type: 'text/plain' });
  var link = document.createElement('a');
  link.href = window.URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
}

function getDescription() {
  var descriptionElement = document.querySelector('.workshopItemDescription');
  if (descriptionElement) {
    // Get the HTML content of the description
    var descriptionHTML = descriptionElement.innerHTML;

    return descriptionHTML;
  }
  return null;
}

function getHTMLtoBBC(descriptionHTML) {
  // Custom replacements for Steam HTML to BBCode conversion
  var bbReplacements = {
    // Essential
    '<br>': '\n',
    '<span class="bb_link_host">([\\s\\S]*?)<\/span>': '',
    '<span>([\\s\\S]*?)<\/span>': '$1',
    // Headers
    '<div class="bb_h1">([^<]+)<\/div>': '[h1]$1[/h1]\n',
    '<div class="bb_h2">([^<]+)<\/div>': '[h2]$1[/h2]\n',
    '<div class="bb_h3">([^<]+)<\/div>': '[h3]$1[/h3]\n',
    // Font styling
    '<b>([\\s\\S]*?)<\/b>': '[b]$1[/b]',
    '<u>([\\s\\S]*?)<\/u>': '[u]$1[/u]',
    '<i>([\\s\\S]*?)<\/i>': '[i]$1[/i]',
    // Font formatting
    '<span class="bb_strike">([\\s\\S]*?)<\/span>': '[strike]$1[/strike]',
    '<span class="bb_spoiler">([\\s\\S]*?)<\/span>': '[spoiler]$1[/spoiler]',
    '<a[^>]*class="bb_link"[^>]*href="([^"]+)"(?:[^>]*target="([^"]+)")?(?:[^>]*rel="([^"]+)")?[^>]*>([\\s\\S]*?)<\/a>': '[url=$1]$4[/url]',
    // Lists
    '<ul class="bb_ul">([\\s\\S]*?)<\/ul>': '\n[list]\n$1\n[/list]',
    '<li>([\\s\\S]*?)<\/li>': '[*]$1',
    '<ol>([\\s\\S]*?)<\/ol>': '\n[olist]\n$1\n[/olist]',
    // Font formatting
    '<div class="bb_code">([\\s\\S]*?)<\/div>': '\n[code]\n$1[/code]\n',
    // TODO: Fix noparse. It's not working properly. Do PR if you can fix it.
    // Tables
    // TODO: Fix bb_table. It's not working properly. Do PR if you can fix it.
    '<div class="bb_table_tr">([\\s\\S]*?)<\/div>': '\n[tr]\n$1\n[/tr]\n',
    '<div class="bb_table_th">([\\s\\S]*?)<\/div>': '[th]$1[/th]',
    '<div class="bb_table_td">([\\s\\S]*?)<\/div>': '[td]$1[/td]',
    // Images
    '<img src="([^"]+)"[^>]*>': '[img]$1[/img]',
    // Others
    '<hr>': '[hr]',
    '<blockquote class="bb_blockquote">([\\s\\S]*?)</blockquote>' : '[quote]$1[/quote]',
  };

  // Apply custom replacements
  for (var pattern in bbReplacements) {
    var regex = new RegExp(pattern, 'gi');
    descriptionHTML = descriptionHTML.replace(regex, bbReplacements[pattern]);
  }

  // Clear unsupported tags
  descriptionHTML = descriptionHTML.replace(/<(?!\/?(h1|h2|h3|b|u|i|strike|spoiler|ul|li|ol|code|tr|th|td|img|hr|blockquote|\/blockquote))[^>]+>/g, '');

  var bbcodeContent = descriptionHTML;

  return bbcodeContent;
}

function getHTMLtoMD(descriptionHTML) {
  // Custom replacements for Steam HTML to Markdown conversion
  var mdReplacements = {
    // Essential
    '<br>': '\n',
    '<span class="bb_link_host">([\\s\\S]*?)<\/span>': '',
    '<span>([\\s\\S]*?)<\/span>': '$1',
    // Headers
    '<div class="bb_h1">([\\s\\S]*?)<\/div>': '# $1\n',
    '<div class="bb_h2">([\\s\\S]*?)<\/div>': '## $1\n',
    '<div class="bb_h3">([\\s\\S]*?)<\/div>': '### $1\n',
    // Font styling
    '<b>([\\s\\S]*?)<\/b>': '**$1**',
    '<u>([\\s\\S]*?)<\/u>': '__$1__',
    '<i>([\\s\\S]*?)<\/i>': '*$1*',
    // Font formatting
    '<span class="bb_strike">([\\s\\S]*?)<\/span>': '~~$1~~',
    '<span class="bb_spoiler">([\\s\\S]*?)<\/span>': '<details><summary>Spoiler</summary>$1</details>',
    '<a[^>]*class="bb_link"[^>]*href="([^"]+)"(?:[^>]*target="([^"]+)")?(?:[^>]*rel="([^"]+)")?[^>]*>([\\s\\S]*?)<\/a>': '[$4]($1)',
    // Lists
    '<li>([\\s\\S]*?)<\/li>': '* $1',
    '<ol>([\\s\\S]*?)<\/ol>': (match, p1, offset, string) => {
      const lines = p1.trim().split('\n');
      let currentIndex = 1;
    
      const formattedLines = lines.map((line, index) => {
        // Check if the current line starts a new ordered list
        const isNewList = line.trim().startsWith('<li>');
    
        // If it's a new list, reset currentIndex to 1
        if (isNewList) {
          currentIndex = 1;
        }
    
        // Replace asterisks (*) or hyphens (-) with incremental numbers
        return line.replace(/^\s*[\*\-]/, () => {
          const updatedNumber = currentIndex++;
          return `${updatedNumber}.`;
        });
      });
    
      return `<ol>\n${formattedLines.join('\n')}\n</ol>`;
    },        
    // Font formatting
    '<div class="bb_code">([\\s\\S]*?)<\/div>': '\n```\n$1\n```\n',
    // TODO: Fix noparse. It's not working properly. Do PR if you can fix it.
    // Tables
    // TODO: Fix bb_table. It's not working properly. Do PR if you can fix it.
    // TODO: Fix bb_table_tr. It's not working properly. Do PR if you can fix it.
    // TODO: Fix bb_table_th. It's not working properly. Do PR if you can fix it.
    // TODO: Fix bb_table_td. It's not working properly. Do PR if you can fix it.
    // Images
    '<img src="([^"]+)"[^>]*>': '![image]($1)',
    // Others
    '<hr>': '---',
    '<blockquote class="bb_blockquote">([\\s\\S]*?)</blockquote>' : '> $1',
  };

  // Apply custom replacements
  for (var pattern in mdReplacements) {
    var regex = new RegExp(pattern, 'gi');
    descriptionHTML = descriptionHTML.replace(regex, mdReplacements[pattern]);
  }

  // Clear unsupported tags except for <details><summary>Spoiler</summary>$1</details>
  descriptionHTML = descriptionHTML.replace(/<(?!details><summary>Spoiler<\/summary>\$1<\/details>)[^>]+>/g, '');

  var markdownContent = descriptionHTML;

  return markdownContent;
}

function insertButton(downloadButton) {
  var fixedMargin = document.querySelector('.game_area_purchase_margin');
  if (fixedMargin) {
    // Better alignment ...
    fixedMargin.style.marginBottom = 'auto';
    // Find the element after which the button should be inserted
    var targetElement = document.querySelector('.workshopItemDescription');
    if (targetElement) {
      // Insert the button after the target element
      targetElement.parentNode.insertBefore(downloadButton, targetElement.nextSibling);
    }
  }
}
// Create go to repo button
function createGoToRepoButton() {
  var goToRepoButton = document.createElement('a');
  goToRepoButton.innerHTML = '<img src="https://raw.githubusercontent.com/criskkky/SWDD/stable/static/icons/github_line.png" style="vertical-align: middle; margin-right: 5px; margin-left: -4px; max-width: 20px; max-height: 20px;">Repository';
  goToRepoButton.classList.add('btn_darkblue_white_innerfade', 'btn_border_2px', 'btn_medium');
  goToRepoButton.style.marginBottom = '5px';
  goToRepoButton.style.marginRight = '5px';
  goToRepoButton.style.padding = '5px 10px';
  goToRepoButton.style.height = '21px';
  goToRepoButton.style.fontSize = '14px';
  goToRepoButton.href = 'https://github.com/criskkky/SWDD';
  goToRepoButton.target = '_blank';

  insertButton(goToRepoButton);
}

// Create the download button for Markdown
function createDownloadButtonMD() {
  var downloadButton = document.createElement('button');
  downloadButton.innerHTML = '<img src="https://raw.githubusercontent.com/criskkky/SWDD/adb175e273c563b3ffad4e81e7afe7f76449fd04/static/icons/cloud-download-white.svg" style="vertical-align: middle; margin-right: 5px; margin-left: -6px; max-width: 20px; max-height: 20px;">Download .MD';
  downloadButton.classList.add('btn_green_white_innerfade', 'btn_border_2px', 'btn_medium');
  downloadButton.style.marginBottom = '5px';
  downloadButton.style.marginRight = '5px';
  downloadButton.style.padding = '5px 10px';
  downloadButton.style.height = '34.43px';
  downloadButton.style.fontSize = '14px';
  downloadButton.addEventListener('click', function () {
    var markdownContent = getHTMLtoMD(getDescription());
    if (markdownContent) {
      downloadContent(markdownContent, 'WorkshopDownload.md');
    } else {
      alert('No content found in Markdown format.');
    }
  });

  insertButton(downloadButton);
}

// Create the download button for BBCode
function createDownloadButtonBBC() {
  var downloadButton = document.createElement('button');
  downloadButton.innerHTML = '<img src="https://raw.githubusercontent.com/criskkky/SWDD/adb175e273c563b3ffad4e81e7afe7f76449fd04/static/icons/cloud-download-white.svg" style="vertical-align: middle; margin-right: 5px; margin-left: -6px; max-width: 20px; max-height: 20px;">Download .BBCode';
  downloadButton.classList.add('btn_green_white_innerfade', 'btn_border_2px', 'btn_medium');
  downloadButton.style.marginBottom = '5px';
  downloadButton.style.marginRight = '5px';
  downloadButton.style.padding = '5px 10px';
  downloadButton.style.height = '34.43px';
  downloadButton.style.fontSize = '14px';
  downloadButton.addEventListener('click', function () {
    var bbcodeContent = getHTMLtoBBC(getDescription());
    if (bbcodeContent) {
      downloadContent(bbcodeContent, 'WorkshopDownload.bbcode');
    } else {
      alert('No content found in BBCode format.');
    }
  });

  insertButton(downloadButton);
}

// Execute the functions when the page loads
window.addEventListener('load', function () {
  createGoToRepoButton();
  createDownloadButtonMD();
  createDownloadButtonBBC();
});