Illuvitar

Vimium-style Highlight-All Enhanced

Sep 2nd, 2025
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.02 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Highlight-All Enhanced
  3. // @namespace https://github.com/yourusername/highlight-all-enhanced
  4. // @version 1.1.0
  5. // @description Enhanced highlight script with improved performance and error handling
  6. // @author Your Name
  7. // @match *://*/*
  8. // @grant none
  9. // @run-at document-idle
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // ====================================================================
  16. // ENHANCED STATE AND CONFIGURATION
  17. // ====================================================================
  18.  
  19. let currentObserver = null;
  20. let currentPattern = null;
  21. let isHighlighting = false;
  22. let mutationThrottle = null;
  23. let pendingMutations = new Set();
  24.  
  25. const HIGHLIGHT_CLASS = 'highlight-all-mark';
  26. const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT', 'HEAD', 'TITLE', 'SVG']);
  27. const VALID_FLAGS = new Set(['d', 'g', 'i', 'm', 's', 'u', 'y']);
  28. const MAX_ZERO_LENGTH_ITERATIONS = 1000; // Safety limit for problematic patterns
  29.  
  30. // Enhanced idle callback with better fallback
  31. const scheduleIdleWork = window.requestIdleCallback || function(callback, options = {}) {
  32. const timeout = options.timeout || 50;
  33. return setTimeout(() => {
  34. const start = performance.now();
  35. callback({
  36. timeRemaining: () => Math.max(0, 50 - (performance.now() - start)),
  37. didTimeout: false
  38. });
  39. }, timeout);
  40. };
  41.  
  42. const cancelIdleWork = window.cancelIdleCallback || clearTimeout;
  43.  
  44. // ====================================================================
  45. // ENHANCED PATTERN PARSING WITH STRICT VALIDATION
  46. // ====================================================================
  47.  
  48. /**
  49. * Enhanced pattern parsing with strict flag validation and better error handling
  50. * @param {string} input - User input pattern
  51. * @returns {RegExp|null} Validated and compiled regex
  52. */
  53. function parsePattern(input) {
  54. if (!input || typeof input !== 'string') return null;
  55.  
  56. input = input.trim();
  57. if (!input) return null;
  58.  
  59. // Detect regex literal format: /pattern/flags
  60. const regexMatch = input.match(/^\/(.+?)\/([dgimsuyx]*)$/);
  61.  
  62. if (regexMatch) {
  63. try {
  64. const [, pattern, flags] = regexMatch;
  65.  
  66. // Validate flags - only allow valid JavaScript regex flags
  67. const validFlags = flags.split('').filter(f => VALID_FLAGS.has(f));
  68. const flagString = [...new Set(validFlags)].join(''); // Remove duplicates
  69.  
  70. // Ensure global flag for "highlight all" behavior
  71. const globalFlags = flagString.includes('g') ? flagString : flagString + 'g';
  72.  
  73. return new RegExp(pattern, globalFlags);
  74. } catch (error) {
  75. console.warn('Invalid regex pattern:', input, error.message);
  76. return null;
  77. }
  78. } else {
  79. // Plain string: escape metacharacters with enhanced escaping
  80. try {
  81. const escapedPattern = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  82. return new RegExp(escapedPattern, 'gi');
  83. } catch (error) {
  84. console.warn('Failed to create regex from string:', input, error.message);
  85. return null;
  86. }
  87. }
  88. }
  89.  
  90. // ====================================================================
  91. // ENHANCED DOM TRAVERSAL WITH IMPROVED FILTERING
  92. // ====================================================================
  93.  
  94. /**
  95. * Enhanced TreeWalker filter with better skip logic and shadow DOM awareness
  96. * @param {Node} node - Text node to evaluate
  97. * @returns {number} NodeFilter result
  98. */
  99. function enhancedTextNodeFilter(node) {
  100. // Skip empty or whitespace-only text nodes
  101. if (!node.data || !node.data.trim()) {
  102. return NodeFilter.FILTER_REJECT;
  103. }
  104.  
  105. // Check parent chain for problematic elements
  106. let parent = node.parentNode;
  107. while (parent && parent.nodeType === Node.ELEMENT_NODE) {
  108. // Skip if parent is problematic tag
  109. if (SKIP_TAGS.has(parent.tagName)) {
  110. return NodeFilter.FILTER_REJECT;
  111. }
  112.  
  113. // Skip if already inside highlight mark
  114. if (parent.classList && parent.classList.contains(HIGHLIGHT_CLASS)) {
  115. return NodeFilter.FILTER_REJECT;
  116. }
  117.  
  118. // Skip contenteditable="false" elements
  119. if (parent.contentEditable === 'false') {
  120. return NodeFilter.FILTER_REJECT;
  121. }
  122.  
  123. parent = parent.parentNode;
  124. }
  125.  
  126. return NodeFilter.FILTER_ACCEPT;
  127. }
  128.  
  129. /**
  130. * Create enhanced TreeWalker with improved filtering
  131. * @param {Node} root - Root node to traverse
  132. * @returns {TreeWalker}
  133. */
  134. function createEnhancedTextWalker(root) {
  135. return document.createTreeWalker(
  136. root,
  137. NodeFilter.SHOW_TEXT,
  138. enhancedTextNodeFilter,
  139. false
  140. );
  141. }
  142.  
  143. // ====================================================================
  144. // ENHANCED MATCHING WITH BETTER ZERO-LENGTH PROTECTION
  145. // ====================================================================
  146.  
  147. /**
  148. * Enhanced text node wrapping with improved zero-length match handling
  149. * @param {Text} textNode - Text node to process
  150. * @param {RegExp} regex - Pattern to match
  151. */
  152. function enhancedWrapMatches(textNode, regex) {
  153. if (!textNode || !textNode.data || !textNode.parentNode) return;
  154.  
  155. // Reset regex state
  156. regex.lastIndex = 0;
  157.  
  158. let currentNode = textNode;
  159. let match;
  160. let iterationCount = 0;
  161. const maxIterations = Math.min(MAX_ZERO_LENGTH_ITERATIONS, currentNode.data.length * 2);
  162.  
  163. while ((match = regex.exec(currentNode.data)) !== null && iterationCount < maxIterations) {
  164. iterationCount++;
  165.  
  166. const matchStart = match.index;
  167. const matchLength = match[0].length;
  168.  
  169. // Enhanced zero-length match protection
  170. if (matchLength === 0) {
  171. // If lastIndex didn't advance, manually increment to prevent infinite loop
  172. if (regex.lastIndex === match.index) {
  173. regex.lastIndex = Math.min(match.index + 1, currentNode.data.length);
  174. }
  175. // Break if we've reached the end
  176. if (regex.lastIndex >= currentNode.data.length) break;
  177. continue;
  178. }
  179.  
  180. // Validate match boundaries
  181. if (matchStart < 0 || matchStart >= currentNode.data.length) break;
  182. if (matchStart + matchLength > currentNode.data.length) break;
  183.  
  184. try {
  185. // Split text node at exact match boundaries
  186. let matchNode;
  187.  
  188. if (matchStart > 0) {
  189. matchNode = currentNode.splitText(matchStart);
  190. currentNode = matchNode;
  191. } else {
  192. matchNode = currentNode;
  193. }
  194.  
  195. let afterMatch;
  196. if (matchLength < matchNode.data.length) {
  197. afterMatch = matchNode.splitText(matchLength);
  198. }
  199.  
  200. // Create enhanced highlight mark
  201. const mark = document.createElement('mark');
  202. mark.className = HIGHLIGHT_CLASS;
  203. mark.textContent = matchNode.data;
  204. mark.title = `Highlighted: "${matchNode.data}"`;
  205.  
  206. // Replace match with highlight
  207. matchNode.parentNode.replaceChild(mark, matchNode);
  208.  
  209. // Continue with remaining text
  210. if (afterMatch) {
  211. currentNode = afterMatch;
  212. regex.lastIndex = 0;
  213. } else {
  214. break;
  215. }
  216.  
  217. } catch (error) {
  218. console.warn('Error wrapping match:', error);
  219. break;
  220. }
  221. }
  222.  
  223. if (iterationCount >= maxIterations) {
  224. console.warn('Stopped processing due to iteration limit - possible problematic regex pattern');
  225. }
  226. }
  227.  
  228. // ====================================================================
  229. // ENHANCED HIGHLIGHTING WITH BETTER ERROR HANDLING
  230. // ====================================================================
  231.  
  232. /**
  233. * Enhanced highlighting function with better error handling and progress tracking
  234. * @param {string} query - Search query
  235. */
  236. function enhancedHighlightAll(query) {
  237. if (isHighlighting) {
  238. console.log('Highlighting already in progress...');
  239. return;
  240. }
  241.  
  242. const regex = parsePattern(query);
  243. if (!regex) {
  244. alert('Invalid search pattern. Use plain text or /regex/flags format.');
  245. return;
  246. }
  247.  
  248. isHighlighting = true;
  249. currentPattern = regex;
  250.  
  251. try {
  252. // Clear existing highlights
  253. clearAllHighlights();
  254.  
  255. // Find root element with fallback
  256. const root = document.body || document.documentElement || document;
  257. if (!root) {
  258. throw new Error('No valid root element found');
  259. }
  260.  
  261. // Perform highlighting with progress tracking
  262. const startTime = performance.now();
  263. highlightInSubtree(root, regex);
  264. const endTime = performance.now();
  265.  
  266. console.log(`Highlighted pattern "${query}" in ${(endTime - startTime).toFixed(2)}ms`);
  267.  
  268. // Setup enhanced persistence
  269. setupEnhancedPersistence();
  270.  
  271. } catch (error) {
  272. console.error('Error during highlighting:', error);
  273. alert('Error occurred during highlighting. Check console for details.');
  274. currentPattern = null;
  275. } finally {
  276. isHighlighting = false;
  277. }
  278. }
  279.  
  280. /**
  281. * Highlight matches in subtree with enhanced error handling
  282. * @param {Node} root - Root to search
  283. * @param {RegExp} regex - Pattern to match
  284. */
  285. function highlightInSubtree(root, regex) {
  286. const walker = createEnhancedTextWalker(root);
  287. const textNodes = [];
  288.  
  289. // Collect text nodes with size limit for performance
  290. let node;
  291. let nodeCount = 0;
  292. const maxNodes = 10000; // Prevent memory issues on huge pages
  293.  
  294. while ((node = walker.nextNode()) && nodeCount < maxNodes) {
  295. textNodes.push(node);
  296. nodeCount++;
  297. }
  298.  
  299. if (nodeCount >= maxNodes) {
  300. console.warn('Stopped at node limit for performance. Some text may not be highlighted.');
  301. }
  302.  
  303. // Process nodes in batches for responsiveness
  304. const batchSize = 50;
  305. let processedCount = 0;
  306.  
  307. function processBatch() {
  308. const endIndex = Math.min(processedCount + batchSize, textNodes.length);
  309.  
  310. for (let i = processedCount; i < endIndex; i++) {
  311. const textNode = textNodes[i];
  312. // Revalidate node is still in DOM
  313. if (textNode.parentNode && textNode.data) {
  314. try {
  315. enhancedWrapMatches(textNode, regex);
  316. } catch (error) {
  317. console.warn('Error processing text node:', error);
  318. }
  319. }
  320. }
  321.  
  322. processedCount = endIndex;
  323.  
  324. // Schedule next batch if more work remains
  325. if (processedCount < textNodes.length) {
  326. scheduleIdleWork(processBatch, { timeout: 100 });
  327. }
  328. }
  329.  
  330. processBatch();
  331. }
  332.  
  333. // ====================================================================
  334. // ENHANCED PERSISTENCE WITH THROTTLING
  335. // ====================================================================
  336.  
  337. /**
  338. * Setup enhanced mutation observer with throttling and coalescing
  339. */
  340. function setupEnhancedPersistence() {
  341. if (!currentPattern) return;
  342.  
  343. currentObserver = new MutationObserver((mutations) => {
  344. // Coalesce mutations to reduce redundant processing
  345. mutations.forEach(mutation => {
  346. if (mutation.type === 'childList') {
  347. mutation.addedNodes.forEach(node => {
  348. if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) {
  349. pendingMutations.add(node);
  350. }
  351. });
  352. } else if (mutation.type === 'characterData') {
  353. pendingMutations.add(mutation.target);
  354. }
  355. });
  356.  
  357. // Throttle processing to avoid overwhelming the browser
  358. if (!mutationThrottle && pendingMutations.size > 0) {
  359. mutationThrottle = scheduleIdleWork(() => {
  360. processQueuedMutations();
  361. mutationThrottle = null;
  362. }, { timeout: 200 });
  363. }
  364. });
  365.  
  366. // Enhanced observation options
  367. currentObserver.observe(document.body || document.documentElement, {
  368. childList: true,
  369. subtree: true,
  370. characterData: true,
  371. characterDataOldValue: false // Don't need old values for performance
  372. });
  373. }
  374.  
  375. /**
  376. * Process queued mutations efficiently
  377. */
  378. function processQueuedMutations() {
  379. if (!currentPattern || pendingMutations.size === 0) return;
  380.  
  381. const mutations = Array.from(pendingMutations);
  382. pendingMutations.clear();
  383.  
  384. mutations.forEach(node => {
  385. if (!node.parentNode) return; // Node was removed
  386.  
  387. try {
  388. if (node.nodeType === Node.TEXT_NODE) {
  389. // Skip if inside existing highlight
  390. let parent = node.parentNode;
  391. while (parent && parent.nodeType === Node.ELEMENT_NODE) {
  392. if (parent.classList && parent.classList.contains(HIGHLIGHT_CLASS)) {
  393. return;
  394. }
  395. parent = parent.parentNode;
  396. }
  397. enhancedWrapMatches(node, currentPattern);
  398. } else if (node.nodeType === Node.ELEMENT_NODE) {
  399. highlightInSubtree(node, currentPattern);
  400. }
  401. } catch (error) {
  402. console.warn('Error processing mutation:', error);
  403. }
  404. });
  405. }
  406.  
  407. // ====================================================================
  408. // ENHANCED CLEARING WITH BETTER NORMALIZATION
  409. // ====================================================================
  410.  
  411. /**
  412. * Enhanced clearing with improved text normalization
  413. */
  414. function clearAllHighlights() {
  415. // Disconnect observer during clearing
  416. if (currentObserver) {
  417. currentObserver.disconnect();
  418. currentObserver = null;
  419. }
  420.  
  421. // Clear throttle
  422. if (mutationThrottle) {
  423. cancelIdleWork(mutationThrottle);
  424. mutationThrottle = null;
  425. }
  426.  
  427. pendingMutations.clear();
  428.  
  429. try {
  430. // Enhanced mark removal with batch processing
  431. const marks = Array.from(document.querySelectorAll(`mark.${HIGHLIGHT_CLASS}`));
  432.  
  433. // Process in batches for large numbers of highlights
  434. const batchSize = 100;
  435. let processed = 0;
  436.  
  437. function clearBatch() {
  438. const endIndex = Math.min(processed + batchSize, marks.length);
  439.  
  440. for (let i = processed; i < endIndex; i++) {
  441. const mark = marks[i];
  442. if (mark.parentNode) {
  443. const textNode = document.createTextNode(mark.textContent);
  444. mark.parentNode.replaceChild(textNode, mark);
  445. }
  446. }
  447.  
  448. processed = endIndex;
  449.  
  450. if (processed < marks.length) {
  451. scheduleIdleWork(clearBatch, { timeout: 50 });
  452. } else {
  453. // Final normalization
  454. finalizeClearing();
  455. }
  456. }
  457.  
  458. function finalizeClearing() {
  459. // Enhanced text normalization
  460. const root = document.body || document.documentElement;
  461. if (root && root.normalize) {
  462. root.normalize();
  463. }
  464. currentPattern = null;
  465. console.log('All highlights cleared');
  466. }
  467.  
  468. if (marks.length > 0) {
  469. clearBatch();
  470. } else {
  471. finalizeClearing();
  472. }
  473.  
  474. } catch (error) {
  475. console.error('Error clearing highlights:', error);
  476. currentPattern = null;
  477. }
  478. }
  479.  
  480. // ====================================================================
  481. // ENHANCED KEYBOARD SHORTCUTS
  482. // ====================================================================
  483.  
  484. /**
  485. * Enhanced keyboard shortcut handling with better key detection
  486. */
  487. function handleEnhancedKeyboardShortcuts(event) {
  488. // Ignore if user is typing in input fields
  489. const activeElement = document.activeElement;
  490. const isInputField = activeElement && (
  491. activeElement.tagName === 'INPUT' ||
  492. activeElement.tagName === 'TEXTAREA' ||
  493. activeElement.contentEditable === 'true'
  494. );
  495.  
  496. // Ctrl+Shift+H: New search (case-insensitive)
  497. if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'h') {
  498. event.preventDefault();
  499. event.stopPropagation();
  500.  
  501. const currentQuery = currentPattern ? currentPattern.source : '';
  502. const query = prompt('Enter search pattern (plain text or /regex/flags):', currentQuery);
  503.  
  504. if (query !== null && query.trim()) {
  505. enhancedHighlightAll(query.trim());
  506. }
  507. }
  508.  
  509. // Ctrl+Shift+C or Escape: Clear highlights (case-insensitive)
  510. if (((event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'c') ||
  511. event.key === 'Escape') && currentPattern) {
  512. event.preventDefault();
  513. event.stopPropagation();
  514. clearAllHighlights();
  515. }
  516.  
  517. // Ctrl+Shift+I: Show current pattern info
  518. if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'i') {
  519. event.preventDefault();
  520. if (currentPattern) {
  521. const marks = document.querySelectorAll(`mark.${HIGHLIGHT_CLASS}`);
  522. alert(`Active pattern: ${currentPattern}\nMatches found: ${marks.length}`);
  523. } else {
  524. alert('No active highlighting pattern');
  525. }
  526. }
  527. }
  528.  
  529. // ====================================================================
  530. // ENHANCED PUBLIC API
  531. // ====================================================================
  532.  
  533. window.highlightAll = {
  534. run: enhancedHighlightAll,
  535. clear: clearAllHighlights,
  536. getCurrentPattern: () => currentPattern,
  537. isActive: () => currentPattern !== null,
  538. getMatchCount: () => document.querySelectorAll(`mark.${HIGHLIGHT_CLASS}`).length,
  539.  
  540. // New enhanced methods
  541. validate: (pattern) => parsePattern(pattern) !== null,
  542. getStats: () => ({
  543. pattern: currentPattern ? currentPattern.toString() : null,
  544. matches: document.querySelectorAll(`mark.${HIGHLIGHT_CLASS}`).length,
  545. isActive: currentPattern !== null,
  546. pendingMutations: pendingMutations.size
  547. })
  548. };
  549.  
  550. // ====================================================================
  551. // ENHANCED INITIALIZATION
  552. // ====================================================================
  553.  
  554. // Enhanced CSS with better visibility and accessibility
  555. const enhancedStyle = document.createElement('style');
  556. enhancedStyle.textContent = `
  557. .${HIGHLIGHT_CLASS} {
  558. background-color: #ffff00;
  559. background-image: linear-gradient(135deg, #ffff00 25%, transparent 25%,
  560. transparent 50%, #ffff00 50%, #ffff00 75%,
  561. transparent 75%, transparent);
  562. background-size: 4px 4px;
  563. color: #000000;
  564. padding: 1px 2px;
  565. margin: 0;
  566. border-radius: 3px;
  567. border: 1px solid #e6e600;
  568. box-shadow: 0 1px 2px rgba(0,0,0,0.1);
  569. position: relative;
  570. line-height: 1.2;
  571. }
  572.  
  573. .${HIGHLIGHT_CLASS}:hover {
  574. background-color: #ffff66;
  575. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  576. }
  577.  
  578. .${HIGHLIGHT_CLASS}::before {
  579. content: attr(title);
  580. position: absolute;
  581. bottom: 100%;
  582. left: 50%;
  583. transform: translateX(-50%);
  584. background: rgba(0,0,0,0.8);
  585. color: white;
  586. padding: 4px 8px;
  587. border-radius: 4px;
  588. font-size: 12px;
  589. white-space: nowrap;
  590. opacity: 0;
  591. pointer-events: none;
  592. transition: opacity 0.2s;
  593. z-index: 9999;
  594. }
  595.  
  596. .${HIGHLIGHT_CLASS}:hover::before {
  597. opacity: 1;
  598. }
  599.  
  600. /* High contrast mode support */
  601. @media (prefers-contrast: high) {
  602. .${HIGHLIGHT_CLASS} {
  603. background-color: #000000;
  604. color: #ffffff;
  605. border: 2px solid #ffffff;
  606. }
  607. }
  608.  
  609. /* Reduced motion support */
  610. @media (prefers-reduced-motion: reduce) {
  611. .${HIGHLIGHT_CLASS} {
  612. transition: none;
  613. }
  614. .${HIGHLIGHT_CLASS}::before {
  615. transition: none;
  616. }
  617. }
  618. `;
  619. document.head.appendChild(enhancedStyle);
  620.  
  621. // Enhanced event listeners with better error handling
  622. document.addEventListener('keydown', handleEnhancedKeyboardShortcuts, true);
  623.  
  624. // Cleanup on page unload
  625. window.addEventListener('beforeunload', () => {
  626. if (currentObserver) {
  627. currentObserver.disconnect();
  628. }
  629. if (mutationThrottle) {
  630. cancelIdleWork(mutationThrottle);
  631. }
  632. });
  633.  
  634. // Enhanced initialization message
  635. console.log(`
  636. 🔍 Highlight-All Enhanced v1.1.0 loaded successfully!
  637.  
  638. Keyboard shortcuts:
  639. • Ctrl+Shift+H: Search/highlight text
  640. • Ctrl+Shift+C or Escape: Clear highlights
  641. • Ctrl+Shift+I: Show pattern info
  642.  
  643. API methods:
  644. • window.highlightAll.run(pattern)
  645. • window.highlightAll.clear()
  646. • window.highlightAll.getStats()
  647. • window.highlightAll.validate(pattern)
  648. `);
  649.  
  650. })();
  651.  
Advertisement
Add Comment
Please, Sign In to add comment