Auto All Page

Otomatis menampilkan semua halaman artikel berita dalam 1 halaman

当前为 2023-06-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto All Page
  3. // @version 2.1.1
  4. // @author reforget-id
  5. // @namespace autoallpage
  6. // @description Otomatis menampilkan semua halaman artikel berita dalam 1 halaman
  7. // @homepage https://github.com/reforget-id/AutoAllPage
  8. // @supportURL https://github.com/reforget-id/AutoAllPage/issues
  9. // @icon https://raw.githubusercontent.com/reforget-id/AutoAllPage/main/assets/icon.png
  10. // @run-at document-start
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_registerMenuCommand
  14. // @noframes
  15. // @exclude https://*?single=1
  16. // @exclude https://*?showpage=all
  17. // @exclude https://*.inews.id/*/all
  18. // @exclude https://*?page=all#page*
  19. // @exclude https://*?page=all#sectionall
  20. // @exclude /^https:\/\/(?!.+\.idntimes\.com).+\?page=all$/
  21. // @exclude https://*/amp/*
  22. // @exclude https://amp.*
  23. // @exclude https://*/amp-*/*
  24. // @exclude https://*/?amp*
  25. // @exclude https://*?amp=1*
  26. // @exclude https://*/*&amp*
  27. // @exclude https://*/*&amp=1*
  28. // @include https://*.100kpj.com/*/*
  29. // @include https://*.aboutmalang.com/*/*/*
  30. // @include https://*.ayocirebon.com/*/*/*
  31. // @include https://akurat.co/*
  32. // @include https://*.bolasport.com/read/*
  33. // @include https://*.cnbcindonesia.com/*/*/*
  34. // @include https://*.cnnindonesia.com/*/*/*
  35. // @include https://*.detik.com/*/d-*/*
  36. // @include https://*.genpi.co/*/*/*
  37. // @include https://*.grid.id/read/*
  38. // @include https://*.gridoto.com/read/*
  39. // @include https://herstory.co.id/read*
  40. // @include https://*.hops.id/*/*/*
  41. // @include https://*.idntimes.com/*/*/*/*
  42. // @include https://*.idxchannel.com/*/*
  43. // @include https://*.inews.id/*/*
  44. // @include https://*.intipseleb.com/*/*
  45. // @include https://*.jatimnetwork.com/*/*/*
  46. // @include https://*.jpnn.com/*/*
  47. // @include https://*.kompas.com/read/*
  48. // @include https://*.kompas.com/*/read/*
  49. // @include https://*.kompas.tv/article/*
  50. // @include https://*.kompasiana.com/*/*/*
  51. // @include https://*.kontan.co.id/news/*
  52. // @include https://*.motorplus-online.com/read/*
  53. // @include https://*.parapuan.co/read/*
  54. // @include https://*.pikiran-rakyat.com/*/pr-*/*
  55. // @include https://*.republika.co.id/berita/*
  56. // @include https://republika.co.id/berita/*
  57. // @include https://*.sahijab.com/*/*
  58. // @include https://*.sindonews.com/read/*
  59. // @include https://*.sonora.id/read/*
  60. // @include https://*.suara.com/*/*/*/*
  61. // @include https://*.tempo.co/read/*
  62. // @include https://*.tribunnews.com/*/*/*/*
  63. // @include https://*.tvonenews.com/*/*
  64. // @include https://*.unews.id/*/*/*
  65. // @include https://*.viva.co.id/*/*
  66. // @include https://wartaekonomi.co.id/read*
  67. // ==/UserScript==
  68.  
  69. // https://*.okezone.com/read/*
  70.  
  71. 'use strict';
  72.  
  73. (() => {
  74. GM_addStyle(`
  75. .aap-divider {
  76. font-size: 18px !important;
  77. font-weight: 600 !important;
  78. margin: 30px 0 30px 0 !important;
  79. text-align: center !important;
  80. }
  81. `)
  82.  
  83. GM_registerMenuCommand('Donate me on Trakteer', () => {
  84. window.open('https://trakteer.id/reforget-id', '_blank')
  85. })
  86.  
  87. class URLBuilder {
  88. constructor() {
  89. this._protocol = 'https'
  90. this._hostname = ''
  91. this._path = ''
  92. this._param = ''
  93. }
  94.  
  95. hostname(hostname) {
  96. this._hostname = hostname
  97. return this
  98. }
  99.  
  100. path(...pathname) {
  101. this._path = this._path + pathname.join('/')
  102. return this
  103. }
  104.  
  105. param(query) {
  106. this._param = '?' + query
  107. return this
  108. }
  109.  
  110. toString() {
  111. let url = `${this._protocol}://${this._hostname}/${this._path}`
  112. if (this._param !== '') url = url + this._param
  113. return url
  114. }
  115. }
  116.  
  117. const url = {
  118. get url() {
  119. return window.location
  120. },
  121. href: () => url.url.href,
  122. hostname: () => url.url.hostname,
  123. path: () => url.url.pathname,
  124. param: () => url.url.search,
  125. }
  126.  
  127. function splitPath(pathname) {
  128. return pathname.split('/').filter(v => v)
  129. }
  130.  
  131. function redirect(url) {
  132. window.location.replace(url)
  133. }
  134.  
  135. function urlChecker(site) {
  136. return site.hostname.test(url.hostname()) && site.path.test(url.path())
  137. }
  138.  
  139. function log(message) {
  140. console.log('[AutoAllPage] ' + message)
  141. }
  142.  
  143. function pageDivider(currentPage, totalPages) {
  144. const divider = new DOMParser().parseFromString(`
  145. <div class="aap-divider">
  146. === [AutoAllPage] Halaman ${currentPage} dari ${totalPages} ===
  147. </div>
  148. `, 'text/html')
  149. return divider.body.firstElementChild
  150. }
  151.  
  152. //******************************************************************************
  153.  
  154. const websites = [
  155. {
  156. id: 'akurat',
  157. description: 'akurat.co',
  158. hostname: /(^|\.)akurat\.co$/,
  159. path: /^\/.+(?<!\/\w+)$/,
  160. method: 'param',
  161. fullpage: 'page=all',
  162. },
  163. {
  164. id: 'cnbc',
  165. description: 'cnbcindonesia.com',
  166. hostname: /(^|\.)cnbcindonesia\.com$/,
  167. path: /\/\d+-\d+-\d+\/.+(\/\d+|(?<!\/\w+))$/,
  168. method: 'param',
  169. fullpage: 'page=all',
  170. },
  171. {
  172. id: 'detik',
  173. description: 'detik.com',
  174. hostname: /(^|\.)detik\.com$/,
  175. path: /\/d-\d+\/.+(\/\d+|(?<!\/\w+))$/,
  176. method: 'param',
  177. fullpage: 'single=1',
  178. },
  179. {
  180. id: 'grid',
  181. description: 'bolasport.com, grid.id, gridoto.com, motorplus-online.com, parapuan.co, sonora.id',
  182. hostname: /(^|\.)(parapuan\.co|(grid|sonora)\.id|(bolasport|gridoto|motorplus-online)\.com)$/,
  183. path: /^\/read\/.+(?<!\/\w+)$/,
  184. method: 'param',
  185. fullpage: 'page=all',
  186. },
  187. {
  188. id: 'idntimes',
  189. description: 'idntimes.com',
  190. hostname: /(^|\.)idntimes\.com$/,
  191. path: /\/[\w-]+\/[\w-]+\/.+\/.+(?<!\/\w+)$/,
  192. method: 'dom',
  193. pagination: 'page=all',
  194. },
  195. {
  196. id: 'idx',
  197. description: 'idxchannel.com',
  198. hostname: /(^|\.)idxchannel\.com$/,
  199. path: /\/.+\/.+(\/\d+|(?<!\/\w+))$/,
  200. method: 'path',
  201. fullpage: 'all',
  202. },
  203. {
  204. id: 'inews',
  205. description: 'inews.id',
  206. hostname: /(^|\.)inews\.id$/,
  207. path: /\/(berita|read\/\d+|[a-z-]+\/[a-z-]+)\/.+(\/\d+|(?<!\/\w+))$/,
  208. method: 'path',
  209. fullpage: 'all',
  210. },
  211. {
  212. id: 'kompascom',
  213. description: 'kompas.com',
  214. hostname: /(^|\.)kompas\.com$/,
  215. path: /\/read\/.+(?<!\/\w+)$/,
  216. method: 'param',
  217. fullpage: 'page=all',
  218. },
  219. {
  220. id: 'kompasiana',
  221. description: 'kompasiana.com',
  222. hostname: /(^|\.)kompasiana\.com$/,
  223. path: /\/.+(?<!series)\/\w{24}\/.+(?<!\/\w+)$/,
  224. method: 'param',
  225. fullpage: 'page=all',
  226. },
  227. {
  228. id: 'kompastv',
  229. description: 'kompas.tv',
  230. hostname: /(^|\.)kompas\.tv$/,
  231. path: /^\/article\/\d+\/.+(?<!\/\w+)$/,
  232. method: 'param',
  233. fullpage: 'page=all',
  234. },
  235. {
  236. id: 'kontan',
  237. description: 'kontan.co.id',
  238. hostname: /(^|\.)kontan\.co\.id$/,
  239. path: /^\/news\/.+(?<!\/\w+)$/,
  240. method: 'param',
  241. fullpage: 'page=all',
  242. },
  243. {
  244. id: 'pr',
  245. description: 'pikiran-rakyat.com',
  246. hostname: /(^|\.)pikiran-rakyat\.com$/,
  247. path: /\/pr-\d+\/.+(?<!\/\w+)$/,
  248. method: 'param',
  249. fullpage: 'page=all',
  250. },
  251. {
  252. id: 'promedia',
  253. description: 'aboutmalang.com, ayocirebon.com, jatimnetwork.com, hops.id, unews.id',
  254. hostname: /(^|\.)((aboutmalang|ayocirebon|jatimnetwork)\.com|(hops|unews)\.id)$/,
  255. path: /\/(pr-|)\d+\/.+(?<!\/\w+)$/,
  256. method: 'param',
  257. fullpage: 'page=all',
  258. },
  259. {
  260. id: 'sindo',
  261. description: 'sindonews.com',
  262. hostname: /(^|\.)sindonews\.com$/,
  263. path: /^\/read\/\d+\/\d+\/.+(\/\d+0|(?<!\/\w+))$/,
  264. method: 'param',
  265. fullpage: 'showpage=all',
  266. },
  267. {
  268. id: 'suara',
  269. description: 'suara.com',
  270. hostname: /(^|\.)suara\.com$/,
  271. path: /\/\d{4}\/\d{2}\/\d{2}\/\d+\/.+(?<!\/\w+)$/,
  272. method: 'param',
  273. fullpage: 'page=all',
  274. },
  275. {
  276. id: 'tribun',
  277. description: 'tribunnews.com',
  278. hostname: /(^|\.)tribunnews\.com$/,
  279. path: /\/\d{4}\/\d{2}\/\d{2}\/.+(?<!\/\w+)$/,
  280. method: 'param',
  281. fullpage: 'page=all',
  282. },
  283. {
  284. id: 'viva',
  285. description: 'viva.co.id, tvonenews.com, intipseleb.com, sahijab.com, 100kpj.com',
  286. hostname: /(^|\.)(viva\.co\.id|(tvonenews|intipseleb|sahijab|100kpj)\.com)$/,
  287. path: /\/.+\/\d+-.+(?<!\/\w+)$/,
  288. method: 'param',
  289. fullpage: 'page=all',
  290. },
  291. {
  292. id: 'wartaekonomi',
  293. description: 'wartaekonomi.co.id, herstory.co.id',
  294. hostname: /(^|\.)(wartaekonomi|herstory)\.co\.id$/,
  295. path: /^\/read\d+\/.+(?<!\/\w+)$/,
  296. method: 'param',
  297. fullpage: 'page=all',
  298. },
  299. {
  300. id: 'cnn',
  301. description: 'cnnindonesia.com',
  302. hostname: /(^|\.)cnnindonesia\.com$/,
  303. path: /\/\d+-\d+-\d+\/.+(\/\d+|(?<!\/\w+))$/,
  304. method: 'xhr',
  305. nextURL: '/',
  306. urlHelper: 0,
  307. desktop: {
  308. pagination: '.anchor_article_long',
  309. totalPages: 'a:last-of-type',
  310. content: '.detail_text',
  311. },
  312. mobile: {
  313. pagination: '.anchor_article_long',
  314. totalPages: 'a:last-of-type',
  315. content: '.detail_text',
  316. },
  317. },
  318. {
  319. id: 'genpi',
  320. description: 'genpi.co',
  321. hostname: /(^|\.)genpi\.co$/,
  322. path: /\/\d+\/.+(?<!\/\w+)$/,
  323. method: 'xhr',
  324. nextURL: '?page=',
  325. urlHelper: 0,
  326. desktop: {
  327. pagination: '.mnmd-pagination',
  328. totalPages: 'li:nth-last-of-type(3) a',
  329. content: 'div.entry-content div.col-md-10',
  330. },
  331. mobile: {
  332. pagination: '.mnmd-pagination',
  333. totalPages: 'li:nth-last-of-type(3) a',
  334. content: '.entry-content',
  335. },
  336. },
  337. {
  338. id: 'jpnn',
  339. description: 'jpnn.com',
  340. hostname: /(^|\.)jpnn\.com$/,
  341. path: /\/(news|[a-z-]+\/\d+)\/.+(?<!\/\w+)$/,
  342. method: 'xhr',
  343. nextURL: '?page=',
  344. urlHelper: 0,
  345. desktop: {
  346. pagination: '.pagination',
  347. totalPages: 'li:nth-last-of-type(3) a',
  348. content: 'div[itemprop=articleBody]',
  349. },
  350. mobile: {
  351. pagination: '.pagination',
  352. totalPages: 'li:nth-last-of-type(3) a',
  353. content: '.page-content',
  354. },
  355. },
  356. {
  357. id: 'okezone',
  358. description: 'okezone.com',
  359. hostname: /(^|\.)okezone\.com$/,
  360. path: /^\/read\/.+(?<!\/\w+)$/,
  361. method: 'xhr',
  362. nextURL: '?page=',
  363. urlHelper: 0,
  364. desktop: {
  365. pagination: '.paging',
  366. totalPages: '.second-paging',
  367. content: '#contentx',
  368. },
  369. mobile: {
  370. pagination: '.pagingxm',
  371. totalPages: '.halnext',
  372. content: '.description',
  373. },
  374. },
  375. {
  376. id: 'republika',
  377. description: 'republika.co.id',
  378. hostname: /(^|\.)republika\.co\.id$/,
  379. path: /^\/berita\/.+(?<!\/\w+)$/,
  380. method: 'xhr',
  381. nextURL: '-part',
  382. urlHelper: 1,
  383. desktop: {
  384. pagination: '.pagination',
  385. totalPages: 'li:nth-last-of-type(2) a',
  386. content: '.artikel',
  387. },
  388. mobile: {
  389. pagination: '.pagination',
  390. totalPages: 'li:nth-last-of-type(2) a',
  391. content: 'article',
  392. },
  393. },
  394. {
  395. id: 'tempo',
  396. description: 'tempo.co',
  397. hostname: /(^|\.)tempo\.co$/,
  398. path: /^\/read\/.+(?<!\/\w+)$/,
  399. method: 'xhr',
  400. nextURL: '?page_num=',
  401. urlHelper: 0,
  402. desktop: {
  403. pagination: '.pagging',
  404. totalPages: 'li:nth-last-of-type(2) a',
  405. content: '#isi',
  406. },
  407. mobile: {
  408. pagination: '.pagging',
  409. totalPages: 'li:nth-last-of-type(2) a',
  410. content: '#isi',
  411. },
  412. },
  413. ]
  414.  
  415. //******************************************************************************
  416.  
  417. const isValidURL = websites.find(urlChecker)
  418. if (isValidURL !== undefined) urlRedirector(isValidURL)
  419.  
  420. function urlRedirector(site) {
  421. const redirectURL = new URLBuilder().hostname(url.hostname())
  422.  
  423. switch (site.method) {
  424. case 'param' :
  425. if (site.id === 'cnbc' || site.id === 'detik') {
  426. const newPath = url.path().replace(/\/\d+$/, '')
  427. redirectURL.path(...splitPath(newPath))
  428. } else {
  429. redirectURL.path(...splitPath(url.path()))
  430. }
  431. redirectURL.param(site.fullpage)
  432. redirect(redirectURL.toString())
  433. break
  434. case 'path' :
  435. inewsRedirect(redirectURL, site.fullpage)
  436. break
  437. case 'dom' :
  438. case 'xhr' :
  439. neutralizeURL(redirectURL, site.id)
  440. }
  441. }
  442.  
  443. function inewsRedirect(redirectURL, fullPage) {
  444. const newPath = url.path().replace(/\/\d+$/, '')
  445. redirectURL.path(...splitPath(newPath), fullPage)
  446. redirect(redirectURL.toString())
  447. }
  448.  
  449. function neutralizeURL(redirectURL, id) {
  450. switch (id) {
  451. case 'genpi' :
  452. case 'idntimes' :
  453. case 'jpnn' :
  454. case 'okezone' :
  455. case 'tempo' :
  456. clearParamRedirect(redirectURL)
  457. break
  458. case 'cnn' :
  459. cnnRedirect(redirectURL)
  460. break
  461. case 'republika' :
  462. republikaRedirect(redirectURL)
  463. }
  464. }
  465.  
  466. function clearParamRedirect(redirectURL) {
  467. if (url.param() !== '') {
  468. redirectURL.path(...splitPath(url.path()))
  469. redirect(redirectURL.toString())
  470. }
  471. }
  472.  
  473. function cnnRedirect(redirectURL) {
  474. if (/\/\d+$/.test(url.path())) {
  475. const newPath = url.path().replace(/\/\d+$/, '')
  476. redirectURL.path(...splitPath(newPath))
  477. redirect(redirectURL.toString())
  478. }
  479. }
  480.  
  481. function republikaRedirect(redirectURL) {
  482. if (/-part\d+$/.test(url.path())) {
  483. const newPath = url.path().replace(/-part\d+$/, '')
  484. redirectURL.path(...splitPath(newPath))
  485. redirect(redirectURL)
  486. }
  487. }
  488.  
  489. //******************************************************************************
  490.  
  491. window.addEventListener('DOMContentLoaded', async () => {
  492. log('DOM telah selesai dimuat')
  493. switch (isValidURL.method) {
  494. case 'dom' :
  495. generalDOM(isValidURL)
  496. break
  497. case 'xhr' :
  498. await generalXHR(isValidURL)
  499. }
  500. })
  501.  
  502. const isMobile = /(^|\.)m\./.test(url.hostname()) || window.navigator.userAgent.includes('Mobi')
  503. const isDesktop = !isMobile
  504.  
  505. function generalDOM(website) {
  506. if (website.id === 'idntimes') {
  507. idntimesDOM()
  508. }
  509. }
  510.  
  511. function idntimesDOM() {
  512. const readMoreButton = document.querySelector('.read-more-btn-check')
  513. const splitPage = document.getElementsByClassName('split-page')
  514.  
  515. if (readMoreButton !== null) {
  516. readMoreButton.remove()
  517. for (let i = 1; i < splitPage.length; i++) {
  518. splitPage[i].classList.add('open')
  519. }
  520. }
  521. }
  522.  
  523. async function generalXHR(website) {
  524. const selector = isMobile ? website.mobile : website.desktop
  525. const totalPages = findTotalPages(selector.pagination, selector.totalPages)
  526. if (totalPages === 1) return
  527. const mainPageNode = document.querySelector(selector.content)
  528. cleaner(website.id, mainPageNode, 1)
  529.  
  530. for (let i = 2; i <= totalPages; i++) {
  531. let nextPageUrl = url.href() + website.nextURL + (i - website.urlHelper)
  532. let nextPageNode = await getNextPage(nextPageUrl, i, selector.content)
  533. if (nextPageNode === null) return
  534. cleaner(website.id, nextPageNode, i, selector.pagination)
  535. let divider = pageDivider(i, totalPages)
  536. mainPageNode.append(divider, ...nextPageNode.children)
  537. log('Menambahkan halaman ke ' + i)
  538. }
  539. }
  540.  
  541. function findTotalPages(paginationSelector, totalPagesSelector) {
  542. let pagination, totalPages
  543. try {
  544. pagination = document.querySelector(paginationSelector)
  545. totalPages = pagination.querySelector(totalPagesSelector)
  546. .textContent
  547. .match(/\d+/)
  548. } catch (e) {
  549. totalPages = 1
  550. log(e)
  551. } finally {
  552. if (totalPages > 1) {
  553. pagination.style.display = 'none'
  554. log('Pagination ditemukan, halaman berjumlah ' + totalPages)
  555. } else {
  556. totalPages = 1
  557. log('Pagination tidak ditemukan')
  558. }
  559. }
  560. return totalPages
  561. }
  562.  
  563. function getNextPage(url, pageNumber, target) {
  564. log('Bersiap membuat XHR')
  565. return new Promise((resolve, reject) => {
  566. GM_xmlhttpRequest({
  567. method: 'GET',
  568. url: url,
  569. overrideMimeType: 'text/html; charset=UTF-8',
  570. responseType: 'document',
  571. binary: false,
  572. timeout: 0,
  573. headers: {
  574. 'user-agent': window.navigator.userAgent,
  575. },
  576. onerror: function () {
  577. alert('[AutoAllPage] Tidak bisa membuka halaman ke ' + pageNumber)
  578. log('Gagal membuat request XHR')
  579. reject(null)
  580. },
  581. onload: function (res) {
  582. const content = res.response.querySelector(target)
  583. if (content != null) {
  584. log('Berhasil mendapatkan halaman ke ' + pageNumber)
  585. resolve(content)
  586. } else {
  587. alert('[AutoAllPage] Gagal mendapatkan halaman ke ' + pageNumber)
  588. log('Gagal mendapatkan halaman ke ' + pageNumber)
  589. reject(null)
  590. }
  591. },
  592. })
  593. })
  594. }
  595.  
  596. //******************************************************************************
  597.  
  598. function cleaner(id, pageNode, pageNumber, pagination) {
  599. switch (id) {
  600. case 'cnn' :
  601. cnnCleaner(pageNode, pageNumber, pagination)
  602. break
  603. case 'genpi' :
  604. genpiCleaner(pageNode, pageNumber, pagination)
  605. break
  606. case 'okezone' :
  607. okezoneCleaner(pageNode, pageNumber)
  608. break
  609. case 'republika' :
  610. republikaCleaner(pageNode, pageNumber, pagination)
  611. break
  612. case 'tempo' :
  613. tempoCleaner(pageNode, pageNumber, pagination)
  614. }
  615. }
  616.  
  617. function cnnCleaner(pageNode, pageNumber, pagination) {
  618. if (isDesktop) {
  619. if (pageNumber === 1) {
  620. document.querySelector('.styled-select')?.remove()
  621. document.querySelector('.skybanner')?.remove()
  622. pageNode.parentNode.style.display = 'block'
  623. }
  624. } else {
  625. if (pageNumber === 1) {
  626. pageNode.querySelector('select[name="page"]')?.parentElement.remove()
  627. } else {
  628. const headerArticle = pageNode.querySelector('.picdetail')
  629. while (pageNode.contains(headerArticle)) {
  630. pageNode.firstElementChild.remove()
  631. }
  632. }
  633. }
  634. pageNode.querySelector('.nav_article_long')?.remove()
  635. pageNode.querySelector(pagination)?.remove()
  636. log('Membersihkan halaman ke ' + pageNumber)
  637. }
  638.  
  639. function genpiCleaner(pageNode, pageNumber, pagination) {
  640. if (isDesktop && pageNumber > 1) pageNode.querySelector('.entry-thumb')?.remove()
  641. const footer = pageNode.querySelector(pagination)
  642. footer?.nextElementSibling.remove()
  643. footer?.remove()
  644. log('Membersihkan halaman ke ' + pageNumber)
  645. }
  646.  
  647. function okezoneCleaner(pageNode, pageNumber) {
  648. const footerArticle = pageNode.querySelector('#rctiplus')?.previousElementSibling
  649. while (pageNode.contains(footerArticle)) {
  650. pageNode.lastElementChild.remove()
  651. }
  652. log('Membersihkan halaman ke ' + pageNumber)
  653. }
  654.  
  655. function republikaCleaner(pageNode, pageNumber, pagination) {
  656. if (isDesktop) {
  657. if (pageNumber > 1) pageNode.querySelector('.taiching')?.remove()
  658. pageNode.querySelector('.baca-juga')?.remove()
  659. } else {
  660. pageNode.querySelector(pagination)?.remove()
  661. }
  662. log('Membersihkan halaman ke ' + pageNumber)
  663. }
  664.  
  665. function tempoCleaner(pageNode, pageNumber, pagination) {
  666. pageNode.querySelector(pagination)?.remove()
  667. log('Membersihkan halaman ke ' + pageNumber)
  668. }
  669. })()