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.