Habr.Features

Всякое-разное для Habr aka habr.com

当前为 2018-09-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Habr.Features
// @version 3.2.24
// @description Всякое-разное для Habr aka habr.com
// @author AngReload
// @include https://habr.com/*
// @include http://habr.com/*
// @namespace habr_comments
// @run-at document-start
// @grant none
// @icon https://habr.com/favicon.ico
// ==/UserScript==
/* global localStorage, MutationObserver */

const FLAGS = {};

// остановка гифок
// клик по гифке заменит картинку на заглушку
// повторный клик вернет гифку на место
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 = true;

// показывать счетчики рейтинга в виде:
// рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
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 = true;
// свернуть комментарии если их глубина вложенности равна некому числу
FLAGS.HIDE_LEVEL = 4;
// сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
FLAGS.LINE_LEN = 8;
// отбивать каждый следущий уровень вложенности дополнительным отступом
FLAGS.REDUCE_NEW_LINES = true;
// глубина вложенности при «возврате каретки» комментариев обозначается разным цветом
FLAGS.RAINBOW_NEW_LINE = true;

// заменить ссылки ведущие к новым комментариям на дерево комментариев
FLAGS.COMMENTS_LINKS = true;

// запоминание галки «Использовать MarkDown» для комментариев
FLAGS.COMMENTS_MD = true;

// в огнелисе, в отличии о хрома, при загрузке изображений не сохраняется прокрутка
// меня раздражает когда дерево комментариев скачками пытается уехать вниз
// эта опция зафиксирует высоту публикации пока та находится вне обзора
// надо переделать
FLAGS.FIX_JUMPING_SCROLL = false;

// линии вдоль прокрутки для отображения размеров поста и комментариев
FLAGS.SCROLL_LEGEND = true;

// добавляет кнопку активации ночного режима
FLAGS.NIGHT_MODE = true;

// добавляет диалок настроек
FLAGS.CONFIG_INTERFACE = true;

// включить разные стили
FLAGS.USERSTYLE = true;
// показывать гифки только при наведении
FLAGS.USERSTYLE_GIF_HOVER = false;
// лента постов с увеличенными отступами, нижний бар выровнен вправо
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_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;

const configOptions = [
  ['KARMA_DETAILS', 'карма с плюсами и минусами'],
  ['RATING_DETAILS', 'рейтинг с подробностями о плюсах и минусах'],
  ['RATING_DETAILS_PN', 'рейтинг в абсолютных еденицах вместо процентов'],
  ['TIME_DETAILS', 'отметки о времени с подробностями'],
  ['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_RATING', 'сортировка по рейтингу'],
  ['COMMENTS_SORT_BY_POPULARITY', 'сортировка по популярности'],
  ['COMMENTS_SORT_BY_REDDIT', 'сортировка по уверенности'],
  ['COMMENTS_SORT_BY_RANDOM', 'перемешивание комментариев'],
  ['COMMENTS_HIDE', 'сворачивание комментов'],
  ['REDUCE_NEW_LINES', 'возврат каретки с уменьшением ширины'],
  ['RAINBOW_NEW_LINE', 'возврат каретки комментов в разных цветах'],
  ['COMMENTS_LINKS', 'ссылка на корень комментов вместо новых'],
  ['COMMENTS_MD', 'запоминать галку MarkDown'],
  ['FIX_JUMPING_SCROLL', 'заморозить высоту статьи при загрузке'],
  ['SCROLL_LEGEND', 'ленгенда страницы у скроллбара'],
  ['NIGHT_MODE', 'ночной режим'],
  ['USERSTYLE', 'стилизация'],
  ['USERSTYLE_COMMENTS_FIX', 'стили комментов'],
  ['USERSTYLE_HLJS_LANG', 'показать язык для блоков кода'],
  ['USERSTYLE_HLJS_LANG_HOVER', 'язык кода скрыт до наведении курсора'],
  ['USERSTYLE_GIF_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];
    }
  });
}

// интерфейс для хранения настроек
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.USERSTYLE_GIF_HOVER) {
  userStyle += `
  .post__text img[src$=".gif"],
  .comment__message img[src$=".gif"] {
    filter: contrast(0) brightness(0.33) !important;
  }
  .post__text img[src$=".gif"]:hover,
  .comment__message img[src$=".gif"]:hover {
    filter: none !important;
  }
  `;
}

if (FLAGS.SCROLL_LEGEND) {
  userStyle += `
    .legend_el {
      position: fixed;
      width: 4px;
      right: 0;
      transition: top 1s ease-out, height 1s ease-out;
      z-index: 101;
    }

    #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_REMOVE_SIDEBAR_RIGHT) {
  // 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 = /^\/post\/\d+\/$/.test(path);
  const isSandbox = /^\/sandbox\//.test(path);
  const isUserPosts = /^\/users\/[^/]+\/posts\//.test(path);
  const isUserComments = /^\/users\/[^/]+\/comments\//.test(path);
  const isUserFavorites = /^\/users\/[^/]+\/favorites\//.test(path);
  const isCompanyBlog = /^\/company\/[^/]+\/blog\//.test(path);
  const isCompanyBlog2 = /^\/company\/[^/]+\/(page\d+\/)?$/.test(path);
  const isFeed = /^\/feed\//.test(path);
  const isHome = /^\/$/.test(path);
  const isTop = /^\/top\//.test(path);
  const isAll = /^\/all\//.test(path);

  if (
    isPost || isSandbox ||
    isUserPosts || isUserComments || isUserFavorites ||
    isCompanyBlog || isCompanyBlog2 ||
    isFeed || isHome || isTop || isAll
  ) {
    userStyle += `
      .sidebar_right {
        display: none;
      }

      .content_left {
        padding-right: 0;
      }

      .comment_plain {
        max-width: 860px;
      }
    `;
  }
}

if (FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  userStyle += `
    .user-info__image-pic,
    .user-pic_popover,
    .media-obj__image-pic {
      border-radius: 0;
    }
  `;
}

if (FLAGS.USERSTYLE_USERINFO_BIG_AVATARS) {
  userStyle += `
    .page-header {
      height: auto;
    }

    .media-obj__image-pic_hub,
    .media-obj__image-pic_user,
    .media-obj__image-pic_company {
      width: auto;
      height: auto;
    }
  `;
}

if (FLAGS.USERSTYLE_COMMENTS_FIX) {
  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;
    }

    .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 {
      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;
    }
  `;
}

if (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;
    }
  `;
}

if (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 {
      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;
    }
  `;
}

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'],
    ['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;
    z-index: 101;
    background-color: transparent;
    border-radius: 50%;
    border: 4px solid #aaa;
    border-right-width: 16px;
    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;
  }


  /* 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 {
    background: #171c20;
  }

  /* text */
  .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,
  .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,
  .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 {
    color: #9e9e9e;
  }

  .night .n-dropdown-menu__item-link:hover {
    color: white;
  }

  /* top lvl bg */
  .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 .company-info__author,
  .night .layout__row_footer-links {
    background: #22272B;    
  }

  /* not important bg */
  .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 {
    background: #1f2327;
  }

  /* borders */
  .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 .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 {
    border-color: #393d41;
  }

  .night .poll-result__progress {
    background-color: #515151;
  }

  .night .poll-result__progress_winner {
    background-color: #5e8eac;
  }

  .night .layout__elevator {
    color: #515151;
  }

  .night .layout__elevator:hover {
    background-color: #22272B;
  }

  .night .comment__head_topic-author {
    background: rgba(145, 120, 21, 0.1);
  }

  .night .comment__head_my-comment {
    background: rgba(86, 120, 66, 0.1);
  }

  .night .comment__head_new-comment {
    background: rgba(71, 93, 253, 0.1)
  }

  .night .icon-svg_logo-habrahabr {
    color: inherit;
  }

  /* 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: all .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: 32px;
    height: 25px;
    right: 32px;
    bottom: ${FLAGS.NIGHT_MODE ? 88 : 32}px;
    z-index: 101;
    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: 102;
    border: 1px solid #aaa;
    padding: 8px;
    background: #f7f7f7;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
  .config_frame label:hover {
    cursor: pointer;
    background: rgba(128, 128, 128, 0.3);
  }
  .night .config_frame {
    background: #22272B;
    border-color: #393d41;
  }
  .config_frame span {
    padding-left: 0.3em;
  }
  `;
}


userStyleEl.innerHTML = userStyle;

function readyHead(fn) {
  if (document.body) { // если есть body, значит head готов
    fn();
  } else if (document.documentElement) {
    const observer = new MutationObserver(() => {
      if (document.body) {
        observer.disconnect();
        fn();
      }
    });
    observer.observe(document.documentElement, { childList: true });
  } else {
    // рекурсивное ожидание появления DOM
    setTimeout(() => readyHead(fn), 10);
  }
}

readyHead(() => {
  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 (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);
      });
    }
  }

  // надо ли ещё
  [...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) {
    [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')]
      .forEach((el) => {
        if (FLAGS.GIF_STOP_ONLOAD) toggleGIF(el);
        el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
      });
  }

  // фиксирование высоты публикации чтобы убрать прыжки прокрутки
  if (FLAGS.FIX_JUMPING_SCROLL) {
    const postBodyEl = document.querySelector('.post__body_full');
    const checkPostBodyInViewport = () => postBodyEl.getBoundingClientRect().bottom > 0;
    const autoHeightPost = () => {
      if (checkPostBodyInViewport()) {
        window.removeEventListener('scroll', autoHeightPost);
        postBodyEl.style.height = 'auto';
      }
    };
    if (postBodyEl && !checkPostBodyInViewport()) {
      const h = postBodyEl.clientHeight;
      postBodyEl.style.height = `${h}px`;
      window.addEventListener('scroll', autoHeightPost);
    }
  }

  // счетчики кармы
  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 = '84px'; // eslint-disable-line no-param-reassign
    });
    Array.from(document.querySelectorAll('.stacked-counter[href="/info/help/karma/"]')).forEach((couterEl) => {
      let total = parseInt(couterEl.title, 10);
      const scoreEl = couterEl.querySelector('.stacked-counter__value');
      if (!scoreEl || !total) return;
      couterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
      couterEl.style.minWidth = '84px'; // 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 = `&nbsp;= ${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);
      couterEl.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;
      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 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.el, { childList: true });
    }

    static parse(el) {
      let [total, likes, dislikes] = el
        .attributes.title.textContent
        .match(/[0-9]+/g).map(Number);
      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) {
          details = `&nbsp;= ${this.likes} − ${this.dislikes}`;
        } else {
          const percent = Math.round((100 * this.likes) / this.total);
          details = `&nbsp;= ${this.total} × (${percent} − ${100 - percent})%`;
        }
        innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
      }
      this.el.innerHTML = innerHTML;
    }
  }

  // парсим их
  [...document.querySelectorAll('.voting-wjt__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 = [
    'января', 'февраля', 'марта',
    'апреля', 'мая', 'июня',
    'июля', 'августа', 'сентября',
    'октября', 'ноября', 'декабря',
  ];

  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');
      if (imagination) return imagination;

      const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
      let [,,
        recently, // eslint-disable-line prefer-const
        day, month, year,
        time, // eslint-disable-line prefer-const
      ] = el.innerHTML.match(re);

      // и местное время
      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 = 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)}`;
    }
  }

  // собираем метки времени
  const datesMap = new Map();
  const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
  (megapostTimeEl ? [megapostTimeEl] : [])
    .concat([...document.querySelectorAll(`
        .post__time,
        .preview-data__time-published,
        time.comment__date-time_published,
        .tm-post__date,
        .user-message__date-time
      `)]).forEach((el) => {
      datesMap.set(el, new HabraTime(el));
    });

  function updateTime() {
    datesMap.forEach((habraTime) => {
      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;
    });
  }

  // создаем дерево комментариев
  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_RATING) FLAGS.sortVariants.push(['rating', 'рейтингу']);
  if (FLAGS.COMMENTS_SORT_BY_POPULARITY) FLAGS.sortVariants.push(['popularity', 'популярности']);
  if (FLAGS.COMMENTS_SORT_BY_REDDIT) FLAGS.sortVariants.push(['reddit', 'уверенности']);
  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);
    }
  }

  // сворачивание комментов
  if (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;
      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';
      }
    };
  }
});