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
data/lib/commenter/comment.rb
CHANGED
|
@@ -1,172 +1,157 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
3
5
|
module Commenter
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
6
|
+
# A single comment on a reviewed document. (De)serialization is declared
|
|
7
|
+
# with lutaml-model; the only custom mapping logic is the type expansion
|
|
8
|
+
# (short codes to full names) and empty-observation omission, both at the
|
|
9
|
+
# YAML boundary.
|
|
10
|
+
class Comment < Lutaml::Model::Serializable
|
|
11
|
+
attribute :id, :string
|
|
12
|
+
attribute :body, :string
|
|
13
|
+
attribute :locality, CommentLocality
|
|
14
|
+
attribute :type, :string
|
|
15
|
+
attribute :comments, :string
|
|
16
|
+
attribute :proposed_change, :string
|
|
17
|
+
attribute :observations, :string
|
|
18
|
+
|
|
19
|
+
# OSD-specific fields
|
|
20
|
+
attribute :user_name, :string
|
|
21
|
+
attribute :comment_type, :string
|
|
22
|
+
attribute :resolution_status, :string
|
|
23
|
+
attribute :resolution_date, :string
|
|
24
|
+
attribute :feedbacks, :string
|
|
25
|
+
attribute :motivation, :string
|
|
26
|
+
attribute :created_date, :string
|
|
27
|
+
attribute :stage_code, :string
|
|
28
|
+
|
|
29
|
+
attribute :github, GithubInfo
|
|
30
|
+
|
|
31
|
+
yaml do
|
|
32
|
+
map "id", to: :id
|
|
33
|
+
map "body", to: :body
|
|
34
|
+
map "locality", to: :locality
|
|
35
|
+
map "type", with: { to: :type_to_yaml, from: :type_from_yaml }
|
|
36
|
+
map "comments", to: :comments
|
|
37
|
+
map "proposed_change", to: :proposed_change
|
|
38
|
+
map "observations", with: { to: :observations_to_yaml, from: :observations_from_yaml }
|
|
39
|
+
map "user_name", to: :user_name
|
|
40
|
+
map "comment_type", to: :comment_type
|
|
41
|
+
map "resolution_status", to: :resolution_status
|
|
42
|
+
map "resolution_date", to: :resolution_date
|
|
43
|
+
map "feedbacks", to: :feedbacks
|
|
44
|
+
map "motivation", to: :motivation
|
|
45
|
+
map "created_date", to: :created_date
|
|
46
|
+
map "stage_code", to: :stage_code
|
|
47
|
+
map "github", to: :github
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def type_from_yaml(model, value)
|
|
51
|
+
model.type = CommentType.full_name(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def type_to_yaml(model, doc)
|
|
55
|
+
doc["type"] = model.type if model.type && !model.type.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def observations_from_yaml(model, value)
|
|
59
|
+
model.observations = value.to_s.empty? ? nil : value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def observations_to_yaml(model, doc)
|
|
63
|
+
doc["observations"] = model.observations if model.observations && !model.observations.empty?
|
|
31
64
|
end
|
|
32
65
|
|
|
33
66
|
def expand_comment_type(type)
|
|
34
67
|
CommentType.full_name(type)
|
|
35
68
|
end
|
|
36
69
|
|
|
70
|
+
def to_h
|
|
71
|
+
Comment.to_hash(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias to_yaml_h to_h
|
|
75
|
+
|
|
37
76
|
def line_number
|
|
38
|
-
|
|
77
|
+
locality&.line_number
|
|
39
78
|
end
|
|
40
79
|
|
|
41
80
|
def line_number=(value)
|
|
42
|
-
|
|
81
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
82
|
+
locality.line_number = value
|
|
43
83
|
end
|
|
44
84
|
|
|
45
85
|
def clause
|
|
46
|
-
|
|
86
|
+
locality&.clause
|
|
47
87
|
end
|
|
48
88
|
|
|
49
89
|
def clause=(value)
|
|
50
|
-
|
|
90
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
91
|
+
locality.clause = value
|
|
51
92
|
end
|
|
52
93
|
|
|
53
94
|
def element
|
|
54
|
-
|
|
95
|
+
locality&.element
|
|
55
96
|
end
|
|
56
97
|
|
|
57
98
|
def element=(value)
|
|
58
|
-
|
|
99
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
100
|
+
locality.element = value
|
|
59
101
|
end
|
|
60
102
|
|
|
61
|
-
def brief_summary(max_length =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Add locality information first
|
|
65
|
-
parts << "Clause #{clause}" if clause && !clause.strip.empty?
|
|
66
|
-
parts << element if element && !element.strip.empty?
|
|
67
|
-
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
103
|
+
def brief_summary(max_length = 60)
|
|
104
|
+
description = comments.to_s.split(/[.!?\n]/).map(&:strip).reject(&:empty?).first.to_s
|
|
105
|
+
locality_text = format_locality
|
|
68
106
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Add description from comment text
|
|
72
|
-
if @comments && !@comments.strip.empty?
|
|
73
|
-
# Extract first sentence or truncate
|
|
74
|
-
clean_text = @comments.strip.gsub(/\s+/, " ")
|
|
75
|
-
first_sentence = clean_text.split(/[.!?]/).first&.strip
|
|
76
|
-
description = if first_sentence && first_sentence.length < max_length
|
|
77
|
-
first_sentence
|
|
78
|
-
else
|
|
79
|
-
clean_text[0...50]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if locality_text.empty?
|
|
83
|
-
description
|
|
84
|
-
else
|
|
85
|
-
# Combine locality + description, respecting max_length
|
|
86
|
-
combined = "#{locality_text}: #{description}"
|
|
87
|
-
combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
|
|
88
|
-
end
|
|
89
|
-
else
|
|
107
|
+
if description.empty?
|
|
90
108
|
locality_text.empty? ? "No description" : locality_text
|
|
109
|
+
elsif locality_text.empty?
|
|
110
|
+
combined = description
|
|
111
|
+
combined.length <= max_length ? combined : "#{combined[0...(max_length - 3)]}..."
|
|
112
|
+
else
|
|
113
|
+
combined = "#{locality_text}: #{description}"
|
|
114
|
+
combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
|
|
91
115
|
end
|
|
92
116
|
end
|
|
93
117
|
|
|
94
118
|
def github_issue_number
|
|
95
|
-
|
|
119
|
+
github&.issue_number
|
|
96
120
|
end
|
|
97
121
|
|
|
98
122
|
def github_issue_url
|
|
99
|
-
|
|
123
|
+
github&.issue_url
|
|
100
124
|
end
|
|
101
125
|
|
|
102
126
|
def github_status
|
|
103
|
-
|
|
127
|
+
github&.status
|
|
104
128
|
end
|
|
105
129
|
|
|
106
130
|
def github_created_at
|
|
107
|
-
|
|
131
|
+
github&.created_at
|
|
108
132
|
end
|
|
109
133
|
|
|
110
134
|
def github_updated_at
|
|
111
|
-
|
|
135
|
+
github&.updated_at
|
|
112
136
|
end
|
|
113
137
|
|
|
114
138
|
def has_github_issue?
|
|
115
|
-
|
|
139
|
+
!github&.issue_number.nil?
|
|
116
140
|
end
|
|
117
141
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
body: @body,
|
|
122
|
-
locality: @locality,
|
|
123
|
-
type: @type,
|
|
124
|
-
comments: @comments,
|
|
125
|
-
proposed_change: @proposed_change,
|
|
126
|
-
observations: @observations,
|
|
127
|
-
user_name: @user_name,
|
|
128
|
-
comment_type: @comment_type,
|
|
129
|
-
resolution_status: @resolution_status,
|
|
130
|
-
resolution_date: @resolution_date,
|
|
131
|
-
feedbacks: @feedbacks,
|
|
132
|
-
motivation: @motivation,
|
|
133
|
-
created_date: @created_date,
|
|
134
|
-
stage_code: @stage_code,
|
|
135
|
-
github: @github.empty? ? nil : @github
|
|
136
|
-
}.compact
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def to_yaml_h
|
|
140
|
-
hash = to_h
|
|
141
|
-
# Remove observations if it's nil or empty
|
|
142
|
-
hash.delete(:observations) if hash[:observations].nil? || hash[:observations] == ""
|
|
143
|
-
stringify_keys(hash)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def self.from_hash(hash)
|
|
147
|
-
new(hash)
|
|
142
|
+
def record_github_issue(issue_number:, issue_url:, status:, created_at: nil)
|
|
143
|
+
self.github = GithubInfo.new(issue_number: issue_number, issue_url: issue_url,
|
|
144
|
+
status: status, created_at: created_at)
|
|
148
145
|
end
|
|
149
146
|
|
|
150
147
|
private
|
|
151
148
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
result[new_key] = new_value
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def stringify_keys(hash)
|
|
163
|
-
return hash unless hash.is_a?(Hash)
|
|
164
|
-
|
|
165
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
166
|
-
new_key = key.to_s
|
|
167
|
-
new_value = value.is_a?(Hash) ? stringify_keys(value) : value
|
|
168
|
-
result[new_key] = new_value
|
|
169
|
-
end
|
|
149
|
+
def format_locality
|
|
150
|
+
parts = []
|
|
151
|
+
parts << "Clause #{clause}" if clause && !clause.strip.empty?
|
|
152
|
+
parts << element if element && !element.strip.empty?
|
|
153
|
+
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
154
|
+
parts.join(", ")
|
|
170
155
|
end
|
|
171
156
|
end
|
|
172
157
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Commenter
|
|
6
|
+
# Location of a comment within the reviewed document.
|
|
7
|
+
class CommentLocality < Lutaml::Model::Serializable
|
|
8
|
+
attribute :line_number, :string
|
|
9
|
+
attribute :clause, :string
|
|
10
|
+
attribute :element, :string
|
|
11
|
+
|
|
12
|
+
yaml do
|
|
13
|
+
map "line_number", to: :line_number
|
|
14
|
+
map "clause", to: :clause
|
|
15
|
+
map "element", to: :element
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "lutaml/model"
|
|
4
4
|
|
|
5
5
|
module Commenter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
# One ballot submission: metadata plus its comments. Serialization is
|
|
7
|
+
# declared with lutaml-model.
|
|
8
|
+
class CommentSheet < Lutaml::Model::Serializable
|
|
9
|
+
attribute :version, :string, default: -> { "2012-03" }
|
|
10
|
+
attribute :date, :string
|
|
11
|
+
attribute :document, :string
|
|
12
|
+
attribute :project, :string
|
|
13
|
+
attribute :stage, :string
|
|
14
|
+
attribute :title_en, :string
|
|
15
|
+
attribute :title_fr, :string
|
|
16
|
+
attribute :comments, Comment, collection: true
|
|
17
|
+
|
|
18
|
+
yaml do
|
|
19
|
+
map "version", to: :version
|
|
20
|
+
map "date", to: :date
|
|
21
|
+
map "document", to: :document
|
|
22
|
+
map "project", to: :project
|
|
23
|
+
map "stage", to: :stage
|
|
24
|
+
map "title_en", to: :title_en
|
|
25
|
+
map "title_fr", to: :title_fr
|
|
26
|
+
map "comments", to: :comments
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def add_comment(comment)
|
|
24
|
-
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def to_h
|
|
28
|
-
{
|
|
29
|
-
version: @version,
|
|
30
|
-
date: @date,
|
|
31
|
-
document: @document,
|
|
32
|
-
project: @project,
|
|
33
|
-
stage: @stage,
|
|
34
|
-
title_en: @title_en,
|
|
35
|
-
title_fr: @title_fr,
|
|
36
|
-
comments: @comments.map(&:to_h)
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def to_yaml_h
|
|
41
|
-
hash = to_h.merge(comments: @comments.map(&:to_yaml_h))
|
|
42
|
-
# Remove nil-valued keys for cleaner YAML output
|
|
43
|
-
hash.delete(:title_en) if hash[:title_en].nil?
|
|
44
|
-
hash.delete(:title_fr) if hash[:title_fr].nil?
|
|
45
|
-
stringify_keys(hash)
|
|
30
|
+
self.comments = (comments || []) + [comment.is_a?(Comment) ? comment : Comment.from_hash(comment)]
|
|
46
31
|
end
|
|
47
32
|
|
|
48
33
|
def schema_name
|
|
@@ -50,40 +35,15 @@ module Commenter
|
|
|
50
35
|
end
|
|
51
36
|
|
|
52
37
|
def to_yaml_document(schema_dir = "schema")
|
|
53
|
-
"# yaml-language-server: $schema=#{File.join(schema_dir.to_s, schema_name)}\n\n#{
|
|
38
|
+
"# yaml-language-server: $schema=#{File.join(schema_dir.to_s, schema_name)}\n\n#{to_yaml}"
|
|
54
39
|
end
|
|
55
40
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def symbolize_keys(hash)
|
|
63
|
-
return hash unless hash.is_a?(Hash)
|
|
64
|
-
|
|
65
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
66
|
-
new_key = key.to_sym
|
|
67
|
-
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
|
68
|
-
result[new_key] = new_value
|
|
69
|
-
end
|
|
41
|
+
def to_yaml_h
|
|
42
|
+
CommentSheet.to_hash(self)
|
|
70
43
|
end
|
|
71
44
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
76
|
-
new_key = key.to_s
|
|
77
|
-
new_value = case value
|
|
78
|
-
when Hash
|
|
79
|
-
stringify_keys(value)
|
|
80
|
-
when Array
|
|
81
|
-
value.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
|
|
82
|
-
else
|
|
83
|
-
value
|
|
84
|
-
end
|
|
85
|
-
result[new_key] = new_value
|
|
86
|
-
end
|
|
45
|
+
def to_h
|
|
46
|
+
CommentSheet.to_hash(self)
|
|
87
47
|
end
|
|
88
48
|
end
|
|
89
49
|
end
|
|
@@ -25,7 +25,11 @@ module Commenter
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def full_name(type)
|
|
28
|
-
|
|
28
|
+
return nil if type.nil?
|
|
29
|
+
|
|
30
|
+
value = type.to_s.strip
|
|
31
|
+
downcased = value.downcase
|
|
32
|
+
FULL_NAMES[downcased] || (FULL_NAMES.values.include?(downcased) ? downcased : value)
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def display_name(type)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Canonical disposition vocabulary, shared by cell shading (Filler) and
|
|
5
|
+
# ballot statistics (BallotReport). Matches free-text observations and
|
|
6
|
+
# OSD's structured resolution_status values. One home for the question
|
|
7
|
+
# "what counts as an accepted disposition".
|
|
8
|
+
module DispositionStatus
|
|
9
|
+
PATTERNS = [
|
|
10
|
+
[:accept_with_modifications, /awm|accept(ed)?[ ,]*with +(modifications|changes)/].freeze,
|
|
11
|
+
[:rejected, /reject(ed)?|not accepted/].freeze,
|
|
12
|
+
[:accepted, /accept(ed)?/].freeze,
|
|
13
|
+
[:noted, /noted/].freeze,
|
|
14
|
+
[:todo, /todo/].freeze
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Returns the canonical status symbol for the given text, or nil when
|
|
20
|
+
# nothing matches. Order matters: "accept with modifications" and
|
|
21
|
+
# "not accepted" must be classified before the bare "accept" pattern.
|
|
22
|
+
def match(text)
|
|
23
|
+
value = text.to_s.downcase.strip
|
|
24
|
+
return nil if value.empty?
|
|
25
|
+
|
|
26
|
+
PATTERNS.each do |status, pattern|
|
|
27
|
+
return status if value.match?(pattern)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def statuses
|
|
34
|
+
PATTERNS.map(&:first)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/commenter/filler.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "docx"
|
|
4
|
+
require "zip"
|
|
4
5
|
|
|
5
6
|
module Commenter
|
|
6
7
|
class Filler
|
|
8
|
+
# Labels of the sheet metadata fields in the template's page header
|
|
9
|
+
# (word/header*.xml). Values supplied to #fill are appended after each
|
|
10
|
+
# label; the docx gem cannot write header parts (see docx PR #73), so
|
|
11
|
+
# this is done by rewriting the entry directly.
|
|
12
|
+
HEADER_METADATA = { date: "Date", document: "Document", project: "Project" }.freeze
|
|
13
|
+
|
|
7
14
|
def fill(template_path, output_path, comments, options = {})
|
|
8
15
|
doc = Docx::Document.open(template_path)
|
|
9
16
|
table = doc.tables.first
|
|
@@ -11,48 +18,88 @@ module Commenter
|
|
|
11
18
|
raise "No table found in template" unless table
|
|
12
19
|
raise "Template table must have at least one row" if table.row_count < 1
|
|
13
20
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
# The template row (first row in the table) is copied for each comment;
|
|
22
|
+
# all original template rows are removed afterwards via their XML
|
|
23
|
+
# nodes (the shipped 2012-03 template carries extra blank rows beyond
|
|
24
|
+
# the first, and the docx gem's Row exposes no #remove).
|
|
25
|
+
template_rows = table.rows.to_a
|
|
26
|
+
comments.each { |comment| fill_row(template_rows.first, comment, options) }
|
|
27
|
+
template_rows.each { |row| row.node.remove }
|
|
28
|
+
|
|
29
|
+
doc.save(output_path)
|
|
30
|
+
write_header_metadata(output_path, options)
|
|
31
|
+
output_path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def fill_row(template_row, comment, options)
|
|
37
|
+
comment = Comment.from_hash(comment) unless comment.is_a?(Comment)
|
|
38
|
+
|
|
39
|
+
new_row = template_row.copy
|
|
40
|
+
new_row.insert_before(template_row)
|
|
41
|
+
|
|
42
|
+
set_cell_text(new_row.cells[0], comment.id.to_s)
|
|
43
|
+
set_cell_text(new_row.cells[1], comment.line_number.to_s)
|
|
44
|
+
set_cell_text(new_row.cells[2], comment.clause.to_s)
|
|
45
|
+
set_cell_text(new_row.cells[3], comment.element.to_s)
|
|
46
|
+
set_cell_text(new_row.cells[4], comment.type.to_s)
|
|
47
|
+
set_cell_text(new_row.cells[5], comment.comments.to_s)
|
|
48
|
+
set_cell_text(new_row.cells[6], comment.proposed_change.to_s)
|
|
49
|
+
|
|
50
|
+
observations = comment.observations.to_s
|
|
51
|
+
return if observations.empty?
|
|
52
|
+
|
|
53
|
+
set_cell_text(new_row.cells[7], observations)
|
|
54
|
+
apply_shading(new_row.cells[7], observations) if options[:shading]
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
puts "Warning: Could not add row for comment #{comment.id}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_header_metadata(output_path, options)
|
|
60
|
+
metadata = HEADER_METADATA.map do |key, label|
|
|
61
|
+
value = options[key].to_s.strip
|
|
62
|
+
value.empty? ? nil : [label, value]
|
|
63
|
+
end.compact
|
|
64
|
+
return if metadata.empty?
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
rewrite_headers(output_path, metadata)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def rewrite_headers(output_path, metadata)
|
|
70
|
+
# In-memory buffer + File.binwrite rather than rewriting the archive
|
|
71
|
+
# in place: rubyzip commits in-place edits with File.rename, which
|
|
72
|
+
# fails with EACCES on Windows.
|
|
73
|
+
buffer = Zip::OutputStream.write_buffer do |out|
|
|
74
|
+
Zip::File.open(output_path) do |zip|
|
|
75
|
+
zip.entries.each do |entry|
|
|
76
|
+
# Block form: the entry stream is closed eagerly, otherwise its
|
|
77
|
+
# handle keeps the file locked on Windows and the binwrite below
|
|
78
|
+
# fails with EACCES.
|
|
79
|
+
content = entry.get_input_stream(&:read)
|
|
80
|
+
out.put_next_entry(entry.name)
|
|
81
|
+
out.write(patch_header(entry.name, content, metadata))
|
|
82
|
+
end
|
|
46
83
|
end
|
|
47
84
|
end
|
|
85
|
+
File.binwrite(output_path, buffer.string)
|
|
86
|
+
end
|
|
48
87
|
|
|
49
|
-
|
|
50
|
-
|
|
88
|
+
def patch_header(entry_name, content, metadata)
|
|
89
|
+
return content unless entry_name.match?(%r{\Aword/header\d*\.xml\z})
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
xml = content.dup.force_encoding(Encoding::UTF_8)
|
|
92
|
+
metadata.each do |label, value|
|
|
93
|
+
xml.sub!(%r{(<w:t[^>]*>)\s*#{label}:\s*(</w:t>)}) do
|
|
94
|
+
"#{Regexp.last_match(1)}#{label}: #{escape(value)}#{Regexp.last_match(2)}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
xml
|
|
53
98
|
end
|
|
54
99
|
|
|
55
|
-
|
|
100
|
+
def escape(text)
|
|
101
|
+
text.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
102
|
+
end
|
|
56
103
|
|
|
57
104
|
def set_cell_text(cell, text)
|
|
58
105
|
return if text.nil? || text.empty?
|
|
@@ -86,14 +133,16 @@ module Commenter
|
|
|
86
133
|
puts "Warning: Could not set text '#{text}' in cell: #{e.message}"
|
|
87
134
|
end
|
|
88
135
|
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
SHADING_COLORS = {
|
|
137
|
+
accept_with_modifications: "C4D79B", # Olive Green
|
|
138
|
+
accepted: "92D050", # Green
|
|
139
|
+
noted: "8DB4E2", # Blue
|
|
140
|
+
rejected: "FF99CC", # Pink
|
|
141
|
+
todo: "D9D9D9" # Light Gray (for diagonal stripes, we use solid for now)
|
|
142
|
+
}.freeze
|
|
91
143
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
|
95
|
-
result[new_key] = new_value
|
|
96
|
-
end
|
|
144
|
+
def determine_shading_color(observation)
|
|
145
|
+
SHADING_COLORS[DispositionStatus.match(observation)]
|
|
97
146
|
end
|
|
98
147
|
|
|
99
148
|
def apply_shading(cell, observation)
|
|
@@ -111,23 +160,6 @@ module Commenter
|
|
|
111
160
|
puts "Warning: Could not apply shading to cell: #{e.message}"
|
|
112
161
|
end
|
|
113
162
|
|
|
114
|
-
def determine_shading_color(observation)
|
|
115
|
-
text = observation.downcase.strip
|
|
116
|
-
|
|
117
|
-
case text
|
|
118
|
-
when /awm|accept with modifications/
|
|
119
|
-
"C4D79B" # Olive Green
|
|
120
|
-
when /accept(ed)?/
|
|
121
|
-
"92D050" # Green
|
|
122
|
-
when /noted/
|
|
123
|
-
"8DB4E2" # Blue
|
|
124
|
-
when /reject(ed)?/
|
|
125
|
-
"FF99CC" # Pink
|
|
126
|
-
when /todo/
|
|
127
|
-
"D9D9D9" # Light Gray (for diagonal stripes, we'll use solid for now)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
163
|
def apply_cell_shading(cell, color)
|
|
132
164
|
# Access the cell's XML node
|
|
133
165
|
cell_node = cell.node
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Commenter
|
|
6
|
+
# GitHub issue tracking state attached to a comment by github-create /
|
|
7
|
+
# github-retrieve.
|
|
8
|
+
class GithubInfo < Lutaml::Model::Serializable
|
|
9
|
+
attribute :issue_number, :integer
|
|
10
|
+
attribute :issue_url, :string
|
|
11
|
+
attribute :status, :string
|
|
12
|
+
attribute :created_at, :string
|
|
13
|
+
attribute :updated_at, :string
|
|
14
|
+
|
|
15
|
+
yaml do
|
|
16
|
+
map "issue_number", to: :issue_number
|
|
17
|
+
map "issue_url", to: :issue_url
|
|
18
|
+
map "status", to: :status
|
|
19
|
+
map "created_at", to: :created_at
|
|
20
|
+
map "updated_at", to: :updated_at
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|