MediaWiki:Common.js
Appearance
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 '';
}
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();
});
});
})();