commenter 0.3.1 → 0.5.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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "nokogiri"
5
+ require "zip"
6
+
7
+ RSpec.describe Commenter::Filler do
8
+ let(:template_path) { File.expand_path("../../data/iso_comment_template_2012-03.docx", __dir__) }
9
+
10
+ def comment(attrs)
11
+ Commenter::Comment.new({ body: "US", type: "technical", comments: "Text" }.merge(attrs))
12
+ end
13
+
14
+ def header_xml(output_path)
15
+ Zip::File.open(output_path) do |zip|
16
+ zip.entries.find { |entry| entry.name == "word/header2.xml" }.get_input_stream(&:read)
17
+ end
18
+ end
19
+
20
+ def body_xml(output_path)
21
+ Zip::File.open(output_path) do |zip|
22
+ zip.entries.find { |entry| entry.name == "word/document.xml" }.get_input_stream(&:read)
23
+ end
24
+ end
25
+
26
+ # Rows of the comment table as arrays of cell text. Read via Nokogiri over
27
+ # the extracted XML rather than Docx::Document, whose open zip handle keeps
28
+ # the file locked on Windows and breaks Dir.mktmpdir teardown.
29
+ def body_rows(output_path)
30
+ document = Nokogiri::XML(body_xml(output_path))
31
+ document.xpath("//w:tbl").flat_map { |table| table.xpath("./w:tr") }.map do |row|
32
+ row.xpath("./w:tc").map { |cell| cell.xpath(".//w:t").map(&:text).join }
33
+ end
34
+ end
35
+
36
+ it "writes comment rows into the template table" do
37
+ Dir.mktmpdir do |dir|
38
+ output = File.join(dir, "filled.docx")
39
+ described_class.new.fill(template_path, output, [
40
+ comment(id: "US-001", locality: { clause: "5.2.1", element: "Table 3", line_number: "45" },
41
+ proposed_change: "Fix them", observations: "Accepted")
42
+ ])
43
+
44
+ row = body_rows(output).first
45
+ expect(row[0]).to include("US-001")
46
+ expect(row[1]).to include("45")
47
+ expect(row[2]).to include("5.2.1")
48
+ expect(row[3]).to include("Table 3")
49
+ expect(row[4]).to include("technical")
50
+ expect(row[5]).to include("Text")
51
+ expect(row[6]).to include("Fix them")
52
+ expect(row[7]).to include("Accepted")
53
+ end
54
+ end
55
+
56
+ it "keeps multiple comments in input order and accepts plain hashes" do
57
+ Dir.mktmpdir do |dir|
58
+ output = File.join(dir, "filled.docx")
59
+ described_class.new.fill(template_path, output, [
60
+ { "id" => "DE-001", "comments" => "First" },
61
+ comment(id: "US-001", comments: "Second")
62
+ ])
63
+
64
+ rows = body_rows(output)
65
+ expect(rows.length).to eq(2)
66
+ expect(rows[0][0]).to include("DE-001")
67
+ expect(rows[1][0]).to include("US-001")
68
+ end
69
+ end
70
+
71
+ it "fills the header metadata labels in the page header" do
72
+ Dir.mktmpdir do |dir|
73
+ output = File.join(dir, "filled.docx")
74
+ described_class.new.fill(template_path, output, [comment(id: "US-001")],
75
+ date: "2026-08-28", document: "ISO 2533:2026", project: "Ballot review")
76
+
77
+ header = header_xml(output)
78
+ expect(header).to include("Date: 2026-08-28")
79
+ expect(header).to include("Document: ISO 2533:2026")
80
+ expect(header).to include("Project: Ballot review")
81
+ end
82
+ end
83
+
84
+ it "leaves header labels untouched when no metadata is given" do
85
+ Dir.mktmpdir do |dir|
86
+ output = File.join(dir, "filled.docx")
87
+ described_class.new.fill(template_path, output, [comment(id: "US-001")])
88
+
89
+ header = header_xml(output)
90
+ expect(header).to include(">Date: <")
91
+ expect(header).to include(">Document:<")
92
+ expect(header).to include(">Project:<")
93
+ end
94
+ end
95
+
96
+ it "escapes XML special characters in metadata values" do
97
+ Dir.mktmpdir do |dir|
98
+ output = File.join(dir, "filled.docx")
99
+ described_class.new.fill(template_path, output, [comment(id: "US-001")],
100
+ document: "A&B <standard>")
101
+
102
+ expect(header_xml(output)).to include("Document: A&amp;B &lt;standard&gt;")
103
+ end
104
+ end
105
+
106
+ it "shades observation cells according to disposition status" do
107
+ Dir.mktmpdir do |dir|
108
+ output = File.join(dir, "filled.docx")
109
+ described_class.new.fill(template_path, output, [
110
+ comment(id: "US-001", observations: "Accepted"),
111
+ comment(id: "US-002", observations: "Not accepted")
112
+ ], shading: true)
113
+
114
+ xml = body_xml(output)
115
+ expect(xml).to include('w:fill="92D050"')
116
+ expect(xml).to include('w:fill="FF99CC"')
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tempfile"
5
+
6
+ RSpec.describe Commenter::GitHubSync do
7
+ describe ".reconcile_actions" do
8
+ def actions(**overrides)
9
+ described_class.reconcile_actions(
10
+ issue: { title: "Title", body: "Body", state: "open" },
11
+ rendered: { title: "Title", body: "Body" },
12
+ disposition: nil,
13
+ **overrides
14
+ )
15
+ end
16
+
17
+ it "reports nothing when the issue is in sync" do
18
+ expect(actions).to eq([])
19
+ end
20
+
21
+ it "re-renders differing content under the yaml policy" do
22
+ expect(actions(conflict: "yaml", issue: { title: "Title", body: "Old body", state: "open" })).to eq([:update_content])
23
+ end
24
+
25
+ it "keeps differing GitHub content under the github policy" do
26
+ expect(actions(conflict: "github", issue: { title: "Title", body: "Edited on GitHub", state: "open" }))
27
+ .to eq([:keep_github_content])
28
+ end
29
+
30
+ it "reports a conflict under the skip policy" do
31
+ expect(actions(conflict: "skip", issue: { title: "Edited on GitHub", body: "Body", state: "open" })).to eq([:conflict])
32
+ end
33
+
34
+ it "closes an open issue whose comment has a disposition" do
35
+ expect(actions(disposition: "Accepted.")).to eq([:close])
36
+ end
37
+
38
+ it "combines content update and close" do
39
+ expect(actions(issue: { title: "Title", body: "Old", state: "open" }, disposition: "Accepted.")).to eq(%i[update_content close])
40
+ end
41
+
42
+ it "does not close a closed issue, an undispositioned comment, or when disabled" do
43
+ expect(actions(disposition: "Accepted.", issue: { title: "Title", body: "Body", state: "closed" })).to eq([])
44
+ expect(actions(disposition: " ")).to eq([])
45
+ expect(actions(disposition: "Accepted.", close_on_disposition: false)).to eq([])
46
+ end
47
+
48
+ it "ignores whitespace when comparing content" do
49
+ expect(actions(issue: { title: "Title", body: " Body ", state: "open" })).to eq([])
50
+ end
51
+ end
52
+
53
+ describe "#sync dry run" do
54
+ let(:config_file) do
55
+ file = Tempfile.new(["config", ".yaml"])
56
+ file.write({ "github" => { "repository" => "test-org/test-repo", "token" => "test-token" } }.to_yaml)
57
+ file.close
58
+ file
59
+ end
60
+
61
+ def yaml_file(comments)
62
+ file = Tempfile.new(["comments", ".yaml"])
63
+ file.write({ "version" => "2012-03", "stage" => "DIS", "comments" => comments }.to_yaml)
64
+ file.close
65
+ file
66
+ end
67
+
68
+ after { config_file.unlink }
69
+
70
+ it "plans creation for comments without issues and closure for dispositioned open ones" do
71
+ input = yaml_file([
72
+ { "id" => "US-001", "body" => "US", "comments" => "No issue yet" },
73
+ { "id" => "US-002", "body" => "US", "comments" => "Resolved", "observations" => "Accepted.",
74
+ "github" => { "issue_number" => 12, "status" => "open" } }
75
+ ])
76
+
77
+ sync = described_class.new(config_file.path)
78
+ results = sync.sync(input.path, dry_run: true)
79
+
80
+ expect(results[0]).to eq(comment_id: "US-001", actions: [:create])
81
+ expect(results[1][:issue_number]).to eq(12)
82
+ expect(results[1][:actions]).to include(:close)
83
+
84
+ input.unlink
85
+ end
86
+ end
87
+ end
@@ -27,7 +27,8 @@ RSpec.describe Commenter::Parser::OsdXlsxParser do
27
27
 
28
28
  expect(comment.id).to eq("1")
29
29
  expect(comment.body).to eq("John Doe")
30
- expect(comment.locality).to eq({ clause: "5.2.1", element: "Requirements" })
30
+ expect(comment.locality.clause).to eq("5.2.1")
31
+ expect(comment.locality.element).to eq("Requirements")
31
32
  expect(comment.comments).to eq("The values in Table 3 are inconsistent.")
32
33
  expect(comment.proposed_change).to eq("Correct the values in column 2.")
33
34
  expect(comment.comment_type).to eq("Editorial")
@@ -62,7 +63,7 @@ RSpec.describe Commenter::Parser::OsdXlsxParser do
62
63
  comments = described_class.new.parse(path).comments
63
64
 
64
65
  expect(comments.length).to eq(2)
65
- expect(comments.last.locality).to eq({})
66
+ expect(comments.last.locality.clause).to be_nil
66
67
  expect(comments.last.proposed_change).to be_nil
67
68
  end
68
69
  end
@@ -25,10 +25,17 @@ module RedlineDocxBuilder
25
25
  private_constant :HEADING_LEVEL_STYLE, :CHANGE_TAG
26
26
 
27
27
  def self.write(path, paragraphs, comments: [])
28
- zip = Zip::File.new(path, create: true)
29
- zip.get_output_stream("word/document.xml") { |stream| stream.write(document_xml(paragraphs)) }
30
- zip.get_output_stream("word/comments.xml") { |stream| stream.write(comments_xml(comments)) }
31
- zip.close
28
+ # Written via an in-memory buffer and File.binwrite rather than
29
+ # Zip::File#create + close: rubyzip commits the latter with File.rename,
30
+ # which fails with EACCES on Windows when the caller still holds an open
31
+ # handle on the destination (e.g. an unclosed Tempfile).
32
+ buffer = Zip::OutputStream.write_buffer do |out|
33
+ out.put_next_entry("word/document.xml")
34
+ out.write(document_xml(paragraphs))
35
+ out.put_next_entry("word/comments.xml")
36
+ out.write(comments_xml(comments))
37
+ end
38
+ File.binwrite(path, buffer.string)
32
39
  path
33
40
  end
34
41
 
@@ -18,22 +18,27 @@ module XlsxBuilder
18
18
  shared = []
19
19
  worksheets = sheets.map { |name, rows| [name, worksheet_xml(rows, shared)] }
20
20
 
21
- zip = Zip::File.new(path, create: true)
22
- write_entry(zip, "[Content_Types].xml", content_types_xml(worksheets.size))
23
- write_entry(zip, "_rels/.rels", root_rels_xml)
24
- write_entry(zip, "xl/workbook.xml", workbook_xml(worksheets.map(&:first)))
25
- write_entry(zip, "xl/_rels/workbook.xml.rels", workbook_rels_xml(worksheets.size))
26
- write_entry(zip, "xl/sharedStrings.xml", shared_strings_xml(shared))
27
- write_entry(zip, "xl/styles.xml", styles_xml)
28
- worksheets.each_with_index do |(_name, xml), index|
29
- write_entry(zip, "xl/worksheets/sheet#{index + 1}.xml", xml)
21
+ # In-memory buffer + File.binwrite rather than Zip::File#create + close:
22
+ # rubyzip commits the latter with File.rename, which fails with EACCES on
23
+ # Windows when the caller still holds an open handle on the destination.
24
+ buffer = Zip::OutputStream.write_buffer do |out|
25
+ write_entry(out, "[Content_Types].xml", content_types_xml(worksheets.size))
26
+ write_entry(out, "_rels/.rels", root_rels_xml)
27
+ write_entry(out, "xl/workbook.xml", workbook_xml(worksheets.map(&:first)))
28
+ write_entry(out, "xl/_rels/workbook.xml.rels", workbook_rels_xml(worksheets.size))
29
+ write_entry(out, "xl/sharedStrings.xml", shared_strings_xml(shared))
30
+ write_entry(out, "xl/styles.xml", styles_xml)
31
+ worksheets.each_with_index do |(_name, xml), index|
32
+ write_entry(out, "xl/worksheets/sheet#{index + 1}.xml", xml)
33
+ end
30
34
  end
31
- zip.close
35
+ File.binwrite(path, buffer.string)
32
36
  path
33
37
  end
34
38
 
35
- def self.write_entry(zip, name, content)
36
- zip.get_output_stream(name) { |stream| stream.write(content) }
39
+ def self.write_entry(out, name, content)
40
+ out.put_next_entry(name)
41
+ out.write(content)
37
42
  end
38
43
 
39
44
  def self.worksheet_xml(rows, shared)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: commenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-27 00:00:00.000000000 Z
11
+ date: 2026-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: lutaml-model
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: octokit
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -125,6 +139,7 @@ files:
125
139
  - ".rubocop_todo.yml"
126
140
  - CLAUDE.md
127
141
  - CODE_OF_CONDUCT.md
142
+ - CONTEXT.md
128
143
  - Gemfile
129
144
  - README.adoc
130
145
  - Rakefile
@@ -136,13 +151,20 @@ files:
136
151
  - data/iso_comment_template_2012-03.docx
137
152
  - exe/commenter
138
153
  - lib/commenter.rb
154
+ - lib/commenter/ballot.rb
155
+ - lib/commenter/ballot_diff.rb
156
+ - lib/commenter/ballot_report.rb
139
157
  - lib/commenter/cli.rb
140
158
  - lib/commenter/comment.rb
159
+ - lib/commenter/comment_locality.rb
141
160
  - lib/commenter/comment_sheet.rb
142
161
  - lib/commenter/comment_type.rb
162
+ - lib/commenter/disposition_status.rb
143
163
  - lib/commenter/filler.rb
164
+ - lib/commenter/github_info.rb
144
165
  - lib/commenter/github_integration.rb
145
166
  - lib/commenter/github_session.rb
167
+ - lib/commenter/github_sync.rb
146
168
  - lib/commenter/parser.rb
147
169
  - lib/commenter/parser/osd_xlsx_parser.rb
148
170
  - lib/commenter/parser/track_change_docx_parser.rb
@@ -150,11 +172,17 @@ files:
150
172
  - schema/iso_comment_2012-03.yaml
151
173
  - schema/iso_comment_osd.yaml
152
174
  - sig/commenter.rbs
175
+ - spec/commenter/ballot_diff_spec.rb
176
+ - spec/commenter/ballot_report_spec.rb
177
+ - spec/commenter/ballot_spec.rb
153
178
  - spec/commenter/cli_spec.rb
154
179
  - spec/commenter/comment_sheet_spec.rb
155
180
  - spec/commenter/comment_spec.rb
156
181
  - spec/commenter/comment_type_spec.rb
182
+ - spec/commenter/disposition_status_spec.rb
183
+ - spec/commenter/filler_spec.rb
157
184
  - spec/commenter/github_integration_spec.rb
185
+ - spec/commenter/github_sync_spec.rb
158
186
  - spec/commenter/osd_xlsx_parser_spec.rb
159
187
  - spec/commenter/track_change_docx_parser_spec.rb
160
188
  - spec/commenter_spec.rb