PageSourceSearch

https://petitfaucheux.fr/modules/supersoniks/ssks_gtranslate/js/ssks_gtranslate_notranslate.js?v=1

js petitfaucheux.fr collected 2026-10-02 05:22:14 UTC 10,380 bytes, 219 lines download raw bytes

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.