mwoffliner 1.17.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -62
- package/docs/functional_architecture.md +34 -38
- package/extensions/wiktionary_fr.js +18 -17
- package/jest.config.cjs +2 -2
- package/lib/DOMUtils.js +0 -1
- package/lib/DOMUtils.js.map +1 -1
- package/lib/Downloader.d.ts +39 -31
- package/lib/Downloader.js +418 -349
- package/lib/Downloader.js.map +1 -1
- package/lib/Dump.d.ts +24 -10
- package/lib/Dump.js +95 -50
- package/lib/Dump.js.map +1 -1
- package/lib/Gadgets.d.ts +2 -1
- package/lib/Gadgets.js +7 -5
- package/lib/Gadgets.js.map +1 -1
- package/lib/Logger.d.ts +6 -6
- package/lib/Logger.js +33 -33
- package/lib/Logger.js.map +1 -1
- package/lib/MediaWiki.d.ts +14 -31
- package/lib/MediaWiki.js +119 -172
- package/lib/MediaWiki.js.map +1 -1
- package/lib/RedisStore.d.ts +3 -3
- package/lib/RedisStore.js +21 -23
- package/lib/RedisStore.js.map +1 -1
- package/lib/S3.js +1 -1
- package/lib/S3.js.map +1 -1
- package/lib/Templates.d.ts +3 -10
- package/lib/Templates.js +5 -14
- package/lib/Templates.js.map +1 -1
- package/lib/cli.js +7 -6
- package/lib/cli.js.map +1 -1
- package/lib/config.d.ts +10 -16
- package/lib/config.js +33 -55
- package/lib/config.js.map +1 -1
- package/lib/error.manager.d.ts +2 -2
- package/lib/error.manager.js +42 -49
- package/lib/error.manager.js.map +1 -1
- package/lib/i18n.d.ts +2 -0
- package/lib/i18n.js +53 -0
- package/lib/i18n.js.map +1 -0
- package/lib/mutex.d.ts +2 -1
- package/lib/mutex.js +2 -1
- package/lib/mutex.js.map +1 -1
- package/lib/mwoffliner.lib.js +370 -222
- package/lib/mwoffliner.lib.js.map +1 -1
- package/lib/parameterList.d.ts +20 -8
- package/lib/parameterList.js +30 -18
- package/lib/parameterList.js.map +1 -1
- package/lib/renderers/abstract.renderer.d.ts +48 -55
- package/lib/renderers/abstract.renderer.js +377 -98
- package/lib/renderers/abstract.renderer.js.map +1 -1
- package/lib/renderers/action-parse.renderer.d.ts +22 -3
- package/lib/renderers/action-parse.renderer.js +123 -81
- package/lib/renderers/action-parse.renderer.js.map +1 -1
- package/lib/renderers/renderer.builder.js +6 -69
- package/lib/renderers/renderer.builder.js.map +1 -1
- package/lib/renderers/rendering.context.d.ts +2 -3
- package/lib/renderers/rendering.context.js +6 -14
- package/lib/renderers/rendering.context.js.map +1 -1
- package/lib/sanitize-argument.d.ts +7 -4
- package/lib/sanitize-argument.js +99 -41
- package/lib/sanitize-argument.js.map +1 -1
- package/lib/util/FileManager.d.ts +36 -0
- package/lib/util/FileManager.js +246 -0
- package/lib/util/FileManager.js.map +1 -0
- package/lib/util/RedisKvs.js +4 -4
- package/lib/util/RedisKvs.js.map +1 -1
- package/lib/util/RedisQueue.js +1 -1
- package/lib/util/RedisQueue.js.map +1 -1
- package/lib/util/builders/url/action-parse.director.d.ts +2 -2
- package/lib/util/builders/url/action-parse.director.js +7 -7
- package/lib/util/builders/url/action-parse.director.js.map +1 -1
- package/lib/util/builders/url/api.director.d.ts +2 -4
- package/lib/util/builders/url/api.director.js +10 -17
- package/lib/util/builders/url/api.director.js.map +1 -1
- package/lib/util/builders/url/base.director.d.ts +0 -4
- package/lib/util/builders/url/base.director.js +0 -25
- package/lib/util/builders/url/base.director.js.map +1 -1
- package/lib/util/builders/url/basic.director.d.ts +1 -1
- package/lib/util/builders/url/basic.director.js +1 -1
- package/lib/util/builders/url/web.director.d.ts +1 -1
- package/lib/util/builders/url/web.director.js +2 -2
- package/lib/util/builders/url/web.director.js.map +1 -1
- package/lib/util/categories.d.ts +5 -5
- package/lib/util/categories.js +175 -174
- package/lib/util/categories.js.map +1 -1
- package/lib/util/const.d.ts +4 -8
- package/lib/util/const.js +9 -17
- package/lib/util/const.js.map +1 -1
- package/lib/util/customCssJs.d.ts +6 -0
- package/lib/util/customCssJs.js +70 -0
- package/lib/util/customCssJs.js.map +1 -0
- package/lib/util/dump.d.ts +15 -4
- package/lib/util/dump.js +282 -86
- package/lib/util/dump.js.map +1 -1
- package/lib/util/index.d.ts +1 -1
- package/lib/util/index.js +1 -1
- package/lib/util/index.js.map +1 -1
- package/lib/util/metaData.js.map +1 -1
- package/lib/util/misc.d.ts +23 -13
- package/lib/util/misc.js +90 -59
- package/lib/util/misc.js.map +1 -1
- package/lib/util/mw-api.d.ts +7 -5
- package/lib/util/mw-api.js +167 -221
- package/lib/util/mw-api.js.map +1 -1
- package/lib/util/pageListMainPage.d.ts +3 -0
- package/lib/util/pageListMainPage.js +10 -0
- package/lib/util/pageListMainPage.js.map +1 -0
- package/lib/util/pages.d.ts +30 -0
- package/lib/util/pages.js +146 -0
- package/lib/util/pages.js.map +1 -0
- package/lib/util/rewriteUrls.d.ts +2 -2
- package/lib/util/rewriteUrls.js +33 -31
- package/lib/util/rewriteUrls.js.map +1 -1
- package/lib/util/savePages.d.ts +8 -0
- package/lib/util/savePages.js +182 -0
- package/lib/util/savePages.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/offliner-definition.json +178 -90
- package/package.json +50 -49
- package/res/page_list_home.js +20 -0
- package/res/{article_not_found.svg → page_not_found.svg} +1 -1
- package/res/script.js +30 -45
- package/res/style.css +52 -68
- package/res/templates/download_error_placeholder.html +7 -8
- package/res/templates/javaScript.html +19 -0
- package/res/templates/pageFallback.html +9 -7
- package/res/templates/pageVector2022.html +9 -7
- package/res/templates/pageVectorLegacy.html +9 -7
- package/res/templates/{article_list_home.html → page_list_home.html} +1 -2
- package/translation/ar.json +21 -21
- package/translation/bn.json +1 -1
- package/translation/de.json +46 -19
- package/translation/en.json +54 -21
- package/translation/es.json +17 -19
- package/translation/fi.json +23 -3
- package/translation/fr.json +57 -21
- package/translation/he.json +8 -6
- package/translation/ia.json +19 -19
- package/translation/id.json +13 -13
- package/translation/it.json +13 -8
- package/translation/ko.json +55 -3
- package/translation/lb.json +5 -2
- package/translation/mk.json +5 -5
- package/translation/nl.json +57 -22
- package/translation/or.json +2 -2
- package/translation/pt.json +2 -2
- package/translation/qqq.json +55 -22
- package/translation/ru.json +2 -2
- package/translation/sc.json +2 -2
- package/translation/sk.json +62 -0
- package/translation/sl.json +54 -21
- package/translation/sv.json +20 -19
- package/translation/sw.json +45 -2
- package/translation/zh-hans.json +56 -22
- package/translation/zh-hant.json +26 -19
- package/lib/renderers/abstractDesktop.render.d.ts +0 -12
- package/lib/renderers/abstractDesktop.render.js +0 -67
- package/lib/renderers/abstractDesktop.render.js.map +0 -1
- package/lib/renderers/abstractMobile.render.d.ts +0 -12
- package/lib/renderers/abstractMobile.render.js +0 -54
- package/lib/renderers/abstractMobile.render.js.map +0 -1
- package/lib/renderers/rest-api.renderer.d.ts +0 -7
- package/lib/renderers/rest-api.renderer.js +0 -68
- package/lib/renderers/rest-api.renderer.js.map +0 -1
- package/lib/renderers/visual-editor.renderer.d.ts +0 -7
- package/lib/renderers/visual-editor.renderer.js +0 -76
- package/lib/renderers/visual-editor.renderer.js.map +0 -1
- package/lib/renderers/wikimedia-desktop.renderer.d.ts +0 -4
- package/lib/renderers/wikimedia-desktop.renderer.js +0 -7
- package/lib/renderers/wikimedia-desktop.renderer.js.map +0 -1
- package/lib/renderers/wikimedia-mobile.renderer.d.ts +0 -19
- package/lib/renderers/wikimedia-mobile.renderer.js +0 -243
- package/lib/renderers/wikimedia-mobile.renderer.js.map +0 -1
- package/lib/util/articleListMainPage.d.ts +0 -3
- package/lib/util/articleListMainPage.js +0 -10
- package/lib/util/articleListMainPage.js.map +0 -1
- package/lib/util/articles.d.ts +0 -26
- package/lib/util/articles.js +0 -102
- package/lib/util/articles.js.map +0 -1
- package/lib/util/builders/url/desktop.director.d.ts +0 -8
- package/lib/util/builders/url/desktop.director.js +0 -15
- package/lib/util/builders/url/desktop.director.js.map +0 -1
- package/lib/util/builders/url/mobile.director.d.ts +0 -8
- package/lib/util/builders/url/mobile.director.js +0 -15
- package/lib/util/builders/url/mobile.director.js.map +0 -1
- package/lib/util/builders/url/rest-api.director.d.ts +0 -8
- package/lib/util/builders/url/rest-api.director.js +0 -18
- package/lib/util/builders/url/rest-api.director.js.map +0 -1
- package/lib/util/builders/url/visual-editor.director.d.ts +0 -9
- package/lib/util/builders/url/visual-editor.director.js +0 -18
- package/lib/util/builders/url/visual-editor.director.js.map +0 -1
- package/lib/util/saveArticles.d.ts +0 -8
- package/lib/util/saveArticles.js +0 -411
- package/lib/util/saveArticles.js.map +0 -1
- package/res/article_list_home.js +0 -24
- package/res/content.parsoid.css +0 -196
- package/res/inserted_style.css +0 -45
- package/res/templates/categories.html +0 -11
- package/res/templates/lead_section_wrapper.html +0 -6
- package/res/templates/pageWikimediaDesktop.html +0 -24
- package/res/templates/pageWikimediaMobile.html +0 -24
- package/res/templates/section_wrapper.html +0 -5
- package/res/templates/subcategories.html +0 -23
- package/res/templates/subpages.html +0 -17
- package/res/templates/subsection_wrapper.html +0 -5
- package/res/webpHandler.js +0 -165
- package/res/wm_mobile_override_script.js +0 -15
- package/res/wm_mobile_override_style.css +0 -20
- package/translation/br.json +0 -8
- package/translation/dag.json +0 -9
- package/translation/es-formal.json +0 -29
- package/translation/ha.json +0 -10
- package/translation/hi.json +0 -9
- package/translation/ig.json +0 -10
- package/translation/kaa.json +0 -9
- package/translation/nb.json +0 -9
- package/translation/nqo.json +0 -8
- package/translation/pt-br.json +0 -10
- package/translation/ro.json +0 -9
- package/translation/scn.json +0 -8
- package/translation/sq.json +0 -9
- package/translation/te.json +0 -9
- package/translation/tn.json +0 -9
- package/translation/tr.json +0 -10
|
@@ -7,19 +7,24 @@ import RedisStore from '../RedisStore.js';
|
|
|
7
7
|
import DOMUtils from '../DOMUtils.js';
|
|
8
8
|
import DU from '../DOMUtils.js';
|
|
9
9
|
import { config } from '../config.js';
|
|
10
|
+
import Downloader from '../Downloader.js';
|
|
10
11
|
import { rewriteUrlsOfDoc } from '../util/rewriteUrls.js';
|
|
11
12
|
import { footerTemplate } from '../Templates.js';
|
|
12
|
-
import { getFullUrl, getMediaBase, getRelativeFilePath,
|
|
13
|
+
import { getFullUrl, getMediaBase, getRelativeFilePath, encodePageTitleForZimHtmlUrl, makeZimPath } from '../util/misc.js';
|
|
13
14
|
import { processStylesheetContent } from '../util/dump.js';
|
|
14
|
-
import { isMainPage, isSubpage } from '../util/
|
|
15
|
+
import { isMainPage, isSubpage } from '../util/pages.js';
|
|
16
|
+
import { buildCategoryTypeItems } from '../util/categories.js';
|
|
15
17
|
export class Renderer {
|
|
16
|
-
staticFilesListCommon =
|
|
18
|
+
staticFilesListCommon = new Set();
|
|
17
19
|
constructor() {
|
|
18
|
-
if (this.staticFilesListCommon.
|
|
19
|
-
this.staticFilesListCommon =
|
|
20
|
+
if (this.staticFilesListCommon.size === 0) {
|
|
21
|
+
this.staticFilesListCommon = new Set([...config.output.jsResourcesCommon, ...config.output.cssResourcesCommon]);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
addDownloadErrorStaticFiles() {
|
|
25
|
+
this.staticFilesListCommon = new Set([...this.staticFilesListCommon, ...config.output.downloadErrorResources]);
|
|
26
|
+
}
|
|
27
|
+
async treatAudioVideo(dump, srcCache, pagePath, audioVideoEl) {
|
|
23
28
|
const imageDependencies = [];
|
|
24
29
|
const videoDependencies = [];
|
|
25
30
|
const subtitles = [];
|
|
@@ -37,17 +42,17 @@ export class Renderer {
|
|
|
37
42
|
}
|
|
38
43
|
const chosenAudioVideoSourceEl = isVideo ? this.chooseBestVideoSource(audioVideoEl) : this.chooseBestAudioSource(audioVideoEl);
|
|
39
44
|
if (!chosenAudioVideoSourceEl) {
|
|
40
|
-
logger.warn(`Deleting audio/video HTML node missing an appropriate source in
|
|
45
|
+
logger.warn(`Deleting audio/video HTML node missing an appropriate source in page at '${pagePath}':\n${audioVideoEl.outerHTML}`);
|
|
41
46
|
DOMUtils.deleteNode(audioVideoEl);
|
|
42
47
|
return { imageDependencies, videoDependencies, subtitles };
|
|
43
48
|
}
|
|
44
49
|
if (isVideo) {
|
|
45
|
-
this.handleVideoPoster(audioVideoEl,
|
|
50
|
+
this.handleVideoPoster(audioVideoEl, pagePath, imageDependencies, srcCache);
|
|
46
51
|
}
|
|
47
|
-
this.updateAudioVideoSrc(chosenAudioVideoSourceEl,
|
|
52
|
+
this.updateAudioVideoSrc(chosenAudioVideoSourceEl, pagePath, srcCache, videoDependencies);
|
|
48
53
|
const trackElements = Array.from(audioVideoEl.querySelectorAll('track'));
|
|
49
54
|
for (const track of trackElements) {
|
|
50
|
-
subtitles.push(await this.treatSubtitle(track,
|
|
55
|
+
subtitles.push(await this.treatSubtitle(track, pagePath));
|
|
51
56
|
}
|
|
52
57
|
return { imageDependencies, videoDependencies, subtitles };
|
|
53
58
|
}
|
|
@@ -65,7 +70,7 @@ export class Renderer {
|
|
|
65
70
|
const videoSourceEls = Array.from(videoEl.children).filter((child) => child.tagName === 'SOURCE');
|
|
66
71
|
const originalSrc = videoEl.getAttribute('src');
|
|
67
72
|
// Take into account the rare edge case where <video> has no <source> child
|
|
68
|
-
if (videoSourceEls.length
|
|
73
|
+
if (videoSourceEls.length === 0) {
|
|
69
74
|
if (originalSrc && originalSrc.endsWith('.webm')) {
|
|
70
75
|
// If video has a webm `src` attribute, this is an acceptable src
|
|
71
76
|
return videoEl;
|
|
@@ -142,7 +147,7 @@ export class Renderer {
|
|
|
142
147
|
audioEl.removeAttribute('src');
|
|
143
148
|
audioEl.removeAttribute('resource');
|
|
144
149
|
// Take into account the standard case where <audio> has no <source> child
|
|
145
|
-
if (audioSourceEls.length
|
|
150
|
+
if (audioSourceEls.length === 0) {
|
|
146
151
|
if (originalSrc) {
|
|
147
152
|
// If audio has a single `src` or `resource` attribute, this is an acceptable src
|
|
148
153
|
// Set this source back in `src`, no matter where it originally was
|
|
@@ -175,11 +180,11 @@ export class Renderer {
|
|
|
175
180
|
});
|
|
176
181
|
return chosenAudioSourceEl;
|
|
177
182
|
}
|
|
178
|
-
handleVideoPoster(videoEl,
|
|
183
|
+
handleVideoPoster(videoEl, pagePath, imageDependencies, srcCache) {
|
|
179
184
|
const posterUrl = videoEl.getAttribute('poster');
|
|
180
185
|
if (posterUrl) {
|
|
181
186
|
const videoPosterUrl = getFullUrl(posterUrl, MediaWiki.baseUrl);
|
|
182
|
-
const newVideoPosterUrl = getRelativeFilePath(
|
|
187
|
+
const newVideoPosterUrl = getRelativeFilePath(pagePath, getMediaBase(videoPosterUrl, true));
|
|
183
188
|
if (posterUrl) {
|
|
184
189
|
videoEl.setAttribute('poster', newVideoPosterUrl);
|
|
185
190
|
}
|
|
@@ -191,7 +196,7 @@ export class Renderer {
|
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
|
-
updateAudioVideoSrc(chosenVideoSourceEl,
|
|
199
|
+
updateAudioVideoSrc(chosenVideoSourceEl, pagePath, srcCache, videoDependencies) {
|
|
195
200
|
/* Download content, but avoid duplicate calls */
|
|
196
201
|
const sourceUrl = getFullUrl(chosenVideoSourceEl.getAttribute('src'), MediaWiki.baseUrl);
|
|
197
202
|
// eslint-disable-next-line no-prototype-builtins
|
|
@@ -201,20 +206,20 @@ export class Renderer {
|
|
|
201
206
|
}
|
|
202
207
|
/* Set new URL for the video element */
|
|
203
208
|
const fileBase = getMediaBase(sourceUrl, true);
|
|
204
|
-
chosenVideoSourceEl.setAttribute('src', getRelativeFilePath(
|
|
209
|
+
chosenVideoSourceEl.setAttribute('src', getRelativeFilePath(pagePath, fileBase));
|
|
205
210
|
}
|
|
206
|
-
async treatSubtitle(trackEle,
|
|
211
|
+
async treatSubtitle(trackEle, pagePath) {
|
|
207
212
|
const subtitleSourceUrl = getFullUrl(trackEle.getAttribute('src'), MediaWiki.baseUrl);
|
|
208
213
|
const { title, lang } = QueryStringParser.parse(subtitleSourceUrl);
|
|
209
|
-
// The source URL we get from Mediawiki
|
|
214
|
+
// The source URL we get from Mediawiki page is in srt format, so we replace it to vtt which is standard subtitle trackformat for <track> src attribute.
|
|
210
215
|
const vttFormatUrl = new URL(subtitleSourceUrl);
|
|
211
216
|
vttFormatUrl.searchParams.set('trackformat', 'vtt');
|
|
212
|
-
trackEle.setAttribute('src', `${getRelativeFilePath(
|
|
217
|
+
trackEle.setAttribute('src', `${getRelativeFilePath(pagePath, title)}-${lang}.vtt`);
|
|
213
218
|
return vttFormatUrl.href;
|
|
214
219
|
}
|
|
215
220
|
treatImageFrames(dump, parsoidDoc, imageNode) {
|
|
216
221
|
const image = imageNode.getElementsByTagName('img')[0] || imageNode.getElementsByTagName('video')[0];
|
|
217
|
-
if (!this.shouldKeepNode(
|
|
222
|
+
if (!this.shouldKeepNode(imageNode, image)) {
|
|
218
223
|
DOMUtils.deleteNode(imageNode);
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
@@ -240,19 +245,27 @@ export class Renderer {
|
|
|
240
245
|
thumbDiv.appendChild(thumbinnerDiv);
|
|
241
246
|
imageNode.parentNode.replaceChild(thumbDiv, imageNode);
|
|
242
247
|
}
|
|
243
|
-
async treatImage(dump, srcCache,
|
|
248
|
+
async treatImage(dump, srcCache, zimPath, img) {
|
|
244
249
|
const imageDependencies = [];
|
|
245
|
-
if (!this.shouldKeepImage(
|
|
250
|
+
if (!this.shouldKeepImage(img)) {
|
|
246
251
|
DOMUtils.deleteNode(img);
|
|
247
252
|
return { imageDependencies };
|
|
248
253
|
}
|
|
254
|
+
/* Preserve inline data: URL images (e.g. from mw.svg) as-is */
|
|
255
|
+
const imgSrc = img.getAttribute('src');
|
|
256
|
+
if (imgSrc.startsWith('data:')) {
|
|
257
|
+
img.setAttribute('loading', 'lazy');
|
|
258
|
+
return { imageDependencies };
|
|
259
|
+
}
|
|
260
|
+
if (dump.nopic && !this.isMathFallbackImage(img)) {
|
|
261
|
+
this.convertImageToPlaceholder(img);
|
|
262
|
+
return { imageDependencies };
|
|
263
|
+
}
|
|
249
264
|
/* Rewrite image src attribute */
|
|
250
265
|
const src = getFullUrl(img.getAttribute('src'), MediaWiki.baseUrl);
|
|
251
266
|
let newSrc;
|
|
252
267
|
try {
|
|
253
|
-
|
|
254
|
-
const upStr = slashesInUrl ? '../'.repeat(slashesInUrl) : './';
|
|
255
|
-
newSrc = upStr + getMediaBase(src, true);
|
|
268
|
+
newSrc = getRelativeFilePath(zimPath, getMediaBase(src, true));
|
|
256
269
|
/* Download image, but avoid duplicate calls */
|
|
257
270
|
// eslint-disable-next-line no-prototype-builtins
|
|
258
271
|
if (!srcCache.hasOwnProperty(src)) {
|
|
@@ -273,14 +286,98 @@ export class Renderer {
|
|
|
273
286
|
img.setAttribute('loading', 'lazy');
|
|
274
287
|
return { imageDependencies };
|
|
275
288
|
}
|
|
276
|
-
|
|
289
|
+
convertImageToPlaceholder(img) {
|
|
290
|
+
const width = this.getImageDimension(img, 'width');
|
|
291
|
+
const height = this.getImageDimension(img, 'height');
|
|
292
|
+
const meaningfulAlt = this.getMeaningfulAltText(img);
|
|
293
|
+
const svgPayload = this.getPlaceholderSvgPayload(width, height, meaningfulAlt, this.shouldRenderVisibleAlt(width, height));
|
|
294
|
+
if (!img.getAttribute('width') && width) {
|
|
295
|
+
img.setAttribute('width', `${width}`);
|
|
296
|
+
}
|
|
297
|
+
if (!img.getAttribute('height') && height) {
|
|
298
|
+
img.setAttribute('height', `${height}`);
|
|
299
|
+
}
|
|
300
|
+
img.setAttribute('src', `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgPayload)}`);
|
|
301
|
+
img.removeAttribute('resource');
|
|
302
|
+
img.removeAttribute('srcset');
|
|
303
|
+
img.setAttribute('loading', 'lazy');
|
|
304
|
+
}
|
|
305
|
+
getImageDimension(img, attrName) {
|
|
306
|
+
const attrValue = Number(img.getAttribute(attrName));
|
|
307
|
+
if (Number.isFinite(attrValue) && attrValue > 0) {
|
|
308
|
+
return attrValue;
|
|
309
|
+
}
|
|
310
|
+
const styleValue = this.getImageDimensionFromStyle(img, attrName);
|
|
311
|
+
if (styleValue) {
|
|
312
|
+
return styleValue;
|
|
313
|
+
}
|
|
314
|
+
const dataAttrName = attrName === 'width' ? 'data-file-width' : 'data-file-height';
|
|
315
|
+
const dataAttrValue = Number(img.getAttribute(dataAttrName));
|
|
316
|
+
if (Number.isFinite(dataAttrValue) && dataAttrValue > 0) {
|
|
317
|
+
return dataAttrValue;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
getImageDimensionFromStyle(img, attrName) {
|
|
322
|
+
const style = img.getAttribute('style');
|
|
323
|
+
if (!style) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const regex = new RegExp(`(?:^|;)\\s*${attrName}\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)\\s*(?:px)?\\s*(?:;|$)`, 'i');
|
|
327
|
+
const match = style.match(regex);
|
|
328
|
+
if (!match) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const value = Math.round(Number(match[1]));
|
|
332
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
333
|
+
}
|
|
334
|
+
shouldRenderVisibleAlt(width, height) {
|
|
335
|
+
return !!width && !!height && width >= 48 && height >= 24;
|
|
336
|
+
}
|
|
337
|
+
getPlaceholderSvgPayload(width, height, meaningfulAlt, shouldRenderVisibleAlt) {
|
|
338
|
+
const safeWidth = width || 1;
|
|
339
|
+
const safeHeight = height || 1;
|
|
340
|
+
const title = meaningfulAlt ? `<title>${this.escapeXml(meaningfulAlt)}</title>` : '';
|
|
341
|
+
const defs = shouldRenderVisibleAlt ? '<defs><clipPath id="alt-text-clip"><rect x="0" y="0" width="100%" height="100%" /></clipPath></defs>' : '';
|
|
342
|
+
let text = '';
|
|
343
|
+
if (meaningfulAlt && shouldRenderVisibleAlt) {
|
|
344
|
+
const fontSize = Math.max(10, Math.min(16, Math.floor(safeHeight / 3)));
|
|
345
|
+
text =
|
|
346
|
+
`<g clip-path="url(#alt-text-clip)">` +
|
|
347
|
+
`<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#54595d" font-size="${fontSize}" font-family="sans-serif">${this.escapeXml(meaningfulAlt)}</text>` +
|
|
348
|
+
'</g>';
|
|
349
|
+
}
|
|
350
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${safeWidth}" height="${safeHeight}" viewBox="0 0 ${safeWidth} ${safeHeight}" preserveAspectRatio="none">${title}${defs}<rect width="100%" height="100%" fill="transparent"/>${text}</svg>`;
|
|
351
|
+
}
|
|
352
|
+
getMeaningfulAltText(img) {
|
|
353
|
+
const alt = img.getAttribute('alt');
|
|
354
|
+
if (!alt) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const compactAlt = alt.replace(/\u00a0/g, ' ').trim();
|
|
358
|
+
return compactAlt.length > 0 ? compactAlt : null;
|
|
359
|
+
}
|
|
360
|
+
escapeXml(input) {
|
|
361
|
+
return input.replace(/[<>&"']/g, (char) => {
|
|
362
|
+
const entities = {
|
|
363
|
+
'<': '<',
|
|
364
|
+
'>': '>',
|
|
365
|
+
'&': '&',
|
|
366
|
+
'"': '"',
|
|
367
|
+
"'": ''',
|
|
368
|
+
};
|
|
369
|
+
return entities[char];
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
isMathFallbackImage(img) {
|
|
277
373
|
const imageNodeClass = img.getAttribute('class') || '';
|
|
374
|
+
return imageNodeClass.includes('mwe-math-fallback-image-inline') || img.getAttribute('typeof') === 'mw:Extension/math';
|
|
375
|
+
}
|
|
376
|
+
shouldKeepImage(img) {
|
|
278
377
|
const src = img.getAttribute('src');
|
|
279
|
-
return
|
|
280
|
-
src &&
|
|
281
|
-
!src.includes('./Special:FilePath/'));
|
|
378
|
+
return !!src && !src.includes('./Special:FilePath/');
|
|
282
379
|
}
|
|
283
|
-
async treatMedias(parsoidDoc, dump,
|
|
380
|
+
async treatMedias(parsoidDoc, dump, pagePath) {
|
|
284
381
|
let imageDependencies = [];
|
|
285
382
|
let videoDependencies = [];
|
|
286
383
|
let subtitles = [];
|
|
@@ -290,13 +387,13 @@ export class Renderer {
|
|
|
290
387
|
const srcCache = {};
|
|
291
388
|
for (const audioVideoEl of audiosAndVideos) {
|
|
292
389
|
// <video /> and <audio />
|
|
293
|
-
const ret = await this.treatAudioVideo(dump, srcCache,
|
|
390
|
+
const ret = await this.treatAudioVideo(dump, srcCache, pagePath, audioVideoEl);
|
|
294
391
|
imageDependencies = imageDependencies.concat(ret.imageDependencies);
|
|
295
392
|
videoDependencies = videoDependencies.concat(ret.videoDependencies);
|
|
296
393
|
subtitles = subtitles.concat(ret.subtitles);
|
|
297
394
|
}
|
|
298
395
|
for (const imgEl of imgs) {
|
|
299
|
-
const ret = await this.treatImage(dump, srcCache,
|
|
396
|
+
const ret = await this.treatImage(dump, srcCache, pagePath, imgEl);
|
|
300
397
|
imageDependencies = imageDependencies.concat(ret.imageDependencies);
|
|
301
398
|
}
|
|
302
399
|
/* Treat image figures + special spans */
|
|
@@ -311,8 +408,8 @@ export class Renderer {
|
|
|
311
408
|
isStillLinked(image) {
|
|
312
409
|
return image && image.parentNode && image.parentNode.tagName === 'A';
|
|
313
410
|
}
|
|
314
|
-
shouldKeepNode(
|
|
315
|
-
return
|
|
411
|
+
shouldKeepNode(imageNode, image) {
|
|
412
|
+
return !!(imageNode && image);
|
|
316
413
|
}
|
|
317
414
|
makeThumbDiv(dump, parsoidDoc, imageNode) {
|
|
318
415
|
const imageNodeClass = imageNode.getAttribute('class') || '';
|
|
@@ -336,16 +433,57 @@ export class Renderer {
|
|
|
336
433
|
}
|
|
337
434
|
return thumbDiv;
|
|
338
435
|
}
|
|
436
|
+
getPositiveIntegerAttribute(node, attribute) {
|
|
437
|
+
const val = Number.parseInt(node.getAttribute(attribute), 10);
|
|
438
|
+
return Number.isFinite(val) && val > 0 ? val : undefined;
|
|
439
|
+
}
|
|
440
|
+
getRequestedImageWidths(doc) {
|
|
441
|
+
const requestedWidths = {};
|
|
442
|
+
const setRequestedWidth = (url, width) => {
|
|
443
|
+
const prevWidth = requestedWidths[url] || 0;
|
|
444
|
+
requestedWidths[url] = Math.max(prevWidth, width);
|
|
445
|
+
};
|
|
446
|
+
for (const image of Array.from(doc.getElementsByTagName('img'))) {
|
|
447
|
+
const imageSrc = image.getAttribute('src');
|
|
448
|
+
const width = this.getPositiveIntegerAttribute(image, 'width') || this.getPositiveIntegerAttribute(image, 'data-width');
|
|
449
|
+
if (!imageSrc || !width) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
setRequestedWidth(getFullUrl(imageSrc, MediaWiki.baseUrl), width);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const video of Array.from(doc.getElementsByTagName('video'))) {
|
|
460
|
+
const posterSrc = video.getAttribute('poster');
|
|
461
|
+
const width = this.getPositiveIntegerAttribute(video, 'width');
|
|
462
|
+
if (!posterSrc || !width) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
setRequestedWidth(getFullUrl(posterSrc, MediaWiki.baseUrl), width);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return requestedWidths;
|
|
473
|
+
}
|
|
339
474
|
// TODO: The first part of this method is common for all renders
|
|
340
475
|
async processHtml(processHtmlOpts) {
|
|
341
|
-
const { html, dump,
|
|
342
|
-
let {
|
|
476
|
+
const { html, dump, pageTitle, pageDetail, displayTitle, categoryMembers, categoriesHtml, moduleDependencies, callback } = processHtmlOpts;
|
|
477
|
+
let { pageSubtitle } = processHtmlOpts;
|
|
343
478
|
let imageDependencies = [];
|
|
344
479
|
let videoDependencies = [];
|
|
345
480
|
let mediaDependencies = [];
|
|
346
481
|
let subtitles = [];
|
|
347
482
|
let doc = domino.createDocument(html);
|
|
348
|
-
|
|
483
|
+
// Simplification for now, ZIM path = pageTitle
|
|
484
|
+
const pagePath = makeZimPath(pageTitle);
|
|
485
|
+
const pageItems = [];
|
|
486
|
+
const ruRet = await rewriteUrlsOfDoc(doc, pagePath, dump);
|
|
349
487
|
doc = ruRet.doc;
|
|
350
488
|
mediaDependencies = mediaDependencies.concat(ruRet.mediaDependencies
|
|
351
489
|
.filter((a) => a)
|
|
@@ -353,27 +491,81 @@ export class Renderer {
|
|
|
353
491
|
const path = getMediaBase(url, false);
|
|
354
492
|
return { url, path };
|
|
355
493
|
}));
|
|
356
|
-
|
|
357
|
-
|
|
494
|
+
// applyOtherTreatments must run before treatMedias so that iframe
|
|
495
|
+
// placeholders (e.g. YouTube thumbnails) are already in the DOM
|
|
496
|
+
// when treatMedias processes <img> tags for download into the ZIM.
|
|
497
|
+
doc = await this.applyOtherTreatments(doc, dump, pagePath);
|
|
498
|
+
const imageRequestedWidths = this.getRequestedImageWidths(doc);
|
|
499
|
+
/* The content of a category page, listing all its members */
|
|
500
|
+
if (pageDetail.categoryinfo) {
|
|
501
|
+
const categoryinfo = categoryMembers.categoryinfo;
|
|
502
|
+
const categoryContent = doc.createElement('div');
|
|
503
|
+
categoryContent.lang = pageDetail.pagelang;
|
|
504
|
+
categoryContent.dir = pageDetail.pagedir;
|
|
505
|
+
categoryContent.classList.add('mw-category-generated');
|
|
506
|
+
if (categoryinfo.size) {
|
|
507
|
+
const categoryCollation = MediaWiki.metaData.categoryCollation;
|
|
508
|
+
const numericSorting = categoryCollation === 'numeric' || (categoryCollation.startsWith('uca-') && categoryCollation.endsWith('-u-kn'));
|
|
509
|
+
moduleDependencies.styleDependenciesList.push('mediawiki.action.styles');
|
|
510
|
+
await buildCategoryTypeItems('subcats', categoryinfo, categoryMembers, pagePath, pageDetail, doc, dump, numericSorting, moduleDependencies, pageTitle, categoryContent, pageItems);
|
|
511
|
+
await buildCategoryTypeItems('pages', categoryinfo, categoryMembers, pagePath, pageDetail, doc, dump, numericSorting, moduleDependencies, pageTitle, categoryContent, pageItems);
|
|
512
|
+
await buildCategoryTypeItems('files', categoryinfo, categoryMembers, pagePath, pageDetail, doc, dump, numericSorting, moduleDependencies, pageTitle, categoryContent, pageItems);
|
|
513
|
+
if (MediaWiki.getCategories) {
|
|
514
|
+
const p = doc.createElement('p');
|
|
515
|
+
p.innerHTML = dump.t('categoryLimited');
|
|
516
|
+
categoryContent.appendChild(p);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
const p = doc.createElement('p');
|
|
521
|
+
p.innerHTML = dump.t('categoryEmpty');
|
|
522
|
+
categoryContent.appendChild(p);
|
|
523
|
+
}
|
|
524
|
+
const ruRetCategoryContent = await rewriteUrlsOfDoc(categoryContent, pagePath, dump);
|
|
525
|
+
doc.getElementsByTagName('body')[0].appendChild(ruRetCategoryContent.doc);
|
|
526
|
+
}
|
|
527
|
+
const tmRet = await this.treatMedias(doc, dump, pagePath);
|
|
358
528
|
doc = tmRet.doc;
|
|
359
529
|
// Subtitle
|
|
360
|
-
if (
|
|
361
|
-
let
|
|
362
|
-
const ruRetSubtitle = await rewriteUrlsOfDoc(
|
|
363
|
-
|
|
364
|
-
|
|
530
|
+
if (pageSubtitle) {
|
|
531
|
+
let pageSubtitleDoc = domino.createDocument(pageSubtitle);
|
|
532
|
+
const ruRetSubtitle = await rewriteUrlsOfDoc(pageSubtitleDoc, pagePath, dump);
|
|
533
|
+
pageSubtitleDoc = ruRetSubtitle.doc;
|
|
534
|
+
pageSubtitle = pageSubtitleDoc.getElementsByTagName('body')[0].innerHTML;
|
|
535
|
+
}
|
|
536
|
+
// List of categories the page is a member of
|
|
537
|
+
if (categoriesHtml) {
|
|
538
|
+
let categoriesDoc = domino.createDocument(categoriesHtml);
|
|
539
|
+
// Remove categories which do not have an associated page
|
|
540
|
+
const catLinksDiv = categoriesDoc.getElementById('catlinks');
|
|
541
|
+
const listItems = catLinksDiv.getElementsByTagName('li');
|
|
542
|
+
for (const listItem of Array.from(listItems)) {
|
|
543
|
+
const link = listItem.getElementsByTagName('a')[0];
|
|
544
|
+
if (Array.from(link.classList).includes('new')) {
|
|
545
|
+
listItem.parentNode.removeChild(listItem);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const nbRemainingLinks = catLinksDiv.getElementsByTagName('li').length;
|
|
549
|
+
// Rewrite and add categories HTML only if there is at least one category remaining in the list
|
|
550
|
+
if (nbRemainingLinks > 0) {
|
|
551
|
+
const ruRetCategories = await rewriteUrlsOfDoc(categoriesDoc, pagePath, dump);
|
|
552
|
+
categoriesDoc = ruRetCategories.doc;
|
|
553
|
+
doc.getElementsByTagName('body')[0].appendChild(categoriesDoc.getElementById('catlinks'));
|
|
554
|
+
}
|
|
365
555
|
}
|
|
366
556
|
videoDependencies = videoDependencies.concat(tmRet.videoDependencies
|
|
367
557
|
.filter((a) => a)
|
|
368
558
|
.map((url) => {
|
|
369
559
|
const path = getMediaBase(url, false);
|
|
370
|
-
|
|
560
|
+
const width = imageRequestedWidths[url];
|
|
561
|
+
return width ? { url, path, width } : { url, path };
|
|
371
562
|
}));
|
|
372
563
|
imageDependencies = imageDependencies.concat(tmRet.imageDependencies
|
|
373
564
|
.filter((a) => a)
|
|
374
565
|
.map((url) => {
|
|
375
566
|
const path = getMediaBase(url, false);
|
|
376
|
-
|
|
567
|
+
const width = imageRequestedWidths[url];
|
|
568
|
+
return width ? { url, path, width } : { url, path };
|
|
377
569
|
}));
|
|
378
570
|
subtitles = subtitles.concat(tmRet.subtitles
|
|
379
571
|
.filter((a) => a)
|
|
@@ -382,15 +574,16 @@ export class Renderer {
|
|
|
382
574
|
const path = `${title}-${lang}.vtt`;
|
|
383
575
|
return { url, path };
|
|
384
576
|
}));
|
|
385
|
-
if (!isMainPage(
|
|
386
|
-
doc = await dump.customProcessor.
|
|
577
|
+
if (!isMainPage(pageTitle) && dump.customProcessor?.preProcessPage) {
|
|
578
|
+
doc = await dump.customProcessor.preProcessPage(pageTitle, doc);
|
|
387
579
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
580
|
+
const templateDoc = callback(moduleDependencies, pagePath);
|
|
581
|
+
const pageFinalDoc = domino.createDocument(templateDoc.documentElement.outerHTML);
|
|
582
|
+
await this.mergeTemplateDoc(pageFinalDoc, doc, dump, pageDetail, pageTitle, displayTitle, pageSubtitle);
|
|
583
|
+
if (dump.customProcessor && dump.customProcessor.postProcessPage) {
|
|
584
|
+
await dump.customProcessor.postProcessPage(pageTitle, pageFinalDoc);
|
|
392
585
|
}
|
|
393
|
-
let outHtml =
|
|
586
|
+
let outHtml = pageFinalDoc.documentElement.outerHTML;
|
|
394
587
|
if (dump.opts.minifyHtml) {
|
|
395
588
|
outHtml = await htmlMinifier.minify(outHtml, {
|
|
396
589
|
removeComments: true,
|
|
@@ -401,46 +594,70 @@ export class Renderer {
|
|
|
401
594
|
minifyCSS: true,
|
|
402
595
|
});
|
|
403
596
|
}
|
|
404
|
-
const
|
|
597
|
+
const div = doc.createElement('div');
|
|
598
|
+
div.innerHTML = displayTitle;
|
|
599
|
+
const displayTitleText = div.innerText;
|
|
600
|
+
pageItems.push({
|
|
601
|
+
pageTitle,
|
|
602
|
+
zimPath: pagePath,
|
|
603
|
+
zimTitle: displayTitleText ? displayTitleText : pageTitle,
|
|
604
|
+
htmlContent: '<!DOCTYPE html>\n' + outHtml,
|
|
605
|
+
});
|
|
405
606
|
return {
|
|
406
|
-
|
|
607
|
+
items: pageItems,
|
|
407
608
|
mediaDependencies,
|
|
408
609
|
imageDependencies,
|
|
409
610
|
videoDependencies,
|
|
611
|
+
moduleDependencies,
|
|
410
612
|
subtitles,
|
|
613
|
+
needsDownloadErrorStaticFiles: false,
|
|
411
614
|
};
|
|
412
615
|
}
|
|
413
|
-
async mergeTemplateDoc(htmlTemplateDoc, parsoidDoc, dump,
|
|
616
|
+
async mergeTemplateDoc(htmlTemplateDoc, parsoidDoc, dump, pageDetail, pageTitle, displayTitle, pageSubtitle) {
|
|
414
617
|
/* Create final document by merging template and parsoid documents */
|
|
415
618
|
const mwContentText = htmlTemplateDoc.getElementById('mw-content-text');
|
|
416
|
-
mwContentText.lang =
|
|
417
|
-
mwContentText.dir =
|
|
418
|
-
mwContentText.classList.add('mw-content-' +
|
|
619
|
+
mwContentText.lang = pageDetail.pagelang;
|
|
620
|
+
mwContentText.dir = pageDetail.pagedir;
|
|
621
|
+
mwContentText.classList.add('mw-content-' + pageDetail.pagedir);
|
|
419
622
|
mwContentText.innerHTML = parsoidDoc.getElementsByTagName('body')[0].innerHTML;
|
|
420
623
|
/* Title */
|
|
421
|
-
|
|
422
|
-
htmlTemplateDoc.
|
|
624
|
+
let htmlTitle;
|
|
625
|
+
if (htmlTemplateDoc.getElementById('title_0')) {
|
|
626
|
+
htmlTitle = htmlTemplateDoc.getElementById('title_0').innerText;
|
|
627
|
+
}
|
|
628
|
+
else if (displayTitle) {
|
|
629
|
+
// displayTitle may contain HTML markup (e.g. italics), which has no place in the <title> tag
|
|
630
|
+
const titleDiv = htmlTemplateDoc.createElement('div');
|
|
631
|
+
titleDiv.innerHTML = displayTitle;
|
|
632
|
+
htmlTitle = titleDiv.textContent;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
htmlTitle = pageTitle;
|
|
636
|
+
}
|
|
637
|
+
htmlTemplateDoc.getElementsByTagName('title')[0].textContent = htmlTitle;
|
|
423
638
|
// Set inline page title when missing
|
|
424
639
|
const inlineTitle = htmlTemplateDoc.getElementById('firstHeading');
|
|
425
640
|
if (inlineTitle && !inlineTitle.innerHTML) {
|
|
426
|
-
inlineTitle.innerHTML = displayTitle ||
|
|
641
|
+
inlineTitle.innerHTML = displayTitle || htmlTitle;
|
|
427
642
|
}
|
|
428
643
|
DOMUtils.deleteNode(htmlTemplateDoc.getElementById('titleHeading'));
|
|
429
644
|
/* Subpage */
|
|
430
645
|
if (this.constructor.name === 'ActionParseRenderer') {
|
|
431
646
|
const mwContentSubtitle = htmlTemplateDoc.getElementById('mw-content-subtitle');
|
|
432
|
-
mwContentSubtitle.innerHTML =
|
|
647
|
+
mwContentSubtitle.innerHTML = pageSubtitle || '';
|
|
433
648
|
}
|
|
434
|
-
else if (isSubpage(
|
|
649
|
+
else if (isSubpage(pageTitle) && !isMainPage(pageTitle)) {
|
|
435
650
|
const headingNode = htmlTemplateDoc.getElementById('mw-content-text');
|
|
436
651
|
const subpagesNode = htmlTemplateDoc.createElement('span');
|
|
437
|
-
const parents =
|
|
652
|
+
const parents = pageTitle.split('/');
|
|
438
653
|
parents.pop();
|
|
439
654
|
let subpages = '';
|
|
440
655
|
await Promise.all(parents.map(async (parent) => {
|
|
441
656
|
const label = parent.replace(/_/g, ' ');
|
|
442
|
-
const isParentMirrored = await
|
|
443
|
-
subpages += `< ${isParentMirrored
|
|
657
|
+
const isParentMirrored = await RedisStore.pagesStore.exists(`${pageTitle.split(parent)[0]}${parent}`);
|
|
658
|
+
subpages += `< ${isParentMirrored
|
|
659
|
+
? `<a href="${getRelativeFilePath(makeZimPath(pageTitle), encodePageTitleForZimHtmlUrl(`${pageTitle.split(parent)[0]}${parent}`))}" title="${label}">`
|
|
660
|
+
: ''}${label}${isParentMirrored ? '</a> ' : ' '}`;
|
|
444
661
|
}));
|
|
445
662
|
subpagesNode.innerHTML = subpages;
|
|
446
663
|
subpagesNode.setAttribute('class', 'subpages');
|
|
@@ -449,31 +666,23 @@ export class Renderer {
|
|
|
449
666
|
/* Set footer */
|
|
450
667
|
const div = htmlTemplateDoc.createElement('div');
|
|
451
668
|
/* Revision date */
|
|
452
|
-
const date = new Date(
|
|
453
|
-
const lastEditedOnString = date
|
|
454
|
-
? interpolateTranslationString(dump.strings.LAST_EDITED_ON, {
|
|
455
|
-
date: date.toISOString().substring(0, 10),
|
|
456
|
-
})
|
|
457
|
-
: null;
|
|
669
|
+
const date = new Date(pageDetail.timestamp);
|
|
670
|
+
const lastEditedOnString = date ? dump.t('LAST_EDITED_ON', { date: date.toISOString().substring(0, 10) }) : null;
|
|
458
671
|
const creatorLink = '<a class="external text" ' +
|
|
459
672
|
`${lastEditedOnString ? `title="${lastEditedOnString}"` : ''} ` +
|
|
460
|
-
`href="${MediaWiki.webUrl.href}?title=${encodeURIComponent(
|
|
673
|
+
`href="${MediaWiki.webUrl.href}?title=${encodeURIComponent(pageTitle.replace(/ /g, '_'))}&oldid=${pageDetail.revisionId}">` +
|
|
461
674
|
`${dump.mwMetaData.title || dump.mwMetaData.creator}</a>`;
|
|
462
675
|
const licenseLink = (dump.mwMetaData.licenseUrl ? `<a class="external text" href="${dump.mwMetaData.licenseUrl}">` : '') +
|
|
463
|
-
(dump.mwMetaData.licenseName || dump.mwMetaData.licenseUrl || dump.
|
|
676
|
+
(dump.mwMetaData.licenseName || dump.mwMetaData.licenseUrl || dump.t('LICENSE_UNKNOWN')).replace(/</g, '<') +
|
|
464
677
|
(dump.mwMetaData.licenseUrl ? `</a>` : '');
|
|
465
678
|
div.innerHTML = footerTemplate({
|
|
466
|
-
disclaimer:
|
|
467
|
-
creator: creatorLink,
|
|
468
|
-
license: licenseLink,
|
|
469
|
-
}),
|
|
470
|
-
strings: dump.strings,
|
|
679
|
+
disclaimer: dump.t('DISCLAIMER', { creator: creatorLink, license: licenseLink }),
|
|
471
680
|
});
|
|
472
|
-
|
|
681
|
+
mwContentText.appendChild(div);
|
|
473
682
|
this.addNoIndexCommentToElement(div);
|
|
474
683
|
/* Geo-coordinates */
|
|
475
|
-
if (
|
|
476
|
-
const geoCoordinates =
|
|
684
|
+
if (pageDetail.coordinates) {
|
|
685
|
+
const geoCoordinates = pageDetail.coordinates;
|
|
477
686
|
const metaNode = htmlTemplateDoc.createElement('meta');
|
|
478
687
|
metaNode.name = 'geo.position';
|
|
479
688
|
metaNode.content = geoCoordinates;
|
|
@@ -485,13 +694,81 @@ export class Renderer {
|
|
|
485
694
|
const slices = element.parentElement.innerHTML.split(element.outerHTML);
|
|
486
695
|
element.parentElement.innerHTML = `${slices[0]}<!--htdig_noindex-->${element.outerHTML}<!--/htdig_noindex-->${slices[1]}`;
|
|
487
696
|
}
|
|
488
|
-
|
|
489
|
-
|
|
697
|
+
// Rewrite iframe tags into external placeholders
|
|
698
|
+
processIframeTags(parsoidDoc) {
|
|
490
699
|
const iframes = Array.from(parsoidDoc.getElementsByTagName('iframe'));
|
|
491
700
|
for (const iframe of iframes) {
|
|
492
|
-
|
|
701
|
+
const src = iframe.getAttribute('src');
|
|
702
|
+
if (!src) {
|
|
703
|
+
DU.deleteNode(iframe);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
let fullSrc = src;
|
|
707
|
+
try {
|
|
708
|
+
fullSrc = getFullUrl(src, MediaWiki.baseUrl);
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// Keep original src when URL parsing fails
|
|
712
|
+
}
|
|
713
|
+
const placeholder = this.isYouTubeIframeUrl(fullSrc)
|
|
714
|
+
? this.createYouTubePlaceholder(parsoidDoc, iframe, fullSrc, this.extractYouTubeVideoId(fullSrc))
|
|
715
|
+
: this.createExternalIframePlaceholder(parsoidDoc, iframe, fullSrc);
|
|
716
|
+
if (iframe.parentNode) {
|
|
717
|
+
iframe.parentNode.replaceChild(placeholder, iframe);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
DU.deleteNode(iframe);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
isYouTubeIframeUrl(url) {
|
|
725
|
+
const lowerUrl = url.toLowerCase();
|
|
726
|
+
return lowerUrl.includes('youtube.com/embed/') || lowerUrl.includes('youtube-nocookie.com/embed/') || lowerUrl.includes('youtu.be/') || lowerUrl.includes('youtube.com/watch');
|
|
727
|
+
}
|
|
728
|
+
extractYouTubeVideoId(url) {
|
|
729
|
+
const match = url.match(/(?:embed\/|v=|\.be\/)([a-zA-Z0-9_-]+)/);
|
|
730
|
+
return match ? match[1] : null;
|
|
731
|
+
}
|
|
732
|
+
createExternalIframePlaceholder(parsoidDoc, iframe, originalUrl) {
|
|
733
|
+
const card = parsoidDoc.createElement('div');
|
|
734
|
+
this.copyNodeAttributes(iframe, card);
|
|
735
|
+
const iframeClass = card.getAttribute('class');
|
|
736
|
+
card.setAttribute('class', iframeClass ? `${iframeClass} mwo-external-video-card` : 'mwo-external-video-card');
|
|
737
|
+
const link = parsoidDoc.createElement('a');
|
|
738
|
+
link.setAttribute('class', 'external');
|
|
739
|
+
link.setAttribute('href', originalUrl);
|
|
740
|
+
link.textContent = 'Open embedded content (external)';
|
|
741
|
+
card.appendChild(link);
|
|
742
|
+
return card;
|
|
743
|
+
}
|
|
744
|
+
copyNodeAttributes(sourceNode, targetNode) {
|
|
745
|
+
for (let attributeIndex = 0; attributeIndex < sourceNode.attributes.length; attributeIndex++) {
|
|
746
|
+
const attribute = sourceNode.attributes.item(attributeIndex);
|
|
747
|
+
if (!attribute) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
targetNode.setAttribute(attribute.name, attribute.value);
|
|
493
751
|
}
|
|
494
752
|
}
|
|
753
|
+
createYouTubePlaceholder(parsoidDoc, iframe, originalUrl, videoId) {
|
|
754
|
+
const link = parsoidDoc.createElement('a');
|
|
755
|
+
link.setAttribute('class', 'external');
|
|
756
|
+
link.setAttribute('href', videoId ? `https://www.youtube.com/watch?v=${videoId}` : originalUrl);
|
|
757
|
+
if (!videoId) {
|
|
758
|
+
link.textContent = 'View this content externally';
|
|
759
|
+
link.setAttribute('aria-label', 'View this content externally');
|
|
760
|
+
return link;
|
|
761
|
+
}
|
|
762
|
+
const thumbnail = parsoidDoc.createElement('img');
|
|
763
|
+
this.copyNodeAttributes(iframe, thumbnail);
|
|
764
|
+
// Set the online thumbnail URL here; treatMedias will process this
|
|
765
|
+
// <img> tag later to download the image and rewrite src to a local path in the ZIM.
|
|
766
|
+
thumbnail.setAttribute('src', `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`);
|
|
767
|
+
thumbnail.setAttribute('alt', 'Video thumbnail');
|
|
768
|
+
thumbnail.setAttribute('loading', 'lazy');
|
|
769
|
+
link.appendChild(thumbnail);
|
|
770
|
+
return link;
|
|
771
|
+
}
|
|
495
772
|
removeCitations(parsoidDoc) {
|
|
496
773
|
// Remove all citation-related material
|
|
497
774
|
const sups = Array.from(parsoidDoc.getElementsByTagName('sup'));
|
|
@@ -600,8 +877,8 @@ export class Renderer {
|
|
|
600
877
|
node.textContent = '🌍';
|
|
601
878
|
}
|
|
602
879
|
}
|
|
603
|
-
applyOtherTreatments(parsoidDoc, dump,
|
|
604
|
-
this.
|
|
880
|
+
async applyOtherTreatments(parsoidDoc, dump, pagePath) {
|
|
881
|
+
this.processIframeTags(parsoidDoc);
|
|
605
882
|
if (dump.nodet) {
|
|
606
883
|
this.removeCitations(parsoidDoc);
|
|
607
884
|
}
|
|
@@ -632,7 +909,7 @@ export class Renderer {
|
|
|
632
909
|
// We use MediaWiki.baseUrl which is an approximation but it is deemed sufficient because
|
|
633
910
|
// all non-absolute URL found in inline CSS are expected to be relative to the root, not to
|
|
634
911
|
// current web URL which is "moving" (could use something like /w/index.php?title=... or /wiki/...)
|
|
635
|
-
style.textContent = processStylesheetContent(MediaWiki.baseUrl.toString(), '', style.textContent,
|
|
912
|
+
style.textContent = await processStylesheetContent(MediaWiki.baseUrl.toString(), '', style.textContent, pagePath);
|
|
636
913
|
}
|
|
637
914
|
/* Remove element with id in the blacklist */
|
|
638
915
|
filtersConfig.idBlackList.forEach((id) => {
|
|
@@ -642,11 +919,13 @@ export class Renderer {
|
|
|
642
919
|
}
|
|
643
920
|
});
|
|
644
921
|
/*
|
|
645
|
-
*
|
|
922
|
+
* We can't trust the content of inline scripts, only keep when all JavaScript is allowed
|
|
646
923
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
924
|
+
if (!Downloader.trustedJs || Downloader.trustedJs.length) {
|
|
925
|
+
const scripts = Array.from(parsoidDoc.getElementsByTagName('script'));
|
|
926
|
+
for (const script of scripts) {
|
|
927
|
+
script.parentNode.removeChild(script);
|
|
928
|
+
}
|
|
650
929
|
}
|
|
651
930
|
/* Force display of element with that CSS class */
|
|
652
931
|
filtersConfig.cssClassDisplayList.map((classname) => {
|
|
@@ -674,14 +953,14 @@ export class Renderer {
|
|
|
674
953
|
return parsoidDoc;
|
|
675
954
|
}
|
|
676
955
|
/**
|
|
677
|
-
* Add an H1 tag with page title on top of
|
|
956
|
+
* Add an H1 tag with page title on top of page except main page
|
|
678
957
|
*/
|
|
679
|
-
injectH1TitleToHtml(content,
|
|
958
|
+
injectH1TitleToHtml(content, pageDetail) {
|
|
680
959
|
const doc = domino.createDocument(content);
|
|
681
960
|
const header = doc.createElement('h1');
|
|
682
|
-
if (
|
|
683
|
-
header.appendChild(doc.createTextNode(
|
|
684
|
-
header.classList.add('
|
|
961
|
+
if (pageDetail?.title) {
|
|
962
|
+
header.appendChild(doc.createTextNode(pageDetail.title));
|
|
963
|
+
header.classList.add('page-header');
|
|
685
964
|
const target = doc.querySelector('body.mw-body-content');
|
|
686
965
|
if (target) {
|
|
687
966
|
target.insertAdjacentElement('afterbegin', header);
|