changelogGenerator.cpp (8657B)
1 #include <fstream> 2 #include <iostream> 3 #include <ostream> 4 #include <vector> 5 #include <functional> 6 7 #include "baseLib/commandline.h" 8 9 using Lines = std::vector<std::string>; 10 using LinesPerKey = std::map<std::string, Lines>; 11 12 namespace 13 { 14 enum class Format 15 { 16 Txt, 17 Discord 18 }; 19 20 std::string& trim(std::string& _line) 21 { 22 auto needsTrim = [](const char _c) -> bool 23 { 24 return _c == ' ' || _c == '\t' || _c == '\n' || _c == '\r'; 25 }; 26 27 while (!_line.empty()) 28 { 29 if (needsTrim(_line.back())) 30 _line.pop_back(); 31 else if (needsTrim(_line.front())) 32 _line.erase(_line.begin()); 33 else 34 break; 35 } 36 return _line; 37 } 38 39 Lines& trim(Lines& _lines) 40 { 41 while (_lines.front().empty()) 42 _lines.erase(_lines.begin()); 43 while (_lines.back().empty()) 44 _lines.pop_back(); 45 return _lines; 46 } 47 48 std::string parseVersion(const std::string& _line) 49 { 50 const auto posA = _line.find('.'); 51 const auto posB = _line.find('.', posA + 1); 52 53 if (posA == std::string::npos || posB == std::string::npos || posB < posA) 54 { 55 return {}; 56 } 57 58 for (size_t i = 0; i < posA; ++i) 59 { 60 if (!std::isdigit(_line[i])) 61 return {}; 62 } 63 64 for (size_t i = posA + 1; i < posB; ++i) 65 { 66 if (!std::isdigit(_line[i])) 67 return {}; 68 } 69 70 for (size_t i = posA + 1; i < posB; ++i) 71 { 72 if (!std::isdigit(_line[i])) 73 return {}; 74 } 75 76 return _line; 77 } 78 79 std::string parseProduct(const std::string& _line) 80 { 81 // Needs to start with an uppercase letter A-Z 82 if (_line.empty() || !std::isupper(_line.front())) 83 return {}; 84 85 // Needs to have : at the end 86 if (_line.back() != ':') 87 return {}; 88 89 auto res = _line; 90 res.pop_back(); 91 return res; 92 } 93 94 LinesPerKey groupBy(const Lines& _lines, const std::function<std::string(const std::string&)>& _eval) 95 { 96 Lines currentLines; 97 98 std::map<std::string, Lines> linesPerKey; 99 100 std::string currentKey; 101 102 for (const auto& line : _lines) 103 { 104 const auto key = _eval(line); 105 if (!key.empty()) 106 { 107 if (!currentKey.empty()) 108 { 109 linesPerKey.insert({currentKey, trim(currentLines)}); 110 currentLines.clear(); 111 } 112 currentKey = key; 113 } 114 else if (!currentKey.empty()) 115 { 116 currentLines.push_back(line); 117 } 118 } 119 120 if (!currentKey.empty() && !currentLines.empty()) 121 linesPerKey.insert({ currentKey, trim(currentLines) }); 122 123 if (linesPerKey.empty()) 124 linesPerKey.insert({ "", _lines }); 125 return linesPerKey; 126 } 127 128 Lines& fixSpacing(Lines& _lines) 129 { 130 size_t spaces = 0; 131 132 for (auto& line : _lines) 133 { 134 if (line.empty()) 135 continue; 136 137 if (line.front() == '-') 138 { 139 // lines either start with "- " or with "- [...] ", adjust spaces accordingly 140 auto bracketPos = line.find(']'); 141 if (bracketPos != std::string::npos) 142 spaces = bracketPos + 2; 143 else 144 spaces = 2; 145 } 146 else if (spaces) 147 { 148 for (size_t i=0; i<spaces; ++i) 149 line.insert(line.begin(), ' '); 150 } 151 } 152 return _lines; 153 } 154 155 std::string fixFilename(const std::string& _filename) 156 { 157 std::string out; 158 for (auto& c : _filename) 159 { 160 if (c != ':') 161 out += c; 162 } 163 return out; 164 } 165 bool writeProduct(std::ofstream& _out, const std::string& _product, const Lines& _lines, const bool _needsSpace) 166 { 167 if (_lines.empty()) 168 return false; 169 170 if (_needsSpace) 171 _out << '\n'; 172 173 if (!_product.empty()) 174 { 175 _out << _product << ":\n"; 176 _out << '\n'; 177 } 178 179 for (const auto& line : _lines) 180 _out << line << '\n'; 181 182 return true; 183 } 184 } 185 186 int main(const int _argc, char* _argv[]) 187 { 188 const baseLib::CommandLine cmdLine(_argc, _argv); 189 190 const auto inFile = cmdLine.get("i"); 191 192 if (inFile.empty()) 193 { 194 std::cout << "No input file specified" << '\n'; 195 return -1; 196 } 197 198 auto outPath = cmdLine.get("o"); 199 200 if (outPath.empty()) 201 { 202 std::cout << "No output path specified" << '\n'; 203 return -1; 204 } 205 206 if (outPath.back() != '/' && outPath.back() != '\\') 207 outPath.push_back('/'); 208 209 auto f = cmdLine.get("f"); 210 211 auto format = Format::Txt; 212 213 if (!f.empty()) 214 { 215 if (f == "discord") 216 { 217 format = Format::Discord; 218 } 219 else 220 { 221 std::cout << "Unknown format '" << f << "'\n"; 222 return -1; 223 } 224 } 225 226 std::ifstream file(inFile); 227 228 if (!file.is_open()) 229 { 230 std::cout << "Failed to open input file '" << inFile << "'" << '\n'; 231 return -1; 232 } 233 234 Lines allLines; 235 236 while (true) 237 { 238 std::string line; 239 std::getline(file, line); 240 if (file.eof()) 241 break; 242 trim(line); 243 allLines.push_back(line); 244 } 245 246 if (allLines.empty()) 247 { 248 std::cout << "No lines read from input file" << '\n'; 249 return -1; 250 } 251 252 // group by version 253 const auto linesPerVersion = groupBy(allLines, parseVersion); 254 255 // group by product 256 std::map<std::string, LinesPerKey> productPerVersion; 257 258 for (const auto& it : linesPerVersion) 259 { 260 const auto& version = it.first; 261 const auto& l = it.second; 262 const auto linesPerProduct = groupBy(l, parseProduct); 263 productPerVersion.insert({ version, linesPerProduct }); 264 } 265 266 // multiple products might have been specified via /, i.e. Osirus/OsTIrus, add them to two individual products 267 for (auto& itVersion : productPerVersion) 268 { 269 const auto& version = itVersion.first; 270 auto& productPerLines = itVersion.second; 271 272 for (auto itProduct = productPerLines.begin(); itProduct != productPerLines.end();) 273 { 274 const auto& product = itProduct->first; 275 const auto& lines = itProduct->second; 276 const auto pos = product.find('/'); 277 278 if (pos == std::string::npos) 279 { 280 ++itProduct; 281 continue; 282 } 283 284 const auto productA = product.substr(0, pos); 285 const auto productB = product.substr(pos + 1); 286 auto& linesA = productPerVersion[version][productA]; 287 linesA.insert(linesA.end(), lines.begin(), lines.end()); 288 auto& linesB = productPerVersion[version][productB]; 289 linesB.insert(linesB.end(), lines.begin(), lines.end()); 290 itProduct = productPerLines.erase(itProduct); 291 } 292 } 293 294 // adjust spacing for all lines 295 for (auto& itVersion : productPerVersion) 296 { 297 for (auto& itProduct : itVersion.second) 298 fixSpacing(itProduct.second); 299 } 300 301 // create individual files per version and product 302 std::set<std::string> globalProducts = { "DSP", "Framework", "Patch Manager" }; 303 std::set<std::string> localProducts = { "Osirus", "OsTIrus", "Xenia", "Vavra", "NodalRed2x" }; 304 305 auto formatHeader = [format](const std::string & _header) 306 { 307 if (format == Format::Discord) 308 { 309 return "**" + _header + "**"; 310 } 311 return _header; 312 }; 313 314 for (auto& itVersion : productPerVersion) 315 { 316 const auto& version = itVersion.first; 317 const auto& products = itVersion.second; 318 319 std::map<std::string, Lines> globals; 320 std::map<std::string, Lines> locals; 321 322 for (const auto& itProduct : products) 323 { 324 const auto& product = itProduct.first; 325 326 if (globalProducts.find(product) != globalProducts.end()) 327 globals.insert({ product, itProduct.second }); 328 else 329 locals.insert({ product, itProduct.second }); 330 } 331 332 if (!globals.empty()) 333 { 334 for (const auto& localProduct : localProducts) 335 { 336 if (locals.find(localProduct) == locals.end()) 337 locals.insert({ localProduct, {} }); 338 } 339 } 340 341 // write one file per product 342 if (locals.size() > 1) 343 { 344 for (const auto& itProduct : locals) 345 { 346 const auto& product = itProduct.first; 347 348 const auto outName = outPath + fixFilename(version + "_" + product + ".txt"); 349 std::ofstream outFile(outName); 350 351 if (!outFile.is_open()) 352 { 353 std::cout << "Failed to create output file '" << outName << '\n'; 354 return -1; 355 } 356 357 if (!product.empty()) 358 outFile << formatHeader(product + " Version " + version) << '\n'; 359 else 360 outFile << formatHeader("Version " + version) << '\n'; 361 outFile << '\n'; 362 363 if (format == Format::Discord) 364 outFile << "```\n"; 365 366 bool needsSpace = false; 367 368 for (const auto& global : globals) 369 needsSpace |= writeProduct(outFile, global.first, global.second, needsSpace); 370 371 writeProduct(outFile, product, itProduct.second, needsSpace); 372 373 if (format == Format::Discord) 374 outFile << "```\n"; 375 } 376 } 377 378 // write one file for all products 379 const auto outName = outPath + fixFilename(version + ".txt"); 380 std::ofstream outFile(outName); 381 382 if (!outFile.is_open()) 383 { 384 std::cout << "Failed to create output file '" << outName << '\n'; 385 return -1; 386 } 387 outFile << formatHeader("Version " + version) << '\n'; 388 outFile << '\n'; 389 390 if (format == Format::Discord) 391 outFile << "```\n"; 392 393 bool needsSpace = false; 394 395 for (const auto& global : globals) 396 needsSpace |= writeProduct(outFile, global.first, global.second, needsSpace); 397 398 for (const auto& local : locals) 399 needsSpace |= writeProduct(outFile, local.first, local.second, needsSpace); 400 401 if (format == Format::Discord) 402 outFile << "```\n"; 403 } 404 return 0; 405 }