Empty search help; Biography wording
Add Internet Archive click-to-play embeds for interview watchlist
 
(21 intermediate revisions by the same user not shown)
Line 9: Line 9:
  * Hide retired “(merged duplicate)” roster pages from find/search suggestions and results.
  * Hide retired “(merged duplicate)” roster pages from find/search suggestions and results.
  * Fill soldier Teammates rails from live Category:Team … membership (no roster-module rebuild).
  * Fill soldier Teammates rails from live Category:Team … membership (no roster-module rebuild).
* Inject site hub links into the Minerva hamburger (Sidebar is not used by Minerva 1.43).
  */
  */
function rbIsMergedDuplicateTitle(title) {
function rbIsMergedDuplicateTitle(title) {
Line 15: Line 16:


$(function () {
$(function () {
(function injectMinervaSiteMenu() {
if (mw.config.get('skin') !== 'minerva') {
return;
}
var $drawer = $('#mw-mf-page-left');
var $after = $('#p-navigation');
if (!$drawer.length || !$after.length || $drawer.data('rbSiteMenu')) {
return;
}
$drawer.data('rbSiteMenu', 1);
var links = [
{ title: 'Contents', label: 'Contents', icon: 'listBullet' },
{ title: 'Start here', label: 'Start here', icon: 'lightbulb' },
{ title: 'Introduction', label: 'Introduction', icon: 'book' },
{ title: 'The Stories', label: 'Stories', icon: 'newspaper' },
{ title: 'Category:Classes', label: 'Classes', icon: 'userGroup' },
{ title: 'Photos', label: 'Photos', icon: 'imageGallery' },
{ title: 'Training materials', label: 'Training materials', icon: 'articles' },
{ title: 'Contribute', label: 'Contribute', icon: 'edit' },
{ title: 'Contact Us', label: 'Contact Us', icon: 'speechBubbles' },
{ title: 'About', label: 'About', icon: 'infoFilled' }
];
var $ul = $('<ul id="p-ritchie" class="toggle-list__list"></ul>');
links.forEach(function (item) {
var name = 'rb-' + String(item.title).toLowerCase().replace(/[^a-z0-9]+/g, '-');
var $a = $('<a class="toggle-list-item__anchor"></a>')
.addClass('menu__item--' + name)
.attr('href', mw.util.getUrl(item.title));
$a.append($('<span class="minerva-icon"></span>').addClass('minerva-icon--' + item.icon));
$a.append($('<span class="toggle-list-item__label"></span>').text(item.label));
$ul.append($('<li class="toggle-list-item"></li>').append($a));
});
$after.after($ul);
}());
(function buildHomeFind() {
(function buildHomeFind() {
var $slot = $('.mw-parser-output .home-find-slot').first();
var $slot = $('.mw-parser-output .home-find-slot').first();
Line 510: Line 546:
});
});


var $cards = $('.mw-parser-output .yt-interview[data-youtube-id]');
function interviewCardRef($el) {
var yt = String($el.attr('data-youtube-id') || '');
if (yt) {
return { key: 'yt:' + yt, href: '#yt-' + yt };
}
var sc = String($el.attr('data-soundcloud-track') || '');
if (sc) {
return { key: 'sc:' + sc, href: '#sc-' + sc };
}
return null;
}
 
var $cards = $('.mw-parser-output .yt-interview').filter(function () {
return interviewCardRef($(this)) !== null;
});
$cards.each(function (idx) {
$cards.each(function (idx) {
var $card = $(this);
var $card = $(this);
Line 522: Line 572:
}
}
var mine = splitSoldiers($card.attr('data-soldiers'));
var mine = splitSoldiers($card.attr('data-soldiers'));
var selfRef = interviewCardRef($card);
var picks = [];
var picks = [];
var seen = {};
var seen = {};


function addCard($other, why) {
function addCard($other, why) {
var oid = String($other.attr('data-youtube-id') || '');
var ref = interviewCardRef($other);
if (!oid || seen[oid] || oid === $card.attr('data-youtube-id')) {
if (!ref || !selfRef || seen[ref.key] || ref.key === selfRef.key) {
return;
return;
}
}
seen[oid] = 1;
seen[ref.key] = 1;
picks.push({
picks.push({
id: oid,
href: ref.href,
heading: String($other.attr('data-heading') || $other.find('h3, h2').first().text() || 'Interview'),
heading: String($other.attr('data-heading') || $other.find('h3, h2').first().text() || 'Interview'),
short: String($other.attr('data-short') || ''),
short: String($other.attr('data-short') || ''),
Line 557: Line 608:
picks.slice(0, 3).forEach(function (item) {
picks.slice(0, 3).forEach(function (item) {
var label = item.short ? (item.heading + ' — ' + item.short) : item.heading;
var label = item.short ? (item.heading + ' — ' + item.short) : item.heading;
var $a = $('<a></a>').attr('href', '#yt-' + item.id).text(label);
var $a = $('<a></a>').attr('href', item.href).text(label);
var $li = $('<li></li>').append($a);
var $li = $('<li></li>').append($a);
if (item.why === 'same soldier') {
if (item.why === 'same soldier') {
Line 570: Line 621:
}());
}());


(function placeSoldierAwardsSection() {
(function buildSoundCloudEmbeds() {
// Ribbon racks belong in the main column after Biography (like Wikipedia's
var SC_TRACK = /^[0-9]{6,12}$/;
// Awards section). Do not use a wiki == heading here — that stole the TOC.
 
var $out = $('.mw-parser-output').first();
function playSoundCloud($slot, trackId, title) {
var $awards = $out.find('.soldier-awards-section').first();
var src = 'https://w.soundcloud.com/player/?url=' +
if (!$out.length) {
encodeURIComponent('https://api.soundcloud.com/tracks/' + trackId) +
return;
'&color=%23ff5500&auto_play=true&hide_related=true&show_comments=false' +
'&show_user=true&show_reposts=false&show_teaser=false&visual=true';
var $iframe = $('<iframe class="sc-embed-frame" scrolling="no" frameborder="no" allow="autoplay"></iframe>')
.attr({
src: src,
title: title || 'SoundCloud audio',
loading: 'lazy'
});
$slot.empty().append($iframe).addClass('sc-embed-playing');
}
}


function sectionAfter($h) {
$('.mw-parser-output .sc-embed[data-soundcloud-track]').each(function () {
var $nodes = $h.nextUntil('h2');
var $slot = $(this);
if ($nodes.length) {
if ($slot.data('rbScEmbed')) {
return $nodes.last();
return;
}
var trackId = String($slot.attr('data-soundcloud-track') || '');
if (!SC_TRACK.test(trackId)) {
return;
}
}
return $h;
$slot.data('rbScEmbed', 1);
}
var title = String($slot.attr('data-title') || 'SoundCloud audio');
 
var $fallback = $slot.find('.sc-embed-fallback').first();
if ($awards.length && !$awards.data('rbAwardsPlaced')) {
var $btn = $('<button type="button" class="sc-embed-play"></button>')
$awards.data('rbAwardsPlaced', 1);
.attr('aria-label', 'Play ' + title)
var $bio = $out.find('h2').filter(function () {
.append($('<span class="sc-embed-play-icon" aria-hidden="true"></span>'))
var t = $(this).find('.mw-headline').text() || $(this).text();
.append($('<span class="sc-embed-play-text"></span>').text('Play audio'));
return /^(Biography|Life)\b/i.test($.trim(t));
var $poster = $('<div class="sc-embed-poster"></div>').append($btn);
}).first();
if ($fallback.length) {
if ($bio.length) {
$fallback.before($poster);
sectionAfter($bio).after($awards);
$fallback.hide();
} else {
} else {
var $before = $out.find('h2').filter(function () {
$slot.prepend($poster);
var t = $(this).find('.mw-headline').text() || $(this).text();
return /^(Gallery|Research notes|Sources|Technical record)\b/i.test($.trim(t));
}).first();
if ($before.length) {
$before.before($awards);
} else {
$out.append($awards);
}
}
}
$poster.on('click', function (ev) {
ev.preventDefault();
playSoundCloud($slot, trackId, title);
});
});
}());
(function buildAviaryEmbeds() {
var AV_MEDIA = /^[0-9]{4,12}$/;
function playAviary($slot, mediaId, title) {
var src = 'https://milken.aviaryplatform.com/embed/media/' +
encodeURIComponent(mediaId) +
'?embed=true&media_player=true';
var $iframe = $('<iframe class="aviary-embed-frame" allowfullscreen allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>')
.attr({
src: src,
title: title || 'Aviary media',
loading: 'lazy'
});
$slot.empty().append($iframe).addClass('aviary-embed-playing');
}
}


// Class/team rail rides with awards (after Biography).
$('.mw-parser-output .aviary-embed[data-aviary-media]').each(function () {
var $related = $out.find('.soldier-related').first();
var $slot = $(this);
if ($related.length && !$related.data('rbRelatedPlaced')) {
if ($slot.data('rbAviaryEmbed')) {
$related.data('rbRelatedPlaced', 1);
return;
var $awardsNow = $out.find('.soldier-awards-section').first();
}
if ($awardsNow.length) {
var mediaId = String($slot.attr('data-aviary-media') || '');
$awardsNow.after($related);
if (!AV_MEDIA.test(mediaId)) {
return;
}
$slot.data('rbAviaryEmbed', 1);
var title = String($slot.attr('data-title') || 'Aviary media');
var playLabel = String($slot.attr('data-play-label') || 'Play video');
var $fallback = $slot.find('.aviary-embed-fallback').first();
var $btn = $('<button type="button" class="aviary-embed-play"></button>')
.attr('aria-label', playLabel + ' — ' + title)
.append($('<span class="aviary-embed-play-icon" aria-hidden="true"></span>'))
.append($('<span class="aviary-embed-play-text"></span>').text(playLabel));
var $poster = $('<div class="aviary-embed-poster"></div>').append($btn);
if ($fallback.length) {
$fallback.before($poster);
$fallback.hide();
} else {
} else {
var $bio2 = $out.find('h2').filter(function () {
$slot.prepend($poster);
var t = $(this).find('.mw-headline').text() || $(this).text();
return /^(Biography|Life)\b/i.test($.trim(t));
}).first();
if ($bio2.length) {
sectionAfter($bio2).after($related);
}
}
}
$poster.on('click', function (ev) {
ev.preventDefault();
playAviary($slot, mediaId, title);
});
});
}());
(function buildInternetArchiveEmbeds() {
// archive.org item identifiers (e.g. RobertA.Potash13january2012YiddishBookCenter)
var IA_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{5,120}$/;
function playInternetArchive($slot, itemId, title) {
var src = 'https://archive.org/embed/' + encodeURIComponent(itemId);
var $iframe = $('<iframe class="ia-embed-frame" allowfullscreen allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>')
.attr({
src: src,
title: title || 'Internet Archive media',
loading: 'lazy'
});
$slot.empty().append($iframe).addClass('ia-embed-playing');
}
}


// Story cue belongs under the lead (above Biography).
$('.mw-parser-output .ia-embed[data-archive-id]').each(function () {
var $storyCue = $out.find('.soldier-story-cue').first();
var $slot = $(this);
if ($storyCue.length && !$storyCue.data('rbStoryCuePlaced')) {
if ($slot.data('rbIaEmbed')) {
$storyCue.data('rbStoryCuePlaced', 1);
return;
var $leadCue = $out.find('.soldier-lead, .page-lead').first();
if ($leadCue.length) {
$leadCue.after($storyCue);
}
}
}
var itemId = String($slot.attr('data-archive-id') || '');
 
if (!IA_ID.test(itemId)) {
// Interview / film cue sits with the story cue under the lead.
return;
var $mediaCue = $out.find('.soldier-media-cue').first();
if ($mediaCue.length && !$mediaCue.data('rbMediaCuePlaced')) {
$mediaCue.data('rbMediaCuePlaced', 1);
var $afterStory = $out.find('.soldier-story-cue').first();
if ($afterStory.length) {
$afterStory.after($mediaCue);
} else {
var $leadMedia = $out.find('.soldier-lead, .page-lead').first();
if ($leadMedia.length) {
$leadMedia.after($mediaCue);
}
}
}
}
$slot.data('rbIaEmbed', 1);
 
var title = String($slot.attr('data-title') || 'Internet Archive media');
// Contents box belongs after the lead on soldier/profile pages only.
var playLabel = String($slot.attr('data-play-label') || 'Play interview');
// Other pages (e.g. Site changelog) keep MediaWiki's default TOC placement.
var $fallback = $slot.find('.ia-embed-fallback').first();
var $toc = $out.find('#toc, .toc, .mw-table-of-contents').first();
var $btn = $('<button type="button" class="ia-embed-play"></button>')
if ($toc.length && !$toc.data('rbTocPlaced')) {
.attr('aria-label', playLabel + ' — ' + title)
$toc.data('rbTocPlaced', 1);
.append($('<span class="ia-embed-play-icon" aria-hidden="true"></span>'))
var $lead = $out.find('.soldier-lead, .page-lead, .home-lead').first();
.append($('<span class="ia-embed-play-text"></span>').text(playLabel));
if ($lead.length) {
var $poster = $('<div class="ia-embed-poster"></div>').append($btn);
$lead.after($toc);
if ($fallback.length) {
$fallback.before($poster);
$fallback.hide();
} else {
} else {
var $ibox = $out.find('table.soldier-infobox, table.infobox').first();
$slot.prepend($poster);
if ($ibox.length) {
$ibox.after($toc);
}
}
}
}
$poster.on('click', function (ev) {
ev.preventDefault();
playInternetArchive($slot, itemId, title);
});
});
}());
}());


(function moveSoldierFooterCues() {
(function placeSoldierAwardsSection() {
// Ribbon racks belong in the main column after Biography (like Wikipedia's
// Awards section). Do not use a wiki == heading here — that stole the TOC.
var $out = $('.mw-parser-output').first();
var $out = $('.mw-parser-output').first();
var $awards = $out.find('.soldier-awards-section').first();
if (!$out.length) {
if (!$out.length) {
return;
return;
}
}
var $notes = $out.find('.research-notes-disclaimer').first();
 
var $sources = $out.find('.source-confidence-cue').first();
function sectionAfter($h) {
if (!$notes.length && !$sources.length) {
var $nodes = $h.nextUntil('h2');
return;
if ($nodes.length) {
}
return $nodes.last();
var $footer = $out.find('.soldier-page-footer-notes').first();
}
if (!$footer.length) {
return $h;
$footer = $('<div class="soldier-page-footer-notes"></div>');
$out.append($footer);
}
if ($notes.length) {
$footer.append($notes);
}
if ($sources.length) {
$footer.append($sources);
}
}
}());


var NAME_PARTICLES = {
if ($awards.length && !$awards.data('rbAwardsPlaced')) {
von: 1, van: 1, de: 1, der: 1, den: 1, da: 1, di: 1, du: 1,
$awards.data('rbAwardsPlaced', 1);
la: 1, le: 1, del: 1, della: 1, ter: 1, ten: 1, zu: 1, zum: 1, bin: 1
var $bio = $out.find('h2').filter(function () {
};
var t = $(this).find('.mw-headline').text() || $(this).text();
var NAME_SUFFIX = {
return /^(Biography|Life)\b/i.test($.trim(t));
jr: 'Jr', 'jr.': 'Jr.', sr: 'Sr', 'sr.': 'Sr.',
}).first();
esq: 'Esq', 'esq.': 'Esq.', ii: 'II', iii: 'III', iv: 'IV'
if ($bio.length) {
};
sectionAfter($bio).after($awards);
function lettersOnly(s) {
} else {
return String(s || '').replace(/[^A-Za-z]/g, '');
var $before = $out.find('h2').filter(function () {
}
var t = $(this).find('.mw-headline').text() || $(this).text();
function isAllCaps(s) {
return /^(Gallery|Research notes|Sources|Technical record)\b/i.test($.trim(t));
var letters = lettersOnly(s);
}).first();
return letters.length >= 1 && letters === letters.toUpperCase();
if ($before.length) {
}
$before.before($awards);
function isAllLower(s) {
} else {
var letters = lettersOnly(s);
$out.append($awards);
return letters.length >= 2 && letters === letters.toLowerCase();
}
}
}
function titleCaseWord(s) {
s = String(s || '').toLowerCase();
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
function prettyCapsWord(word) {
var trail = '';
while (word && /[.,]$/.test(word)) {
trail = word.slice(-1) + trail;
word = word.slice(0, -1);
}
}
var up = lettersOnly(word).toUpperCase();
 
if (up.length >= 5 && up.slice(0, 2) === 'MC') {
// Class/team rail rides with awards (after Biography).
return 'Mc' + titleCaseWord(up.slice(2)) + trail;
var $related = $out.find('.soldier-related').first();
if ($related.length && !$related.data('rbRelatedPlaced')) {
$related.data('rbRelatedPlaced', 1);
var $awardsNow = $out.find('.soldier-awards-section').first();
if ($awardsNow.length) {
$awardsNow.after($related);
} else {
var $bio2 = $out.find('h2').filter(function () {
var t = $(this).find('.mw-headline').text() || $(this).text();
return /^(Biography|Life)\b/i.test($.trim(t));
}).first();
if ($bio2.length) {
sectionAfter($bio2).after($related);
}
}
}
}
if (up.length >= 7 && up.slice(0, 3) === 'MAC') {
 
return 'Mac' + titleCaseWord(up.slice(3)) + trail;
// Story cue belongs under the lead (above Biography).
var $storyCue = $out.find('.soldier-story-cue').first();
if ($storyCue.length && !$storyCue.data('rbStoryCuePlaced')) {
$storyCue.data('rbStoryCuePlaced', 1);
var $leadCue = $out.find('.soldier-lead, .page-lead').first();
if ($leadCue.length) {
$leadCue.after($storyCue);
}
}
}
return titleCaseWord(up) + trail;
 
}
// Interview / film cue sits with the story cue under the lead.
function prettyNameToken(word, leading) {
var $mediaCue = $out.find('.soldier-media-cue').first();
word = $.trim(word);
if ($mediaCue.length && !$mediaCue.data('rbMediaCuePlaced')) {
if (!word) {
$mediaCue.data('rbMediaCuePlaced', 1);
return word;
var $afterStory = $out.find('.soldier-story-cue').first();
if ($afterStory.length) {
$afterStory.after($mediaCue);
} else {
var $leadMedia = $out.find('.soldier-lead, .page-lead').first();
if ($leadMedia.length) {
$leadMedia.after($mediaCue);
}
}
}
}
if (/\d/.test(word)) {
 
return word;
// Contents box belongs after the lead on soldier/profile pages only.
// Other pages (e.g. Site changelog) keep MediaWiki's default TOC placement.
var $toc = $out.find('#toc, .toc, .mw-table-of-contents').first();
if ($toc.length && !$toc.data('rbTocPlaced')) {
$toc.data('rbTocPlaced', 1);
var $lead = $out.find('.soldier-lead, .page-lead, .home-lead').first();
if ($lead.length) {
$lead.after($toc);
} else {
var $ibox = $out.find('table.soldier-infobox, table.infobox').first();
if ($ibox.length) {
$ibox.after($toc);
}
}
}
}
if (word.indexOf('-') !== -1 && word.charAt(0) !== '-' && word.slice(-1) !== '-') {
}());
return $.map(word.split('-'), function (part, i) {
 
return prettyNameToken(part, leading && i === 0);
(function moveSoldierFooterCues() {
}).join('-');
var $out = $('.mw-parser-output').first();
if (!$out.length) {
return;
}
}
var apos = word.indexOf("'") !== -1 ? "'" : (word.indexOf('\u2019') !== -1 ? '\u2019' : '');
var $notes = $out.find('.research-notes-disclaimer').first();
if (apos) {
var $sources = $out.find('.source-confidence-cue').first();
var bits = word.split(apos);
if (!$notes.length && !$sources.length) {
return prettyNameToken(bits[0], leading) + apos + prettyNameToken(bits.slice(1).join(apos), false);
return;
}
}
if (/^(?:[A-Za-z]\.)+$/.test(word)) {
var $footer = $out.find('.soldier-page-footer-notes').first();
return (isAllCaps(word) || isAllLower(word)) ? word.toUpperCase() : word;
if (!$footer.length) {
$footer = $('<div class="soldier-page-footer-notes"></div>');
$out.append($footer);
}
}
var low = word.toLowerCase();
if ($notes.length) {
if (NAME_SUFFIX[low]) {
$footer.append($notes);
return NAME_SUFFIX[low];
}
}
var bare = lettersOnly(word);
if ($sources.length) {
if (bare.length === 1) {
$footer.append($sources);
return bare.toUpperCase() + word.replace(/[A-Za-z]/, '');
}
}
var lowBare = bare.toLowerCase();
}());
if (NAME_SUFFIX[lowBare] && /^[A-Za-z]+\.?$/.test(word)) {
 
var canon = NAME_SUFFIX[lowBare];
var NAME_PARTICLES = {
if (word.slice(-1) === '.' && canon.slice(-1) !== '.') {
von: 1, van: 1, de: 1, der: 1, den: 1, da: 1, di: 1, du: 1,
return canon + '.';
la: 1, le: 1, del: 1, della: 1, ter: 1, ten: 1, zu: 1, zum: 1, bin: 1
}
};
return canon;
var NAME_SUFFIX = {
}
jr: 'Jr', 'jr.': 'Jr.', sr: 'Sr', 'sr.': 'Sr.',
if (NAME_PARTICLES[lowBare] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) {
esq: 'Esq', 'esq.': 'Esq.', ii: 'II', iii: 'III', iv: 'IV'
return leading ? titleCaseWord(lowBare) : lowBare;
};
function lettersOnly(s) {
return String(s || '').replace(/[^A-Za-z]/g, '');
}
function isAllCaps(s) {
var letters = lettersOnly(s);
return letters.length >= 1 && letters === letters.toUpperCase();
}
function isAllLower(s) {
var letters = lettersOnly(s);
return letters.length >= 2 && letters === letters.toLowerCase();
}
function titleCaseWord(s) {
s = String(s || '').toLowerCase();
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
function prettyCapsWord(word) {
var trail = '';
while (word && /[.,]$/.test(word)) {
trail = word.slice(-1) + trail;
word = word.slice(0, -1);
}
}
if (isAllCaps(word) && bare.length >= 2 && bare.length <= 3 && !/[AEIOUY]/i.test(bare)) {
var up = lettersOnly(word).toUpperCase();
return word;
if (up.length >= 5 && up.slice(0, 2) === 'MC') {
return 'Mc' + titleCaseWord(up.slice(2)) + trail;
}
}
if (isAllCaps(word) || isAllLower(word)) {
if (up.length >= 7 && up.slice(0, 3) === 'MAC') {
return prettyCapsWord(word);
return 'Mac' + titleCaseWord(up.slice(3)) + trail;
}
}
return word;
return titleCaseWord(up) + trail;
}
}
function prettyNamePhrase(text, capLead) {
function prettyNameToken(word, leading) {
text = $.trim(text || '').replace(/\s*;\s*/g, ' ');
word = $.trim(word);
if (!text) {
if (!word) {
return '';
return word;
}
}
return $.map(text.split(/\s*,\s*/), function (clause, clauseI) {
if (/\d/.test(word)) {
clause = $.trim(clause);
return word;
if (!clause) {
return null;
}
var words = clause.split(/\s+/);
return $.map(words, function (word, i) {
var particle = lettersOnly(word).toLowerCase();
var leading = (clauseI === 0 && i === 0 && capLead) || words.length === 1;
if (NAME_PARTICLES[particle] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) {
return leading ? titleCaseWord(particle) : word.toLowerCase();
}
return prettyNameToken(word, i === 0);
}).join(' ');
}).join(', ');
}
function prettySoldierLabel(text) {
var m = $.trim(text || '').match(/^(.*) \(([^()]+)\)\s*$/);
if (!m || m[1].indexOf('(') !== -1) {
return prettyNamePhrase(text, true);
}
}
return prettyNamePhrase(m[1], true) + ' (' + m[2] + ')';
if (word.indexOf('-') !== -1 && word.charAt(0) !== '-' && word.slice(-1) !== '-') {
}
return $.map(word.split('-'), function (part, i) {
function maybePrettySoldierTitle(label) {
return prettyNameToken(part, leading && i === 0);
var text = $.trim(label || '');
}).join('-');
if (!text || text.indexOf(',') === -1) {
return text;
}
}
var sur = text.split(',')[0];
var apos = word.indexOf("'") !== -1 ? "'" : (word.indexOf('\u2019') !== -1 ? '\u2019' : '');
var letters = lettersOnly(sur);
if (apos) {
if (!(letters.length >= 2 && letters === letters.toUpperCase())) {
var bits = word.split(apos);
return text;
return prettyNameToken(bits[0], leading) + apos + prettyNameToken(bits.slice(1).join(apos), false);
}
if (/^(?:[A-Za-z]\.)+$/.test(word)) {
return (isAllCaps(word) || isAllLower(word)) ? word.toUpperCase() : word;
}
var low = word.toLowerCase();
if (NAME_SUFFIX[low]) {
return NAME_SUFFIX[low];
}
var bare = lettersOnly(word);
if (bare.length === 1) {
return bare.toUpperCase() + word.replace(/[A-Za-z]/, '');
}
}
return prettySoldierLabel(text);
var lowBare = bare.toLowerCase();
}
if (NAME_SUFFIX[lowBare] && /^[A-Za-z]+\.?$/.test(word)) {
function applyPrettySoldierLabels($links) {
var canon = NAME_SUFFIX[lowBare];
$links.each(function () {
if (word.slice(-1) === '.' && canon.slice(-1) !== '.') {
var $a = $(this);
return canon + '.';
var label = $a.text();
var pretty = maybePrettySoldierTitle(label);
if (pretty && pretty !== label) {
$a.text(pretty);
if ($a.attr('title') === label || !$a.attr('title')) {
$a.attr('title', pretty);
}
}
}
});
return canon;
}
if (NAME_PARTICLES[lowBare] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) {
return leading ? titleCaseWord(lowBare) : lowBare;
}
if (isAllCaps(word) && bare.length >= 2 && bare.length <= 3 && !/[AEIOUY]/i.test(bare)) {
return word;
}
if (isAllCaps(word) || isAllLower(word)) {
return prettyCapsWord(word);
}
return word;
}
}
if (mw.config.get('wgNamespaceNumber') === 14) {
function prettyNamePhrase(text, capLead) {
applyPrettySoldierLabels($('.mw-category-group li a'));
text = $.trim(text || '').replace(/\s*;\s*/g, ' ');
}
if (!text) {
if (mw.config.get('wgCanonicalSpecialPageName') === 'Search') {
return '';
applyPrettySoldierLabels($('.mw-search-result-heading a'));
}
return $.map(text.split(/\s*,\s*/), function (clause, clauseI) {
clause = $.trim(clause);
if (!clause) {
return null;
}
var words = clause.split(/\s+/);
return $.map(words, function (word, i) {
var particle = lettersOnly(word).toLowerCase();
var leading = (clauseI === 0 && i === 0 && capLead) || words.length === 1;
if (NAME_PARTICLES[particle] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) {
return leading ? titleCaseWord(particle) : word.toLowerCase();
}
return prettyNameToken(word, i === 0);
}).join(' ');
}).join(', ');
}
function prettySoldierLabel(text) {
var m = $.trim(text || '').match(/^(.*) \(([^()]+)\)\s*$/);
if (!m || m[1].indexOf('(') !== -1) {
return prettyNamePhrase(text, true);
}
return prettyNamePhrase(m[1], true) + ' (' + m[2] + ')';
}
function maybePrettySoldierTitle(label) {
var text = $.trim(label || '');
if (!text || text.indexOf(',') === -1) {
return text;
}
var sur = text.split(',')[0];
var letters = lettersOnly(sur);
if (!(letters.length >= 2 && letters === letters.toUpperCase())) {
return text;
}
return prettySoldierLabel(text);
}
function applyPrettySoldierLabels($links) {
$links.each(function () {
var $a = $(this);
var label = $a.text();
var pretty = maybePrettySoldierTitle(label);
if (pretty && pretty !== label) {
$a.text(pretty);
if ($a.attr('title') === label || !$a.attr('title')) {
$a.attr('title', pretty);
}
}
});
}
if (mw.config.get('wgNamespaceNumber') === 14) {
applyPrettySoldierLabels($('.mw-category-group li a'));
}
if (mw.config.get('wgCanonicalSpecialPageName') === 'Search') {
applyPrettySoldierLabels($('.mw-search-result-heading a'));
}
}
// Search box suggestions use the real page title; pretty-case ALL-CAPS surnames as they appear.
// Search box suggestions use the real page title; pretty-case ALL-CAPS surnames as they appear.
Line 883: Line 1,074:


function teamHref(name) {
function teamHref(name) {
return '/index.php?title=' + encodeURIComponent('Team ' + name).replace(/%20/g, '_');
return mw.util.getUrl('Team ' + name);
}
}
function teamLink(name, label) {
function teamLink(name, label) {
Line 995: Line 1,186:
}
}


var negative = /fail(?:ed|ure)?|flunk|wash(?:ed)?\s*out|kick(?:ed)?\s*out|dismiss|expel|unsatisfactor|poor\s+(?:performance|work|grades)|awol|court[-\s]?martial|disciplin|discharged\s+for\s+cause|drop(?:ped)?\s+(?:out|from)|relieved|eliminat|unfit|misconduct|desert|excess\s+absence|absence\s*\(|\billness\b|sick\s+call|medical\s+board|psychiatr|neurot|homosexual|suicid|alcohol(?:ism)?|drunk|intoxicat|venereal|\bVD\b|syphili|gonorrh|mental\s+(?:case|illness)|nervous\s+breakdown|not\s+graduat|did\s+not\s+graduate|physically\s+unfit|unfit\s+for|non[-\s]?graduate|certificate\s+listed\s+as\s+ng/i;
var negative = /fail(?:ed|ure)?|flunk|wash(?:ed)?\s*out|kick(?:ed)?\s*out|dismiss|expel|unsatisfactor|poor\s+(?:performance|work|grades)|awol|court[-\s]?martial|disciplin|discharged\s+for\s+cause|drop(?:ped)?\s+(?:out|from)|relieved|eliminat|unfit|misconduct|desert|excess\s+absence|absence\s*\(|\billness\b|sick\s+call|medical\s+board|psychiatr|neurot|homosexual|suicid|alcohol(?:ism)?|drunk|intoxicat|venereal|\bVD\b|syphili|gonorrh|mental\s+(?:case|illness)|nervous\s+breakdown|not\s+graduat|did\s+not\s+graduate|physically\s+unfit|unfit\s+for|non[-\s]?graduate|certificate\s+listed\s+as\s+ng|(?:^|\b)NG\b(?:\s+Class|\s+OB|\s*$|\s*[,;:/]|\s+\d)|\bnongrad/i;
var $ul = $heading.nextAll('ul').first();
var $ul = $heading.nextAll('ul').first();
if (!$ul.length) {
if (!$ul.length) {
Line 1,238: Line 1,429:
{ key: 'death_year', label: 'Death year', hint: 'Four digits, e.g. 2001' },
{ key: 'death_year', label: 'Death year', hint: 'Four digits, e.g. 2001' },
{ key: 'nativity', label: 'Nativity (birthplace)' },
{ key: 'nativity', label: 'Nativity (birthplace)' },
{ key: 'religion', label: 'Religion' },
{ key: 'religion', label: 'Religion' }
{ key: 'marital', label: 'Marital status' },
{ key: 'education', label: 'Education' },
{ key: 'occupation', label: 'Civilian occupation' },
{ key: 'state', label: 'State (residence)' }
]
]
},
},
Line 1,264: Line 1,451:
collapsed: true,
collapsed: true,
fields: [
fields: [
{ key: 'other_class', label: 'Other class' },
{
key: 'other_class',
label: 'Other class',
hint: 'Roster code, e.g. SC8, DC36, MITU4 (shown expanded on the page)'
},
{ key: 'division', label: 'Unit / division' },
{ key: 'division', label: 'Unit / division' },
{ key: 'loc', label: 'LOC' },
{ key: 'loc', label: 'LOC' }
{ key: 'date_arrive', label: 'Date arrive' },
{ key: 'date_depart', label: 'Date depart' }
]
]
}
}
Line 1,274: Line 1,463:


var $box = $('<div>', { 'class': 'soldier-facts-box' });
var $box = $('<div>', { 'class': 'soldier-facts-box' });
var $intro = $('<p>').append(
var $intro = $('<p>', { 'class': 'soldier-facts-intro' });
$('<strong>').text('Editing this page'),
document.createTextNode(' ')
);
$intro.append(
$intro.append(
$('<strong>', { 'class': 'soldier-facts-label' }).text('Editing this page'),
$('<span>', { 'class': 'soldier-facts-help' }).text(' — '),
$('<button>', { type: 'button', 'class': 'soldier-facts-toggle', text: 'Edit soldier facts' }),
$('<button>', { type: 'button', 'class': 'soldier-facts-toggle', text: 'Edit soldier facts' }),
document.createTextNode(' (infobox fields), '),
$('<span>', { 'class': 'soldier-facts-help' }).text(' (infobox fields), '),
$('<a>', { href: editUrl, text: 'Edit biography' }),
$('<a>', { href: editUrl, text: 'Edit biography' }),
document.createTextNode(' (visual editor for Biography and notes), or '),
$('<span>', { 'class': 'soldier-facts-help' }).text(' (visual editor for Biography and notes), or '),
$('<a>', { href: sourceUrl, text: 'Edit source' }),
$('<a>', { href: sourceUrl, text: 'Edit source' }),
document.createTextNode(' (full page markup).')
$('<span>', { 'class': 'soldier-facts-help' }).text(' (full page markup).')
);
);
$box.append($intro);
$box.append($intro);
Line 1,292: Line 1,480:
$draftBar.append(
$draftBar.append(
$draftBtn,
$draftBtn,
document.createTextNode(' — optional 5–10 sentence opening from Personal details and the Biography section. Review before saving.')
$('<span>', { 'class': 'soldier-summary-help' }).text(
' — optional 5–10 sentence opening from Personal details and the Biography section. Review before saving.'
)
);
);
$box.append($draftBar);
$box.append($draftBar);
Line 2,275: Line 2,465:
$panel.append($banner);
$panel.append($banner);
} else {
} else {
var engineBit = job.ocr_engine ? (' · Engine: ' + job.ocr_engine) : '';
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text(
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text(
(job.doc_type_label || 'Document') +
(job.doc_type_label || 'Document') +
' · OCR quality ' + (job.ocr_quality || 0) + '%' +
' · OCR quality ' + (job.ocr_quality || 0) + '%' +
engineBit +
(job.error ? (' · Error: ' + job.error) : '')
(job.error ? (' · Error: ' + job.error) : '')
));
));
if (job.ocr_engine_notes && job.ocr_engine_notes.length) {
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text(
job.ocr_engine_notes.slice(0, 3).join(' · ')
));
}
}
}


Line 2,462: Line 2,659:
if (isMultiPersonJob(job) && !job.bound_title) {
if (isMultiPersonJob(job) && !job.bound_title) {
$forceMulti.hide();
$forceMulti.hide();
}
var $forceVision = $('<label>', { 'class': 'ocr-force-vision' }).append(
$('<input>', { type: 'checkbox', checked: mwApiBool(job.force_vision) }),
' Handwriting / hard scan (print OCR first; paid vision only if weak)'
);
if (!cfg.llmVision) {
$forceVision.append(document.createTextNode(' — needs RITCHIE_LLM_API_KEY on OCR service'));
}
}
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append(
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append(
Line 2,476: Line 2,680:
$applyNotes, $('<br>'),
$applyNotes, $('<br>'),
$forceMulti, $('<br>'),
$forceMulti, $('<br>'),
$forceVision, $('<br>'),
$forceId
$forceId
);
);


var $save = $('<button>', { type: 'button', text: 'Save draft' });
var $save = $('<button>', { type: 'button', text: 'Save draft' });
var $rebuild = $('<button>', { type: 'button', text: 'Rebuild from pages / roster mode' });
var $rebuild = $('<button>', { type: 'button', text: 'Rebuild facts / re-OCR if handwriting checked' });
if (pages.length < 2 && !job.bound_title) {
$rebuild.hide();
}
$advBody.append($('<div>', { 'class': 'ocr-advanced-actions' }).append($save, $rebuild));
$advBody.append($('<div>', { 'class': 'ocr-advanced-actions' }).append($save, $rebuild));
$adv.append($advBody);
$adv.append($advBody);
Line 2,567: Line 2,769:
force_identity: $forceId.find('input').prop('checked'),
force_identity: $forceId.find('input').prop('checked'),
force_multi: $forceMulti.find('input').prop('checked'),
force_multi: $forceMulti.find('input').prop('checked'),
force_vision: $forceVision.find('input').prop('checked'),
apply_notes: $applyNotes.find('input').prop('checked')
apply_notes: $applyNotes.find('input').prop('checked')
};
};
Line 2,769: Line 2,972:
bound_title: boundTitle || '',
bound_title: boundTitle || '',
force_multi: opts.forceMulti ? true : false,
force_multi: opts.forceMulti ? true : false,
force_vision: opts.forceVision ? true : false,
file: file,
file: file,
format: 'json'
format: 'json'
Line 2,815: Line 3,019:
var $section = $('<div>', { 'class': 'soldier-contrib-section soldier-contrib-ocr' });
var $section = $('<div>', { 'class': 'soldier-contrib-section soldier-contrib-ocr' });
$section.append($('<p>', { 'class': 'soldier-contrib-heading' }).append(
$section.append($('<p>', { 'class': 'soldier-contrib-heading' }).append(
$('<strong>').text('OCR a document')
$('<strong>').text('OCR a document to enrich this profile')
));
));
$section.append($('<p>', { 'class': 'soldier-contrib-help' }).text(
$section.append($('<p>', { 'class': 'soldier-contrib-help' }).text(
'Scan or photo of a form, letter, or roster. Proposed facts appear here for review — nothing is written until Apply.'
'Scan a form, letter, certificate, clipping, or roster. Review proposes infobox facts and a short biography draft — nothing is written until Apply. Team rosters can update many soldiers at once.'
));
));
var $file = $('<input>', {
var $file = $('<input>', {
Line 2,828: Line 3,032:
$('<input>', { type: 'checkbox' }),
$('<input>', { type: 'checkbox' }),
' This is a multi-person roster (not only this soldier)'
' This is a multi-person roster (not only this soldier)'
);
var $visionMode = $('<label>', { 'class': 'ocr-force-multi-upload' }).append(
$('<input>', { type: 'checkbox' }),
' Handwriting / hard scan (free print OCR first; vision only if needed)'
);
);
var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' });
var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' });
var $host = $('<div>', { 'class': 'ocr-review-host' });
var $host = $('<div>', { 'class': 'ocr-review-host' });
$section.append($file, $rosterMode, $btn, $host);
$section.append($file, $rosterMode, $visionMode, $btn, $host);
$btn.on('click', function () {
$btn.on('click', function () {
startUpload($file, pageTitle, $host, cfg, {
startUpload($file, pageTitle, $host, cfg, {
forceMulti: $rosterMode.find('input').prop('checked')
forceMulti: $rosterMode.find('input').prop('checked'),
forceVision: $visionMode.find('input').prop('checked')
});
});
});
});
Line 2,857: Line 3,066:
$up.append($('<h3>').text('Upload a document'));
$up.append($('<h3>').text('Upload a document'));
$up.append($('<p>').text(
$up.append($('<p>').text(
'For a roster, review is a table: who is on the scan, which wiki page, and what would be written on that profile (usually Team). Rank, role, and ASN are matching cues, not automatic edits.'
'Enrich soldier profiles from scans: propose infobox facts, a short biography draft, and Sources. For multi-person IPW rosters, review is a table (usually Team membership). Rank, role, and ASN are matching cues until you approve.'
));
));
var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' });
var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' });
var $vision2 = $('<label>', { 'class': 'ocr-force-multi-upload' }).append(
$('<input>', { type: 'checkbox' }),
' Handwriting / hard scan (vision only if print OCR is weak)'
);
var $btn2 = $('<button>', { type: 'button', text: 'Upload & OCR' });
var $btn2 = $('<button>', { type: 'button', text: 'Upload & OCR' });
var $host2 = $('<div>', { 'class': 'ocr-review-host' });
var $host2 = $('<div>', { 'class': 'ocr-review-host' });
$up.append($file2, ' ', $btn2, $host2);
$up.append($file2, $vision2, ' ', $btn2, $host2);
$btn2.on('click', function () {
$btn2.on('click', function () {
startUpload($file2, '', $host2, cfg);
startUpload($file2, '', $host2, cfg, {
forceVision: $vision2.find('input').prop('checked')
});
});
});
$slot.append($up);
$slot.append($up);
Line 3,107: Line 3,322:
var $block = $(this);
var $block = $(this);
var cat = String($block.attr('data-team-cat') || '');
var cat = String($block.attr('data-team-cat') || '');
if (!/^Category:Team /.test(cat)) {
if (!/^Category:Team /.test(cat)) {
return;
return;
}
}
var $links = $block.find('.soldier-related-links-live').first();
var $links = $block.find('.soldier-related-links-live').first();
if (!$links.length) {
if (!$links.length) {
return;
return;
}
}
$links.text('Loading teammates…');
$links.text('Loading teammates…');
fetchCategoryMembers(cat).done(function (titles) {
fetchCategoryMembers(cat).done(function (titles) {
var others = titles.filter(function (t) {
var others = titles.filter(function (t) {
return t.replace(/_/g, ' ') !== current;
return t.replace(/_/g, ' ') !== current;
});
});
others.sort(function (a, b) {
others.sort(function (a, b) {
return titleToCaption(a).localeCompare(titleToCaption(b));
return titleToCaption(a).localeCompare(titleToCaption(b));
});
});
$links.empty();
$links.empty();
if (!others.length) {
if (!others.length) {
$links.append(document.createTextNode('No other teammates listed yet · '));
$links.append(document.createTextNode('No other teammates listed yet · '));
$links.append(wikiLink(cat, 'full roster'));
$links.append(wikiLink(cat, 'full roster'));
return;
return;
}
}
var max = 24;
var max = 24;
others.slice(0, max).forEach(function (title, i) {
others.slice(0, max).forEach(function (title, i) {
if (i > 0) {
if (i > 0) {
$links.append(document.createTextNode(' · '));
$links.append(document.createTextNode(' · '));
}
}
$links.append(wikiLink(title, titleToCaption(title)));
$links.append(wikiLink(title, titleToCaption(title)));
});
});
$links.append(document.createTextNode(' · '));
$links.append(document.createTextNode(' · '));
$links.append(wikiLink(cat, others.length > max ? 'full roster (' + others.length + ')' : 'full roster'));
$links.append(wikiLink(cat, others.length > max ? 'full roster (' + others.length + ')' : 'full roster'));
}).fail(function () {
}).fail(function () {
$links.empty().append(wikiLink(cat, 'Browse team roster'));
$links.empty().append(wikiLink(cat, 'Browse team roster'));
});
});
});
});
});
});
}());
}());
 
$(function rbCampManualUpload() {
var $slot = $('.mw-parser-output .camp-manual-upload-slot').first();
if (!$slot.length || $slot.data('rbCampManualUpload')) {
return;
}
$slot.data('rbCampManualUpload', 1);
 
var LIBRARY = 'Training materials/Library';
var PLACEHOLDER = "''No manuals listed yet. Upload a PDF above and it will appear here.''";
var DOC_EXT = /\.(pdf|docx?|txt|rtf|odt|tiff?|jpe?g|png|gif|webp)$/i;
var pageTitle = mw.config.get('wgPageName');
var loggedIn = !!mw.config.get('wgUserName');
var uploadUrl = mw.util.getUrl('Special:Upload');
 
$slot.empty();
var $box = $('<div>', { 'class': 'camp-manual-upload-box soldier-upload-body' });
$box.append($('<p>', { 'class': 'soldier-contrib-help' }).append(
document.createTextNode('Upload a manual or related Camp Ritchie document (PDF preferred). Also '),
$('<a>', { href: uploadUrl, text: 'Special:Upload' }),
document.createTextNode('.')
));
 
if (!loggedIn) {
$box.append($('<p>', { 'class': 'soldier-upload-login' }).html(
'<a href="' + mw.util.getUrl('Special:UserLogin', { returnto: pageTitle }) + '">Log in</a> to upload files.'
));
$slot.append($box);
return;
}
 
var $form = $('<form>', { 'class': 'soldier-upload-form camp-manual-upload-form' });
var $file = $('<input>', {
type: 'file',
name: 'campManualFile',
'class': 'soldier-upload-file',
accept: '.pdf,.doc,.docx,.txt,.rtf,.odt,.tif,.tiff,.jpg,.jpeg,.png'
});
var $label = $('<label>').append(
document.createTextNode('Display title (optional)'),
$('<input>', {
type: 'text',
name: 'campManualTitle',
'class': 'camp-manual-title',
placeholder: 'e.g. Interrogation manual, 1943',
maxlength: 200
})
);
var $note = $('<label>').append(
document.createTextNode('Short note / source (optional)'),
$('<input>', {
type: 'text',
name: 'campManualNote',
'class': 'camp-manual-note',
placeholder: 'e.g. NARA / museum scan',
maxlength: 300
})
);
var $status = $('<p>', { 'class': 'soldier-upload-status', text: '' });
var $progress = $('<div>', { 'class': 'camp-manual-progress', hidden: true });
var $progressFill = $('<div>', { 'class': 'camp-manual-progress-fill' });
var $progressBar = $('<div>', {
'class': 'camp-manual-progress-bar',
role: 'progressbar',
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0
}).append($progressFill);
var $progressLabel = $('<p>', { 'class': 'camp-manual-progress-label', text: '' });
$progress.append($progressBar, $progressLabel);
var $submit = $('<button>', { type: 'submit', text: 'Upload to library' });
$form.append($file, $label, $note, $submit, $progress, $status);
$box.append($form);
$slot.append($box);
 
function setStatus(msg, isError) {
$status.text(msg).css('color', isError ? '#ba0000' : '#202122');
}
 
function formatBytes(n) {
if (n >= 1024 * 1024) {
return (n / (1024 * 1024)).toFixed(1) + ' MB';
}
if (n >= 1024) {
return Math.round(n / 1024) + ' KB';
}
return n + ' B';
}
 
function setProgress(loaded, total) {
$progress.prop('hidden', false);
if (!total) {
$progress.addClass('is-indeterminate');
$progressBar.attr('aria-valuenow', 0);
$progressFill.css('width', '30%');
$progressLabel.text('Uploading…');
return;
}
$progress.removeClass('is-indeterminate');
var pct = Math.max(0, Math.min(100, Math.round((loaded / total) * 100)));
$progressBar.attr('aria-valuenow', pct);
$progressFill.css('width', pct + '%');
$progressLabel.text(pct + '% · ' + formatBytes(loaded) + ' of ' + formatBytes(total));
}
 
function resetProgress() {
$progress.prop('hidden', true).removeClass('is-indeterminate');
$progressFill.css('width', '0%');
$progressBar.attr('aria-valuenow', 0);
$progressLabel.text('');
}
 
function escapePipe(s) {
return String(s || '').replace(/\|/g, '–').replace(/\[\[/g, '').replace(/\]\]/g, '');
}
 
function buildBullet(filename, displayTitle, note) {
var label = escapePipe(displayTitle || filename.replace(/_/g, ' '));
var line = '* [[Media:' + filename + '|' + label + ']]';
if (note) {
line += ' — ' + escapePipe(note);
}
return line;
}
 
function appendToLibrary(api, filename, displayTitle, note) {
var bullet = buildBullet(filename, displayTitle, note);
return api.get({
action: 'query',
prop: 'revisions|info',
titles: LIBRARY,
rvprop: 'content|timestamp',
rvslots: 'main',
curtimestamp: 1,
formatversion: 2,
maxage: 0,
smaxage: 0
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var rev = page && page.revisions && page.revisions[0];
var text = (rev && rev.slots && rev.slots.main && rev.slots.main.content) || '';
var basets = rev && rev.timestamp;
var startts = data.curtimestamp;
var next;
var trimmed = $.trim(text);
var already = trimmed.indexOf('[[Media:' + filename + '|') !== -1 ||
trimmed.indexOf('[[File:' + filename + '|') !== -1;
if (already) {
return purgeLibraryPages(api).then(function () {
return { edit: { result: 'Success', nochange: true } };
});
}
if (!trimmed || trimmed.indexOf(PLACEHOLDER) !== -1) {
next = bullet + '\n';
} else {
next = trimmed + '\n' + bullet + '\n';
}
var params = {
action: 'edit',
title: LIBRARY,
text: next,
summary: 'Add training material: ' + filename,
format: 'json'
};
if (basets) {
params.basetimestamp = basets;
}
if (startts) {
params.starttimestamp = startts;
}
return api.postWithToken('csrf', params).then(function (editData) {
if (editData.edit && editData.edit.result === 'Success') {
return purgeLibraryPages(api).then(function () {
return editData;
});
}
if (editData.error && editData.error.code === 'editconflict') {
return appendToLibrary(api, filename, displayTitle, note);
}
throw new Error((editData.error && editData.error.info) || 'Could not update library list');
});
});
}
 
function failMessage(code, data) {
if (data && data.error && data.error.info) {
return data.error.info;
}
if (data && data.exception && data.exception.message) {
return data.exception.message;
}
if (code && typeof code === 'object' && code.message) {
return code.message;
}
if (typeof code === 'string' && code && code !== 'http' && code !== 'error' && code !== 'timeout') {
return code;
}
if (data && data.xhr && data.xhr.status) {
return 'Upload failed (HTTP ' + data.xhr.status + '). Try again, or use Special:Upload.';
}
return 'Upload failed (connection dropped or timed out). Try again, or use Special:Upload.';
}
 
function purgeLibraryPages(api) {
return api.postWithToken('csrf', {
action: 'purge',
titles: 'Training materials|' + LIBRARY,
forcelinkupdate: 1,
format: 'json'
});
}
 
function readLibraryText(api) {
return api.get({
action: 'query',
prop: 'revisions|info',
titles: LIBRARY,
rvprop: 'content|timestamp',
rvslots: 'main',
curtimestamp: 1,
formatversion: 2,
maxage: 0,
smaxage: 0
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var rev = page && page.revisions && page.revisions[0];
return {
text: (rev && rev.slots && rev.slots.main && rev.slots.main.content) || '',
basetimestamp: rev && rev.timestamp,
starttimestamp: data.curtimestamp
};
});
}
 
function writeLibraryText(api, next, summary, basets, startts) {
var params = {
action: 'edit',
title: LIBRARY,
text: next,
summary: summary,
format: 'json'
};
if (basets) {
params.basetimestamp = basets;
}
if (startts) {
params.starttimestamp = startts;
}
return api.postWithToken('csrf', params).then(function (editData) {
if (editData.edit && editData.edit.result === 'Success') {
return purgeLibraryPages(api).then(function () {
return editData;
});
}
if (editData.error && editData.error.code === 'editconflict') {
throw new Error('editconflict');
}
throw new Error((editData.error && editData.error.info) || 'Could not update library list');
});
}
 
function removeFromLibrary(api, filename) {
return readLibraryText(api).then(function (cur) {
var lines = String(cur.text || '').split(/\r?\n/);
var nextLines = lines.filter(function (line) {
return line.indexOf('[[Media:' + filename + '|') === -1 &&
line.indexOf('[[File:' + filename + '|') === -1 &&
line.indexOf(filename) === -1;
});
var next = $.trim(nextLines.join('\n'));
if (!next) {
next = PLACEHOLDER;
} else {
next = next + '\n';
}
return writeLibraryText(
api,
next,
'Remove from Training materials library: ' + filename,
cur.basetimestamp,
cur.starttimestamp
).then(null, function (err) {
if (String(err && err.message || err) === 'editconflict') {
return removeFromLibrary(api, filename);
}
throw err;
});
});
}
 
function deleteLibraryFile(api, filename) {
return api.postWithToken('csrf', {
action: 'delete',
title: 'File:' + filename,
reason: 'Removed from Training materials library',
format: 'json'
}).then(function (data) {
if (data.error) {
throw new Error(data.error.info || 'Could not delete file');
}
return removeFromLibrary(api, filename);
});
}
 
function filenameFromHref(href, titleAttr) {
if (titleAttr && /\.[a-z0-9]{2,5}$/i.test(titleAttr)) {
return String(titleAttr).replace(/ /g, '_');
}
if (!href) {
return '';
}
try {
href = decodeURIComponent(href);
} catch (e) { /* ignore */ }
var m = href.match(/\/wiki\/(?:File|Media):([^?#]+)/i) ||
href.match(/[?&]title=(?:File|Media):([^&#]+)/i) ||
href.match(/\/images\/(?:thumb\/)?[^/]+\/[^/]+\/([^/?#]+)$/i);
if (!m) {
return '';
}
return m[1].replace(/_/g, ' ').trim().replace(/ /g, '_');
}
 
function enhanceLibraryManage() {
if (!mw.config.get('wgUserName')) {
return;
}
var $heading = $('.mw-parser-output .mw-heading, .mw-parser-output h2').filter(function () {
return /^Library\b/i.test($(this).text().replace(/\s*\[.*$/, '').trim());
}).first();
if (!$heading.length) {
return;
}
var $list = $heading.nextAll('ul').first();
if (!$list.length) {
$list = $heading.parent().nextAll('ul').first();
}
if (!$list.length || $list.data('rbCampManualManage')) {
return;
}
$list.data('rbCampManualManage', 1);
 
var groups = mw.config.get('wgUserGroups') || [];
var canDeleteFile = groups.indexOf('sysop') !== -1;
var api = new mw.Api();
 
$list.children('li').each(function () {
var $li = $(this);
if ($li.find('.camp-manual-lib-actions').length) {
return;
}
var $a = $li.find('a.internal, a[href*="/images/"], a[href*="File:"], a[href*="Media:"]').first();
if (!$a.length) {
$a = $li.find('a[href]').first();
}
var filename = filenameFromHref($a.attr('href'), $a.attr('title'));
if (!filename) {
return;
}
var $actions = $('<span>', { 'class': 'camp-manual-lib-actions' });
var $remove = $('<button>', {
type: 'button',
'class': 'camp-manual-lib-remove',
text: 'Remove'
}).attr('title', 'Remove from this list (file stays on the wiki)');
$actions.append($remove);
if (canDeleteFile) {
$actions.append($('<button>', {
type: 'button',
'class': 'camp-manual-lib-delete',
text: 'Delete file'
}).attr('title', 'Delete the file from the wiki (administrators only)'));
}
$li.append($actions);
 
$remove.on('click', function () {
if (!window.confirm('Remove “' + filename.replace(/_/g, ' ') + '” from the Library list?\n(The file will remain on the wiki.)')) {
return;
}
$actions.find('button').prop('disabled', true);
removeFromLibrary(api, filename).done(function () {
window.location.reload();
}).fail(function (code, data) {
window.alert(failMessage(code, data));
$actions.find('button').prop('disabled', false);
});
});
 
$actions.on('click', '.camp-manual-lib-delete', function () {
if (!window.confirm('Permanently delete file “' + filename.replace(/_/g, ' ') + '” from the wiki?')) {
return;
}
$actions.find('button').prop('disabled', true);
deleteLibraryFile(api, filename).done(function () {
window.location.reload();
}).fail(function (code, data) {
window.alert(failMessage(code, data));
$actions.find('button').prop('disabled', false);
});
});
});
}
 
enhanceLibraryManage();
 
$form.on('submit', function (evt) {
evt.preventDefault();
var file = $file[0].files[0];
if (!file) {
setStatus('Choose a file first.', true);
return;
}
if (!DOC_EXT.test(file.name)) {
setStatus('File type not allowed (PDF, Office docs, images, TIFF).', true);
return;
}
var maxBytes = 120 * 1024 * 1024;
if (file.size > maxBytes) {
setStatus('File is too large (max about 120 MB). Split the PDF or compress it, then try again.', true);
return;
}
var displayTitle = $.trim($form.find('.camp-manual-title').val() || '');
var note = $.trim($form.find('.camp-manual-note').val() || '');
$submit.prop('disabled', true);
resetProgress();
setProgress(0, file.size);
setStatus('Uploading…');
 
var api = new mw.Api({ ajax: { timeout: 0 } });
var fileText = 'Camp Ritchie / MITC training material.';
var uploadedName = null;
var uploadParams = {
filename: file.name,
comment: 'Uploaded to Training materials library',
text: fileText,
ignorewarnings: true
};
 
function doUpload() {
/* Multipart + XHR progress (api.upload alone often has no progress events). */
return api.postWithToken('csrf', $.extend({
action: 'upload',
file: file,
format: 'json'
}, uploadParams), {
contentType: 'multipart/form-data',
timeout: 0,
xhr: function () {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener('progress', function (ev) {
if (ev.lengthComputable) {
setProgress(ev.loaded, ev.total);
}
});
}
return xhr;
}
});
}
 
doUpload().then(function (data) {
uploadedName = data.upload && data.upload.filename;
if (!uploadedName) {
throw new Error((data.error && data.error.info) || 'Upload failed');
}
setProgress(file.size, file.size);
$progress.addClass('is-indeterminate');
$progressLabel.text('Finishing…');
setStatus('Adding to Library list…');
return appendToLibrary(api, uploadedName, displayTitle, note);
}).done(function () {
$progress.removeClass('is-indeterminate');
$progressLabel.text('100% · done');
setStatus('Done. Reloading…');
window.location.reload();
}).fail(function (code, data) {
var err = failMessage(code, data);
if (uploadedName) {
err = 'File uploaded as “' + uploadedName + '”, but the Library list was not updated: ' + err +
' Refresh this page; if it is missing from the list, say so and we can add it.';
}
setStatus(String(err), true);
resetProgress();
$submit.prop('disabled', false);
});
});
});


(function rbBooksFilters() {
(function rbBooksFilters() {
Line 3,182: Line 3,885:
}());
}());


 
/* Related-media wrap removed: story/interview cues already sit under the lead
(function rbRelatedMediaWrap() {
* with their own labels (Full story, etc.). Wrapping them added a stray
var $out = $('.mw-parser-output').first();
* Related media heading near the top of the article. */
if (!$out.length) return;
var $cues = $out.find('.soldier-story-cue, .soldier-media-cue');
if ($cues.length < 1) return;
if ($out.find('.soldier-related-media').length) return;
var $wrap = $('<div class="soldier-related-media"><div class="soldier-related-media-title">Related media</div></div>');
$cues.first().before($wrap);
$cues.appendTo($wrap);
}());