import Redirect from '../Archive/Redirect' import Board from '../classes/Board' import Callbacks from '../classes/Callbacks' import CatalogThreadNative from '../classes/CatalogThreadNative' import DataBoard from '../classes/DataBoard' import Notice from '../classes/Notice' import Post from '../classes/Post' import SimpleDict from '../classes/SimpleDict' import Thread from '../classes/Thread' import Config from '../config/Config' import Anonymize from '../Filtering/Anonymize' import Filter from '../Filtering/Filter' import PostHiding from '../Filtering/PostHiding' import Recursive from '../Filtering/Recursive' import ThreadHiding from '../Filtering/ThreadHiding' import Index from '../General/Index' import Settings from '../General/Settings' import FappeTyme from '../Images/FappeTyme' import Gallery from '../Images/Gallery' import ImageCommon from '../Images/ImageCommon' import ImageExpand from '../Images/ImageExpand' import ImageHost from '../Images/ImageHost' import ImageHover from '../Images/ImageHover' import ImageLoader from '../Images/ImageLoader' import Metadata from '../Images/Metadata' import RevealSpoilers from '../Images/RevealSpoilers' import Sauce from '../Images/Sauce' import Volume from '../Images/Volume' import Linkify from '../Linkification/Linkify' import ArchiveLink from '../Menu/ArchiveLink' import CopyTextLink from '../Menu/CopyTextLink' import DeleteLink from '../Menu/DeleteLink' import DownloadLink from '../Menu/DownloadLink' import ReportLink from '../Menu/ReportLink' import AntiAutoplay from '../Miscellaneous/AntiAutoplay' import Banner from '../Miscellaneous/Banner' import CatalogLinks from '../Miscellaneous/CatalogLinks' import CustomCSS from '../Miscellaneous/CustomCSS' import ExpandComment from '../Miscellaneous/ExpandComment' import ExpandThread from '../Miscellaneous/ExpandThread' import FileInfo from '../Miscellaneous/FileInfo' import Flash from '../Miscellaneous/Flash' import Fourchan from '../Miscellaneous/Fourchan' import IDColor from '../Miscellaneous/IDColor' import IDHighlight from '../Miscellaneous/IDHighlight' import IDPostCount from '../Miscellaneous/IDPostCount' import Keybinds from '../Miscellaneous/Keybinds' import ModContact from '../Miscellaneous/ModContact' import Nav from '../Miscellaneous/Nav' import NormalizeURL from '../Miscellaneous/NormalizeURL' import PostJumper from '../Miscellaneous/PostJumper' import PSA from '../Miscellaneous/PSA' import PSAHiding from '../Miscellaneous/PSAHiding' import RelativeDates from '../Miscellaneous/RelativeDates' import RemoveSpoilers from '../Miscellaneous/RemoveSpoilers' import ThreadLinks from '../Miscellaneous/ThreadLinks' import Time from '../Miscellaneous/Time' import Tinyboard from '../Miscellaneous/Tinyboard' import Favicon from '../Monitoring/Favicon' import MarkNewIPs from '../Monitoring/MarkNewIPs' import ReplyPruning from '../Monitoring/ReplyPruning' import ThreadStats from '../Monitoring/ThreadStats' import ThreadUpdater from '../Monitoring/ThreadUpdater' import ThreadWatcher from '../Monitoring/ThreadWatcher' import Unread from '../Monitoring/Unread' import UnreadIndex from '../Monitoring/UnreadIndex' import $ from '../platform/$' import $$ from '../platform/$$' import PassLink from '../Posting/PassLink' import PostRedirect from '../Posting/PostRedirect' import QR from '../Posting/QR' import QuoteBacklink from '../Quotelinks/QuoteBacklink' import QuoteCT from '../Quotelinks/QuoteCT' import QuoteInline from '../Quotelinks/QuoteInline' import QuoteOP from '../Quotelinks/QuoteOP' import QuotePreview from '../Quotelinks/QuotePreview' import QuoteStrikeThrough from '../Quotelinks/QuoteStrikeThrough' import QuoteThreading from '../Quotelinks/QuoteThreading' import QuoteYou from '../Quotelinks/QuoteYou' import Quotify from '../Quotelinks/Quotify' import Site from '../site/Site' import SW from '../site/SW' import CSS from '../css/CSS' import meta from '../../package.json' import Header from '../General/Header' import { c, Conf, d, doc, docSet, E, g } from '../globals/globals' import Menu from '../Menu/Menu' import BoardConfig from '../General/BoardConfig' import CaptchaReplace from '../Posting/Captcha.replace' import Get from '../General/Get' import { dict, platform } from '../platform/helpers' import Polyfill from '../General/Polyfill' // import Test from "../General/Test"; /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS104: Avoid inline assignments * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ var Main = { init() { // XXX dwb userscripts extension reloads scripts run at document-start when replaceState/pushState is called. // XXX Firefox reinjects WebExtension content scripts when extension is updated / reloaded. let key try { let w = window if (platform === 'crx') { w = w.wrappedJSObject || w } if (`${meta.name} antidup` in w) { return } w[`${meta.name} antidup`] = true } catch (error) {} // Don't run inside ad iframes. try { if ( window.frameElement && ['', 'about:blank'].includes(window.frameElement.src) ) { return } } catch (error1) {} // Detect multiple copies of 4chan X if (doc && $.hasClass(doc, 'fourchan-x')) { return } $.asap(docSet, function () { $.addClass(doc, 'fourchan-x', 'seaweedchan') if ($.engine) { return $.addClass(doc, `ua-${$.engine}`) } }) $.on(d, '4chanXInitFinished', function () { if (Main.expectInitFinished) { return delete Main.expectInitFinished } else { new Notice('error', 'Error: Multiple copies of 4chan X are enabled.') return $.addClass(doc, 'tainted') } }) // Detect "mounted" event from Kissu var mountedCB = function () { d.removeEventListener('mounted', mountedCB, true) Main.isMounted = true return Main.mountedCBs.map((cb) => (() => { try { return cb() } catch (error2) {} })(), ) } d.addEventListener('mounted', mountedCB, true) // Flatten default values from Config into Conf var flatten = function (parent, obj) { if (obj instanceof Array) { Conf[parent] = dict.clone(obj[0]) } else if (typeof obj === 'object') { for (var key in obj) { var val = obj[key] flatten(key, val) } } else { // string or number Conf[parent] = obj } } // XXX Remove document-breaking ad if ( ['boards.4chan.org', 'boards.4channel.org'].includes(location.hostname) ) { $.global(function () { const fromCharCode0 = String.fromCharCode return (String.fromCharCode = function () { if (document.body) { String.fromCharCode = fromCharCode0 } else if (document.currentScript && !document.currentScript.src) { throw Error() } return fromCharCode0.apply(this, arguments) }) }) $.asap(docSet, () => $.onExists(doc, 'iframe[srcdoc]', $.rm)) } flatten(null, Config) for (var db of DataBoard.keys) { Conf[db] = dict() } Conf['customTitles'] = dict.clone({ '4chan.org': { boards: { qa: { boardTitle: { orig: '/qa/ - Question & Answer', title: '/qa/ - 2D/Random', }, }, }, }, }) Conf['boardConfig'] = { boards: dict() } Conf['archives'] = Redirect.archives Conf['selectedArchives'] = dict() Conf['cooldowns'] = dict() Conf['Index Sort'] = dict() for (let i = 0; i < 2; i++) { Conf[`Last Long Reply Thresholds ${i}`] = dict() } Conf['siteProperties'] = dict() // XXX old key names Conf['Except Archives from Encryption'] = false Conf['JSON Navigation'] = true Conf['Oekaki Links'] = true Conf['Show Name and Subject'] = false Conf['QR Shortcut'] = true Conf['Bottom QR Link'] = true Conf['Toggleable Thread Watcher'] = true Conf['siteSoftware'] = '' Conf['Use Faster Image Host'] = 'true' Conf['Captcha Fixes'] = true Conf['captchaServiceDomain'] = '' Conf['captchaServiceKey'] = dict() // Enforce JS whitelist if ( /\.4chan(?:nel)?\.org$/.test(location.hostname) && !SW.yotsuba.regexp.pass.test(location.href) && !SW.yotsuba.regexp.captcha.test(location.href) && !$$('script:not([src])', d).filter((s) => /this\[/.test(s.textContent)) .length ) { ;($.getSync || $.get)( { jsWhitelist: Conf['jsWhitelist'] }, ({ jsWhitelist }) => $.addCSP( `script-src ${jsWhitelist .replace(/^#.*$/gm, '') .replace(/[\s;]+/g, ' ') .trim()}`, ), ) } // Get saved values as items const items = dict() for (key in Conf) { items[key] = undefined } items['previousversion'] = undefined return ($.getSync || $.get)(items, function (items) { if ( !$.perProtocolSettings && /\.4chan(?:nel)?\.org$/.test(location.hostname) && (items['Redirect to HTTPS'] ?? Conf['Redirect to HTTPS']) && location.protocol !== 'https:' ) { location.replace( 'https://' + location.host + location.pathname + location.search + location.hash, ) return } return $.asap(docSet, function () { // Don't hide the local storage warning behind a settings panel. if ($.cantSet) { // pass // Fresh install } else if (items.previousversion == null) { Main.isFirstRun = true Main.ready(function () { $.set('previousversion', g.VERSION) return Settings.open() }) // Migrate old settings } else if (items.previousversion !== g.VERSION) { Main.upgrade(items) } // Combine default values with saved values for (key in Conf) { var val = Conf[key] Conf[key] = items[key] ?? val } return Site.init(Main.initFeatures) }) }) }, upgrade(items) { const { previousversion } = items const changes = Settings.upgrade(items, previousversion) items.previousversion = changes.previousversion = g.VERSION return $.set(changes, function () { if (items['Show Updated Notifications'] ?? true) { const el = $.el('span', { innerHTML: `${meta.name} has been updated to version ${g.VERSION}.`, }) return new Notice('info', el, 15) } }) }, parseURL(site = g.SITE, url = location) { const r = {} if (!site) { return r } r.siteID = site.ID if (site.isBoardlessPage?.(url)) { return r } const pathname = url.pathname.split(/\/+/) r.boardID = pathname[1] if (site.isFileURL(url)) { r.VIEW = 'file' } else if (site.isAuxiliaryPage?.(url)) { // pass } else if (['thread', 'res'].includes(pathname[2])) { r.VIEW = 'thread' r.threadID = r.THREADID = +pathname[3].replace(/\.\w+$/, '') } else if (pathname[2] === 'archive' && pathname[3] === 'res') { r.VIEW = 'thread' r.threadID = r.THREADID = +pathname[4].replace(/\.\w+$/, '') r.threadArchived = true } else if (/^(?:catalog|archive)(?:\.\w+)?$/.test(pathname[2])) { r.VIEW = pathname[2].replace(/\.\w+$/, '') } else if (/^(?:index|\d*)(?:\.\w+)?$/.test(pathname[2])) { r.VIEW = 'index' } return r }, initFeatures() { $.global(function () { document.documentElement.classList.add('js-enabled') return (window.FCX = {}) }) Main.jsEnabled = $.hasClass(doc, 'js-enabled') // XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 $.ajaxPageInit?.() $.extend(g, Main.parseURL()) if (g.boardID) { g.BOARD = new Board(g.boardID) } if (!g.VIEW) { g.SITE.initAuxiliary?.() return } if (g.VIEW === 'file') { $.asap( () => d.readyState !== 'loading', function () { let video if ( g.SITE.software === 'yotsuba' && Conf['404 Redirect'] && g.SITE.is404?.() ) { const pathname = location.pathname.split(/\/+/) return Redirect.navigate('file', { boardID: g.BOARD.ID, filename: pathname[pathname.length - 1], }) } else if ((video = $('video'))) { if (Conf['Volume in New Tab']) { Volume.setup(video) } if (Conf['Loop in New Tab']) { video.loop = true video.controls = false video.play() return ImageCommon.addControls(video) } } }, ) return } g.threads = new SimpleDict() g.posts = new SimpleDict() // set up CSS when is completely loaded $.onExists(doc, 'body', Main.initStyle) // c.time 'All initializations' for (var [name, feature] of Main.features) { if (g.SITE.disabledFeatures && g.SITE.disabledFeatures.includes(name)) { continue } // c.time "#{name} initialization" try { feature.init() } catch (err) { Main.handleErrors({ message: `\"${name}\" initialization crashed.`, error: err, }) } } // finally // c.timeEnd "#{name} initialization" // c.timeEnd 'All initializations' return $.ready(Main.initReady) }, initStyle() { if (!Main.isThisPageLegit()) { return } // disable the mobile layout const mobileLink = $('link[href*=mobile]', d.head) if (mobileLink) mobileLink.disabled = true doc.dataset.host = location.host $.addClass(doc, `sw-${g.SITE.software}`) $.addClass(doc, g.VIEW === 'thread' ? 'thread-view' : g.VIEW) $.onExists(doc, '.ad-cnt, .adg-rects > .desktop', (ad) => $.onExists(ad, 'img, iframe', () => $.addClass(doc, 'ads-loaded')), ) if (Conf['Autohiding Scrollbar']) { $.addClass(doc, 'autohiding-scrollbar') } $.ready(function () { if ( d.body.clientHeight > doc.clientHeight && (window.innerWidth === doc.clientWidth) !== Conf['Autohiding Scrollbar'] ) { Conf['Autohiding Scrollbar'] = !Conf['Autohiding Scrollbar'] $.set('Autohiding Scrollbar', Conf['Autohiding Scrollbar']) return $.toggleClass(doc, 'autohiding-scrollbar') } }) $.addStyle(CSS.sub(CSS.boards), 'fourchanx-css') Main.bgColorStyle = $.el('style', { id: 'fourchanx-bgcolor-css' }) let keyboard = false $.on(d, 'mousedown', () => (keyboard = false)) $.on(d, 'keydown', function (e) { if (e.keyCode === 9) { return (keyboard = true) } }) // tab window.addEventListener( 'focus', () => doc.classList.toggle('keyboard-focus', keyboard), true, ) return Main.setClass() }, setClass() { let mainStyleSheet, style, styleSheets const knownStyles = [ 'yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'photon', 'tomorrow', 'spooky', ] if (g.SITE.software === 'yotsuba' && g.VIEW === 'catalog') { if ((mainStyleSheet = $.id('base-css'))) { style = mainStyleSheet.href .match(/catalog_(\w+)/)?.[1] .replace('_new', '') .replace(/_+/g, '-') if (knownStyles.includes(style)) { $.addClass(doc, style) return } } } style = mainStyleSheet = styleSheets = null const setStyle = function () { // Use preconfigured CSS for 4chan's default themes. if (g.SITE.software === 'yotsuba') { $.rmClass(doc, style) style = null for (var styleSheet of styleSheets) { if (styleSheet.href === mainStyleSheet?.href) { style = styleSheet.title .toLowerCase() .replace('new', '') .trim() .replace(/\s+/g, '-') if (style === '_special') { style = styleSheet.href.match(/[a-z]*(?=[^/]*$)/)[0] } if (!knownStyles.includes(style)) { style = null } break } } if (style) { $.addClass(doc, style) $.rm(Main.bgColorStyle) return } } // Determine proper dialog background color for other themes. const div = g.SITE.bgColoredEl() div.style.position = 'absolute' div.style.visibility = 'hidden' $.add(d.body, div) let bgColor = window.getComputedStyle(div).backgroundColor $.rm(div) const rgb = bgColor.match(/[\d.]+/g) // Use body background if reply background is transparent if (!/^rgb\(/.test(bgColor)) { const s = window.getComputedStyle(d.body) bgColor = `${s.backgroundColor} ${s.backgroundImage} ${s.backgroundRepeat} ${s.backgroundPosition}` } let css = `\ .dialog, .suboption-list > div:last-of-type, :root.catalog-hover-expand .catalog-container:hover > .post { background: ${bgColor}; } .unread-mark-read { background-color: rgba(${rgb.slice(0, 3).join(', ')}, ${0.5 * (rgb[3] || 1)}); }\ ` if ($.luma(rgb) < 100) { css += `\ .watch-thread-link { background-image: url("data:image/svg+xml,"); }\ ` } Main.bgColorStyle.textContent = css return $.after($.id('fourchanx-css'), Main.bgColorStyle) } $.onExists(d.head, g.SITE.selectors.styleSheet, function (el) { mainStyleSheet = el if (g.SITE.software === 'yotsuba') { styleSheets = $$('link[rel="alternate stylesheet"]', d.head) } new MutationObserver(setStyle).observe(mainStyleSheet, { attributes: true, attributeFilter: ['href'], }) $.on(mainStyleSheet, 'load', setStyle) return setStyle() }) if (!mainStyleSheet) { for (var styleSheet of $$('link[rel="stylesheet"]', d.head)) { $.on(styleSheet, 'load', setStyle) } return setStyle() } }, initReady() { if (g.SITE.is404?.()) { if (g.VIEW === 'thread') { ThreadWatcher.set404(g.BOARD.ID, g.THREADID, function () { if (Conf['404 Redirect']) { return Redirect.navigate( 'thread', { boardID: g.BOARD.ID, threadID: g.THREADID, postID: +location.hash.match(/\d+/), }, // post number or 0 `/${g.BOARD}/`, ) } }) } return } if (g.SITE.isIncomplete?.()) { const msg = $.el('div', { innerHTML: 'The page didn't load completely.
Some features may not work unless you reload.', }) $.on($('a', msg), 'click', () => location.reload()) new Notice('warning', msg) } // Parse HTML or skip it and start building from JSON. if (g.VIEW === 'catalog') { return Main.initCatalog() } else if (!Index.enabled) { if (g.SITE.awaitBoard) { return g.SITE.awaitBoard(Main.initThread) } else { return Main.initThread() } } else { Main.expectInitFinished = true return $.event('4chanXInitFinished') } }, initThread() { let board const s = g.SITE.selectors if ((board = $(s.boardFor?.[g.VIEW] || s.board))) { const threads = [] const posts = [] const errors = [] try { g.SITE.preParsingFixes?.(board) } catch (error) {} Main.addThreadsObserver = new MutationObserver(Main.addThreads) Main.addPostsObserver = new MutationObserver(Main.addPosts) Main.addThreadsObserver.observe(board, { childList: true }) Main.parseThreads($$(s.thread, board), threads, posts, errors) if (errors.length) { Main.handleErrors(errors) } if (g.VIEW === 'thread') { if (g.threadArchived) { threads[0].isArchived = true threads[0].kill() } g.SITE.parseThreadMetadata?.(threads[0]) } Main.callbackNodes('Thread', threads) return Main.callbackNodesDB('Post', posts, function () { for (var post of posts) { QuoteThreading.insert(post) } Main.expectInitFinished = true return $.event('4chanXInitFinished') }) } else { Main.expectInitFinished = true return $.event('4chanXInitFinished') } }, parseThreads(threadRoots, threads, posts, errors) { for (var threadRoot of threadRoots) { var boardObj = (() => { let boardID if ((boardID = threadRoot.dataset.board)) { boardID = encodeURIComponent(boardID) return g.boards[boardID] || new Board(boardID) } else { return g.BOARD } })() var threadID = +threadRoot.id.match(/\d*$/)[0] if (!threadID || boardObj.threads.get(threadID)?.nodes.root) { return } var thread = new Thread(threadID, boardObj) thread.nodes.root = threadRoot threads.push(thread) var postRoots = $$(g.SITE.selectors.postContainer, threadRoot) if (g.SITE.isOPContainerThread) { postRoots.unshift(threadRoot) } Main.parsePosts(postRoots, thread, posts, errors) Main.addPostsObserver.observe(threadRoot, { childList: true }) } }, parsePosts(postRoots, thread, posts, errors) { for (var postRoot of postRoots) { if ( !(postRoot.dataset.fullID && g.posts.get(postRoot.dataset.fullID)) && $(g.SITE.selectors.comment, postRoot) ) { try { posts.push(new Post(postRoot, thread, thread.board)) } catch (err) { // Skip posts that we failed to parse. errors.push({ message: `Parsing of Post No.${postRoot.id.match( /\d+/, )} failed. Post will be skipped.`, error: err, html: postRoot.outerHTML, }) } } } }, addThreads(records) { const threadRoots = [] for (var record of records) { for (var node of record.addedNodes) { if ( node.nodeType === Node.ELEMENT_NODE && node.matches(g.SITE.selectors.thread) ) { threadRoots.push(node) } } } if (!threadRoots.length) { return } const threads = [] const posts = [] const errors = [] Main.parseThreads(threadRoots, threads, posts, errors) if (errors.length) { Main.handleErrors(errors) } Main.callbackNodes('Thread', threads) return Main.callbackNodesDB('Post', posts, () => $.event('PostsInserted', null, records[0].target), ) }, addPosts(records) { let thread const threads = [] const threadsRM = [] const posts = [] const errors = [] for (var record of records) { thread = Get.threadFromRoot(record.target) var postRoots = [] for (var node of record.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if ( node.matches(g.SITE.selectors.postContainer) || (node = $(g.SITE.selectors.postContainer, node)) ) { postRoots.push(node) } } } var n = posts.length Main.parsePosts(postRoots, thread, posts, errors) if (posts.length > n && !threads.includes(thread)) { threads.push(thread) } var anyRemoved = false for (var el of record.removedNodes) { if (Get.postFromRoot(el)?.nodes.root === el && !doc.contains(el)) { anyRemoved = true break } } if (anyRemoved && !threadsRM.includes(thread)) { threadsRM.push(thread) } } if (errors.length) { Main.handleErrors(errors) } return Main.callbackNodesDB('Post', posts, function () { for (thread of threads) { $.event('PostsInserted', null, thread.nodes.root) } for (thread of threadsRM) { $.event('PostsRemoved', null, thread.nodes.root) } }) }, initCatalog() { let board const s = g.SITE.selectors.catalog if (s && (board = $(s.board))) { const threads = [] const errors = [] Main.addCatalogThreadsObserver = new MutationObserver( Main.addCatalogThreads, ) Main.addCatalogThreadsObserver.observe(board, { childList: true }) Main.parseCatalogThreads($$(s.thread, board), threads, errors) if (errors.length) { Main.handleErrors(errors) } Main.callbackNodes('CatalogThreadNative', threads) } Main.expectInitFinished = true return $.event('4chanXInitFinished') }, parseCatalogThreads(threadRoots, threads, errors) { for (var threadRoot of threadRoots) { try { var thread = new CatalogThreadNative(threadRoot) if (thread.thread.catalogViewNative?.nodes.root !== threadRoot) { thread.thread.catalogViewNative = thread threads.push(thread) } } catch (err) { // Skip threads that we failed to parse. errors.push({ message: `Parsing of Catalog Thread No.${( threadRoot.dataset.id || threadRoot.id ).match(/\d+/)} failed. Thread will be skipped.`, error: err, html: threadRoot.outerHTML, }) } } }, addCatalogThreads(records) { const threadRoots = [] for (var record of records) { for (var node of record.addedNodes) { if ( node.nodeType === Node.ELEMENT_NODE && node.matches(g.SITE.selectors.catalog.thread) ) { threadRoots.push(node) } } } if (!threadRoots.length) { return } const threads = [] const errors = [] Main.parseCatalogThreads(threadRoots, threads, errors) if (errors.length) { Main.handleErrors(errors) } return Main.callbackNodes('CatalogThreadNative', threads) }, callbackNodes(klass, nodes) { let node let i = 0 const cb = Callbacks[klass] while ((node = nodes[i++])) { cb.execute(node) } }, callbackNodesDB(klass, nodes, cb) { let i = 0 const cbs = Callbacks[klass] const fn = function () { let node if (!(node = nodes[i])) { return false } cbs.execute(node) return ++i % 25 } var softTask = function () { while (fn()) { continue } if (!nodes[i]) { if (cb) { cb() } return } return setTimeout(softTask, 0) } return softTask() }, handleErrors(errors) { // Detect conflicts with 4chan X v2 let error if ( d.body && $.hasClass(d.body, 'fourchan_x') && !$.hasClass(doc, 'tainted') ) { new Notice('error', 'Error: Multiple copies of 4chan X are enabled.') $.addClass(doc, 'tainted') } // Detect conflicts with native extension if (g.SITE.testNativeExtension && !$.hasClass(doc, 'tainted')) { const { enabled } = g.SITE.testNativeExtension() if (enabled) { $.addClass(doc, 'tainted') if (Conf['Disable Native Extension'] && !Main.isFirstRun) { const msg = $.el('div', { innerHTML: 'Failed to disable the native extension. You may need to block it.', }) new Notice('error', msg) } } } if (!(errors instanceof Array)) { error = errors } else if (errors.length === 1) { error = errors[0] } if (error) { new Notice('error', Main.parseError(error, Main.reportLink([error])), 15) return } const div = $.el('div', { innerHTML: `${errors.length} errors occurred.${ Main.reportLink(errors).innerHTML } [show]`, }) $.on(div.lastElementChild, 'click', function () { let ref return ( ([this.textContent, logs.hidden] = Array.from( (ref = this.textContent === 'show' ? ['hide', false] : ['show', true]), )), ref ) }) var logs = $.el('div', { hidden: true }) for (error of errors) { $.add(logs, Main.parseError(error)) } return new Notice('error', [div, logs], 30) }, parseError(data, reportLink) { c.error(data.message, data.error.stack) const message = $.el('div', { innerHTML: E(data.message) + (reportLink ? reportLink.innerHTML : ''), }) const error = $.el('div', { textContent: `${data.error.name || 'Error'}: ${ data.error.message || 'see console for details' }`, }) const lines = data.error.stack ?.match(/\d+(?=:\d+\)?$)/gm) ?.join() .replace(/^/, ' at ') || '' const context = $.el('div', { textContent: `(${meta.name} ${meta.fork} v${g.VERSION} ${platform} on ${$.engine}${lines})`, }) return [message, error, context] }, reportLink(errors) { let info const data = errors[0] let title = data.message if (errors.length > 1) { title += ` (+${errors.length - 1} other errors)` } let details = '' const addDetails = function (text) { if ( encodeURIComponent(title + details + text + '\n').length <= meta.newIssueMaxLength - meta.newIssue.replace(/%(title|details)/, '').length ) { return (details += text + '\n') } } addDetails(`\ [Please describe the steps needed to reproduce this error.] Script: ${meta.name} ${meta.fork} v${g.VERSION} ${platform} URL: ${location.href} User agent: ${navigator.userAgent}\ `) if ( platform === 'userscript' && (info = (() => { if (typeof GM !== 'undefined' && GM !== null) { return GM.info } else { if (typeof GM_info !== 'undefined' && GM_info !== null) { return GM_info } } })()) ) { addDetails(`Userscript manager: ${info.scriptHandler} ${info.version}`) } addDetails('\n' + data.error) if (data.error.stack) { addDetails(data.error.stack.replace(data.error.toString(), '').trim()) } if (data.html) { addDetails('\n`' + data.html + '`') } details = details.replace(/file:\/{3}.+\//g, '') // Remove local file paths const url = meta.newIssue .replace('%title', encodeURIComponent(title)) .replace('%details', encodeURIComponent(details)) return { innerHTML: ` [report]`, } }, isThisPageLegit() { // not 404 error page or similar. if (!('thisPageIsLegit' in Main)) { Main.thisPageIsLegit = g.SITE.isThisPageLegit ? g.SITE.isThisPageLegit() : !/^[45]\d\d\b/.test(document.title) && !/\.(?:json|rss)$/.test(location.pathname) } return Main.thisPageIsLegit }, ready(cb) { return $.ready(function () { if (Main.isThisPageLegit()) { return cb() } }) }, mounted(cb) { if (Main.isMounted) { return cb() } else { return Main.mountedCBs.push(cb) } }, mountedCBs: [], features: [ ['Polyfill', Polyfill], ['Board Configuration', BoardConfig], ['Normalize URL', NormalizeURL], ['Delay Redirect on Post', PostRedirect], ['Captcha Configuration', CaptchaReplace], ['Image Host Rewriting', ImageHost], ['Redirect', Redirect], ['Header', Header], ['Catalog Links', CatalogLinks], ['Settings', Settings], ['Index Generator', Index], ['Disable Autoplay', AntiAutoplay], ['Announcement Hiding', PSAHiding], ['Fourchan thingies', Fourchan], ['Tinyboard Glue', Tinyboard], ['Color User IDs', IDColor], ['Highlight by User ID', IDHighlight], ['Count Posts by ID', IDPostCount], ['Custom CSS', CustomCSS], ['Thread Links', ThreadLinks], ['Linkify', Linkify], ['Reveal Spoilers', RemoveSpoilers], ['Resurrect Quotes', Quotify], ['Filter', Filter], ['Thread Hiding Buttons', ThreadHiding], ['Reply Hiding Buttons', PostHiding], ['Recursive', Recursive], ['Strike-through Quotes', QuoteStrikeThrough], ['Quick Reply Personas', QR.persona], ['Quick Reply', QR], ['Cooldown', QR.cooldown], ['Post Jumper', PostJumper], ['Pass Link', PassLink], ['Menu', Menu], ['Index Generator (Menu)', Index.menu], ['Report Link', ReportLink], ['Copy Text Link', CopyTextLink], ['Thread Hiding (Menu)', ThreadHiding.menu], ['Reply Hiding (Menu)', PostHiding.menu], ['Delete Link', DeleteLink], ['Filter (Menu)', Filter.menu], ['Edit Link', QR.oekaki.menu], ['Download Link', DownloadLink], ['Archive Link', ArchiveLink], ['Quote Inlining', QuoteInline], ['Quote Previewing', QuotePreview], ['Quote Backlinks', QuoteBacklink], ['Mark Quotes of You', QuoteYou], ['Mark OP Quotes', QuoteOP], ['Mark Cross-thread Quotes', QuoteCT], ['Anonymize', Anonymize], ['Time Formatting', Time], ['Relative Post Dates', RelativeDates], ['File Info Formatting', FileInfo], ['Fappe Tyme', FappeTyme], ['Gallery', Gallery], ['Gallery (menu)', Gallery.menu], ['Sauce', Sauce], ['Image Expansion', ImageExpand], ['Image Expansion (Menu)', ImageExpand.menu], ['Reveal Spoiler Thumbnails', RevealSpoilers], ['Image Loading', ImageLoader], ['Image Hover', ImageHover], ['Volume Control', Volume], ['WEBM Metadata', Metadata], ['Comment Expansion', ExpandComment], ['Thread Expansion', ExpandThread], ['Favicon', Favicon], ['Unread', Unread], ['Unread Line in Index', UnreadIndex], ['Quote Threading', QuoteThreading], ['Thread Stats', ThreadStats], ['Thread Updater', ThreadUpdater], ['Thread Watcher', ThreadWatcher], ['Thread Watcher (Menu)', ThreadWatcher.menu], ['Mark New IPs', MarkNewIPs], ['Index Navigation', Nav], ['Keybinds', Keybinds], ['Banner', Banner], ['Announcements', PSA], ['Flash Features', Flash], ['Reply Pruning', ReplyPruning], ['Mod Contact Links', ModContact], ], } export default Main $.ready(() => Main.init()) // <% if (readJSON('/.tests_enabled')) { %> // Main.features.push(['Build Test', Test]); // <% } %>