forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathextract-release-notes.mjs
More file actions
executable file
·130 lines (120 loc) · 3.53 KB
/
extract-release-notes.mjs
File metadata and controls
executable file
·130 lines (120 loc) · 3.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env node
/**
* Extract a release-notes block from CHANGELOG.md for a given version
* (or unwrap text supplied on stdin), then join hard-wrapped paragraphs.
*
* Why: GitHub renders release-note Markdown with GFM hard breaks, so
* every `\n` becomes `<br>`. The CHANGELOG is hard-wrapped at ~75
* chars for readable diffs, which then renders as awkward visible
* line breaks on the release page. This script joins indented
* continuation lines into a single line per bullet so the GFM
* renderer produces clean paragraphs.
*
* Repo-level CHANGELOG.md viewing is unaffected (CommonMark treats
* newlines as spaces there).
*
* Usage:
* extract-release-notes.mjs <version> # read CHANGELOG.md
* extract-release-notes.mjs --stdin # read from stdin (any text)
*/
import { readFileSync } from 'fs';
const arg = process.argv[2];
if (!arg) {
console.error('usage: extract-release-notes.mjs <version> | --stdin');
process.exit(1);
}
let block;
if (arg === '--stdin') {
block = readFileSync(0, 'utf8').replace(/\r\n?/g, '\n').split('\n');
} else {
const version = arg;
const escaped = version.replace(/\./g, '\\.');
const headerRe = new RegExp(`^## \\[${escaped}\\]`);
const anyHeaderRe = /^## \[/;
const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
const start = lines.findIndex((l) => headerRe.test(l));
if (start === -1) {
console.error(`no '## [${version}]' entry found in CHANGELOG.md`);
process.exit(1);
}
const after = lines.findIndex((l, i) => i > start && anyHeaderRe.test(l));
block = lines.slice(start, after === -1 ? lines.length : after);
}
// Track a stack of `{ indent: number }` frames so a continuation line
// can attach to the right ancestor. Handles the post-nested-list
// continuation pattern:
//
// - top-level
// - nested
// back to top-level <- 2-space indent, joins the top-level bullet
const out = [];
let buf = '';
let stack = [];
function flushBuf() {
if (buf !== '') {
out.push(buf);
buf = '';
}
}
function leadingSpaces(s) {
const m = s.match(/^(\s*)/);
return m ? m[1].length : 0;
}
// Bullets: `-`, `*`, `digit.` only. `+` is intentionally excluded — the
// CHANGELOG uses literal `+` inline (`config + instructions`) and we
// don't want to misread those as nested bullets.
const listItemRe = /^(\s*)([-*]|\d+\.)\s+/;
const fenceRe = /^\s*```/;
let inFence = false;
for (const line of block) {
// Fenced code blocks: pass through verbatim, no joining.
if (fenceRe.test(line)) {
flushBuf();
stack = [];
out.push(line);
inFence = !inFence;
continue;
}
if (inFence) {
out.push(line);
continue;
}
if (/^\s*$/.test(line)) {
flushBuf();
out.push('');
continue;
}
if (/^#/.test(line)) {
flushBuf();
stack = [];
out.push(line);
continue;
}
const itemMatch = line.match(listItemRe);
if (itemMatch) {
flushBuf();
const indent = itemMatch[1].length;
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
stack.pop();
}
stack.push({ indent });
buf = line;
continue;
}
if (/^\s/.test(line)) {
const indent = leadingSpaces(line);
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
flushBuf();
stack.pop();
}
const trimmed = line.replace(/^\s+/, '');
buf = buf === '' ? trimmed : `${buf} ${trimmed}`;
continue;
}
flushBuf();
stack = [];
out.push(line);
}
flushBuf();
process.stdout.write(out.join('\n'));
if (!out[out.length - 1]?.endsWith('\n')) process.stdout.write('\n');