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.
/**
 * reFill.js
 * Fill bare / [url title] refs only.
 * Do not rename or edit existing named refs.
 * New duplicate URLs use the next free :N.
 */
(function () {
	'use strict';
	var CITOID = 'https://en.wikipedia.org/api/rest_v1/data/citation/mediawiki/';
	var TYPE_MAP = {
		blogPost: 'Cite web', webpage: 'Cite web', email: 'Cite web', forumPost: 'Cite web',
		newspaperArticle: 'Cite news', magazineArticle: 'Cite news',
		journalArticle: 'Cite journal', conferencePaper: 'Cite journal', presentation: 'Cite journal',
		book: 'Cite book', manuscript: 'Cite book', _default: 'Cite web'
	};

	function $box() { return $('#wpTextbox1'); }
	function getWikitext() {
		var $t = $box();
		if ($t.length && $.fn.textSelection) return $t.textSelection('getContents');
		return $t.length ? $t.val() : '';
	}
	function setWikitext(text) {
		var $t = $box();
		if ($t.length && $.fn.textSelection) { $t.textSelection('setContents', text); return; }
		if ($t.length) $t.val(text).trigger('input');
	}
	function setSummary(ok, fail) {
		var $s = $('#wpSummary');
		if (!$s.length) return;
		var note = 'Filled in ' + ok + ' bare reference' + (ok === 1 ? '' : 's') + ' with [[Bharatpedia:reFill|reFill]]';
		if (fail) note += ' (' + fail + ' failed)';
		var cur = ($s.val() || '').trim();
		if (!cur) $s.val(note);
		else if (cur.toLowerCase().indexOf('refill') === -1) $s.val(cur.replace(/\s*$/, '') + '; ' + note);
		$s.trigger('change');
	}
	function showDiff() {
		var btn = document.getElementById('wpDiff');
		if (btn) { btn.click(); return; }
		var form = document.getElementById('editform');
		if (!form) return;
		var input = document.createElement('input');
		input.type = 'hidden';
		input.name = 'wpDiff';
		input.value = 'Show changes';
		form.appendChild(input);
		form.submit();
	}
	function esc(s) { return String(s || '').replace(/\|/g, '{{!}}').replace(/\n/g, ' ').trim(); }
	function hostTitle(url) {
		try { return new URL(url).hostname.replace(/^www\./, ''); } catch (e) { return ''; }
	}
	function normUrl(url) { return String(url || '').trim().replace(/\/+$/, '').toLowerCase(); }

	function usedAutoNames(text) {
		var used = {}, maxN = -1, m;
		var re = /<ref[^>]*\bname\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>/]+))/gi;
		while ((m = re.exec(text)) !== null) {
			var n = m[1] || m[2] || m[3];
			used[n] = true;
			var a = String(n).match(/^:(\d+)$/);
			if (a) maxN = Math.max(maxN, parseInt(a[1], 10));
		}
		return { used: used, next: maxN + 1 };
	}

	function templateFromCitoid(item, fallbackUrl) {
		var name = TYPE_MAP[item && item.itemType] || TYPE_MAP._default;
		var p = [];
		var url = (item && item.url) || fallbackUrl;
		if (url) p.push('url=' + esc(url));
		var title = (item && item.title) || (url ? hostTitle(url) : '');
		if (title) p.push('title=' + esc(title));
		var site = item && (item.websiteTitle || item.publicationTitle || item.publisher);
		if (site) p.push((name === 'Cite news' || name === 'Cite journal' ? 'work=' : 'website=') + esc(site));
		if (item && item.date) p.push('date=' + esc(item.date));
		if (item && item.author && item.author[0]) {
			var a = item.author[0];
			if (a[1]) p.push('last=' + esc(a[1]));
			if (a[0]) p.push('first=' + esc(a[0]));
		}
		p.push('access-date=' + new Date().toISOString().slice(0, 10));
		return '{{' + name + '|' + p.join('|') + '}}';
	}

	function findBare(wikitext) {
		var out = [], m, seen = {};
		function add(full, attrs, url, index) {
			if (seen[index]) return;
			if (/\bname\s*=/i.test(attrs || '')) return;
			if (/\{\{\s*cite/i.test(full)) return;
			seen[index] = true;
			out.push({ full: full, url: url, index: index });
		}
		var reBare = /<ref(\s[^>]*)?>\s*(https?:\/\/[^\s<>[\]|{]+)\s*(?:\{\{[^}]+\}\})?\s*<\/ref>/gi;
		while ((m = reBare.exec(wikitext)) !== null) add(m[0], m[1], m[2], m.index);
		var reLink = /<ref(\s[^>]*)?>\s*\[(https?:\/\/[^\s\]]+)\s+[^\]]*\]\s*<\/ref>/gi;
		while ((m = reLink.exec(wikitext)) !== null) add(m[0], m[1], m[2], m.index);
		out.sort(function (a, b) { return a.index - b.index; });
		return out;
	}

	function fetchOne(url) {
		function once() {
			return fetch(CITOID + encodeURIComponent(url), { headers: { Accept: 'application/json' } })
				.then(function (r) {
					if (!r.ok) throw new Error('HTTP ' + r.status);
					return r.json();
				})
				.then(function (data) {
					var item = Array.isArray(data) ? data[0] : data;
					if (!item) throw new Error('empty');
					return item;
				});
		}
		return once().catch(function () {
			return new Promise(function (res) { setTimeout(res, 700); }).then(once);
		});
	}

	function run() {
		var text = getWikitext();
		if (!text) { window.alert('reFill: no editor text.'); return; }

		var names = usedAutoNames(text);
		var bare = findBare(text);
		if (!bare.length) {
			window.alert('reFill finished: 0 filled, 0 failed.');
			return;
		}

		var tab = document.querySelector('.tab-refill, [rel="refill"]');
		if (tab) tab.textContent = 'Filling…';

		var groups = {};
		var order = [];
		bare.forEach(function (item) {
			var key = normUrl(item.url);
			if (!groups[key]) {
				groups[key] = { url: item.url, count: 0, first: true, name: null };
				order.push(key);
			}
			groups[key].count++;
		});

		var next = names.next;
		order.forEach(function (key) {
			if (groups[key].count > 1) {
				while (names.used[':' + next]) next++;
				groups[key].name = ':' + next;
				names.used[':' + next] = true;
				next++;
			}
		});

		var metaByUrl = {};
		var chain = Promise.resolve();
		var fail = 0;
		order.forEach(function (key) {
			chain = chain.then(function () {
				return fetchOne(groups[key].url).then(function (meta) {
					metaByUrl[key] = meta;
				}).catch(function () {
					fail++;
					metaByUrl[key] = { url: groups[key].url };
				}).then(function () {
					return new Promise(function (res) { setTimeout(res, 350); });
				});
			});
		});

		chain.then(function () {
			var ok = 0;
			bare.forEach(function (item) {
				var key = normUrl(item.url);
				var g = groups[key];
				var meta = metaByUrl[key] || { url: item.url };
				var neu;
				if (!g.name) {
					neu = '<ref>' + templateFromCitoid(meta, item.url) + '</ref>';
				} else if (g.first) {
					g.first = false;
					neu = '<ref name="' + g.name + '">' + templateFromCitoid(meta, item.url) + '</ref>';
				} else {
					neu = '<ref name="' + g.name + '" />';
				}
				var pos = text.indexOf(item.full);
				if (pos !== -1) {
					text = text.slice(0, pos) + neu + text.slice(pos + item.full.length);
					ok++;
				}
			});
			setWikitext(text);
			setSummary(ok, fail);
			if (tab) tab.textContent = 'reFill';
			window.alert('reFill finished: ' + ok + ' filled, ' + fail + ' failed.');
			showDiff();
		});
	}

	function addSection($textarea) {
		if (!$textarea || !$textarea.length || typeof $textarea.wikiEditor !== 'function') return;
		if ($textarea.data('refillAdded')) return;
		try {
			$textarea.wikiEditor('addToToolbar', { sections: { refill: { type: 'toolbar', label: 'reFill' } } });
			$textarea.data('refillAdded', true);
		} catch (e) {}
	}
	function bindTab() {
		var root = document.querySelector('.wikiEditor-ui-toolbar');
		if (!root || root.getAttribute('data-refill-bound')) return;
		root.setAttribute('data-refill-bound', '1');
		root.addEventListener('click', function (e) {
			var t = e.target.closest('.tab-refill, [rel="refill"], .wikiEditor-ui-toolbar [rel="refill"]');
			if (!t) return;
			e.preventDefault();
			e.stopPropagation();
			run();
		}, true);
	}
	function start() {
		addSection($box());
		bindTab();
		if (mw.hook) mw.hook('wikiEditor.toolbarReady').add(function ($t) { addSection($t); bindTab(); });
		setTimeout(function () { addSection($box()); bindTab(); }, 800);
	}
	mw.loader.using(['jquery', 'jquery.textSelection', 'ext.wikiEditor']).then(start).catch(start);
})();