- // ==UserScript==
- // @name Habr.Features
- // @version 3.7.71
- // @description Всякое-разное для Habr aka habr.com
- // @author AngReload
- // @include https://habr.com/*
- // @include http://habr.com/*
- // @namespace habr_comments
- // @run-at document-start
- // @grant GM.xmlHttpRequest
- // @connect m.habr.com
- // @icon https://habr.com/favicon.ico
- // ==/UserScript==
- /* global localStorage, MutationObserver, GM */
-
- // настройки по умолчанию
- const FLAGS = {};
-
- // размеры кнопок
- FLAGS.SMALL_BUTTONS = false;
-
- // Предотвратить закрытие страницы, если статья удалена
- FLAGS.PREVENT_CLOSING_DELETED_PAGES = false;
-
- // остановка гифок
- // клик по гифке заменит картинку на заглушку
- // повторный клик вернет гифку на место
- FLAGS.GIF_STOP = true;
- // остановить гифки при загрузке страницы
- FLAGS.GIF_STOP_ONLOAD = false;
- // цвета заглушки
- FLAGS.GIF_STOP_COLOR_FG = 'White'; // White
- FLAGS.GIF_STOP_COLOR_BG = 'LightGray'; // LightGray or WhiteSmoke
- // менять src вместо создания-удаления нод
- FLAGS.GIF_STOP_OVERTYPE = false;
-
- // показывать счетчики рейтинга в виде:
- // рейтинг = число_плюсов - число_минусов
- FLAGS.RATING_DETAILS = true;
- // клик мышкой по рейтингу меняет вид на простой \ детальный
- FLAGS.RATING_DETAILS_ONCLICK = false;
- // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
- FLAGS.RATING_DETAILS_PN = false;
-
- // карма = число_голосовавших * (процент_товарищей - процент_неприятелей)%
- FLAGS.KARMA_DETAILS = true;
-
- // показывать метки времени в текущем часовом поясе
- // абсолютно, либо относительно текущего времени, либо относительно родительского времени
- // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
- FLAGS.TIME_DETAILS = true;
-
- // добавить возможность сортировки комментариев
- FLAGS.COMMENTS_SORT = true;
- // сортировать комменты при загрузке страницы или оставить сортировку по времени
- FLAGS.COMMENTS_SORT_ONLOAD = true;
- // список доступных сортировок
- FLAGS.COMMENTS_SORT_BY_FRESHNESS = true;
- FLAGS.COMMENTS_SORT_BY_TREND = true; // самая полезная сортировка
- FLAGS.COMMENTS_SORT_BY_QUALITY = false;
- FLAGS.COMMENTS_SORT_BY_RATING = false;
- FLAGS.COMMENTS_SORT_BY_POPULARITY = false;
- FLAGS.COMMENTS_SORT_BY_REDDIT = false;
- FLAGS.COMMENTS_SORT_BY_RANDOM = false;
-
- // добавить возможность сворачивать комментарии
- FLAGS.COMMENTS_HIDE = false;
- // свернуть комментарии если их глубина вложенности равна некому числу
- FLAGS.HIDE_LEVEL = 4;
- // свернуть комменты с 4-го уровня
- FLAGS.HIDE_LEVEL_4 = false;
- // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
- FLAGS.LINE_LEN = 8;
- // отбивать каждый следущий уровень вложенности дополнительным отступом
- FLAGS.REDUCE_NEW_LINES = true;
- // глубина вложенности при «возврате каретки» комментариев обозначается разным цветом
- FLAGS.RAINBOW_NEW_LINE = true;
-
- // заменить ссылки ведущие к новым комментариям на дерево комментариев
- FLAGS.COMMENTS_LINKS = true;
-
- // запоминание галки «Использовать MarkDown» для комментариев
- FLAGS.COMMENTS_MD = true;
-
- // добавить ссылку на хабрасторадж в форму редактирования комментариев
- FLAGS.HTSO_BTN = true;
-
- // добавить ссылку на отслеживаемых в странице профиля
- FLAGS.SUBS_BTN = true;
-
- // включить поиск комментариев в профилях
- FLAGS.FIND_COMMENTS = true;
-
- // линии вдоль прокрутки для отображения размеров поста и комментариев
- FLAGS.SCROLL_LEGEND = true;
-
- // добавляет кнопку активации ночного режима
- FLAGS.NIGHT_MODE = true;
-
- // добавляет диалок настроек
- FLAGS.CONFIG_INTERFACE = true;
-
- // включить разные стили
- FLAGS.USERSTYLE = true;
- // лента постов с увеличенными отступами, нижний бар выровнен вправо
- FLAGS.USERSTYLE_FEED_DISTANCED = true;
- // мелкий фикс для отступа в счетчике ленты
- FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
- // удаляет правую колонку, там где не жалко
- FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
- // аватарки без скруглений
- FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
- // большие аватарки в профиле и карточках
- FLAGS.USERSTYLE_USERINFO_BIG_AVATARS = true;
- // ограничить размер картинок в комментах
- FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
- // ограничить размер картинок в комментах
- FLAGS.USERSTYLE_COMMENTS_OPACITY = false;
- // нормальные стили для комментариев
- FLAGS.USERSTYLE_COMMENTS_FIX = true;
- // нормальные стили для кода
- FLAGS.USERSTYLE_CODE_FIX = true;
- // окантовка границ спойлеров
- FLAGS.USERSTYLE_SPOILER_BORDERS = true;
- // делает глючные плавающие блоки статичными
- FLAGS.USERSTYLE_STATIC_STICKY = true;
- // показывать язык подсветки блоков кода
- FLAGS.USERSTYLE_HLJS_LANG = true;
- // показывать язык кода только при наведении
- FLAGS.USERSTYLE_HLJS_LANG_HOVER = false;
- // свой шрифт для блоков кода
- FLAGS.USERSTYLE_CODE_FONT = ''; // PT Mono
- // размер табов в коде
- FLAGS.USERSTYLE_CODE_TABSIZE = 2;
- // для ночного режима
- FLAGS.USERSTYLE_CODE_NIGHT = true;
- // для настроек
- FLAGS.USERSTYLE_CONFIG_INTERFACE = true;
- FLAGS.SCROLL_BEHAVIOR_SMOOTH = true;
-
- const configOptions = [
- ['KARMA_DETAILS', 'счётчики кармы с плюсами и минусами'],
- ['RATING_DETAILS', 'рейтинги с плюсами и минусами'],
- ['RATING_DETAILS_PN', 'рейтинги в процентах'],
- ['TIME_DETAILS', 'отметки о времени с подробностями'],
- ['PREVENT_CLOSING_DELETED_PAGES', 'предотвратить закрытие страницы, если статья удалена'],
- ['GIF_STOP', 'остановка гифок'],
- ['GIF_STOP_ONLOAD', 'остановить гифки при загрузке страницы'],
- ['COMMENTS_SORT', 'сортировка комментов'],
- ['COMMENTS_SORT_ONLOAD', 'сортировать комменты при загрузке'],
- ['COMMENTS_SORT_BY_FRESHNESS', 'сортировка новые'],
- ['COMMENTS_SORT_BY_TREND', 'сортировка горячие'],
- ['COMMENTS_SORT_BY_QUALITY', 'сортировка хорошие'],
- ['COMMENTS_SORT_BY_REDDIT', 'сортировка проверенные'],
- ['COMMENTS_SORT_BY_RATING', 'сортировка рейтинговые'],
- ['COMMENTS_SORT_BY_POPULARITY', 'сортировка популярные'],
- ['COMMENTS_SORT_BY_RANDOM', 'сортировка случайные'],
- // ['COMMENTS_HIDE', 'сворачивание комментов'],
- // ['HIDE_LEVEL_4', 'свернуть комменты с 4-го уровня'],
- // ['REDUCE_NEW_LINES', 'возврат каретки с уменьшением ширины'],
- // ['RAINBOW_NEW_LINE', 'возврат каретки комментов в разных цветах'],
- // ['COMMENTS_LINKS', '"читать комментарии" ведёт на корень комментов'],
- // ['COMMENTS_MD', 'запоминать галку MarkDown'],
- ['USERSTYLE_COMMENTS_OPACITY', 'заминусованные комменты без прозрачности'],
- // ['FIX_JUMPING_SCROLL', 'заморозить высоту статьи при загрузке'],
- ['SCROLL_LEGEND', 'ленгенда страницы у скроллбара'],
- ['SUBS_BTN', 'табы подписок в профилях'],
- ['FIND_COMMENTS', 'поиск по комментариям в профилях'],
- ['NIGHT_MODE', 'ночной режим'],
- ['SMALL_BUTTONS', 'маленькие кнопки настроек и ночного режима'],
- // ['USERSTYLE', 'стилизация'],
- ['USERSTYLE_COMMENTS_FIX', 'стили комментов'],
- ['USERSTYLE_HLJS_LANG', 'показать язык для блоков кода'],
- // ['USERSTYLE_HLJS_LANG_HOVER', 'язык кода скрыт до наведении курсора'],
- ['USERSTYLE_STATIC_STICKY', 'зафиксировать плавающие блоки'],
- // ['USERSTYLE_SPOILER_BORDERS', 'видимые границы спойлеров'],
- ['USERSTYLE_FEED_DISTANCED', 'большие отступы между постами в ленте'],
- ['USERSTYLE_REMOVE_SIDEBAR_RIGHT', 'удалить правую колонку сайта'],
- // ['USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS', 'квадратные аватарки'],
- ['USERSTYLE_USERINFO_BIG_AVATARS', 'по возможности большие аватарки в профилях'],
- ];
-
- // сохраняем умолчания для панели настроек
- if (!localStorage.getItem('habrafixFlags')) {
- localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
- } else {
- const jsonString = localStorage.getItem('habrafixFlags');
- const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
- const loadedKeys = Object.keys(loadedConfig);
- Object.keys(FLAGS).forEach((key) => {
- if (
- loadedKeys.includes(key) &&
- configOptions.find(arr => arr[0] === key)
- ) {
- FLAGS[key] = loadedConfig[key];
- }
- });
- }
-
- let BUTTON_SIZE = 16;
- let BUTTON_SIZE2 = 25;
- let BUTTON_SIZE4 = 48;
- let KARMA_WIDTH = 84; // ?
-
- if (FLAGS.SMALL_BUTTONS) {
- BUTTON_SIZE = 16;
- BUTTON_SIZE2 = 25;
- BUTTON_SIZE4 = 48;
- KARMA_WIDTH = 84; // ?
- } else {
- BUTTON_SIZE = 32;
- BUTTON_SIZE2 = 25;
- BUTTON_SIZE4 = 88;
- KARMA_WIDTH = 84;
- }
-
- // интерфейс для хранения настроек
- const userConfig = {
- // имя записи в localsorage
- key: 'habrafix',
- // модель настроек: ключ - возможные значения
- model: {
- time_publications: ['fromNow', 'absolute'],
- time_comments: ['fromParent', 'fromNow', 'absolute'],
- comments_order: ['trend', 'time'],
- scores_details: [true, false],
- comment_markdown: [false, true],
- night_mode: [false, true],
- },
- config: {},
- // при старте для конфига берем сохраненные параметры либо по умолчанию
- init() {
- let jsonString = localStorage.getItem(userConfig.key);
- const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
- const loadedKeys = Object.keys(loadedConfig);
- const config = {};
- Object.keys(userConfig.model).forEach((key) => {
- const exist = loadedKeys.indexOf(key) >= 0;
- config[key] = exist ? loadedConfig[key] : userConfig.model[key][0];
- });
- jsonString = JSON.stringify(config);
- localStorage.setItem(userConfig.key, jsonString);
- userConfig.config = config;
- },
- getItem(key) {
- const jsonString = localStorage.getItem(userConfig.key);
- const config = JSON.parse(jsonString);
- return config[key];
- // return userConfig.config[key];
- },
- setItem(key, value) {
- let jsonString = localStorage.getItem(userConfig.key);
- const config = JSON.parse(jsonString);
- config[key] = value;
- jsonString = JSON.stringify(config);
- localStorage.setItem(userConfig.key, jsonString);
- userConfig.config = config;
- },
- // каруселит параметр по значения модели
- shiftItem(key) {
- const currentValue = userConfig.getItem(key);
- const availableValues = userConfig.model[key];
- const currentIdx = availableValues.indexOf(currentValue);
- const nextIdx = (currentIdx + 1) % availableValues.length;
- const nextValue = availableValues[nextIdx];
- userConfig.setItem(key, nextValue);
- return nextValue;
- },
- };
- userConfig.init();
-
- // свои стили
- const userStyleEl = document.createElement('style');
- let userStyle = '';
-
- if (FLAGS.SCROLL_LEGEND) {
- userStyle += `
- .legend_el {
- position: fixed;
- width: 4px;
- right: 0;
- transition: top 1s ease-out, height 1s ease-out;
- z-index: 10000;
- }
-
- #xpanel {
- right: 4px;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_FEED_DISTANCED) {
- userStyle += `
- .post__body_crop {
- text-align: right;
- }
-
- .post__body_crop .post__text {
- text-align: left;
- }
-
- .post__footer {
- text-align: right;
- }
-
- .posts_list .content-list__item_post {
- padding: 40px 0;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE) {
- userStyle += `
- .toggle-menu__item-counter_new {
- margin-left: 4px;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COMMENTS_OPACITY) {
- userStyle += `
- .comment__message_downgrade {
- opacity: 1;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT || FLAGS.PREVENT_CLOSING_DELETED_PAGES) {
- // remove for
- // https://habr.com/post/352896/
- // https://habr.com/sandbox/
- // https://habr.com/sandbox/115216/
- // https://habr.com/users/saggid/posts/
- // https://habr.com/users/saggid/comments/
- // https://habr.com/users/saggid/favorites/
- // https://habr.com/users/saggid/favorites/posts/
- // https://habr.com/users/saggid/favorites/comments/
- // https://habr.com/company/pvs-studio/blog/353640/
- // https://habr.com/company/pvs-studio/blog/
- // https://habr.com/company/pvs-studio/blog/top/
- // https://habr.com/company/pvs-studio/
- // https://habr.com/feed/
- // https://habr.com/top/
- // https://habr.com/top/yearly/
- // https://habr.com/all/
- // https://habr.com/all/top10/
-
- // display for
- // https://habr.com/company/pvs-studio/profile/
- // https://habr.com/company/pvs-studio/vacancies/
- // https://habr.com/company/pvs-studio/fans/all/rating/
- // https://habr.com/company/pvs-studio/workers/new/rating/
- // https://habr.com/feed/settings/
- // https://habr.com/users/
- // https://habr.com/hubs/
- // https://habr.com/hubs/admin/
- // https://habr.com/companies/
- // https://habr.com/companies/category/software/
- // https://habr.com/companies/new/
- // https://habr.com/flows/design/
-
- const path = window.location.pathname;
- const isPost = /^\/(ru|en)\/post\/\d+\/$/.test(path);
- const isSandbox = /^\/(ru|en)\/sandbox\//.test(path);
- const isUserPosts = /^\/(ru|en)\/users\/[^/]+\/posts\//.test(path);
- const isUserComments = /^\/(ru|en)\/users\/[^/]+\/comments\//.test(path);
- const isUserFavorites = /^\/(ru|en)\/users\/[^/]+\/favorites\//.test(path);
- // const isUserSubscription = /^\/(ru|en)\/users\/[^/]+\/subscription\//.test(path);
- const isCompanyBlog = /^\/(ru|en)\/company\/[^/]+\/blog\//.test(path);
- const isCompanyBlog2 = /^\/(ru|en)\/company\/[^/]+\/(page\d+\/)?$/.test(path);
- const isCompanyBlogPost = /^\/(ru|en)\/company\/[^/]+\/blog\/[^/]+/.test(path);
- const isFeed = /^\/(ru|en)\/feed\//.test(path);
- const isHome = /^\/(ru|en)\/$/.test(path);
- const isTop = /^\/(ru|en)\/top\//.test(path);
- const isAll = /^\/(ru|en)\/all\//.test(path);
- const isNews = /^\/(ru|en)\/news\//.test(path);
- const isNewsT = /^\/(ru|en)\/news\/t\/\d+\/$/.test(path);
-
- if (FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT && (
- isPost || isSandbox ||
- isUserPosts || isUserComments || isUserFavorites ||
- isCompanyBlog || isCompanyBlog2 ||
- isFeed || isHome || isTop || isAll ||
- isNews || isNewsT
- )) {
- userStyle += `
- .sidebar_right,
- .sidebar {
- display: none;
- }
-
- .content_left {
- padding-right: 0;
- }
-
- .comment_plain {
- max-width: 860px;
- }
- `;
- }
-
- if (FLAGS.PREVENT_CLOSING_DELETED_PAGES && (
- isPost || isSandbox ||
- isCompanyBlogPost ||
- isNews || isNewsT
- )) {
- window.onbeforeunload = (e) => {
- const xhr = new XMLHttpRequest();
- xhr.open('HEAD', '', false);
- xhr.send();
- if (xhr.status < 200 || xhr.status >= 400) {
- e.preventDefault();
- const message = 'Статья уже удалена или недоступна';
- e.returnValue = message;
- return message;
- }
- return undefined;
- };
- }
- }
-
- if (FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
- userStyle += `
- .user-info__image-pic,
- .user-pic_popover,
- .media-obj__image-pic,
- .company-info__image-pic {
- border-radius: 0;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_USERINFO_BIG_AVATARS) {
- userStyle += `
- .page-header {
- height: auto;
- }
-
- .media-obj__image-pic_hub,
- .user-info__stats .media-obj__image-pic_user,
- /* .media-obj__image-pic_company, */
- .company-info__image-pic {
- width: auto;
- height: auto;
- }
-
- .page-header_tall .company-info__image-pic {
- width: 48px;
- height: 48px;
- }
- `;
- }
-
- if (FLAGS.COMMENTS_SORT) {
- userStyle += `
- .comments_order {
- color: #333;
- font-size: 14px;
- font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif;
- text-rendering: optimizeLegibility;
- border-bottom: 1px solid #e3e3e3;
- padding: 8px;
- text-align: right;
- }
-
- .comments_order a {
- color: #548eaa;
- font-style: normal;
- text-decoration: none;
- }
-
- .comments_order a:hover {
- color: #487284;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COMMENTS_FIX) {
- userStyle += `
- .content-list_comments {
- overflow: visible;
- }
-
- .comment__folding-dotholder {
- display: none !important;
- }
-
- .content-list_nested-comments {
- border-left: 1px solid #e3e3e3;
- margin: 0;
- /*padding-top: 20px;*/
- padding-left: 20px !important;
- }
-
- .content-list_comments {
- /*border-left: 1px solid silver;*/
- margin: 0;
- /*padding-left: 0;*/
- padding-top: 20px;
- /*background: #FCE4EC;*/
- }
-
- #comments-list .js-form_placeholder:not(:empty) {
- border-left: 1px solid #e3e3e3;
- padding-left: 20px;
- }
-
- .comments_new-line {
- border-left: 1px solid #777;
- border-bottom: 1px solid #777;
- border-top: 1px solid #777;
- margin-left: -${FLAGS.LINE_LEN * 21}px !important;
- background: white;
- padding-bottom: 4px;
- }
-
- /* .comment__head_topic-author.comment__head_new-comment */
- .comment__head_topic-author .user-info {
- text-decoration: underline;
- }
-
- /* фикс, когда не добавляется класс &_plus или minus при user_vote_action */
- .voting-wjt__button[title="Вы проголосовали положительно"] {
- color: #7ba600;
- }
- .voting-wjt__button[title="Вы проголосовали отрицательно"] {
- color: #d53c30;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COMMENTS_FIX && FLAGS.RAINBOW_NEW_LINE) {
- userStyle += `
- .comments_new-line-1 {
- border-color: #0caefb;
- }
-
- .comments_new-line-2 {
- border-color: #06feb7;
- }
-
- .comments_new-line-3 {
- border-color: #fbcb02;
- }
-
- .comments_new-line-0 {
- border-color: #fb0543;
- }
-
- .js-comment_parent:not(:hover) {
- color: #cd66cd !important;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COMMENTS_FIX && FLAGS.REDUCE_NEW_LINES) {
- userStyle += `
- .comments_new-line .comments_new-line {
- margin-left: -${(FLAGS.LINE_LEN - 1) * 21}px !important;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE) {
- userStyle += `
- .comment__message img {
- max-height: ${FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
- }
-
- .comment__message .spoiler .img {
- max-height: auto;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_CODE_FIX) {
- let addFont = '';
- if (FLAGS.USERSTYLE_CODE_FONT) {
- addFont = FLAGS.USERSTYLE_CODE_FONT;
- if (addFont.indexOf(' ') >= 0) {
- addFont = `"${addFont}"`;
- }
- addFont += ',';
- }
-
- const tabSize = FLAGS.USERSTYLE_CODE_TABSIZE || 4;
-
- userStyle += `
- .editor .text-holder textarea,
- .tm-editor__textarea,
- pre {
- font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace;
- }
-
- code {
- font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;;
- -o-tab-size: ${tabSize};
- -moz-tab-size: ${tabSize};
- tab-size: ${tabSize};
- background: #f7f7f7;
- border-radius: 3px;
- color: #505c66;
- display: inline-block;
- font-weight: 500;
- line-height: 1.29;
- padding: 5px 9px;
- vertical-align: 1px;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_SPOILER_BORDERS) {
- userStyle += `
- .spoiler .spoiler_text {
- border: 1px dashed rgb(12, 174, 251);
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_STATIC_STICKY) {
- userStyle += `
- .wrapper-sticky,
- .js-ad_sticky,
- .js-ad_sticky_comments {
- position: static !important;
- }
- .sticky-spacer {
- display: none !important;
- }
- `;
- }
-
- if (FLAGS.GIF_STOP) {
- userStyle += `
- .habrafix_gif-stop:hover {
- outline: 4px solid #548eaa;
- outline-offset: -4px;
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_HLJS_LANG) {
- let hover = '';
- if (FLAGS.USERSTYLE_HLJS_LANG_HOVER) hover = ':hover';
- userStyle += `
- pre {
- position: relative;
- }
-
- .hljs${hover}::after {
- position: absolute;
- font-size: 12px;
- content: 'code';
- right: 0;
- top: 0;
- padding: 1px 5px 0 4px;
- /*border-bottom: 1px solid #e5e8ec;
- border-left: 1px solid #e5e8ec;
- border-bottom-left-radius: 3px;
- color: #505c66;*/
- opacity: .5;
- }
- `;
- userStyle += [
- ['1c', '1C:Enterprise (v7, v8)'],
- ['abnf', 'Augmented Backus-Naur Form'],
- ['accesslog', 'Access log'],
- ['actionscript', 'ActionScript'],
- ['ada', 'Ada'],
- ['apache', 'Apache'],
- ['applescript', 'AppleScript'],
- ['arduino', 'Arduino'],
- ['armasm', 'ARM Assembly'],
- ['asciidoc', 'AsciiDoc'],
- ['aspectj', 'AspectJ'],
- ['autohotkey', 'AutoHotkey'],
- ['autoit', 'AutoIt'],
- ['avrasm', 'AVR Assembler'],
- ['awk', 'Awk'],
- ['axapta', 'Axapta'],
- ['bash', 'Bash'],
- ['basic', 'Basic'],
- ['bnf', 'Backus–Naur Form'],
- ['brainfuck', 'Brainfuck'],
- ['cal', 'C/AL'],
- ['capnproto', 'Cap’n Proto'],
- ['ceylon', 'Ceylon'],
- ['clean', 'Clean'],
- ['clojure-repl', 'Clojure REPL'],
- ['clojure', 'Clojure'],
- ['cmake', 'CMake'],
- ['coffeescript', 'CoffeeScript'],
- ['coq', 'Coq'],
- ['cos', 'Caché Object Script'],
- ['cpp', 'C++'],
- ['crmsh', 'crmsh'],
- ['crystal', 'Crystal'],
- ['cs', 'C#'],
- ['csp', 'CSP'],
- ['css', 'CSS'],
- ['d', 'D'],
- ['dart', 'Dart'],
- ['delphi', 'Delphi'],
- ['diff', 'Diff'],
- ['django', 'Django'],
- ['dns', 'DNS Zone file'],
- ['dockerfile', 'Dockerfile'],
- ['dos', 'DOS .bat'],
- ['dsconfig', 'dsconfig'],
- ['dts', 'Device Tree'],
- ['dust', 'Dust'],
- ['ebnf', 'Extended Backus-Naur Form'],
- ['elixir', 'Elixir'],
- ['elm', 'Elm'],
- ['erb', 'ERB (Embedded Ruby)'],
- ['erlang-repl', 'Erlang REPL'],
- ['erlang', 'Erlang'],
- ['excel', 'Excel'],
- ['fix', 'FIX'],
- ['flix', 'Flix'],
- ['fortran', 'Fortran'],
- ['fsharp', 'F#'],
- ['gams', 'GAMS'],
- ['gauss', 'GAUSS'],
- ['gcode', 'G-code (ISO 6983)'],
- ['gherkin', 'Gherkin'],
- ['glsl', 'GLSL'],
- ['go', 'Go'],
- ['golo', 'Golo'],
- ['gradle', 'Gradle'],
- ['groovy', 'Groovy'],
- ['haml', 'Haml'],
- ['handlebars', 'Handlebars'],
- ['haskell', 'Haskell'],
- ['haxe', 'Haxe'],
- ['hsp', 'HSP'],
- ['htmlbars', 'HTMLBars'],
- ['http', 'HTTP'],
- ['hy', 'Hy'],
- ['inform7', 'Inform 7'],
- ['ini', 'Ini'],
- ['irpf90', 'IRPF90'],
- ['java', 'Java'],
- ['javascript', 'JavaScript'],
- ['jboss-cli', 'jboss-cli'],
- ['json', 'JSON'],
- ['julia-repl', 'Julia REPL'],
- ['julia', 'Julia'],
- ['kotlin', 'Kotlin'],
- ['lasso', 'Lasso'],
- ['ldif', 'LDIF'],
- ['leaf', 'Leaf'],
- ['less', 'Less'],
- ['lisp', 'Lisp'],
- ['livecodeserver', 'LiveCode'],
- ['livescript', 'LiveScript'],
- ['llvm', 'LLVM IR'],
- ['lsl', 'Linden Scripting Language'],
- ['lua', 'Lua'],
- ['makefile', 'Makefile'],
- ['markdown', 'Markdown'],
- ['mathematica', 'Mathematica'],
- ['matlab', 'Matlab'],
- ['maxima', 'Maxima'],
- ['mel', 'MEL'],
- ['mercury', 'Mercury'],
- ['mipsasm', 'MIPS Assembly'],
- ['mizar', 'Mizar'],
- ['mojolicious', 'Mojolicious'],
- ['monkey', 'Monkey'],
- ['moonscript', 'MoonScript'],
- ['n1ql', 'N1QL'],
- ['nginx', 'Nginx'],
- ['nimrod', 'Nimrod'],
- ['nix', 'Nix'],
- ['nsis', 'NSIS'],
- ['objectivec', 'Objective-C'],
- ['ocaml', 'OCaml'],
- ['openscad', 'OpenSCAD'],
- ['oxygene', 'Oxygene'],
- ['parser3', 'Parser3'],
- ['perl', 'Perl'],
- ['pf', 'pf'],
- ['pgsql', 'PostgreSQL'],
- ['plaintext', 'просто текст'], // на будущее
- ['php', 'PHP'],
- ['pony', 'Pony'],
- ['powershell', 'PowerShell'],
- ['processing', 'Processing'],
- ['profile', 'Python profile'],
- ['prolog', 'Prolog'],
- ['protobuf', 'Protocol Buffers'],
- ['puppet', 'Puppet'],
- ['purebasic', 'PureBASIC'],
- ['python', 'Python'],
- ['q', 'Q'],
- ['qml', 'QML'],
- ['r', 'R'],
- ['rib', 'RenderMan RIB'],
- ['roboconf', 'Roboconf'],
- ['routeros', 'Microtik RouterOS script'],
- ['rsl', 'RenderMan RSL'],
- ['ruby', 'Ruby'],
- ['ruleslanguage', 'Oracle Rules Language'],
- ['rust', 'Rust'],
- ['scala', 'Scala'],
- ['scheme', 'Scheme'],
- ['scilab', 'Scilab'],
- ['scss', 'SCSS'],
- ['shell', 'Shell Session'],
- ['smali', 'Smali'],
- ['smalltalk', 'Smalltalk'],
- ['sml', 'SML'],
- ['sqf', 'SQF'],
- ['sql', 'SQL'],
- ['stan', 'Stan'],
- ['stata', 'Stata'],
- ['step21', 'STEP Part 21'],
- ['stylus', 'Stylus'],
- ['subunit', 'SubUnit'],
- ['swift', 'Swift'],
- ['taggerscript', 'Tagger Script'],
- ['tap', 'Test Anything Protocol'],
- ['tcl', 'Tcl'],
- ['tex', 'TeX'],
- ['thrift', 'Thrift'],
- ['tp', 'TP'],
- ['twig', 'Twig'],
- ['typescript', 'TypeScript'],
- ['vala', 'Vala'],
- ['vbnet', 'VB.NET'],
- ['vbscript-html', 'VBScript in HTML'],
- ['vbscript', 'VBScript'],
- ['verilog', 'Verilog'],
- ['vhdl', 'VHDL'],
- ['vim', 'Vim Script'],
- ['x86asm', 'Intel x86 Assembly'],
- ['xl', 'XL'],
- ['xml', 'HTML, XML'],
- ['xquery', 'XQuery'],
- ['yaml', 'YAML'],
- ['zephir', 'Zephir'],
- ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join('');
- }
-
- if (FLAGS.USERSTYLE_CODE_NIGHT) {
- userStyle += `
- .night_mode_switcher {
- box-sizing: border-box;
- position: fixed;
- width: 32px;
- height: 32px;
- right: 32px;
- bottom: 32px;
- width: ${BUTTON_SIZE}px;
- height: ${BUTTON_SIZE}px;
- right: ${BUTTON_SIZE}px;
- bottom: ${BUTTON_SIZE}px;
- z-index: 10000;
- background-color: transparent;
- border-radius: 50%;
- border: 4px solid #aaa;
- border-right-width: ${BUTTON_SIZE / 2}px;
- transition: border-color 0.1s ease-out;
- }
-
- .night_mode_switcher:hover {
- border-color: #333;
- }
-
- .night .night_mode_switcher {
- border-color: #515151;
- }
-
- .night .night_mode_switcher:hover {
- border-color: #9e9e9e;
- }
-
- .night ::-webkit-scrollbar,
- .night ::-webkit-scrollbar-corner,
- .night ::-webkit-scrollbar-track-piece {
- background-color: #000;
- }
-
- .night ::-webkit-scrollbar-thumb {
- background-color: #22272b;
- border: 1px solid #000;
- }
-
- .night ::-webkit-scrollbar-thumb:hover {
- background-color: #2C3237;
- }
-
- .night {
- scrollbar-color: dark;
- scrollbar-face-color: #22272b;
- scrollbar-track-color: #000;
- scrollbar-color: #22272b #000;
- }
-
- /* bg */
- .night .sidebar-block__suggest,
- .night .dropdown-container,
- .night .poll-result__bar,
- .night .comments_new-line,
- .night .tm-editor__textarea,
- .night .layout,
- .night .toggle-menu__most-read,
- .night .toggle-menu_most-comments,
- .night .partner-info {
- background: #171c20;
- }
-
- /* text */
- .night .companies-rating__name:not(:hover),
- .night .profile-section__title,
- .night .profile-section__about-text,
- .night .profile-section__invited,
- .night .sidebar-block__suggest,
- .night .user-message__body,
- .night .promo-block__title_total,
- .night .beta-anounce__text,
- .night .defination-list__label,
- .night .defination-list__value,
- .night .search-field__select,
- .night .search-field__input[type="text"],
- .night .search-form__field,
- .night .post-info__title:not(:hover),
- .night .dropdown__user-stats,
- .night .dropdown-container_white .user-info__special,
- .night .n-dropdown-menu__item-link,
- .night body,
- .night .default-block__polling-title,
- .night .poll-result__data-label,
- .night code,
- .night .user-info__fullname,
- .night .user-info__specialization,
- .night .page-header__info-title,
- .night .page-header__info-desc,
- .night .post__title-text,
- .night .post__title_link:not(:visited),
- .night .checkbox__label,
- .night .radio__label,
- .night .tm-editor__textarea,
- .night .footer-block__title,
- .night #TMpanel .container .bmenu > a.current,
- .night .post__text-html,
- .night .comment__message,
- .night .comment-form__preview,
- .night .post-share__title,
- .night .post-donate__title,
- .night .news-block__title,
- .night .news-topic__title:not(:visited),
- .night .partner-info__title,
- .night .partner-info__description {
- color: #9e9e9e;
- }
-
- /* non important text */
- .night .icon-svg_bookmark,
- .night .icon-svg_views-count,
- .night .icon-svg_post-comments,
- .night .icon-svg_edit,
- .night .icon-svg_recommend,
- .night .icon-svg_report,
- .night .voting-wjt__button:not(.voting-wjt__button_plus):not(.voting-wjt__button_minus),
- .night .icon_comment-edit,
- .night .icon_comment-anchor,
- .night .icon_comment-bookmark,
- .night .icon_comment-branch,
- .night .icon_comment-arrow-up,
- .night .icon_comment-arrow-down,
- .night .layout__elevator {
- color: #515151;
- }
-
- .night .voting-wjt__button:not(.voting-wjt__button_plus):not(.voting-wjt__button_minus):hover,
- .night .icon_comment-anchor:hover,
- .night .icon_comment-bookmark:hover,
- .night .icon_comment-branch:hover,
- .night .icon_comment-arrow-up:hover,
- .night .icon_comment-arrow-down:hover {
- color: #548eaa;
- }
-
- .night .n-dropdown-menu__item-link:hover {
- color: white;
- }
-
- /* top lvl bg */
- .night .h-popover,
- .night .profile-section__user-hub:not(.profile-section__user-hub_cross),
- .night a.sort-panel__item-toggler.active,
- .night .checkbox__label::before,
- .night .radio__label::before,
- .night .content-list__item_conversation:hover,
- .night .search-field__select,
- .night .search-field__input[type="text"],
- .night .search-form__field,
- .night .dropdown-container,
- .night .n-dropdown-menu,
- .night .post__translatation,
- .night code,
- .night .megapost-teasers,
- .night .tm-editor_comments,
- .night .promo-block__header,
- .night .post__text-html blockquote,
- .night .default-block,
- .night .post-share,
- .night .post-donate,
- .night .company-info__author,
- .night .layout__row_footer-links {
- background: #22272B;
- }
-
- /* not important bg */
- .night .profile-section__user-hub:not(.profile-section__user-hub_cross):hover,
- .night .btn_blue.disabled,
- .night .btn_blue[disabled],
- .night .tracker_page table.tracker_folowers tr.new,
- .night .dropdown__user-stats,
- .night .comment__head_topic-author,
- .night .promo-item:hover,
- .night .layout__row_navbar,
- .night .layout__row_footer,
- .night #TMpanel,
- .night .n-dropdown-menu__item-link_flow:hover,
- .night #tracker-page .tracker-table__row.new {
- background: #1f2327;
- }
-
- /* borders */
- .night #tracker-page .tracker-table__header,
- .night #tracker-page .tracker-table__cell,
- .night .h-popover,
- .night .h-popover__stats,
- .night .default-block__footer,
- .night .toggle-menu__item-link_bordered,
- .night .default-block_promote,
- .night .sort-panel,
- .night .n-dropdown-menu_flows,
- .night .for_users_only_msg,
- .night #comments-list .js-form_placeholder,
- .night .sidebar-block__suggest,
- .night .content-list_preview-message,
- .night .btn_outline_blue[disabled],
- .night .user-message__body_html pre code,
- .night .content-list_user-dialog,
- .night .wysiwyg-toolbar,
- .night .content-list__item_bordered,
- .night .promo-block__total,
- .night .search-field__select,
- .night .search-field__input[type="text"],
- .night .search-form__field,
- .night .tracker_page table.tracker_folowers tr td,
- .night .tracker_page table.tracker_folowers tr th,
- .night .stacked-menu__item_devided,
- .night .post__text-html table,
- .night .post__text-html table td,
- .night .post__text-html table th,
- .night .n-dropdown-menu__item_border,
- .night .dropdown-container,
- .night .default-block_bordered,
- .night .default_block_polling,
- .night .column-wrapper_tabs .sidebar_right,
- .night .post__type-label,
- .night .promo-block__header,
- .night .user-info__contacts,
- .night .comment__message pre code,
- .night .comment-form__preview pre code,
- .night .sandbox-panel,
- .night .comment__post-title,
- .night .tm-editor__textarea,
- .night .promo-block__footer,
- .night .author-panel,
- .night .promo-block,
- .night .post__text-html pre code,
- .night .footer-block__title,
- .night #TMpanel,
- .night .layout__row_navbar,
- .night .page-header_bordered,
- .night .post-stats,
- .night .company-info__about,
- .night .company-info_post-additional,
- .night .company-info__contacts,
- .night .post-share,
- .night .post-donate,
- .night .content-list__item_devided,
- .night .comments_order,
- .night .comments-section__head,
- .night .content-list_nested-comments,
- .night .default-block__header,
- .night .column-wrapper_bordered,
- .night .tabs-menu,
- .night .toggle-menu,
- .night .news-block__header,
- .night .news-block__footer {
- border-color: #393d41;
- }
-
- .night .rating-info__progress,
- .night .poll-result__progress {
- background-color: #515151;
- }
-
- .night .poll-result__progress_winner {
- background-color: #5e8eac;
- }
-
- .night .layout__elevator:hover {
- background-color: #22272B;
- }
-
- .night .comment__head_topic-author {
- background: #003030;
- }
-
- .night .comment__head_my-comment {
- background: #003000;
- }
-
- .night .comment__head_new-comment {
- background: black
- }
-
- .night .user-info__nickname_comment,
- .night .icon-svg_logo-habrahabr {
- color: inherit;
- }
-
- .night [disabled] {
- opacity: 0.5
- }
-
- .night .content-list_comments .comment__folding-dotholder::before,
- .night .comment.is_selected::after {
- filter: invert(0.9);
- }
-
- /* img filter */
- .night .comment__message img,
- .night .comment-form__preview img,
- .night .default-block__content #facebook_like_box,
- .night .default-block__content #vk_groups,
- .night .post img,
- .night .page-header__banner img,
- .night .company_top_banner img,
- .night img .teaser__image,
- .night .teaser__image-pic,
- .night .article__body img {
- filter: brightness(0.5);
- transition: filter .6s ease-out;
- }
-
- .night .comment__message img:hover,
- .night .comment-form__preview img:hover,
- .night .default-block__content #facebook_like_box:hover,
- .night .default-block__content #vk_groups:hover,
- .night img[alt="en"],
- .night img[alt="habr"],
- .night img:hover,
- .night a.post-author__link img,
- .night img.user-info__image-pic,
- .night .teaser__image-pic:hover,
- .night .teaser__image:hover {
- filter: none;
- }
-
- /* Atelier Cave Dark */
- .night .hljs-comment,
- .night .hljs-quote {
- color:#7e7887 !important
- }
- .night .hljs-variable,
- .night .hljs-template-variable,
- .night .hljs-attribute,
- .night .hljs-regexp,
- .night .hljs-link,
- .night .hljs-tag,
- .night .hljs-name,
- .night .hljs-selector-id,
- .night .hljs-selector-class {
- color:#be4678 !important
- }
- .night .hljs-number,
- .night .hljs-meta,
- .night .hljs-built_in,
- .night .hljs-builtin-name,
- .night .hljs-literal,
- .night .hljs-type,
- .night .hljs-params {
- color:#aa573c !important
- }
- .night .hljs-string,
- .night .hljs-symbol,
- .night .hljs-bullet {
- color:#2a9292 !important
- }
- .night .hljs-title,
- .night .hljs-section {
- color:#576ddb !important
- }
- .night .hljs-keyword,
- .night .hljs-selector-tag {
- color:#955ae7 !important
- }
- .night .hljs-deletion,
- .night .hljs-addition {
- color:#19171c !important;
- display:inline-block !important;
- width:100% !important
- }
- .night .hljs-deletion {
- background-color:#be4678 !important
- }
- .night .hljs-addition {
- background-color:#2a9292 !important
- }
- .night .hljs {
- display:block !important;
- overflow-x:auto !important;
- background:#19171c !important;
- color:#8b8792 !important;
- /*padding:0.5em !important*/
- }
- .night .hljs-emphasis {
- font-style:italic !important
- }
- .night .hljs-strong {
- font-weight:bold !important
- }
- `;
- }
-
- if (FLAGS.USERSTYLE_CONFIG_INTERFACE) {
- userStyle += `
- .config_button {
- box-sizing: border-box;
- position: fixed;
- width: ${BUTTON_SIZE}px;
- height: ${BUTTON_SIZE2}px;
- right: ${BUTTON_SIZE}px;
- bottom: ${FLAGS.NIGHT_MODE ? BUTTON_SIZE4 : BUTTON_SIZE}px;
- z-index: 10000;
- background: -webkit-linear-gradient(top, #aaa 50%, transparent 50%);
- background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
- background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
- background-size: 10px 10px;
- transition: background 0.1s ease-out;
- }
-
- .config_button:hover {
- background: -webkit-linear-gradient(top, #333 50%, transparent 50%);
- background: -moz-linear-gradient(top, #333 50%, transparent 50%);
- background: -moz-linear-gradient(top, #333 50%, transparent 50%);
- background-size: 10px 10px;
- }
-
- .night .config_button {
- background: -webkit-linear-gradient(top, #515151 50%, transparent 50%);
- background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
- background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
- background-size: 10px 10px;
- }
-
- .night .config_button:hover {
- background: -webkit-linear-gradient(top, #9e9e9e 50%, transparent 50%);
- background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
- background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
- background-size: 10px 10px;
- }
-
- .config_frame {
- box-sizing: border-box;
- position: fixed;
- right: 80px;
- bottom: 32px;
- z-index: 10000;
- border: 1px solid #aaa;
- padding: 8px;
- background: #f7f7f7;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- overflow-y: auto;
- max-height: calc(100vh - 64px);
- min-width: 390px;
- }
- .config_frame label:hover {
- cursor: pointer;
- background: rgba(128, 128, 128, 0.3);
- }
- .config_frame input {
- cursor: pointer;
- position: absolute;
- opacity: 0;
- }
- .config_frame input + span:before {
- content: '';
- display: inline-block;
- width: 0.5em;
- height: 0.5em;
- margin: 0 0.4em 0.1em 0.3em;
- outline: 1px solid currentcolor;
- outline-offset: 1px;
- }
- .config_frame input:checked + span:before {
- background: currentcolor;
- }
- .night .config_frame {
- background: #22272B;
- border-color: #393d41;
- }
- `;
- }
-
- userStyleEl.innerHTML = userStyle;
-
- const navigatorEdge = /Edge/.test(navigator.userAgent);
-
- function readyHead(fn) {
- if (document.body) { // если есть body, значит head готов
- fn();
- } else if (document.documentElement && !navigatorEdge) {
- const observer = new MutationObserver(() => {
- if (document.body) {
- observer.disconnect();
- fn();
- }
- });
- observer.observe(document.documentElement, { childList: true });
- } else {
- // рекурсивное ожидание появления DOM
- setTimeout(() => readyHead(fn), 16);
- }
- }
-
- readyHead(() => {
- if (document.getElementById('habrafixmarker')) return;
- if (FLAGS.USERSTYLE) document.head.appendChild(userStyleEl);
- if (FLAGS.NIGHT_MODE && userConfig.getItem('night_mode')) {
- document.documentElement.classList.add('night');
- }
- });
-
- function ready(fn) {
- const { readyState } = document;
- if (readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => {
- fn();
- });
- } else {
- fn();
- }
- }
-
- ready(() => {
- if (document.getElementById('habrafixmarker')) return;
- if (FLAGS.COMMENTS_MD) {
- const mdSelectorEl = document.getElementById('comment_markdown');
- if (mdSelectorEl) {
- if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true;
- mdSelectorEl.addEventListener('input', () => {
- userConfig.setItem('comment_markdown', mdSelectorEl.checked);
- });
- }
- }
-
- if (FLAGS.HTSO_BTN) {
- const commentForm = document.getElementById('comment-form');
- if (commentForm) {
- const toolbar = commentForm.querySelector('.tm-editor__toolbar');
- if (toolbar) {
- const item = document.createElement('li');
- item.classList.add('wysiwyg-toolbar__item');
- item.innerHTML = `
- <button type="button" class="btn btn_wysiwyg" tabindex="0" title="Загрузка картинок"
- onclick="window.open('//hsto.org', '_blank').focus();">
- <svg class="icon-svg icon-svg_spoiler_wysiwyg" aria-hidden="true" aria-labelledby="title"
- version="1.1" role="img" width="24" viewBox="0 0 100 82.9">
- <path
- d="M77.9,82.9H5.6c-3.4,0-5.6-2.4-5.6-5.2V21.9c0-3.5,2.1-6.1,
- 5.6-6.1H50v11.1H11v45h61V54.8l12,0v22.9 C84,80.5,81.2,82.9,77.9,82.9L77.9,82.9z">
- </path>
- <polygon points="16.5,66.9 39.8,44.6 50.2,54.4 61.5,39.6 67,50.2 67,66.9 "></polygon>
- <path
- d="M28,44.4c-3.2,0-5.7-2.6-5.7-5.7c0-3.2,2.6-5.8,5.7-5.8c3.2,0,5.8,2.6,5.8,
- 5.8C33.8,41.9,31.2,44.4,28,44.4 L28,44.4z">
- </path>
- <polygon points="84,21.9 84,44 72,44 72,21.9 56.1,21.9 78.1,0 100,21.9 "></polygon>
- </svg>
- </button>
- `;
- toolbar.appendChild(item);
- }
- }
- }
-
- if (FLAGS.SUBS_BTN) {
- const userBtn = document.querySelector('.tabs-menu__item_link');
- const isUserPage = /^\/(ru|en)\/users\/[^/]+\//.test(window.location.pathname);
- if (userBtn && isUserPage) {
- const bar = userBtn.parentElement;
-
- const tab = document.createElement('a');
- const isSubs = /subscription\/$/.test(window.location.pathname);
- tab.classList.add('tabs-menu__item', 'tabs-menu__item_link');
- tab.href = `${userBtn.href}subscription/`;
- tab.innerHTML = `<h3 class="tabs-menu__item-text ${isSubs ? 'tabs-menu__item-text_active' : ''}">Он читает</h3>`;
- bar.appendChild(tab);
-
- const tab2 = document.createElement('a');
- const isFols = /followers\/$/.test(window.location.pathname);
- tab2.classList.add('tabs-menu__item', 'tabs-menu__item_link');
- tab2.href = `${userBtn.href}subscription/followers/`;
- tab2.innerHTML = `<h3 class="tabs-menu__item-text ${isFols ? 'tabs-menu__item-text_active' : ''}">Его читают</h3>`;
- bar.appendChild(tab2);
- }
- }
-
- // надо ли ещё
- Array.from(document.querySelectorAll('iframe[src^="https://codepen.io/"]'))
- .map(el => el.setAttribute('scrolling', 'no'));
-
- // остановка гифок по клику и воспроизведение при повторном клике
- function toggleGIF(el) {
- // если атрибут со старым линком пуст или отсутствует
- if (!el.dataset.oldSrc) {
- // заменим ссылку на data-url-svg с треугольником в круге
- const w = Math.max(el.clientWidth || 256, 16);
- const h = Math.max(el.clientHeight || 128, 16);
- const cx = w / 2;
- const cy = h / 2;
- const r = Math.min(w, h) / 4;
- const ax = (r * 61) / 128;
- const by = (r * 56) / 128;
- const bx = (r * 35) / 128;
- const svg = `data:image/svg+xml;utf8,
- <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
- <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
- <circle cx='${cx}' cy='${cy}' r='${r}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
- <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${FLAGS.GIF_STOP_COLOR_BG}' />
- </svg>
- `;
- el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
- el.setAttribute('src', svg);
- } else if (FLAGS.GIF_STOP_OVERTYPE) {
- // иначе поставим svg с троеточием
- const w = el.clientWidth;
- const h = el.clientHeight;
- const cx = w / 2;
- const cy = h / 2;
- const r = Math.min(w, h) / 4;
- const r2 = r / 4;
- const svg = `data:image/svg+xml;utf8,
- <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
- <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
- <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
- <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
- <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
- </svg>
- `;
- el.setAttribute('src', svg);
- // когда отрендерится троеточие, можно менять на исходную гифку
- setTimeout(() => {
- if (el.dataset.oldSrc) {
- el.setAttribute('src', el.dataset.oldSrc);
- el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign
- }
- }, 100);
- } else {
- const img = document.createElement('img');
- img.setAttribute('src', el.dataset.oldSrc);
- if (el.hasAttribute('align')) {
- img.setAttribute('align', el.getAttribute('align'));
- }
- el.parentNode.insertBefore(img, el);
- img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign
- el.parentNode.removeChild(el);
- }
- }
-
- if (FLAGS.GIF_STOP) {
- Array.from(document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]'))
- .filter((el) => {
- const excludes = [
- 'https://habrastorage.org/storage3/976/d3e/38a/976d3e38a34b003f86f91795524af9f8.gif',
- 'https://habrastorage.org/storage3/2e2/522/737/2e2522737ec404a9f76047e108dfaea0.gif',
- 'https://habrastorage.org/getpro/habr/post_images/d4b/289/ef0/d4b289ef0a00e969108c25d0c3d75f58.gif',
- ];
- return !excludes.includes(el.getAttribute('src'));
- })
- .forEach((el) => {
- if (FLAGS.GIF_STOP_ONLOAD) toggleGIF(el);
- el.classList.add('habrafix_gif-stop');
- el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
- });
- }
-
- // счетчики кармы
- if (FLAGS.KARMA_DETAILS) {
- Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => {
- itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign
- });
- Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => {
- karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
- karmaEl.style.minWidth = `${KARMA_WIDTH}px`; // eslint-disable-line no-param-reassign
- });
- Array.from(document.querySelectorAll(`
- .stacked-counter[href="https://habr.com/ru/info/help/karma/"],
- .stacked-counter[href="https://habr.com/en/info/help/karma/"]
- `)).forEach((counterEl) => {
- let total = parseInt(counterEl.title, 10);
- const scoreEl = counterEl.querySelector('.stacked-counter__value');
- if (!scoreEl || !total) return;
- counterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
- counterEl.style.minWidth = `${KARMA_WIDTH}px`; // eslint-disable-line no-param-reassign
- const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10);
- if (score > total) total = score;
- const likes = (total + score) / 2;
- const percent = Math.round((100 * likes) / total);
- const details = ` = ${total} × (${percent} − ${100 - percent})%`;
- const detailsEl = document.createElement('span');
- detailsEl.innerHTML = details;
- detailsEl.style.color = '#545454';
- detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif';
- detailsEl.style.fontSize = '13px';
- detailsEl.style.fontWeight = 'normal';
- detailsEl.style.verticalAlign = 'middle';
- scoreEl.appendChild(detailsEl);
- counterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign
- });
- }
-
- // счетчики рейтинга с подробностями
- const scoresMap = new Map();
-
- class Score {
- constructor(el) {
- this.el = el;
- this.parentEl = el.parentNode;
- const data = this.constructor.parse(el);
- this.rating = data.rating;
- this.total = data.total;
- this.likes = data.likes;
- this.dislikes = data.dislikes;
- this.isDetailed = false;
- this.observer = new MutationObserver(() => this.update());
- }
-
- setDetails(isDetailed) {
- if (this.isDetailed === isDetailed) return;
- this.isDetailed = isDetailed;
- this.update();
- }
-
- update() {
- const newChild = this.parentEl.querySelector('.voting-wjt__counter, .post-stats__result-counter');
- if (!newChild) return;
- this.el = newChild;
- const data = this.constructor.parse(this.el);
- this.rating = data.rating;
- this.total = data.total;
- this.likes = data.likes;
- this.dislikes = data.dislikes;
- this.observer.disconnect();
- if (this.isDetailed) {
- this.details();
- } else {
- this.simply();
- }
- this.observer.observe(this.parentEl, { childList: true });
- }
-
- static parse(el) {
- let [, likes, dislikes] = el
- .attributes.title.textContent
- .match(/[0-9]+/g).map(Number);
- let total = likes + dislikes;
- let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const
- rating = Number(rating);
- if (sign) rating = -rating;
- // не знаю что там происходит при голосовании, так что на всякий случай
- const diff = rating - (likes - dislikes);
- if (diff < 0) {
- total += Math.abs(diff);
- dislikes += Math.abs(diff);
- } else if (diff > 0) {
- total += diff;
- likes += diff;
- }
- return {
- rating,
- total,
- likes,
- dislikes,
- };
- }
-
- simply() {
- let innerHTML = '';
- if (this.rating > 0) {
- innerHTML = `+${this.rating}`;
- } else if (this.rating < 0) {
- innerHTML = `–${Math.abs(this.rating)}`;
- } else {
- innerHTML = '0';
- }
- this.el.innerHTML = innerHTML;
- }
-
- details() {
- let innerHTML = '';
- if (this.rating > 0) {
- innerHTML = `+${this.rating}`;
- } else if (this.rating < 0) {
- innerHTML = `–${Math.abs(this.rating)}`;
- } else {
- innerHTML = '0';
- }
- if (this.total !== 0) {
- let details = '';
- if (FLAGS.RATING_DETAILS_PN) {
- const percent = Math.round((100 * this.likes) / this.total);
- details = ` = ${this.total} × (${percent} − ${100 - percent})%`;
- } else {
- details = ` = ${this.likes} − ${this.dislikes}`;
- }
- innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
- }
- this.el.innerHTML = innerHTML;
- }
- }
-
- // парсим их
- Array.from(document.querySelectorAll('.voting-wjt__counter, .post-stats__result-counter')).forEach((el) => {
- scoresMap.set(el, new Score(el));
- });
-
- // добавляем подробностей
- if (FLAGS.RATING_DETAILS) {
- if (FLAGS.RATING_DETAILS_ONCLICK) {
- const isDetailed = userConfig.getItem('scores_details');
- if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed));
- scoresMap.forEach((score) => {
- score.el.onclick = () => { // eslint-disable-line no-param-reassign
- const nowDetailed = userConfig.shiftItem('scores_details');
- scoresMap.forEach(s => s.setDetails(nowDetailed));
- };
- });
- } else {
- scoresMap.forEach(score => score.setDetails(true));
- }
- }
-
- // метки времени и работа с ними
- const pageLoadTime = new Date();
- const monthNames = [
- 'января', 'февраля', 'марта',
- 'апреля', 'мая', 'июня',
- 'июля', 'августа', 'сентября',
- 'октября', 'ноября', 'декабря',
- ];
- const monthNamesEng = [
- 'January', 'February', 'March',
- 'April', 'May', 'June',
- 'July', 'August', 'September',
- 'October', 'November', 'December',
- ];
-
- class HabraTime {
- constructor(el, parent) {
- this.el = el;
- this.parent = parent;
- this.attrDatetime = this.constructor.getAttributeDatetime(el);
- this.date = new Date(this.attrDatetime);
- }
-
- // вот было бы хорошо, если б на хабре были datetime атрибуты
- static getAttributeDatetime(el) {
- const imagination = el.getAttribute('datetime') || el.getAttribute('data-time_published');
- if (imagination) return imagination;
-
- let recently;
- let day;
- let month;
- let year;
- let time;
- let isEng = false;
- let meridiem;
- if (/^\d\d:\d\d$/.test(el.innerHTML)) {
- [, time] = el.innerHTML.match(/(\d\d:\d\d)/);
- recently = 'сегодня';
- } else if (/^\d\d:\d\d (AM|PM)$/.test(el.innerHTML)) {
- [, time, meridiem] = el.innerHTML.match(/(\d\d:\d\d) (AM|PM)/);
- recently = 'сегодня';
- } else if (/at/.test(el.innerHTML)) {
- isEng = true;
- const re = /((today|yesterday)|([A-z]+) (\d+), (\d+)) at (\d\d:\d\d) (AM|PM)/;
- [,,
- recently,
- month, day, year,
- time,
- meridiem,
- ] = el.innerHTML.match(re);
-
- if (recently === 'today') {
- recently = 'сегодня';
- } else if (recently === 'yesterday') {
- recently = 'вчера';
- }
- } else {
- const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
- [,,
- recently,
- day, month, year,
- time,
- ] = el.innerHTML.match(re);
- }
-
- const [, h, m] = time.match(/(\d\d):(\d\d)/);
- if (meridiem === 'PM') {
- time = `${Number(h === '12' ? 0 : h) + 12}:${m}`;
- } else if (meridiem === 'AM') {
- time = `${h === '12' ? '00' : h}:${m}`;
- }
-
- // и местное время
- let moscow;
- if (recently || year === undefined) {
- const offsetMoscow = 3 * 60 * 60 * 1000;
- const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0;
- const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000;
- const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset;
- moscow = new Date(value);
- }
-
- if (recently) {
- day = moscow.getDate();
- month = moscow.getMonth() + 1;
- } else if (month.length !== 2) {
- month = (isEng ? monthNamesEng : monthNames).indexOf(month) + 1;
- } else {
- month = +month;
- }
-
- if (day < 10) day = `0${+day}`;
- if (month < 10) month = `0${month}`;
- if (year < 100) year = `20${year}`;
- if (year === undefined) year = moscow.getFullYear();
-
- return `${year}-${month}-${day}T${time}+03:00`;
- }
-
- absolute() {
- let result = '';
-
- const time = this.date;
- const day = time.getDate();
- const month = time.getMonth();
- const monthName = monthNames[month];
- const year = time.getFullYear();
- const hours = time.getHours();
- const minutes = time.getMinutes();
-
- const now = new Date();
- const nowDay = now.getDate();
- const nowMonth = now.getMonth();
- const nowYear = now.getFullYear();
-
- const yesterday = new Date(now - (24 * 60 * 60 * 1000));
- const yesterdayDay = yesterday.getDate();
- const yesterdayMonth = yesterday.getMonth();
- const yesterdayYear = yesterday.getFullYear();
-
- const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`;
-
- const isToday =
- day === nowDay &&
- month === nowMonth &&
- year === nowYear;
- const isYesterday =
- day === yesterdayDay &&
- month === yesterdayMonth &&
- year === yesterdayYear;
-
- if (isToday) {
- result = `сегодня в ${hhmm}`;
- } else if (isYesterday) {
- result = `вчера в ${hhmm}`;
- } else if (nowYear === year) {
- result = `${day} ${monthName} в ${hhmm}`;
- } else {
- result = `${day} ${monthName} ${year} в ${hhmm}`;
- }
-
- return result;
- }
-
- static relative(milliseconds) {
- let result = '';
-
- const pluralForm = (n, forms) => {
- if (n % 10 === 1 && n % 100 !== 11) return forms[0];
- if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1];
- return forms[2];
- };
-
- const formats = [
- ['год', 'года', 'лет'],
- ['месяц', 'месяца', 'месяцев'],
- ['день', 'дня', 'дней'],
- ['час', 'часа', 'часов'],
- ['минуту', 'минуты', 'минут'],
- ];
-
- const minutes = milliseconds / 60000;
- const hours = minutes / 60;
- const days = hours / 24;
- const months = days / 30;
- const years = months / 12;
- const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1);
-
- if (idx === -1) {
- result = 'несколько секунд';
- } else {
- const value = Math.floor([years, months, days, hours, minutes][idx]);
- const forms = formats[idx];
- const form = pluralForm(value, forms);
- result = `${value} ${form}`;
- }
- return result;
- }
-
- fromNow() {
- const diff = Math.abs(Date.now() - this.date);
- return `${this.constructor.relative(diff)} назад`;
- }
-
- fromParent() {
- const diff = Math.abs(this.date - this.parent.date);
- return `через ${this.constructor.relative(diff)}`;
- }
-
- static datetimeToMsk(datetime) {
- const [, yyyy, mm, dd, h, m] = datetime
- .match(/([0-9]+)-([0-9]+)-([0-9]+)T([0-9]+):([0-9]+)/);
- return `${Number(dd)} ${monthNames[mm - 1]} ${yyyy} в ${h}:${m}`;
- }
- }
-
- // собираем метки времени
- const datesMap = new Map();
- const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
- (megapostTimeEl ? [megapostTimeEl] : [])
- .concat(Array.from(document.querySelectorAll(`
- .post__time,
- .preview-data__time-published,
- time.comment__date-time_published,
- .tm-post__date,
- .user-message__date-time,
- .news-topic__attr_date-time
- `))).forEach((el) => {
- datesMap.set(el, new HabraTime(el));
- });
-
- function updateTime(el) {
- datesMap.forEach((habraTime) => {
- if (
- !habraTime.el ||
- !document.body.contains(habraTime.el) ||
- (el && el !== habraTime.el)
- ) return;
- let type;
- let otherTypes;
- if (habraTime.parent) {
- type = userConfig.config.time_comments;
- otherTypes = userConfig.model.time_comments
- .filter(str => str !== type);
- } else {
- type = userConfig.config.time_publications;
- otherTypes = userConfig.model.time_publications
- .filter(str => str !== type);
- }
- const title = otherTypes.map(otherType => habraTime[otherType]()).join(', ');
- habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign
- habraTime.el.setAttribute('title', title);
- });
- }
-
- if (FLAGS.TIME_DETAILS) {
- datesMap.forEach((habraTime) => {
- habraTime.el.setAttribute(
- 'style',
- 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
- );
- habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign
- if (habraTime.parent) {
- userConfig.shiftItem('time_comments');
- } else {
- userConfig.shiftItem('time_publications');
- }
- updateTime();
- };
- });
- // подождём, когда дерево комментариев будет построено
- // у некоторых меток времени будут установлены родители
- // тогда и обновим их тексты
- setTimeout(updateTime, 100);
- setInterval(updateTime, 30 * 1000);
- }
-
- // время публикации, понадобится для корня древа комментариев
- let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time'));
- // если нету публикации поищем самую раннюю метку времени
- if (!datePublication) {
- datePublication = { date: pageLoadTime };
- datesMap.forEach((date) => {
- if (date.date < datePublication.date) datePublication = date;
- });
- }
-
- if (FLAGS.FIND_COMMENTS) {
- const commentsList = document.querySelector('.user_comments');
- const match = document.location.pathname.match(/users\/([^/]+)\/comments/i);
- if (match && commentsList) {
- const nickname = match[1];
- const originalTitle = document.title;
-
- const searchForm = document.createElement('div');
- searchForm.classList.add('search-form', 'search-form_expanded');
- searchForm.style.width = 'auto';
- searchForm.innerHTML = `
- <span class="search-field__icon icon-svg_search" style="left: 0;"><svg class="icon-svg" width="32" height="32"
- viewBox="0 0 32 32" aria-hidden="true" version="1.1" role="img"><path d="M21.416 13.21c0 4.6-3.65 8.34-8.14
- 8.34S5.11 17.81 5.11 13.21c0-4.632 3.65-8.373 8.167-8.373 4.488 0 8.14 3.772 8.14 8.372zm1.945
- 7.083c1.407-2.055 2.155-4.57 2.155-7.084C25.515 6.277 20.04.665 13.277.665S1.04 6.278 1.04 13.21c0 6.93 5.475
- 12.542 12.237 12.542 2.454 0 4.907-.797 6.942-2.208l7.6 7.79 3.14-3.22-7.6-7.82z"></path></svg></span>
- <span class="search-field__icon icon-svg_loading" style="left: 0;"><svg class="icon-svg" width="40" height="40"
- viewBox="0 0 100 100" enable-background="new 0 0 0 0"><circle cx="50" cy="50" fill="none" stroke="#333333"
- stroke-width="4" r="20" stroke-dasharray="94.24777960769379 33.41592653589793"
- transform="rotate(88.5132 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear"
- values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform>
- </circle></svg></span>
- <label
- id="comments_search_label"
- class="search-form__field-wrapper"
- style="background: linear-gradient(to right, rgba(84, 142, 170, 0.2) 0%, transparent 0%)"
- >
- <input type="text" class="search-form__field" id="search-comments-field" placeholder="Поиск по тексту комментариев"
- style="position: absolute;background-color: transparent;">
- <button type="button" class="btn btn_search-close" id="search-comments-clear" title="Очистить">
- <svg class="icon-svg icon-svg_navbar-close-search" width="31" height="32" viewBox="0 0 31 32" aria-hidden="true" version="1.1" role="img">
- <path d="M26.67 0L15.217 11.448 3.77 0 0 3.77l11.447 11.45L0 26.666l3.77
- 3.77L15.218 18.99l11.45 11.448 3.772-3.77-11.448-11.45L30.44 3.772z">
- </path>
- </svg>
- </button>
- </label>
- `;
-
- const notFoundLabel = document.createElement('p');
- notFoundLabel.style.textAlign = 'center';
- notFoundLabel.style.fontSize = '18px';
- notFoundLabel.style.color = 'gray';
- notFoundLabel.style.display = 'none';
- notFoundLabel.textContent = 'Ничего не найдено';
-
- const commentsSubList = document.createElement('ul');
- commentsSubList.classList.add('content-list', 'content-list_comments');
- commentsSubList.id = 'search-comments';
- commentsSubList.style.display = 'none';
- commentsList.insertBefore(commentsSubList, commentsList.firstChild);
- commentsList.insertBefore(notFoundLabel, commentsList.firstChild);
- commentsList.insertBefore(searchForm, commentsList.firstChild);
-
- const Progress = {
- set(value) {
- if (value === 1) {
- document.title = 'Поиск завершён';
- this.setProgressBar(0);
- } else {
- const percent = Math.round(value * 100);
- document.title = `${percent}%, идёт поиск`;
- this.setProgressBar(value);
- }
- },
- setProgressBar(value) {
- const percent = value * 100;
- document.getElementById('comments_search_label').style.background = `
- linear-gradient(to right, rgba(84, 142, 170, 0.2) ${percent}%, transparent ${percent}%)
- `;
- },
- reset() {
- this.setProgressBar(0);
- document.title = originalTitle;
- },
- };
-
- const makeComment = (comment) => {
- const commentEl = document.createElement('li');
- commentEl.classList.add('content-list__item', 'content-list__item_comment', 'content-list__item_comment-plain');
- let ratingClass = '';
- let rating = '0';
- if (comment.score > 0) {
- rating = `+${comment.score}`;
- ratingClass = 'voting-wjt__counter_positive';
- } else if (comment.score < 0) {
- rating = `–${Math.abs(comment.score)}`;
- ratingClass = 'voting-wjt__counter_negative';
- }
- let avatar;
- if (comment.avatar === 'https://habr.com/images/avatars/stub-user-middle.gif') {
- avatar = `<svg class="default-image default-image_mini default-image_green" width="24"
- height="24"><use xlink:href="https://habr.com/images/1558430991/common-svg-sprite.svg#slug"></use></svg>`;
- } else {
- avatar = `<img src="${comment.avatar}" class="user-info__image-pic user-info__image-pic_small" width="24" height="24">`;
- }
- commentEl.innerHTML = `
- <div class="comment__post-title">
- <a href="${comment.post.url}" class="comment__post-link">${comment.post.title}</a>
- <div class="comment__post-footer">
- <a href="${comment.post.url}#comments">
- <svg class="icon-svg_comments icon-svg_comments-plain" width="14" height="13">
- <use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#comment"></use>
- </svg>
- <span class="comment__post-comments-counter">${comment.post.comments_count}</span>
- </a>
- </div>
- </div>
- <div class="comment comment_plain" rel="${comment.id}" id="habrafix_comment_${comment.id}">
- <div class="comment__head">
- <a href="https://habr.com/ru/users/${comment.author.login}/"
- class="user-info user-info_inline" rel="user-popover" data-user-login="${comment.author.login}">
- ${avatar}
- <span class="user-info__nickname user-info__nickname_small user-info__nickname_comment">${comment.author.login}</span>
- </a>
- <svg class="icon_comment-edit" title="Комментарий был изменен"
- style="display: ${comment.time_changed !== '0' ? 'block' : 'none'}" width="12"
- height="12"><use xlink:href="https://habr.com/images/1558430991/common-svg-sprite.svg#pencil"></use></svg>
- <time class="comment__date-time comment__date-time_published"
- datetime="${comment.time_published}">${comment.time_published}</time>
- <ul class="inline-list inline-list_comment-nav">
- <li class="inline-list__item inline-list__item_comment-nav">
- <a href="${comment.post.url}#comment_${comment.id}" class="icon_comment-anchor"
- title="Ссылка на комментарий"><svg width="12" height="12">
- <use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#anchor"></use></svg></a>
- </li>
- <li class="inline-list__item inline-list__item_comment-nav">
- <a href="#" class="icon_comment-bookmark " onclick="comments_add_to_favorite(this)" data-type="3"
- data-id="${comment.id}" data-action="add" title="Добавить в закладки">
- <svg width="12" height="12"><use xlink:href="https://habr.com/images/1556525186/common-svg-sprite.svg#book"></use></svg>
- </a>
- </li>
- </ul>
- <div class="voting-wjt voting-wjt_comments js-comment-vote">
- <span class="voting-wjt__counter ${ratingClass}
- js-score" title="Общий рейтинг ${rating}">${rating}</span>
- </div>
- </div>
- <div class="comment__message">${comment.message}</div>
- </div>`;
- return commentEl;
- };
-
- const addComment = (comment) => {
- document.getElementById('search-comments').appendChild(makeComment(comment));
- const timeEl = document.querySelector(`#habrafix_comment_${comment.id} time`);
- if (FLAGS.TIME_DETAILS) {
- timeEl.setAttribute(
- 'style',
- 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
- );
- timeEl.onclick = () => { // eslint-disable-line no-param-reassign
- userConfig.shiftItem('time_publications');
- updateTime();
- };
- datesMap.set(timeEl, new HabraTime(timeEl));
- updateTime(timeEl);
- } else {
- timeEl.textContent = HabraTime.datetimeToMsk(comment.time_published);
- }
- };
-
- // let fetchCors = url => fetch(`https://cors.io/?${url}`);
- let fetchCors = url => fetch(url);
-
- if (typeof GM !== 'undefined' && GM.xmlHttpRequest) {
- fetchCors = url => new Promise((resolve, reject) => GM.xmlHttpRequest({
- method: 'GET',
- url,
- onload(response) {
- resolve({
- json: () => Promise.resolve(JSON.parse(response.responseText)),
- });
- },
- onerror() { reject(); },
- ontimeout() { reject(); },
- }));
- }
-
- const loadComments = async (page) => {
- const url = `https://m.habr.com/kek/v1/users/${nickname}/comments?comments=true&user=${nickname}&page=${page}`;
- const res = await fetchCors(url);
- const json = await res.json();
- return json.data;
- };
-
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
-
- const pageCache = {};
- const memoizedLoadComments = async (page) => {
- if (pageCache[page]) {
- await sleep(0);
- return pageCache[page];
- }
- const result = await loadComments(page);
- pageCache[page] = result;
- return result;
- };
-
- let latestSearchId = 0;
-
- const removeResultsList = () => {
- document.getElementById('search-comments-field').value = '';
- latestSearchId += 1;
- document.getElementById('search-comments').innerHTML = '';
- document.getElementById('search-comments').style.display = 'none';
- document.getElementById('comments').style.display = '';
- const footer = document.querySelector('.page__footer');
- if (footer) footer.style.display = '';
- Progress.reset();
- notFoundLabel.style.display = 'none';
- searchForm.classList.remove('loading');
- };
-
- const addResultsList = () => {
- document.getElementById('search-comments').innerHTML = '';
- document.getElementById('search-comments').style.display = '';
- document.getElementById('comments').style.display = 'none';
- const footer = document.querySelector('.page__footer');
- if (footer) footer.style.display = 'none';
- Progress.set(0);
- notFoundLabel.style.display = 'none';
- searchForm.classList.add('loading');
- };
-
- document.getElementById('search-comments-clear').onclick = removeResultsList;
-
- // eslint-disable-next-line consistent-return
- const search = async () => {
- latestSearchId += 1;
- const currentSearchId = latestSearchId;
-
- const text = document.getElementById('search-comments-field').value;
- if (!text) return removeResultsList();
-
- addResultsList();
-
- const filter = (comment) => {
- const t = text.toLowerCase();
- const message = comment.message.toLowerCase();
- const title = comment.post.title.toLowerCase();
- return message.includes(t) || title.includes(t);
- };
-
- let pages = 1;
- for (let curPage = 1; curPage <= pages; curPage += 1) {
- let data;
-
- while (!data) {
- try {
- // eslint-disable-next-line no-await-in-loop
- data = await memoizedLoadComments(curPage);
- } catch (e) {
- document.title = 'Ошибка сети, повтор...';
- // eslint-disable-next-line no-await-in-loop
- await sleep(5000);
- }
- }
-
- // eslint-disable-next-line consistent-return
- if (currentSearchId !== latestSearchId) return; // пользователь изменил текст
-
- pages = pages === 1 ? data.pages : pages;
- Progress.set(curPage / pages);
-
- data.comments
- .filter(filter)
- .forEach(addComment);
- }
-
- if (commentsSubList.childNodes.length === 0) notFoundLabel.style.display = '';
- searchForm.classList.remove('loading');
- };
-
- const awaitEndOfInput = (func, ms) => {
- let timerId;
- return () => {
- clearTimeout(timerId);
- timerId = setTimeout(func, ms);
- };
- };
-
- document.getElementById('search-comments-field').oninput = awaitEndOfInput(search, 1000);
- }
- }
-
- // создаем дерево комментариев
- class ItemComment {
- constructor(el, parent) {
- this.parent = parent;
- this.el = el;
- this.lvl = parent.lvl + 1;
- this.id = Number(el.getAttribute('rel'));
- this.commentEl = el.querySelector('.comment');
- if (this.commentEl) {
- this.timeEl = this.commentEl.querySelector('time');
- this.ratingEl = this.commentEl.querySelector('.js-score');
- }
- this.date = datesMap.get(this.timeEl);
- if (this.date) {
- this.date.parent = parent.date;
- } else {
- this.date = parent.date;
- }
- this.votes = scoresMap.get(this.ratingEl) || {
- total: 0, likes: 0, dislikes: 0, rating: 0,
- };
- this.elList = el.querySelector('.content-list_nested-comments');
- }
-
- existId(id) {
- return !!this.elList.querySelector(id);
- }
-
- existNew() {
- return !!this.elList.querySelector('.js-comment_new');
- }
-
- getLength() {
- let { length } = this.list;
- this.list.forEach((node) => {
- length += node.getLength();
- });
- return length;
- }
- }
-
- class CommentsTree {
- constructor() {
- this.root = {
- isRoot: true,
- date: datePublication,
- lvl: 0,
- elList: document.getElementById('comments-list'),
- list: [],
- };
- }
-
- static exist() {
- return !!document.getElementById('comments-list');
- }
-
- update() {
- if (!this.root.elList) return;
- const recAdd = (node) => {
- node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign
- .map(el => new ItemComment(el, node));
- node.list.forEach(recAdd);
- };
- recAdd(this.root);
- }
-
- walkTree(fn) {
- const walk = (tree) => {
- fn(tree);
- tree.list.forEach(walk);
- };
- walk(this.root);
- }
-
- sort(fn) {
- if (!this.root.elList) return;
- this.walkTree((tree) => {
- tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el));
- });
- }
-
- shuffle() {
- if (!this.root.elList) return;
- const randInt = maximum => Math.floor(Math.random() * (maximum + 1));
- this.walkTree((tree) => {
- const { list } = tree;
- for (let i = 0; i < list.length; i += 1) {
- const j = randInt(i);
- [list[i], list[j]] = [list[j], list[i]];
- }
- list.forEach(subtree => tree.elList.appendChild(subtree.el));
- });
- }
- }
-
- const commentsTree = new CommentsTree();
- commentsTree.update();
-
- FLAGS.sortVariants = [
- ['time', 'старые'],
- ];
-
- if (FLAGS.COMMENTS_SORT_BY_FRESHNESS) FLAGS.sortVariants.push(['freshness', 'новые']);
- if (FLAGS.COMMENTS_SORT_BY_TREND) FLAGS.sortVariants.push(['trend', 'горячие']);
- if (FLAGS.COMMENTS_SORT_BY_QUALITY) FLAGS.sortVariants.push(['quality', 'хорошие']);
- if (FLAGS.COMMENTS_SORT_BY_REDDIT) FLAGS.sortVariants.push(['reddit', 'проверенные']);
- if (FLAGS.COMMENTS_SORT_BY_RATING) FLAGS.sortVariants.push(['rating', 'рейтинговые']);
- if (FLAGS.COMMENTS_SORT_BY_POPULARITY) FLAGS.sortVariants.push(['popularity', 'популярные']);
- if (FLAGS.COMMENTS_SORT_BY_RANDOM) FLAGS.sortVariants.push(['shuffle', 'случайные']);
-
- // здесь начинается сортировка комментариев
- const commentsOrderEl = document.createElement('div');
- commentsOrderEl.classList.add('comments_order');
- commentsOrderEl.innerHTML = FLAGS.sortVariants.map(([type, text]) => {
- const underline = (type === 'time') ? '; text-decoration: underline' : '';
- return `<a data-order="${type}" style="cursor: pointer${underline}">${text}</a>`;
- }).join(', ');
-
- if (FLAGS.COMMENTS_SORT && document.getElementById('comments-list')) {
- const commentsList = document.getElementById('comments-list');
- commentsList.parentElement.insertBefore(commentsOrderEl, commentsList);
- }
-
- const commentsComparators = {
- time(a, b) {
- return a.id - b.id;
- },
-
- freshness(a, b) {
- return b.id - a.id;
- },
-
- rating(a, b) {
- const ascore = a.votes.rating;
- const bscore = b.votes.rating;
- if (bscore !== ascore) return bscore - ascore;
- return b.id - a.id;
- },
-
- popularity(a, b) {
- const aVotes = a.votes.total;
- const bVotes = b.votes.total;
- if (aVotes !== bVotes) return bVotes - aVotes;
- const aLength = a.getLength();
- const bLength = b.getLength();
- if (aLength !== bLength) return bLength - aLength;
- return b.id - a.id;
- },
-
- quality(a, b) {
- const aQuality = a.votes.rating / a.votes.total || 0;
- const bQuality = b.votes.rating / b.votes.total || 0;
- if (aQuality !== bQuality) return bQuality - aQuality;
- if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating;
- return b.id - a.id;
- },
-
- trend(a, b) {
- // в первые сутки после публикации статьи число посещений больше чем в остальное время
- const oneDay = 24 * 60 * 60 * 1000;
- const firstDayEnd = +datePublication.date + oneDay;
- // у комментария есть только три дня на голосование с момента его создания
- const threeDays = 3 * oneDay;
- const now = Date.now();
-
- // прикинем число голосов в первый день
- const aDate = +a.date.date;
- let aViews = 0;
- // в первый день
- if (aDate <= firstDayEnd) {
- aViews += Math.min(firstDayEnd, now) - aDate;
- }
- // и в остальное время
- if (now >= firstDayEnd) {
- const threeDaysEnd = aDate + threeDays;
- // для этого соотношения я собрал статистику
- aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16;
- }
- const aScore = a.votes.rating / aViews;
-
- // аналогично
- const bDate = +b.date.date;
- let bViews = 0;
- if (bDate <= firstDayEnd) {
- bViews += Math.min(firstDayEnd, now) - bDate;
- }
- if (now >= firstDayEnd) {
- const threeDaysEnd = bDate + threeDays;
- // найти зависимость активности голосования от времени суток не удалось
- bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16;
- }
- const bScore = b.votes.rating / bViews;
-
- if (bScore === aScore) return b.id - a.id;
- return bScore - aScore;
- },
-
- reddit(a, b) {
- const wilsonScore = (ups, downs) => {
- const n = ups + downs;
- if (n === 0) return 0;
- const z = 1.281551565545;
- const p = ups / n;
- const left = p + ((1 / (2 * n)) * z * z);
- const right = z * Math.sqrt(((p * (1 - p)) / n) + ((z * z) / (4 * n * n)));
- const under = 1 + ((1 / n) * (z * z));
- return (left - right) / under;
- };
- const aScore = wilsonScore(a.votes.likes, a.votes.dislikes);
- const bScore = wilsonScore(b.votes.likes, b.votes.dislikes);
- if (bScore === aScore) return b.id - a.id;
- return bScore - aScore;
- },
- };
-
- const sortComments = () => {
- const order = userConfig.getItem('comments_order');
-
- Array.from(commentsOrderEl.children).forEach((el) => {
- if (el.dataset.order === order) {
- el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign
- } else {
- el.style.textDecoration = ''; // eslint-disable-line no-param-reassign
- }
- });
-
- if (order === 'shuffle') {
- commentsTree.shuffle();
- } else {
- const compare = commentsComparators[order];
- commentsTree.sort(compare);
- }
- };
-
- // сортируем комменты при загрузке страницы
- // или не сортируем, если они уже по порядку
- if (FLAGS.COMMENTS_SORT && FLAGS.COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') {
- sortComments();
- }
-
- Array.from(commentsOrderEl.children).forEach((el) => {
- el.onclick = () => { // eslint-disable-line no-param-reassign
- userConfig.setItem('comments_order', el.dataset.order);
- sortComments();
- };
- });
-
-
- // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев
- if (FLAGS.COMMENTS_LINKS) {
- const commentsLinks = document.getElementsByClassName('post-stats__comments-link');
-
- for (let i = 0; i < commentsLinks.length; i += 1) {
- const iLink = commentsLinks[i];
- const hrefValue = iLink.getAttribute('href');
- const hrefToComments = hrefValue.replace('#first_unread', '#comments');
- iLink.setAttribute('href', hrefToComments);
- }
- }
-
- // сворачивание комментов
- // eslint-disable-next-line no-constant-condition
- if (false) { // FLAGS.COMMENTS_HIDE
- const commentHash = window.location.hash;
-
- const toggle = (subtree) => {
- const listLength = subtree.list.length;
- if (listLength === 0) return;
- /* eslint-disable */
- if (subtree.switcherEl.dataset.isVisibleList === 'true') {
- subtree.switcherEl.dataset.isVisibleList = 'false';
- subtree.switcherEl.innerHTML = `\u229E раскрыть ветку ${subtree.getLength()}`;
- subtree.elList.style.display = 'none';
- } else {
- subtree.switcherEl.dataset.isVisibleList = 'true';
- subtree.switcherEl.innerHTML = '\u229F';
- subtree.elList.style.display = 'block';
- }
- /* eslint-enable */
- };
-
- commentsTree.walkTree((subtree) => {
- // не пытаемся сворачивать корень
- if (subtree.isRoot) return;
- // у похищенных нет футера
- const footerEl = subtree.commentEl.querySelector('.comment__footer');
- if (footerEl === null) return;
- // создаём переключатель
- const switcher = document.createElement('a');
- switcher.classList.add('comment__footer-link');
- switcher.classList.add('comment__switcher');
- switcher.dataset.isVisibleList = 'true';
-
- switcher.innerHTML = '\u229F';
- if (subtree.list.length === 0) switcher.innerHTML = '\u22A1';
- switcher.style.cursor = 'pointer';
- switcher.style.marginLeft = '-5px';
-
- footerEl.insertBefore(switcher, footerEl.children[0]);
- subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign
-
- switcher.onclick = () => toggle(subtree);
-
- const isHideLvl = subtree.lvl === FLAGS.HIDE_LEVEL && FLAGS.HIDE_LEVEL_4;
- const isLineLvl = subtree.lvl % FLAGS.LINE_LEN === 0;
- if (isLineLvl) {
- subtree.elList.classList.add('comments_new-line');
- const lineNumber = subtree.lvl / FLAGS.LINE_LEN;
- subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
- }
- // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
- if (
- (isHideLvl || isLineLvl) && !subtree.existNew() &&
- !(commentHash && subtree.existId(commentHash))
- ) {
- toggle(subtree);
- }
- });
- }
-
- if (FLAGS.SCROLL_LEGEND) {
- const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body');
- const commentsEl = document.getElementById('comments-list');
- const getPercents = (el) => {
- if (!el) return { topPercent: 0, heightPercent: 0 };
- const pageHeight = document.documentElement.scrollHeight;
- const top = el.getBoundingClientRect().top + window.pageYOffset;
- const topPercent = ((100 * top) / pageHeight).toFixed(2);
- const height = el.clientHeight;
- const heightPercent = ((100 * height) / pageHeight).toFixed(2);
-
- return { topPercent, heightPercent };
- };
-
- const updateLegend = (pageEl, legendEl) => {
- const { topPercent, heightPercent } = getPercents(pageEl);
- legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign
- legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign
- };
-
- const legendPost = document.createElement('div');
- legendPost.classList.add('legend_el');
- legendPost.style.background = 'rgba(84, 142, 170, 0.66)';
- updateLegend(postBodyEl, legendPost);
- document.body.appendChild(legendPost);
-
- const legendComments = document.createElement('div');
- legendComments.classList.add('legend_el');
- legendComments.style.background = 'rgba(49, 176, 7, 0.66)';
- updateLegend(commentsEl, legendComments);
- document.body.appendChild(legendComments);
-
- setInterval(() => {
- updateLegend(postBodyEl, legendPost);
- updateLegend(commentsEl, legendComments);
- }, 1000);
- }
-
- if (FLAGS.NIGHT_MODE) {
- const switcherEl = document.createElement('div');
- switcherEl.classList.add('night_mode_switcher');
- switcherEl.onclick = () => {
- const isNightMode = userConfig.shiftItem('night_mode');
- document.documentElement.classList.toggle('night', isNightMode);
- };
- document.body.appendChild(switcherEl);
- setInterval(() => {
- const boolClass = document.documentElement.classList.contains('night');
- const isNightMode = userConfig.getItem('night_mode');
- if (boolClass !== isNightMode) {
- document.documentElement.classList.toggle('night', isNightMode);
- }
- }, 1000);
- }
-
- if (FLAGS.CONFIG_INTERFACE) {
- const configFrame = document.createElement('div');
- configOptions.forEach(([key, text]) => {
- if (typeof FLAGS[key] !== 'boolean') return;
- const inputEl = document.createElement('input');
- inputEl.type = 'checkbox';
- inputEl.value = key;
- inputEl.checked = FLAGS[key];
- const labelEl = document.createElement('label');
- labelEl.setAttribute('unselectable', 'on');
- labelEl.setAttribute('onselectstart', 'return false');
- const spanEl = document.createElement('span');
- spanEl.innerHTML = text;
- configFrame.appendChild(labelEl);
- labelEl.appendChild(inputEl);
- labelEl.appendChild(spanEl);
- inputEl.onchange = () => {
- FLAGS[key] = inputEl.checked;
- localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
- };
- configFrame.appendChild(document.createElement('br'));
- });
- const reloadText = document.createElement('div');
- reloadText.style.textAlign = 'right';
- reloadText.innerHTML = `
- * чтобы увидеть изменения
- <a href="#" onclick="location.reload(); return false">
- обновите страницу
- </a>`;
- configFrame.appendChild(reloadText);
- configFrame.classList.add('config_frame');
- configFrame.style.display = 'none';
- document.body.appendChild(configFrame);
-
- const configButton = document.createElement('div');
- configButton.classList.add('config_button');
- document.body.appendChild(configButton);
-
- configButton.onclick = () => {
- if (configFrame.style.display) {
- configFrame.style.display = '';
- } else {
- configFrame.style.display = 'none';
- }
- };
- }
-
- setTimeout(() => {
- const marker = document.createElement('meta');
- marker.id = 'habrafixmarker';
- document.head.appendChild(marker);
- }, 300);
- });