1import { Controller } from "@hotwired/stimulus"; 2 3export default class extends Controller { 4 static tailwindHookRegistered = false; 5 static values = { 6 hideUntilReady: { type: Boolean, default: true }, 7 }; 8 9 connect() { 10 // Hide the element initially to prevent FOUC 11 const codeElement = this.element.tagName === "CODE" ? this.element : this.element.querySelector("code"); 12 if (this.hideUntilReadyValue && (!codeElement || !codeElement.classList.contains("prism-highlighted"))) { 13 this.element.style.visibility = "hidden"; 14 } 15 16 // Listen for turbo:before-cache to clean up state before caching 17 this.boundBeforeCache = this.beforeCache.bind(this); 18 document.addEventListener("turbo:before-cache", this.boundBeforeCache); 19 20 // Register Tailwind CSS hook once 21 this.registerTailwindHook(); 22 23 // Wait for Prism and required language components 24 this.waitForPrism(); 25 } 26 27 registerTailwindHook() { 28 // Only register once globally 29 if (this.constructor.tailwindHookRegistered) return; 30 if (typeof Prism === "undefined" || !Prism.hooks) return; 31 32 this.constructor.tailwindHookRegistered = true; 33 34 // Add hook to fix Tailwind syntax after CSS highlighting 35 Prism.hooks.add("after-highlight", (env) => { 36 if (env.language !== "css") return; 37 38 // Process all atrule spans that contain @apply 39 const atruleSpans = env.element.querySelectorAll(".token.atrule"); 40 41 atruleSpans.forEach((span) => { 42 const ruleSpan = span.querySelector(".token.rule"); 43 if (!ruleSpan || ruleSpan.textContent !== "@apply") return; 44 45 // Get all text content after @apply 46 let textContent = ""; 47 let node = ruleSpan.nextSibling; 48 while (node) { 49 textContent += node.textContent || ""; 50 node = node.nextSibling; 51 } 52 53 // Remove all nodes after the rule span 54 while (ruleSpan.nextSibling) { 55 ruleSpan.nextSibling.remove(); 56 } 57 58 // Add styled utility spans 59 const utilities = textContent.trim().split(/\s+/); 60 utilities.forEach((utility, index) => { 61 if (utility) { 62 if (index > 0 || textContent.startsWith(" ")) { 63 span.appendChild(document.createTextNode(" ")); 64 } 65 const utilSpan = document.createElement("span"); 66 utilSpan.className = "token tailwind-utility"; 67 utilSpan.textContent = utility; 68 span.appendChild(utilSpan); 69 } 70 }); 71 }); 72 }); 73 } 74 75 disconnect() { 76 document.removeEventListener("turbo:before-cache", this.boundBeforeCache); 77 } 78 79 beforeCache() { 80 // Remove prism-highlighted class before Turbo caches the page 81 // This ensures the code will be re-highlighted when the page is restored 82 const codeElement = this.element.tagName === "CODE" ? this.element : this.element.querySelector("code"); 83 if (codeElement) { 84 codeElement.classList.remove("prism-highlighted"); 85 } 86 this.element.classList.remove("prism-processed"); 87 } 88 89 highlightCode() { 90 const codeElement = this.element.tagName === "CODE" ? this.element : this.element.querySelector("code"); 91 if (codeElement) { 92 if (!codeElement.classList.contains("prism-highlighted")) { 93 // Map language classes for Prism compatibility 94 this.mapLanguageClass(codeElement); 95 96 try { 97 Prism.highlightElement(codeElement); 98 codeElement.classList.add("prism-highlighted"); 99 } catch (e) { 100 console.warn("Prism highlighting failed:", e); 101 } 102 } 103 } else { 104 // Fallback, though the snippet structure should ensure a <code> element exists 105 this.element.querySelectorAll("pre code").forEach((block) => { 106 if (!block.classList.contains("prism-highlighted")) { 107 this.mapLanguageClass(block); 108 try { 109 Prism.highlightElement(block); 110 block.classList.add("prism-highlighted"); 111 } catch (e) { 112 console.warn("Prism highlighting failed:", e); 113 } 114 } 115 }); 116 } 117 118 // Mark as processed and show the element 119 this.element.classList.add("prism-processed"); 120 this.element.style.visibility = "visible"; 121 122 // Dispatch event to notify other controllers that highlighting is complete 123 this.element.dispatchEvent(new CustomEvent("highlight:complete", { bubbles: true })); 124 } 125 126 mapLanguageClass(element) { 127 // Map common language aliases to Prism's expected classes 128 const classMap = { 129 "language-html": "language-markup", 130 "language-xml": "language-markup", 131 }; 132 133 for (const [from, to] of Object.entries(classMap)) { 134 if (element.classList.contains(from)) { 135 element.classList.remove(from); 136 element.classList.add(to); 137 } 138 } 139 } 140 141 getRequiredLanguage() { 142 const codeElement = this.element.tagName === "CODE" ? this.element : this.element.querySelector("code"); 143 if (!codeElement) return null; 144 145 // Extract language from class like "language-erb" -> "erb" 146 const match = codeElement.className.match(/language-(\w+)/); 147 return match ? match[1] : null; 148 } 149 150 isPrismReady() { 151 if (typeof Prism === "undefined" || !Prism.languages) return false;
152 153 const lang = this.getRequiredLanguage(); 154 if (!lang) return true; // No specific language, Prism core is enough 155 156 // Map language names to what Prism expects 157 const langMap = { 158 html: "markup", 159 xml: "markup", 160 }; 161 const prismLang = langMap[lang] || lang; 162 163 // Check if the language is loaded 164 // For ERB, also check that markup-templating hooks are available 165 if (prismLang === "erb") { 166 return Prism.languages.erb && Prism.languages.ruby && Prism.hooks; 167 } 168 169 return Prism.languages[prismLang] !== undefined; 170 } 171 172 waitForPrism() { 173 let attempts = 0; 174 const maxAttempts = 60; // 3 seconds / 50ms 175 176 const checkForPrism = () => { 177 if (this.isPrismReady()) { 178 this.highlightCode(); 179 } else if (attempts < maxAttempts) { 180 attempts++; 181 setTimeout(checkForPrism, 50); 182 } else { 183 console.warn("Prism.js or required language failed to load within 3 seconds"); 184 // Show the element even if highlighting failed 185 this.element.style.visibility = "visible"; 186 } 187 }; 188 189 checkForPrism(); 190 } 191};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.