PageSourceSearch

https://latinprayer.org/wp-content/plugins/dwtexttools/assets/js/dwtexttools-util.js?ver=1776418656

js latinprayer.org collected 2026-10-02 01:07:19 UTC 7,678 bytes, 185 lines download raw bytes

1/**
2 * WHAT: Stateless shared helpers — string cleaning, clipboard, network,
3 *       HTML escaping, verdict labels, language tables.
4 * WHY:  Every other module uses these. Keeping them separate means each has
5 *       a single, obvious home and is swappable in isolation.
6 * DEPENDS ON: dwtextToolsConfig (only read lazily inside postJSON / submitFeedback).
7 * EXPORTS on window.DwTextTools.util:
8 *   AI_URLS, LANG_NAMES, LANG_LABELS,
9 *   normalizeWhitespace, cleanVerseTextForOutput,
10 *   copyToClipboard, postJSON, submitFeedback,
11 *   escapeHtml, verdictLabel
12 */
13
14(function () {
15    'use strict';
16
17    var ns   = (window.DwTextTools = window.DwTextTools || {});
18
19    // -----------------------------------------------------------------------
20    // Constants
21    // -----------------------------------------------------------------------
22
23    var AI_URLS = {
24        grok:    'https://grok.com/?q=',
25        chatgpt: 'https://chatgpt.com/?q=',
26        claude:  'https://claude.ai/new?q='
27    };
28
29    /** Language-code → English name (used in AI prompts etc). */
30    var LANG_NAMES  = { en: 'English', de: 'German',  es: 'Spanish', fr: 'French' };
31    /** Language-code → human label shown in the UI. */
32    var LANG_LABELS = { en: 'English', de: 'Deutsch', es: 'Español', fr: 'Français' };
33
34    // -----------------------------------------------------------------------
35    // String cleaning (ported from dwbible-frontend.js)
36    // -----------------------------------------------------------------------
37
38    /**
39     * Normalize Unicode whitespace to ASCII space — mirrors PHP normalize_whitespace().
40     */
41    function normalizeWhitespace(s) {
42        if (!s) return '';
43        s = String(s);
44        var map = {
45            '\u00A0':' ','\u00AD':'','\u1680':' ',
46            '\u2000':' ','\u2001':' ','\u2002':' ','\u2003':' ','\u2004':' ',
47            '\u2005':' ','\u2006':' ','\u2007':' ','\u2008':' ','\u2009':' ',
48            '\u200A':' ','\u200B':'','\u200C':'','\u200D':'','\u200E':'',
49            '\u200F':'','\u2028':' ','\u2029':' ','\u202F':' ','\u2060':' ','\uFEFF':''
50        };
51        s = s.replace(/[\u00A0\u00AD\u1680\u2000-\u200F\u2028\u2029\u202F\u2060\uFEFF]/g, function (ch) {
52            return Object.prototype.hasOwnProperty.call(map, ch) ? map[ch] : ' ';
53        });
54        return s.replace(/\s+/g, ' ').trim();
55    }
56
57    /**
58     * Clean verse text for sharing: strip control chars, balance guillemets, wrap in » «.
59     * Mirrors PHP clean_verse_text_for_output().
60     */
61    function cleanVerseTextForOutput(s) {
62        var qL = '\u00BB', qR = '\u00AB';
63        s = normalizeWhitespace(s);
64        if (!s) return s;
65        s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
66        try { s = s.replace(new RegExp('[^\\p{L}\\p{N}\\p{P}\\p{S}\\s]', 'gu'), ''); } catch (e) {}
67
68        var hasL = s.indexOf(qR) !== -1, hasR = s.indexOf(qL) !== -1;
69        if (hasR && !hasL) { s += qR; hasL = true; }
70        else if (hasL && !hasR) { s = qL + s; hasR = true; }
71        if (hasL && hasR) {
72            s = s.replace(/[\u00AB\u00BB]/g, function (ch) { return ch === qR ? '\u2039' : '\u203A'; });
73        }
74        if (s.length >= 2 && s.slice(0, 2) === (qL + '\u203A') && s.slice(-2) === ('\u2039' + qR)) {
75            s = qL + s.slice(2, -2) + qR;
76        }
77        s = s.trim();
78        s = s.replace(/\s*[\u2013\u2014]\s*([\u00AB\u00BB\u2039\u203A])\s*$/u, '$1');
79        s = s.replace(/[\u2013\u2014]\s*$/u, '').trim();
80
81        if (s.length >= 2 && s.charAt(0) === qL && s.charAt(s.length - 1) === qR) { /* already wrapped */ }
82        else if (s.length >= 2 && s.charAt(0) === '\u203A' && s.charAt(s.length - 1) === '\u2039') {
83            s = qL + s.substring(1, s.length - 1) + qR;
84        } else {
85            s = qL + s + qR;
86        }
87        return s;
88    }
89
90    // -----------------------------------------------------------------------
91    // Clipboard
92    // -----------------------------------------------------------------------
93
94    /**
95     * Write text to the clipboard. Uses Clipboard API on HTTPS, falls back
96     * to a hidden textarea + execCommand on HTTP / older browsers.
97     */
98    function copyToClipboard(text, onSuccess) {
99        if (navigator.clipboard && navigator.clipboard.writeText) {
100            navigator.clipboard.writeText(text).then(onSuccess);
101            return;
102        }
103        var ta = document.createElement('textarea');
104        ta.value = text;
105        ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none;';
106        document.body.appendChild(ta);
107        ta.focus();
108        ta.select();
109        try { document.execCommand('copy'); onSuccess(); } catch (err) { /* silent */ }
110        document.body.removeChild(ta);
111    }
112
113    // -----------------------------------------------------------------------
114    // Network — REST JSON POST
115    // -----------------------------------------------------------------------
116
117    /**
118     * POST a JSON payload to a REST URL. Always includes the WP nonce when
119     * one is configured. Calls `callback` with the parsed response object,
120     * or with null on any network / parse failure.
121     */
122    function postJSON(url, data, callback) {
123        var nonce = (typeof dwtextToolsConfig !== 'undefined') ? dwtextToolsConfig.nonce : '';
124        if (!url) { callback(null); return; }
125
126        var xhr = new XMLHttpRequest();
127        xhr.open('POST', url, true);
128        xhr.setRequestHeader('Content-Type', 'application/json');
129        if (nonce) xhr.setRequestHeader('X-WP-Nonce', nonce);
130
131        xhr.onload = function () {
132            try   { callback(JSON.parse(xhr.responseText)); }
133            catch (e) { callback(null); }
134        };
135        xhr.onerror = function () { callback(null); };
136        xhr.send(JSON.stringify(data));
137    }
138
139    /**
140     * Convenience wrapper for the feedback endpoint: unwraps the response
141     * and only passes `true` / `false` to the callback.
142     */
143    function submitFeedback(payload, callback) {
144        var url = (typeof dwtextToolsConfig !== 'undefined') ? dwtextToolsConfig.feedbackUrl : '';
145        postJSON(url, payload, function (res) {
146            callback(!!(res && res.ok === true));
147        });
148    }
149
150    // -----------------------------------------------------------------------
151    // DOM / display helpers
152    // -----------------------------------------------------------------------
153
154    /** Minimal HTML-entity escaping for safe insertion via innerHTML. */
155    function escapeHtml(str) {
156        var div = document.createElement('div');
157        div.appendChild(document.createTextNode(str));
158        return div.innerHTML;
159    }
160
161    /** Map AI verdict code → display label. */
162    function verdictLabel(verdict) {
163        if (verdict === 'approve') return '\u2705 Looks good';
164        if (verdict === 'suggest') return '\uD83D\uDCA1 Consider';
165        if (verdict === 'warn')    return '\u26A0 Careful';
166        return verdict || 'Result';
167    }
168
169    // -----------------------------------------------------------------------
170    // Export
171    // -----------------------------------------------------------------------
172
173    ns.util = {
174        AI_URLS:                AI_URLS,
175        LANG_NAMES:             LANG_NAMES,
176        LANG_LABELS:            LANG_LABELS,
177        normalizeWhitespace:    normalizeWhitespace,
178        cleanVerseTextForOutput: cleanVerseTextForOutput,
179        copyToClipboard:        copyToClipboard,
180        postJSON:               postJSON,
181        submitFeedback:         submitFeedback,
182        escapeHtml:             escapeHtml,
183        verdictLabel:           verdictLabel
184    };
185})();

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.