mirror of
https://github.com/OpenRCT2/OpenRCT2.git
synced 2025-01-22 10:21:57 -05:00
304 lines
No EOL
9.4 KiB
JavaScript
304 lines
No EOL
9.4 KiB
JavaScript
const fs = require('fs');
|
|
|
|
// This is also the order expected for each version.
|
|
const ChangeTypes = ["Headline feature", "Feature", "Improved", "Change", "Removed", "Fix"];
|
|
// A list of change types that were previously used but aren't accepted anymore, will be replaced with the value in the map.
|
|
const ChangeTypeSubstitutions = {
|
|
"Improve": "Improved",
|
|
};
|
|
const HeaderSeperator = "------------------------------------------------------------------------";
|
|
|
|
function reportError(ctx, message) {
|
|
console.error(`Error on line ${ctx.lineIndex}: ${message}`);
|
|
process.exit(-1);
|
|
}
|
|
|
|
function reportLineError(line, message) {
|
|
console.error(`Error on line ${line.index + 1}: ${message}`);
|
|
process.exit(-1);
|
|
}
|
|
|
|
function consumeLine(ctx) {
|
|
if (ctx.lineIndex >= ctx.lines.length) {
|
|
reportError(ctx, "Unexpected end of file");
|
|
}
|
|
const line = ctx.lines[ctx.lineIndex];
|
|
ctx.lineIndex++;
|
|
return line;
|
|
}
|
|
|
|
function peekLine(ctx) {
|
|
if (ctx.lineIndex >= ctx.lines.length) {
|
|
return null;
|
|
}
|
|
return ctx.lines[ctx.lineIndex];
|
|
}
|
|
|
|
function readVersionHeader(ctx) {
|
|
// Extract the version number.
|
|
const versionLine = consumeLine(ctx);
|
|
const versionMatch = versionLine.text.match(/^((\d+\.\d+\.\d+(\.\d+)?(-\w+)?) \((\d{4}-\d{2}-\d{2}|in development)\))$/);
|
|
if (versionMatch == null) {
|
|
reportLineError(versionLine, "Expected a version number");
|
|
}
|
|
|
|
// Check the version seperator
|
|
const versionSeperator = consumeLine(ctx);
|
|
if (versionSeperator.text != HeaderSeperator) {
|
|
reportLineError(versionSeperator, "Expected version seperator");
|
|
}
|
|
|
|
// Check for optional headline, this means the there is text not starting with - and an empty line before
|
|
// the first entry.
|
|
var headline = peekLine(ctx);
|
|
if (headline != null && headline.text != "" && !headline.text.startsWith("-")) {
|
|
consumeLine(ctx);
|
|
|
|
// Check for empty line
|
|
const emptyLine = peekLine(ctx);
|
|
if (emptyLine == null || emptyLine.text != "") {
|
|
reportError(ctx, "Expected empty line after headline");
|
|
}
|
|
|
|
consumeLine(ctx);
|
|
headline = headline.text;
|
|
} else {
|
|
headline = "";
|
|
}
|
|
|
|
return {
|
|
version: versionMatch[2],
|
|
date: versionMatch[5],
|
|
headline: headline,
|
|
};
|
|
}
|
|
|
|
function isValidReference(ref) {
|
|
const regex = /^\w*#\d+( \(partial\))?$/;
|
|
return regex.test(ref);
|
|
}
|
|
|
|
function readVersionEntry(ctx, rawEntry, versionInfo) {
|
|
// Check for the '- ' prefix
|
|
var text = rawEntry.text;
|
|
if (!text.startsWith("- ")) {
|
|
reportLineError(rawEntry, "Expected entry prefix '- '");
|
|
}
|
|
text = text.substr(2);
|
|
|
|
// Extract the change type
|
|
const changeTypeIdx = text.indexOf(":");
|
|
if (changeTypeIdx == -1) {
|
|
reportLineError(rawEntry, "Expected change type");
|
|
}
|
|
var changeType = text.substring(0, changeTypeIdx).trim();
|
|
text = text.substring(changeTypeIdx + 1).trim();
|
|
|
|
// Check if changeType is valid
|
|
if (!ChangeTypes.includes(changeType)) {
|
|
// Check for substitution
|
|
if (changeType in ChangeTypeSubstitutions) {
|
|
changeType = ChangeTypeSubstitutions[changeType];
|
|
} else {
|
|
reportLineError(rawEntry, `Invalid change type '${changeType}', types can be ${ChangeTypes.join(", ")}`);
|
|
}
|
|
}
|
|
|
|
// Extract the references.
|
|
var refs = [];
|
|
if (text.startsWith("[")) {
|
|
const refsEndIdx = text.indexOf("]");
|
|
if (refsEndIdx == -1) {
|
|
reportLineError(rawEntry, "Open reference bracket '[' without closing bracket ']'");
|
|
}
|
|
refs = text.substring(1, refsEndIdx).split(",").map(ref => ref.trim());
|
|
text = text.substring(refsEndIdx + 1).trim();
|
|
} else {
|
|
// Text must start with '[', allow having this missing in older versions.
|
|
if (versionInfo.date == "in development") {
|
|
reportLineError(rawEntry, "Expected reference [#REF]");
|
|
}
|
|
}
|
|
|
|
// Check if reference is correct, can be '#123' or 'project#123'
|
|
for (const ref of refs) {
|
|
if(!isValidReference(ref)) {
|
|
reportLineError(rawEntry, `Invalid reference '${ref}', must be '#123' or 'project#123'`);
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
changeType: changeType,
|
|
refs: refs,
|
|
description: text.trim(),
|
|
};
|
|
return result;
|
|
}
|
|
|
|
function readVersionEntries(ctx, versionInfo) {
|
|
var entries = [];
|
|
while (ctx.lineIndex < ctx.lines.length) {
|
|
const rawEntry = consumeLine(ctx);
|
|
// Empty line means end of version block.
|
|
if (rawEntry.text == "") {
|
|
break;
|
|
}
|
|
const entry = readVersionEntry(ctx, rawEntry, versionInfo);
|
|
entries.push(entry);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function readVersionBlock(ctx) {
|
|
const versionInfo = readVersionHeader(ctx);
|
|
const entries = readVersionEntries(ctx, versionInfo);
|
|
return {
|
|
version: versionInfo.version,
|
|
date: versionInfo.date,
|
|
headline: versionInfo.headline,
|
|
entries: entries
|
|
};
|
|
}
|
|
|
|
function parseChangelog(changelog) {
|
|
// Split the changelog into lines but also keep the line numbers.
|
|
const lines = changelog.split('\n').map((text, index) => ({ text, index }));
|
|
|
|
// Remove duplicate empty lines.
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (lines[i].text == "" && (i == 0 || lines[i - 1].text == "")) {
|
|
lines.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
var ctx = {
|
|
lines,
|
|
lineIndex: 0,
|
|
versions: []
|
|
};
|
|
|
|
while (ctx.lineIndex < ctx.lines.length) {
|
|
const versionBlock = readVersionBlock(ctx);
|
|
ctx.versions.push(versionBlock);
|
|
}
|
|
|
|
return ctx.versions;
|
|
}
|
|
|
|
function referenceSort(a, b) {
|
|
// Extract the number from the reference
|
|
const numA = parseInt(a.replace(/\D/g, ''), 10);
|
|
const numB = parseInt(b.replace(/\D/g, ''), 10);
|
|
|
|
// Check if both are direct references or subproject references
|
|
const isDirectA = a.startsWith('#');
|
|
const isDirectB = b.startsWith('#');
|
|
|
|
if (isDirectA && isDirectB) {
|
|
// If both are direct references, sort by the number
|
|
return numA - numB;
|
|
} else if (!isDirectA && !isDirectB) {
|
|
// If both are subproject references, sort by the name and then the number
|
|
const nameA = a.split('#')[0];
|
|
const nameB = b.split('#')[0];
|
|
if (nameA.toLowerCase() < nameB.toLowerCase()) return -1;
|
|
if (nameA.toLowerCase() > nameB.toLowerCase()) return 1;
|
|
return numA - numB;
|
|
} else {
|
|
// If one is a direct reference and the other is a subproject reference,
|
|
// the direct reference should come first
|
|
return isDirectA ? -1 : 1;
|
|
}
|
|
}
|
|
|
|
function reorderGroups(a, b) {
|
|
const changeTypeDiff = ChangeTypes.indexOf(a.changeType) - ChangeTypes.indexOf(b.changeType);
|
|
if (changeTypeDiff != 0) {
|
|
return changeTypeDiff;
|
|
}
|
|
// If changeType is identical, sort based on reference
|
|
const hasMainRefA = a.refs.length > 0 && a.refs[0].startsWith("#");
|
|
const hasMainRefB = b.refs.length > 0 && b.refs[0].startsWith("#");
|
|
// Sort by main reference number.
|
|
if (hasMainRefA && hasMainRefB) {
|
|
const numA = parseInt(a.refs[0].replace(/\D/g, ''), 10);
|
|
const numB = parseInt(b.refs[0].replace(/\D/g, ''), 10);
|
|
return numA - numB;
|
|
} else if (hasMainRefA && !hasMainRefB) {
|
|
return -1;
|
|
} else if (!hasMainRefA && hasMainRefB) {
|
|
return 1;
|
|
} else {
|
|
// Sort by reference name.
|
|
const nameA = a.refs.length > 0 ? a.refs[0].split('#')[0] : "";
|
|
const nameB = b.refs.length > 0 ? b.refs[0].split('#')[0] : "";
|
|
if (nameA.toLowerCase() < nameB.toLowerCase()) return -1;
|
|
if (nameA.toLowerCase() > nameB.toLowerCase()) return 1;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function cleanupVersionBlock(version) {
|
|
// Sort the references first, numbers come before sub project refs.
|
|
version.entries.forEach(entry => entry.refs.sort((a, b) => referenceSort(a, b)));
|
|
|
|
// Sort the entries by change type.
|
|
version.entries.sort((a, b) => reorderGroups(a, b));
|
|
|
|
// Ensure entries end with a dot.
|
|
version.entries.forEach(entry => {
|
|
if (!entry.description.endsWith(".")) {
|
|
entry.description += ".";
|
|
}
|
|
});
|
|
}
|
|
|
|
function cleanupVersions(versions) {
|
|
for (const version of versions) {
|
|
cleanupVersionBlock(version);
|
|
}
|
|
}
|
|
|
|
function printChangelog(versions) {
|
|
for (const version of versions) {
|
|
console.log(`${version.version} (${version.date})`);
|
|
console.log(HeaderSeperator);
|
|
if (version.headline != "") {
|
|
console.log(version.headline);
|
|
console.log();
|
|
}
|
|
for (const entry of version.entries) {
|
|
var line = `- ${entry.changeType}: `;
|
|
if (entry.refs.length > 0) {
|
|
line += `[${entry.refs.join(", ")}] `;
|
|
}
|
|
line += `${entry.description}`;
|
|
console.log(line);
|
|
}
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
function checkLineEndings(changelog) {
|
|
if (changelog.indexOf("\r\n") != -1) {
|
|
console.error("Changelog contains Windows line endings, please convert to Unix line endings.");
|
|
process.exit(-1);
|
|
}
|
|
}
|
|
|
|
const filePath = process.argv[2];
|
|
fs.readFile(filePath, 'utf8', (err, data) => {
|
|
if (err) {
|
|
console.error('Error reading the file:', err);
|
|
process.exit(-1);
|
|
}
|
|
|
|
checkLineEndings(data);
|
|
|
|
var versions = parseChangelog(data);
|
|
|
|
cleanupVersions(versions);
|
|
|
|
printChangelog(versions);
|
|
}); |