Habr.Features

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

目前為 2018-06-24 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Habr.Features
// @version 3.1.18
// @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 GIF_STOP = true;
// остановить гифки при загрузке страницы
const GIF_STOP_ONLOAD = false;
// цвета заглушки
const GIF_STOP_COLOR_FG = 'White'; // White
const GIF_STOP_COLOR_BG = 'LightGray'; // LightGray or WhiteSmoke
// менять src вместо создания-удаления нод
const GIF_STOP_OVERTYPE = true;

// показывать счетчики рейтинга в виде:
// рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
const RATING_DETAILS = true;
// клик мышкой по рейтингу меняет вид на простой \ детальный
const RATING_DETAILS_ONCLICK = false;

const RATING_DETAILS_PN = false;

const KARMA_DETAILS = true;

// показывать метки времени в текущем часовом поясе
// абсолютно, либо относительно текущего времени, либо относительно родительского времени
// меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
const TIME_DETAILS = true;

// добавить возможность сортировки комментариев
const COMMENTS_SORT = true;
// сортировать комменты при загрузке страницы или оставить сортировку по времени
const COMMENTS_SORT_ONLOAD = true;
// список доступных сортировок
const sortVariants = [
  ['time', 'по времени'],
  ['freshness', 'свежести'],
  ['trend', 'трендам'],
  // ['quality', 'качеству'],
  // ['rating', 'рейтингу'],
  // ['popularity', 'популярности'],
  // ['shuffle', 'перемешать'],
];

// добавить возможность сворачивать комментарии
const COMMENTS_HIDE = true;
// свернуть комментарии если их глубина вложенности равна некому числу
const HIDE_LEVEL = 4;
// сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
const LINE_LEN = 8;
// const LINE_LEN = 16;
const REDUCE_NEW_LINES = true;
const RAINBOW_NEW_LINE = true;

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

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

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

const SCROLL_LEGEND = true;

const NIGHT_MODE = true;

// включить разные стили
const USERSTYLE = true;

const USERSTYLE_GIF_HOVER = false;
const USERSTYLE_FEED_DISTANCED = true;
const USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
const USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
const USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
const USERSTYLE_USERINFO_BIG_AVATARS = true;
const USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
const USERSTYLE_COMMENTS_FIX = true;
const USERSTYLE_CODE_FIX = true;
const USERSTYLE_SPOILER_BORDERS = true;
const USERSTYLE_STATIC_STICKY = true;
const USERSTYLE_HLJS_LANG = true;
const USERSTYLE_HLJS_LANG_HOVER = false;
const USERSTYLE_CODE_FONT = 'PT Mono';
const USERSTYLE_CODE_TABSIZE = 2;
const USERSTYLE_CODE_NIGHT = true;

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

if (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 (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 (USERSTYLE_COUNTER_NEW_FIX_SPACE) {
  userStyle += `
    .toggle-menu__item-counter_new {
      margin-left: 4px;
    }
  `;
}

if (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 (USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  userStyle += `
    .user-info__image-pic,
    .user-pic_popover,
    .media-obj__image-pic {
      border-radius: 0;
    }
  `;
}

if (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 (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: -${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 (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 (REDUCE_NEW_LINES) {
  userStyle += `
    .comments_new-line .comments_new-line {
      margin-left: -${(LINE_LEN - 1) * 21}px !important;
    }
  `;
}

if (USERSTYLE_COMMENTS_IMG_MAXSIZE) {
  userStyle += `
    .comment__message img {
      max-height: ${USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
    }

    .comment__message .spoiler .img {
      max-height: auto;
    }
  `;
}

if (USERSTYLE_CODE_FIX) {
  let addFont = '';
  if (USERSTYLE_CODE_FONT) {
    addFont = USERSTYLE_CODE_FONT;
    if (addFont.indexOf(' ') >= 0) {
      addFont = `"${addFont}"`;
    }
    addFont += ',';
  }

  const tabSize = 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 (USERSTYLE_SPOILER_BORDERS) {
  userStyle += `
    .spoiler .spoiler_text {
      border: 1px dashed rgb(12, 174, 251);
    }
  `;
}

if (USERSTYLE_STATIC_STICKY) {
  userStyle += `
    .wrapper-sticky,
    .js-ad_sticky,
    .js-ad_sticky_comments {
      position: static !important;
    }
  `;
}

if (USERSTYLE_HLJS_LANG) {
  let hover = '';
  if (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 (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 .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 .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 {
    -webkit-filter: grayscale(1);
    filter: grayscale(1);
    opacity: .5;
    transition: all .6s ease-in-out
  }

  .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 {
    -webkit-filter: none !important;
    filter: none !important;
    opacity: 1 !important;
    transition-duration: .2s !important
  }

  /* 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
  }
  `;
}

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 (USERSTYLE) document.head.appendChild(userStyleEl);
  if (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 (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='${GIF_STOP_COLOR_BG}'/>
          <circle cx='${cx}' cy='${cy}' r='${r}' fill='${GIF_STOP_COLOR_FG}'/>
          <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${GIF_STOP_COLOR_BG}' />
        </svg>
      `;
      el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
      el.setAttribute('src', svg);
    } else if (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='${GIF_STOP_COLOR_BG}'/>
          <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
          <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
          <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${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 (GIF_STOP) {
    [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')]
      .forEach((el) => {
        if (GIF_STOP_ONLOAD) toggleGIF(el);
        el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
      });
  }

  // фиксирование высоты публикации чтобы убрать прыжки прокрутки
  if (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 (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 (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 (RATING_DETAILS) {
    if (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 (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();

  // здесь начинается сортировка комментариев
  const commentsOrderEl = document.createElement('div');
  commentsOrderEl.classList.add('comments_order');
  commentsOrderEl.innerHTML = sortVariants.map(([type, text]) => {
    const underline = (type === 'time') ? 'style="text-decoration: underline;"' : '';
    return `<a href='#' data-order="${type}" ${underline}>${text}</a>`;
  }).join(', ');

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

  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 (COMMENTS_SORT && 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 (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 (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.setAttribute('href', '#');
      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.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 === HIDE_LEVEL;
      const isLineLvl = subtree.lvl % LINE_LEN === 0;
      if (isLineLvl) {
        subtree.elList.classList.add('comments_new-line');
        const lineNumber = subtree.lvl / LINE_LEN;
        subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
      }
      // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
      if (
        (isHideLvl || isLineLvl) && !subtree.existNew() &&
        !(commentHash && subtree.existId(commentHash))
      ) {
        toggle(subtree);
      }
    });
  }

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