1(function ($) { 2 Drupal.behaviors.ssks_gtranslate_notranslate = { 3 attach: function (context, settings) { 4 excludeAdminElementsTranslation(context); 5 excludeEmailLinksTranslation(); 6 protectExcludedWords(context, settings); 7 protectLeadingPunctuation(context); 8 }, 9 }; 10 11 function excludeAdminElementsTranslation(context) { 12 const selector = "#toolbar-administration, .toolbar-menu, .block-local-tasks-block, .block-local-actions-block, .contextual, [data-contextual-id]"; 13 $(context).find(selector).addBack(selector) 14 .addClass("notranslate") 15 .attr("translate", "no"); 16 } 17 18 function excludeEmailLinksTranslation() { 19 // add class notranslate to all email 20 $(once("notranlate-class-mail", "a[href*=mailto]")).each(function() { 21 $(this).addClass("notranslate"); 22 }); 23 } 24 25 function protectLeadingPunctuation(context) { 26 const root = context === document ? document.body : context; 27 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); 28 const nodesToProcess = []; 29 let node; 30 while ((node = walker.nextNode())) { 31 const match = node.textContent.match(/^[,.â¦]+\s*/); 32 if (!match || !node.parentElement.closest(".text-formatted") || 33 node.parentElement.closest('.notranslate, [translate="no"], script, style, noscript, template')) { 34 continue; 35 } 36 nodesToProcess.push({node, length: match[0].length}); 37 } 38 39 // Google ajoute parfois un espace avant la ponctuation suivant un élément inline. 40 // Conserver cette ponctuation et ses espaces dans un fragment protégé. 41 nodesToProcess.forEach(({node, length}) => { 42 node.splitText(length); 43 const span = document.createElement("span"); 44 span.className = "notranslate"; 45 node.replaceWith(span); 46 span.appendChild(node); 47 }); 48 } 49 50 function protectExcludedWords(context, settings) { 51 // Get all excluded words, clean them (remove empty string, escape regex chars, order them) 52 let excluded_words = settings.ssks_gtranslate.excluded_words; 53 excluded_words = cleanWords(excluded_words); 54 if (!excluded_words.length) return; 55 56 // Get the target element from the context. 57 // Can be the document or a DOM element 58 // Typically to process ajax content, like the widget horaires 59 const $targetElement = (context === document) ? $("body") : $(context); 60 // Build the regex to match at least one word 61 const wordsRegex = new RegExp(`(?:^|(?<=[^a-zA-Z\\d'ââ²]))(${excluded_words.join("|")})(?:(?=[^a-zA-Z\\d])|$)`, "i"); 62 $(once("notranlate-class-added", $targetElement)).each((k, el) => { 63 const queue = [el]; 64 let curr; 65 while ((curr = queue.pop())) { 66 // Utiliser TreeWalker pour ne traiter que les nÅuds de texte 67 const walker = document.createTreeWalker( 68 curr, 69 NodeFilter.SHOW_TEXT, 70 { 71 acceptNode: function(node) { 72 const parent = node.parentElement; 73 if (!parent) return NodeFilter.FILTER_ACCEPT; 74 75 // Respecter aussi les exclusions portées par les éléments englobants. 76 if (parent.closest('.notranslate, [translate="no"]')) { 77 return NodeFilter.FILTER_REJECT; 78 } 79 80 // Skip les contenus bruts non-DOM : noscript, script, style, template 81 // (leur "texte" peut contenir du HTML brut, ex: fallback d'images lazy-load) 82 const tag = parent.tagName; 83 if (tag === "NOSCRIPT" || tag === "SCRIPT" || tag === "STYLE" || tag === "TEMPLATE") { 84 return NodeFilter.FILTER_REJECT; 85 } 86 87 return NodeFilter.FILTER_ACCEPT; 88 }, 89 }, 90 false, 91 ); 92 93 // On collecte d'abord tous les nÅuds à traiter SANS muter le DOM : 94 // remplacer un nÅud en cours de traversée détache le nÅud courant du 95 // TreeWalker, et nextNode() s'arrête alors après le premier remplacement. 96 const nodesToProcess = []; 97 let textNode; 98 while ((textNode = walker.nextNode())) { 99 if (textNode.textContent.match(new RegExp(wordsRegex.source, "gi"))) { 100 nodesToProcess.push(textNode); 101 } 102 } 103 104 // La traversée terminée, on peut muter le DOM sans casser le walker. 105 nodesToProcess.forEach((node) => { 106 const textRegex = new RegExp(`(\\s*)${wordsRegex.source}(\\s*)`, "gi"); 107 // Créer un fragment de document pour le nouveau contenu 108 const fragment = document.createDocumentFragment(); 109 const tempDiv = document.createElement("div"); 110 tempDiv.innerHTML = node.textContent.replace( 111 textRegex, 112 (fullMatch, spaceBefore, match, spaceAfter, offset, string) => { 113 // Si le match touche une borne du texte local, ce n'est pas forcément 114 // un début/fin réel : le texte peut continuer dans un nÅud frère situé 115 // hors de l'élément inline courant (ex: "<strong>mot</strong>, suite"). 116 const charBefore = offset > 0 ? string[offset - 1] : getAdjacentChar(node, "before"); 117 const charAfter = (offset + fullMatch.length < string.length) ? string[offset + fullMatch.length] : getAdjacentChar(node, "after"); 118 const hasSpaceBefore = !spaceBefore && (c
118harBefore === undefined || /\s/.test(charBefore)); 119 const hasSpaceAfter = !spaceAfter && (charAfter === undefined || /\s/.test(charAfter)); 120 const classes = ["notranslate"]; 121 if (hasSpaceBefore) { 122 classes.push("hasspacebefore"); 123 } 124 if (hasSpaceAfter) { 125 classes.push("hasspaceafter") 126 } 127 // Protéger les espaces du texte lui-même, sans les dupliquer en CSS. 128 return `<span class="${classes.join(" ")}">${spaceBefore}${match}${spaceAfter}</span>`; 129 }, 130 ); 131 132 // Transférer les nÅuds du div temporaire vers le fragment 133 while (tempDiv.firstChild) { 134 fragment.appendChild(tempDiv.firstChild); 135 } 136 137 // Remplacer le nÅud de texte par le fragment 138 node.parentNode.replaceChild(fragment, node); 139 }); 140 } 141 }); 142 } 143 144 // Ãléments inline usuels : traverser leurs frontières pour retrouver le 145 // caractère réel adjacent (ex: "<strong>mot</strong>, suite" -> ','). 146 const INLINE_TAGS = new Set([ 147 "A", "ABBR", "B", "BDI", "BDO", "CITE", "CODE", "DATA", "DEL", "DFN", 148 "EM", "FONT", "I", "INS", "KBD", "LABEL", "MARK", "Q", "S", "SAMP", 149 "SMALL", "SPAN", "STRONG", "SUB", "SUP", "TIME", "U", "VAR", 150 ]); 151 152 function getLastTextChar(node) { 153 if (node.nodeType === Node.TEXT_NODE) { 154 return node.textContent.length ? node.textContent[node.textContent.length - 1] : null; 155 } 156 for (let i = node.childNodes.length - 1; i >= 0; i--) { 157 const char = getLastTextChar(node.childNodes[i]); 158 if (char !== null) return char; 159 } 160 return null; 161 } 162 163 function getFirstTextChar(node) { 164 if (node.nodeType === Node.TEXT_NODE) { 165 return node.textContent.length ? node.textContent[0] : null; 166 } 167 for (let i = 0; i < node.childNodes.length; i++) { 168 const char = getFirstTextChar(node.childNodes[i]); 169 if (char !== null) return char; 170 } 171 return null; 172 } 173 174 /** 175 * Cherche le caractère réel précédant/suivant un nÅud de texte, en 176 * franchissant les éléments inline englobants (ex: <strong>, <em>) mais 177 * pas les éléments de type bloc, dont la frontière est traitée comme un 178 * espace (comportement inchangé pour ce cas). 179 * 180 * @param {Text} textNode 181 * @param {"before"|"after"} direction 182 * @return {string|undefined} 183 */ 184 function getAdjacentChar(textNode, direction) { 185 let current = textNode; 186 while (current) { 187 const sibling = direction === "before" ? current.previousSibling : current.nextSibling; 188 if (sibling) { 189 const char = direction === "before" ? getLastTextChar(sibling) : getFirstTextChar(sibling); 190 if (char !== null) return char; 191 current = sibling; 192 continue; 193 } 194 const parent = current.parentElement; 195 if (!parent || !INLINE_TAGS.has(parent.tagName)) { 196 return undefined; 197 } 198 current = parent; 199 } 200 return undefined; 201 } 202 203 /** 204 * Cleans up and processes an array of words by removing empty strings, 205 * sorting them by length in descending order, and escaping special 206 * characters. 207 * 208 * @param {string[]} words - The array of words to be cleaned and processed. 209 * @return {string[]} The cleaned and processed array of words. 210 */ 211 function cleanWords(words) { 212 // Order words by length, bigger to smaller, to be sure to parse all words 213 // Exemple : parse "Le Rocher de Palmer" before "Le Rocher" 214 return words.filter((w) => w.trim().length) 215 .sort((a, b) => b.length - a.length) 216 .map((str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); 217 } 218 219})(jQuery, Drupal, drupalSettings);
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.