Grok Code Filter Menu 1.21.18

Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек

  1. // ==UserScript==
  2. // @name Grok Code Filter Menu 1.21.18
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.21.18
  5. // @description Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
  6. // @author tapeavion
  7. // @license MIT
  8. // @match https://grok.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @downloadURL
  14. // @updateURL
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Стили
  21. const style = document.createElement('style');
  22. style.textContent = `
  23. .filter-menu-btn {
  24. position: absolute;
  25. top: 4px;
  26. right: 460px;
  27. height: 31px !important;
  28. z-index: 1;
  29. padding: 4px 8px;
  30. background: #1d5752;
  31. color: #b9bcc1;
  32. border: none;
  33. border-radius: 8px;
  34. cursor: pointer;
  35. font-size: 12px;
  36. transition: background 0.2s ease, color 0.2s ease;
  37. }
  38. .filter-menu-btn:hover {
  39. background: #4a8983;
  40. color: #ffffff;
  41. }
  42. .filter-menu {
  43. position: absolute;
  44. top: 40px;
  45. right: 10px;
  46. background: #2d2d2d;
  47. border: 1px solid #444;
  48. border-radius: 8px;
  49. padding: 5px;
  50. z-index: 9999;
  51. display: none;
  52. box-shadow: 0 2px 4px rgba(0,0,0,0.3);
  53. width: 200px;
  54. max-height: 550px;
  55. overflow-y: auto;
  56. }
  57. .filter-item {
  58. display: flex;
  59. align-items: center;
  60. padding: 5px 0;
  61. color: #a0a0a0;
  62. font-size: 12px;
  63. }
  64. .filter-item input[type="checkbox"] {
  65. margin-right: 5px;
  66. }
  67. .filter-item label {
  68. flex: 1;
  69. cursor: pointer;
  70. }
  71. .filter-slider {
  72. display: none;
  73. margin: 5px 0 5px 20px;
  74. width: calc(100% - 20px);
  75. }
  76. .filter-slider-label {
  77. display: none;
  78. color: #a0a0a0;
  79. font-size: 12px;
  80. margin: 2px 0 2px 20px;
  81. }
  82. .language-select {
  83. width: 100%;
  84. padding: 5px;
  85. margin-bottom: 5px;
  86. background: #3a3a3a;
  87. color: #a0a0a0;
  88. border: none;
  89. border-radius: 4px;
  90. font-size: 12px;
  91. }
  92. .color-picker {
  93. margin: 5px 0 5px 20px;
  94. width: calc(100% - 20px);
  95. }
  96. .color-picker-label {
  97. display: block;
  98. color: #a0a0a0;
  99. font-size: 12px;
  100. margin: 2px 0 2px 20px;
  101. }
  102. button.inline-flex {
  103. background-color: #1d5752 !important;
  104. opacity: 0;
  105. animation: fadeIn 1s ease-in-out forwards;
  106. }
  107. button.inline-flex:hover {
  108. background-color: #1d5752 !important;
  109. opacity: 1;
  110. }
  111. @keyframes fadeIn {
  112. 0% { opacity: 0; }
  113. 100% { opacity: 1; }
  114. }
  115. `;
  116. document.head.appendChild(style);
  117.  
  118. // Определение языка пользователя
  119. const userLang = navigator.language || navigator.languages[0];
  120. const isRussian = userLang.startsWith('ru');
  121. const defaultLang = isRussian ? 'ru' : 'en';
  122. const savedLang = localStorage.getItem('filterMenuLang') || defaultLang;
  123.  
  124. // Локализация
  125. const translations = {
  126. ru: {
  127. filtersBtn: 'Фильтры',
  128. sliderLabel: 'Степень:',
  129. commentColorLabel: 'Цвет комментариев:',
  130. filters: [
  131. { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  132. { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  133. { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  134. { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  135. { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  136. { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  137. { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  138. { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  139. { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  140. ],
  141. langSelect: 'Выберите язык:',
  142. langOptions: [
  143. { value: 'ru', label: 'Русский' },
  144. { value: 'en', label: 'English' }
  145. ]
  146. },
  147. en: {
  148. filtersBtn: 'Filters',
  149. sliderLabel: 'Level:',
  150. commentColorLabel: 'Comment color:',
  151. filters: [
  152. { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  153. { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  154. { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  155. { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  156. { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  157. { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  158. { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  159. { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  160. { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  161. ],
  162. langSelect: 'Select language:',
  163. langOptions: [
  164. { value: 'ru', label: 'Русский' },
  165. { value: 'en', label: 'English' }
  166. ]
  167. }
  168. };
  169.  
  170. // Глобальная переменная для текущего цвета комментариев
  171. let currentCommentColor = localStorage.getItem('commentColor') || '#5c6370';
  172. // Массив для хранения всех пар headerBlock и codeContainer
  173. const codeBlockRegistry = [];
  174.  
  175. // Функция создания меню фильтров
  176. function addFilterMenu(headerBlock, codeContainer) {
  177. if (headerBlock.querySelector('.filter-menu-btn')) {
  178. console.log('Фильтр уже существует для заголовка:', headerBlock);
  179. return;
  180. }
  181.  
  182. // Сохраняем пару headerBlock и codeContainer
  183. codeBlockRegistry.push({ headerBlock, codeContainer });
  184. console.log('Добавлен кодовый блок в реестр:', codeContainer.outerHTML);
  185.  
  186. let currentLang = savedLang;
  187. const filterBtn = document.createElement('button');
  188. filterBtn.className = 'filter-menu-btn';
  189. filterBtn.textContent = translations[currentLang].filtersBtn;
  190.  
  191. const filterMenu = document.createElement('div');
  192. filterMenu.className = 'filter-menu';
  193.  
  194. // Целевой блок — контейнер кода
  195. const targetBlock = codeContainer;
  196.  
  197. // Загружаем сохраненные настройки
  198. const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
  199. const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
  200.  
  201. // Инициализируем значения по умолчанию
  202. const filters = translations[currentLang].filters;
  203. filters.forEach(filter => {
  204. if (!(filter.value in savedFilterStates)) {
  205. savedFilterStates[filter.value] = false;
  206. }
  207. if (!(filter.value in savedFilterValues)) {
  208. savedFilterValues[filter.value] = filter.default;
  209. }
  210. });
  211.  
  212. // Применяем сохраненные фильтры
  213. function applyFilters() {
  214. const activeFilters = filters
  215. .filter(filter => savedFilterStates[filter.value])
  216. .map(filter => {
  217. const unit = filter.unit || '';
  218. const value = savedFilterValues[filter.value];
  219. return `${filter.value}(${value}${unit})`;
  220. });
  221. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  222. console.log('Применены фильтры к контейнеру:', targetBlock, activeFilters);
  223. }
  224.  
  225. // Применяем цвет комментариев
  226. // Альтернативный подход: применение цвета через глобальный стиль
  227. function applyGlobalCommentColor() {
  228. const existingStyle = document.getElementById('custom-comment-style');
  229. if (existingStyle) existingStyle.remove();
  230.  
  231. const style = document.createElement('style');
  232. style.id = 'custom-comment-style';
  233. style.textContent = `
  234. .hljs-comment, span[style*="color: rgb(92, 99, 112)"] {
  235. color: ${currentCommentColor} !important;
  236. }
  237. `;
  238. document.head.appendChild(style);
  239. console.log('Применен глобальный стиль для комментариев:', currentCommentColor);
  240. }
  241. applyFilters();
  242. applyGlobalCommentColor();
  243.  
  244. // Создаем выпадающий список для выбора языка
  245. const langSelect = document.createElement('select');
  246. langSelect.className = 'language-select';
  247. const langLabel = document.createElement('label');
  248. langLabel.textContent = translations[currentLang].langSelect;
  249. langLabel.style.color = '#a0a0a0';
  250. langLabel.style.fontSize = '12px';
  251. langLabel.style.marginBottom = '2px';
  252. langLabel.style.display = 'block';
  253.  
  254. translations[currentLang].langOptions.forEach(option => {
  255. const opt = document.createElement('option');
  256. opt.value = option.value;
  257. opt.textContent = option.label;
  258. if (option.value === currentLang) {
  259. opt.selected = true;
  260. }
  261. langSelect.appendChild(opt);
  262. });
  263.  
  264. // Создаем элемент для выбора цвета комментариев
  265. const colorPickerLabel = document.createElement('label');
  266. colorPickerLabel.className = 'color-picker-label';
  267. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  268.  
  269. const colorPicker = document.createElement('input');
  270. colorPicker.type = 'color';
  271. colorPicker.className = 'color-picker';
  272. colorPicker.value = currentCommentColor;
  273.  
  274. colorPicker.addEventListener('input', () => {
  275. currentCommentColor = colorPicker.value;
  276. localStorage.setItem('commentColor', currentCommentColor);
  277. console.log('Изменен цвет комментариев:', currentCommentColor);
  278. refreshAllCodeBlocks();
  279. });
  280.  
  281. // Функция обновления интерфейса при смене языка
  282. function updateLanguage(lang) {
  283. currentLang = lang;
  284. localStorage.setItem('filterMenuLang', currentLang);
  285. filterBtn.textContent = translations[currentLang].filtersBtn;
  286. langLabel.textContent = translations[currentLang].langSelect;
  287. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  288. filterMenu.innerHTML = '';
  289. filterMenu.appendChild(langLabel);
  290. filterMenu.appendChild(langSelect);
  291. filterMenu.appendChild(colorPickerLabel);
  292. filterMenu.appendChild(colorPicker);
  293. renderFilters();
  294. }
  295.  
  296. // Обработчик смены языка
  297. langSelect.addEventListener('change', () => {
  298. updateLanguage(langSelect.value);
  299. });
  300.  
  301. // Рендеринг фильтров
  302. function renderFilters() {
  303. const filters = translations[currentLang].filters;
  304. filters.forEach(filter => {
  305. const filterItem = document.createElement('div');
  306. filterItem.className = 'filter-item';
  307.  
  308. const checkbox = document.createElement('input');
  309. checkbox.type = 'checkbox';
  310. checkbox.checked = savedFilterStates[filter.value];
  311. checkbox.id = `filter-${filter.value}`;
  312.  
  313. const label = document.createElement('label');
  314. label.htmlFor = `filter-${filter.value}`;
  315. label.textContent = filter.name;
  316.  
  317. const sliderLabel = document.createElement('label');
  318. sliderLabel.className = 'filter-slider-label';
  319. sliderLabel.textContent = translations[currentLang].sliderLabel;
  320.  
  321. const slider = document.createElement('input');
  322. slider.type = 'range';
  323. slider.className = 'filter-slider';
  324. slider.min = filter.min;
  325. slider.max = filter.max;
  326. slider.step = filter.step;
  327. slider.value = savedFilterValues[filter.value];
  328.  
  329. if (checkbox.checked && filter.hasSlider) {
  330. slider.style.display = 'block';
  331. sliderLabel.style.display = 'block';
  332. }
  333.  
  334. checkbox.addEventListener('change', () => {
  335. savedFilterStates[filter.value] = checkbox.checked;
  336. localStorage.setItem('codeFilterStates', JSON.stringify(savedFilterStates));
  337. if (filter.hasSlider) {
  338. slider.style.display = checkbox.checked ? 'block' : 'none';
  339. sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
  340. }
  341. console.log('Изменен фильтр:', filter.value, checkbox.checked);
  342. refreshAllCodeBlocks();
  343. });
  344.  
  345. slider.addEventListener('input', () => {
  346. savedFilterValues[filter.value] = slider.value;
  347. localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues));
  348. console.log('Изменено значение фильтра:', filter.value, slider.value);
  349. refreshAllCodeBlocks();
  350. });
  351.  
  352. filterItem.appendChild(checkbox);
  353. filterItem.appendChild(label);
  354. filterMenu.appendChild(filterItem);
  355. filterMenu.appendChild(sliderLabel);
  356. filterMenu.appendChild(slider);
  357. });
  358. }
  359.  
  360. // Инициализация
  361. filterMenu.appendChild(langLabel);
  362. filterMenu.appendChild(langSelect);
  363. filterMenu.appendChild(colorPickerLabel);
  364. filterMenu.appendChild(colorPicker);
  365. renderFilters();
  366.  
  367. // Обработчики для кнопки
  368. filterBtn.addEventListener('click', () => {
  369. filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
  370. });
  371.  
  372. document.addEventListener('click', (e) => {
  373. if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
  374. filterMenu.style.display = 'none';
  375. }
  376. });
  377.  
  378. headerBlock.style.position = 'relative';
  379. headerBlock.appendChild(filterBtn);
  380. headerBlock.appendChild(filterMenu);
  381. }
  382.  
  383. // Функция логирования структуры DOM для отладки
  384. function logDomStructure(headerBlock) {
  385. console.log('Заголовок блока кода:', headerBlock.outerHTML);
  386. console.log('Следующий элемент (nextElementSibling):', headerBlock.nextElementSibling?.outerHTML || 'Не найден');
  387. console.log('Родительский элемент:', headerBlock.parentElement.outerHTML);
  388. console.log('Все <code> в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('code')).map(el => el.outerHTML));
  389. console.log('Все .not-prose в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('.not-prose')).map(el => el.outerHTML));
  390. }
  391.  
  392. // Функция для управления селекторами и контейнерами
  393. function manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer) {
  394. const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');
  395.  
  396. // Сохраняем успешные селекторы и контейнеры
  397. if (headerBlocks.length > 0 && codeContainer) {
  398. savedSelectors[headerSelectorUsed] = savedSelectors[headerSelectorUsed] || {};
  399. savedSelectors[headerSelectorUsed].headerCount = headerBlocks.length;
  400. savedSelectors[headerSelectorUsed].containerSelector = containerSelectorUsed;
  401. savedSelectors[headerSelectorUsed].lastUsed = Date.now();
  402. localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
  403. console.log('Сохранены селекторы:', headerSelectorUsed, containerSelectorUsed);
  404. }
  405.  
  406. // Очистка устаревших селекторов (старше 7 дней)
  407. const oneWeek = 7 * 24 * 60 * 60 * 1000;
  408. Object.keys(savedSelectors).forEach(selector => {
  409. if (Date.now() - savedSelectors[selector].lastUsed > oneWeek) {
  410. delete savedSelectors[selector];
  411. }
  412. });
  413. localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
  414. return savedSelectors;
  415. }
  416.  
  417. // Функция для обновления всех кодовых блоков
  418. function refreshAllCodeBlocks() {
  419. const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
  420. const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
  421. const filters = [
  422. { value: 'invert', default: 1 },
  423. { value: 'sepia', default: 1 },
  424. { value: 'grayscale', default: 1 },
  425. { value: 'blur', unit: 'px', default: 2 },
  426. { value: 'contrast', default: 2 },
  427. { value: 'brightness', default: 1.5 },
  428. { value: 'hue-rotate', unit: 'deg', default: 90 },
  429. { value: 'saturate', default: 1 },
  430. { value: 'opacity', default: 1 }
  431. ];
  432.  
  433. // Применяем к зарегистрированным блокам
  434. console.log('Реестр кодовых блоков:', codeBlockRegistry.length);
  435. codeBlockRegistry.forEach(({ codeContainer }) => {
  436. const activeFilters = filters
  437. .filter(filter => savedFilterStates[filter.value])
  438. .map(filter => {
  439. const unit = filter.unit || '';
  440. const value = savedFilterValues[filter.value] || filter.default;
  441. return `${filter.value}(${value}${unit})`;
  442. });
  443. codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  444. console.log('Применены фильтры к зарегистрированному контейнеру:', codeContainer, activeFilters);
  445.  
  446. const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
  447. console.log('Найдено комментариев в зарегистрированном контейнере:', commentElements.length);
  448. commentElements.forEach(element => {
  449. element.style.color = currentCommentColor;
  450. });
  451. });
  452.  
  453. // Глобальное применение к новым или незарегистрированным блокам
  454. const allCodeContainers = document.querySelectorAll('.not-prose div > code, .not-prose pre > code');
  455. allCodeContainers.forEach(codeContainer => {
  456. if (!codeBlockRegistry.some(reg => reg.codeContainer === codeContainer.parentElement)) {
  457. const activeFilters = filters
  458. .filter(filter => savedFilterStates[filter.value])
  459. .map(filter => {
  460. const unit = filter.unit || '';
  461. const value = savedFilterValues[filter.value] || filter.default;
  462. return `${filter.value}(${value}${unit})`;
  463. });
  464. codeContainer.parentElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  465. console.log('Применены фильтры к глобальному .not-prose контейнеру:', codeContainer.parentElement, activeFilters);
  466.  
  467. const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
  468. console.log('Найдено комментариев в глобальном .not-prose контейнере:', commentElements.length);
  469. commentElements.forEach(element => {
  470. element.style.color = currentCommentColor;
  471. });
  472. }
  473. });
  474. }
  475.  
  476. // Функция поиска и обработки блоков кода
  477. function processCodeBlocks() {
  478. // Селекторы для заголовков блоков кода
  479. const headerSelectors = [
  480. 'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
  481. 'div[class*="flex"][class*="bg-surface"] > span',
  482. 'div > span[class*="font-mono"]'
  483. ];
  484.  
  485. // Селекторы для контейнеров кода
  486. const containerSelectors = [
  487. 'nextElementSibling',
  488. '.not-prose div[style*="overflow-x: auto"]',
  489. '.not-prose div > code',
  490. '.not-prose pre > code',
  491. '.not-prose div[style*="background: hsl"]'
  492. ];
  493.  
  494. // Загружаем сохраненные селекторы
  495. const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');
  496. let headerBlocks = [];
  497. let headerSelectorUsed = null;
  498. let containerSelectorUsed = null;
  499. let codeContainer = null;
  500.  
  501. // Сначала пытаемся использовать сохраненные селекторы
  502. for (const savedSelector of Object.keys(savedSelectors)) {
  503. const headers = Array.from(document.querySelectorAll(savedSelector))
  504. .filter(span => {
  505. const text = span.textContent.toLowerCase();
  506. return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
  507. })
  508. .map(span => span.closest('div'));
  509. if (headers.length > 0) {
  510. headerBlocks = [...new Set(headers)];
  511. headerSelectorUsed = savedSelector;
  512. const savedContainerSelector = savedSelectors[savedSelector].containerSelector;
  513.  
  514. // Проверяем сохраненный селектор контейнера
  515. for (const headerBlock of headers) {
  516. if (savedContainerSelector === 'nextElementSibling') {
  517. codeContainer = headerBlock.nextElementSibling?.querySelector('code') ? headerBlock.nextElementSibling : null;
  518. } else if (savedContainerSelector === '.not-prose div > code') {
  519. codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
  520. } else if (savedContainerSelector === '.not-prose pre > code') {
  521. codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
  522. } else {
  523. codeContainer = headerBlock.parentElement.querySelector(savedContainerSelector);
  524. }
  525. if (codeContainer) {
  526. containerSelectorUsed = savedContainerSelector;
  527. console.log('Использованы сохраненные селекторы:', headerSelectorUsed, containerSelectorUsed);
  528. break;
  529. }
  530. }
  531. if (codeContainer) break;
  532. }
  533. }
  534.  
  535. // Если сохраненные селекторы не сработали, ищем новые
  536. if (headerBlocks.length === 0) {
  537. for (const selector of headerSelectors) {
  538. const headers = Array.from(document.querySelectorAll(selector))
  539. .filter(span => {
  540. const text = span.textContent.toLowerCase();
  541. return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
  542. })
  543. .map(span => span.closest('div'));
  544. if (headers.length > 0) {
  545. headerBlocks = [...new Set(headers)];
  546. headerSelectorUsed = selector;
  547. break;
  548. }
  549. }
  550. }
  551.  
  552. console.log('Найдено заголовков блоков кода:', headerBlocks.length);
  553.  
  554. headerBlocks.forEach(headerBlock => {
  555. const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
  556. if (!langSpan) {
  557. console.log('Заголовок без span с языком:', headerBlock);
  558. return;
  559. }
  560.  
  561. // Пытаемся найти контейнер кода
  562. codeContainer = null;
  563. if (headerBlock.nextElementSibling?.querySelector('code')) {
  564. codeContainer = headerBlock.nextElementSibling;
  565. containerSelectorUsed = 'nextElementSibling';
  566. } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]')) {
  567. codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]');
  568. containerSelectorUsed = '.not-prose div[style*="overflow-x: auto"]';
  569. } else if (headerBlock.parentElement.querySelector('.not-prose div > code')) {
  570. codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
  571. containerSelectorUsed = '.not-prose div > code';
  572. } else if (headerBlock.parentElement.querySelector('.not-prose pre > code')) {
  573. codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
  574. containerSelectorUsed = '.not-prose pre > code';
  575. } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]')) {
  576. codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]');
  577. containerSelectorUsed = '.not-prose div[style*="background: hsl"]';
  578. }
  579.  
  580. if (codeContainer) {
  581. console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML);
  582. addFilterMenu(headerBlock, codeContainer);
  583. // Сохраняем успешные селекторы
  584. manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer);
  585. } else {
  586. console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML);
  587. logDomStructure(headerBlock);
  588. }
  589. });
  590.  
  591. // Обновляем все кодовые блоки после обработки
  592. refreshAllCodeBlocks();
  593. }
  594.  
  595. // Инициализация
  596. setTimeout(() => {
  597. console.log('Инициализация processCodeBlocks');
  598. processCodeBlocks();
  599. }, 2000);
  600.  
  601. // Наблюдатель за изменениями DOM
  602. const observer = new MutationObserver((mutations) => {
  603. console.log('Обнаружены изменения DOM, вызывается processCodeBlocks');
  604. processCodeBlocks();
  605. });
  606. observer.observe(document.body, {
  607. childList: true,
  608. subtree: true,
  609. attributes: true,
  610. attributeFilter: ['class', 'style']
  611. });
  612.  
  613. // Инициализируем глобальное применение фильтров
  614. refreshAllCodeBlocks();
  615. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址