Append uploads to Training materials/Library subpage
Add Internet Archive click-to-play embeds for interview watchlist
 
(11 intermediate revisions by the same user not shown)
Line 546: 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 558: 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 593: 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 606: 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;
}
$slot.data('rbScEmbed', 1);
var title = String($slot.attr('data-title') || 'SoundCloud audio');
var $fallback = $slot.find('.sc-embed-fallback').first();
var $btn = $('<button type="button" class="sc-embed-play"></button>')
.attr('aria-label', 'Play ' + title)
.append($('<span class="sc-embed-play-icon" aria-hidden="true"></span>'))
.append($('<span class="sc-embed-play-text"></span>').text('Play audio'));
var $poster = $('<div class="sc-embed-poster"></div>').append($btn);
if ($fallback.length) {
$fallback.before($poster);
$fallback.hide();
} else {
$slot.prepend($poster);
}
}
return $h;
$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');
}
}


if ($awards.length && !$awards.data('rbAwardsPlaced')) {
$('.mw-parser-output .aviary-embed[data-aviary-media]').each(function () {
$awards.data('rbAwardsPlaced', 1);
var $slot = $(this);
var $bio = $out.find('h2').filter(function () {
if ($slot.data('rbAviaryEmbed')) {
var t = $(this).find('.mw-headline').text() || $(this).text();
return;
return /^(Biography|Life)\b/i.test($.trim(t));
}
}).first();
var mediaId = String($slot.attr('data-aviary-media') || '');
if ($bio.length) {
if (!AV_MEDIA.test(mediaId)) {
sectionAfter($bio).after($awards);
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 $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();
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');
}
}


// Class/team rail rides with awards (after Biography).
$('.mw-parser-output .ia-embed[data-archive-id]').each(function () {
var $related = $out.find('.soldier-related').first();
var $slot = $(this);
if ($related.length && !$related.data('rbRelatedPlaced')) {
if ($slot.data('rbIaEmbed')) {
$related.data('rbRelatedPlaced', 1);
return;
var $awardsNow = $out.find('.soldier-awards-section').first();
}
if ($awardsNow.length) {
var itemId = String($slot.attr('data-archive-id') || '');
$awardsNow.after($related);
if (!IA_ID.test(itemId)) {
return;
}
$slot.data('rbIaEmbed', 1);
var title = String($slot.attr('data-title') || 'Internet Archive media');
var playLabel = String($slot.attr('data-play-label') || 'Play interview');
var $fallback = $slot.find('.ia-embed-fallback').first();
var $btn = $('<button type="button" class="ia-embed-play"></button>')
.attr('aria-label', playLabel + ' — ' + title)
.append($('<span class="ia-embed-play-icon" aria-hidden="true"></span>'))
.append($('<span class="ia-embed-play-text"></span>').text(playLabel));
var $poster = $('<div class="ia-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));
$poster.on('click', function (ev) {
}).first();
ev.preventDefault();
if ($bio2.length) {
playInternetArchive($slot, itemId, title);
sectionAfter($bio2).after($related);
});
}
});
}
}());
 
(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 $awards = $out.find('.soldier-awards-section').first();
if (!$out.length) {
return;
}
}


// Story cue belongs under the lead (above Biography).
function sectionAfter($h) {
var $storyCue = $out.find('.soldier-story-cue').first();
var $nodes = $h.nextUntil('h2');
if ($storyCue.length && !$storyCue.data('rbStoryCuePlaced')) {
if ($nodes.length) {
$storyCue.data('rbStoryCuePlaced', 1);
return $nodes.last();
var $leadCue = $out.find('.soldier-lead, .page-lead').first();
if ($leadCue.length) {
$leadCue.after($storyCue);
}
}
return $h;
}
}


// Interview / film cue sits with the story cue under the lead.
if ($awards.length && !$awards.data('rbAwardsPlaced')) {
var $mediaCue = $out.find('.soldier-media-cue').first();
$awards.data('rbAwardsPlaced', 1);
if ($mediaCue.length && !$mediaCue.data('rbMediaCuePlaced')) {
var $bio = $out.find('h2').filter(function () {
$mediaCue.data('rbMediaCuePlaced', 1);
var t = $(this).find('.mw-headline').text() || $(this).text();
var $afterStory = $out.find('.soldier-story-cue').first();
return /^(Biography|Life)\b/i.test($.trim(t));
if ($afterStory.length) {
}).first();
$afterStory.after($mediaCue);
if ($bio.length) {
sectionAfter($bio).after($awards);
} else {
} else {
var $leadMedia = $out.find('.soldier-lead, .page-lead').first();
var $before = $out.find('h2').filter(function () {
if ($leadMedia.length) {
var t = $(this).find('.mw-headline').text() || $(this).text();
$leadMedia.after($mediaCue);
return /^(Gallery|Research notes|Sources|Technical record)\b/i.test($.trim(t));
}).first();
if ($before.length) {
$before.before($awards);
} else {
$out.append($awards);
}
}
}
}
}
}


// Contents box belongs after the lead on soldier/profile pages only.
// Class/team rail rides with awards (after Biography).
// Other pages (e.g. Site changelog) keep MediaWiki's default TOC placement.
var $related = $out.find('.soldier-related').first();
var $toc = $out.find('#toc, .toc, .mw-table-of-contents').first();
if ($related.length && !$related.data('rbRelatedPlaced')) {
if ($toc.length && !$toc.data('rbTocPlaced')) {
$related.data('rbRelatedPlaced', 1);
$toc.data('rbTocPlaced', 1);
var $awardsNow = $out.find('.soldier-awards-section').first();
var $lead = $out.find('.soldier-lead, .page-lead, .home-lead').first();
if ($awardsNow.length) {
if ($lead.length) {
$awardsNow.after($related);
$lead.after($toc);
} else {
} else {
var $ibox = $out.find('table.soldier-infobox, table.infobox').first();
var $bio2 = $out.find('h2').filter(function () {
if ($ibox.length) {
var t = $(this).find('.mw-headline').text() || $(this).text();
$ibox.after($toc);
return /^(Biography|Life)\b/i.test($.trim(t));
}).first();
if ($bio2.length) {
sectionAfter($bio2).after($related);
}
}
}
}
}
}
}());


(function moveSoldierFooterCues() {
// Story cue belongs under the lead (above Biography).
var $out = $('.mw-parser-output').first();
var $storyCue = $out.find('.soldier-story-cue').first();
if (!$out.length) {
if ($storyCue.length && !$storyCue.data('rbStoryCuePlaced')) {
return;
$storyCue.data('rbStoryCuePlaced', 1);
}
var $leadCue = $out.find('.soldier-lead, .page-lead').first();
var $notes = $out.find('.research-notes-disclaimer').first();
if ($leadCue.length) {
var $sources = $out.find('.source-confidence-cue').first();
$leadCue.after($storyCue);
if (!$notes.length && !$sources.length) {
}
return;
}
var $footer = $out.find('.soldier-page-footer-notes').first();
if (!$footer.length) {
$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 = {
// Interview / film cue sits with the story cue under the lead.
von: 1, van: 1, de: 1, der: 1, den: 1, da: 1, di: 1, du: 1,
var $mediaCue = $out.find('.soldier-media-cue').first();
la: 1, le: 1, del: 1, della: 1, ter: 1, ten: 1, zu: 1, zum: 1, bin: 1
if ($mediaCue.length && !$mediaCue.data('rbMediaCuePlaced')) {
};
$mediaCue.data('rbMediaCuePlaced', 1);
var NAME_SUFFIX = {
var $afterStory = $out.find('.soldier-story-cue').first();
jr: 'Jr', 'jr.': 'Jr.', sr: 'Sr', 'sr.': 'Sr.',
if ($afterStory.length) {
esq: 'Esq', 'esq.': 'Esq.', ii: 'II', iii: 'III', iv: 'IV'
$afterStory.after($mediaCue);
};
} else {
function lettersOnly(s) {
var $leadMedia = $out.find('.soldier-lead, .page-lead').first();
return String(s || '').replace(/[^A-Za-z]/g, '');
if ($leadMedia.length) {
}
$leadMedia.after($mediaCue);
function isAllCaps(s) {
}
var letters = lettersOnly(s);
}
return letters.length >= 1 && letters === letters.toUpperCase();
}
}
 
function isAllLower(s) {
// Contents box belongs after the lead on soldier/profile pages only.
var letters = lettersOnly(s);
// Other pages (e.g. Site changelog) keep MediaWiki's default TOC placement.
return letters.length >= 2 && letters === letters.toLowerCase();
var $toc = $out.find('#toc, .toc, .mw-table-of-contents').first();
}
if ($toc.length && !$toc.data('rbTocPlaced')) {
function titleCaseWord(s) {
$toc.data('rbTocPlaced', 1);
s = String(s || '').toLowerCase();
var $lead = $out.find('.soldier-lead, .page-lead, .home-lead').first();
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
if ($lead.length) {
}
$lead.after($toc);
function prettyCapsWord(word) {
} else {
var trail = '';
var $ibox = $out.find('table.soldier-infobox, table.infobox').first();
while (word && /[.,]$/.test(word)) {
if ($ibox.length) {
trail = word.slice(-1) + trail;
$ibox.after($toc);
word = word.slice(0, -1);
}
}
}
}
var up = lettersOnly(word).toUpperCase();
}());
if (up.length >= 5 && up.slice(0, 2) === 'MC') {
 
return 'Mc' + titleCaseWord(up.slice(2)) + trail;
(function moveSoldierFooterCues() {
var $out = $('.mw-parser-output').first();
if (!$out.length) {
return;
}
}
if (up.length >= 7 && up.slice(0, 3) === 'MAC') {
var $notes = $out.find('.research-notes-disclaimer').first();
return 'Mac' + titleCaseWord(up.slice(3)) + trail;
var $sources = $out.find('.source-confidence-cue').first();
if (!$notes.length && !$sources.length) {
return;
}
}
return titleCaseWord(up) + trail;
var $footer = $out.find('.soldier-page-footer-notes').first();
}
if (!$footer.length) {
function prettyNameToken(word, leading) {
$footer = $('<div class="soldier-page-footer-notes"></div>');
word = $.trim(word);
$out.append($footer);
if (!word) {
return word;
}
}
if (/\d/.test(word)) {
if ($notes.length) {
return word;
$footer.append($notes);
}
}
if (word.indexOf('-') !== -1 && word.charAt(0) !== '-' && word.slice(-1) !== '-') {
if ($sources.length) {
return $.map(word.split('-'), function (part, i) {
$footer.append($sources);
return prettyNameToken(part, leading && i === 0);
}).join('-');
}
}
var apos = word.indexOf("'") !== -1 ? "'" : (word.indexOf('\u2019') !== -1 ? '\u2019' : '');
}());
if (apos) {
 
var bits = word.split(apos);
var NAME_PARTICLES = {
return prettyNameToken(bits[0], leading) + apos + prettyNameToken(bits.slice(1).join(apos), false);
von: 1, van: 1, de: 1, der: 1, den: 1, da: 1, di: 1, du: 1,
}
la: 1, le: 1, del: 1, della: 1, ter: 1, ten: 1, zu: 1, zum: 1, bin: 1
if (/^(?:[A-Za-z]\.)+$/.test(word)) {
};
return (isAllCaps(word) || isAllLower(word)) ? word.toUpperCase() : word;
var NAME_SUFFIX = {
}
jr: 'Jr', 'jr.': 'Jr.', sr: 'Sr', 'sr.': 'Sr.',
var low = word.toLowerCase();
esq: 'Esq', 'esq.': 'Esq.', ii: 'II', iii: 'III', iv: 'IV'
if (NAME_SUFFIX[low]) {
};
return NAME_SUFFIX[low];
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);
}
var up = lettersOnly(word).toUpperCase();
if (up.length >= 5 && up.slice(0, 2) === 'MC') {
return 'Mc' + titleCaseWord(up.slice(2)) + trail;
}
}
var bare = lettersOnly(word);
if (up.length >= 7 && up.slice(0, 3) === 'MAC') {
if (bare.length === 1) {
return 'Mac' + titleCaseWord(up.slice(3)) + trail;
return bare.toUpperCase() + word.replace(/[A-Za-z]/, '');
}
}
var lowBare = bare.toLowerCase();
return titleCaseWord(up) + trail;
if (NAME_SUFFIX[lowBare] && /^[A-Za-z]+\.?$/.test(word)) {
}
function prettyNameToken(word, leading) {
word = $.trim(word);
if (!word) {
return word;
}
if (/\d/.test(word)) {
return word;
}
if (word.indexOf('-') !== -1 && word.charAt(0) !== '-' && word.slice(-1) !== '-') {
return $.map(word.split('-'), function (part, i) {
return prettyNameToken(part, leading && i === 0);
}).join('-');
}
var apos = word.indexOf("'") !== -1 ? "'" : (word.indexOf('\u2019') !== -1 ? '\u2019' : '');
if (apos) {
var bits = word.split(apos);
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]/, '');
}
var lowBare = bare.toLowerCase();
if (NAME_SUFFIX[lowBare] && /^[A-Za-z]+\.?$/.test(word)) {
var canon = NAME_SUFFIX[lowBare];
var canon = NAME_SUFFIX[lowBare];
if (word.slice(-1) === '.' && canon.slice(-1) !== '.') {
if (word.slice(-1) === '.' && canon.slice(-1) !== '.') {
Line 1,031: 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,274: 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,300: 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 3,350: Line 3,503:
var next;
var next;
var trimmed = $.trim(text);
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) {
if (!trimmed || trimmed.indexOf(PLACEHOLDER) !== -1) {
next = bullet + '\n';
next = bullet + '\n';
} else if (trimmed.indexOf('[[Media:' + filename + '|') !== -1 ||
trimmed.indexOf('[[File:' + filename + '|') !== -1) {
next = text;
} else {
} else {
next = trimmed + '\n' + bullet + '\n';
next = trimmed + '\n' + bullet + '\n';
Line 3,373: Line 3,530:
return api.postWithToken('csrf', params).then(function (editData) {
return api.postWithToken('csrf', params).then(function (editData) {
if (editData.edit && editData.edit.result === 'Success') {
if (editData.edit && editData.edit.result === 'Success') {
return editData;
return purgeLibraryPages(api).then(function () {
return editData;
});
}
}
if (editData.error && editData.error.code === 'editconflict') {
if (editData.error && editData.error.code === 'editconflict') {
Line 3,383: Line 3,542:
}
}


$form.on('submit', function (evt) {
function failMessage(code, data) {
evt.preventDefault();
if (data && data.error && data.error.info) {
var file = $file[0].files[0];
return data.error.info;
if (!file) {
}
setStatus('Choose a file first.', true);
if (data && data.exception && data.exception.message) {
return;
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.';
}
}
if (!DOC_EXT.test(file.name)) {
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);
setStatus('File type not allowed (PDF, Office docs, images, TIFF).', true);
return;
return;
}
}
var maxBytes = 120 * 1024 * 1024;
var maxBytes = 120 * 1024 * 1024;
if (file.size > maxBytes) {
if (file.size > maxBytes) {
setStatus('File is too large (max about 120 MB). Split the PDF or compress it, then try again.', true);
setStatus('File is too large (max about 120 MB). Split the PDF or compress it, then try again.', true);
return;
return;
}
}
var displayTitle = $.trim($form.find('.camp-manual-title').val() || '');
var displayTitle = $.trim($form.find('.camp-manual-title').val() || '');
var note = $.trim($form.find('.camp-manual-note').val() || '');
var note = $.trim($form.find('.camp-manual-note').val() || '');
$submit.prop('disabled', true);
$submit.prop('disabled', true);
resetProgress();
resetProgress();
setProgress(0, file.size);
setProgress(0, file.size);
setStatus('Uploading…');
setStatus('Uploading…');
 
 
var api = new mw.Api();
var api = new mw.Api({ ajax: { timeout: 0 } });
var fileText = 'Camp Ritchie / MITC training material.';
var fileText = 'Camp Ritchie / MITC training material.';
var uploadParams = {
var uploadedName = null;
action: 'upload',
var uploadParams = {
filename: file.name,
filename: file.name,
comment: 'Uploaded to Training materials library',
comment: 'Uploaded to Training materials library',
text: fileText,
text: fileText,
ignorewarnings: true,
ignorewarnings: true
file: file,
};
format: 'json'
 
};
function doUpload() {
api.postWithToken('csrf', uploadParams, {
/* Multipart + XHR progress (api.upload alone often has no progress events). */
contentType: 'multipart/form-data',
return api.postWithToken('csrf', $.extend({
timeout: 0,
action: 'upload',
xhr: function () {
file: file,
var xhr = $.ajaxSettings.xhr();
format: 'json'
if (xhr.upload) {
}, uploadParams), {
xhr.upload.addEventListener('progress', function (evt) {
contentType: 'multipart/form-data',
if (evt.lengthComputable) {
timeout: 0,
setProgress(evt.loaded, evt.total);
xhr: function () {
}
var xhr = $.ajaxSettings.xhr();
});
if (xhr.upload) {
}
xhr.upload.addEventListener('progress', function (ev) {
return xhr;
if (ev.lengthComputable) {
}
setProgress(ev.loaded, ev.total);
}).then(function (data) {
}
var uploaded = data.upload && data.upload.filename;
});
if (!uploaded) {
}
throw new Error((data.error && data.error.info) || 'Upload failed');
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');
$progress.addClass('is-indeterminate');
$progressFill.css('width', '100%');
$progressLabel.text('Finishing…');
$progressLabel.text('Finishing…');
setStatus('Adding to Library list…');
setStatus('Adding to Library list…');
return appendToLibrary(api, uploaded, displayTitle, note);
return appendToLibrary(api, uploadedName, displayTitle, note);
}).done(function () {
}).done(function () {
setProgress(file.size, file.size);
$progress.removeClass('is-indeterminate');
$progress.removeClass('is-indeterminate');
$progressLabel.text('100% · done');
$progressLabel.text('100% · done');
Line 3,448: Line 3,835:
window.location.reload();
window.location.reload();
}).fail(function (code, data) {
}).fail(function (code, data) {
var err = (data && data.error && data.error.info) ||
var err = failMessage(code, data);
(data && data.exception && data.exception.message) ||
if (uploadedName) {
'';
err = 'File uploaded as “' + uploadedName + '”, but the Library list was not updated: ' + err +
if (!err || err === 'http' || err === 'error' || err === 'timeout') {
' Refresh this page; if it is missing from the list, say so and we can add it.';
err = 'Upload failed (connection dropped or timed out). For large PDFs, wait and retry; if it keeps failing, try a smaller file or Special:Upload.';
}
}
setStatus(String(err), true);
setStatus(String(err), true);
Line 3,499: 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);
}());