$.when( mw.loader.using( 'mediawiki.util' ), $.ready ).then( function () {
// Cease and desist when outside of article space
if ( mw.config.get( 'wgCanonicalNamespace' ) !== '') return;
mcss = 3; // minimum common substring to count
numEggs = 0; // will exclude "hidden" category
marked = false; // flag to keep from double-marking
// Recursively find total length of *ordered* common sequences using
// a greedy approach that always selects the longest unused sequence first
//
// xs -> x_start, xe -> e_end, x_i -> x_iterator, mx -> (position of) max_x
// same pattern with y vars
var inCommon = function(mat, xs, xe, ys, ye) {
if (xe - xs < mcss || ye - ys < mcss) return 0;
// set up contextual vars (to avoid clobbering)
var max = mat[xs][ys], mx = xs, my = ys;
for (xi = xs; xi < xe; xi++) {
for (yi = ys; yi < ye; yi++) {
if (mat[xi][yi] > max) {
max = mat[xi][yi];
mx = xi;
my = yi;
}
}
}
return (max < mcss) ? 0 : max + inCommon(mat, xs, mx-max+1, ys, my-max+1) + inCommon(mat, mx+1, xe, my+1, ye);
};
// Score a link using longest common substring as a percent of the length of the shortest string
var strScore = function(short, long) {
if (typeof(short) != "string" || typeof(long) != "string") return 0;
shortA = new Array(short.length);
for (s=0; s<short.length; s++) {
shortA[s] = new Array(long.length);
shortA[s][0] = short[s] == long[0] ? 1 : 0;
}
for (l=0; l<long.length; l++) {
shortA[0][l] = short[0] == long[l] ? 1 : 0;
}
for (s=1; s<short.length; s++) {
for (l=1; l<long.length; l++) {
if (short[s] == long[l]) {
shortA[s][l] = 1 + shortA[s-1][l-1];
} else {
shortA[s][l] = 0;
}
}
}
sharedSeqs = inCommon(shortA, 0, shortA.length, 0, shortA[0].length);
return sharedSeqs / Math.min(short.length, long.length);
//return shortA.flat().reduce(function(a,b){return Math.max(a,b);}, 0) / Math.min(short.length, long.length);
};
// Categories and scoring
var egg_cats = [
{min: 0.85, mark:'' , cnt: 0, name:'hidden'},
{min: 0.40, mark:'🥚', cnt: 0, name:'unlikely'},
{min: 0.20, mark:'🐣', cnt: 0, name:'possible'},
{min: 0.02, mark:'🐥', cnt: 0, name:'probable'},
{min: 0.00, mark:'🐤', cnt: 0, name:'unmatched'},
];
// Filter level 1: links in paragraph tags of the article content
$("#mw-content-text p a").filter(
function(idx, el) {
// Filter level 2: links must have title attribute, visible text, and a target beginning with /wiki/....
// (mostly so we can use these assumptions later)
return $(this).attr("title") && $(this).text() && (!$(this).attr("href").indexOf("/wiki/"));
}
).filter(
function(idx, el) {
// Filter level 3: Remove inline timeplates
return $(this).parents(".Inline-Template").length == 0;
}
).each(
function(idx, el) {
// build a lower case text and a title with any disambig clarifiers removed (trailing parens)
loc = $(this).attr("title").search(/[ _]\(.+\)$/);
title_lc = (loc >= 0) ? $(this).attr("title").substr(0,loc).toLowerCase() : $(this).attr("title").toLowerCase();
text_lc = $(this).text().toLowerCase();
// short-circuit if either the title or link text is fully contained in the other (not EGG)
if (text_lc.indexOf(title_lc) >= 0 || title_lc.indexOf(text_lc) >= 0 ) return 1;
// otherwise report possible EGG
score = strScore(text_lc, title_lc);
for (c = 0; c<egg_cats.length; c++) {
if (score >= egg_cats[c].min) {
egg_cats[c].cnt++;
$(this).addClass("eggHunt-"+egg_cats[c].name);
break;
}
}
console.log('[' + score.toFixed(3) + '], "' + $(this).text() + '", "' + $(this).attr("title") + '"');
numEggs++;
}
);
// remove count of hidden eggs
numEggs -= egg_cats[0].cnt;
// Install UI hook
var node = mw.util.addPortletLink('p-cactions', "#", numEggs + ' possible 🥚s', 'ca-egghunt', 'Tag '+numEggs+' possible EASTEREGGs in article');
$(node).on('click', function(e) {
if (!marked) {
for (c=0; c<egg_cats.length; c++) {
$("a.eggHunt-"+egg_cats[c].name).after(egg_cats[c].mark);
}
marked = true;
}
return false;
});
});