]+id=["']cite[-_]note[-_]([^"']+)[^>]+>(?![^/]+?[↑^])/ig, function (match, id) {
var fnReturnMatch = '';
try {
var fnSearchRegxp = new RegExp('id=["' + "'](cite[-_]ref[-_]" + id.replace(/[-_()+?]/g, '[-_()]+?') + '[^"' + "']*)", 'i');
fnReturnMatch = htmlArticle.match(fnSearchRegxp);
} catch (err) {
console.error('Error constructiong regular expression in app.js', err);
}
var fnReturnID = fnReturnMatch ? fnReturnMatch[1] : '';
return match + '\r\n
^ ';
});
// Exempt Nautilus and YouTube based ZIMs from stylesheet preloading
var nautilus = params.contentInjectionMode === 'serviceworker'
? htmlArticle.match(/\r\n' +
'\r\n$1');
}
// Prevent the script that detects whether wombat is loaded from running
if (params.zimType === 'zimit') htmlArticle = htmlArticle.replace(/!(window._WBWombat)/, '$1');
// Add doctype if missing so that scripts run in standards mode
// (quirks mode prevents katex from running, and is incompatible with jQuery)
transformedHTML = !/^\s*(?:\n' + htmlArticle : htmlArticle;
transDirEntry = dirEntry;
// We will need the encoded URL on article load so that we can set the iframe's src correctly,
// but we must not encode the '/' character or else relative links may fail [kiwix-js #498]
var encodedUrl = dirEntry.url.replace(/[^/]+/g, function (matchedSubstring) {
return encodeURIComponent(matchedSubstring);
});
// If the request was not initiated by an existing controlled window, we instantiate the request here
if (!messageChannelWaiting) {
// We put the ZIM filename as a prefix in the URL, so that browser caches are separate for each ZIM file
var newLocation = '../' + appstate.selectedArchive.file.name + '/' + dirEntry.namespace + '/' + encodedUrl + (params.zimType === 'zimit' ? '?isKiwixHref' : '');
if (navigator.serviceWorker.controller) {
loaded = false;
articleWindow.location.href = newLocation;
} else {
console.warn('No Service Worker controller found while waiting for transformed HTML to be loaded! Let\'s wait...');
setTimeout(function () {
document.getElementById('btnHome').click();
}, 1800);
}
}
return;
}
// Write article html to the article container
// articleWindow.document.open('text/html', 'replace');
// articleWindow.document.write(htmlArticle);
// articleWindow.document.close();
if (appstate.target === 'iframe') {
// Store the frame article's target in the top-level window, so that when we retrieve the window with
// history manipulation, we'll know where to place the iframe contentWindow
window.kiwixType = appstate.target;
articleContainer.onload = articleLoaded;
articleContainer.src = 'article.html';
} else {
// Attempt to establish an independent history record for windows (Restricted / window-tab mode)
articleWindow.onpopstate = historyPop;
// The articleWindow has already been set in the click event of the ZIM link and the dummy article was loaded there
// (to avoid popup blockers). Firefox loads windows asynchronously, so we need to wait for onclick load to be fully
// cleared, or else Firefox overwrites the window immediately after we load the html content into it.
setTimeout(articleLoaded, 400);
}
// Failsafe for spinner
setTimeout(function () {
uiUtil.clearSpinner();
}, 6000);
} // End of injectHtml
} // End of displayArticleInForm()
function parseAnchorsJQuery (dirEntry) {
var currentProtocol = articleWindow.location.protocol;
currentProtocol = currentProtocol === 'about:' ? ':' : currentProtocol;
var currentHost = articleWindow.location.host;
// Percent-encode dirEntry.url and add regex escape character \ to the RegExp special characters - see https://www.regular-expressions.info/characters.html;
// NB dirEntry.url can also contain path separator / in some ZIMs (Stackexchange). } and ] do not need to be escaped as they have no meaning on their own.
var escapedUrl = encodeURIComponent(dirEntry.url).replace(/([\\$^.|?*+/()[{])/g, '\\$1');
// Pattern to match a local anchor in an href even if prefixed by escaped url; will also match # on its own
// Note that we exclude any # with a semicolon between it and the end of the string, to avoid accidentally matching e.g. '
var regexpLocalAnchorHref = new RegExp('^(?:#|' + escapedUrl + '#)([^#;]*$)');
Array.prototype.slice.call(articleDocument.querySelectorAll('a, area')).forEach(function (anchor) {
// Attempts to access any properties of 'this' with malformed URLs causes app crash in Edge/UWP [kiwix-js #430]
try {
var testHref = anchor.href;
} catch (err) {
console.error('Malformed href caused error:' + err.message);
return;
}
var href = anchor.getAttribute('href');
if (href === null || href === undefined || /^javascript:/i.test(anchor.protocol)) return;
var anchorTarget = href.match(regexpLocalAnchorHref);
if (href.length === 0) {
// It's a link with an empty href, pointing to the current page: do nothing.
} else if (anchorTarget) {
// It's a local anchor link : remove escapedUrl if any (see above)
anchor.setAttribute('href', '#' + anchorTarget[1]);
} else if (anchor.protocol && anchor.protocol !== currentProtocol || anchor.host && anchor.host !== currentHost) {
// It's an external URL : we should open it in a new tab
anchor.addEventListener('click', function (event) {
if (anchor.protocol === 'bingmaps:') {
anchor.removeAttribute('target');
event.preventDefault();
window.location = href;
} else {
// Find the closest enclosing A tag
var clickedAnchor = uiUtil.closestAnchorEnclosingElement(event.target);
uiUtil.warnAndOpenExternalLinkInNewTab(event, clickedAnchor);
}
});
} else {
// Intercept YouTube videos in Zimit archives
if (params.zimType === 'zimit' && /youtu(?:be(?:-nocookie)?\.com|\.be)\//i.test(href)) {
transformZimit.transformVideoUrl(href, articleDocument, function (transHref) {
addListenersToLink(anchor, transHref, params.baseURL);
});
} else {
addListenersToLink(anchor, href, params.baseURL);
}
}
});
// Add event listeners to the main heading so user can open current document in new tab or window by clicking on it
if (articleWindow.document.body) {
var h1 = articleWindow.document.body.querySelector('h1');
if (h1 && dirEntry) addListenersToLink(h1, encodeURIComponent(dirEntry.url.replace(/[^/]+\//g, '')), params.baseURL);
}
}
function loadCSSJQuery () {
// Ensure all sections are open for clients that lack JavaScript support, or that have some restrictive CSP [kiwix-js #355].
// This is needed only for some versions of ZIM files generated by mwoffliner (at least in early 2018), where the article sections are closed by default on small screens.
// These sections can be opened by clicking on them, but this is done with some javascript.
// The code below is a workaround we still need for compatibility with ZIM files generated by mwoffliner in 2018.
// A better fix has been made for more recent ZIM files, with the use of noscript tags : see https://github.com/openzim/mwoffliner/issues/324
var collapsedBlocks = articleDocument.querySelectorAll('.collapsible-block:not(.open-block), .collapsible-heading:not(.open-block)');
// Using decrementing loop to optimize performance : see https://stackoverflow.com/questions/3520688
for (var i = collapsedBlocks.length; i--;) {
collapsedBlocks[i].classList.add('open-block');
}
var cssCount = 0;
var cssFulfilled = 0;
Array.prototype.slice.call(articleDocument.querySelectorAll('link[data-kiwixurl]')).forEach(function (link) {
cssCount++;
var linkUrl = link.getAttribute('data-kiwixurl');
var url = decodeURIComponent(/zimit/.test(appstate.selectedArchive.zimType) ? linkUrl : uiUtil.removeUrlParameters(linkUrl));
// See if we can get asset from cache. However, if we don't have the link type, the assets cache can fail, so we had better extract the asset instead of getting it from the cache
if (assetsCache.has(url) && link.type) {
var nodeContent = assetsCache.get(url);
uiUtil.feedNodeWithBlob(link, 'href', nodeContent, link.type, true);
cssFulfilled++;
} else {
if (params.assetsCache) document.getElementById('cachingAssets').style.display = '';
appstate.selectedArchive.getDirEntryByPath(url).then(function (dirEntry) {
if (!dirEntry) {
assetsCache.set(url, ''); // Prevent repeated lookups of this unfindable asset
throw new Error('DirEntry ' + typeof dirEntry);
}
var mimetype = dirEntry.getMimetype();
var readFile = /^text\//i.test(mimetype) ? appstate.selectedArchive.readUtf8File : appstate.selectedArchive.readBinaryFile;
return readFile(dirEntry, function (fileDirEntry, content) {
var fullUrl = fileDirEntry.namespace + '/' + fileDirEntry.url;
if (params.assetsCache) assetsCache.set(fullUrl, content);
uiUtil.feedNodeWithBlob(link, 'href', content, mimetype, true);
cssFulfilled++;
renderIfCSSFulfilled(fileDirEntry.url);
});
}).catch(function (e) {
console.error('Could not find DirEntry for link element: ' + url, e);
cssCount--;
renderIfCSSFulfilled();
});
}
});
renderIfCSSFulfilled();
// Some pages are extremely heavy to render, so we prevent rendering by keeping the iframe hidden
// until all CSS content is available [kiwix-js #381]
function renderIfCSSFulfilled (title) {
if (cssFulfilled >= cssCount) {
uiUtil.clearSpinner();
document.getElementById('articleContent').style.display = '';
// We have to resize here for devices with On Screen Keyboards when loading from the article search list
resizeIFrame();
}
}
}
/**
* Add event listeners to a hyperlinked element to extract the linked article or file from the ZIM instead
* of following links
* @param {Node} a The anchor or other linked element to which event listeners will be attached
* @param {String} href The href of the linked element
* @param {String} baseUrl The baseUrl against which relative links will be calculated
*/
function addListenersToLink (a, href, baseUrl) {
appstate.baseUrl = baseUrl;
var uriComponent = uiUtil.removeUrlParameters(href);
// var namespace = baseUrl.replace(/^([-ABCIJMUVWX])\/.+/, '$1');
var loadingContainer = false;
var contentType;
var downloadAttrValue;
// Some file types need to be downloaded rather than displayed (e.g. *.epub)
// The HTML download attribute can be Boolean or a string representing the specified filename for saving the file
// For Boolean values, getAttribute can return any of the following: download="" download="download" download="true"
// So we need to test hasAttribute first: see https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute
// However, we cannot rely on the download attribute having been set, so we also need to test for known download file types
var isDownloadableLink = a.hasAttribute('download') || regexpDownloadLinks.test(href);
if (isDownloadableLink) {
// if (!/UWP/.test(params.appType) && params.contentInjectionMode === 'serviceworker') return;
downloadAttrValue = a.getAttribute('download');
// Normalize the value to a true Boolean or a filename string or true if there is no download attribute
downloadAttrValue = /^(download|true|\s*)$/i.test(downloadAttrValue) || downloadAttrValue || true;
contentType = a.getAttribute('type');
}
// DEV: We need to use the '#' location trick here for cross-browser compatibility with opening a new tab/window
// if (params.windowOpener && a.tagName !== 'IFRAME') a.setAttribute('href', '#' + href);
// Store the current values, as they may be changed if user switches to another tab before returning to this one
var kiwixTarget = appstate.target;
var thisWindow = articleWindow;
var thisContainer = articleContainer;
var reset = function () {
if (appstate.target === 'window') {
// By delaying unblocking of the touch event, we prevent multiple touch events launching the same window
a.touched = false;
a.newcontainer = false;
}
loadingContainer = false;
a.articleisloading = false;
a.dataset.touchevoked = false;
a.popoverisloading = false;
};
var onDetectedClick = function (e) {
// Restore original values for this window/tab
appstate.target = kiwixTarget;
articleWindow = thisWindow;
articleContainer = thisContainer;
var isNautilusPopup = a.dataset.popup && !/0|false/i.test(a.dataset.popup);
if (a.tagName === 'H1' || isNautilusPopup) {
// We have registered a click on the header or on a dynamic link (e.g. in Nautilus archives)
if (isNautilusPopup) {
// Pop-up window sometimes opens out of view, so we have to scroll into view
iframe.contentWindow.scrollTo({
top: '0',
behavior: 'smooth'
});
}
if (!a.newcontainer) return; // A new tab wasn't requested, so ignore
}
if (params.windowOpener) {
// This processes Ctrl-click, Command-click, the long-press event, and middle-click
if (a.newcontainer) {
// We open the new window immediately so that it is a direct result of user action (click)
// and we'll populate it later - this avoids most popup blockers
loadingContainer = true;
articleContainer = window.open('article.html', params.windowOpener === 'tab' ? '_blank' : a.title,
params.windowOpener === 'window' ? 'toolbar=0,location=0,menubar=0,width=800,height=600,resizable=1,scrollbars=1' : null);
appstate.target = 'window';
// We have to make this conditional, because sometimes this action is blocked by the browser
if (articleContainer) {
articleContainer.kiwixType = appstate.target;
articleWindow = articleContainer;
}
}
}
e.preventDefault();
e.stopPropagation();
anchorParameter = href.match(/#([^#;]+)$/);
anchorParameter = anchorParameter ? anchorParameter[1] : '';
var indexRoot = window.location.pathname.replace(/[^/]+$/, '') + encodeURI(appstate.selectedArchive.file.name) + '/';
var zimRoot = indexRoot.replace(/^.+?\/www\//, '/');
var zimUrl = href;
var zimUrlFullEncoding;
// Some URLs are incorrectly given with spaces at the beginning and end, so we remove these
zimUrl = zimUrl.replace(/^\s+|\s+$/g, '');
if (/zimit/.test(params.zimType)) {
// Deal with root-relative URLs in zimit ZIMs
if (!zimUrl.indexOf(indexRoot)) { // If begins with indexRoot
zimUrl = zimUrl.replace(indexRoot, '').replace('#' + anchorParameter, '');
} else if (!zimUrl.indexOf(zimRoot)) { // If begins with zimRoot
zimUrl = zimUrl.replace(zimRoot, '').replace('#' + anchorParameter, '');
} else if (/^\//.test(zimUrl)) {
zimUrl = zimUrl.replace(/^\//, appstate.selectedArchive.zimitPseudoContentNamespace + appstate.selectedArchive.zimitPrefix.replace(/^A\//, ''));
} else if (!~zimUrl.indexOf(appstate.selectedArchive.zimitPseudoContentNamespace)) { // Doesn't begin with pseudoContentNamespace
// Zimit ZIMs store URLs percent-encoded and with querystring and
// deriveZimUrlFromRelativeUrls strips any querystring and decodes
var zimUrlToTransform = zimUrl;
zimUrl = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, baseUrl)) +
href.replace(uriComponent, '').replace('#' + anchorParameter, '');
zimUrlFullEncoding = encodeURI(uiUtil.deriveZimUrlFromRelativeUrl(zimUrlToTransform, baseUrl) +
href.replace(uriComponent, '').replace('#' + anchorParameter, ''));
}
} else {
zimUrl = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, baseUrl);
}
// @TODO: We are getting double activations of the click event. This needs debugging. For now, we use a flag to prevent this.
a.newcontainer = true; // Prevents double activation
// uiUtil.showSlidingUIElements();
// Tear down contents of articleWindow.document
if (!/UWP/.test(params.appType) && articleWindow && articleWindow.document && articleWindow.document.body) {
articleWindow.document.body.innerHTML = '';
}
goToArticle(zimUrl, downloadAttrValue, contentType, zimUrlFullEncoding);
setTimeout(reset, 1400);
};
var darkTheme = (params.cssUITheme == 'auto' ? cssUIThemeGetOrSet('auto', true) : params.cssUITheme) !== 'light';
/* Event processing */
a.addEventListener('touchstart', function (e) {
// console.debug('a.touchstart');
var timeout = 500;
if (!appstate.wikimediaZimLoaded || !params.showPopoverPreviews) {
if (!params.windowOpener || a.touched) return;
loadingContainer = true;
} else {
timeout = 200;
}
a.touched = true;
var event = e;
// The link will be clicked if the user long-presses for more than 500ms (if the option is enabled), or 200ms for popover
setTimeout(function () {
// DEV: appstate.startVector indicates that the app is processing a touch zoom event, so we cancel any new windows
// see uiUtil.pointermove_handler
if (!a.touched || a.newcontainer || appstate.startVector) return;
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
a.dataset.touchevoked = true;
popovers.populateKiwixPopoverDiv(event, a, appstate, darkTheme, appstate.selectedArchive);
} else {
a.newcontainer = true;
onDetectedClick(event);
}
event.preventDefault();
}, timeout);
}, { passive: false });
a.addEventListener('touchend', function () {
// console.debug('a.touchend');
a.touched = false;
a.newcontainer = false;
loadingContainer = false;
// Cancel any popovers because user has clicked
a.articleisloading = true;
setTimeout(reset, 1000);
});
// This detects right-click in all browsers (only if the option is enabled)
a.addEventListener('contextmenu', function (e) {
// console.debug('contextmenu');
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
e.preventDefault();
e.stopPropagation();
// console.debug('suppressed contextmenu because processing popovers');
var kiwixPopover = e.target.ownerDocument.querySelector('.kiwixtooltip');
if (kiwixPopover) {
// return;
} else if (!a.touched) {
a.touched = true;
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
}
} else {
if (!params.windowOpener) return;
if (params.rightClickType === 'double' && !a.touched) {
a.touched = true;
setTimeout(function () {
a.touched = false;
}, 700);
} else {
if (a.newcontainer) return; // Prevent accidental double activation
e.preventDefault();
e.stopPropagation();
a.newcontainer = true;
a.touched = false;
onDetectedClick(e);
}
}
});
// This traps the middle-click event before tha auxclick event fires
a.addEventListener('mousedown', function (e) {
// console.debug('a.mousedown');
a.dataset.touchevoked = true; // This is needed to simulate touch events in UWP app
if (!params.windowOpener) return;
e.preventDefault();
e.stopPropagation();
if (a.touched || a.newcontainer) return; // Prevent double activations
if (e.ctrlKey || e.metaKey || e.which === 2 || e.button === 4) {
a.newcontainer = true;
onDetectedClick(e);
} else {
// console.debug('suppressed mousedown');
}
});
a.addEventListener('mouseup', function (e) {
setTimeout(reset, 1400); // Needed for UWP app which doesn't have touch events, so touchevoked simulates them
});
// This detects the middle-click event that opens a new tab in recent Firefox and Chrome
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event
a.addEventListener('auxclick', function (e) {
// console.debug('a.auxclick');
if (!params.windowOpener) return;
e.preventDefault();
e.stopPropagation();
});
// The popover feature requires as a minimum that the browser supports the css matches function
// (having this condition prevents very erratic popover placement in IE11, for example, so the feature is disabled)
if (appstate.wikimediaZimLoaded && params.showPopoverPreviews && 'matches' in Element.prototype) {
// Prevent accidental selection of the anchor text in some contexts
if (a.style.userSelect === undefined && appstate.wikimediaZimLoaded && params.showPopoverPreviews) {
// This prevents selection of the text in a touched link in iOS Safari
a.style.webkitUserSelect = 'none';
a.style.msUserSelect = 'none';
}
a.addEventListener('mouseover', function (e) {
// console.debug('a.mouseover');
if (a.dataset.touchevoked === 'true') return;
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
});
a.addEventListener('mouseout', function (e) {
if (a.dataset.touchevoked === 'true') return;
popovers.removeKiwixPopoverDivs(e.target.ownerDocument);
setTimeout(reset, 1000);
});
a.addEventListener('focus', function (e) {
setTimeout(function () { // Delay focus event so touchstart can fire first
// console.debug('a.focus');
if (a.touched) return;
a.focused = true;
popovers.populateKiwixPopoverDiv(e, a, appstate, darkTheme, appstate.selectedArchive);
}, 200);
});
a.addEventListener('blur', function (e) {
// console.debug('a.blur');
a.focused = false;
setTimeout(reset, 1400);
});
}
// The main click routine (called by other events above as well)
a.addEventListener('click', function (e) {
console.log('a.click', e);
// Cancel any popovers because user has clicked
a.articleisloading = true;
// Prevent opening multiple windows
if (loadingContainer || a.touched) {
e.preventDefault();
e.stopPropagation();
} else {
onDetectedClick(e);
}
});
}
/**
* Unhides all hidden divs or tables, for use in Wikimedia mobile display style, which hides some crucial
* elements that users want optionally to be able to access
*/
function displayHiddenBlockElements (win, doc) {
if (!doc) return;
console.debug('Searching for hidden block elements to display...');
Array.prototype.slice.call(doc.querySelectorAll('table, div')).forEach(function (element) {
if (win.getComputedStyle(element).display === 'none') {
element.style.setProperty('display', 'block', 'important');
if (!params.noHiddenElementsWarning) {
var message;
if (!appstate.wikimediaZimLoaded) {
message = '
The way the Display hidden block elements setting works has changed! Because it is currently set ' +
'to always, it will now apply to any ZIM type. This can have unexpected effects in non-Wikipedia ZIMs.
' +
'
We strongly recommend that you change this setting to auto in Configuration. The new auto setting allows the ' +
'app to decide when to apply the setting. If you never want to see hidden elements, even in Wikimedia ZIMs, change the ' +
'setting to never.
';
}
if (message) {
message += '
This message will not be displayed again, unless you reset the app.
';
params.noHiddenElementsWarning = true;
uiUtil.systemAlert(message, 'One-time message!').then(function () {
settingsStore.setItem('noHiddenElementsWarning', true, Infinity);
});
}
}
}
});
// Ensure images are picked up by lazy loading
win.scrollBy(0, 5);
win.scrollBy(0, -5);
}
var dropup = document.getElementById('dropup');
dropup.addEventListener('click', function () {
var ToCList = document.getElementById('ToCList');
ToCList.style.display = ToCList.style.display === 'block' ? 'none' : 'block';
});
function setupTableOfContents () {
var iframe = document.getElementById('articleContent');
var innerDoc = iframe.contentDocument;
var tableOfContents = new uiUtil.ToC(innerDoc);
var headings = tableOfContents.getHeadingObjects();
dropup.style.fontSize = ~~(params.relativeUIFontSize * 0.14) + 'px';
var dropupHtml = '';
headings.forEach(function (heading) {
if (/^h1$/i.test(heading.tagName)) {
dropupHtml += '
' + heading.textContent + '';
} else if (/^h2$/i.test(heading.tagName)) {
dropupHtml += '
' + heading.textContent + '';
} else if (/^h3$/i.test(heading.tagName)) {
dropupHtml += '
' + heading.textContent + '';
} else if (/^h4$/i.test(heading.tagName)) {
dropupHtml += '
' + heading.textContent + '';
}
// Skip smaller headings (if there are any) to avoid making list too long
});
var ToCList = document.getElementById('ToCList');
ToCList.style.maxHeight = ~~(window.innerHeight * 0.75) + 'px';
ToCList.style.marginLeft = ~~(window.innerWidth / 2) - ~~(window.innerWidth * 0.16) + 'px';
ToCList.innerHTML = dropupHtml;
Array.prototype.slice.call(ToCList.getElementsByTagName('a')).forEach(function (listElement) {
listElement.addEventListener('click', function () {
var sectionEle = innerDoc.getElementById(this.dataset.headingId);
var csec = util.closest(sectionEle, 'details, section');
csec = csec && /DETAILS|SECTION/.test(csec.parentElement.tagName) ? csec.parentElement : csec;
openAllSections(true, csec);
// Scroll to element
sectionEle.scrollIntoView();
// Scrolling up then down ensures that the toolbars show according to user settings
iframe.contentWindow.scrollBy(0, -5);
setTimeout(function () {
iframe.contentWindow.scrollBy(0, 5);
iframe.contentWindow.focus();
}, 250);
ToCList.style.display = 'none';
});
});
}
/**
* Sets the state of collapsible sections for the iframe document, or for the given node
* @param {Boolean} override An optional value that overrides params.openAllSections (true to open, false to close)
* @param {Node} node An optional node within which elements will be opened or closed (this will normally be a details element)
*/
// Sets state of collapsible sections
function openAllSections (override, node) {
var open = override === false ? false : override || params.openAllSections;
var container = node || articleDocument;
if (container) {
var blocks = container.querySelectorAll('details, section:not([data-mw-section-id="0"]), .collapsible-block, .collapsible-heading');
if (node) processSection(open, node);
for (var x = blocks.length; x--;) {
processSection(open, blocks[x]);
}
}
}
function processSection (open, node) {
if (/DETAILS|SECTION/.test(node.tagName)) {
if (open) {
node.setAttribute('open', '');
node.style.display = '';
} else {
node.removeAttribute('open');
}
if (typeof HTMLDetailsElement === 'undefined' || node.tagName === 'SECTION') {
var children = node.children;
for (var y = children.length; y--;) {
if (/SUMMARY|H\d/.test(children[y].tagName)) continue;
if (open) {
if (/DETAILS|SECTION/.test(children[y].tagName)) children[y].setAttribute('open', '');
children[y].style.removeProperty('display');
} else {
if (/DETAILS|SECTION/.test(children[y].tagName)) children[y].removeAttribute('open');
children[y].style.display = 'none';
}
}
}
} else {
if (open) node.classList.add('open-block');
else node.classList.remove('open-block');
}
}
// Attach listeners to headers to open-close following sections
function setupHeadings () {
var headings = document.getElementById('articleContent').querySelectorAll('h2, h3, h4, h5');
for (var i = headings.length; i--;) {
// Prevent heading from being selected when user clicks on it
headings[i].style.userSelect = 'none';
headings[i].style.msUserSelect = 'none';
headings[i].addEventListener('click', function (e) {
// Override the built-in simplistic polyfill
e.preventDefault();
var that = e.currentTarget;
var detailsEl = util.closest(that, 'details, section');
if (detailsEl) {
var toggle = !detailsEl.hasAttribute('open');
openAllSections(toggle, detailsEl);
}
});
}
}
params.preloadAllImages = function () {
if (params.preloadingAllImages !== true) {
setTimeout(function () {
if (params.preloadingAllImages) {
uiUtil.pollSpinner('Extracting images...');
}
}, 1000);
params.preloadingAllImages = true;
if (params.imageDisplay) {
params.contentInjectionMode === 'jquery'
? images.prepareImagesJQuery(articleWindow, true) : images.prepareImagesServiceWorker(articleWindow, true);
}
return;
}
// All images should now be loaded, or else user did not request loading images
uiUtil.clearSpinner();
uiUtil.extractHTML();
uiUtil.clearSpinner();
};
/**
* Changes the URL of the browser page, so that the user might go back to it
*
* @param {String} title The title of the article to store (if storing an article)
* @param {String} titleSearch The title of the search (if storing a search)
*/
function pushBrowserHistoryState (title, titleSearch) {
// DEV: Note that appstate.target will always be 'iframe' for title searches, so we do not need to account for that
var targetWin = appstate.target === 'iframe' ? window : articleWindow;
var stateObj = {};
var urlParameters;
var stateLabel;
if (title && !(title === '')) {
// Prevents creating a double history for the same page (wrapped to prevent exception in IE and Edge Legacy for tabs)
try {
if (targetWin.history.state && targetWin.history.state.title === title) return;
} catch (err) { console.error('Unable to access History for this window', err); return; }
stateObj.title = title;
urlParameters = '?title=' + title;
stateLabel = 'Wikipedia Article : ' + title;
} else if (titleSearch && !(titleSearch === '')) {
stateObj.titleSearch = titleSearch;
urlParameters = '?titleSearch=' + titleSearch;
stateLabel = 'Wikipedia search : ' + titleSearch;
} else return;
// Edge Legacy and IE cannot push history state to another window/tab and produce an exception;
// independent navigation history is therefore disabled for these browsers
try {
targetWin.history.pushState(stateObj, stateLabel, urlParameters);
} catch (error) {
history.pushState(stateObj, stateLabel, urlParameters);
}
}
/**
* Extracts the content of the given article pathname, or a downloadable file, from the ZIM
*
* @param {String} path The pathname (namespace + filename) to the article or file to be extracted
* @param {Boolean|String} download A Bolean value that will trigger download of title, or the filename that should
* be used to save the file in local FS (in HTML5 spec, a string value for the download attribute is optional)
* @param {String} contentType The mimetype of the downloadable file, if known
* @param {String} pathEnc The fully encoded version of the path for use with some Zimit archives
*/
function goToArticle (path, download, contentType, pathEnc) {
var pathForServiceWorker = path;
path = path.replace(/\??isKiwixHref/, '');
appstate.expectedArticleURLToBeDisplayed = path;
// This removes any search highlighting
clearFindInArticle();
var shortTitle = path.replace(/[^/]+\//g, '').substring(0, 18);
uiUtil.pollSpinner('Loading ' + shortTitle);
var zimName = appstate.selectedArchive.file.name.replace(/\.[^.]+$/, '').replace(/_\d+-\d+$/, '');
if (~path.indexOf(params.cachedStartPages[zimName])) {
goToMainArticle();
return;
}
appstate.selectedArchive.getDirEntryByPath(path).then(function (dirEntry) {
var mimetype = contentType || dirEntry ? dirEntry.getMimetype() : '';
if (dirEntry === null || dirEntry === undefined) {
uiUtil.clearSpinner();
console.error('Article with title ' + path + ' not found in the archive');
if (params.zimType === 'zimit') {
if (pathEnc) {
// We failed to get path, so we should try the fully encoded version instead
goToArticle(pathEnc, download, contentType);
} else {
var anchor = {
href: path.replace(/^(C\/)?A\//, ''),
target: '_blank'
};
uiUtil.warnAndOpenExternalLinkInNewTab(null, anchor)
setTab();
}
} else {
uiUtil.systemAlert('
Sorry, but we couldn\'t find the article:
' + path + '
in this archive!
');
}
} else if (download || /\/(epub|pdf|zip|.*opendocument|.*officedocument|tiff|mp4|webm|mpeg|octet-stream)\b/i.test(mimetype)) {
// PDFs can be treated as a special case, as they can be displayed directly in a browser window or tab in most browsers (but not UWP)
if (!/UWP/.test(params.appType) && params.contentInjectionMode === 'serviceworker' && (/\/pdf\b/.test(mimetype) || /\.pdf([?#]|$)/i.test(dirEntry.url))) {
window.open(document.location.pathname.replace(/[^/]+$/, '') + appstate.selectedArchive.file.name + '/' + pathForServiceWorker,
params.windowOpener === 'tab' ? '_blank' : 'Download PDF',
params.windowOpener === 'window' ? 'toolbar=0,location=0,menubar=0,width=800,height=600,resizable=1,scrollbars=1' : null);
} else {
download = true;
appstate.selectedArchive.readBinaryFile(dirEntry, function (fileDirEntry, content) {
uiUtil.displayFileDownloadAlert(path, download, mimetype, content);
uiUtil.clearSpinner();
});
}
} else {
// params.isLandingPage = false;
document.querySelectorAll('.alert').forEach(function (el) {
el.style.display = 'none';
});
document.getElementById('welcomeText').style.display = 'none';
resizeIFrame();
readArticle(dirEntry);
}
}).catch(function (e) {
console.error('Error reading article with title ' + path, e);
if (params.appIsLaunching) goToMainArticle();
// Line below prevents bootloop
params.appIsLaunching = false;
});
}
function goToRandomArticle () {
if (appstate.selectedArchive !== null && appstate.selectedArchive.isReady()) {
uiUtil.pollSpinner();
appstate.selectedArchive.getRandomDirEntry(function (dirEntry) {
if (dirEntry === null || dirEntry === undefined) {
uiUtil.clearSpinner();
uiUtil.systemAlert('Error finding random article', 'Error finding article');
} else {
// We fall back to the old A namespace to support old ZIM files without a text/html MIME type for articles
// DEV: If minorVersion is 1, then we are using a v1 article-only title listing. By definition,
// all dirEntries in an article-only listing must be articles.
if (appstate.selectedArchive.file.minorVersion >= 1 || /text\/html\b/i.test(dirEntry.getMimetype()) ||
params.zimType !== 'zimit' && dirEntry.namespace === 'A') {
params.isLandingPage = false;
uiUtil.hideActiveContentWarning();
readArticle(dirEntry);
} else {
// If the random title search did not end up on an article,
// we try again, until we find one
goToRandomArticle();
}
}
});
} else {
// Showing the relevant error message and redirecting to config page for adding the ZIM file
uiUtil.systemAlert('Archive not set: please select an archive', 'No archive selected').then(function () {
document.getElementById('btnConfigure').click();
});
}
}
function goToMainArticle () {
uiUtil.pollSpinner();
params.isLandingPage = true;
appstate.selectedArchive.getMainPageDirEntry(function (dirEntry) {
if (dirEntry === null || dirEntry === undefined) {
params.isLandingPage = false;
console.error('Error finding main article.');
uiUtil.clearSpinner();
document.getElementById('welcomeText').style.display = '';
uiUtil.systemAlert('We cannot find the landing page!
' +
'Please check that this ZIM archive is valid. You may be able to search for other pages in the ZIM above.',
'Main page not found!');
} else {
// DEV: see comment above under goToRandomArticle()
var setMainPage = function (dirEntry) {
params.isLandingPage = true;
appstate.selectedArchive.landingPageUrl = dirEntry.namespace + '/' + dirEntry.url;
readArticle(dirEntry);
}
if (dirEntry.redirect) {
appstate.selectedArchive.resolveRedirect(dirEntry, setMainPage);
} else if (/text/.test(dirEntry.getMimetype()) || dirEntry.namespace === 'A') {
setMainPage(dirEntry);
} else {
params.isLandingPage = false;
console.error('The main page of this archive does not seem to be an article');
uiUtil.clearSpinner();
document.getElementById('welcomeText').style.display = '';
uiUtil.systemAlert('The main page of this archive does not seem to be an article!
' +
'Please check that this ZIM archive is valid. You may be able to search for other pages in the ZIM above.',
'Invalid article!');
}
}
});
}
export default {};