// ==UserScript== // @name HES // @namespace http://github.com/keyten/hes // @description Habrahabr Enhancement Suite // @include https://habr.com/* // @match https://habr.com/* // @exclude %exclude% // @author HabraCommunity // @version 2.6.22 // @grant none // @run-at document-start // ==/UserScript== /* modules.module = { config: {state: string, default state: 'on', 'off', custom} scriptLoaded: function documentLoaded: function, on document load commentsReloaded: function, on comments reloaded button: { text: string states: { on: function, fires when setting is triggered on off: function, fires when setting is triggered off custom } } } */ (function (window) { "use strict" const postsListSelector = '.content-list_posts'; const postsSelector = `${postsListSelector} .post` const helper = { get $postsList() { return $(postsListSelector) }, get $posts() { return $(postsSelector) } } const getName = function (i, el) { const pathParts = el.getAttribute("href").split("/"); return pathParts[pathParts.length - 2] } const version = '2.6.21'; // modules describe const modules = {} modules.hidePosts = { config: { state: 'partially', onClass: 'hes-hide', partiallyClass: 'hes-hide-partially' }, documentLoaded: function () { this.button.states[this.config.state].call(this) }, button: { text: 'Hide posts', states: { on: function () { this.button.states.off.call(this); helper.$postsList.addClass(this.config.onClass) }, off: function () { helper.$postsList.removeClass(`${this.config.onClass} ${this.config.partiallyClass}`) }, partially: function () { this.button.states.off.call(this); helper.$postsList.addClass(this.config.partiallyClass) } } } } modules.hideImgs = { config: { state: 'off', onClass: 'hes-hide-img' }, documentLoaded: function () { this.button.states[this.config.state].call(this) }, button: { text: 'Hide images', states: { on: function () { this.button.states.off.call(this); helper.$postsList.addClass(this.config.onClass) }, off: function () { helper.$postsList.removeClass(this.config.onClass) } } } } modules.hideAuthors = { config: { list: [ 'alizar', 'marks', 'ivansychev', 'ragequit', 'SLY_G' ], hidePostClass: 'hes-hide-post-a' }, documentLoaded: function () { if (!(this.config.list || []).length) return; this.updatePosts(); }, updatePosts: function () { helper.$posts.removeClass(this.config.hidePostClass).filter((i, el) => { const author = $('.user-info__nickname', el).text(); return this.config.list.includes(author); }).addClass(this.config.hidePostClass); }, button: { text: 'Hide authors', states: { on: function () { const list = (this.config.list || []).join(', '); const authors = window.prompt('Через запятую (можно пробелы), регистр важен', list); this.config.list = (authors || '').replace(/\s/g, '').split(','); this.updatePosts(); } } } } modules.hideHubs = { config: { list: [ 'mvideo', 'icover', 'gearbest' ], hidePostClass: 'hes-hide-post-h' }, documentLoaded: function () { if (!(this.config.list || []).length) return; this.updatePosts(); }, updatePosts: function () { helper.$posts.removeClass(this.config.hidePostClass).filter((i, el) => { const hubNames = $('.hub-link, .preview-data__hubs .list__item-link', el).map(getName).get() return this.config.list.some(hub => hubNames.includes(hub)) }).addClass(this.config.hidePostClass); }, button: { text: 'Hide hubs', states: { on: function () { const list = (this.config.list || []).join(', '); const hubs = window.prompt('Через запятую (можно пробелы), регистр важен', list); this.config.list = (hubs || '').replace(/\s/g, '').split(','); this.updatePosts(); } } } } // TODO check native support for old posts like this https://habr.com/ru/post/261803/ modules.mathjax = { config: {state: 'on'}, replaceTeX: function (base) { $(base || '.post__body_full').find('img[src*="//tex.s2cms.ru/"], img[src*="//latex.codecogs.com/"]').filter(':visible') .each(function () { var $this = $(this); // парсим код var decodedURL = decodeURIComponent(this.src); var code = decodedURL.replace(/^https?:\/\/tex\.s2cms\.ru\/(svg|png)\/(\\inline)?/, '') .replace(/^https?:\/\/latex\.codecogs\.com\/gif\.latex\?(\\dpi\{\d+\})?/, '') .replace(/\\(right|left)\s([\[\{\]\}(|)])/g, "\\$1$2") // мерджим ошибочные резделители .replace(/\\(right|left)\s/g, '') // игнорируем пустые разделители // проверяем, использовать $ или $$ code = '$tex' + code + '$'; if ($this.parent().is('div[style="text-align:center;"]') || $this.prev().is('br')) { code = '$' + code + '$'; } // скрываем картинку и выводим TeX $this.hide().after('' + code + '') }); // $.getScript('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js?config=TeX-MML-AM_CHTML&locale=ru') }, scriptLoaded: function () { }, documentLoaded: function () { // подключаем mathjax if (!$('[type="text/x-mathjax-config"]').length) { $("").appendTo('head') } // заменяем картинки на формулы this.replaceTeX() }, commentsReloaded: function () { this.replaceTeX('.comment-item.is_new + .message') }, button: { text: 'MathJax', states: { on: function () { this.documentLoaded() }, off: null } } } modules.nightMode = { config: { state: 'off', id: 'hes_nmstyle', styleUri: 'https://rawgit.com/WaveCutz/habrahabr.ru_night-mode/master/source.css' }, scriptLoaded: function () { let styles; const s = document.createElement('style'); s.id = this.config.id; s.setAttribute('media', 'screen'); const layout = document.querySelector('.layout') if (layout) { document.body.insertBefore(s, layout) } else { (document.body || document.head).appendChild(s) } if (styles = localStorage.getItem(this.config.id)) { s.textContent = styles; } ajax(this.config.styleUri, data => { localStorage.setItem(this.config.id, data); s.textContent = data; }); this.nmInterval = setInterval(function () { const layout = document.querySelector('.layout') if (layout) { document.body.insertBefore(s, layout) } }, 200) setTimeout(this.documentLoaded.bind(this), 2000) }, documentLoaded: function () { if (this.nmInterval) { clearInterval(this.nmInterval); this.nmInterval = null; } }, button: { text: 'Night mode', states: { on: function () { this.scriptLoaded() this.documentLoaded() }, off: function () { $(`style#${this.config.id}`).remove() } } } } modules.invertImages = { config: {state: 'off', scriptUri: 'https://rawgit.com/extempl/Resemble.js/master/resemble.js'}, documentLoaded: function () { if (this.nmInterval) { clearInterval(this.nmInterval); this.nmInterval = null; } const _process = function () { $('.comment__message img[src], .post__text img[src]').each(function () { const $el = $(this); if ($el.is('[src*="latex.codecogs.com"], [src*="tex.s2cms.ru"]')) { return $el.addClass('image-inverted') } if (config.invertImages.state === 'on') { const $wrapper = $('
') const link = $el.wrap($wrapper).attr('src') .replace('habrastorage', 'hsto').replace(/^\/\//, 'https://') $el.parent().css('float', $el.css('float')) $el.after('◐') resemble(link).onComplete(function (data) { if (data.brightness < 10 && data.alpha > 60 || data.brightness < 6 && data.alpha > 30 || data.brightness < 1 || data.brightness > 87 && data.white > 60) { $el.addClass('image-inverted') } }) } }) } if (window['resemble']) { return _process() } $.getScript(this.config.scriptUri, function () { delayedStart(() => (window['resemble']), _process) }) $(document).off('click', '.inverse-toggle') .on('click', '.inverse-toggle', function (e) { e.preventDefault() $(e.target).prev('img').toggleClass('image-inverted') }) }, button: { text: 'Invert images', states: { on: function () { this.documentLoaded() }, off: function () { $('.image-wrapper').each(function (i, imageWrapper) { const $imageWrapper = $(imageWrapper); $imageWrapper.replaceWith($imageWrapper.children('img')) }) const $inverted = $('.image-inverted') if ($inverted.length) { $inverted.removeClass('image-inverted') return } }, } } } modules.liveLinksPreview = { config: {state: 'on'}, loadLink: function (i, link) { const $link = $(link) let url; try { url = new URL($link.attr('href')) } catch (e) { console.error($link) return } url.protocol = 'https'; if (['geektimes.ru', 'habrahabr.ru'].includes(url.host)) { url.host = 'habr.com' } fetch(url).then(xhr => xhr.text()).then(function (str) { const $html = (new window.DOMParser()).parseFromString(str, "text/html"); const $head = $($html.querySelector('head')); const title = $head.find('meta[property="og:title"]').attr('content'); const description = $head.find('meta[property="og:description"]').attr('content'); const image = $head.find('meta[property="og:image"]').first().attr('content'); // const json = JSON.parse($head.querySelector('script[type="application/ld+json"]').textContent); if (title) { $link.attr('data-title', title); $link.attr('data-description', description); $link.attr('data-image', image); $link.text(title); } $link.addClass('preview-processed') }) }, loadLinks: function (base, filter = () => true) { const $base = $(base || '.post__body_full') let $links = $base.find('a'); $links = $links.filter(':not([href^="#"]):not([href=""])') const unnamedLinks = (i, link) => $(link).text().startsWith('http') switch (this.config.state) { case 'on': $links = $links.filter(unnamedLinks) case 'force': $links = $links.filter('[href^="http://habr"], [href^="https://habr"]') break; case 'all': $links = $links.filter(unnamedLinks) case 'force_all': } $links.not('.preview-processed').each(this.loadLink) }, previewLink: function (link) { // TODO popup with preview on hover - add as a separated module }, documentLoaded: function () { this.loadLinks(); this.loadLinks('.comment_message'); }, commentsRealoded: function () { this.loadLinks('.comment_message'); }, button: { text: 'Preview Links', states: { on: function () { // заменять текст на title если текст не установлен (ссылка) this.documentLoaded() }, force: function () {// заменять любой текст на title this.documentLoaded() }, all: function () { // для всех ссылок this.documentLoaded() }, force_all: function () { this.documentLoaded() }, off: null } } } modules.hideUserInfo = { config: {state: 'off'}, documentLoaded: function () { $('*[rel=user-popover]').webuiPopover('destroy'); }, button: { text: 'Hide UserInfo', states: { on: function () { this.documentLoaded() }, off: null } } } modules.timeForReading = { config: {state: 'on', scriptUri: 'https://rawgit.com/michael-lynch/reading-time/master/src/readingtime.js'}, documentLoaded: function () { const $article = $('.post__body_full') const _process = function () { $article.readingTime({lang: 'ru'}) } $article.prepend('