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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/CONTEXT.md +33 -0
- data/README.adoc +100 -11
- data/commenter.gemspec +1 -0
- data/lib/commenter/ballot.rb +67 -0
- data/lib/commenter/ballot_diff.rb +64 -0
- data/lib/commenter/ballot_report.rb +60 -0
- data/lib/commenter/cli.rb +139 -31
- data/lib/commenter/comment.rb +100 -115
- data/lib/commenter/comment_locality.rb +18 -0
- data/lib/commenter/comment_sheet.rb +28 -68
- data/lib/commenter/comment_type.rb +5 -1
- data/lib/commenter/disposition_status.rb +37 -0
- data/lib/commenter/filler.rb +91 -59
- data/lib/commenter/github_info.rb +23 -0
- data/lib/commenter/github_integration.rb +14 -14
- data/lib/commenter/github_sync.rb +105 -0
- data/lib/commenter/parser/osd_xlsx_parser.rb +1 -3
- data/lib/commenter/parser/track_change_docx_parser.rb +3 -5
- data/lib/commenter/parser.rb +4 -5
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +13 -5
- data/spec/commenter/ballot_diff_spec.rb +61 -0
- data/spec/commenter/ballot_report_spec.rb +62 -0
- data/spec/commenter/ballot_spec.rb +68 -0
- data/spec/commenter/cli_spec.rb +41 -0
- data/spec/commenter/comment_sheet_spec.rb +10 -10
- data/spec/commenter/comment_spec.rb +22 -9
- data/spec/commenter/disposition_status_spec.rb +40 -0
- data/spec/commenter/filler_spec.rb +119 -0
- data/spec/commenter/github_sync_spec.rb +87 -0
- data/spec/commenter/osd_xlsx_parser_spec.rb +3 -2
- data/spec/support/redline_docx_builder.rb +11 -4
- data/spec/support/xlsx_builder.rb +17 -12
- metadata +30 -2
|
@@ -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&B <standard>")
|
|
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(
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
write_entry(
|
|
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
|
-
|
|
35
|
+
File.binwrite(path, buffer.string)
|
|
32
36
|
path
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
def self.write_entry(
|
|
36
|
-
|
|
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.
|
|
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-
|
|
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
|