Jump to content

MediaWiki:Common.js

From Resco's Wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
/**
 * Open external links in the sidebar in a new window
 *
 * @source mediawiki.org/wiki/Snippets/Open_external_sidebar_links_in_new_window
 * @version 1
 */
$( '#mw-panel, #panel' ) // #panel is for pre-1.17 compatibility
	.find( 'li a' )
	.filter( "[href^='http://'], [href^='https://']" )
	.attr( 'target' , '_blank' );


/* Export page content as Markdown - add to MediaWiki:Common.js or a gadget */
( function () {
  'use strict';

  // ---- CONFIG ----
  var buttonText = 'Export as Markdown';
  // Set to true to auto-load Turndown from CDN (recommended for better fidelity).
  var tryLoadTurndown = true;
  var turndownCdn = 'https://cdn.jsdelivr.net/npm/turndown/dist/turndown.min.js';

  // ---- UTIL ----
  function $(sel, root) { return (root || document).querySelector(sel); }
  function $all(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }
  function onReady(cb) {
    if (document.readyState !== 'loading') cb();
    else document.addEventListener('DOMContentLoaded', cb);
  }

// ---- UI: add link into Vector sidebar (Tools menu or Actions menu) ----
function injectButton() {
  // Choose ONE of the following two lines:

  // 1) Add to "Tools" sidebar section (p-tb) — most common:
  var link = mw.util.addPortletLink(
    'p-tb',
    '#',
    'Export as Markdown',
    'md-export-link',
    'Convert this page to Markdown and view/copy it'
  );

  // 2) OR add to "Actions" menu (p-cactions) instead:
  // var link = mw.util.addPortletLink(
  //   'p-cactions',
  //   '#',
  //   'Export as Markdown',
  //   'md-export-link',
  //   'Convert this page to Markdown and view/copy it'
  // );

  link.addEventListener('click', function (e) {
    e.preventDefault();
    handleExportClick();
  });
}

  // ---- Modal ----
  function showModal(markdown) {
    // remove old modal if exists
    var old = document.getElementById('md-export-modal');
    if (old) old.remove();

    var overlay = document.createElement('div');
    overlay.id = 'md-export-modal';
    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1300;display:flex;align-items:center;justify-content:center;padding:20px;';
    var box = document.createElement('div');
    box.style.cssText = 'width:min(1100px,100%);height:min(80vh,900px);background:#fff;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.3);display:flex;flex-direction:column;overflow:hidden;';

    var header = document.createElement('div');
    header.style.cssText = 'padding:10px 14px;border-bottom:1px solid #eee;display:flex;align-items:center;gap:8px;';
    var title = document.createElement('strong'); title.textContent = 'Markdown export';
    var info = document.createElement('span'); info.style.cssText = 'color:#666;font-size:13px;margin-left:6px;'; info.textContent = ' — editable';
    header.appendChild(title); header.appendChild(info);

    var toolbar = document.createElement('div');
    toolbar.style.cssText = 'margin-left:auto;display:flex;gap:8px;align-items:center;';
    var copyBtn = document.createElement('button');
    copyBtn.textContent = 'Copy';
    copyBtn.style.cssText = 'padding:6px 10px;border-radius:6px;border:0;cursor:pointer;background:#2e7d32;color:#fff';
    var downloadBtn = document.createElement('button');
    downloadBtn.textContent = 'Download .md';
    downloadBtn.style.cssText = 'padding:6px 10px;border-radius:6px;border:0;cursor:pointer;background:#1565c0;color:#fff';
    var closeBtn = document.createElement('button');
    closeBtn.textContent = 'Close';
    closeBtn.style.cssText = 'padding:6px 10px;border-radius:6px;border:0;cursor:pointer;background:#eee;';
    toolbar.appendChild(copyBtn); toolbar.appendChild(downloadBtn); toolbar.appendChild(closeBtn);
    header.appendChild(toolbar);

    var textarea = document.createElement('textarea');
    textarea.value = markdown;
    textarea.style.cssText = 'flex:1;padding:12px;border:0;resize:vertical;font-family:Menlo,monospace,monospace;font-size:13px;line-height:1.45;';

    var footer = document.createElement('div');
    footer.style.cssText = 'padding:8px 12px;border-top:1px solid #eee;font-size:12px;color:#666;';
    footer.textContent = 'Note: Markdown generation is best-effort. For complex pages you may prefer to enable Turndown (better fidelity).';

    box.appendChild(header);
    box.appendChild(textarea);
    box.appendChild(footer);
    overlay.appendChild(box);
    document.body.appendChild(overlay);

    // events
    closeBtn.addEventListener('click', function () { overlay.remove(); });
    overlay.addEventListener('click', function (ev) { if (ev.target === overlay) overlay.remove(); });
    copyBtn.addEventListener('click', function () {
      copyToClipboard(textarea.value).then(function () {
        copyBtn.textContent = 'Copied ✓';
        setTimeout(function () { copyBtn.textContent = 'Copy'; }, 1800);
      }).catch(function () {
        alert('Copy failed — please select and copy manually.');
      });
    });
    downloadBtn.addEventListener('click', function () {
      var blob = new Blob([textarea.value], { type: 'text/markdown;charset=utf-8' });
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = url;
      a.download = document.title.replace(/\s+/g, '_') + '.md';
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
    });
  }

  function copyToClipboard(text) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text);
    }
    return new Promise(function (resolve, reject) {
      var ta = document.createElement('textarea');
      ta.value = text; document.body.appendChild(ta);
      ta.select();
      try { document.execCommand('copy'); document.body.removeChild(ta); resolve(); }
      catch (e) { document.body.removeChild(ta); reject(e); }
    });
  }

  // ---- Export click handler ----
  function handleExportClick() {
    var contentEl = document.getElementById('mw-content-text') || document.querySelector('.mw-parser-output') || document.querySelector('#content');
    if (!contentEl) { alert('Could not find page content to export.'); return; }
    var html = contentEl.innerHTML;

    // prefer Turndown if available
    if (window.TurndownService) {
      try {
        var turndownService = new window.TurndownService({ codeBlockStyle: 'fenced' });
        var md = turndownService.turndown(html);
        showModal(md);
        return;
      } catch (e) { console.warn('Turndown failed, falling back to builtin converter', e); }
    }

    // fallback converter
    var md = builtinHtmlToMarkdown(html);
    showModal(md);
  }

  // ---- Builtin HTML -> Markdown converter (reasonable, not perfect) ----
  function builtinHtmlToMarkdown(html) {
    var tmp = document.createElement('div');
    tmp.innerHTML = html;

    function escapeText(s) {
      // minimal escaping for markdown special characters in plain text
      return s.replace(/\r/g,'').replace(/\n{3,}/g,'\n\n').replace(/\t/g,' ').replace(/[ \u00A0]+/g,' ');
    }

    function nodeToMd(node, listLevel) {
      listLevel = listLevel || 0;
      if (node.nodeType === Node.TEXT_NODE) {
        return escapeText(node.nodeValue);
      }
      if (node.nodeType !== Node.ELEMENT_NODE) return '';

      var tag = node.tagName.toLowerCase();
      if (/^h[1-6]$/.test(tag)) {
        var level = parseInt(tag.charAt(1), 10);
        var prefix = '#'.repeat(level) + ' ';
        return '\n\n' + prefix + inlineChildren(node) + '\n\n';
      }
      if (tag === 'p') {
        return '\n\n' + inlineChildren(node) + '\n\n';
      }
      if (tag === 'br') return '  \n';
      if (tag === 'a') {
        var href = node.getAttribute('href') || '';
        var text = inlineChildren(node) || href;
        // if internal link like /wiki/.. keep as full path
        return '[' + text.trim() + '](' + href + ')';
      }
      if (tag === 'img') {
        var alt = node.getAttribute('alt') || '';
        var src = node.getAttribute('src') || node.getAttribute('data-src') || '';
        return '![' + alt + '](' + src + ')';
      }
      if (tag === 'strong' || tag === 'b') {
        return '**' + inlineChildren(node) + '**';
      }
      if (tag === 'em' || tag === 'i') {
        return '*' + inlineChildren(node) + '*';
      }
      if (tag === 'code' && node.parentElement && node.parentElement.tagName.toLowerCase() !== 'pre') {
        return '`' + node.textContent.trim() + '`';
      }
      if (tag === 'pre') {
        var codeChild = node.querySelector('code');
        var codeText = codeChild ? codeChild.textContent : node.textContent;
        return '\n\n```\n' + codeText.replace(/^\n+|\n+$/g,'') + '\n```\n\n';
      }
      if (tag === 'ul' || tag === 'ol') {
        var items = Array.from(node.children).filter(function (n) { return n.tagName && n.tagName.toLowerCase() === 'li'; });
        return '\n' + items.map(function (li) {
          var prefix = (tag === 'ul') ? '- ' : '1. ';
          var content = nodeToMd(li, listLevel + 1).replace(/\n/g, '\n' + '  '.repeat(listLevel + 1));
          return '  '.repeat(listLevel) + prefix + content.trim();
        }).join('\n') + '\n';
      }
      if (tag === 'li') {
        // concatenate inline children and block children
        var parts = [];
        node.childNodes.forEach(function (ch) {
          parts.push(nodeToMd(ch, listLevel));
        });
        return parts.join('').trim();
      }
      if (tag === 'table') {
        try {
          var rows = Array.from(node.querySelectorAll('tr'));
          if (rows.length === 0) return '';
          var matrix = rows.map(function (r) {
            return Array.from(r.children).map(function (cell) {
              var txt = inlineChildren(cell).replace(/\|/g, '|');
              return txt.trim();
            });
          });
          // header detection: first row contains th
          var header = rows[0].querySelectorAll('th').length > 0;
          var out = '\n\n';
          // header row
          out += '| ' + matrix[0].join(' | ') + ' |\n';
          if (header) {
            out += '| ' + matrix[0].map(function () { return '---'; }).join(' | ') + ' |\n';
            // remaining rows
            for (var r = 1; r < matrix.length; r++) {
              out += '| ' + matrix[r].join(' | ') + ' |\n';
            }
          } else {
            // no header: create a separator row using dashes
            out += '| ' + matrix[0].map(function () { return '---'; }).join(' | ') + ' |\n';
            for (var r2 = 1; r2 < matrix.length; r2++) {
              out += '| ' + matrix[r2].join(' | ') + ' |\n';
            }
          }
          out += '\n\n';
          return out;
        } catch (e) { console.warn('table conversion failed', e); return '\n\n<!-- table omitted: complex table -->\n\n'; }
      }
      if (tag === 'blockquote') {
        var inner = inlineChildren(node).split('\n').map(function (ln) { return '> ' + ln; }).join('\n');
        return '\n\n' + inner + '\n\n';
      }
      // generic block container: div, section, article, aside
      if (['div','section','article','main','aside'].indexOf(tag) !== -1) {
        return Array.from(node.childNodes).map(function (c) { return nodeToMd(c, listLevel); }).join('');
      }

      // fallback: inline children for span and unknown tags
      return inlineChildren(node);
    }

    function inlineChildren(node) {
      return Array.from(node.childNodes).map(function (ch) {
        return nodeToMd(ch);
      }).join('');
    }

    var result = inlineChildren(tmp).replace(/^\s+|\s+$/g, '');

    // normalize some whitespace
    result = result.replace(/\n{3,}/g, '\n\n');

    // add metadata header with title and source URL
    var title = document.querySelector('#firstHeading') ? document.querySelector('#firstHeading').textContent.trim() : document.title;
    var url = location.href;
    var header = '# ' + title + '\n\n' + '[Original page](' + url + ')\n\n---\n\n';
    return header + result;
  }

  // ---- optional: dynamic load Turndown from CDN ----
  function loadTurndownIfRequested(callback) {
    if (!tryLoadTurndown) { callback(); return; }
    if (window.TurndownService) { callback(); return; }
    var s = document.createElement('script');
    s.src = turndownCdn;
    s.onload = function () { callback(); };
    s.onerror = function () { console.warn('Failed to load Turndown from CDN'); callback(); };
    document.head.appendChild(s);
  }

  // ---- init ----
  onReady(function () {
    loadTurndownIfRequested(function () {
      injectButton();
    });
  });

})();