changelog-parser 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a52b0f653d48af02c20cc4f1287dad6a76bcdc66efb802ba80470b21659a027e
4
+ data.tar.gz: f77abb93586e09bdf5b11e9e8022ab5d86656ec07ccbfad74fbd73c7f9cbed82
5
+ SHA512:
6
+ metadata.gz: 1fb1a622fe5b843d14f092acd112824221478ed0ec316b603d99f83e2dede43a31df04833fc226aa00a5574dd509731f3d88a62f19b91eed5cfa298740b6e9cc
7
+ data.tar.gz: 9d7c22d58a1520b85215c578778754d68d797fee125ee4cedd0e64cdb5caa6534d9935c9c3d19efb49a567b458ce6fa86393046f366cd7c8e0f2649593d80976
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-23
4
+
5
+ ### Added
6
+
7
+ - Core changelog parsing with auto-format detection
8
+ - Support for Keep a Changelog, markdown headers, and underline formats
9
+ - Custom regex pattern support for non-standard formats
10
+ - CLI with JSON output, version listing, and content extraction
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "changelog-parser" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["andrewnez@gmail.com"](mailto:"andrewnez@gmail.com").
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Nesbitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # Changelog::Parser
2
+
3
+ A Ruby gem for parsing changelog files into structured data. Supports the Keep a Changelog format, markdown headers, and custom patterns. Zero runtime dependencies.
4
+
5
+ Inspired by [vandamme](https://github.com/tech-angels/vandamme).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gem install changelog-parser
11
+ ```
12
+
13
+ Or add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "changelog-parser"
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Ruby API
22
+
23
+ ```ruby
24
+ require "changelog/parser"
25
+
26
+ # Parse a string
27
+ changelog = File.read("CHANGELOG.md")
28
+ parser = Changelog::Parser.new(changelog)
29
+ result = parser.parse
30
+
31
+ # Or use the class method
32
+ result = Changelog::Parser.parse(changelog)
33
+
34
+ # Or parse a file directly
35
+ result = Changelog::Parser.parse_file("CHANGELOG.md")
36
+ ```
37
+
38
+ The result is a hash where keys are version strings and values contain the date and content:
39
+
40
+ ```ruby
41
+ {
42
+ "Unreleased" => { date: nil, content: "### Added\n- New feature" },
43
+ "1.0.0" => { date: #<Date: 2024-01-15>, content: "### Added\n- Initial release" }
44
+ }
45
+ ```
46
+
47
+ ### Accessing versions
48
+
49
+ ```ruby
50
+ parser = Changelog::Parser.new(changelog)
51
+
52
+ # Get all version strings
53
+ parser.versions
54
+ # => ["Unreleased", "1.0.0"]
55
+
56
+ # Get a specific version
57
+ parser["1.0.0"]
58
+ # => { date: #<Date: 2024-01-15>, content: "..." }
59
+
60
+ # Convert to JSON
61
+ parser.to_json
62
+
63
+ # Convert to HTML (requires a markdown gem)
64
+ parser.to_html
65
+ ```
66
+
67
+ ### HTML conversion
68
+
69
+ The `to_html` method converts markdown content to HTML. It requires one of these optional gems to be installed separately:
70
+
71
+ - [commonmarker](https://github.com/gjtorikian/commonmarker)
72
+ - [redcarpet](https://github.com/vmg/redcarpet)
73
+ - [kramdown](https://github.com/gettalong/kramdown)
74
+
75
+ ```ruby
76
+ gem "commonmarker" # Add to your Gemfile
77
+
78
+ parser.to_html
79
+ # => { "1.0.0" => { date: #<Date>, content: "<ul><li>Feature</li></ul>" } }
80
+ ```
81
+
82
+ ### Formats
83
+
84
+ The parser auto-detects the changelog format. You can also specify it explicitly:
85
+
86
+ ```ruby
87
+ # Keep a Changelog: ## [1.0.0] - 2024-01-15
88
+ Changelog::Parser.new(content, format: :keep_a_changelog)
89
+
90
+ # Markdown headers: ## 1.0.0 or ### v1.0.0 (2024-01-15)
91
+ Changelog::Parser.new(content, format: :markdown)
92
+
93
+ # Underline style: 1.0.0\n=====
94
+ Changelog::Parser.new(content, format: :underline)
95
+ ```
96
+
97
+ ### Custom patterns
98
+
99
+ For changelogs with non-standard formats, provide a custom regex:
100
+
101
+ ```ruby
102
+ # Custom format: "Version 1.0.0 released 2024-01-15"
103
+ pattern = /^Version ([\d.]+) released (\d{4}-\d{2}-\d{2})/
104
+ parser = Changelog::Parser.new(content, version_pattern: pattern)
105
+ ```
106
+
107
+ The first capture group should be the version string. The second capture group (if present) is parsed as a date.
108
+
109
+ ## CLI
110
+
111
+ The gem includes a command-line interface:
112
+
113
+ ```bash
114
+ # Parse a changelog and output JSON
115
+ changelog-parser CHANGELOG.md
116
+
117
+ # Pretty print JSON
118
+ changelog-parser --pretty CHANGELOG.md
119
+
120
+ # List versions only
121
+ changelog-parser --list CHANGELOG.md
122
+
123
+ # Get a specific version
124
+ changelog-parser --version 1.0.0 CHANGELOG.md
125
+
126
+ # Get content only for a version
127
+ changelog-parser --version 1.0.0 --content CHANGELOG.md
128
+
129
+ # Specify format
130
+ changelog-parser --format keep_a_changelog CHANGELOG.md
131
+
132
+ # Read from stdin
133
+ cat CHANGELOG.md | changelog-parser -
134
+
135
+ # Custom regex pattern
136
+ changelog-parser --pattern "^## v([\d.]+)" CHANGELOG.md
137
+ ```
138
+
139
+ ### CLI options
140
+
141
+ ```
142
+ -f, --format FORMAT Changelog format (keep_a_changelog, markdown, underline)
143
+ -v, --version VERSION Show only a specific version
144
+ -l, --list List versions only
145
+ -c, --content Output content only (requires --version)
146
+ -p, --pattern REGEX Custom version header regex pattern
147
+ -m, --match-group N Regex capture group for version (default: 1)
148
+ --pretty Pretty print JSON output
149
+ -h, --help Show help message
150
+ --gem-version Show gem version
151
+ ```
152
+
153
+ ## Supported formats
154
+
155
+ ### Keep a Changelog
156
+
157
+ The default format follows the [Keep a Changelog](https://keepachangelog.com) specification:
158
+
159
+ ```markdown
160
+ ## [Unreleased]
161
+
162
+ ## [1.0.0] - 2024-01-15
163
+
164
+ ### Added
165
+ - New feature
166
+ ```
167
+
168
+ ### Markdown headers
169
+
170
+ Standard markdown headers with optional dates:
171
+
172
+ ```markdown
173
+ ## 1.0.0 (2024-01-15)
174
+
175
+ ### v0.9.0
176
+ ```
177
+
178
+ ### Underline style
179
+
180
+ Setext-style headers:
181
+
182
+ ```markdown
183
+ 1.0.0
184
+ =====
185
+ ```
186
+
187
+ ## Vandamme compatibility
188
+
189
+ For projects migrating from the [vandamme](https://github.com/tech-angels/vandamme) gem, a compatibility layer is provided:
190
+
191
+ ```ruby
192
+ require "changelog/parser/vandamme"
193
+
194
+ parser = Vandamme::Parser.new(
195
+ changelog: content,
196
+ version_header_exp: /^## ([\d.]+)/,
197
+ format: :markdown,
198
+ match_group: 1
199
+ )
200
+
201
+ # Returns plain strings like vandamme (no date hash)
202
+ parser.parse
203
+ # => { "1.0.0" => "### Added\n- Feature" }
204
+
205
+ # HTML conversion (requires markdown gem)
206
+ parser.to_html
207
+ # => { "1.0.0" => "<h3>Added</h3><ul><li>Feature</li></ul>" }
208
+ ```
209
+
210
+ ## Development
211
+
212
+ ```bash
213
+ bin/setup
214
+ rake test
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "json"
6
+ require_relative "../lib/changelog/parser"
7
+
8
+ options = {
9
+ format: nil,
10
+ output: :json,
11
+ version: nil
12
+ }
13
+
14
+ parser = OptionParser.new do |opts|
15
+ opts.banner = "Usage: changelog-parser [options] [file]"
16
+ opts.separator ""
17
+ opts.separator "Parse changelog files and extract version information."
18
+ opts.separator ""
19
+ opts.separator "Options:"
20
+
21
+ opts.on("-f", "--format FORMAT", %i[keep_a_changelog markdown underline],
22
+ "Changelog format (keep_a_changelog, markdown, underline)") do |f|
23
+ options[:format] = f
24
+ end
25
+
26
+ opts.on("-v", "--version VERSION", "Show only a specific version") do |v|
27
+ options[:version] = v
28
+ end
29
+
30
+ opts.on("-l", "--list", "List versions only") do
31
+ options[:output] = :list
32
+ end
33
+
34
+ opts.on("-c", "--content", "Output content only (requires --version)") do
35
+ options[:output] = :content
36
+ end
37
+
38
+ opts.on("-p", "--pattern REGEX", "Custom version header regex pattern") do |p|
39
+ options[:pattern] = Regexp.new(p)
40
+ end
41
+
42
+ opts.on("-m", "--match-group N", Integer, "Regex capture group for version (default: 1)") do |n|
43
+ options[:match_group] = n
44
+ end
45
+
46
+ opts.on("--pretty", "Pretty print JSON output") do
47
+ options[:pretty] = true
48
+ end
49
+
50
+ opts.on_tail("-h", "--help", "Show this help message") do
51
+ puts opts
52
+ exit
53
+ end
54
+
55
+ opts.on_tail("--gem-version", "Show gem version") do
56
+ puts Changelog::Parser::VERSION
57
+ exit
58
+ end
59
+ end
60
+
61
+ begin
62
+ parser.parse!
63
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
64
+ warn "Error: #{e.message}"
65
+ warn parser
66
+ exit 1
67
+ end
68
+
69
+ input = if ARGV.empty? || ARGV.first == "-"
70
+ $stdin.read
71
+ else
72
+ file = ARGV.first
73
+ unless File.exist?(file)
74
+ warn "Error: File not found: #{file}"
75
+ exit 1
76
+ end
77
+ File.read(file)
78
+ end
79
+
80
+ if input.nil? || input.empty?
81
+ warn "Error: No input provided"
82
+ warn parser
83
+ exit 1
84
+ end
85
+
86
+ parsed = Changelog::Parser.new(
87
+ input,
88
+ format: options[:format],
89
+ version_pattern: options[:pattern],
90
+ match_group: options[:match_group] || 1
91
+ )
92
+
93
+ result = parsed.parse
94
+
95
+ case options[:output]
96
+ when :list
97
+ puts result.keys.join("\n")
98
+ when :content
99
+ unless options[:version]
100
+ warn "Error: --content requires --version"
101
+ exit 1
102
+ end
103
+ entry = result[options[:version]]
104
+ if entry
105
+ puts entry[:content]
106
+ else
107
+ warn "Version not found: #{options[:version]}"
108
+ exit 1
109
+ end
110
+ when :json
111
+ output = if options[:version]
112
+ entry = result[options[:version]]
113
+ unless entry
114
+ warn "Version not found: #{options[:version]}"
115
+ exit 1
116
+ end
117
+ { options[:version] => entry }
118
+ else
119
+ result
120
+ end
121
+
122
+ # Convert dates to strings for JSON
123
+ json_output = output.transform_values do |entry|
124
+ entry.transform_values { |v| v.is_a?(Date) ? v.to_s : v }
125
+ end
126
+
127
+ if options[:pretty]
128
+ puts JSON.pretty_generate(json_output)
129
+ else
130
+ puts JSON.generate(json_output)
131
+ end
132
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser"
4
+
5
+ module Vandamme
6
+ class Parser
7
+ attr_reader :parser
8
+
9
+ def initialize(changelog: "", version_header_exp: nil, format: nil, match_group: 1)
10
+ @parser = Changelog::Parser.new(
11
+ changelog,
12
+ version_pattern: version_header_exp,
13
+ format: format,
14
+ match_group: match_group
15
+ )
16
+ end
17
+
18
+ def parse
19
+ @parser.parse.transform_values { |entry| entry[:content] }
20
+ end
21
+
22
+ def to_html
23
+ parsed = parse
24
+ parsed.transform_values { |content| render_html(content) }
25
+ end
26
+
27
+ def render_html(content)
28
+ return content if content.nil? || content.empty?
29
+
30
+ if defined?(Commonmarker)
31
+ Commonmarker.to_html(content)
32
+ elsif defined?(CommonMarker)
33
+ CommonMarker.render_html(content)
34
+ elsif defined?(Redcarpet)
35
+ markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
36
+ markdown.render(content)
37
+ elsif defined?(Kramdown)
38
+ Kramdown::Document.new(content).to_html
39
+ else
40
+ raise Changelog::Parser::Error,
41
+ "No markdown renderer found. Install commonmarker, redcarpet, or kramdown."
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Changelog
4
+ class Parser
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parser/version"
4
+
5
+ module Changelog
6
+ class Parser
7
+ class Error < StandardError; end
8
+ class ParseError < Error; end
9
+
10
+ # Keep a Changelog format: ## [1.0.0] - 2024-01-15 or ## [Unreleased]
11
+ KEEP_A_CHANGELOG = /^##\s+\[([^\]]+)\](?:\s+-\s+(\d{4}-\d{2}-\d{2}))?/
12
+
13
+ # Markdown headers with version: ## 1.0.0 or ### v1.0.0 (2024-01-15)
14
+ MARKDOWN_HEADER = /^\#{1,3}\s+v?([\w.+-]+\.[\w.+-]+[a-zA-Z0-9])(?:\s+\((\d{4}-\d{2}-\d{2})\))?/
15
+
16
+ # Underline style: 1.0.0\n===== or 1.0.0\n-----
17
+ UNDERLINE_HEADER = /^([\w.+-]+\.[\w.+-]+[a-zA-Z0-9])\n[=-]+/
18
+
19
+ FORMATS = {
20
+ keep_a_changelog: KEEP_A_CHANGELOG,
21
+ markdown: MARKDOWN_HEADER,
22
+ underline: UNDERLINE_HEADER
23
+ }.freeze
24
+
25
+ attr_reader :changelog, :version_pattern, :match_group
26
+
27
+ def initialize(changelog, format: nil, version_pattern: nil, match_group: 1)
28
+ @changelog = changelog.to_s
29
+ @version_pattern = resolve_pattern(format, version_pattern)
30
+ @match_group = match_group
31
+ end
32
+
33
+ def parse
34
+ return {} if changelog.empty?
35
+
36
+ versions = {}
37
+ matches = find_version_matches
38
+
39
+ matches.each_with_index do |match, index|
40
+ version = match[:version]
41
+ start_pos = match[:end_pos]
42
+ end_pos = matches[index + 1]&.dig(:start_pos) || changelog.length
43
+
44
+ content = changelog[start_pos...end_pos].strip
45
+ versions[version] = build_entry(match, content)
46
+ end
47
+
48
+ versions
49
+ end
50
+
51
+ def versions
52
+ parse.keys
53
+ end
54
+
55
+ def [](version)
56
+ parse[version]
57
+ end
58
+
59
+ def to_h
60
+ parse
61
+ end
62
+
63
+ def to_json(*)
64
+ require "json"
65
+ parse.to_json
66
+ end
67
+
68
+ def to_html
69
+ parse.transform_values do |entry|
70
+ {
71
+ date: entry[:date],
72
+ content: render_html(entry[:content])
73
+ }
74
+ end
75
+ end
76
+
77
+ def render_html(content)
78
+ return content if content.nil? || content.empty?
79
+
80
+ if defined?(Commonmarker)
81
+ Commonmarker.to_html(content)
82
+ elsif defined?(CommonMarker)
83
+ CommonMarker.render_html(content)
84
+ elsif defined?(Redcarpet)
85
+ markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
86
+ markdown.render(content)
87
+ elsif defined?(Kramdown)
88
+ Kramdown::Document.new(content).to_html
89
+ else
90
+ raise Error, "No markdown renderer found. Install commonmarker, redcarpet, or kramdown."
91
+ end
92
+ end
93
+
94
+ def self.parse(changelog, **options)
95
+ new(changelog, **options).parse
96
+ end
97
+
98
+ def self.parse_file(path, **options)
99
+ content = File.read(path)
100
+ new(content, **options).parse
101
+ end
102
+
103
+ def resolve_pattern(format, custom_pattern)
104
+ return custom_pattern if custom_pattern
105
+ return FORMATS.fetch(format) if format
106
+
107
+ detect_format
108
+ end
109
+
110
+ def detect_format
111
+ return KEEP_A_CHANGELOG if changelog.match?(KEEP_A_CHANGELOG)
112
+ return UNDERLINE_HEADER if changelog.match?(UNDERLINE_HEADER)
113
+
114
+ MARKDOWN_HEADER
115
+ end
116
+
117
+ def find_version_matches
118
+ matches = []
119
+ scanner = StringScanner.new(changelog)
120
+
121
+ while scanner.scan_until(version_pattern)
122
+ matched = scanner.matched
123
+ match_data = matched.match(version_pattern)
124
+
125
+ matches << {
126
+ version: match_data[match_group],
127
+ date: extract_date(match_data),
128
+ start_pos: scanner.pos - matched.length,
129
+ end_pos: scanner.pos
130
+ }
131
+ end
132
+
133
+ matches
134
+ end
135
+
136
+ def extract_date(match_data)
137
+ return nil if match_data.captures.length < 2
138
+
139
+ date_str = match_data[match_group + 1]
140
+ return nil unless date_str
141
+
142
+ Date.parse(date_str) rescue nil
143
+ end
144
+
145
+ def build_entry(match, content)
146
+ {
147
+ date: match[:date],
148
+ content: content
149
+ }
150
+ end
151
+ end
152
+ end
153
+
154
+ require "strscan"
155
+ require "date"
@@ -0,0 +1,6 @@
1
+ module Changelog
2
+ module Parser
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: changelog-parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Nesbitt
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Ruby gem for parsing changelog files. Supports Keep a Changelog format,
13
+ markdown headers, and custom patterns. Zero runtime dependencies.
14
+ email:
15
+ - andrewnez@gmail.com
16
+ executables:
17
+ - changelog-parser
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - CODE_OF_CONDUCT.md
23
+ - LICENSE
24
+ - README.md
25
+ - Rakefile
26
+ - exe/changelog-parser
27
+ - lib/changelog/parser.rb
28
+ - lib/changelog/parser/vandamme.rb
29
+ - lib/changelog/parser/version.rb
30
+ - sig/changelog/parser.rbs
31
+ homepage: https://github.com/andrew/changelog-parser
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://github.com/andrew/changelog-parser
36
+ source_code_uri: https://github.com/andrew/changelog-parser
37
+ changelog_uri: https://github.com/andrew/changelog-parser/blob/main/CHANGELOG.md
38
+ rubygems_mfa_required: 'true'
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.2.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 4.0.1
54
+ specification_version: 4
55
+ summary: Parse changelog files into structured data
56
+ test_files: []