gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

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 }