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.
Files changed (227) hide show
  1. package/README.md +355 -62
  2. package/docs/functional_architecture.md +34 -38
  3. package/extensions/wiktionary_fr.js +18 -17
  4. package/jest.config.cjs +2 -2
  5. package/lib/DOMUtils.js +0 -1
  6. package/lib/DOMUtils.js.map +1 -1
  7. package/lib/Downloader.d.ts +39 -31
  8. package/lib/Downloader.js +418 -349
  9. package/lib/Downloader.js.map +1 -1
  10. package/lib/Dump.d.ts +24 -10
  11. package/lib/Dump.js +95 -50
  12. package/lib/Dump.js.map +1 -1
  13. package/lib/Gadgets.d.ts +2 -1
  14. package/lib/Gadgets.js +7 -5
  15. package/lib/Gadgets.js.map +1 -1
  16. package/lib/Logger.d.ts +6 -6
  17. package/lib/Logger.js +33 -33
  18. package/lib/Logger.js.map +1 -1
  19. package/lib/MediaWiki.d.ts +14 -31
  20. package/lib/MediaWiki.js +119 -172
  21. package/lib/MediaWiki.js.map +1 -1
  22. package/lib/RedisStore.d.ts +3 -3
  23. package/lib/RedisStore.js +21 -23
  24. package/lib/RedisStore.js.map +1 -1
  25. package/lib/S3.js +1 -1
  26. package/lib/S3.js.map +1 -1
  27. package/lib/Templates.d.ts +3 -10
  28. package/lib/Templates.js +5 -14
  29. package/lib/Templates.js.map +1 -1
  30. package/lib/cli.js +7 -6
  31. package/lib/cli.js.map +1 -1
  32. package/lib/config.d.ts +10 -16
  33. package/lib/config.js +33 -55
  34. package/lib/config.js.map +1 -1
  35. package/lib/error.manager.d.ts +2 -2
  36. package/lib/error.manager.js +42 -49
  37. package/lib/error.manager.js.map +1 -1
  38. package/lib/i18n.d.ts +2 -0
  39. package/lib/i18n.js +53 -0
  40. package/lib/i18n.js.map +1 -0
  41. package/lib/mutex.d.ts +2 -1
  42. package/lib/mutex.js +2 -1
  43. package/lib/mutex.js.map +1 -1
  44. package/lib/mwoffliner.lib.js +370 -222
  45. package/lib/mwoffliner.lib.js.map +1 -1
  46. package/lib/parameterList.d.ts +20 -8
  47. package/lib/parameterList.js +30 -18
  48. package/lib/parameterList.js.map +1 -1
  49. package/lib/renderers/abstract.renderer.d.ts +48 -55
  50. package/lib/renderers/abstract.renderer.js +377 -98
  51. package/lib/renderers/abstract.renderer.js.map +1 -1
  52. package/lib/renderers/action-parse.renderer.d.ts +22 -3
  53. package/lib/renderers/action-parse.renderer.js +123 -81
  54. package/lib/renderers/action-parse.renderer.js.map +1 -1
  55. package/lib/renderers/renderer.builder.js +6 -69
  56. package/lib/renderers/renderer.builder.js.map +1 -1
  57. package/lib/renderers/rendering.context.d.ts +2 -3
  58. package/lib/renderers/rendering.context.js +6 -14
  59. package/lib/renderers/rendering.context.js.map +1 -1
  60. package/lib/sanitize-argument.d.ts +7 -4
  61. package/lib/sanitize-argument.js +99 -41
  62. package/lib/sanitize-argument.js.map +1 -1
  63. package/lib/util/FileManager.d.ts +36 -0
  64. package/lib/util/FileManager.js +246 -0
  65. package/lib/util/FileManager.js.map +1 -0
  66. package/lib/util/RedisKvs.js +4 -4
  67. package/lib/util/RedisKvs.js.map +1 -1
  68. package/lib/util/RedisQueue.js +1 -1
  69. package/lib/util/RedisQueue.js.map +1 -1
  70. package/lib/util/builders/url/action-parse.director.d.ts +2 -2
  71. package/lib/util/builders/url/action-parse.director.js +7 -7
  72. package/lib/util/builders/url/action-parse.director.js.map +1 -1
  73. package/lib/util/builders/url/api.director.d.ts +2 -4
  74. package/lib/util/builders/url/api.director.js +10 -17
  75. package/lib/util/builders/url/api.director.js.map +1 -1
  76. package/lib/util/builders/url/base.director.d.ts +0 -4
  77. package/lib/util/builders/url/base.director.js +0 -25
  78. package/lib/util/builders/url/base.director.js.map +1 -1
  79. package/lib/util/builders/url/basic.director.d.ts +1 -1
  80. package/lib/util/builders/url/basic.director.js +1 -1
  81. package/lib/util/builders/url/web.director.d.ts +1 -1
  82. package/lib/util/builders/url/web.director.js +2 -2
  83. package/lib/util/builders/url/web.director.js.map +1 -1
  84. package/lib/util/categories.d.ts +5 -5
  85. package/lib/util/categories.js +175 -174
  86. package/lib/util/categories.js.map +1 -1
  87. package/lib/util/const.d.ts +4 -8
  88. package/lib/util/const.js +9 -17
  89. package/lib/util/const.js.map +1 -1
  90. package/lib/util/customCssJs.d.ts +6 -0
  91. package/lib/util/customCssJs.js +70 -0
  92. package/lib/util/customCssJs.js.map +1 -0
  93. package/lib/util/dump.d.ts +15 -4
  94. package/lib/util/dump.js +282 -86
  95. package/lib/util/dump.js.map +1 -1
  96. package/lib/util/index.d.ts +1 -1
  97. package/lib/util/index.js +1 -1
  98. package/lib/util/index.js.map +1 -1
  99. package/lib/util/metaData.js.map +1 -1
  100. package/lib/util/misc.d.ts +23 -13
  101. package/lib/util/misc.js +90 -59
  102. package/lib/util/misc.js.map +1 -1
  103. package/lib/util/mw-api.d.ts +7 -5
  104. package/lib/util/mw-api.js +167 -221
  105. package/lib/util/mw-api.js.map +1 -1
  106. package/lib/util/pageListMainPage.d.ts +3 -0
  107. package/lib/util/pageListMainPage.js +10 -0
  108. package/lib/util/pageListMainPage.js.map +1 -0
  109. package/lib/util/pages.d.ts +30 -0
  110. package/lib/util/pages.js +146 -0
  111. package/lib/util/pages.js.map +1 -0
  112. package/lib/util/rewriteUrls.d.ts +2 -2
  113. package/lib/util/rewriteUrls.js +33 -31
  114. package/lib/util/rewriteUrls.js.map +1 -1
  115. package/lib/util/savePages.d.ts +8 -0
  116. package/lib/util/savePages.js +182 -0
  117. package/lib/util/savePages.js.map +1 -0
  118. package/lib/version.d.ts +1 -1
  119. package/lib/version.js +1 -1
  120. package/lib/version.js.map +1 -1
  121. package/offliner-definition.json +178 -90
  122. package/package.json +50 -49
  123. package/res/page_list_home.js +20 -0
  124. package/res/{article_not_found.svg → page_not_found.svg} +1 -1
  125. package/res/script.js +30 -45
  126. package/res/style.css +52 -68
  127. package/res/templates/download_error_placeholder.html +7 -8
  128. package/res/templates/javaScript.html +19 -0
  129. package/res/templates/pageFallback.html +9 -7
  130. package/res/templates/pageVector2022.html +9 -7
  131. package/res/templates/pageVectorLegacy.html +9 -7
  132. package/res/templates/{article_list_home.html → page_list_home.html} +1 -2
  133. package/translation/ar.json +21 -21
  134. package/translation/bn.json +1 -1
  135. package/translation/de.json +46 -19
  136. package/translation/en.json +54 -21
  137. package/translation/es.json +17 -19
  138. package/translation/fi.json +23 -3
  139. package/translation/fr.json +57 -21
  140. package/translation/he.json +8 -6
  141. package/translation/ia.json +19 -19
  142. package/translation/id.json +13 -13
  143. package/translation/it.json +13 -8
  144. package/translation/ko.json +55 -3
  145. package/translation/lb.json +5 -2
  146. package/translation/mk.json +5 -5
  147. package/translation/nl.json +57 -22
  148. package/translation/or.json +2 -2
  149. package/translation/pt.json +2 -2
  150. package/translation/qqq.json +55 -22
  151. package/translation/ru.json +2 -2
  152. package/translation/sc.json +2 -2
  153. package/translation/sk.json +62 -0
  154. package/translation/sl.json +54 -21
  155. package/translation/sv.json +20 -19
  156. package/translation/sw.json +45 -2
  157. package/translation/zh-hans.json +56 -22
  158. package/translation/zh-hant.json +26 -19
  159. package/lib/renderers/abstractDesktop.render.d.ts +0 -12
  160. package/lib/renderers/abstractDesktop.render.js +0 -67
  161. package/lib/renderers/abstractDesktop.render.js.map +0 -1
  162. package/lib/renderers/abstractMobile.render.d.ts +0 -12
  163. package/lib/renderers/abstractMobile.render.js +0 -54
  164. package/lib/renderers/abstractMobile.render.js.map +0 -1
  165. package/lib/renderers/rest-api.renderer.d.ts +0 -7
  166. package/lib/renderers/rest-api.renderer.js +0 -68
  167. package/lib/renderers/rest-api.renderer.js.map +0 -1
  168. package/lib/renderers/visual-editor.renderer.d.ts +0 -7
  169. package/lib/renderers/visual-editor.renderer.js +0 -76
  170. package/lib/renderers/visual-editor.renderer.js.map +0 -1
  171. package/lib/renderers/wikimedia-desktop.renderer.d.ts +0 -4
  172. package/lib/renderers/wikimedia-desktop.renderer.js +0 -7
  173. package/lib/renderers/wikimedia-desktop.renderer.js.map +0 -1
  174. package/lib/renderers/wikimedia-mobile.renderer.d.ts +0 -19
  175. package/lib/renderers/wikimedia-mobile.renderer.js +0 -243
  176. package/lib/renderers/wikimedia-mobile.renderer.js.map +0 -1
  177. package/lib/util/articleListMainPage.d.ts +0 -3
  178. package/lib/util/articleListMainPage.js +0 -10
  179. package/lib/util/articleListMainPage.js.map +0 -1
  180. package/lib/util/articles.d.ts +0 -26
  181. package/lib/util/articles.js +0 -102
  182. package/lib/util/articles.js.map +0 -1
  183. package/lib/util/builders/url/desktop.director.d.ts +0 -8
  184. package/lib/util/builders/url/desktop.director.js +0 -15
  185. package/lib/util/builders/url/desktop.director.js.map +0 -1
  186. package/lib/util/builders/url/mobile.director.d.ts +0 -8
  187. package/lib/util/builders/url/mobile.director.js +0 -15
  188. package/lib/util/builders/url/mobile.director.js.map +0 -1
  189. package/lib/util/builders/url/rest-api.director.d.ts +0 -8
  190. package/lib/util/builders/url/rest-api.director.js +0 -18
  191. package/lib/util/builders/url/rest-api.director.js.map +0 -1
  192. package/lib/util/builders/url/visual-editor.director.d.ts +0 -9
  193. package/lib/util/builders/url/visual-editor.director.js +0 -18
  194. package/lib/util/builders/url/visual-editor.director.js.map +0 -1
  195. package/lib/util/saveArticles.d.ts +0 -8
  196. package/lib/util/saveArticles.js +0 -411
  197. package/lib/util/saveArticles.js.map +0 -1
  198. package/res/article_list_home.js +0 -24
  199. package/res/content.parsoid.css +0 -196
  200. package/res/inserted_style.css +0 -45
  201. package/res/templates/categories.html +0 -11
  202. package/res/templates/lead_section_wrapper.html +0 -6
  203. package/res/templates/pageWikimediaDesktop.html +0 -24
  204. package/res/templates/pageWikimediaMobile.html +0 -24
  205. package/res/templates/section_wrapper.html +0 -5
  206. package/res/templates/subcategories.html +0 -23
  207. package/res/templates/subpages.html +0 -17
  208. package/res/templates/subsection_wrapper.html +0 -5
  209. package/res/webpHandler.js +0 -165
  210. package/res/wm_mobile_override_script.js +0 -15
  211. package/res/wm_mobile_override_style.css +0 -20
  212. package/translation/br.json +0 -8
  213. package/translation/dag.json +0 -9
  214. package/translation/es-formal.json +0 -29
  215. package/translation/ha.json +0 -10
  216. package/translation/hi.json +0 -9
  217. package/translation/ig.json +0 -10
  218. package/translation/kaa.json +0 -9
  219. package/translation/nb.json +0 -9
  220. package/translation/nqo.json +0 -8
  221. package/translation/pt-br.json +0 -10
  222. package/translation/ro.json +0 -9
  223. package/translation/scn.json +0 -8
  224. package/translation/sq.json +0 -9
  225. package/translation/te.json +0 -9
  226. package/translation/tn.json +0 -9
  227. package/translation/tr.json +0 -10
@@ -4,7 +4,6 @@
4
4
  /* MODULE VARIABLE SECTION ********** */
5
5
  /* ********************************** */
6
6
  import fs, { readFileSync } from 'fs';
7
- import os from 'os';
8
7
  import pmap from 'p-map';
9
8
  import sharp from 'sharp';
10
9
  import domino from 'domino';
@@ -13,10 +12,11 @@ import { fileURLToPath } from 'url';
13
12
  import semver from 'semver';
14
13
  import * as path from 'path';
15
14
  import { Blob, Compression, Creator, StringItem } from '@openzim/libzim';
16
- import { checkApiAvailability, getArticleIds } from './util/mw-api.js';
15
+ import { getPages } from './util/mw-api.js';
16
+ import { createTranslator } from './i18n.js';
17
17
  import { zimCreatorMutex } from './mutex.js';
18
18
  import { check_all } from './sanitize-argument.js';
19
- import { MAX_CPU_CORES, MIN_IMAGE_THRESHOLD_ARTICLELIST_PAGE, downloadAndSaveModule, genCanonicalLink, genHeaderCSSLink, genHeaderScript, getDumps, getMediaBase, getRelativeFilePath, getSizeFromUrl, isValidEmail, makeArticleImageTile, makeArticleListItem, mkdirPromise, sanitizeString, saveStaticFiles, addWebpJsScripts, extractArticleList, getTmpDirectory, validateMetadata, truncateUtf8Bytes, } from './util/index.js';
19
+ import { MIN_IMAGE_THRESHOLD_INDEX_PAGE, downloadAndSaveModule, downloadAndSaveStartupModule, getModuleDependencies, downloadAndSaveCustomCss, downloadAndSaveCustomJs, downloadAndSaveMathJaxSource, genCanonicalLink, genHeaderCSSLink, genHeaderScript, getDumps, getMediaBase, getRelativeFilePath, getSizeFromUrl, isValidEmail, makePageImageTile, makePageListItem, mkdirPromise, sanitizeString, saveStaticFiles, extractPageList, getTmpDirectory, validateMetadata, truncateZimEntryTitleWords, makeZimPath, replaceSafe, } from './util/index.js';
20
20
  import S3 from './S3.js';
21
21
  import RedisStore from './RedisStore.js';
22
22
  import * as logger from './Logger.js';
@@ -25,49 +25,80 @@ import { config } from './config.js';
25
25
  import MediaWiki from './MediaWiki.js';
26
26
  import Downloader from './Downloader.js';
27
27
  import RenderingContext from './renderers/rendering.context.js';
28
- import { articleListHomeTemplate, htmlRedirectTemplateCode } from './Templates.js';
29
- import { downloadFiles, saveArticles } from './util/saveArticles.js';
30
- import { getCategoriesForArticles, trimUnmirroredPages } from './util/categories.js';
28
+ import { pageListHomeTemplate, htmlRedirectTemplateCode } from './Templates.js';
29
+ import { savePages } from './util/savePages.js';
30
+ import { PAGE_REQUEST_INTERVAL, CATEGORIES_PAGE_SIZE } from './util/const.js';
31
31
  import urlHelper from './util/url.helper.js';
32
+ import { parseCustomCssUrls, customCssUrlToFilename, parseCustomJsUrls, customJsUrlToFilename } from './util/customCssJs.js';
33
+ import FileManager from './util/FileManager.js';
32
34
  const __filename = fileURLToPath(import.meta.url);
33
35
  const __dirname = path.dirname(__filename);
34
36
  const packageJSON = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
35
37
  async function execute(argv) {
38
+ const { speed: _speed, adminEmail, logLevel, minifyHtml, keepEmptySections, mwUrl, mwActionApiPath, mwModulePath, mwDomain, mwUsername, mwPassword, requestTimeout, customMainPage, customZimTitle, customZimDescription, customZimLongDescription, customZimTags, customZimLanguage, customZimName, customZimFilename, withoutZimFullTextIndex, webp, format, resume, publisher: _publisher, outputDirectory: _outputDirectory, addNamespaces: _addNamespaces, onlyNamespaces: _onlyNamespaces, addContentModels: _addContentModels, javaScript: _javaScript, addModules: _addModules, customZimFavicon, optimisationCacheUrl, customFlavour, forceRender, forceSkin, langVariant, customCss, customJs, userAgent: _userAgent, stableRevision, getCategories, categoriesPageSize, mathJaxSource, mathJaxConfig, mathJaxEntryPoint, mathJaxAllPages, } = argv;
39
+ let { pageList, pageListToIgnore } = argv;
40
+ /* *********************************** */
41
+ /* SYSTEM VARIABLE SECTION */
42
+ /* *********************************** */
43
+ const dumpFormats = getDumps(format);
44
+ const langVariants = getDumps(langVariant);
36
45
  /* ********************************* */
37
46
  /* CUSTOM VARIABLE SECTION ********* */
38
47
  /* ********************************* */
39
- const { speed: _speed, adminEmail, verbose, minifyHtml, keepEmptySections, mwUrl, mwWikiPath, mwIndexPhpPath, mwActionApiPath, mwRestApiPath, mwModulePath, mwDomain, mwUsername, mwPassword, requestTimeout, customMainPage, customZimTitle, customZimDescription, customZimLongDescription, customZimTags, customZimLanguage, withoutZimFullTextIndex, webp, format, filenamePrefix, resume, publisher: _publisher, outputDirectory: _outputDirectory, addNamespaces: _addNamespaces, customZimFavicon, optimisationCacheUrl, customFlavour, forceRender, forceSkin, langVariant, } = argv;
40
- let { articleList, articleListToIgnore } = argv;
41
- logger.setVerboseLevel(verbose ? verbose : 'log'); // Default log level is 'log'
42
- logger.log(`Starting mwoffliner v${packageJSON.version}...`);
43
- // TODO: Move it to sanitaze method
44
- if (articleList)
45
- articleList = String(articleList);
46
- if (articleListToIgnore)
47
- articleListToIgnore = String(articleListToIgnore);
48
+ logger.setLogLevel(logLevel ?? 'info');
49
+ logger.debug(`Starting mwoffliner v${packageJSON.version}...`);
50
+ // TODO: Move it to sanitize method
51
+ if (pageList)
52
+ pageList = String(pageList);
53
+ if (pageListToIgnore)
54
+ pageListToIgnore = String(pageListToIgnore);
55
+ // Parse --customCss and populate Downloader so renderers can use the list
56
+ if (customCss) {
57
+ Downloader.customCssUrls = parseCustomCssUrls(String(customCss));
58
+ if (Downloader.customCssUrls.length > 0) {
59
+ logger.info(`Custom CSS URLs configured: ${Downloader.customCssUrls.join(', ')}`);
60
+ }
61
+ }
62
+ // Parse --customJs and populate Downloader so renderers can use the list
63
+ if (customJs) {
64
+ Downloader.customJsUrls = parseCustomJsUrls(String(customJs));
65
+ if (Downloader.customJsUrls.length > 0) {
66
+ logger.info(`Custom JS URLs configured: ${Downloader.customJsUrls.join(', ')}`);
67
+ }
68
+ }
69
+ if (mathJaxSource) {
70
+ Downloader.mathJaxSource = String(mathJaxSource);
71
+ logger.info(`MathJax source configured: ${Downloader.mathJaxSource}`);
72
+ }
73
+ if (mathJaxConfig) {
74
+ Downloader.mathJaxConfig = String(mathJaxConfig);
75
+ logger.info(`MathJax config configured: ${Downloader.mathJaxConfig}`);
76
+ }
77
+ if (mathJaxEntryPoint) {
78
+ Downloader.mathJaxEntryPoint = String(mathJaxEntryPoint);
79
+ }
80
+ if (mathJaxAllPages) {
81
+ Downloader.mathJaxAllPages = true;
82
+ logger.info('MathJax scripts will be injected on all pages');
83
+ }
48
84
  const publisher = _publisher || config.defaults.publisher;
49
- // TODO: Move it to sanitaze method
85
+ // TODO: Move it to sanitize method
50
86
  /* HTTP user-agent string */
51
87
  // const adminEmail = argv.adminEmail;
52
88
  if (!isValidEmail(adminEmail)) {
53
89
  throw new Error(`Admin email [${adminEmail}] is not valid`);
54
90
  }
55
- // TODO: Move it to sanitaze method
56
- /* Number of parallel requests. To secure stability and avoid HTTP
57
- 429 errors, no more than MAX_CPU_CORES can be considered */
58
- if (_speed && isNaN(_speed)) {
59
- throw new Error('speed is not a number, please give a number value to --speed');
60
- }
61
- const cpuCount = Math.min(os.cpus().length, MAX_CPU_CORES);
62
- const speed = Math.max(1, Math.round(cpuCount * (_speed || 1)));
91
+ const speed = _speed || 1;
92
+ const workers = speed >= 1 ? Math.floor(speed) : 1;
93
+ const pageRequestInterval = speed < 1 ? Math.round(PAGE_REQUEST_INTERVAL / speed) : PAGE_REQUEST_INTERVAL;
63
94
  /* Check Node.js version */
64
95
  const nodeVersionSatisfiesPackage = semver.satisfies(process.version, packageJSON.engines.node);
65
96
  if (!nodeVersionSatisfiesPackage) {
66
97
  logger.warn(`***********\n\n\tCurrent node version is [${process.version}]. We recommend [${packageJSON.engines.node}]\n\n***********`);
67
98
  }
68
- /* Instanciate custom flavour module */
69
- logger.info(`Using custom flavour: ${customFlavour || 'no'}`);
70
- const customProcessor = customFlavour ? new (await import(customFlavour))() : null;
99
+ /* Instantiate custom flavour module */
100
+ logger.debug(`Using custom flavour: ${customFlavour || 'no'}`);
101
+ const customProcessor = customFlavour ? new (await import(customFlavour)).default() : null;
71
102
  let s3Obj;
72
103
  // Check for S3 creds
73
104
  if (optimisationCacheUrl) {
@@ -76,26 +107,35 @@ async function execute(argv) {
76
107
  const s3Url = (s3UrlObj.protocol || 'https:') + '//' + (s3UrlObj.host || '') + (s3UrlObj.pathname || '');
77
108
  s3Obj = new S3(s3Url, s3UrlObj.searchParams, requestTimeout * 1000 || config.defaults.requestTimeout, argv.insecure);
78
109
  await s3Obj.initialise().then(() => {
79
- logger.log('Successfully logged in S3');
110
+ logger.info('Successfully logged in S3');
80
111
  });
81
112
  }
82
113
  // Extract S3 obj to pass to downloader class
83
114
  const s3 = s3Obj ? s3Obj : {};
84
115
  /* Wikipedia/... URL; Normalize by adding trailing / as necessary */
85
116
  MediaWiki.base = mwUrl;
86
- MediaWiki.getCategories = !!argv.getCategories;
117
+ MediaWiki.getCategories = !!getCategories;
118
+ MediaWiki.categoriesPageSize = categoriesPageSize || CATEGORIES_PAGE_SIZE;
87
119
  MediaWiki.actionApiPath = mwActionApiPath;
88
120
  MediaWiki.domain = mwDomain;
89
121
  MediaWiki.password = mwPassword;
90
122
  MediaWiki.username = mwUsername;
123
+ const javaScript = _javaScript || 'trusted';
124
+ const addModules = _addModules ? String(_addModules).split(',') : [];
125
+ const trustedJs = javaScript === 'none' ? null : javaScript === 'trusted' ? config.output.mw.js_trusted.concat(addModules) : [];
126
+ /* HTTP user-agent string */
127
+ const uaString = `${_userAgent || config.userAgent} (${adminEmail})`;
128
+ logger.info(`Using User-Agent: ${uaString}`);
91
129
  /* Download helpers; TODO: Merge with something else / expand this. */
92
130
  Downloader.init = {
93
- uaString: `${config.userAgent} (${adminEmail})`,
94
- speed,
131
+ uaString,
132
+ workers,
133
+ pageRequestInterval: pageRequestInterval,
95
134
  reqTimeout: requestTimeout * 1000 || config.defaults.requestTimeout,
96
135
  optimisationCacheUrl,
97
136
  s3,
98
137
  webp,
138
+ trustedJs,
99
139
  insecure: argv.insecure,
100
140
  };
101
141
  /* perform login */
@@ -110,10 +150,20 @@ async function execute(argv) {
110
150
  .split(',')
111
151
  .map((a) => Number(a))
112
152
  : [];
153
+ const addContentModels = _addContentModels
154
+ ? String(_addContentModels)
155
+ .split(',')
156
+ .map((a) => a.trim())
157
+ : [];
158
+ const onlyNamespaces = _onlyNamespaces
159
+ ? String(_onlyNamespaces)
160
+ .split(',')
161
+ .map((a) => Number(a))
162
+ : [];
113
163
  /* Get MediaWiki Info */
114
164
  let mwMetaData;
115
165
  try {
116
- mwMetaData = await MediaWiki.getMwMetaData({ mwWikiPath, mwIndexPhpPath, addNamespaces, mwRestApiPath, mwModulePath, forceSkin, langVariant });
166
+ mwMetaData = await MediaWiki.getMwMetaData({ addNamespaces, onlyNamespaces, mwModulePath, forceSkin, langVariants });
117
167
  }
118
168
  catch (err) {
119
169
  logger.error('FATAL - Failed to get MediaWiki Metadata');
@@ -130,122 +180,122 @@ async function execute(argv) {
130
180
  };
131
181
  validateMetadata(metaDataRequiredKeys);
132
182
  // Sanitizing main page
133
- let mainPage = articleList ? '' : mwMetaData.mainPage;
183
+ let mainPage = pageList ? '' : mwMetaData.mainPage;
134
184
  if (customMainPage) {
135
- mainPage = customMainPage;
136
- const mainPageUrl = MediaWiki.webUrl + encodeURIComponent(mainPage);
137
- if (!(await checkApiAvailability(mainPageUrl))) {
138
- throw new Error(`customMainPage doesn't return 200 status code for url ${mainPageUrl}`);
139
- }
185
+ mainPage = customMainPage.replace(/_/g, ' ');
140
186
  }
141
- MediaWiki.apiCheckArticleId = mwMetaData.mainPage;
187
+ MediaWiki.apiCheckPageTitle = mwMetaData.mainPage;
142
188
  await MediaWiki.hasCoordinates();
143
- await MediaWiki.hasWikimediaDesktopApi();
144
- const hasWikimediaMobileApi = await MediaWiki.hasWikimediaMobileApi();
145
- await MediaWiki.hasRestApi();
146
- await MediaWiki.hasVisualEditorApi();
147
189
  await MediaWiki.hasActionParseApi();
148
190
  await MediaWiki.hasModuleApi();
149
- await RenderingContext.createRenderers(forceRender, hasWikimediaMobileApi);
191
+ if (stableRevision && !(await MediaWiki.hasFlaggedRevs())) {
192
+ throw new Error('--stableRevision was specified but this wiki does not support stable revisions (FlaggedRevs extension not found)');
193
+ }
194
+ await RenderingContext.createRenderers(forceRender);
150
195
  await RedisStore.connect();
151
- const { articleDetailXId, filesToDownloadXPath, redirectsXId } = RedisStore;
152
196
  // Output directory
153
197
  const outputDirectory = path.isAbsolute(_outputDirectory || '') ? _outputDirectory : path.join(process.cwd(), _outputDirectory || 'out');
154
198
  await mkdirPromise(outputDirectory);
155
- logger.log(`Using output directory ${outputDirectory}`);
199
+ logger.info(`Using output directory ${outputDirectory}`);
156
200
  // Temporary directory
157
201
  const tmpDirectory = await getTmpDirectory();
158
- logger.log(`Using temporary directory ${tmpDirectory}`);
202
+ logger.info(`Using temporary directory ${tmpDirectory}`);
159
203
  process.on('exit', async (code) => {
160
- logger.log(`Exiting with code [${code}]`);
161
- logger.log(`Deleting temporary directory [${tmpDirectory}]`);
204
+ logger.info(`Exiting with code [${code}]`);
205
+ logger.info(`Deleting temporary directory [${tmpDirectory}]`);
162
206
  rimraf.sync(tmpDirectory);
163
207
  });
164
208
  process.on('SIGTERM', async () => {
165
- logger.log('SIGTERM');
209
+ logger.info('SIGTERM');
166
210
  await RedisStore.close();
167
211
  process.exit(128 + 15);
168
212
  });
169
213
  process.on('SIGINT', async () => {
170
- logger.log('SIGINT');
214
+ logger.info('SIGINT');
171
215
  await RedisStore.close();
172
216
  process.exit(128 + 2);
173
217
  });
174
- /* *********************************** */
175
- /* SYSTEM VARIABLE SECTION */
176
- /* *********************************** */
177
- const dumpFormats = getDumps(format);
218
+ const filenameDate = new Date().toISOString().slice(0, 7);
219
+ const dumps = [];
178
220
  /* ********************************* */
179
221
  /* GET CONTENT ********************* */
180
222
  /* ********************************* */
181
- let articleListToIgnoreLines;
182
- if (articleListToIgnore) {
223
+ const pagesToIgnore = [];
224
+ if (pageListToIgnore) {
183
225
  try {
184
- articleListToIgnoreLines = await extractArticleList(articleListToIgnore);
185
- logger.info(`ArticleListToIgnore has [${articleListToIgnoreLines.length}] items`);
226
+ pagesToIgnore.push(...(await extractPageList(pageListToIgnore)));
227
+ logger.debug(`pageListToIgnore has [${pagesToIgnore.length}] items`);
186
228
  }
187
229
  catch (err) {
188
- logger.error(`Failed to read articleListToIgnore from [${articleListToIgnore}]`, err);
230
+ logger.error(`Failed to read pageListToIgnore from [${pageListToIgnore}]`, err);
189
231
  throw err;
190
232
  }
191
233
  }
192
- let articleListLines;
193
- if (articleList) {
234
+ const pages = [];
235
+ if (pageList) {
194
236
  try {
195
- articleListLines = await extractArticleList(articleList);
196
- if (articleListToIgnore) {
197
- articleListLines = articleListLines.filter((title) => !articleListToIgnoreLines.includes(title));
198
- }
199
- logger.info(`ArticleList has [${articleListLines.length}] items`);
237
+ pages.push(...(await extractPageList(pageList)));
238
+ logger.info(`List of pages to include in the ZIM has [${pages.length}] items`);
200
239
  }
201
240
  catch (err) {
202
- logger.error(`Failed to read articleList from [${articleList}]`, err);
241
+ logger.error(`Failed to read pageList from [${pageList}]`, err);
203
242
  throw err;
204
243
  }
205
244
  }
206
- logger.info('Getting article ids');
207
- let stime = Date.now();
208
- await getArticleIds(mainPage, articleList ? articleListLines : null, articleListToIgnore ? articleListToIgnoreLines : null);
209
- logger.log(`Got ArticleIDs in ${(Date.now() - stime) / 1000} seconds`);
210
- if (MediaWiki.getCategories) {
211
- await getCategoriesForArticles(articleDetailXId);
212
- while ((await trimUnmirroredPages()) > 0) {
213
- // Remove unmirrored pages, categories, subCategories
214
- // trimUnmirroredPages returns number of modified articles
245
+ // When the page list contains exactly one page and no custom main
246
+ // page was provided, use that page as the ZIM main page directly
247
+ //(#1891)
248
+ if (pages.length === 1 && !customMainPage) {
249
+ mainPage = pages[0];
250
+ logger.info(`Using single page list entry as main page: ${mainPage}`);
251
+ }
252
+ const t = await createTranslator(mwMetaData.langIso2 || 'en', 'en');
253
+ for (const langVar of langVariants) {
254
+ for (const dumpFormat of dumpFormats) {
255
+ const dump = new Dump(dumpFormat, langVar, {
256
+ tmpDir: tmpDirectory,
257
+ username: mwUsername,
258
+ password: mwPassword,
259
+ outputDirectory,
260
+ mainPage,
261
+ pageList,
262
+ publisher,
263
+ customZimDescription,
264
+ customZimLongDescription,
265
+ customZimTags,
266
+ customZimTitle,
267
+ customZimName,
268
+ customZimFilename,
269
+ withoutZimFullTextIndex,
270
+ resume,
271
+ minifyHtml,
272
+ keepEmptySections,
273
+ tags: customZimTags,
274
+ filenameDate,
275
+ stableRevision,
276
+ }, { ...mwMetaData, mainPage }, customProcessor, t);
277
+ dumps.push(dump);
278
+ // compute ZIM metadata early to fail early in case of issue ; we do that check
279
+ // for each dump, even if it will probably pass on all or fail on first one
280
+ computeZimMetadata(dump);
215
281
  }
216
- // while ((await simplifyGraph(downloader, redisStore)).deletedNodes !== 0) {
217
- // // keep simplifying graph
218
- // }
219
- // await trimUnmirroredPages(downloader); // TODO: improve simplify graph to remove the need for a second trim
220
282
  }
221
- const filenameDate = new Date().toISOString().slice(0, 7);
222
- // Getting total number of articles from Redis
223
- logger.log(`Total articles found in Redis: ${await articleDetailXId.len()}`);
224
- const dumps = [];
225
- for (const dumpFormat of dumpFormats) {
226
- const dump = new Dump(dumpFormat, {
227
- tmpDir: tmpDirectory,
228
- username: mwUsername,
229
- password: mwPassword,
230
- outputDirectory,
231
- mainPage,
232
- filenamePrefix,
233
- articleList,
234
- publisher,
235
- customZimDescription,
236
- customZimLongDescription,
237
- customZimTags,
238
- customZimTitle,
239
- customZimLanguage,
240
- withoutZimFullTextIndex,
241
- resume,
242
- minifyHtml,
243
- keepEmptySections: keepEmptySections ?? argv.keepEmptyParagraphs,
244
- tags: customZimTags,
245
- filenameDate,
246
- }, { ...mwMetaData, mainPage }, customProcessor);
247
- dumps.push(dump);
248
- logger.log('Doing dump');
283
+ const allowedContentModels = ['wikitext', ...addContentModels];
284
+ logger.debug('Getting pages details');
285
+ let stime = Date.now();
286
+ await getPages(mainPage, pages, pagesToIgnore, allowedContentModels);
287
+ logger.info(`Got pages details in ${(Date.now() - stime) / 1000} seconds`);
288
+ // Getting total number of pages from Redis
289
+ logger.info(`Total pages found in Redis: ${await RedisStore.pagesStore.len()}`);
290
+ if (mainPage && !(await RedisStore.pagesStore.exists(mainPage)) && !(await RedisStore.redirectsStore.exists(mainPage))) {
291
+ throw new Error(`mainPage '${mainPage}' was not found`);
292
+ }
293
+ for (const dump of dumps) {
294
+ let logStr = 'Doing dump:';
295
+ if (dump.langVar)
296
+ logStr += ' variant=' + dump.langVar;
297
+ logStr += ` format=${dump.format || 'all'}`;
298
+ logger.info(`******************** ${logStr} ********************`);
249
299
  let shouldSkip = false;
250
300
  try {
251
301
  dump.checkResume();
@@ -254,36 +304,41 @@ async function execute(argv) {
254
304
  shouldSkip = true;
255
305
  }
256
306
  if (shouldSkip) {
257
- logger.log('Skipping dump');
307
+ logger.info('Skipping dump');
258
308
  }
259
309
  else {
310
+ // Reset FileManager for the new dump
311
+ FileManager.reset();
312
+ // Check scraper is still logged-in (session from previous dump might have expired
313
+ // due to inactivity when downloading files)
314
+ await MediaWiki.login(true);
260
315
  await doDump(dump);
261
- await filesToDownloadXPath.flush();
262
- Downloader.cssDependenceUrls = {};
263
- logger.log('Finished dump');
316
+ await RedisStore.filesStore.flush();
317
+ logger.info('Finished dump');
264
318
  }
265
319
  }
266
- logger.log('Closing HTTP agents...');
267
- logger.log('All dumping(s) finished with success.');
268
- async function doDump(dump) {
269
- const outZim = path.resolve(dump.opts.outputDirectory, dump.computeFilenameRadical() + '.zim');
270
- logger.log(`Writing ZIM to [${outZim}]`);
271
- dump.outFile = outZim;
272
- const metadata = {
320
+ logger.info('Closing HTTP agents...');
321
+ logger.info('All dumping(s) finished with success.');
322
+ function computeZimMetadata(dump) {
323
+ dump.outFile = dump.computeZimFullPath();
324
+ logger.info(`Writing ZIM to ${dump.outFile}`);
325
+ dump.zimMetadata = {
273
326
  ...metaDataRequiredKeys,
274
327
  Tags: dump.computeZimTags(),
275
- Name: dump.computeFilenameRadical(false, true, true),
328
+ Name: dump.computeZimName(),
276
329
  Flavour: dump.computeFlavour(),
277
330
  Scraper: `mwoffliner ${packageJSON.version}`,
278
331
  Source: MediaWiki.webUrl.hostname,
279
332
  ...(dump.opts.customZimLongDescription ? { LongDescription: `${dump.opts.customZimLongDescription}` } : {}),
280
333
  };
281
- validateMetadata(metadata);
282
- const zimCreator = new Creator().configCompression(Compression.Zstd);
334
+ validateMetadata(dump.zimMetadata);
335
+ }
336
+ async function doDump(dump) {
337
+ const zimCreator = new Creator().configCompression(Compression.Zstd).configVerbose(logLevel === 'debug');
283
338
  if (!dump.opts.withoutZimFullTextIndex) {
284
339
  zimCreator.configIndexing(true, dump.mwMetaData.langIso3);
285
340
  }
286
- zimCreator.startZimCreation(outZim);
341
+ zimCreator.startZimCreation(dump.outFile);
287
342
  // Helper function to transform a Buffer into a libzim ContentProvider
288
343
  const createBufferContentProvider = (buffer) => {
289
344
  let dataSent = false;
@@ -298,88 +353,180 @@ async function execute(argv) {
298
353
  },
299
354
  };
300
355
  };
301
- Object.entries(metadata).forEach(([key, value]) => {
356
+ Object.entries(dump.zimMetadata).forEach(([key, value]) => {
302
357
  zimCreator.addMetadata(key, Buffer.isBuffer(value) ? createBufferContentProvider(value) : value, key.startsWith('Illustration_') ? 'image/png' : 'text/plain');
303
358
  });
304
359
  await saveFavicon(zimCreator, metaDataRequiredKeys['Illustration_48x48@1']);
305
- if (Downloader.webp) {
306
- logger.log('Adding webp polyfilling JS scripts');
307
- await addWebpJsScripts(zimCreator);
308
- }
309
360
  await getThumbnailsData();
310
361
  if (!mainPage) {
311
- logger.log('Checking Main Page rendering');
362
+ logger.info('Checking Main Page rendering');
312
363
  await createIndexPage(dump, zimCreator, true);
313
364
  }
314
- logger.log('Getting articles');
365
+ // Download and save custom CSS files
366
+ if (Downloader.customCssUrls.length > 0) {
367
+ logger.info(`Downloading ${Downloader.customCssUrls.length} custom CSS file(s)`);
368
+ const cssErrors = [];
369
+ for (const cssUrl of Downloader.customCssUrls) {
370
+ try {
371
+ const filename = customCssUrlToFilename(cssUrl);
372
+ await downloadAndSaveCustomCss(zimCreator, cssUrl, filename);
373
+ }
374
+ catch (err) {
375
+ cssErrors.push(` - Failed to download [${cssUrl}]: ${err}`);
376
+ }
377
+ }
378
+ if (cssErrors.length > 0) {
379
+ throw new Error(`Failed to download custom CSS file(s):\n${cssErrors.join('\n')}`);
380
+ }
381
+ }
382
+ // Download and save custom JS files
383
+ if (Downloader.customJsUrls.length > 0) {
384
+ logger.info(`Downloading ${Downloader.customJsUrls.length} custom JS file(s)`);
385
+ const jsErrors = [];
386
+ for (const jsUrl of Downloader.customJsUrls) {
387
+ try {
388
+ const filename = customJsUrlToFilename(jsUrl);
389
+ await downloadAndSaveCustomJs(zimCreator, jsUrl, filename);
390
+ }
391
+ catch (err) {
392
+ jsErrors.push(` - Failed to download [${jsUrl}]: ${err}`);
393
+ }
394
+ }
395
+ if (jsErrors.length > 0) {
396
+ throw new Error(`Failed to download custom JS file(s):\n${jsErrors.join('\n')}`);
397
+ }
398
+ }
399
+ // Download MathJax config script and store it for inline injection into each page that needs it
400
+ if (Downloader.mathJaxConfig) {
401
+ logger.info(`Loading MathJax config from [${Downloader.mathJaxConfig}]`);
402
+ if (/^https?:\/\//i.test(Downloader.mathJaxConfig)) {
403
+ const { content } = await Downloader.downloadContent(Downloader.mathJaxConfig, 'data');
404
+ Downloader.mathJaxConfigScript = content.toString();
405
+ }
406
+ else {
407
+ Downloader.mathJaxConfigScript = fs.readFileSync(Downloader.mathJaxConfig, 'utf-8');
408
+ }
409
+ if (!Downloader.mathJaxConfigScript.trimStart().startsWith('<script')) {
410
+ throw new Error(`MathJax config loaded from [${Downloader.mathJaxConfig}] does not start with a <script tag`);
411
+ }
412
+ }
413
+ // Download and extract MathJax source ZIP into the ZIM
414
+ if (Downloader.mathJaxSource) {
415
+ await downloadAndSaveMathJaxSource(zimCreator, Downloader.mathJaxSource);
416
+ }
417
+ logger.info('Getting pages content');
315
418
  stime = Date.now();
316
- const { jsModuleDependencies, cssModuleDependencies, staticFilesList } = await saveArticles(zimCreator, dump);
317
- logger.log(`Fetching Articles finished in ${(Date.now() - stime) / 1000} seconds`);
318
- logger.log(`Found [${jsModuleDependencies.size}] js module dependencies`);
319
- logger.log(`Found [${cssModuleDependencies.size}] style module dependencies`);
320
- logger.info('Copying Static Resource Files');
419
+ const { jsModuleDependencies, cssModuleDependencies, staticFilesList } = await savePages(zimCreator, dump);
420
+ logger.info(`Got pages content in ${(Date.now() - stime) / 1000} seconds`);
421
+ logger.debug('Copying Static Resource Files');
321
422
  await saveStaticFiles(staticFilesList, zimCreator);
423
+ if (javaScript === 'none') {
424
+ jsModuleDependencies.clear();
425
+ }
426
+ else {
427
+ // Get list of all possible modules from startup
428
+ const allModules = await downloadAndSaveStartupModule(zimCreator, dump.langVar);
429
+ addModules.forEach((oneModule) => {
430
+ jsModuleDependencies.add(oneModule);
431
+ });
432
+ // Include known dynamic dependencies
433
+ const dynamicJsDeps = config.output.mw.js_dynamic_dependencies;
434
+ Object.keys(dynamicJsDeps).forEach((oneDep) => {
435
+ if (jsModuleDependencies.has(oneDep)) {
436
+ dynamicJsDeps[oneDep].forEach((extraDep) => {
437
+ jsModuleDependencies.add(extraDep);
438
+ });
439
+ }
440
+ });
441
+ // Include all dependencies of the dependencies
442
+ jsModuleDependencies.forEach((oneDep) => {
443
+ const oneModule = allModules.find((oneModule) => oneModule[0] === oneDep);
444
+ if (!oneModule) {
445
+ jsModuleDependencies.delete(oneDep);
446
+ return logger.warn(`Unknown JS module [${oneDep}] removed`);
447
+ }
448
+ getModuleDependencies(oneModule, allModules).forEach((extraDep) => {
449
+ jsModuleDependencies.add(extraDep);
450
+ });
451
+ });
452
+ // Don't store JS for CSS modules
453
+ cssModuleDependencies.forEach((oneModule) => {
454
+ if (!addModules.includes(oneModule)) {
455
+ jsModuleDependencies.delete(oneModule);
456
+ }
457
+ });
458
+ }
459
+ logger.info(`Found [${jsModuleDependencies.size}] js module dependencies`);
460
+ logger.info(`Found [${cssModuleDependencies.size}] style module dependencies`);
322
461
  const allDependenciesWithType = [
323
462
  { type: 'js', moduleList: Array.from(jsModuleDependencies) },
324
463
  { type: 'css', moduleList: Array.from(cssModuleDependencies) },
325
464
  ];
326
- logger.log('Downloading module dependencies');
465
+ logger.info('Downloading module dependencies');
327
466
  await Promise.all(allDependenciesWithType.map(({ type, moduleList }) => {
328
467
  return pmap(moduleList, (oneModule) => {
329
- return downloadAndSaveModule(zimCreator, oneModule, type);
330
- }, { concurrency: Downloader.speed });
468
+ if (oneModule.startsWith('user')) {
469
+ return;
470
+ }
471
+ return downloadAndSaveModule(zimCreator, oneModule, type, dump.langVar);
472
+ }, { concurrency: Downloader.workers });
331
473
  }));
332
- await downloadFiles(filesToDownloadXPath, zimCreator, dump);
333
- logger.log('Writing Article Redirects');
334
- await writeArticleRedirects(dump, zimCreator);
335
- const mainPath = mainPage ? mainPage : await createIndexPage(dump, zimCreator, false);
474
+ await FileManager.startDownloading(zimCreator, dump);
475
+ logger.info('Writing Pages Redirects');
476
+ await writePageRedirects(dump, zimCreator);
477
+ const mainPath = mainPage ? makeZimPath(mainPage) : await createIndexPage(dump, zimCreator, false);
336
478
  zimCreator.setMainPath(mainPath);
337
- logger.log('Finishing ZIM Creation');
479
+ logger.info('Finishing ZIM Creation');
338
480
  await zimCreator.finishZimCreation();
339
- logger.log('Summary of scrape actions:', JSON.stringify(dump.status, null, '\t'));
340
- logger.log(`ZIM is ready at [${outZim}]`);
481
+ logger.info('Summary of scrape actions:', JSON.stringify(dump.status, null, '\t'));
482
+ logger.info(`ZIM is ready at ${dump.outFile}`);
341
483
  }
342
484
  /* ********************************* */
343
485
  /* FUNCTIONS *********************** */
344
486
  /* ********************************* */
345
- async function writeArticleRedirects(dump, zimCreator) {
487
+ async function writePageRedirects(dump, zimCreator) {
346
488
  let processed = -1;
347
- const total = await redirectsXId.len();
348
- logger.log(`${total} redirects to process`);
349
- await redirectsXId.iterateItems(Downloader.speed, async (redirects) => {
350
- for (const [redirectId, { targetId, fragment }] of Object.entries(redirects)) {
489
+ const total = await RedisStore.redirectsStore.len();
490
+ logger.info(`${total} redirects to process`);
491
+ await RedisStore.redirectsStore.iterateItems(Downloader.workers, async (redirects) => {
492
+ for (const { from, to, fragment } of Object.values(redirects)) {
351
493
  processed += 1;
352
- if (processed > 0 && processed % 5000 == 0) {
353
- logger.log(`${processed} redirects have been processed (${Math.round((processed / total) * 1000) / 10} %)`);
494
+ if (processed > 0 && processed % 5000 === 0) {
495
+ logger.info(`${processed} redirects have been processed (${Math.round((processed / total) * 1000) / 10} %)`);
354
496
  }
355
- if (await RedisStore.articleDetailXId.exists(redirectId)) {
356
- logger.warn(`Skipping redirect of '${redirectId}' because it already exists as an article`);
497
+ if (await RedisStore.pagesStore.exists(from)) {
498
+ logger.warn(`Skipping redirect of '${from}' because it already exists as a page`);
357
499
  continue;
358
500
  }
359
- if (redirectId === targetId) {
360
- logger.warn(`Skipping redirect of '${redirectId}' to self`);
501
+ if (from === to) {
502
+ logger.warn(`Skipping redirect of '${from}' to self`);
361
503
  continue;
362
504
  }
363
- if (!(await RedisStore.articleDetailXId.exists(targetId))) {
364
- logger.warn(`Skipping redirect of '${redirectId}' to '${targetId}' because target is not a known article`);
505
+ if (!(await RedisStore.pagesStore.exists(to))) {
506
+ logger.warn(`Skipping redirect of '${from}' to '${to}' because target is not a known page`);
365
507
  continue;
366
508
  }
367
- // We fake a title, by just removing the underscores
368
- const redirectTitle = truncateUtf8Bytes(String(redirectId).replace(/_/g, ' '), 245);
509
+ const redirectTitle = truncateZimEntryTitleWords(from);
369
510
  if (fragment) {
370
- // Should we have a fragment (i.e. we redirect to a section of an article), this is not (yet) supported by libzim
511
+ // Ignore redirects to fragment in 'nodet' since there is mostly no chance the section does exists
512
+ if (dump.nodet) {
513
+ dump.status.redirects.ignored += 1;
514
+ continue;
515
+ }
516
+ // Should we have a fragment (i.e. we redirect to a section of a page), this is not (yet) supported by libzim
371
517
  // (to have such a redirect with a fragment inside the path), so we create a "fake" entry with only an HTML-based
372
518
  // redirect inside
373
- const htmlTemplateString = htmlRedirectTemplateCode()
519
+ const htmlTemplateString = replaceSafe(htmlRedirectTemplateCode())
374
520
  .replace(/__TITLE__/g, redirectTitle)
375
521
  // we have to replace space in fragment with underscores, see https://phabricator.wikimedia.org/T398724
376
- .replace(/__TARGET__/g, `${targetId}#${fragment.replace(/ /g, '_')}`)
377
- .replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(redirectId, ''));
378
- await zimCreatorMutex.runExclusive(() => zimCreator.addItem(new StringItem(redirectId, 'text/html', redirectTitle, { FRONT_ARTICLE: 1 }, htmlTemplateString)));
522
+ .replace(/__TARGET__/g, `${makeZimPath(to)}#${fragment.replace(/ /g, '_')}`)
523
+ .replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(makeZimPath(from), ''))
524
+ .toString();
525
+ await zimCreatorMutex.runExclusive(() => zimCreator.addItem(new StringItem(makeZimPath(from), 'text/html', redirectTitle, { FRONT_ARTICLE: 1 }, htmlTemplateString)));
379
526
  }
380
527
  else {
381
528
  // Otherwise we simply add a "regular" libzim redirect
382
- await zimCreatorMutex.runExclusive(() => zimCreator.addRedirection(redirectId, redirectTitle, targetId, { FRONT_ARTICLE: 1 }));
529
+ await zimCreatorMutex.runExclusive(() => zimCreator.addRedirection(makeZimPath(from), redirectTitle, makeZimPath(to), { FRONT_ARTICLE: 1 }));
383
530
  }
384
531
  dump.status.redirects.written += 1;
385
532
  }
@@ -391,7 +538,7 @@ async function execute(argv) {
391
538
  const faviconIsRemote = customZimFavicon.includes('http');
392
539
  let content;
393
540
  if (faviconIsRemote) {
394
- logger.log(`Downloading remote ZIM favicon from [${customZimFavicon}]`);
541
+ logger.info(`Downloading remote ZIM favicon from [${customZimFavicon}]`);
395
542
  content = await Downloader.request({ url: customZimFavicon, method: 'GET', ...Downloader.arrayBufferRequestOptions })
396
543
  .then((a) => a.data)
397
544
  .catch(() => {
@@ -418,11 +565,18 @@ async function execute(argv) {
418
565
  }
419
566
  const parsedUrl = new URL(mwMetaData.logo, MediaWiki.baseUrl);
420
567
  const logoUrl = parsedUrl.protocol ? mwMetaData.logo : MediaWiki.baseUrl.protocol + mwMetaData.logo;
421
- const { content } = await Downloader.downloadContent(logoUrl, 'image');
422
- return sharp(content).resize(48, 48, resizeOptions).png().toBuffer();
568
+ try {
569
+ const { content } = await Downloader.downloadContent(logoUrl, 'image');
570
+ return sharp(content).resize(48, 48, resizeOptions).png().toBuffer();
571
+ }
572
+ catch (err) {
573
+ logger.error(`Failed to fetch or resize logo declared in site info: ${logoUrl}`);
574
+ logger.error(`Please try specifying a customZimFavicon (--customZimFavicon=./path/to/your/file.ico)`);
575
+ throw err;
576
+ }
423
577
  }
424
578
  async function saveFavicon(zimCreator, data) {
425
- logger.log('Saving favicon.png...');
579
+ logger.info('Saving favicon.png...');
426
580
  try {
427
581
  return zimCreator.addItem(new StringItem(`${config.output.dirs.res}/favicon.png`, 'image/png', null, { FRONT_ARTICLE: 0 }, data));
428
582
  }
@@ -432,33 +586,33 @@ async function execute(argv) {
432
586
  }
433
587
  async function getIndexPath() {
434
588
  for (const candidate of config.candidateIndexPath) {
435
- if (!(await RedisStore.articleDetailXId.exists(candidate)) && !(await RedisStore.redirectsXId.exists(candidate))) {
589
+ if (!(await RedisStore.pagesStore.exists(candidate)) && !(await RedisStore.redirectsStore.exists(candidate))) {
436
590
  return candidate;
437
591
  }
438
592
  }
439
- throw new Error('All candidate main page paths are already used by an article or a redirect');
593
+ throw new Error('All candidate main page paths are already used by a page or a redirect');
440
594
  }
441
595
  /**
442
- * Create a custom index page used as main page, listing all articles in the ZIM
596
+ * Create a custom index page used as main page, listing all pages in the ZIM
443
597
  * Mainly used for selections which do not have a proper main page to use
444
598
  * Supports a dry-run mode where main page is not really created, to be used before we
445
- * fetch all articles. Real index creation must be done after we have fetched all articles,
599
+ * fetch all pages. Real index creation must be done after we have fetched all pages,
446
600
  * should some have failed we do not want to add them to the index
447
601
  */
448
602
  async function createIndexPage(dump, zimCreator, dryrun) {
449
603
  const indexPagePath = await getIndexPath();
450
- const doc = domino.createDocument(articleListHomeTemplate
451
- .replace('</head>', genHeaderCSSLink(config, 'mobile_main_page', dump.mwMetaData.mainPage, config.output.dirs.res) +
604
+ const doc = domino.createDocument(pageListHomeTemplate
605
+ .replace('</head>', genHeaderCSSLink(config, 'mobile_main_page', indexPagePath, config.output.dirs.res) +
452
606
  '\n' +
453
- genHeaderCSSLink(config, 'style', dump.mwMetaData.mainPage, config.output.dirs.res) +
607
+ genHeaderCSSLink(config, 'style', indexPagePath, config.output.dirs.res) +
454
608
  '\n' +
455
- genHeaderScript(config, 'images_loaded.min', dump.mwMetaData.mainPage, config.output.dirs.res) +
609
+ genHeaderScript(config, 'images_loaded.min', indexPagePath, config.output.dirs.res) +
456
610
  '\n' +
457
- genHeaderScript(config, 'masonry.min', dump.mwMetaData.mainPage, config.output.dirs.res) +
611
+ genHeaderScript(config, 'masonry.min', indexPagePath, config.output.dirs.res) +
458
612
  '\n' +
459
- genHeaderScript(config, 'article_list_home', dump.mwMetaData.mainPage, config.output.dirs.res) +
613
+ genHeaderScript(config, 'page_list_home', indexPagePath, config.output.dirs.res) +
460
614
  '\n' +
461
- genCanonicalLink(config, dump.mwMetaData.webUrl, dump.mwMetaData.mainPage) +
615
+ genCanonicalLink(config, dump.mwMetaData.webUrl, indexPagePath) +
462
616
  '\n' +
463
617
  '\n</head>')
464
618
  .replace(/__ASSETS_DIR__/g, config.output.dirs.assets)
@@ -466,33 +620,30 @@ async function execute(argv) {
466
620
  .replace(/__MW_DIR__/g, config.output.dirs.mediawiki)
467
621
  .replace(/__RELATIVE_FILE_PATH__/g, getRelativeFilePath(indexPagePath, '')));
468
622
  doc.querySelector('title').innerHTML = sanitizeString(dump.mwMetaData.title) || sanitizeString(dump.opts.customZimTitle);
469
- const articlesWithImages = [];
470
- const allArticles = [];
471
- for (const articleTitle of articleListLines) {
472
- let articleId = articleTitle.replace(/ /g, '_');
623
+ const pagesWithImages = [];
624
+ const allPages = [];
625
+ for (const page of pages) {
473
626
  // check if this is a redirect
474
- const redirect = await redirectsXId.get(articleId);
475
- if (redirect) {
476
- articleId = redirect.targetId;
477
- }
478
- const articleDetail = await articleDetailXId.get(articleId);
479
- if (articleDetail) {
480
- allArticles.push(articleDetail);
481
- if (articleDetail.thumbnail && articleDetail.internalThumbnailUrl) {
482
- articlesWithImages.push(articleDetail);
483
- if (articlesWithImages.length >= 100) {
627
+ const redirect = await RedisStore.redirectsStore.get(page);
628
+ const entryTitle = redirect ? redirect.to : page;
629
+ const pageDetail = await RedisStore.pagesStore.get(entryTitle);
630
+ if (pageDetail) {
631
+ allPages.push(pageDetail);
632
+ if (pageDetail.thumbnail && pageDetail.internalThumbnailUrl) {
633
+ pagesWithImages.push(pageDetail);
634
+ if (pagesWithImages.length >= 100) {
484
635
  break;
485
636
  }
486
637
  }
487
638
  }
488
639
  }
489
- if (articlesWithImages.length > MIN_IMAGE_THRESHOLD_ARTICLELIST_PAGE) {
490
- const articlesWithImagesEl = articlesWithImages.map((article) => makeArticleImageTile(dump, article)).join('\n');
491
- doc.body.innerHTML = `<div id='container'><div id='content'>${articlesWithImagesEl}</div></div>`;
640
+ if (pagesWithImages.length > MIN_IMAGE_THRESHOLD_INDEX_PAGE) {
641
+ const pagesWithImagesEl = pagesWithImages.map((page) => makePageImageTile(dump, page)).join('\n');
642
+ doc.body.innerHTML = `<div id='container'><div id='content'>${pagesWithImagesEl}</div></div>`;
492
643
  }
493
644
  else {
494
- const articlesWithoutImagesEl = allArticles.map((article) => makeArticleListItem(dump, article)).join('\n');
495
- doc.body.innerHTML = `<ul id='list'>${articlesWithoutImagesEl}</ul>`;
645
+ const pagesWithoutImagesEl = allPages.map((page) => makePageListItem(dump, page)).join('\n');
646
+ doc.body.innerHTML = `<ul id='list'>${pagesWithoutImagesEl}</ul>`;
496
647
  }
497
648
  /* Write the static html file */
498
649
  if (!dryrun) {
@@ -501,44 +652,41 @@ async function execute(argv) {
501
652
  }
502
653
  return indexPagePath;
503
654
  }
504
- async function fetchArticleDetail(articleId) {
505
- return articleDetailXId.get(articleId);
506
- }
507
- async function updateArticleThumbnail(articleDetail, articleId) {
508
- const imageUrl = articleDetail.thumbnail;
655
+ async function updatePageThumbnail(pageDetail, pageTitle) {
656
+ const imageUrl = pageDetail.thumbnail;
509
657
  const { width: oldWidth } = getSizeFromUrl(imageUrl.source);
510
658
  const suitableResUrl = imageUrl.source.replace(`/${oldWidth}px-`, '/500px-').replace(`-${oldWidth}px-`, '-500px-');
511
659
  const { mult, width } = getSizeFromUrl(suitableResUrl);
512
660
  const path = getMediaBase(suitableResUrl, false);
513
- articleDetail.internalThumbnailUrl = getRelativeFilePath('Main_Page', getMediaBase(suitableResUrl, true));
661
+ pageDetail.internalThumbnailUrl = getRelativeFilePath('Main_Page', getMediaBase(suitableResUrl, true));
514
662
  await Promise.all([
515
- filesToDownloadXPath.set(path, { url: urlHelper.serializeUrl(suitableResUrl), mult, width, kind: 'image' }),
516
- articleDetailXId.set(articleId, articleDetail),
663
+ FileManager.addFileToProcess(path, { url: urlHelper.serializeUrl(suitableResUrl), mult, width, kind: 'image' }),
664
+ RedisStore.pagesStore.set(pageTitle, pageDetail),
517
665
  ]);
518
666
  }
519
667
  async function getThumbnailsData() {
520
- if (customMainPage || !articleList || articleListLines.length <= MIN_IMAGE_THRESHOLD_ARTICLELIST_PAGE)
668
+ if (customMainPage || !pageList || pages.length <= MIN_IMAGE_THRESHOLD_INDEX_PAGE)
521
669
  return;
522
- logger.log('Updating article thumbnails for articles');
523
- let articleIndex = 0;
524
- let articlesWithImages = 0;
525
- while (articleIndex < articleListLines.length && articlesWithImages < 100) {
526
- const articleId = articleListLines[articleIndex];
527
- articleIndex++;
670
+ logger.info('Updating page thumbnails for pages');
671
+ let pageIndex = 0;
672
+ let pagesWithImages = 0;
673
+ while (pageIndex < pages.length && pagesWithImages < 100) {
674
+ const pageTitle = pages[pageIndex];
675
+ pageIndex++;
528
676
  try {
529
- const articleDetail = await fetchArticleDetail(articleId);
530
- if (!articleDetail || !articleDetail.thumbnail)
677
+ const pageDetail = await RedisStore.pagesStore.get(pageTitle);
678
+ if (!pageDetail || !pageDetail.thumbnail)
531
679
  continue;
532
- await updateArticleThumbnail(articleDetail, articleId);
533
- articlesWithImages++;
680
+ await updatePageThumbnail(pageDetail, pageTitle);
681
+ pagesWithImages++;
534
682
  }
535
683
  catch {
536
- logger.warn(`Failed to parse thumbnail for [${articleId}], skipping...`);
684
+ logger.warn(`Failed to parse thumbnail for [${pageTitle}], skipping...`);
537
685
  }
538
686
  }
539
687
  }
540
688
  MediaWiki.reset();
541
- RedisStore.close();
689
+ await RedisStore.close();
542
690
  return dumps;
543
691
  }
544
692
  export { execute };