Habr.Features

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

当前为 2018-06-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Habr.Features
  3. // @version 3.1.21
  4. // @description Всякое-разное для Habr aka habr.com
  5. // @author AngReload
  6. // @include https://habr.com/*
  7. // @include http://habr.com/*
  8. // @namespace habr_comments
  9. // @run-at document-start
  10. // @grant none
  11. // @icon https://habr.com/favicon.ico
  12. // ==/UserScript==
  13. /* global localStorage, MutationObserver */
  14.  
  15. // остановка гифок
  16. // клик по гифке заменит картинку на заглушку
  17. // повторный клик вернет гифку на место
  18. const GIF_STOP = true;
  19. // остановить гифки при загрузке страницы
  20. const GIF_STOP_ONLOAD = false;
  21. // цвета заглушки
  22. const GIF_STOP_COLOR_FG = 'White'; // White
  23. const GIF_STOP_COLOR_BG = 'LightGray'; // LightGray or WhiteSmoke
  24. // менять src вместо создания-удаления нод
  25. const GIF_STOP_OVERTYPE = true;
  26.  
  27. // показывать счетчики рейтинга в виде:
  28. // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
  29. const RATING_DETAILS = true;
  30. // клик мышкой по рейтингу меняет вид на простой \ детальный
  31. const RATING_DETAILS_ONCLICK = false;
  32. // рейтинг = число_плюсов - число_минусов (как альтернатива процентам)
  33. const RATING_DETAILS_PN = false;
  34.  
  35. // карма = число_голосовавших * (процент_товарищей - процент_неприятелей)%
  36. const KARMA_DETAILS = true;
  37.  
  38. // показывать метки времени в текущем часовом поясе
  39. // абсолютно, либо относительно текущего времени, либо относительно родительского времени
  40. // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
  41. const TIME_DETAILS = true;
  42.  
  43. // добавить возможность сортировки комментариев
  44. const COMMENTS_SORT = true;
  45. // сортировать комменты при загрузке страницы или оставить сортировку по времени
  46. const COMMENTS_SORT_ONLOAD = true;
  47. // список доступных сортировок
  48. // можно закомментировать строчку, чтобы убрать её из списка
  49. const sortVariants = [
  50. ['time', 'по времени'],
  51. ['freshness', 'свежести'],
  52. ['trend', 'трендам'], // самая полезная сортировка
  53. // ['quality', 'качеству'],
  54. // ['rating', 'рейтингу'],
  55. // ['popularity', 'популярности'],
  56. // ['shuffle', 'перемешать'],
  57. ];
  58.  
  59. // добавить возможность сворачивать комментарии
  60. const COMMENTS_HIDE = true;
  61. // свернуть комментарии если их глубина вложенности равна некому числу
  62. const HIDE_LEVEL = 4;
  63. // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
  64. const LINE_LEN = 8;
  65. // const LINE_LEN = 16;
  66. // отбивать каждый следущий уровень вложенности дополнительным отступом
  67. const REDUCE_NEW_LINES = true;
  68. // глубина вложенности при «возврате каретки» комментариев обозначается разным цветом
  69. const RAINBOW_NEW_LINE = true;
  70.  
  71. // заменить ссылки ведущие к новым комментариям на дерево комментариев
  72. const COMMENTS_LINKS = true;
  73.  
  74. // запоминание галки «Использовать MarkDown» для комментариев
  75. const COMMENTS_MD = true;
  76.  
  77. // в огнелисе, в отличии о хрома, при загрузке изображений не сохраняется прокрутка
  78. // меня раздражает когда дерево комментариев скачками пытается уехать вниз
  79. // эта опция зафиксирует высоту публикации пока та находится вне обзора
  80. // надо переделать
  81. const FIX_JUMPING_SCROLL = false;
  82.  
  83. // линии вдоль прокрутки для отображения размеров поста и комментариев
  84. const SCROLL_LEGEND = true;
  85.  
  86. // добавляет кнопку активации ночного режима
  87. const NIGHT_MODE = true;
  88.  
  89. // включить разные стили
  90. const USERSTYLE = true;
  91. // показывать гифки только при наведении
  92. const USERSTYLE_GIF_HOVER = false;
  93. // лента постов с увеличенными отступами, нижний бар выровнен вправо
  94. const USERSTYLE_FEED_DISTANCED = true;
  95. // мелкий фикс для отступа в счетчике ленты
  96. const USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
  97. // удаляет правую колонку, там где не жалко
  98. const USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
  99. // аватарки без скруглений
  100. const USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
  101. // большие аватарки в профиле и карточках
  102. const USERSTYLE_USERINFO_BIG_AVATARS = true;
  103. // ограничить размер картинок в комментах
  104. const USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
  105. // нормальные стили для комментариев
  106. const USERSTYLE_COMMENTS_FIX = true;
  107. // нормальные стили для кода
  108. const USERSTYLE_CODE_FIX = true;
  109. // окантовка границ спойлеров
  110. const USERSTYLE_SPOILER_BORDERS = true;
  111. // делает глючные плавающие блоки статичными
  112. const USERSTYLE_STATIC_STICKY = true;
  113. // показывать язык подсветки блоков кода
  114. const USERSTYLE_HLJS_LANG = true;
  115. // показывать язык кода только при наведении
  116. const USERSTYLE_HLJS_LANG_HOVER = false;
  117. // свой шрифт для блоков кода
  118. const USERSTYLE_CODE_FONT = 'PT Mono';
  119. // размер табов в коде
  120. const USERSTYLE_CODE_TABSIZE = 2;
  121. // для ночного режима
  122. const USERSTYLE_CODE_NIGHT = true;
  123.  
  124. // интерфейс для хранения настроек
  125. const userConfig = {
  126. // имя записи в localsorage
  127. key: 'habrafix',
  128. // модель настроек: ключ - возможные значения
  129. model: {
  130. time_publications: ['fromNow', 'absolute'],
  131. time_comments: ['fromParent', 'fromNow', 'absolute'],
  132. comments_order: ['trend', 'time'],
  133. scores_details: [true, false],
  134. comment_markdown: [false, true],
  135. night_mode: [false, true],
  136. },
  137. config: {},
  138. // при старте для конфига берем сохраненные параметры либо по умолчанию
  139. init() {
  140. let jsonString = localStorage.getItem(userConfig.key);
  141. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  142. const loadedKeys = Object.keys(loadedConfig);
  143. const config = {};
  144. Object.keys(userConfig.model).forEach((key) => {
  145. const exist = loadedKeys.indexOf(key) >= 0;
  146. config[key] = exist ? loadedConfig[key] : userConfig.model[key][0];
  147. });
  148. jsonString = JSON.stringify(config);
  149. localStorage.setItem(userConfig.key, jsonString);
  150. userConfig.config = config;
  151. },
  152. getItem(key) {
  153. const jsonString = localStorage.getItem(userConfig.key);
  154. const config = JSON.parse(jsonString);
  155. return config[key];
  156. // return userConfig.config[key];
  157. },
  158. setItem(key, value) {
  159. let jsonString = localStorage.getItem(userConfig.key);
  160. const config = JSON.parse(jsonString);
  161. config[key] = value;
  162. jsonString = JSON.stringify(config);
  163. localStorage.setItem(userConfig.key, jsonString);
  164. userConfig.config = config;
  165. },
  166. // каруселит параметр по значения модели
  167. shiftItem(key) {
  168. const currentValue = userConfig.getItem(key);
  169. const availableValues = userConfig.model[key];
  170. const currentIdx = availableValues.indexOf(currentValue);
  171. const nextIdx = (currentIdx + 1) % availableValues.length;
  172. const nextValue = availableValues[nextIdx];
  173. userConfig.setItem(key, nextValue);
  174. return nextValue;
  175. },
  176. };
  177. userConfig.init();
  178.  
  179. // свои стили
  180. const userStyleEl = document.createElement('style');
  181. let userStyle = '';
  182.  
  183. if (USERSTYLE_GIF_HOVER) {
  184. userStyle += `
  185. .post__text img[src$=".gif"]:hover,
  186. .comment__message img[src$=".gif"]:hover {
  187. opacity: 0.75;
  188. }
  189. `;
  190. }
  191.  
  192. if (SCROLL_LEGEND) {
  193. userStyle += `
  194. .legend_el {
  195. position: fixed;
  196. width: 4px;
  197. right: 0;
  198. transition: top 1s ease-out, height 1s ease-out;
  199. z-index: 101;
  200. }
  201.  
  202. #xpanel {
  203. right: 4px;
  204. }
  205. `;
  206. }
  207.  
  208. if (USERSTYLE_FEED_DISTANCED) {
  209. userStyle += `
  210. .post__body_crop {
  211. text-align: right;
  212. }
  213.  
  214. .post__body_crop .post__text {
  215. text-align: left;
  216. }
  217.  
  218. .post__footer {
  219. text-align: right;
  220. }
  221.  
  222. .posts_list .content-list__item_post {
  223. padding: 40px 0;
  224. }
  225. `;
  226. }
  227.  
  228. if (USERSTYLE_COUNTER_NEW_FIX_SPACE) {
  229. userStyle += `
  230. .toggle-menu__item-counter_new {
  231. margin-left: 4px;
  232. }
  233. `;
  234. }
  235.  
  236. if (USERSTYLE_REMOVE_SIDEBAR_RIGHT) {
  237. // remove for
  238. // https://habr.com/post/352896/
  239. // https://habr.com/sandbox/
  240. // https://habr.com/sandbox/115216/
  241. // https://habr.com/users/saggid/posts/
  242. // https://habr.com/users/saggid/comments/
  243. // https://habr.com/users/saggid/favorites/
  244. // https://habr.com/users/saggid/favorites/posts/
  245. // https://habr.com/users/saggid/favorites/comments/
  246. // https://habr.com/company/pvs-studio/blog/353640/
  247. // https://habr.com/company/pvs-studio/blog/
  248. // https://habr.com/company/pvs-studio/blog/top/
  249. // https://habr.com/company/pvs-studio/
  250. // https://habr.com/feed/
  251. // https://habr.com/top/
  252. // https://habr.com/top/yearly/
  253. // https://habr.com/all/
  254. // https://habr.com/all/top10/
  255.  
  256. // display for
  257. // https://habr.com/company/pvs-studio/profile/
  258. // https://habr.com/company/pvs-studio/vacancies/
  259. // https://habr.com/company/pvs-studio/fans/all/rating/
  260. // https://habr.com/company/pvs-studio/workers/new/rating/
  261. // https://habr.com/feed/settings/
  262. // https://habr.com/users/
  263. // https://habr.com/hubs/
  264. // https://habr.com/hubs/admin/
  265. // https://habr.com/companies/
  266. // https://habr.com/companies/category/software/
  267. // https://habr.com/companies/new/
  268. // https://habr.com/flows/design/
  269.  
  270. const path = window.location.pathname;
  271. const isPost = /^\/post\/\d+\/$/.test(path);
  272. const isSandbox = /^\/sandbox\//.test(path);
  273. const isUserPosts = /^\/users\/[^/]+\/posts\//.test(path);
  274. const isUserComments = /^\/users\/[^/]+\/comments\//.test(path);
  275. const isUserFavorites = /^\/users\/[^/]+\/favorites\//.test(path);
  276. const isCompanyBlog = /^\/company\/[^/]+\/blog\//.test(path);
  277. const isCompanyBlog2 = /^\/company\/[^/]+\/(page\d+\/)?$/.test(path);
  278. const isFeed = /^\/feed\//.test(path);
  279. const isHome = /^\/$/.test(path);
  280. const isTop = /^\/top\//.test(path);
  281. const isAll = /^\/all\//.test(path);
  282.  
  283. if (
  284. isPost || isSandbox ||
  285. isUserPosts || isUserComments || isUserFavorites ||
  286. isCompanyBlog || isCompanyBlog2 ||
  287. isFeed || isHome || isTop || isAll
  288. ) {
  289. userStyle += `
  290. .sidebar_right {
  291. display: none;
  292. }
  293.  
  294. .content_left {
  295. padding-right: 0;
  296. }
  297.  
  298. .comment_plain {
  299. max-width: 860px;
  300. }
  301. `;
  302. }
  303. }
  304.  
  305. if (USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  306. userStyle += `
  307. .user-info__image-pic,
  308. .user-pic_popover,
  309. .media-obj__image-pic {
  310. border-radius: 0;
  311. }
  312. `;
  313. }
  314.  
  315. if (USERSTYLE_USERINFO_BIG_AVATARS) {
  316. userStyle += `
  317. .page-header {
  318. height: auto;
  319. }
  320.  
  321. .media-obj__image-pic_hub,
  322. .media-obj__image-pic_user,
  323. .media-obj__image-pic_company {
  324. width: auto;
  325. height: auto;
  326. }
  327. `;
  328. }
  329.  
  330. if (USERSTYLE_COMMENTS_FIX) {
  331. userStyle += `
  332. .comments_order {
  333. color: #333;
  334. font-size: 14px;
  335. font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif;
  336. text-rendering: optimizeLegibility;
  337. border-bottom: 1px solid #e3e3e3;
  338. padding: 8px;
  339. text-align: right;
  340. }
  341.  
  342. .comments_order a {
  343. color: #548eaa;
  344. font-style: normal;
  345. text-decoration: none;
  346. }
  347.  
  348. .comments_order a:hover {
  349. color: #487284;
  350. }
  351.  
  352. .content-list_comments {
  353. overflow: visible;
  354. }
  355.  
  356. .comment__folding-dotholder {
  357. display: none !important;
  358. }
  359.  
  360. .content-list_nested-comments {
  361. border-left: 1px solid #e3e3e3;
  362. margin: 0;
  363. padding-top: 20px;
  364. padding-left: 20px !important;
  365. }
  366.  
  367. .content-list_comments {
  368. /*border-left: 1px solid silver;*/
  369. margin: 0;
  370. padding-left: 0;
  371. padding-top: 20px;
  372. /*background: #FCE4EC;*/
  373. }
  374.  
  375. #comments-list .js-form_placeholder {
  376. border-left: 1px solid #e3e3e3;
  377. padding-left: 20px;
  378. }
  379.  
  380. .comments_new-line {
  381. border-left: 1px solid #777;
  382. border-bottom: 1px solid #777;
  383. border-top: 1px solid #777;
  384. margin-left: -${LINE_LEN * 21}px !important;
  385. background: white;
  386. padding-bottom: 4px;
  387. }
  388.  
  389. /* .comment__head_topic-author.comment__head_new-comment */
  390. .comment__head_topic-author .user-info {
  391. text-decoration: underline;
  392. }
  393. `;
  394. }
  395.  
  396. if (RAINBOW_NEW_LINE) {
  397. userStyle += `
  398. .comments_new-line-1 {
  399. border-color: #0caefb;
  400. }
  401.  
  402. .comments_new-line-2 {
  403. border-color: #06feb7;
  404. }
  405.  
  406. .comments_new-line-3 {
  407. border-color: #fbcb02;
  408. }
  409.  
  410. .comments_new-line-0 {
  411. border-color: #fb0543;
  412. }
  413. `;
  414. }
  415.  
  416. if (REDUCE_NEW_LINES) {
  417. userStyle += `
  418. .comments_new-line .comments_new-line {
  419. margin-left: -${(LINE_LEN - 1) * 21}px !important;
  420. }
  421. `;
  422. }
  423.  
  424. if (USERSTYLE_COMMENTS_IMG_MAXSIZE) {
  425. userStyle += `
  426. .comment__message img {
  427. max-height: ${USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
  428. }
  429.  
  430. .comment__message .spoiler .img {
  431. max-height: auto;
  432. }
  433. `;
  434. }
  435.  
  436. if (USERSTYLE_CODE_FIX) {
  437. let addFont = '';
  438. if (USERSTYLE_CODE_FONT) {
  439. addFont = USERSTYLE_CODE_FONT;
  440. if (addFont.indexOf(' ') >= 0) {
  441. addFont = `"${addFont}"`;
  442. }
  443. addFont += ',';
  444. }
  445.  
  446. const tabSize = USERSTYLE_CODE_TABSIZE || 4;
  447.  
  448. userStyle += `
  449. .editor .text-holder textarea,
  450. .tm-editor__textarea {
  451. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace;
  452. }
  453.  
  454. code {
  455. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;;
  456. -o-tab-size: ${tabSize};
  457. -moz-tab-size: ${tabSize};
  458. tab-size: ${tabSize};
  459. background: #f7f7f7;
  460. border-radius: 3px;
  461. color: #505c66;
  462. display: inline-block;
  463. font-weight: 500;
  464. line-height: 1.29;
  465. padding: 5px 9px;
  466. vertical-align: 1px;
  467. }
  468. `;
  469. }
  470.  
  471. if (USERSTYLE_SPOILER_BORDERS) {
  472. userStyle += `
  473. .spoiler .spoiler_text {
  474. border: 1px dashed rgb(12, 174, 251);
  475. }
  476. `;
  477. }
  478.  
  479. if (USERSTYLE_STATIC_STICKY) {
  480. userStyle += `
  481. .wrapper-sticky,
  482. .js-ad_sticky,
  483. .js-ad_sticky_comments {
  484. position: static !important;
  485. }
  486. `;
  487. }
  488.  
  489. if (USERSTYLE_HLJS_LANG) {
  490. let hover = '';
  491. if (USERSTYLE_HLJS_LANG_HOVER) hover = ':hover';
  492. userStyle += `
  493. pre {
  494. position: relative;
  495. }
  496.  
  497. .hljs${hover}::after {
  498. position: absolute;
  499. font-size: 12px;
  500. content: 'code';
  501. right: 0;
  502. top: 0;
  503. padding: 1px 5px 0 4px;
  504. /*border-bottom: 1px solid #e5e8ec;
  505. border-left: 1px solid #e5e8ec;
  506. border-bottom-left-radius: 3px;
  507. color: #505c66;*/
  508. opacity: .5;
  509. }
  510. `;
  511. userStyle += [
  512. ['1c', '1C:Enterprise (v7, v8)'],
  513. ['abnf', 'Augmented Backus-Naur Form'],
  514. ['accesslog', 'Access log'],
  515. ['actionscript', 'ActionScript'],
  516. ['ada', 'Ada'],
  517. ['apache', 'Apache'],
  518. ['applescript', 'AppleScript'],
  519. ['arduino', 'Arduino'],
  520. ['armasm', 'ARM Assembly'],
  521. ['asciidoc', 'AsciiDoc'],
  522. ['aspectj', 'AspectJ'],
  523. ['autohotkey', 'AutoHotkey'],
  524. ['autoit', 'AutoIt'],
  525. ['avrasm', 'AVR Assembler'],
  526. ['awk', 'Awk'],
  527. ['axapta', 'Axapta'],
  528. ['bash', 'Bash'],
  529. ['basic', 'Basic'],
  530. ['bnf', 'Backus–Naur Form'],
  531. ['brainfuck', 'Brainfuck'],
  532. ['cal', 'C/AL'],
  533. ['capnproto', 'Cap’n Proto'],
  534. ['ceylon', 'Ceylon'],
  535. ['clean', 'Clean'],
  536. ['clojure-repl', 'Clojure REPL'],
  537. ['clojure', 'Clojure'],
  538. ['cmake', 'CMake'],
  539. ['coffeescript', 'CoffeeScript'],
  540. ['coq', 'Coq'],
  541. ['cos', 'Caché Object Script'],
  542. ['cpp', 'C++'],
  543. ['crmsh', 'crmsh'],
  544. ['crystal', 'Crystal'],
  545. ['cs', 'C#'],
  546. ['csp', 'CSP'],
  547. ['css', 'CSS'],
  548. ['d', 'D'],
  549. ['dart', 'Dart'],
  550. ['delphi', 'Delphi'],
  551. ['diff', 'Diff'],
  552. ['django', 'Django'],
  553. ['dns', 'DNS Zone file'],
  554. ['dockerfile', 'Dockerfile'],
  555. ['dos', 'DOS .bat'],
  556. ['dsconfig', 'dsconfig'],
  557. ['dts', 'Device Tree'],
  558. ['dust', 'Dust'],
  559. ['ebnf', 'Extended Backus-Naur Form'],
  560. ['elixir', 'Elixir'],
  561. ['elm', 'Elm'],
  562. ['erb', 'ERB (Embedded Ruby)'],
  563. ['erlang-repl', 'Erlang REPL'],
  564. ['erlang', 'Erlang'],
  565. ['excel', 'Excel'],
  566. ['fix', 'FIX'],
  567. ['flix', 'Flix'],
  568. ['fortran', 'Fortran'],
  569. ['fsharp', 'F#'],
  570. ['gams', 'GAMS'],
  571. ['gauss', 'GAUSS'],
  572. ['gcode', 'G-code (ISO 6983)'],
  573. ['gherkin', 'Gherkin'],
  574. ['glsl', 'GLSL'],
  575. ['go', 'Go'],
  576. ['golo', 'Golo'],
  577. ['gradle', 'Gradle'],
  578. ['groovy', 'Groovy'],
  579. ['haml', 'Haml'],
  580. ['handlebars', 'Handlebars'],
  581. ['haskell', 'Haskell'],
  582. ['haxe', 'Haxe'],
  583. ['hsp', 'HSP'],
  584. ['htmlbars', 'HTMLBars'],
  585. ['http', 'HTTP'],
  586. ['hy', 'Hy'],
  587. ['inform7', 'Inform 7'],
  588. ['ini', 'Ini'],
  589. ['irpf90', 'IRPF90'],
  590. ['java', 'Java'],
  591. ['javascript', 'JavaScript'],
  592. ['jboss-cli', 'jboss-cli'],
  593. ['json', 'JSON'],
  594. ['julia-repl', 'Julia REPL'],
  595. ['julia', 'Julia'],
  596. ['kotlin', 'Kotlin'],
  597. ['lasso', 'Lasso'],
  598. ['ldif', 'LDIF'],
  599. ['leaf', 'Leaf'],
  600. ['less', 'Less'],
  601. ['lisp', 'Lisp'],
  602. ['livecodeserver', 'LiveCode'],
  603. ['livescript', 'LiveScript'],
  604. ['llvm', 'LLVM IR'],
  605. ['lsl', 'Linden Scripting Language'],
  606. ['lua', 'Lua'],
  607. ['makefile', 'Makefile'],
  608. ['markdown', 'Markdown'],
  609. ['mathematica', 'Mathematica'],
  610. ['matlab', 'Matlab'],
  611. ['maxima', 'Maxima'],
  612. ['mel', 'MEL'],
  613. ['mercury', 'Mercury'],
  614. ['mipsasm', 'MIPS Assembly'],
  615. ['mizar', 'Mizar'],
  616. ['mojolicious', 'Mojolicious'],
  617. ['monkey', 'Monkey'],
  618. ['moonscript', 'MoonScript'],
  619. ['n1ql', 'N1QL'],
  620. ['nginx', 'Nginx'],
  621. ['nimrod', 'Nimrod'],
  622. ['nix', 'Nix'],
  623. ['nsis', 'NSIS'],
  624. ['objectivec', 'Objective-C'],
  625. ['ocaml', 'OCaml'],
  626. ['openscad', 'OpenSCAD'],
  627. ['oxygene', 'Oxygene'],
  628. ['parser3', 'Parser3'],
  629. ['perl', 'Perl'],
  630. ['pf', 'pf'],
  631. ['php', 'PHP'],
  632. ['pony', 'Pony'],
  633. ['powershell', 'PowerShell'],
  634. ['processing', 'Processing'],
  635. ['profile', 'Python profile'],
  636. ['prolog', 'Prolog'],
  637. ['protobuf', 'Protocol Buffers'],
  638. ['puppet', 'Puppet'],
  639. ['purebasic', 'PureBASIC'],
  640. ['python', 'Python'],
  641. ['q', 'Q'],
  642. ['qml', 'QML'],
  643. ['r', 'R'],
  644. ['rib', 'RenderMan RIB'],
  645. ['roboconf', 'Roboconf'],
  646. ['routeros', 'Microtik RouterOS script'],
  647. ['rsl', 'RenderMan RSL'],
  648. ['ruby', 'Ruby'],
  649. ['ruleslanguage', 'Oracle Rules Language'],
  650. ['rust', 'Rust'],
  651. ['scala', 'Scala'],
  652. ['scheme', 'Scheme'],
  653. ['scilab', 'Scilab'],
  654. ['scss', 'SCSS'],
  655. ['shell', 'Shell Session'],
  656. ['smali', 'Smali'],
  657. ['smalltalk', 'Smalltalk'],
  658. ['sml', 'SML'],
  659. ['sqf', 'SQF'],
  660. ['sql', 'SQL'],
  661. ['stan', 'Stan'],
  662. ['stata', 'Stata'],
  663. ['step21', 'STEP Part 21'],
  664. ['stylus', 'Stylus'],
  665. ['subunit', 'SubUnit'],
  666. ['swift', 'Swift'],
  667. ['taggerscript', 'Tagger Script'],
  668. ['tap', 'Test Anything Protocol'],
  669. ['tcl', 'Tcl'],
  670. ['tex', 'TeX'],
  671. ['thrift', 'Thrift'],
  672. ['tp', 'TP'],
  673. ['twig', 'Twig'],
  674. ['typescript', 'TypeScript'],
  675. ['vala', 'Vala'],
  676. ['vbnet', 'VB.NET'],
  677. ['vbscript-html', 'VBScript in HTML'],
  678. ['vbscript', 'VBScript'],
  679. ['verilog', 'Verilog'],
  680. ['vhdl', 'VHDL'],
  681. ['vim', 'Vim Script'],
  682. ['x86asm', 'Intel x86 Assembly'],
  683. ['xl', 'XL'],
  684. ['xml', 'HTML, XML'],
  685. ['xquery', 'XQuery'],
  686. ['yaml', 'YAML'],
  687. ['zephir', 'Zephir'],
  688. ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join('');
  689. }
  690.  
  691. if (USERSTYLE_CODE_NIGHT) {
  692. userStyle += `
  693. .night_mode_switcher {
  694. box-sizing: border-box;
  695. position: fixed;
  696. width: 32px;
  697. height: 32px;
  698. right: 32px;
  699. bottom: 32px;
  700. z-index: 101;
  701. background-color: transparent;
  702. border-radius: 50%;
  703. border: 4px solid #aaa;
  704. border-right-width: 16px;
  705. transition: border-color 0.1s ease-out;
  706. }
  707.  
  708. .night_mode_switcher:hover {
  709. border-color: #333;
  710. }
  711.  
  712. .night .night_mode_switcher {
  713. border-color: #515151;
  714. }
  715.  
  716. .night .night_mode_switcher:hover {
  717. border-color: #9e9e9e;
  718. }
  719.  
  720.  
  721. /* bg */
  722. .night .sidebar-block__suggest,
  723. .night .dropdown-container,
  724. .night .poll-result__bar,
  725. .night .comments_new-line,
  726. .night .tm-editor__textarea,
  727. .night .layout,
  728. .night .toggle-menu__most-read,
  729. .night .toggle-menu_most-comments {
  730. background: #171c20;
  731. }
  732.  
  733. /* text */
  734. .night .sidebar-block__suggest,
  735. .night .user-message__body,
  736. .night .promo-block__title_total,
  737. .night .beta-anounce__text,
  738. .night .defination-list__label,
  739. .night .defination-list__value,
  740. .night .search-field__select,
  741. .night .search-field__input[type="text"],
  742. .night .search-form__field,
  743. .night .post-info__title,
  744. .night .dropdown__user-stats,
  745. .night .dropdown-container_white .user-info__special,
  746. .night .n-dropdown-menu__item-link,
  747. .night body,
  748. .night .default-block__polling-title,
  749. .night .poll-result__data-label,
  750. .night code,
  751. .night .user-info__fullname,
  752. .night .user-info__specialization,
  753. .night .page-header__info-title,
  754. .night .page-header__info-desc,
  755. .night .post__title-text,
  756. .night .post__title_link,
  757. .night .checkbox__label,
  758. .night .radio__label,
  759. .night .tm-editor__textarea,
  760. .night .footer-block__title,
  761. .night #TMpanel .container .bmenu > a.current,
  762. .night .post__text-html,
  763. .night .comment__message,
  764. .night .comment-form__preview {
  765. color: #9e9e9e;
  766. }
  767.  
  768. .night .n-dropdown-menu__item-link:hover {
  769. color: white;
  770. }
  771.  
  772. /* top lvl bg */
  773. .night .checkbox__label::before,
  774. .night .radio__label::before,
  775. .night .content-list__item_conversation:hover,
  776. .night .search-field__select,
  777. .night .search-field__input[type="text"],
  778. .night .search-form__field,
  779. .night .dropdown-container,
  780. .night .n-dropdown-menu,
  781. .night .post__translatation,
  782. .night code,
  783. .night .megapost-teasers,
  784. .night .tm-editor_comments,
  785. .night .promo-block__header,
  786. .night .post__text-html blockquote,
  787. .night .default-block,
  788. .night .post-share,
  789. .night .company-info__author,
  790. .night .layout__row_footer-links {
  791. background: #22272B;
  792. }
  793.  
  794. /* not important bg */
  795. .night .btn_blue.disabled,
  796. .night .btn_blue[disabled],
  797. .night .tracker_page table.tracker_folowers tr.new,
  798. .night .dropdown__user-stats,
  799. .night .comment__head_topic-author,
  800. .night .promo-item:hover,
  801. .night .layout__row_navbar,
  802. .night .layout__row_footer,
  803. .night #TMpanel {
  804. background: #1f2327;
  805. }
  806.  
  807. /* borders */
  808. .night #comments-list .js-form_placeholder,
  809. .night .sidebar-block__suggest,
  810. .night .content-list_preview-message,
  811. .night .btn_outline_blue[disabled],
  812. .night .user-message__body_html pre code,
  813. .night .content-list_user-dialog,
  814. .night .wysiwyg-toolbar,
  815. .night .content-list__item_bordered,
  816. .night .promo-block__total,
  817. .night .search-field__select,
  818. .night .search-field__input[type="text"],
  819. .night .search-form__field,
  820. .night .tracker_page table.tracker_folowers tr td,
  821. .night .tracker_page table.tracker_folowers tr th,
  822. .night .stacked-menu__item_devided,
  823. .night .post__text-html table,
  824. .night .post__text-html table td,
  825. .night .post__text-html table th,
  826. .night .n-dropdown-menu__item_border,
  827. .night .dropdown-container,
  828. .night .default-block_bordered,
  829. .night .default_block_polling,
  830. .night .column-wrapper_tabs .sidebar_right,
  831. .night .post__type-label,
  832. .night .promo-block__header,
  833. .night .user-info__contacts,
  834. .night .comment__message pre code,
  835. .night .comment-form__preview pre code,
  836. .night .sandbox-panel,
  837. .night .comment__post-title,
  838. .night .tm-editor__textarea,
  839. .night .promo-block__footer,
  840. .night .author-panel,
  841. .night .promo-block,
  842. .night .post__text-html pre code,
  843. .night .footer-block__title,
  844. .night #TMpanel,
  845. .night .layout__row_navbar,
  846. .night .page-header_bordered,
  847. .night .post-stats,
  848. .night .company-info__about,
  849. .night .company-info_post-additional,
  850. .night .company-info__contacts,
  851. .night .post-share,
  852. .night .content-list__item_devided,
  853. .night .comments_order,
  854. .night .comments-section__head,
  855. .night .content-list_nested-comments,
  856. .night .default-block__header,
  857. .night .column-wrapper_bordered,
  858. .night .tabs-menu,
  859. .night .toggle-menu {
  860. border-color: #393d41;
  861. }
  862.  
  863. .night .poll-result__progress {
  864. background-color: #515151;
  865. }
  866.  
  867. .night .poll-result__progress_winner {
  868. background-color: #5e8eac;
  869. }
  870.  
  871. .night .layout__elevator {
  872. color: #515151;
  873. }
  874.  
  875. .night .layout__elevator:hover {
  876. background-color: #22272B;
  877. }
  878.  
  879. .night .comment__head_topic-author {
  880. background: rgba(145, 120, 21, 0.1);
  881. }
  882.  
  883. .night .comment__head_my-comment {
  884. background: rgba(86, 120, 66, 0.1);
  885. }
  886.  
  887. .night .comment__head_new-comment {
  888. background: rgba(71, 93, 253, 0.1)
  889. }
  890.  
  891. .night .icon-svg_logo-habrahabr {
  892. color: inherit;
  893. }
  894.  
  895. /* img filter */
  896. .night .comment__message img,
  897. .night .comment-form__preview img,
  898. .night .default-block__content #facebook_like_box,
  899. .night .default-block__content #vk_groups,
  900. .night .post img,
  901. .night .page-header__banner img,
  902. .night .company_top_banner img,
  903. .night img .teaser__image,
  904. .night .teaser__image-pic,
  905. .night .article__body img {
  906. filter: brightness(0.5);
  907. transition: all .6s ease-out;
  908. }
  909.  
  910. .night .comment__message img:hover,
  911. .night .comment-form__preview img:hover,
  912. .night .default-block__content #facebook_like_box:hover,
  913. .night .default-block__content #vk_groups:hover,
  914. .night img[alt="en"],
  915. .night img[alt="habr"],
  916. .night img:hover,
  917. .night a.post-author__link img,
  918. .night img.user-info__image-pic,
  919. .night .teaser__image-pic:hover,
  920. .night .teaser__image:hover {
  921. filter: brightness(1.0);
  922. }
  923.  
  924. /* Atelier Cave Dark */
  925. .night .hljs-comment,
  926. .night .hljs-quote {
  927. color:#7e7887 !important
  928. }
  929. .night .hljs-variable,
  930. .night .hljs-template-variable,
  931. .night .hljs-attribute,
  932. .night .hljs-regexp,
  933. .night .hljs-link,
  934. .night .hljs-tag,
  935. .night .hljs-name,
  936. .night .hljs-selector-id,
  937. .night .hljs-selector-class {
  938. color:#be4678 !important
  939. }
  940. .night .hljs-number,
  941. .night .hljs-meta,
  942. .night .hljs-built_in,
  943. .night .hljs-builtin-name,
  944. .night .hljs-literal,
  945. .night .hljs-type,
  946. .night .hljs-params {
  947. color:#aa573c !important
  948. }
  949. .night .hljs-string,
  950. .night .hljs-symbol,
  951. .night .hljs-bullet {
  952. color:#2a9292 !important
  953. }
  954. .night .hljs-title,
  955. .night .hljs-section {
  956. color:#576ddb !important
  957. }
  958. .night .hljs-keyword,
  959. .night .hljs-selector-tag {
  960. color:#955ae7 !important
  961. }
  962. .night .hljs-deletion,
  963. .night .hljs-addition {
  964. color:#19171c !important;
  965. display:inline-block !important;
  966. width:100% !important
  967. }
  968. .night .hljs-deletion {
  969. background-color:#be4678 !important
  970. }
  971. .night .hljs-addition {
  972. background-color:#2a9292 !important
  973. }
  974. .night .hljs {
  975. display:block !important;
  976. overflow-x:auto !important;
  977. background:#19171c !important;
  978. color:#8b8792 !important;
  979. /*padding:0.5em !important*/
  980. }
  981. .night .hljs-emphasis {
  982. font-style:italic !important
  983. }
  984. .night .hljs-strong {
  985. font-weight:bold !important
  986. }
  987. `;
  988. }
  989.  
  990. userStyleEl.innerHTML = userStyle;
  991.  
  992. function readyHead(fn) {
  993. if (document.body) { // если есть body, значит head готов
  994. fn();
  995. } else if (document.documentElement) {
  996. const observer = new MutationObserver(() => {
  997. if (document.body) {
  998. observer.disconnect();
  999. fn();
  1000. }
  1001. });
  1002. observer.observe(document.documentElement, { childList: true });
  1003. } else {
  1004. // рекурсивное ожидание появления DOM
  1005. setTimeout(() => readyHead(fn), 10);
  1006. }
  1007. }
  1008.  
  1009. readyHead(() => {
  1010. if (USERSTYLE) document.head.appendChild(userStyleEl);
  1011. if (NIGHT_MODE && userConfig.getItem('night_mode')) {
  1012. document.documentElement.classList.add('night');
  1013. }
  1014. });
  1015.  
  1016. function ready(fn) {
  1017. const { readyState } = document;
  1018. if (readyState === 'loading') {
  1019. document.addEventListener('DOMContentLoaded', () => {
  1020. fn();
  1021. });
  1022. } else {
  1023. fn();
  1024. }
  1025. }
  1026.  
  1027. ready(() => {
  1028. if (COMMENTS_MD) {
  1029. const mdSelectorEl = document.getElementById('comment_markdown');
  1030. if (mdSelectorEl) {
  1031. if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true;
  1032. mdSelectorEl.addEventListener('input', () => {
  1033. userConfig.setItem('comment_markdown', mdSelectorEl.checked);
  1034. });
  1035. }
  1036. }
  1037.  
  1038. // надо ли ещё
  1039. [...document.querySelectorAll('iframe[src^="https://codepen.io/"]')]
  1040. .map(el => el.setAttribute('scrolling', 'no'));
  1041.  
  1042. // остановка гифок по клику и воспроизведение при повторном клике
  1043. function toggleGIF(el) {
  1044. // если атрибут со старым линком пуст или отсутствует
  1045. if (!el.dataset.oldSrc) {
  1046. // заменим ссылку на data-url-svg с треугольником в круге
  1047. const w = Math.max(el.clientWidth || 256, 16);
  1048. const h = Math.max(el.clientHeight || 128, 16);
  1049. const cx = w / 2;
  1050. const cy = h / 2;
  1051. const r = Math.min(w, h) / 4;
  1052. const ax = (r * 61) / 128;
  1053. const by = (r * 56) / 128;
  1054. const bx = (r * 35) / 128;
  1055. const svg = `data:image/svg+xml;utf8,
  1056. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1057. <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/>
  1058. <circle cx='${cx}' cy='${cy}' r='${r}' fill='${GIF_STOP_COLOR_FG}'/>
  1059. <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${GIF_STOP_COLOR_BG}' />
  1060. </svg>
  1061. `;
  1062. el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
  1063. el.setAttribute('src', svg);
  1064. } else if (GIF_STOP_OVERTYPE) {
  1065. // иначе поставим svg с троеточием
  1066. const w = el.clientWidth;
  1067. const h = el.clientHeight;
  1068. const cx = w / 2;
  1069. const cy = h / 2;
  1070. const r = Math.min(w, h) / 4;
  1071. const r2 = r / 4;
  1072. const svg = `data:image/svg+xml;utf8,
  1073. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1074. <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/>
  1075. <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  1076. <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  1077. <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  1078. </svg>
  1079. `;
  1080. el.setAttribute('src', svg);
  1081. // когда отрендерится троеточие, можно менять на исходную гифку
  1082. setTimeout(() => {
  1083. if (el.dataset.oldSrc) {
  1084. el.setAttribute('src', el.dataset.oldSrc);
  1085. el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign
  1086. }
  1087. }, 100);
  1088. } else {
  1089. const img = document.createElement('img');
  1090. img.setAttribute('src', el.dataset.oldSrc);
  1091. if (el.hasAttribute('align')) {
  1092. img.setAttribute('align', el.getAttribute('align'));
  1093. }
  1094. el.parentNode.insertBefore(img, el);
  1095. img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign
  1096. el.parentNode.removeChild(el);
  1097. }
  1098. }
  1099.  
  1100. if (GIF_STOP) {
  1101. [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')]
  1102. .forEach((el) => {
  1103. if (GIF_STOP_ONLOAD) toggleGIF(el);
  1104. el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
  1105. });
  1106. }
  1107.  
  1108. // фиксирование высоты публикации чтобы убрать прыжки прокрутки
  1109. if (FIX_JUMPING_SCROLL) {
  1110. const postBodyEl = document.querySelector('.post__body_full');
  1111. const checkPostBodyInViewport = () => postBodyEl.getBoundingClientRect().bottom > 0;
  1112. const autoHeightPost = () => {
  1113. if (checkPostBodyInViewport()) {
  1114. window.removeEventListener('scroll', autoHeightPost);
  1115. postBodyEl.style.height = 'auto';
  1116. }
  1117. };
  1118. if (postBodyEl && !checkPostBodyInViewport()) {
  1119. const h = postBodyEl.clientHeight;
  1120. postBodyEl.style.height = `${h}px`;
  1121. window.addEventListener('scroll', autoHeightPost);
  1122. }
  1123. }
  1124.  
  1125. // счетчики кармы
  1126. if (KARMA_DETAILS) {
  1127. Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => {
  1128. itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign
  1129. });
  1130. Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => {
  1131. karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1132. karmaEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  1133. });
  1134. Array.from(document.querySelectorAll('.stacked-counter[href="/info/help/karma/"]')).forEach((couterEl) => {
  1135. let total = parseInt(couterEl.title, 10);
  1136. const scoreEl = couterEl.querySelector('.stacked-counter__value');
  1137. if (!scoreEl || !total) return;
  1138. couterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1139. couterEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  1140. const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10);
  1141. if (score > total) total = score;
  1142. const likes = (total + score) / 2;
  1143. const percent = Math.round((100 * likes) / total);
  1144. const details = `&nbsp;= ${total} × (${percent} ${100 - percent})%`;
  1145. const detailsEl = document.createElement('span');
  1146. detailsEl.innerHTML = details;
  1147. detailsEl.style.color = '#545454';
  1148. detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif';
  1149. detailsEl.style.fontSize = '13px';
  1150. detailsEl.style.fontWeight = 'normal';
  1151. detailsEl.style.verticalAlign = 'middle';
  1152. scoreEl.appendChild(detailsEl);
  1153. couterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign
  1154. });
  1155. }
  1156.  
  1157. // счетчики рейтинга с подробностями
  1158. const scoresMap = new Map();
  1159.  
  1160. class Score {
  1161. constructor(el) {
  1162. this.el = el;
  1163. const data = this.constructor.parse(el);
  1164. this.rating = data.rating;
  1165. this.total = data.total;
  1166. this.likes = data.likes;
  1167. this.dislikes = data.dislikes;
  1168. this.isDetailed = false;
  1169. this.observer = new MutationObserver(() => this.update());
  1170. }
  1171.  
  1172. setDetails(isDetailed) {
  1173. if (this.isDetailed === isDetailed) return;
  1174. this.isDetailed = isDetailed;
  1175. this.update();
  1176. }
  1177.  
  1178. update() {
  1179. const data = this.constructor.parse(this.el);
  1180. this.rating = data.rating;
  1181. this.total = data.total;
  1182. this.likes = data.likes;
  1183. this.dislikes = data.dislikes;
  1184. this.observer.disconnect();
  1185. if (this.isDetailed) {
  1186. this.details();
  1187. } else {
  1188. this.simply();
  1189. }
  1190. this.observer.observe(this.el, { childList: true });
  1191. }
  1192.  
  1193. static parse(el) {
  1194. let [total, likes, dislikes] = el
  1195. .attributes.title.textContent
  1196. .match(/[0-9]+/g).map(Number);
  1197. let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const
  1198. rating = Number(rating);
  1199. if (sign) rating = -rating;
  1200. // не знаю что там происходит при голосовании, так что на всякий случай
  1201. const diff = rating - (likes - dislikes);
  1202. if (diff < 0) {
  1203. total += Math.abs(diff);
  1204. dislikes += Math.abs(diff);
  1205. } else if (diff > 0) {
  1206. total += diff;
  1207. likes += diff;
  1208. }
  1209. return {
  1210. rating,
  1211. total,
  1212. likes,
  1213. dislikes,
  1214. };
  1215. }
  1216.  
  1217. simply() {
  1218. let innerHTML = '';
  1219. if (this.rating > 0) {
  1220. innerHTML = `+${this.rating}`;
  1221. } else if (this.rating < 0) {
  1222. innerHTML = `–${Math.abs(this.rating)}`;
  1223. } else {
  1224. innerHTML = '0';
  1225. }
  1226. this.el.innerHTML = innerHTML;
  1227. }
  1228.  
  1229. details() {
  1230. let innerHTML = '';
  1231. if (this.rating > 0) {
  1232. innerHTML = `+${this.rating}`;
  1233. } else if (this.rating < 0) {
  1234. innerHTML = `–${Math.abs(this.rating)}`;
  1235. } else {
  1236. innerHTML = '0';
  1237. }
  1238. if (this.total !== 0) {
  1239. let details = '';
  1240. if (RATING_DETAILS_PN) {
  1241. details = `&nbsp;= ${this.likes} ${this.dislikes}`;
  1242. } else {
  1243. const percent = Math.round((100 * this.likes) / this.total);
  1244. details = `&nbsp;= ${this.total} × (${percent} ${100 - percent})%`;
  1245. }
  1246. innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
  1247. }
  1248. this.el.innerHTML = innerHTML;
  1249. }
  1250. }
  1251.  
  1252. // парсим их
  1253. [...document.querySelectorAll('.voting-wjt__counter')].forEach((el) => {
  1254. scoresMap.set(el, new Score(el));
  1255. });
  1256.  
  1257. // добавляем подробностей
  1258. if (RATING_DETAILS) {
  1259. if (RATING_DETAILS_ONCLICK) {
  1260. const isDetailed = userConfig.getItem('scores_details');
  1261. if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed));
  1262. scoresMap.forEach((score) => {
  1263. score.el.onclick = () => { // eslint-disable-line no-param-reassign
  1264. const nowDetailed = userConfig.shiftItem('scores_details');
  1265. scoresMap.forEach(s => s.setDetails(nowDetailed));
  1266. };
  1267. });
  1268. } else {
  1269. scoresMap.forEach(score => score.setDetails(true));
  1270. }
  1271. }
  1272.  
  1273. // метки времени и работа с ними
  1274. const pageLoadTime = new Date();
  1275. const monthNames = [
  1276. 'января', 'февраля', 'марта',
  1277. 'апреля', 'мая', 'июня',
  1278. 'июля', 'августа', 'сентября',
  1279. 'октября', 'ноября', 'декабря',
  1280. ];
  1281.  
  1282. class HabraTime {
  1283. constructor(el, parent) {
  1284. this.el = el;
  1285. this.parent = parent;
  1286. this.attrDatetime = this.constructor.getAttributeDatetime(el);
  1287. this.date = new Date(this.attrDatetime);
  1288. }
  1289.  
  1290. // вот было бы хорошо, если б на хабре были datetime атрибуты
  1291. static getAttributeDatetime(el) {
  1292. const imagination = el.getAttribute('datetime');
  1293. if (imagination) return imagination;
  1294.  
  1295. const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
  1296. let [,,
  1297. recently, // eslint-disable-line prefer-const
  1298. day, month, year,
  1299. time, // eslint-disable-line prefer-const
  1300. ] = el.innerHTML.match(re);
  1301.  
  1302. // и местное время
  1303. let moscow;
  1304. if (recently || year === undefined) {
  1305. const offsetMoscow = 3 * 60 * 60 * 1000;
  1306. const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0;
  1307. const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000;
  1308. const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset;
  1309. moscow = new Date(value);
  1310. }
  1311.  
  1312. if (recently) {
  1313. day = moscow.getDate();
  1314. month = moscow.getMonth() + 1;
  1315. } else if (month.length !== 2) {
  1316. month = monthNames.indexOf(month) + 1;
  1317. } else {
  1318. month = +month;
  1319. }
  1320.  
  1321. if (day < 10) day = `0${+day}`;
  1322. if (month < 10) month = `0${month}`;
  1323. if (year < 100) year = `20${year}`;
  1324. if (year === undefined) year = moscow.getFullYear();
  1325.  
  1326. return `${year}-${month}-${day}T${time}+03:00`;
  1327. }
  1328.  
  1329. absolute() {
  1330. let result = '';
  1331.  
  1332. const time = this.date;
  1333. const day = time.getDate();
  1334. const month = time.getMonth();
  1335. const monthName = monthNames[month];
  1336. const year = time.getFullYear();
  1337. const hours = time.getHours();
  1338. const minutes = time.getMinutes();
  1339.  
  1340. const now = new Date();
  1341. const nowDay = now.getDate();
  1342. const nowMonth = now.getMonth();
  1343. const nowYear = now.getFullYear();
  1344.  
  1345. const yesterday = new Date((now - 24) * 60 * 60 * 1000);
  1346. const yesterdayDay = yesterday.getDate();
  1347. const yesterdayMonth = yesterday.getMonth();
  1348. const yesterdayYear = yesterday.getFullYear();
  1349.  
  1350. const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`;
  1351.  
  1352. const isToday =
  1353. day === nowDay &&
  1354. month === nowMonth &&
  1355. year === nowYear;
  1356. const isYesterday =
  1357. day === yesterdayDay &&
  1358. month === yesterdayMonth &&
  1359. year === yesterdayYear;
  1360.  
  1361. if (isToday) {
  1362. result = `сегодня в ${hhmm}`;
  1363. } else if (isYesterday) {
  1364. result = `вчера в ${hhmm}`;
  1365. } else if (nowYear === year) {
  1366. result = `${day} ${monthName} в ${hhmm}`;
  1367. } else {
  1368. result = `${day} ${monthName} ${year} в ${hhmm}`;
  1369. }
  1370.  
  1371. return result;
  1372. }
  1373.  
  1374. static relative(milliseconds) {
  1375. let result = '';
  1376.  
  1377. const pluralForm = (n, forms) => {
  1378. if (n % 10 === 1 && n % 100 !== 11) return forms[0];
  1379. if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1];
  1380. return forms[2];
  1381. };
  1382.  
  1383. const formats = [
  1384. ['год', 'года', 'лет'],
  1385. ['месяц', 'месяца', 'месяцев'],
  1386. ['день', 'дня', 'дней'],
  1387. ['час', 'часа', 'часов'],
  1388. ['минуту', 'минуты', 'минут'],
  1389. ];
  1390.  
  1391. const minutes = milliseconds / 60000;
  1392. const hours = minutes / 60;
  1393. const days = hours / 24;
  1394. const months = days / 30;
  1395. const years = months / 12;
  1396. const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1);
  1397.  
  1398. if (idx === -1) {
  1399. result = 'несколько секунд';
  1400. } else {
  1401. const value = Math.floor([years, months, days, hours, minutes][idx]);
  1402. const forms = formats[idx];
  1403. const form = pluralForm(value, forms);
  1404. result = `${value} ${form}`;
  1405. }
  1406. return result;
  1407. }
  1408.  
  1409. fromNow() {
  1410. const diff = Math.abs(Date.now() - this.date);
  1411. return `${this.constructor.relative(diff)} назад`;
  1412. }
  1413.  
  1414. fromParent() {
  1415. const diff = Math.abs(this.date - this.parent.date);
  1416. return `через ${this.constructor.relative(diff)}`;
  1417. }
  1418. }
  1419.  
  1420. // собираем метки времени
  1421. const datesMap = new Map();
  1422. const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
  1423. (megapostTimeEl ? [megapostTimeEl] : [])
  1424. .concat([...document.querySelectorAll(`
  1425. .post__time,
  1426. .preview-data__time-published,
  1427. time.comment__date-time_published,
  1428. .tm-post__date,
  1429. .user-message__date-time
  1430. `)]).forEach((el) => {
  1431. datesMap.set(el, new HabraTime(el));
  1432. });
  1433.  
  1434. function updateTime() {
  1435. datesMap.forEach((habraTime) => {
  1436. let type;
  1437. let otherTypes;
  1438. if (habraTime.parent) {
  1439. type = userConfig.config.time_comments;
  1440. otherTypes = userConfig.model.time_comments
  1441. .filter(str => str !== type);
  1442. } else {
  1443. type = userConfig.config.time_publications;
  1444. otherTypes = userConfig.model.time_publications
  1445. .filter(str => str !== type);
  1446. }
  1447. const title = otherTypes.map(otherType => habraTime[otherType]()).join(', ');
  1448. habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign
  1449. habraTime.el.setAttribute('title', title);
  1450. });
  1451. }
  1452.  
  1453. if (TIME_DETAILS) {
  1454. datesMap.forEach((habraTime) => {
  1455. habraTime.el.setAttribute(
  1456. 'style',
  1457. 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
  1458. );
  1459. habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign
  1460. if (habraTime.parent) {
  1461. userConfig.shiftItem('time_comments');
  1462. } else {
  1463. userConfig.shiftItem('time_publications');
  1464. }
  1465. updateTime();
  1466. };
  1467. });
  1468. // подождём, когда дерево комментариев будет построено
  1469. // у некоторых меток времени будут установлены родители
  1470. // тогда и обновим их тексты
  1471. setTimeout(updateTime, 100);
  1472. setInterval(updateTime, 30 * 1000);
  1473. }
  1474.  
  1475. // время публикации, понадобится для корня древа комментариев
  1476. let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time'));
  1477. // если нету публикации поищем самую раннюю метку времени
  1478. if (!datePublication) {
  1479. datePublication = { date: pageLoadTime };
  1480. datesMap.forEach((date) => {
  1481. if (date.date < datePublication.date) datePublication = date;
  1482. });
  1483. }
  1484.  
  1485. // создаем дерево комментариев
  1486. class ItemComment {
  1487. constructor(el, parent) {
  1488. this.parent = parent;
  1489. this.el = el;
  1490. this.lvl = parent.lvl + 1;
  1491. this.id = Number(el.getAttribute('rel'));
  1492. this.commentEl = el.querySelector('.comment');
  1493. if (this.commentEl) {
  1494. this.timeEl = this.commentEl.querySelector('time');
  1495. this.ratingEl = this.commentEl.querySelector('.js-score');
  1496. }
  1497. this.date = datesMap.get(this.timeEl);
  1498. if (this.date) {
  1499. this.date.parent = parent.date;
  1500. } else {
  1501. this.date = parent.date;
  1502. }
  1503. this.votes = scoresMap.get(this.ratingEl) || {
  1504. total: 0, likes: 0, dislikes: 0, rating: 0,
  1505. };
  1506. this.elList = el.querySelector('.content-list_nested-comments');
  1507. }
  1508.  
  1509. existId(id) {
  1510. return !!this.elList.querySelector(id);
  1511. }
  1512.  
  1513. existNew() {
  1514. return !!this.elList.querySelector('.js-comment_new');
  1515. }
  1516.  
  1517. getLength() {
  1518. let { length } = this.list;
  1519. this.list.forEach((node) => {
  1520. length += node.getLength();
  1521. });
  1522. return length;
  1523. }
  1524. }
  1525.  
  1526. class CommentsTree {
  1527. constructor() {
  1528. this.root = {
  1529. isRoot: true,
  1530. date: datePublication,
  1531. lvl: 0,
  1532. elList: document.getElementById('comments-list'),
  1533. list: [],
  1534. };
  1535. }
  1536.  
  1537. static exist() {
  1538. return !!document.getElementById('comments-list');
  1539. }
  1540.  
  1541. update() {
  1542. if (!this.root.elList) return;
  1543. const recAdd = (node) => {
  1544. node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign
  1545. .map(el => new ItemComment(el, node));
  1546. node.list.forEach(recAdd);
  1547. };
  1548. recAdd(this.root);
  1549. }
  1550.  
  1551. walkTree(fn) {
  1552. const walk = (tree) => {
  1553. fn(tree);
  1554. tree.list.forEach(walk);
  1555. };
  1556. walk(this.root);
  1557. }
  1558.  
  1559. sort(fn) {
  1560. if (!this.root.elList) return;
  1561. this.walkTree((tree) => {
  1562. tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el));
  1563. });
  1564. }
  1565.  
  1566. shuffle() {
  1567. if (!this.root.elList) return;
  1568. const randInt = maximum => Math.floor(Math.random() * (maximum + 1));
  1569. this.walkTree((tree) => {
  1570. const { list } = tree;
  1571. for (let i = 0; i < list.length; i += 1) {
  1572. const j = randInt(i);
  1573. [list[i], list[j]] = [list[j], list[i]];
  1574. }
  1575. list.forEach(subtree => tree.elList.appendChild(subtree.el));
  1576. });
  1577. }
  1578. }
  1579.  
  1580. const commentsTree = new CommentsTree();
  1581. commentsTree.update();
  1582.  
  1583. // здесь начинается сортировка комментариев
  1584. const commentsOrderEl = document.createElement('div');
  1585. commentsOrderEl.classList.add('comments_order');
  1586. commentsOrderEl.innerHTML = sortVariants.map(([type, text]) => {
  1587. const underline = (type === 'time') ? '; text-decoration: underline' : '';
  1588. return `<a data-order="${type}" style="cursor: pointer${underline}">${text}</a>`;
  1589. }).join(', ');
  1590.  
  1591. if (COMMENTS_SORT && document.getElementById('comments-list')) {
  1592. const commentsList = document.getElementById('comments-list');
  1593. commentsList.parentElement.insertBefore(commentsOrderEl, commentsList);
  1594. }
  1595.  
  1596. const commentsComparators = {
  1597. time(a, b) {
  1598. return a.id - b.id;
  1599. },
  1600.  
  1601. freshness(a, b) {
  1602. return b.id - a.id;
  1603. },
  1604.  
  1605. rating(a, b) {
  1606. const ascore = a.votes.rating;
  1607. const bscore = b.votes.rating;
  1608. if (bscore !== ascore) return bscore - ascore;
  1609. return b.id - a.id;
  1610. },
  1611.  
  1612. popularity(a, b) {
  1613. const aVotes = a.votes.total;
  1614. const bVotes = b.votes.total;
  1615. if (aVotes !== bVotes) return bVotes - aVotes;
  1616. const aLength = a.getLength();
  1617. const bLength = b.getLength();
  1618. if (aLength !== bLength) return bLength - aLength;
  1619. return b.id - a.id;
  1620. },
  1621.  
  1622. quality(a, b) {
  1623. const aQuality = a.votes.rating / a.votes.total || 0;
  1624. const bQuality = b.votes.rating / b.votes.total || 0;
  1625. if (aQuality !== bQuality) return bQuality - aQuality;
  1626. if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating;
  1627. return b.id - a.id;
  1628. },
  1629.  
  1630. trend(a, b) {
  1631. // в первые сутки после публикации статьи число посещений больше чем в остальное время
  1632. const oneDay = 24 * 60 * 60 * 1000;
  1633. const firstDayEnd = +datePublication.date + oneDay;
  1634. // у комментария есть только три дня на голосование с момента его создания
  1635. const threeDays = 3 * oneDay;
  1636. const now = Date.now();
  1637.  
  1638. // прикинем число голосов в первый день
  1639. const aDate = +a.date.date;
  1640. let aViews = 0;
  1641. // в первый день
  1642. if (aDate <= firstDayEnd) {
  1643. aViews += Math.min(firstDayEnd, now) - aDate;
  1644. }
  1645. // и в остальное время
  1646. if (now >= firstDayEnd) {
  1647. const threeDaysEnd = aDate + threeDays;
  1648. // для этого соотношения я собрал статистику
  1649. aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16;
  1650. }
  1651. const aScore = a.votes.rating / aViews;
  1652.  
  1653. // аналогично
  1654. const bDate = +b.date.date;
  1655. let bViews = 0;
  1656. if (bDate <= firstDayEnd) {
  1657. bViews += Math.min(firstDayEnd, now) - bDate;
  1658. }
  1659. if (now >= firstDayEnd) {
  1660. const threeDaysEnd = bDate + threeDays;
  1661. // найти зависимость активности голосования от времени суток не удалось
  1662. bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16;
  1663. }
  1664. const bScore = b.votes.rating / bViews;
  1665.  
  1666. if (bScore === aScore) return b.id - a.id;
  1667. return bScore - aScore;
  1668. },
  1669. };
  1670.  
  1671. const sortComments = () => {
  1672. const order = userConfig.getItem('comments_order');
  1673.  
  1674. Array.from(commentsOrderEl.children).forEach((el) => {
  1675. if (el.dataset.order === order) {
  1676. el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign
  1677. } else {
  1678. el.style.textDecoration = ''; // eslint-disable-line no-param-reassign
  1679. }
  1680. });
  1681.  
  1682. if (order === 'shuffle') {
  1683. commentsTree.shuffle();
  1684. } else {
  1685. const compare = commentsComparators[order];
  1686. commentsTree.sort(compare);
  1687. }
  1688. };
  1689.  
  1690. // сортируем комменты при загрузке страницы
  1691. // или не сортируем, если они уже по порядку
  1692. if (COMMENTS_SORT && COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') {
  1693. sortComments();
  1694. }
  1695.  
  1696. Array.from(commentsOrderEl.children).forEach((el) => {
  1697. el.onclick = () => { // eslint-disable-line no-param-reassign
  1698. userConfig.setItem('comments_order', el.dataset.order);
  1699. sortComments();
  1700. };
  1701. });
  1702.  
  1703. // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев
  1704. if (COMMENTS_LINKS) {
  1705. const commentsLinks = document.getElementsByClassName('post-stats__comments-link');
  1706.  
  1707. for (let i = 0; i < commentsLinks.length; i += 1) {
  1708. const iLink = commentsLinks[i];
  1709. const hrefValue = iLink.getAttribute('href');
  1710. const hrefToComments = hrefValue.replace('#first_unread', '#comments');
  1711. iLink.setAttribute('href', hrefToComments);
  1712. }
  1713. }
  1714.  
  1715. // сворачивание комментов
  1716. if (COMMENTS_HIDE) {
  1717. const commentHash = window.location.hash;
  1718.  
  1719. const toggle = (subtree) => {
  1720. const listLength = subtree.list.length;
  1721. if (listLength === 0) return;
  1722. /* eslint-disable */
  1723. if (subtree.switcherEl.dataset.isVisibleList === 'true') {
  1724. subtree.switcherEl.dataset.isVisibleList = 'false';
  1725. subtree.switcherEl.innerHTML = `\u229E раскрыть ветвь ${subtree.getLength()}`;
  1726. subtree.elList.style.display = 'none';
  1727. } else {
  1728. subtree.switcherEl.dataset.isVisibleList = 'true';
  1729. subtree.switcherEl.innerHTML = '\u229F';
  1730. subtree.elList.style.display = 'block';
  1731. }
  1732. /* eslint-enable */
  1733. };
  1734.  
  1735. commentsTree.walkTree((subtree) => {
  1736. // не пытаемся сворачивать корень
  1737. if (subtree.isRoot) return;
  1738. // у похищенных нет футера
  1739. const footerEl = subtree.commentEl.querySelector('.comment__footer');
  1740. if (footerEl === null) return;
  1741. // создаём переключатель
  1742. const switcher = document.createElement('a');
  1743. switcher.classList.add('comment__footer-link');
  1744. switcher.classList.add('comment__switcher');
  1745. switcher.dataset.isVisibleList = 'true';
  1746.  
  1747. switcher.innerHTML = '\u229F';
  1748. if (subtree.list.length === 0) switcher.innerHTML = '\u22A1';
  1749. switcher.style.cursor = 'pointer';
  1750. switcher.style.marginLeft = '-5px';
  1751.  
  1752. footerEl.insertBefore(switcher, footerEl.children[0]);
  1753. subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign
  1754.  
  1755. switcher.onclick = () => toggle(subtree);
  1756.  
  1757. const isHideLvl = subtree.lvl === HIDE_LEVEL;
  1758. const isLineLvl = subtree.lvl % LINE_LEN === 0;
  1759. if (isLineLvl) {
  1760. subtree.elList.classList.add('comments_new-line');
  1761. const lineNumber = subtree.lvl / LINE_LEN;
  1762. subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
  1763. }
  1764. // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
  1765. if (
  1766. (isHideLvl || isLineLvl) && !subtree.existNew() &&
  1767. !(commentHash && subtree.existId(commentHash))
  1768. ) {
  1769. toggle(subtree);
  1770. }
  1771. });
  1772. }
  1773.  
  1774. if (SCROLL_LEGEND) {
  1775. const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body');
  1776. const commentsEl = document.getElementById('comments-list');
  1777. const getPercents = (el) => {
  1778. if (!el) return { topPercent: 0, heightPercent: 0 };
  1779. const pageHeight = document.documentElement.scrollHeight;
  1780. const top = el.getBoundingClientRect().top + window.pageYOffset;
  1781. const topPercent = ((100 * top) / pageHeight).toFixed(2);
  1782. const height = el.clientHeight;
  1783. const heightPercent = ((100 * height) / pageHeight).toFixed(2);
  1784.  
  1785. return { topPercent, heightPercent };
  1786. };
  1787.  
  1788. const updateLegend = (pageEl, legendEl) => {
  1789. const { topPercent, heightPercent } = getPercents(pageEl);
  1790. legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign
  1791. legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign
  1792. };
  1793.  
  1794. const legendPost = document.createElement('div');
  1795. legendPost.classList.add('legend_el');
  1796. legendPost.style.background = 'rgba(84, 142, 170, 0.66)';
  1797. updateLegend(postBodyEl, legendPost);
  1798. document.body.appendChild(legendPost);
  1799.  
  1800. const legendComments = document.createElement('div');
  1801. legendComments.classList.add('legend_el');
  1802. legendComments.style.background = 'rgba(49, 176, 7, 0.66)';
  1803. updateLegend(commentsEl, legendComments);
  1804. document.body.appendChild(legendComments);
  1805.  
  1806. setInterval(() => {
  1807. updateLegend(postBodyEl, legendPost);
  1808. updateLegend(commentsEl, legendComments);
  1809. }, 1000);
  1810. }
  1811.  
  1812. if (NIGHT_MODE) {
  1813. const switcherEl = document.createElement('div');
  1814. switcherEl.classList.add('night_mode_switcher');
  1815. switcherEl.onclick = () => {
  1816. const isNightMode = userConfig.shiftItem('night_mode');
  1817. document.documentElement.classList.toggle('night', isNightMode);
  1818. };
  1819. document.body.appendChild(switcherEl);
  1820. setInterval(() => {
  1821. const boolClass = document.documentElement.classList.contains('night');
  1822. const isNightMode = userConfig.getItem('night_mode');
  1823. if (boolClass !== isNightMode) {
  1824. document.documentElement.classList.toggle('night', isNightMode);
  1825. }
  1826. }, 1000);
  1827. }
  1828. });