Skip to content

Commit bf2e303

Browse files
committed
autogenerate news.yml from changelog as part of build process
1 parent 40485af commit bf2e303

File tree

4 files changed

+139
-578
lines changed

4 files changed

+139
-578
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist/
22
node_modules/
3+
src/content/news.yml

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"scripts": {
66
"build:webpack": "webpack --config webpack.config.js",
77
"build:11ty": "eleventy",
8-
"build": "pnpm run build:webpack && pnpm run build:11ty",
8+
"build:news": "node scripts/generate-news.mjs",
9+
"build": "pnpm run build:news && pnpm run build:webpack && pnpm run build:11ty",
910
"start": "concurrently \"webpack --watch\" \"eleventy --serve\""
1011
},
1112
"repository": {

scripts/generate-news.mjs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env node
2+
3+
// Fetches CHANGELOG.md from the spatial-model-editor repo on GitHub
4+
// and generates src/content/news.yml from it.
5+
6+
import fs from "fs";
7+
import path from "path";
8+
import { fileURLToPath } from "url";
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const outputPath = path.resolve(__dirname, "../src/content/news.yml");
12+
13+
const changelogUrl =
14+
"https://raw.githubusercontent.com/spatial-model-editor/spatial-model-editor/main/CHANGELOG.md";
15+
16+
const response = await fetch(changelogUrl);
17+
if (!response.ok) {
18+
console.error(`Failed to fetch CHANGELOG.md: ${response.status}`);
19+
process.exit(1);
20+
}
21+
const changelog = await response.text();
22+
23+
// Parse changelog into releases
24+
const releases = [];
25+
let current = null;
26+
let currentSection = null;
27+
28+
for (const line of changelog.split("\n")) {
29+
// Match release header: ## [1.11.0] - 2026-02-04
30+
const releaseMatch = line.match(
31+
/^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{1,2}-\d{1,2})/,
32+
);
33+
if (releaseMatch) {
34+
if (current) releases.push(current);
35+
current = {
36+
version: releaseMatch[1],
37+
date: releaseMatch[2],
38+
sections: {},
39+
};
40+
currentSection = null;
41+
continue;
42+
}
43+
44+
// Skip unreleased and non-release lines
45+
if (!current) continue;
46+
47+
// Match section header: ### Added, ### Fixed, etc.
48+
const sectionMatch = line.match(/^### (\w+)/);
49+
if (sectionMatch) {
50+
currentSection = sectionMatch[1];
51+
if (!current.sections[currentSection]) {
52+
current.sections[currentSection] = [];
53+
}
54+
continue;
55+
}
56+
57+
// Match list items
58+
if (currentSection && line.match(/^- /)) {
59+
current.sections[currentSection].push(line.slice(2));
60+
} else if (currentSection && line.match(/^ {2,}- /)) {
61+
// Sub-items: append to the last item
62+
const items = current.sections[currentSection];
63+
if (items.length > 0) {
64+
items[items.length - 1] += "\n" + line;
65+
}
66+
}
67+
68+
// Special case: "First official release." (1.0.0)
69+
if (!currentSection && line.match(/\S/) && !line.startsWith("#")) {
70+
current.sections["_text"] = [line.trim()];
71+
}
72+
}
73+
if (current) releases.push(current);
74+
75+
// Convert markdown links to HTML: [text](url) -> <a href="url">text</a>
76+
function mdLinksToHtml(text) {
77+
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
78+
}
79+
80+
// Convert a single item's markdown to HTML, handling sub-lists
81+
function itemToHtml(item) {
82+
const lines = item.split("\n");
83+
let html = mdLinksToHtml(lines[0]);
84+
if (lines.length > 1) {
85+
html += "<ul>";
86+
for (const subLine of lines.slice(1)) {
87+
html += `<li>${mdLinksToHtml(subLine.replace(/^\s*- /, ""))}</li>`;
88+
}
89+
html += "</ul>";
90+
}
91+
return html;
92+
}
93+
94+
// Convert inline code to <code> tags
95+
function inlineCodeToHtml(text) {
96+
return text.replace(/`([^`]+)`/g, "<code>$1</code>");
97+
}
98+
99+
// Build description HTML for a release
100+
function buildDescription(sections) {
101+
if (sections["_text"]) {
102+
return `<p>${sections["_text"][0]}</p>`;
103+
}
104+
105+
let html = "";
106+
for (const [section, items] of Object.entries(sections)) {
107+
html += `<h6>${section}</h6>\n`;
108+
html += "<ul>\n";
109+
for (const item of items) {
110+
html += `<li>${inlineCodeToHtml(itemToHtml(item))}</li>\n`;
111+
}
112+
html += "</ul>\n";
113+
}
114+
return html.trim();
115+
}
116+
117+
// Escape YAML string value
118+
function yamlStr(str) {
119+
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
120+
}
121+
122+
// Generate YAML
123+
let yaml = "page_title: News\nnews:\n";
124+
for (const release of releases) {
125+
const description = buildDescription(release.sections);
126+
yaml += ` - date: ${release.date}\n`;
127+
yaml += ` title: Version ${release.version} released\n`;
128+
yaml += ` url: https://github.com/spatial-model-editor/spatial-model-editor/releases/tag/${release.version}\n`;
129+
yaml += ` description: >\n`;
130+
for (const line of description.split("\n")) {
131+
yaml += ` ${line}\n`;
132+
}
133+
}
134+
135+
fs.writeFileSync(outputPath, yaml);
136+
console.log(`Generated ${outputPath} with ${releases.length} releases.`);

0 commit comments

Comments
 (0)