From 98555a9a801765d7af1887cce7f4f6dccac01561 Mon Sep 17 00:00:00 2001 From: Luke Hamburg <1992842+luckman212@users.noreply.github.com> Date: Mon, 26 May 2025 07:43:38 -0400 Subject: [PATCH] fix(gui): update `uncamel()` to handle strings like 'IDs' (fixes #10128) (#10131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > ⚠️ resubmission targeting `main` instead of `v2` ### Purpose Updates `uncamel()` function in [uncamelFilter.js](https://github.com/syncthing/syncthing/blob/v2/gui/default/syncthing/core/uncamelFilter.js) to fix camelCase conversion edge cases, see #10128 This adds an array called `reservedStrings` which will be printed as-is, e.g. `IDs`, `LAN` etc. I pre-populated this with what I believe makes sense, but of course this is easily updated. ### Testing I compiled all the config variables I could find in `syncthing/lib/config/*configuration.go` and tested this new function against them. Everything seemed to pass. ### Screenshot ![Image](https://github.com/user-attachments/assets/af8c9821-58b3-4a6a-8462-bead8a6d845a) --- gui/default/syncthing/core/uncamelFilter.js | 52 +++++++++++++-------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/gui/default/syncthing/core/uncamelFilter.js b/gui/default/syncthing/core/uncamelFilter.js index 269a42e90..97689f140 100644 --- a/gui/default/syncthing/core/uncamelFilter.js +++ b/gui/default/syncthing/core/uncamelFilter.js @@ -1,27 +1,39 @@ angular.module('syncthing.core') .filter('uncamel', function () { + const reservedStrings = [ + 'IDs', 'ID', // substrings must come AFTER longer keywords containing them + 'URL', 'UR', + 'API', 'QUIC', 'TCP', 'UDP', 'NAT', 'LAN', 'WAN', + 'KiB', 'MiB', 'GiB', 'TiB' + ]; return function (input) { - input = input.replace(/(.)([A-Z][a-z]+)/g, '$1 $2').replace(/([a-z0-9])([A-Z])/g, '$1 $2'); - var parts = input.split(' '); - var lastPart = parts.splice(-1)[0]; + if (!input || typeof input !== 'string') return ''; + const placeholders = {}; + let counter = 0; + reservedStrings.forEach(word => { + const placeholder = `__RSV${counter}__`; + const re = new RegExp(word, 'g'); + input = input.replace(re, placeholder); + placeholders[placeholder] = word; + counter++; + }); + input = input.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); + Object.entries(placeholders).forEach(([ph, word]) => { + input = input.replace(new RegExp(ph, 'g'), ` ${word} `); + }); + let parts = input.split(' '); + const lastPart = parts.pop(); switch (lastPart) { - case "S": - parts.push('(seconds)'); - break; - case "M": - parts.push('(minutes)'); - break; - case "H": - parts.push('(hours)'); - break; - case "Ms": - parts.push('(milliseconds)'); - break; - default: - parts.push(lastPart); - break; + case 'S': parts.push('(seconds)'); break; + case 'M': parts.push('(minutes)'); break; + case 'H': parts.push('(hours)'); break; + case 'Ms': parts.push('(milliseconds)'); break; + default: parts.push(lastPart); break; } - input = parts.join(' '); - return input.charAt(0).toUpperCase() + input.slice(1); + parts = parts.map(part => { + const match = reservedStrings.find(w => w.toUpperCase() === part.toUpperCase()); + return match || part.charAt(0).toUpperCase() + part.slice(1); + }); + return parts.join(' ').replace(/\s+/g, ' ').trim(); }; });