commenter 0.2.2 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +7 -102
- data/CLAUDE.md +58 -0
- data/README.adoc +154 -14
- data/commenter.gemspec +3 -2
- data/data/github_config_sample.yaml +8 -0
- data/data/github_issue_body_template.liquid +1 -1
- data/data/github_issue_title_template.liquid +1 -1
- data/lib/commenter/cli.rb +20 -11
- data/lib/commenter/comment.rb +34 -2
- data/lib/commenter/comment_sheet.rb +10 -2
- data/lib/commenter/github_integration.rb +67 -21
- data/lib/commenter/parser/osd_xlsx_parser.rb +252 -0
- data/lib/commenter/parser.rb +95 -37
- data/lib/commenter/version.rb +1 -1
- data/schema/iso_comment_2012-03.yaml +2 -2
- data/schema/iso_comment_osd.yaml +112 -0
- data/spec/commenter/cli_spec.rb +35 -0
- data/spec/commenter/comment_spec.rb +2 -2
- data/spec/commenter/github_integration_spec.rb +3 -3
- data/spec/commenter/osd_xlsx_parser_spec.rb +180 -0
- data/spec/support/osd_fixtures.rb +94 -0
- data/spec/support/xlsx_builder.rb +147 -0
- metadata +26 -5
data/lib/commenter/parser.rb
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "docx"
|
|
4
|
+
require "pathname"
|
|
4
5
|
require_relative "comment_sheet"
|
|
5
6
|
require_relative "comment"
|
|
7
|
+
require_relative "parser/osd_xlsx_parser"
|
|
6
8
|
|
|
7
9
|
module Commenter
|
|
8
10
|
class Parser
|
|
9
|
-
def parse(
|
|
11
|
+
def parse(input_path, options = {})
|
|
12
|
+
format = detect_format(input_path, options)
|
|
13
|
+
|
|
14
|
+
case format
|
|
15
|
+
when :docx
|
|
16
|
+
parse_docx(input_path, options)
|
|
17
|
+
when :xlsx
|
|
18
|
+
parse_xlsx(input_path, options)
|
|
19
|
+
else
|
|
20
|
+
raise "Unsupported file format: #{input_path}. Supported formats: .docx, .xlsx"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def detect_format(path, options = {})
|
|
27
|
+
return options[:format].to_sym if options[:format]
|
|
28
|
+
|
|
29
|
+
ext = File.extname(path).downcase
|
|
30
|
+
case ext
|
|
31
|
+
when ".docx" then :docx
|
|
32
|
+
when ".xlsx", ".xls" then :xlsx
|
|
33
|
+
else :unknown
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_xlsx(xlsx_path, options)
|
|
38
|
+
OsdXlsxParser.new.parse(xlsx_path, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_docx(docx_path, options)
|
|
10
42
|
doc = Docx::Document.open(docx_path)
|
|
11
43
|
|
|
12
44
|
# Extract metadata from the first table
|
|
@@ -17,41 +49,13 @@ module Commenter
|
|
|
17
49
|
raise "No comments table found in document" unless comments_table
|
|
18
50
|
raise "Comments table appears to be empty" if comments_table.row_count < 2
|
|
19
51
|
|
|
20
|
-
comments = []
|
|
21
|
-
|
|
22
52
|
# Process all rows - don't skip any rows, respect all content
|
|
23
|
-
(0..comments_table.row_count - 1).
|
|
24
|
-
|
|
25
|
-
cells = row.cells.map { |c| c.text.strip }
|
|
26
|
-
|
|
27
|
-
# Skip only completely empty rows
|
|
53
|
+
comments = (0..comments_table.row_count - 1).map do |row_index|
|
|
54
|
+
cells = comments_table.rows[row_index].cells.map { |cell| cell.text.strip }
|
|
28
55
|
next if cells.all?(&:empty?)
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
body = id.include?("-") ? id.split("-").first : id
|
|
33
|
-
|
|
34
|
-
# Create comment with symbol keys, respecting all input data
|
|
35
|
-
comment_attrs = {
|
|
36
|
-
id: id,
|
|
37
|
-
body: body,
|
|
38
|
-
locality: {
|
|
39
|
-
line_number: cells[1] && cells[1].empty? ? nil : cells[1],
|
|
40
|
-
clause: cells[2] && cells[2].empty? ? nil : cells[2],
|
|
41
|
-
element: cells[3] && cells[3].empty? ? nil : cells[3]
|
|
42
|
-
},
|
|
43
|
-
type: cells[4] || "",
|
|
44
|
-
comments: cells[5] || "",
|
|
45
|
-
proposed_change: cells[6] || ""
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Handle observations column
|
|
49
|
-
unless options[:exclude_observations]
|
|
50
|
-
comment_attrs[:observations] = cells[7] && cells[7].empty? ? nil : cells[7]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
comments << Comment.new(comment_attrs)
|
|
54
|
-
end
|
|
57
|
+
osd_docx_row?(cells) ? build_osd_docx_comment(cells, options) : build_classic_docx_comment(cells, options)
|
|
58
|
+
end.compact
|
|
55
59
|
|
|
56
60
|
# Create comment sheet
|
|
57
61
|
CommentSheet.new(
|
|
@@ -63,7 +67,64 @@ module Commenter
|
|
|
63
67
|
)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
# The DOCX from ISO OSD has 9 columns:
|
|
71
|
+
# 0: User name, 1: (empty/line), 2: Clause nb, 3: Clause Title,
|
|
72
|
+
# 4: Type, 5: Comment, 6: (empty/proposal), 7: Observations, 8: Comment number
|
|
73
|
+
# Detected by checking if last column looks like a numeric ID.
|
|
74
|
+
def osd_docx_row?(cells)
|
|
75
|
+
cells.length >= 9 && cells[8].to_s.match?(/^\d+$/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_osd_docx_comment(cells, options)
|
|
79
|
+
attrs = {
|
|
80
|
+
id: cells[8],
|
|
81
|
+
body: cells[0].to_s.strip,
|
|
82
|
+
locality: {
|
|
83
|
+
clause: presence(cells[2]),
|
|
84
|
+
element: presence(cells[3])
|
|
85
|
+
},
|
|
86
|
+
type: normalize_type(cells[4]),
|
|
87
|
+
comments: cells[5] || "",
|
|
88
|
+
proposed_change: presence(cells[6]),
|
|
89
|
+
user_name: cells[0].to_s.strip
|
|
90
|
+
}
|
|
91
|
+
attrs[:observations] = presence(cells[7]) unless options[:exclude_observations]
|
|
92
|
+
Comment.new(attrs)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Classic ISO comment template format (8 columns):
|
|
96
|
+
# 0: ID, 1: line_number, 2: clause, 3: element, 4: type, 5: comments,
|
|
97
|
+
# 6: proposed_change, 7: observations
|
|
98
|
+
def build_classic_docx_comment(cells, options)
|
|
99
|
+
id = cells[0] || ""
|
|
100
|
+
attrs = {
|
|
101
|
+
id: id,
|
|
102
|
+
body: id.include?("-") ? id.split("-").first : id,
|
|
103
|
+
locality: {
|
|
104
|
+
line_number: presence(cells[1]),
|
|
105
|
+
clause: presence(cells[2]),
|
|
106
|
+
element: presence(cells[3])
|
|
107
|
+
},
|
|
108
|
+
type: cells[4] || "",
|
|
109
|
+
comments: cells[5] || "",
|
|
110
|
+
proposed_change: cells[6] || ""
|
|
111
|
+
}
|
|
112
|
+
attrs[:observations] = presence(cells[7]) unless options[:exclude_observations]
|
|
113
|
+
Comment.new(attrs)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def presence(value)
|
|
117
|
+
value && !value.empty? ? value : nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def normalize_type(type_str)
|
|
121
|
+
case type_str.to_s.strip.downcase
|
|
122
|
+
when "editorial" then "ed"
|
|
123
|
+
when "technical" then "te"
|
|
124
|
+
when "general" then "ge"
|
|
125
|
+
else type_str.to_s.strip
|
|
126
|
+
end
|
|
127
|
+
end
|
|
67
128
|
|
|
68
129
|
def extract_metadata(doc)
|
|
69
130
|
metadata = { date: nil, document: nil, project: nil }
|
|
@@ -96,9 +157,6 @@ module Commenter
|
|
|
96
157
|
project_match = all_text.match(/Project:\s*([^\n\r]+)/)
|
|
97
158
|
metadata[:project] = project_match[1]&.strip if project_match
|
|
98
159
|
|
|
99
|
-
# If no metadata found, try to extract from filename or other sources
|
|
100
|
-
# This is a fallback - in practice, users might need to provide metadata manually
|
|
101
|
-
|
|
102
160
|
metadata
|
|
103
161
|
end
|
|
104
162
|
end
|
data/lib/commenter/version.rb
CHANGED
|
@@ -68,8 +68,8 @@ properties:
|
|
|
68
68
|
required: ["clause"]
|
|
69
69
|
type:
|
|
70
70
|
type: string
|
|
71
|
-
enum: ["ge", "te", "ed"]
|
|
72
|
-
description: "Type of comment: ge=general, te=technical, ed=editorial"
|
|
71
|
+
enum: ["ge", "te", "ed", "general", "technical", "editorial"]
|
|
72
|
+
description: "Type of comment: ge=general, te=technical, ed=editorial. Codes are expanded to full names in output"
|
|
73
73
|
comments:
|
|
74
74
|
type: string
|
|
75
75
|
description: "The actual comment text"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ISO Comment Schema for ISO Online Standards Development (OSD) format
|
|
2
|
+
# Supports both resolved and unresolved XLSX exports from ISO OSD
|
|
3
|
+
$schema: http://json-schema.org/draft-07/schema#
|
|
4
|
+
title: ISO Comment OSD
|
|
5
|
+
description: Schema for ISO comments exported from ISO Online Standards Development (OSD) platform
|
|
6
|
+
type: object
|
|
7
|
+
properties:
|
|
8
|
+
version:
|
|
9
|
+
type: string
|
|
10
|
+
const: "osd"
|
|
11
|
+
description: Version identifier for OSD format
|
|
12
|
+
date:
|
|
13
|
+
type: ["string", "null"]
|
|
14
|
+
description: Date from the XLSX header
|
|
15
|
+
format: date
|
|
16
|
+
document:
|
|
17
|
+
type: ["string", "null"]
|
|
18
|
+
description: Document reference (e.g. ISO/DIS 5843-6(en))
|
|
19
|
+
project:
|
|
20
|
+
type: ["string", "null"]
|
|
21
|
+
description: Project name
|
|
22
|
+
title_en:
|
|
23
|
+
type: ["string", "null"]
|
|
24
|
+
description: Document title in English
|
|
25
|
+
title_fr:
|
|
26
|
+
type: ["string", "null"]
|
|
27
|
+
description: Document title in French
|
|
28
|
+
stage:
|
|
29
|
+
type: ["string", "null"]
|
|
30
|
+
description: Approval stage (WD/CD/DIS/FDIS/PRF/PUB)
|
|
31
|
+
enum: [null, "WD", "CD", "DIS", "FDIS", "PRF", "PUB"]
|
|
32
|
+
comments:
|
|
33
|
+
type: array
|
|
34
|
+
description: Array of comment entries from the ISO OSD export
|
|
35
|
+
items:
|
|
36
|
+
type: object
|
|
37
|
+
properties:
|
|
38
|
+
id:
|
|
39
|
+
type: ["string", "number"]
|
|
40
|
+
description: "Comment ID (numeric from OSD)"
|
|
41
|
+
body:
|
|
42
|
+
type: ["string", "null"]
|
|
43
|
+
description: "Member body or user name"
|
|
44
|
+
locality:
|
|
45
|
+
type: object
|
|
46
|
+
description: "Location information for the comment"
|
|
47
|
+
properties:
|
|
48
|
+
line_number:
|
|
49
|
+
type: ["string", "null"]
|
|
50
|
+
clause:
|
|
51
|
+
type: ["string", "null"]
|
|
52
|
+
element:
|
|
53
|
+
type: ["string", "null"]
|
|
54
|
+
description: "Clause Title from OSD"
|
|
55
|
+
type:
|
|
56
|
+
type: ["string", "null"]
|
|
57
|
+
description: "Type of comment; codes (ge/te/ed) are expanded to general/technical/editorial in output"
|
|
58
|
+
enum: [null, "ge", "te", "ed", "general", "technical", "editorial"]
|
|
59
|
+
comments:
|
|
60
|
+
type: ["string", "null"]
|
|
61
|
+
description: "The comment text"
|
|
62
|
+
proposed_change:
|
|
63
|
+
type: ["string", "null"]
|
|
64
|
+
description: "Proposed change text"
|
|
65
|
+
observations:
|
|
66
|
+
type: ["string", "null"]
|
|
67
|
+
description: "Resolution status or observations"
|
|
68
|
+
user_name:
|
|
69
|
+
type: ["string", "null"]
|
|
70
|
+
description: "User name from OSD"
|
|
71
|
+
comment_type:
|
|
72
|
+
type: ["string", "null"]
|
|
73
|
+
description: "Comment type/subtype from OSD (e.g. Editorial, General, Technical)"
|
|
74
|
+
resolution_status:
|
|
75
|
+
type: ["string", "null"]
|
|
76
|
+
description: "Resolution status (Accepted, Partially accepted, Rejected, etc.)"
|
|
77
|
+
resolution_date:
|
|
78
|
+
type: ["string", "null"]
|
|
79
|
+
description: "Date when the comment was resolved"
|
|
80
|
+
feedbacks:
|
|
81
|
+
type: ["string", "null"]
|
|
82
|
+
description: "Feedback/replies from the discussion"
|
|
83
|
+
motivation:
|
|
84
|
+
type: ["string", "null"]
|
|
85
|
+
description: "Motivation/justification for resolution"
|
|
86
|
+
created_date:
|
|
87
|
+
type: ["string", "null"]
|
|
88
|
+
description: "Date when the comment was created"
|
|
89
|
+
stage_code:
|
|
90
|
+
type: ["string", "null"]
|
|
91
|
+
description: "ISO stage code (e.g. 40.20)"
|
|
92
|
+
github:
|
|
93
|
+
type: ["object", "null"]
|
|
94
|
+
description: "GitHub integration information"
|
|
95
|
+
properties:
|
|
96
|
+
issue_number:
|
|
97
|
+
type: integer
|
|
98
|
+
issue_url:
|
|
99
|
+
type: string
|
|
100
|
+
format: uri
|
|
101
|
+
status:
|
|
102
|
+
type: string
|
|
103
|
+
enum: ["open", "closed"]
|
|
104
|
+
created_at:
|
|
105
|
+
type: string
|
|
106
|
+
format: date-time
|
|
107
|
+
updated_at:
|
|
108
|
+
type: string
|
|
109
|
+
format: date-time
|
|
110
|
+
required: ["issue_number", "issue_url", "status"]
|
|
111
|
+
required: ["id", "locality", "comments"]
|
|
112
|
+
required: ["version", "comments"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "commenter/cli"
|
|
5
|
+
require_relative "../support/osd_fixtures"
|
|
6
|
+
require_relative "../support/xlsx_builder"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Commenter::Cli do
|
|
9
|
+
include OsdFixtures
|
|
10
|
+
|
|
11
|
+
describe "#import" do
|
|
12
|
+
it "writes OSD YAML and copies the OSD schema for XLSX input" do
|
|
13
|
+
Dir.mktmpdir do |dir|
|
|
14
|
+
xlsx = File.join(dir, "comments.xlsx")
|
|
15
|
+
rows = header_block(OsdFixtures::RESOLVED_HEADERS) + [
|
|
16
|
+
[1, "John Doe", "5.2.1", "Requirements", "Comment", "Editorial",
|
|
17
|
+
"The values in Table 3 are inconsistent.", nil, "Correct the values.", nil, nil, nil,
|
|
18
|
+
"2026-04-01", "Accepted", nil, "2026-04-10", "40.20"]
|
|
19
|
+
]
|
|
20
|
+
XlsxBuilder.write(xlsx, [["Comments (1)", rows]])
|
|
21
|
+
output = File.join(dir, "out", "comments.yaml")
|
|
22
|
+
schema_dir = File.join(dir, "out", "schema")
|
|
23
|
+
|
|
24
|
+
described_class.start(["import", xlsx, "--output", output, "--schema-dir", schema_dir])
|
|
25
|
+
|
|
26
|
+
data = YAML.safe_load_file(output)
|
|
27
|
+
expect(data["version"]).to eq("osd")
|
|
28
|
+
expect(data["comments"].length).to eq(1)
|
|
29
|
+
expect(data["comments"].first["id"]).to eq("1")
|
|
30
|
+
expect(File.read(output)).to start_with("# yaml-language-server: $schema=")
|
|
31
|
+
expect(File).to exist(File.join(schema_dir, "iso_comment_osd.yaml"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -22,7 +22,7 @@ RSpec.describe Commenter::Comment do
|
|
|
22
22
|
expect(comment.clause).to eq("5.1")
|
|
23
23
|
expect(comment.element).to eq("Table 1")
|
|
24
24
|
expect(comment.line_number).to eq("42")
|
|
25
|
-
expect(comment.type).to eq("
|
|
25
|
+
expect(comment.type).to eq("technical")
|
|
26
26
|
expect(comment.comments).to eq("Test comment")
|
|
27
27
|
expect(comment.proposed_change).to eq("Test change")
|
|
28
28
|
expect(comment.observations).to eq("Test observations")
|
|
@@ -173,7 +173,7 @@ RSpec.describe Commenter::Comment do
|
|
|
173
173
|
expect(hash[:id]).to eq("US-001")
|
|
174
174
|
expect(hash[:body]).to eq("US")
|
|
175
175
|
expect(hash[:locality][:clause]).to eq("5.1")
|
|
176
|
-
expect(hash[:type]).to eq("
|
|
176
|
+
expect(hash[:type]).to eq("technical")
|
|
177
177
|
expect(hash[:comments]).to eq("Test")
|
|
178
178
|
expect(hash[:proposed_change]).to eq("Change")
|
|
179
179
|
expect(hash[:observations]).to eq("Obs")
|
|
@@ -120,7 +120,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
120
120
|
expect(result[:title]).to eq("US-001: Clause 5.1, Table 1: Test comment text")
|
|
121
121
|
expect(result[:body]).to include("Comment: Test comment text")
|
|
122
122
|
expect(result[:body]).to include("Type: Technical")
|
|
123
|
-
expect(result[:labels]).to include("comment-review", "draft-international-standard", "
|
|
123
|
+
expect(result[:labels]).to include("comment-review", "draft-international-standard", "technical")
|
|
124
124
|
expect(result[:assignees]).to eq(["test-assignee"])
|
|
125
125
|
end
|
|
126
126
|
end
|
|
@@ -144,7 +144,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
144
144
|
expect(variables["stage"]).to eq("DIS")
|
|
145
145
|
expect(variables["document"]).to eq("Test Document")
|
|
146
146
|
expect(variables["comment_id"]).to eq("US-001")
|
|
147
|
-
expect(variables["type"]).to eq("
|
|
147
|
+
expect(variables["type"]).to eq("technical")
|
|
148
148
|
expect(variables["type_full_name"]).to eq("Technical")
|
|
149
149
|
expect(variables["clause"]).to eq("5.1")
|
|
150
150
|
expect(variables["element"]).to eq("Table 1")
|
|
@@ -164,7 +164,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
164
164
|
|
|
165
165
|
expect(labels).to include("comment-review") # default
|
|
166
166
|
expect(labels).to include("draft-international-standard") # stage-specific
|
|
167
|
-
expect(labels).to include("
|
|
167
|
+
expect(labels).to include("technical") # comment type (expanded)
|
|
168
168
|
expect(labels.uniq).to eq(labels) # no duplicates
|
|
169
169
|
end
|
|
170
170
|
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "../support/osd_fixtures"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Commenter::Parser::OsdXlsxParser do
|
|
7
|
+
include OsdFixtures
|
|
8
|
+
|
|
9
|
+
describe "resolved variant" do
|
|
10
|
+
it "extracts sheet metadata from the header rows" do
|
|
11
|
+
with_resolved_xlsx do |path|
|
|
12
|
+
sheet = described_class.new.parse(path)
|
|
13
|
+
|
|
14
|
+
expect(sheet.version).to eq("osd")
|
|
15
|
+
expect(sheet.date).to eq("2026-04-21")
|
|
16
|
+
expect(sheet.document).to eq("ISO/DIS 5843-6(en)")
|
|
17
|
+
expect(sheet.stage).to eq("DIS")
|
|
18
|
+
expect(sheet.project).to eq("ISO/DIS 5843-6")
|
|
19
|
+
expect(sheet.title_en).to eq("Aerodromes and heliports")
|
|
20
|
+
expect(sheet.title_fr).to eq("Aerodromes and heliports (FR)")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "maps columns to comment attributes" do
|
|
25
|
+
with_resolved_xlsx do |path|
|
|
26
|
+
comment = described_class.new.parse(path).comments.first
|
|
27
|
+
|
|
28
|
+
expect(comment.id).to eq("1")
|
|
29
|
+
expect(comment.body).to eq("John Doe")
|
|
30
|
+
expect(comment.locality).to eq({ clause: "5.2.1", element: "Requirements" })
|
|
31
|
+
expect(comment.comments).to eq("The values in Table 3 are inconsistent.")
|
|
32
|
+
expect(comment.proposed_change).to eq("Correct the values in column 2.")
|
|
33
|
+
expect(comment.comment_type).to eq("Editorial")
|
|
34
|
+
expect(comment.resolution_status).to eq("Accepted")
|
|
35
|
+
expect(comment.resolution_date).to eq("2026-04-10")
|
|
36
|
+
expect(comment.feedbacks).to eq("Feedback from the working group")
|
|
37
|
+
expect(comment.motivation).to eq("Reviewed and approved by the working group.")
|
|
38
|
+
expect(comment.created_date).to eq("2026-04-01")
|
|
39
|
+
expect(comment.stage_code).to eq("40.20")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "builds observations from resolution status and motivation" do
|
|
44
|
+
with_resolved_xlsx do |path|
|
|
45
|
+
comments = described_class.new.parse(path).comments
|
|
46
|
+
|
|
47
|
+
expect(comments.first.observations).to eq("Accepted. Reviewed and approved by the working group.")
|
|
48
|
+
expect(comments.last.observations).to eq("Rejected")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "normalizes numeric comment ids to strings" do
|
|
53
|
+
with_resolved_xlsx do |path|
|
|
54
|
+
comments = described_class.new.parse(path).comments
|
|
55
|
+
|
|
56
|
+
expect(comments.map(&:id)).to eq(%w[1 2])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "skips empty rows and keeps comments without clause data" do
|
|
61
|
+
with_resolved_xlsx do |path|
|
|
62
|
+
comments = described_class.new.parse(path).comments
|
|
63
|
+
|
|
64
|
+
expect(comments.length).to eq(2)
|
|
65
|
+
expect(comments.last.locality).to eq({})
|
|
66
|
+
expect(comments.last.proposed_change).to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "falls back to proposal-on-text when proposed change is blank" do
|
|
71
|
+
rows = resolved_rows
|
|
72
|
+
rows[4][8] = nil # Proposed change
|
|
73
|
+
with_resolved_xlsx(rows) do |path|
|
|
74
|
+
sheet = described_class.new.parse(path)
|
|
75
|
+
|
|
76
|
+
expect(sheet.comments.first.proposed_change).to eq("Proposal on the text")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "excludes observations when exclude_observations is set" do
|
|
81
|
+
with_resolved_xlsx do |path|
|
|
82
|
+
sheet = described_class.new.parse(path, exclude_observations: true)
|
|
83
|
+
|
|
84
|
+
expect(sheet.comments.first.observations).to be_nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "maps columns by real position when the header row has gaps" do
|
|
89
|
+
with_resolved_xlsx(resolved_rows(gap: true)) do |path|
|
|
90
|
+
comment = described_class.new.parse(path).comments.first
|
|
91
|
+
|
|
92
|
+
expect(comment.id).to eq("1")
|
|
93
|
+
expect(comment.type).to eq("editorial")
|
|
94
|
+
expect(comment.comments).to eq("The values in Table 3 are inconsistent.")
|
|
95
|
+
expect(comment.resolution_status).to eq("Accepted")
|
|
96
|
+
expect(comment.stage_code).to eq("40.20")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
RSpec.describe Commenter::Parser::OsdXlsxParser do
|
|
103
|
+
include OsdFixtures
|
|
104
|
+
|
|
105
|
+
describe "unresolved variant" do
|
|
106
|
+
it "parses the first sheet by default" do
|
|
107
|
+
with_unresolved_workbook do |path|
|
|
108
|
+
sheet = described_class.new.parse(path)
|
|
109
|
+
|
|
110
|
+
expect(sheet.comments.map(&:id)).to eq(%w[7 8])
|
|
111
|
+
expect(sheet.comments.first.body).to eq("John Doe")
|
|
112
|
+
expect(sheet.comments.first.type).to eq("editorial")
|
|
113
|
+
expect(sheet.comments.first.feedbacks).to eq("Reply from secretariat")
|
|
114
|
+
expect(sheet.comments.first.motivation).to eq("Justification text")
|
|
115
|
+
expect(sheet.comments.first.created_date).to eq("2026-04-05")
|
|
116
|
+
expect(sheet.comments.last.observations).to be_nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "uses the unresolved sheet when unresolved_only is set" do
|
|
121
|
+
with_unresolved_workbook do |path|
|
|
122
|
+
sheet = described_class.new.parse(path, unresolved_only: true)
|
|
123
|
+
|
|
124
|
+
expect(sheet.comments.map(&:id)).to eq(["9"])
|
|
125
|
+
expect(sheet.comments.first.type).to eq("general")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "uses the resolved sheet when resolved_only is set" do
|
|
130
|
+
with_unresolved_workbook do |path|
|
|
131
|
+
sheet = described_class.new.parse(path, resolved_only: true)
|
|
132
|
+
|
|
133
|
+
expect(sheet.comments.map(&:id)).to eq(["10"])
|
|
134
|
+
expect(sheet.comments.first.observations).to eq("Partially accepted. Compromise reached.")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "parses the sheet given by name" do
|
|
139
|
+
with_unresolved_workbook do |path|
|
|
140
|
+
sheet = described_class.new.parse(path, sheet: "Resolved comments (1)")
|
|
141
|
+
|
|
142
|
+
expect(sheet.comments.map(&:id)).to eq(["10"])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "raises when the requested sheet does not exist" do
|
|
147
|
+
with_unresolved_workbook do |path|
|
|
148
|
+
expect { described_class.new.parse(path, sheet: "Nope") }
|
|
149
|
+
.to raise_error(/Sheet 'Nope' not found/)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
RSpec.describe Commenter::Parser::OsdXlsxParser do
|
|
156
|
+
include OsdFixtures
|
|
157
|
+
|
|
158
|
+
describe "round-trip stability" do
|
|
159
|
+
it "produces identical YAML when reloaded from its own output" do
|
|
160
|
+
with_resolved_xlsx do |path|
|
|
161
|
+
sheet = described_class.new.parse(path)
|
|
162
|
+
reparsed = Commenter::CommentSheet.from_hash(sheet.to_yaml_h)
|
|
163
|
+
|
|
164
|
+
expect(reparsed.to_yaml_h).to eq(sheet.to_yaml_h)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "omits nil titles from YAML output" do
|
|
169
|
+
rows = resolved_rows
|
|
170
|
+
rows[1] = ["2026-04-21", "ISO/CD 12345(en)"]
|
|
171
|
+
with_resolved_xlsx(rows) do |path|
|
|
172
|
+
sheet = described_class.new.parse(path)
|
|
173
|
+
|
|
174
|
+
expect(sheet.stage).to eq("CD")
|
|
175
|
+
expect(sheet.to_yaml_h).not_to have_key("title_en")
|
|
176
|
+
expect(sheet.to_yaml_h).not_to have_key("title_fr")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "xlsx_builder"
|
|
5
|
+
|
|
6
|
+
# Fixture data and helpers mirroring ISO OSD XLSX exports for specs.
|
|
7
|
+
module OsdFixtures
|
|
8
|
+
RESOLVED_HEADERS = [
|
|
9
|
+
"Comment ID", "User name", "Clause nb", "Clause Title", "Type", "Subtype",
|
|
10
|
+
"Comment", "Proposal on Text", "Proposed change", "Feedbacks", "Topic",
|
|
11
|
+
"Tags", "Created Date", "Resolution status", "Motivation", "Resolution Date", "Stage code"
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
UNRESOLVED_HEADERS = [
|
|
15
|
+
"User name", "Clause nb", "Clause Title", "Type", "Comment type",
|
|
16
|
+
"Comment/Motivation", "Comment on text", "Proposal on Text", "Proposed change",
|
|
17
|
+
"Replies", "Resolution status", "Justification", "Resolution Date", "Date", "Comment number"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
METADATA_ROWS = [
|
|
21
|
+
["Date", "Reference", nil, nil, "Title EN", nil, nil, "Title FR"],
|
|
22
|
+
["2026-04-21", "ISO/DIS 5843-6(en)", nil, nil, "Aerodromes and heliports",
|
|
23
|
+
nil, nil, "Aerodromes and heliports (FR)"]
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
RESOLVED_DATA_ROWS = [
|
|
27
|
+
[1, "John Doe", "5.2.1", "Requirements", "Comment", "Editorial",
|
|
28
|
+
"The values in Table 3 are inconsistent.", "Proposal on the text",
|
|
29
|
+
"Correct the values in column 2.", "Feedback from the working group",
|
|
30
|
+
"Topic A", "tag1", "2026-04-01", "Accepted",
|
|
31
|
+
"Reviewed and approved by the working group.", "2026-04-10", "40.20"],
|
|
32
|
+
[],
|
|
33
|
+
[2, "Jane Roe", nil, nil, "Comment", "Technical", "General remark on the introduction.",
|
|
34
|
+
nil, nil, nil, nil, nil, "2026-04-02", "Rejected", nil, nil, "40.20"]
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
def resolved_rows(gap: false)
|
|
38
|
+
headers = RESOLVED_HEADERS.dup
|
|
39
|
+
data_rows = RESOLVED_DATA_ROWS.map(&:dup)
|
|
40
|
+
if gap
|
|
41
|
+
headers.insert(5, nil)
|
|
42
|
+
data_rows = data_rows.map { |row| row.empty? ? row : row.insert(5, nil) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
METADATA_ROWS + [[]] + [headers] + data_rows
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def unresolved_workbook_sheets
|
|
49
|
+
[
|
|
50
|
+
["Comments (3)",
|
|
51
|
+
header_block(UNRESOLVED_HEADERS) + [
|
|
52
|
+
["John Doe", "5.2.1", "Requirements", "Comment", "Editorial",
|
|
53
|
+
"The scope is too narrow.", "On text remark", "Proposal on the text",
|
|
54
|
+
"Widen the scope to include heliports.", "Reply from secretariat",
|
|
55
|
+
nil, "Justification text", nil, "2026-04-05", 7],
|
|
56
|
+
["Jane Roe", "3.1", "Terms", "Comment", "Technical", "Define the term consistently.",
|
|
57
|
+
nil, nil, "Add a definition.", nil, nil, nil, nil, "2026-04-06", 8]
|
|
58
|
+
]],
|
|
59
|
+
["Unresolved comments (1)",
|
|
60
|
+
header_block(UNRESOLVED_HEADERS) + [
|
|
61
|
+
["Jack Black", "4.2", "Design", "Comment", "General", "Improve the design section.",
|
|
62
|
+
nil, nil, nil, nil, nil, nil, nil, "2026-04-07", 9]
|
|
63
|
+
]],
|
|
64
|
+
["Resolved comments (1)",
|
|
65
|
+
header_block(RESOLVED_HEADERS) + [
|
|
66
|
+
[10, "Jill Hill", "6.1", "Testing", "Comment", "Technical", "Add test requirements.",
|
|
67
|
+
nil, nil, nil, nil, nil, "2026-04-08", "Partially accepted",
|
|
68
|
+
"Compromise reached.", "2026-04-12", "40.92"]
|
|
69
|
+
]]
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def with_resolved_xlsx(rows = nil)
|
|
74
|
+
Dir.mktmpdir do |dir|
|
|
75
|
+
path = File.join(dir, "comments.xlsx")
|
|
76
|
+
XlsxBuilder.write(path, [["Comments (2)", rows || resolved_rows]])
|
|
77
|
+
yield path
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def with_unresolved_workbook
|
|
82
|
+
Dir.mktmpdir do |dir|
|
|
83
|
+
path = File.join(dir, "workbook.xlsx")
|
|
84
|
+
XlsxBuilder.write(path, unresolved_workbook_sheets)
|
|
85
|
+
yield path
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def header_block(headers)
|
|
92
|
+
METADATA_ROWS + [[]] + [headers]
|
|
93
|
+
end
|
|
94
|
+
end
|