1 /** 2 * Common functions for dealing with entries in ini-like file. 3 * Authors: 4 * $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov) 5 * Copyright: 6 * Roman Chistokhodov, 2015-2016 7 * License: 8 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 9 * See_Also: 10 * $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification) 11 */ 12 13 module inilike.common; 14 15 package { 16 import std.algorithm; 17 import std.range; 18 import std.string; 19 import std.traits; 20 import std.typecons; 21 import std.conv : to; 22 23 static if( __VERSION__ < 2066 ) enum nogc = 1; 24 25 auto keyValueTuple(String)(String key, String value) 26 { 27 alias KeyValueTuple = Tuple!(String, "key", String, "value"); 28 return KeyValueTuple(key, value); 29 } 30 } 31 32 private @nogc @trusted auto stripLeftChar(inout(char)[] s) pure nothrow 33 { 34 size_t spaceNum = 0; 35 while(spaceNum < s.length) { 36 const char c = s[spaceNum]; 37 if (c == ' ' || c == '\t') { 38 spaceNum++; 39 } else { 40 break; 41 } 42 } 43 return s[spaceNum..$]; 44 } 45 46 private @nogc @trusted auto stripRightChar(inout(char)[] s) pure nothrow 47 { 48 size_t spaceNum = 0; 49 while(spaceNum < s.length) { 50 const char c = s[$-1-spaceNum]; 51 if (c == ' ' || c == '\t') { 52 spaceNum++; 53 } else { 54 break; 55 } 56 } 57 58 return s[0..$-spaceNum]; 59 } 60 61 62 /** 63 * Test whether the string s represents a comment. 64 */ 65 @nogc @trusted bool isComment(const(char)[] s) pure nothrow 66 { 67 s = s.stripLeftChar; 68 return !s.empty && s[0] == '#'; 69 } 70 71 /// 72 unittest 73 { 74 assert( isComment("# Comment")); 75 assert( isComment(" # Comment")); 76 assert(!isComment("Not comment")); 77 assert(!isComment("")); 78 } 79 80 /** 81 * Test whether the string s represents a group header. 82 * Note: "[]" is not considered as valid group header. 83 */ 84 @nogc @trusted bool isGroupHeader(const(char)[] s) pure nothrow 85 { 86 s = s.stripRightChar; 87 return s.length > 2 && s[0] == '[' && s[$-1] == ']'; 88 } 89 90 /// 91 unittest 92 { 93 assert( isGroupHeader("[Group]")); 94 assert( isGroupHeader("[Group] ")); 95 assert(!isGroupHeader("[]")); 96 assert(!isGroupHeader("[Group")); 97 assert(!isGroupHeader("Group]")); 98 } 99 100 /** 101 * Retrieve group name from header entry. 102 * Returns: group name or empty string if the entry is not group header. 103 */ 104 105 @nogc @trusted auto parseGroupHeader(inout(char)[] s) pure nothrow 106 { 107 s = s.stripRightChar; 108 if (isGroupHeader(s)) { 109 return s[1..$-1]; 110 } else { 111 return null; 112 } 113 } 114 115 /// 116 unittest 117 { 118 assert(parseGroupHeader("[Group name]") == "Group name"); 119 assert(parseGroupHeader("NotGroupName") == string.init); 120 121 assert(parseGroupHeader("[Group name]".dup) == "Group name".dup); 122 } 123 124 /** 125 * Parse entry of kind Key=Value into pair of Key and Value. 126 * Returns: tuple of key and value strings or tuple of empty strings if it's is not a key-value entry. 127 * Note: this function does not check whether parsed key is valid key. 128 * See_Also: isValidKey 129 */ 130 @nogc @trusted auto parseKeyValue(String)(String s) pure nothrow if (is(String : const(char)[])) 131 { 132 auto t = s.findSplit("="); 133 auto key = t[0]; 134 auto value = t[2]; 135 136 if (key.length && t[1].length) { 137 return keyValueTuple(key, value); 138 } 139 return keyValueTuple(String.init, String.init); 140 } 141 142 /// 143 unittest 144 { 145 assert(parseKeyValue("Key=Value") == tuple("Key", "Value")); 146 assert(parseKeyValue("Key=") == tuple("Key", string.init)); 147 assert(parseKeyValue("=Value") == tuple(string.init, string.init)); 148 assert(parseKeyValue("NotKeyValue") == tuple(string.init, string.init)); 149 150 assert(parseKeyValue("Key=Value".dup) == tuple("Key".dup, "Value".dup)); 151 } 152 153 /** 154 * Test whether the string is valid key. 155 * Only the characters A-Za-z0-9- may be used in key names. See $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s02.html, Basic format of the file) 156 * Note: this function automatically separate key from locale. It does not check validity of the locale itself. 157 */ 158 @nogc @safe bool isValidKey(String)(String key) pure nothrow if (is(String : const(char)[])) { 159 key = separateFromLocale(key)[0]; 160 161 @nogc @safe static bool isValidKeyChar(char c) pure nothrow { 162 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-'; 163 } 164 165 if (key.empty) { 166 return false; 167 } 168 for (size_t i = 0; i<key.length; ++i) { 169 if (!isValidKeyChar(key[i])) { 170 return false; 171 } 172 } 173 return true; 174 } 175 176 /// 177 unittest 178 { 179 assert(isValidKey("Generic-Name")); 180 assert(isValidKey("Generic-Name[ru_RU]")); 181 assert(!isValidKey("Name$")); 182 assert(!isValidKey("")); 183 assert(!isValidKey("[ru_RU]")); 184 } 185 186 /** 187 * Test whether the entry value represents true 188 */ 189 @nogc @safe bool isTrue(const(char)[] value) pure nothrow { 190 return (value == "true" || value == "1"); 191 } 192 193 /// 194 unittest 195 { 196 assert(isTrue("true")); 197 assert(isTrue("1")); 198 assert(!isTrue("not boolean")); 199 } 200 201 /** 202 * Test whether the entry value represents false 203 */ 204 @nogc @safe bool isFalse(const(char)[] value) pure nothrow { 205 return (value == "false" || value == "0"); 206 } 207 208 /// 209 unittest 210 { 211 assert(isFalse("false")); 212 assert(isFalse("0")); 213 assert(!isFalse("not boolean")); 214 } 215 216 /** 217 * Check if the entry value can be interpreted as boolean value. 218 * See_Also: isTrue, isFalse 219 */ 220 @nogc @safe bool isBoolean(const(char)[] value) pure nothrow { 221 return isTrue(value) || isFalse(value); 222 } 223 224 /// 225 unittest 226 { 227 assert(isBoolean("true")); 228 assert(isBoolean("1")); 229 assert(isBoolean("false")); 230 assert(isBoolean("0")); 231 assert(!isBoolean("not boolean")); 232 } 233 234 /** 235 * Make locale name based on language, country, encoding and modifier. 236 * Returns: locale name in form lang_COUNTRY.ENCODING@MODIFIER 237 * See_Also: parseLocaleName 238 */ 239 @safe String makeLocaleName(String)( 240 String lang, String country = null, 241 String encoding = null, 242 String modifier = null) pure 243 if (is(String : const(char)[])) 244 { 245 return lang ~ (country.length ? "_".to!String~country : String.init) 246 ~ (encoding.length ? ".".to!String~encoding : String.init) 247 ~ (modifier.length ? "@".to!String~modifier : String.init); 248 } 249 250 /// 251 unittest 252 { 253 assert(makeLocaleName("ru", "RU") == "ru_RU"); 254 assert(makeLocaleName("ru", "RU", "UTF-8") == "ru_RU.UTF-8"); 255 assert(makeLocaleName("ru", "RU", "UTF-8", "mod") == "ru_RU.UTF-8@mod"); 256 assert(makeLocaleName("ru", string.init, string.init, "mod") == "ru@mod"); 257 258 assert(makeLocaleName("ru".dup, (char[]).init, (char[]).init, "mod".dup) == "ru@mod".dup); 259 } 260 261 /** 262 * Parse locale name into the tuple of 4 values corresponding to language, country, encoding and modifier 263 * Returns: Tuple!(string, "lang", string, "country", string, "encoding", string, "modifier") 264 * See_Also: makeLocaleName 265 */ 266 @nogc @trusted auto parseLocaleName(String)(String locale) pure nothrow if (is(String : const(char)[])) 267 { 268 auto modifiderSplit = findSplit(locale, "@"); 269 auto modifier = modifiderSplit[2]; 270 271 auto encodongSplit = findSplit(modifiderSplit[0], "."); 272 auto encoding = encodongSplit[2]; 273 274 auto countrySplit = findSplit(encodongSplit[0], "_"); 275 auto country = countrySplit[2]; 276 277 auto lang = countrySplit[0]; 278 279 alias LocaleTuple = Tuple!(String, "lang", String, "country", String, "encoding", String, "modifier"); 280 281 return LocaleTuple(lang, country, encoding, modifier); 282 } 283 284 /// 285 unittest 286 { 287 assert(parseLocaleName("ru_RU.UTF-8@mod") == tuple("ru", "RU", "UTF-8", "mod")); 288 assert(parseLocaleName("ru@mod") == tuple("ru", string.init, string.init, "mod")); 289 assert(parseLocaleName("ru_RU") == tuple("ru", "RU", string.init, string.init)); 290 291 assert(parseLocaleName("ru_RU.UTF-8@mod".dup) == tuple("ru".dup, "RU".dup, "UTF-8".dup, "mod".dup)); 292 } 293 294 /** 295 * Construct localized key name from key and locale. 296 * Returns: localized key in form key[locale] dropping encoding out if present. 297 * See_Also: separateFromLocale 298 */ 299 @safe String localizedKey(String)(String key, String locale) pure nothrow if (is(String : const(char)[])) 300 { 301 auto t = parseLocaleName(locale); 302 if (!t.encoding.empty) { 303 locale = makeLocaleName(t.lang, t.country, String.init, t.modifier); 304 } 305 return key ~ "[".to!String ~ locale ~ "]".to!String; 306 } 307 308 /// 309 unittest 310 { 311 assert(localizedKey("Name", "ru_RU") == "Name[ru_RU]"); 312 assert(localizedKey("Name", "ru_RU.UTF-8") == "Name[ru_RU]"); 313 } 314 315 /** 316 * ditto, but constructs locale name from arguments. 317 */ 318 @safe String localizedKey(String)(String key, String lang, String country, String modifier = null) pure 319 { 320 return key ~ "[".to!String ~ makeLocaleName(lang, country, String.init, modifier) ~ "]".to!String; 321 } 322 323 /// 324 unittest 325 { 326 assert(localizedKey("Name", "ru", "RU") == "Name[ru_RU]"); 327 assert(localizedKey("Name".dup, "ru".dup, "RU".dup) == "Name[ru_RU]".dup); 328 } 329 330 /** 331 * Separate key name into non-localized key and locale name. 332 * If key is not localized returns original key and empty string. 333 * Returns: tuple of key and locale name. 334 * See_Also: localizedKey 335 */ 336 @nogc @trusted auto separateFromLocale(String)(String key) pure nothrow if (is(String : const(char)[])) { 337 if (key.endsWith("]")) { 338 auto t = key.findSplit("["); 339 if (t[1].length) { 340 return tuple(t[0], t[2][0..$-1]); 341 } 342 } 343 return tuple(key, typeof(key).init); 344 } 345 346 /// 347 unittest 348 { 349 assert(separateFromLocale("Name[ru_RU]") == tuple("Name", "ru_RU")); 350 assert(separateFromLocale("Name") == tuple("Name", string.init)); 351 352 char[] mutableString = "Hello".dup; 353 assert(separateFromLocale(mutableString) == tuple(mutableString, typeof(mutableString).init)); 354 } 355 356 /** 357 * Choose the better localized value matching to locale between two localized values. The "goodness" is determined using algorithm described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys). 358 * Params: 359 * locale = original locale to match to 360 * firstLocale = first locale 361 * firstValue = first value 362 * secondLocale = second locale 363 * secondValue = second value 364 * Returns: The best alternative among two or empty string if none of alternatives match original locale. 365 * Note: value with empty locale is considered better choice than value with locale that does not match the original one. 366 */ 367 @nogc @trusted auto chooseLocalizedValue(String)( 368 String locale, 369 String firstLocale, String firstValue, 370 String secondLocale, String secondValue) pure nothrow 371 { 372 const lt = parseLocaleName(locale); 373 const lt1 = parseLocaleName(firstLocale); 374 const lt2 = parseLocaleName(secondLocale); 375 376 int score1, score2; 377 378 if (lt.lang == lt1.lang) { 379 score1 = 1 + ((lt.country == lt1.country) ? 2 : 0 ) + ((lt.modifier == lt1.modifier) ? 1 : 0); 380 } 381 if (lt.lang == lt2.lang) { 382 score2 = 1 + ((lt.country == lt2.country) ? 2 : 0 ) + ((lt.modifier == lt2.modifier) ? 1 : 0); 383 } 384 385 if (score1 == 0 && score2 == 0) { 386 if (firstLocale.empty && !firstValue.empty) { 387 return tuple(firstLocale, firstValue); 388 } else if (secondLocale.empty && !secondValue.empty) { 389 return tuple(secondLocale, secondValue); 390 } else { 391 return tuple(String.init, String.init); 392 } 393 } 394 395 if (score1 >= score2) { 396 return tuple(firstLocale, firstValue); 397 } else { 398 return tuple(secondLocale, secondValue); 399 } 400 } 401 402 /// 403 unittest 404 { 405 string locale = "ru_RU.UTF-8@jargon"; 406 assert(chooseLocalizedValue(string.init, "ru_RU", "Программист", "ru@jargon", "Кодер") == tuple(string.init, string.init)); 407 assert(chooseLocalizedValue(locale, "fr_FR", "Programmeur", string.init, "Programmer") == tuple(string.init, "Programmer")); 408 assert(chooseLocalizedValue(locale, string.init, "Programmer", "de_DE", "Programmierer") == tuple(string.init, "Programmer")); 409 assert(chooseLocalizedValue(locale, "fr_FR", "Programmeur", "de_DE", "Programmierer") == tuple(string.init, string.init)); 410 411 assert(chooseLocalizedValue(string.init, string.init, "Value", string.init, string.init) == tuple(string.init, "Value")); 412 assert(chooseLocalizedValue(locale, string.init, "Value", string.init, string.init) == tuple(string.init, "Value")); 413 assert(chooseLocalizedValue(locale, string.init, string.init, string.init, "Value") == tuple(string.init, "Value")); 414 415 assert(chooseLocalizedValue(locale, "ru_RU", "Программист", "ru@jargon", "Кодер") == tuple("ru_RU", "Программист")); 416 assert(chooseLocalizedValue(locale, "ru_RU", "Программист", "ru_RU@jargon", "Кодер") == tuple("ru_RU@jargon", "Кодер")); 417 418 assert(chooseLocalizedValue(locale, "ru", "Разработчик", "ru_RU", "Программист") == tuple("ru_RU", "Программист")); 419 } 420 421 /** 422 * Escapes string by replacing special symbols with escaped sequences. 423 * These symbols are: '\\' (backslash), '\n' (newline), '\r' (carriage return) and '\t' (tab). 424 * Note: 425 * Currently the library stores values as they were loaded from file, i.e. escaped. 426 * To keep things consistent you should take care about escaping the value before inserting. The library will not do it for you. 427 * Returns: Escaped string. 428 * See_Also: unescapeValue 429 */ 430 @trusted String escapeValue(String)(String value) pure if (is(String : const(char)[])) { 431 return value.replace("\\", `\\`.to!String).replace("\n", `\n`.to!String).replace("\r", `\r`.to!String).replace("\t", `\t`.to!String); 432 } 433 434 /// 435 unittest 436 { 437 assert("a\\next\nline\top".escapeValue() == `a\\next\nline\top`); // notice how the string on the right is raw. 438 assert("a\\next\nline\top".dup.escapeValue() == `a\\next\nline\top`.dup); 439 } 440 441 442 /** 443 * Unescape value. 444 * Params: 445 * value = string to unescape 446 * pairs = pairs of escaped characters and their unescaped forms. 447 */ 448 @trusted auto doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure { 449 auto toReturn = appender!(typeof(value))(); 450 451 for (size_t i = 0; i < value.length; i++) { 452 if (value[i] == '\\') { 453 if (i+1 < value.length) { 454 const char c = value[i+1]; 455 auto t = pairs.find!"a[0] == b[0]"(tuple(c,c)); 456 if (!t.empty) { 457 toReturn.put(t.front[1]); 458 i++; 459 continue; 460 } 461 } 462 } 463 toReturn.put(value[i]); 464 } 465 return toReturn.data; 466 } 467 468 469 /** 470 * Unescapes string. You should unescape values returned by library before displaying until you want keep them as is (e.g., to allow user to edit values in escaped form). 471 * Returns: Unescaped string. 472 * See_Also: escapeValue 473 */ 474 @trusted auto unescapeValue(inout(char)[] value) nothrow pure 475 { 476 static immutable Tuple!(char, char)[] pairs = [ 477 tuple('s', ' '), 478 tuple('n', '\n'), 479 tuple('r', '\r'), 480 tuple('t', '\t'), 481 tuple('\\', '\\') 482 ]; 483 return doUnescape(value, pairs); 484 } 485 486 /// 487 unittest 488 { 489 assert(`a\\next\nline\top`.unescapeValue() == "a\\next\nline\top"); // notice how the string on the left is raw. 490 assert(`\\next\nline\top`.unescapeValue() == "\\next\nline\top"); 491 assert(`a\\next\nline\top`.dup.unescapeValue() == "a\\next\nline\top".dup); 492 }