mirror of https://github.com/OpenRCT2/OpenRCT2.git
Add script to format changelog
This commit is contained in:
parent
5fa955e1b3
commit
8d7db5e987
|
@ -0,0 +1,304 @@
|
|||
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);
|
||||
});
|
Loading…
Reference in New Issue