Gemini JSON Wrapper – Highlight-Safe,

Unwraps Gemini JSON, soft-fixes \\n, preserves highlight

  1. // ==UserScript==
  2. // @name Gemini JSON Wrapper – Highlight-Safe,
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0
  5. // @description Unwraps Gemini JSON, soft-fixes \\n, preserves highlight
  6. // @author copypaste
  7. // @match *://gemini.google.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const CONFIG = {
  15. wrapStyles: {
  16. whiteSpace: 'pre-wrap',
  17. wordBreak: 'break-word',
  18. overflowWrap: 'break-word',
  19. overflowX: 'auto',
  20. maxWidth: '100%',
  21. fontFamily: 'monospace',
  22. fontSize: '13px',
  23. lineHeight: '1.4'
  24. },
  25. targetSelectors: [
  26. '[data-text-generation-response] pre',
  27. '[data-text-generation-response] code',
  28. '.text-generation-response pre',
  29. '.text-generation-response code',
  30. '.response-output pre',
  31. '.response-output code',
  32. 'pre',
  33. 'code'
  34. ],
  35. checkInterval: 2000,
  36. minJsonLength: 100,
  37. dataAttribute: 'data-json-wrapped'
  38. };
  39.  
  40. function looksLikeJSON(str) {
  41. const trimmed = str.trim();
  42. if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false;
  43. try {
  44. JSON.parse(trimmed);
  45. return true;
  46. } catch {
  47. return false;
  48. }
  49. }
  50.  
  51. function softUnescape(str) {
  52. return str.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
  53. }
  54.  
  55. function processCodeElements() {
  56. let changes = 0;
  57. CONFIG.targetSelectors.forEach(selector => {
  58. document.querySelectorAll(selector).forEach(el => {
  59. if (el.hasAttribute(CONFIG.dataAttribute)) return;
  60.  
  61. const hasSpans = el.innerHTML.includes('<span');
  62. if (!hasSpans) {
  63. const txt = el.textContent.trim();
  64. if (txt.length >= CONFIG.minJsonLength && looksLikeJSON(txt)) {
  65. el.textContent = softUnescape(txt);
  66. changes++;
  67. }
  68. }
  69.  
  70. Object.assign(el.style, CONFIG.wrapStyles);
  71. el.setAttribute(CONFIG.dataAttribute, 'true');
  72. });
  73. });
  74.  
  75. if (changes > 0) console.debug(`[JSON Wrapper] Updated ${changes} elements`);
  76. }
  77.  
  78. function debounce(func, delay) {
  79. let timeoutId;
  80. return function(...args) {
  81. clearTimeout(timeoutId);
  82. timeoutId = setTimeout(() => func.apply(this, args), delay);
  83. };
  84. }
  85.  
  86. const debouncedProcess = debounce(processCodeElements, 500);
  87.  
  88. function initObserver() {
  89. const observer = new MutationObserver((mutations) => {
  90. let shouldProcess = false;
  91. for (const mutation of mutations) {
  92. if (
  93. mutation.type === 'childList' && mutation.addedNodes.length > 0 ||
  94. (mutation.type === 'attributes' &&
  95. CONFIG.targetSelectors.some(sel => sel.includes(mutation.attributeName)))
  96. ) {
  97. shouldProcess = true;
  98. break;
  99. }
  100. }
  101. if (shouldProcess) debouncedProcess();
  102. });
  103.  
  104. const containers = document.querySelectorAll('.response-output, [data-text-generation-response]');
  105. if (containers.length > 0) {
  106. containers.forEach(container => {
  107. observer.observe(container, {
  108. childList: true,
  109. subtree: true,
  110. attributes: true,
  111. attributeFilter: ['class', 'data-text-generation-response']
  112. });
  113. });
  114. } else {
  115. observer.observe(document.body, {
  116. childList: true,
  117. subtree: true
  118. });
  119. }
  120. }
  121.  
  122. function init() {
  123. processCodeElements(); // Initial run
  124. initObserver(); // Watch for dynamic loads
  125.  
  126. setInterval(() => {
  127. debouncedProcess(); // Lazy interval refresh in case observer misses something
  128. }, CONFIG.checkInterval);
  129. }
  130.  
  131. if (document.readyState === 'loading') {
  132. document.addEventListener('DOMContentLoaded', init);
  133. } else {
  134. init();
  135. }
  136.  
  137. })();