PageSourceSearch

https://railsblocks.com/assets/controllers/highlight_controller-2e…226811ca1d4d99ded02a416828bd0cd03d993b36.js

js railsblocks.com collected 2026-09-25 20:23:26 UTC 6,347 bytes, 191 lines download raw bytes

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.