@alleyinteractive/changelog-extractor 0.0.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -31
- package/action.yml +28 -0
- package/dist/index.js +27563 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -16
- package/src/cli.js +136 -0
- package/src/index.js +4 -44
- package/src/main.js +50 -0
- package/src/parser.js +157 -55
- package/cli.js +0 -112
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alleyinteractive/changelog-extractor",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Parse and extract structured information from Keep a Changelog formatted changelogs",
|
|
5
6
|
"main": "src/parser.js",
|
|
6
7
|
"bin": {
|
|
7
|
-
"changelog-extractor": "./cli.js"
|
|
8
|
+
"changelog-extractor": "./src/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"build": "npm run prettier && npm run package",
|
|
12
|
+
"fix-and-build": "npm run prettier:fix && npm run package",
|
|
13
|
+
"package:watch": "npm run package -- --watch",
|
|
14
|
+
"package": "npx rollup --config rollup.config.js",
|
|
15
|
+
"prepublishOnly": "npm test",
|
|
16
|
+
"prettier:fix": "npx prettier --write .",
|
|
17
|
+
"prettier": "npx prettier --check .",
|
|
18
|
+
"release:major": "npm version major && npm publish --access public && git push --follow-tags",
|
|
19
|
+
"release:minor": "npm version minor && npm publish --access public && git push --follow-tags",
|
|
20
|
+
"release:patch": "npm version patch && npm publish --access public && git push --follow-tags",
|
|
21
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest"
|
|
13
22
|
},
|
|
14
23
|
"keywords": [
|
|
15
24
|
"changelog",
|
|
@@ -31,26 +40,27 @@
|
|
|
31
40
|
"homepage": "https://github.com/alleyinteractive/action-changelog-extractor#readme",
|
|
32
41
|
"files": [
|
|
33
42
|
"src/",
|
|
43
|
+
"dist/",
|
|
34
44
|
"cli.js",
|
|
45
|
+
"action.yml",
|
|
35
46
|
"README.md",
|
|
36
47
|
"LICENSE"
|
|
37
48
|
],
|
|
49
|
+
"exports": {
|
|
50
|
+
".": "./dist/index.js"
|
|
51
|
+
},
|
|
38
52
|
"devDependencies": {
|
|
53
|
+
"@jest/globals": "^30.2.0",
|
|
54
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
55
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
39
56
|
"@types/jest": "^29.5.12",
|
|
40
57
|
"@types/node": "^20.11.24",
|
|
41
|
-
"jest": "^
|
|
58
|
+
"jest": "^30.2.0",
|
|
59
|
+
"prettier": "^3.6.2",
|
|
60
|
+
"prettier-eslint": "^16.4.2",
|
|
61
|
+
"rollup": "^4.53.3"
|
|
42
62
|
},
|
|
43
63
|
"dependencies": {
|
|
44
64
|
"@actions/core": "^1.10.1"
|
|
45
|
-
},
|
|
46
|
-
"jest": {
|
|
47
|
-
"testEnvironment": "node",
|
|
48
|
-
"coverageDirectory": "coverage",
|
|
49
|
-
"collectCoverageFrom": [
|
|
50
|
-
"src/**/*.js"
|
|
51
|
-
],
|
|
52
|
-
"testMatch": [
|
|
53
|
-
"**/__tests__/**/*.test.js"
|
|
54
|
-
]
|
|
55
65
|
}
|
|
56
66
|
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Changelog Extractor CLI
|
|
5
|
+
*
|
|
6
|
+
* Parse and extract structured information from Keep a Changelog formatted files.
|
|
7
|
+
* Can be run via npx or installed globally.
|
|
8
|
+
*
|
|
9
|
+
* @package @alleyinteractive/changelog-extractor
|
|
10
|
+
* @author Alley Interactive
|
|
11
|
+
* @license GPL-3.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs'
|
|
15
|
+
import path from 'path'
|
|
16
|
+
import { parseChangelog } from './parser.js'
|
|
17
|
+
|
|
18
|
+
function printUsage() {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: npx @alleyinteractive/changelog-extractor [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-f, --file <path> Path to changelog file (default: CHANGELOG.md)
|
|
24
|
+
-v, --version <ver> Specific version to extract (e.g., "1.14.0" or "v1.14.0")
|
|
25
|
+
Leave empty to return all versions
|
|
26
|
+
-l, --list List the count of versions found and exit
|
|
27
|
+
-h, --help Show this help message
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
npx @alleyinteractive/changelog-extractor # Parse CHANGELOG.md, all versions
|
|
31
|
+
npx @alleyinteractive/changelog-extractor -l # Count versions in CHANGELOG.md
|
|
32
|
+
npx @alleyinteractive/changelog-extractor -v 1.14.0 # Extract specific version
|
|
33
|
+
npx @alleyinteractive/changelog-extractor -f /tmp/test.md # Parse specific file
|
|
34
|
+
npx @alleyinteractive/changelog-extractor -f CHANGELOG.md -v 1.14.0 # Both options
|
|
35
|
+
changelog-extractor -l # If installed globally
|
|
36
|
+
`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse command line arguments
|
|
40
|
+
const args = process.argv.slice(2)
|
|
41
|
+
|
|
42
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
43
|
+
printUsage()
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const listMode = args.includes('-l') || args.includes('--list')
|
|
48
|
+
|
|
49
|
+
// Parse file option
|
|
50
|
+
let changelogFile = 'CHANGELOG.md'
|
|
51
|
+
const fileIndex = args.findIndex((arg) => arg === '-f' || arg === '--file')
|
|
52
|
+
if (fileIndex !== -1 && args[fileIndex + 1]) {
|
|
53
|
+
changelogFile = args[fileIndex + 1]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse version option
|
|
57
|
+
let versionArg = null
|
|
58
|
+
const versionIndex = args.findIndex(
|
|
59
|
+
(arg) => arg === '-v' || arg === '--version'
|
|
60
|
+
)
|
|
61
|
+
if (versionIndex !== -1 && args[versionIndex + 1]) {
|
|
62
|
+
versionArg = args[versionIndex + 1]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const version = versionArg
|
|
66
|
+
|
|
67
|
+
// Read changelog file
|
|
68
|
+
const changelogPath = path.resolve(process.cwd(), changelogFile)
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(changelogPath)) {
|
|
71
|
+
console.error(`❌ Error: Changelog file not found: ${changelogPath}`)
|
|
72
|
+
console.error(
|
|
73
|
+
'\nTip: You can create a test changelog at /tmp/test-changelog.md'
|
|
74
|
+
)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const changelogContent = fs.readFileSync(changelogPath, 'utf8')
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const results = parseChangelog(changelogContent, version)
|
|
82
|
+
|
|
83
|
+
// List mode: just display count and exit
|
|
84
|
+
if (listMode) {
|
|
85
|
+
console.log(`${results.length}`)
|
|
86
|
+
process.exit(0)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse changelog
|
|
90
|
+
console.log(`📖 Parsing: ${changelogPath}`)
|
|
91
|
+
console.log(`🎯 Version: ${versionArg || 'all versions'}`)
|
|
92
|
+
console.log('')
|
|
93
|
+
|
|
94
|
+
if (results.length === 0) {
|
|
95
|
+
console.log('⚠️ No changelog entries found')
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`✅ Found ${results.length} version(s)\n`)
|
|
100
|
+
console.log('─'.repeat(80))
|
|
101
|
+
|
|
102
|
+
// Pretty print results
|
|
103
|
+
results.forEach((entry, index) => {
|
|
104
|
+
console.log(`\n📦 Version: ${entry.name}`)
|
|
105
|
+
console.log(
|
|
106
|
+
`📝 Sections: ${Object.keys(entry.sections).join(', ') || 'none'}`
|
|
107
|
+
)
|
|
108
|
+
console.log('')
|
|
109
|
+
|
|
110
|
+
// Print sections
|
|
111
|
+
for (const [sectionName, sectionContent] of Object.entries(
|
|
112
|
+
entry.sections
|
|
113
|
+
)) {
|
|
114
|
+
console.log(
|
|
115
|
+
` ### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`
|
|
116
|
+
)
|
|
117
|
+
console.log('')
|
|
118
|
+
// Indent content
|
|
119
|
+
const lines = sectionContent.split('\n')
|
|
120
|
+
lines.forEach((line) => console.log(` ${line}`))
|
|
121
|
+
console.log('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (index < results.length - 1) {
|
|
125
|
+
console.log('─'.repeat(80))
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Output JSON at the end
|
|
130
|
+
console.log('\n' + '═'.repeat(80))
|
|
131
|
+
console.log('\n📋 JSON Output:\n')
|
|
132
|
+
console.log(JSON.stringify(results, null, 2))
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('❌ Error parsing changelog:', error.message)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,47 +1,7 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { parseChangelog } = require('./parser');
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
2
|
+
* The entrypoint for the action. This file simply imports and runs the action's
|
|
3
|
+
* main logic.
|
|
7
4
|
*/
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
// Get inputs
|
|
11
|
-
const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md';
|
|
12
|
-
const versionInput = core.getInput('version');
|
|
13
|
-
|
|
14
|
-
// Only pass version if explicitly specified, otherwise return all versions
|
|
15
|
-
const version = versionInput || null;
|
|
16
|
-
|
|
17
|
-
// Read changelog file
|
|
18
|
-
const fullPath = path.resolve(process.cwd(), changelogPath);
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(fullPath)) {
|
|
21
|
-
throw new Error(`Changelog file not found: ${fullPath}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const changelogContent = fs.readFileSync(fullPath, 'utf8');
|
|
25
|
-
|
|
26
|
-
// Parse changelog
|
|
27
|
-
const results = parseChangelog(changelogContent, version);
|
|
28
|
-
|
|
29
|
-
if (results.length === 0) {
|
|
30
|
-
core.warning(`No changelog entries found${version ? ` for version ${version}` : ''}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Set output as JSON string
|
|
34
|
-
core.setOutput('result', JSON.stringify(results));
|
|
35
|
-
|
|
36
|
-
// Log summary
|
|
37
|
-
core.info(`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`);
|
|
38
|
-
for (const entry of results) {
|
|
39
|
-
core.info(` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
} catch (error) {
|
|
43
|
-
core.setFailed(error.message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
5
|
+
import { run } from './main.js'
|
|
46
6
|
|
|
47
|
-
|
|
7
|
+
run()
|
package/src/main.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as core from '@actions/core'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { parseChangelog } from './parser.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main action entry point
|
|
8
|
+
*/
|
|
9
|
+
export async function run() {
|
|
10
|
+
try {
|
|
11
|
+
const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md'
|
|
12
|
+
const versionInput = core.getInput('version')
|
|
13
|
+
|
|
14
|
+
// Only pass version if explicitly specified, otherwise return all versions
|
|
15
|
+
const version = versionInput || null
|
|
16
|
+
|
|
17
|
+
// Read changelog file
|
|
18
|
+
const fullPath = path.resolve(process.cwd(), changelogPath)
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(fullPath)) {
|
|
21
|
+
throw new Error(`Changelog file not found: ${fullPath}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const changelogContent = fs.readFileSync(fullPath, 'utf8')
|
|
25
|
+
|
|
26
|
+
// Parse changelog
|
|
27
|
+
const results = parseChangelog(changelogContent, version)
|
|
28
|
+
|
|
29
|
+
if (results.length === 0) {
|
|
30
|
+
core.warning(
|
|
31
|
+
`No changelog entries found${version ? ` for version ${version}` : ''}`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set output as JSON string
|
|
36
|
+
core.setOutput('result', JSON.stringify(results))
|
|
37
|
+
|
|
38
|
+
// Log summary
|
|
39
|
+
core.info(
|
|
40
|
+
`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`
|
|
41
|
+
)
|
|
42
|
+
for (const entry of results) {
|
|
43
|
+
core.info(
|
|
44
|
+
` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
core.setFailed(error.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unwrap text that has been wrapped across multiple lines.
|
|
3
|
+
* This fixes changelog text that has been hard-wrapped for readability
|
|
4
|
+
* but should flow as continuous text.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} text - The text content to unwrap
|
|
7
|
+
* @returns {string} The unwrapped text
|
|
8
|
+
*/
|
|
9
|
+
function unwrapText(text) {
|
|
10
|
+
const lines = text.split('\n')
|
|
11
|
+
const unwrappedLines = []
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
let currentLine = lines[i]
|
|
15
|
+
|
|
16
|
+
// Skip empty lines - preserve them as-is
|
|
17
|
+
if (currentLine.trim() === '') {
|
|
18
|
+
unwrappedLines.push(currentLine)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Collect continuation lines
|
|
23
|
+
let j = i + 1
|
|
24
|
+
while (j < lines.length) {
|
|
25
|
+
const nextLine = lines[j]
|
|
26
|
+
|
|
27
|
+
// Stop if next line is empty
|
|
28
|
+
if (nextLine.trim() === '') {
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmedCurrent = currentLine.trim()
|
|
33
|
+
const trimmedNext = nextLine.trim()
|
|
34
|
+
|
|
35
|
+
// Don't unwrap if current line looks like it ends a sentence/item
|
|
36
|
+
const endsWithPunctuation = /[.!?:;]$/.test(trimmedCurrent)
|
|
37
|
+
const endsWithCode = /`$/.test(trimmedCurrent)
|
|
38
|
+
const endsWithBullet = /[-*+]\s*$/.test(trimmedCurrent)
|
|
39
|
+
|
|
40
|
+
// Don't unwrap if next line starts a new bullet point, section, or sentence
|
|
41
|
+
const startsWithBullet = /^[-*+]\s/.test(trimmedNext)
|
|
42
|
+
const startsWithNumber = /^\d+\.\s/.test(trimmedNext)
|
|
43
|
+
const startsWithMarkdown = /^[#>]/.test(trimmedNext)
|
|
44
|
+
|
|
45
|
+
// More nuanced check for new sentences
|
|
46
|
+
const prevEndsWithContinuation =
|
|
47
|
+
/[,&]$/.test(trimmedCurrent) ||
|
|
48
|
+
/\b(and|or|but|with|for|to|from|in|on|at|by)$/.test(trimmedCurrent)
|
|
49
|
+
const startsWithCapitalNewSentence =
|
|
50
|
+
/^[A-Z]/.test(trimmedNext) &&
|
|
51
|
+
!prevEndsWithContinuation &&
|
|
52
|
+
endsWithPunctuation
|
|
53
|
+
|
|
54
|
+
// Determine if we should unwrap this line with the next
|
|
55
|
+
const shouldUnwrap =
|
|
56
|
+
!endsWithPunctuation &&
|
|
57
|
+
!endsWithCode &&
|
|
58
|
+
!endsWithBullet &&
|
|
59
|
+
!startsWithBullet &&
|
|
60
|
+
!startsWithNumber &&
|
|
61
|
+
!startsWithCapitalNewSentence &&
|
|
62
|
+
!startsWithMarkdown
|
|
63
|
+
|
|
64
|
+
if (shouldUnwrap) {
|
|
65
|
+
// Join the lines, preserving the original indentation of the first line
|
|
66
|
+
// but removing indentation from continuation lines
|
|
67
|
+
currentLine = currentLine + ' ' + trimmedNext
|
|
68
|
+
j++ // Move to next line
|
|
69
|
+
} else {
|
|
70
|
+
break // Stop unwrapping
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unwrappedLines.push(currentLine)
|
|
75
|
+
i = j - 1 // Skip the lines we've consumed (j-1 because for loop will increment)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return unwrappedLines.join('\n')
|
|
79
|
+
}
|
|
80
|
+
|
|
1
81
|
/**
|
|
2
82
|
* Parse a Keep a Changelog formatted changelog file.
|
|
3
83
|
*
|
|
@@ -5,31 +85,31 @@
|
|
|
5
85
|
* @param {string|null} version - Specific version to extract (e.g., "v1.14.0" or "1.14.0"), or null/undefined for all versions
|
|
6
86
|
* @returns {Array<{name: string, sections: Object, contents: string}>} Array of version objects
|
|
7
87
|
*/
|
|
8
|
-
function parseChangelog(changelogContent, version = null) {
|
|
9
|
-
const versions = []
|
|
10
|
-
const lines = changelogContent.split('\n')
|
|
88
|
+
export function parseChangelog(changelogContent, version = null) {
|
|
89
|
+
const versions = []
|
|
90
|
+
const lines = changelogContent.split('\n')
|
|
11
91
|
|
|
12
|
-
let currentVersion = null
|
|
13
|
-
let currentSection = null
|
|
14
|
-
let currentSectionContent = []
|
|
15
|
-
let generalContent = []
|
|
92
|
+
let currentVersion = null
|
|
93
|
+
let currentSection = null
|
|
94
|
+
let currentSectionContent = []
|
|
95
|
+
let generalContent = [] // Content without section headers
|
|
16
96
|
|
|
17
97
|
// Regex to match version headers: ## v1.14.0 or ## v1.14.0 - 2024-04-29
|
|
18
|
-
const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))
|
|
98
|
+
const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))?$/
|
|
19
99
|
// Regex to match section headers: ### Added, ### Changed, etc.
|
|
20
|
-
const sectionRegex = /^###\s+(.+)
|
|
100
|
+
const sectionRegex = /^###\s+(.+)$/
|
|
21
101
|
|
|
22
102
|
for (let i = 0; i < lines.length; i++) {
|
|
23
|
-
const line = lines[i]
|
|
103
|
+
const line = lines[i]
|
|
24
104
|
|
|
25
105
|
// Check for version header
|
|
26
|
-
const versionMatch = line.match(versionRegex)
|
|
106
|
+
const versionMatch = line.match(versionRegex)
|
|
27
107
|
if (versionMatch) {
|
|
28
108
|
// Save previous version if exists
|
|
29
109
|
if (currentVersion) {
|
|
30
|
-
saveCurrentSection()
|
|
31
|
-
saveGeneralContent()
|
|
32
|
-
versions.push(currentVersion)
|
|
110
|
+
saveCurrentSection()
|
|
111
|
+
saveGeneralContent()
|
|
112
|
+
versions.push(currentVersion)
|
|
33
113
|
}
|
|
34
114
|
|
|
35
115
|
// Start new version
|
|
@@ -37,62 +117,70 @@ function parseChangelog(changelogContent, version = null) {
|
|
|
37
117
|
name: versionMatch[1], // Version without 'v' prefix
|
|
38
118
|
sections: {},
|
|
39
119
|
contents: ''
|
|
40
|
-
}
|
|
41
|
-
currentSection = null
|
|
42
|
-
currentSectionContent = []
|
|
43
|
-
generalContent = []
|
|
44
|
-
continue
|
|
120
|
+
}
|
|
121
|
+
currentSection = null
|
|
122
|
+
currentSectionContent = []
|
|
123
|
+
generalContent = []
|
|
124
|
+
continue
|
|
45
125
|
}
|
|
46
126
|
|
|
47
127
|
// Check for section header
|
|
48
|
-
const sectionMatch = line.match(sectionRegex)
|
|
128
|
+
const sectionMatch = line.match(sectionRegex)
|
|
49
129
|
if (sectionMatch && currentVersion) {
|
|
50
130
|
// If we have general content, save it before starting a new section
|
|
51
131
|
if (generalContent.length > 0) {
|
|
52
|
-
saveGeneralContent()
|
|
132
|
+
saveGeneralContent()
|
|
53
133
|
}
|
|
54
134
|
|
|
55
135
|
// Save previous section if exists
|
|
56
|
-
saveCurrentSection()
|
|
136
|
+
saveCurrentSection()
|
|
57
137
|
|
|
58
138
|
// Start new section
|
|
59
|
-
currentSection = sectionMatch[1].toLowerCase()
|
|
60
|
-
currentSectionContent = []
|
|
61
|
-
continue
|
|
139
|
+
currentSection = sectionMatch[1].toLowerCase()
|
|
140
|
+
currentSectionContent = []
|
|
141
|
+
continue
|
|
62
142
|
}
|
|
63
143
|
|
|
64
144
|
// Add content to current section or general content
|
|
65
145
|
if (currentVersion) {
|
|
66
146
|
if (currentSection) {
|
|
67
|
-
currentSectionContent.push(line)
|
|
147
|
+
currentSectionContent.push(line)
|
|
68
148
|
} else {
|
|
69
149
|
// Content without a section header goes to general
|
|
70
|
-
generalContent.push(line)
|
|
150
|
+
generalContent.push(line)
|
|
71
151
|
}
|
|
72
152
|
}
|
|
73
153
|
}
|
|
74
154
|
|
|
75
155
|
// Save final version and section
|
|
76
156
|
if (currentVersion) {
|
|
77
|
-
saveCurrentSection()
|
|
78
|
-
saveGeneralContent()
|
|
79
|
-
versions.push(currentVersion)
|
|
157
|
+
saveCurrentSection()
|
|
158
|
+
saveGeneralContent()
|
|
159
|
+
versions.push(currentVersion)
|
|
80
160
|
}
|
|
81
161
|
|
|
82
162
|
// Helper function to save the current section
|
|
83
163
|
function saveCurrentSection() {
|
|
84
164
|
if (currentVersion && currentSection && currentSectionContent.length > 0) {
|
|
85
165
|
// Trim empty lines from start and end
|
|
86
|
-
while (
|
|
87
|
-
currentSectionContent.
|
|
166
|
+
while (
|
|
167
|
+
currentSectionContent.length > 0 &&
|
|
168
|
+
currentSectionContent[0].trim() === ''
|
|
169
|
+
) {
|
|
170
|
+
currentSectionContent.shift()
|
|
88
171
|
}
|
|
89
|
-
while (
|
|
90
|
-
currentSectionContent.
|
|
172
|
+
while (
|
|
173
|
+
currentSectionContent.length > 0 &&
|
|
174
|
+
currentSectionContent[currentSectionContent.length - 1].trim() === ''
|
|
175
|
+
) {
|
|
176
|
+
currentSectionContent.pop()
|
|
91
177
|
}
|
|
92
178
|
|
|
93
|
-
const content = currentSectionContent.join('\n')
|
|
179
|
+
const content = currentSectionContent.join('\n')
|
|
94
180
|
if (content.trim()) {
|
|
95
|
-
|
|
181
|
+
// Unwrap any text that has been hard-wrapped across lines
|
|
182
|
+
const unwrappedContent = unwrapText(content)
|
|
183
|
+
currentVersion.sections[currentSection] = unwrappedContent
|
|
96
184
|
}
|
|
97
185
|
}
|
|
98
186
|
}
|
|
@@ -101,59 +189,73 @@ function parseChangelog(changelogContent, version = null) {
|
|
|
101
189
|
function saveGeneralContent() {
|
|
102
190
|
if (currentVersion && generalContent.length > 0) {
|
|
103
191
|
// Trim empty lines from start and end
|
|
104
|
-
let trimmedContent = [...generalContent]
|
|
192
|
+
let trimmedContent = [...generalContent]
|
|
105
193
|
while (trimmedContent.length > 0 && trimmedContent[0].trim() === '') {
|
|
106
|
-
trimmedContent.shift()
|
|
194
|
+
trimmedContent.shift()
|
|
107
195
|
}
|
|
108
|
-
while (
|
|
109
|
-
trimmedContent.
|
|
196
|
+
while (
|
|
197
|
+
trimmedContent.length > 0 &&
|
|
198
|
+
trimmedContent[trimmedContent.length - 1].trim() === ''
|
|
199
|
+
) {
|
|
200
|
+
trimmedContent.pop()
|
|
110
201
|
}
|
|
111
202
|
|
|
112
|
-
const content = trimmedContent.join('\n')
|
|
203
|
+
const content = trimmedContent.join('\n')
|
|
113
204
|
if (content.trim()) {
|
|
114
|
-
|
|
205
|
+
// Unwrap any text that has been hard-wrapped across lines
|
|
206
|
+
const unwrappedContent = unwrapText(content)
|
|
207
|
+
currentVersion.sections['general'] = unwrappedContent
|
|
115
208
|
}
|
|
116
|
-
generalContent = []
|
|
209
|
+
generalContent = []
|
|
117
210
|
}
|
|
118
211
|
}
|
|
119
212
|
|
|
120
213
|
// Build contents for each version (all sections combined)
|
|
121
214
|
for (const ver of versions) {
|
|
122
|
-
const contentParts = []
|
|
215
|
+
const contentParts = []
|
|
123
216
|
|
|
124
217
|
// Order sections in conventional order, with general at the end
|
|
125
|
-
const sectionOrder = [
|
|
218
|
+
const sectionOrder = [
|
|
219
|
+
'added',
|
|
220
|
+
'changed',
|
|
221
|
+
'deprecated',
|
|
222
|
+
'removed',
|
|
223
|
+
'fixed',
|
|
224
|
+
'security'
|
|
225
|
+
]
|
|
126
226
|
|
|
127
227
|
for (const sectionName of sectionOrder) {
|
|
128
228
|
if (ver.sections[sectionName]) {
|
|
129
|
-
contentParts.push(
|
|
229
|
+
contentParts.push(
|
|
230
|
+
`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${ver.sections[sectionName]}`
|
|
231
|
+
)
|
|
130
232
|
}
|
|
131
233
|
}
|
|
132
234
|
|
|
133
235
|
// Add any sections not in the standard order (except 'general')
|
|
134
236
|
for (const [sectionName, sectionContent] of Object.entries(ver.sections)) {
|
|
135
237
|
if (!sectionOrder.includes(sectionName) && sectionName !== 'general') {
|
|
136
|
-
contentParts.push(
|
|
238
|
+
contentParts.push(
|
|
239
|
+
`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${sectionContent}`
|
|
240
|
+
)
|
|
137
241
|
}
|
|
138
242
|
}
|
|
139
243
|
|
|
140
244
|
// Add general section last if it exists (without a header since it's raw content)
|
|
141
245
|
if (ver.sections['general']) {
|
|
142
|
-
contentParts.push(ver.sections['general'])
|
|
246
|
+
contentParts.push(ver.sections['general'])
|
|
143
247
|
}
|
|
144
248
|
|
|
145
|
-
ver.contents = contentParts.join('\n\n')
|
|
249
|
+
ver.contents = contentParts.join('\n\n')
|
|
146
250
|
}
|
|
147
251
|
|
|
148
252
|
// Filter by specific version if requested
|
|
149
253
|
if (version) {
|
|
150
|
-
const normalizedVersion = version.replace(/^v/, '')
|
|
151
|
-
const filtered = versions.filter(v => v.name === normalizedVersion)
|
|
152
|
-
return filtered
|
|
254
|
+
const normalizedVersion = version.replace(/^v/, '') // Remove 'v' prefix if present
|
|
255
|
+
const filtered = versions.filter((v) => v.name === normalizedVersion)
|
|
256
|
+
return filtered
|
|
153
257
|
}
|
|
154
258
|
|
|
155
259
|
// Return all versions by default
|
|
156
|
-
return versions
|
|
260
|
+
return versions
|
|
157
261
|
}
|
|
158
|
-
|
|
159
|
-
module.exports = { parseChangelog };
|