commenter 0.2.1 → 0.2.2

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,78 @@
1
+ # GitHub configuration for commenter gem
2
+ # Copy this file and customize for your project
3
+
4
+ github:
5
+ # Repository in owner/repo format
6
+ repository: "owner/repo-name"
7
+
8
+ # GitHub token (recommended to use GITHUB_TOKEN environment variable instead)
9
+ # token: "ghp_xxxxxxxxxxxx"
10
+
11
+ # Milestone configuration
12
+ milestone:
13
+ # Use existing milestone by name
14
+ name: "ISO Comment Review"
15
+
16
+ # Alternative: Use existing milestone by number
17
+ # number: 5
18
+
19
+ # Create milestone if it doesn't exist (optional)
20
+ # create_if_missing: true
21
+ # description: "Review comments for ISO standard"
22
+ # due_date: "2024-12-31"
23
+
24
+ # Stage-specific milestones (optional)
25
+ stage_milestones:
26
+ WD: "Working Draft Review"
27
+ CD: "Committee Draft Review"
28
+ DIS: "DIS National Review"
29
+ FDIS: "Final DIS Review"
30
+ PRF: "Proof Review"
31
+ PUB: "Publication Review"
32
+
33
+ # Default labels for all issues
34
+ default_labels:
35
+ - "comment-review"
36
+ - "iso-standard"
37
+
38
+ # Stage-specific labels
39
+ stage_labels:
40
+ WD:
41
+ - "working-draft"
42
+ - "early-review"
43
+ CD:
44
+ - "committee-draft"
45
+ - "committee-review"
46
+ DIS:
47
+ - "draft-international-standard"
48
+ - "national-review"
49
+ FDIS:
50
+ - "final-draft"
51
+ - "final-review"
52
+ PRF:
53
+ - "proof-stage"
54
+ - "editorial"
55
+ PUB:
56
+ - "publication"
57
+ - "published"
58
+
59
+ # Default assignee (GitHub username)
60
+ default_assignee: "reviewer-handle"
61
+
62
+ # Custom template paths (optional)
63
+ templates:
64
+ title: "custom_title_template.liquid"
65
+ body: "custom_body_template.liquid"
66
+
67
+ # Retrieval configuration for github-retrieve command
68
+ retrieval:
69
+ # Magic comment markers to look for in GitHub issue comments
70
+ observation_markers:
71
+ - "**OBSERVATION:**"
72
+ - "**COMMENTER OBSERVATION:**"
73
+
74
+ # Fallback to last comment if no magic comment found
75
+ fallback_to_last_comment: true
76
+
77
+ # Only retrieve from closed issues (recommended)
78
+ closed_issues_only: true
@@ -0,0 +1,35 @@
1
+ # {% if stage %}[{{ stage | upcase }}] {% endif %}{% if document %}{{ document }} - {% endif %}Comment {{ comment_id }}
2
+
3
+ **Stage:** {{ stage | default: "Not specified" }}
4
+ **Document:** {{ document | default: "Not specified" }}
5
+ **Comment type:** {{ type }} ({{ type_full_name }})
6
+ **Member body:** {{ body }}
7
+
8
+ ## Location
9
+
10
+ {% if clause or element or line_number %}
11
+ {% if clause %}* **Clause:** {{ clause }}{% endif %}
12
+ {% if element %}* **Element:** {{ element }}{% endif %}
13
+ {% if line_number %}* **Line number:** {{ line_number }}{% endif %}
14
+ {% else %}
15
+ * Location not specified
16
+ {% endif %}
17
+
18
+ ## Comment
19
+
20
+ {{ comments | default: "No comment provided" }}
21
+
22
+ {% if has_proposed_change %}
23
+ ## Proposed change
24
+
25
+ {{ proposed_change }}
26
+ {% endif %}
27
+
28
+ {% if has_observations %}
29
+ ## Observations
30
+
31
+ {{ observations }}
32
+ {% endif %}
33
+
34
+ ---
35
+ *Generated from ISO comment sheet via commenter gem*
@@ -0,0 +1 @@
1
+ {% if stage %}[{{ stage | upcase }}] {% endif %}{% if document %}{{ document }} - {% endif %}{{ comment_id }}: {{ brief_summary }}
data/exe/commenter CHANGED
@@ -4,4 +4,4 @@
4
4
  require "thor"
5
5
  require "commenter/cli"
6
6
 
7
- Commenter::CLI.start(ARGV)
7
+ Commenter::Cli.start(ARGV)
data/lib/commenter/cli.rb CHANGED
@@ -5,9 +5,10 @@ require "yaml"
5
5
  require "fileutils"
6
6
  require "commenter/parser"
7
7
  require "commenter/filler"
8
+ require "commenter/github_integration"
8
9
 
9
10
  module Commenter
10
- class CLI < Thor
11
+ class Cli < Thor
11
12
  desc "import INPUT.docx", "Convert DOCX comment sheet to YAML"
12
13
  option :output, type: :string, aliases: :o, default: "comments.yaml", desc: "Output YAML file"
13
14
  option :exclude_observations, type: :boolean, aliases: :e, desc: "Exclude observations column"
@@ -32,9 +33,7 @@ module Commenter
32
33
  schema_target = File.join(schema_dir, "iso_comment_2012-03.yaml")
33
34
 
34
35
  # Only copy if source and target are different
35
- unless File.expand_path(schema_source) == File.expand_path(schema_target)
36
- FileUtils.cp(schema_source, schema_target)
37
- end
36
+ FileUtils.cp(schema_source, schema_target) unless File.expand_path(schema_source) == File.expand_path(schema_target)
38
37
 
39
38
  puts "Converted #{input_docx} to #{output_yaml}"
40
39
  puts "Schema file created at #{schema_target}"
@@ -67,6 +66,140 @@ module Commenter
67
66
  puts "Filled template to #{output_docx}"
68
67
  end
69
68
 
69
+ desc "github-create INPUT.yaml", "Create GitHub issues from comments"
70
+ option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
71
+ option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
72
+ option :stage, type: :string, desc: "Override approval stage (WD/CD/DIS/FDIS/PRF/PUB)"
73
+ option :milestone, type: :string, desc: "Override milestone name or number"
74
+ option :assignee, type: :string, desc: "Override assignee GitHub handle"
75
+ option :title_template, type: :string, desc: "Custom title template file"
76
+ option :body_template, type: :string, desc: "Custom body template file"
77
+ option :dry_run, type: :boolean, desc: "Preview issues without creating them"
78
+ def github_create(input_yaml)
79
+ creator = GitHubIssueCreator.new(
80
+ options[:config],
81
+ options[:title_template],
82
+ options[:body_template]
83
+ )
84
+
85
+ github_options = {
86
+ stage: options[:stage],
87
+ milestone: options[:milestone],
88
+ assignee: options[:assignee],
89
+ dry_run: options[:dry_run],
90
+ output: options[:output]
91
+ }.compact
92
+
93
+ results = creator.create_issues_from_yaml(input_yaml, github_options)
94
+
95
+ if options[:dry_run]
96
+ puts "DRY RUN - Preview of issues to be created:"
97
+ puts "=" * 50
98
+ results.each do |result|
99
+ puts "\nComment ID: #{result[:comment_id]}"
100
+ puts "Title: #{result[:title]}"
101
+ puts "Labels: #{result[:labels].join(", ")}" if result[:labels]&.any?
102
+ puts "Assignees: #{result[:assignees].join(", ")}" if result[:assignees]&.any?
103
+ puts "Milestone: #{result[:milestone]}" if result[:milestone]
104
+ puts "\nBody preview (first 200 chars):"
105
+ puts result[:body][0...200] + (result[:body].length > 200 ? "..." : "")
106
+ puts "-" * 30
107
+ end
108
+ else
109
+ puts "GitHub issue creation results:"
110
+ puts "=" * 40
111
+
112
+ created_count = 0
113
+ skipped_count = 0
114
+ error_count = 0
115
+
116
+ results.each do |result|
117
+ case result[:status]
118
+ when :created
119
+ created_count += 1
120
+ puts "✓ #{result[:comment_id]}: Created issue ##{result[:issue_number]}"
121
+ puts " URL: #{result[:issue_url]}"
122
+ when :skipped
123
+ skipped_count += 1
124
+ puts "- #{result[:comment_id]}: Skipped (#{result[:message]})"
125
+ puts " URL: #{result[:issue_url]}" if result[:issue_url]
126
+ when :error
127
+ error_count += 1
128
+ puts "✗ #{result[:comment_id]}: Error - #{result[:message]}"
129
+ end
130
+ end
131
+
132
+ puts "\nSummary:"
133
+ puts "Created: #{created_count}, Skipped: #{skipped_count}, Errors: #{error_count}"
134
+ end
135
+ rescue StandardError => e
136
+ puts "Error: #{e.message}"
137
+ exit 1
138
+ end
139
+
140
+ desc "github-retrieve INPUT.yaml", "Retrieve observations from GitHub issues"
141
+ option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
142
+ option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
143
+ option :include_open, type: :boolean, desc: "Include observations from open issues"
144
+ option :dry_run, type: :boolean, desc: "Preview observations without updating"
145
+ def github_retrieve(input_yaml)
146
+ retriever = GitHubIssueRetriever.new(options[:config])
147
+
148
+ retrieve_options = {
149
+ output: options[:output],
150
+ include_open: options[:include_open],
151
+ dry_run: options[:dry_run]
152
+ }.compact
153
+
154
+ results = retriever.retrieve_observations_from_yaml(input_yaml, retrieve_options)
155
+
156
+ if options[:dry_run]
157
+ puts "DRY RUN - Preview of observations to be retrieved:"
158
+ puts "=" * 50
159
+ results.each do |result|
160
+ puts "\nComment ID: #{result[:comment_id]}"
161
+ puts "Issue ##{result[:issue_number]}: #{result[:status]}"
162
+ if result[:observation]
163
+ puts "Observation preview (first 200 chars):"
164
+ puts result[:observation][0...200] + (result[:observation].length > 200 ? "..." : "")
165
+ else
166
+ puts "No observation found"
167
+ end
168
+ puts "-" * 30
169
+ end
170
+ else
171
+ puts "GitHub observation retrieval results:"
172
+ puts "=" * 40
173
+
174
+ retrieved_count = 0
175
+ skipped_count = 0
176
+ error_count = 0
177
+
178
+ results.each do |result|
179
+ case result[:status]
180
+ when :retrieved
181
+ retrieved_count += 1
182
+ puts "✓ #{result[:comment_id]}: Retrieved observation from issue ##{result[:issue_number]}"
183
+ when :skipped
184
+ skipped_count += 1
185
+ puts "- #{result[:comment_id]}: Skipped (#{result[:message]})"
186
+ when :error
187
+ error_count += 1
188
+ puts "✗ #{result[:comment_id]}: Error - #{result[:message]}"
189
+ end
190
+ end
191
+
192
+ puts "\nSummary:"
193
+ puts "Retrieved: #{retrieved_count}, Skipped: #{skipped_count}, Errors: #{error_count}"
194
+
195
+ output_file = options[:output] || input_yaml
196
+ puts "Updated YAML file: #{output_file}"
197
+ end
198
+ rescue StandardError => e
199
+ puts "Error: #{e.message}"
200
+ exit 1
201
+ end
202
+
70
203
  def self.exit_on_failure?
71
204
  true
72
205
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Commenter
4
4
  class Comment
5
- attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations
5
+ attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations, :github
6
6
 
7
7
  def initialize(attributes = {})
8
8
  # Normalize input to symbols
@@ -15,6 +15,7 @@ module Commenter
15
15
  @comments = attrs[:comments]
16
16
  @proposed_change = attrs[:proposed_change]
17
17
  @observations = attrs[:observations]
18
+ @github = symbolize_keys(attrs[:github] || {})
18
19
  end
19
20
 
20
21
  def line_number
@@ -41,6 +42,63 @@ module Commenter
41
42
  @locality[:element] = value
42
43
  end
43
44
 
45
+ def brief_summary(max_length = 80)
46
+ parts = []
47
+
48
+ # Add locality information first
49
+ parts << "Clause #{clause}" if clause && !clause.strip.empty?
50
+ parts << element if element && !element.strip.empty?
51
+ parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
52
+
53
+ locality_text = parts.join(", ")
54
+
55
+ # Add description from comment text
56
+ if @comments && !@comments.strip.empty?
57
+ # Extract first sentence or truncate
58
+ clean_text = @comments.strip.gsub(/\s+/, " ")
59
+ first_sentence = clean_text.split(/[.!?]/).first&.strip
60
+ description = if first_sentence && first_sentence.length < max_length
61
+ first_sentence
62
+ else
63
+ clean_text[0...50]
64
+ end
65
+
66
+ if locality_text.empty?
67
+ description
68
+ else
69
+ # Combine locality + description, respecting max_length
70
+ combined = "#{locality_text}: #{description}"
71
+ combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
72
+ end
73
+ else
74
+ locality_text.empty? ? "No description" : locality_text
75
+ end
76
+ end
77
+
78
+ def github_issue_number
79
+ @github[:issue_number]
80
+ end
81
+
82
+ def github_issue_url
83
+ @github[:issue_url]
84
+ end
85
+
86
+ def github_status
87
+ @github[:status]
88
+ end
89
+
90
+ def github_created_at
91
+ @github[:created_at]
92
+ end
93
+
94
+ def github_updated_at
95
+ @github[:updated_at]
96
+ end
97
+
98
+ def has_github_issue?
99
+ !@github[:issue_number].nil?
100
+ end
101
+
44
102
  def to_h
45
103
  {
46
104
  id: @id,
@@ -49,8 +107,9 @@ module Commenter
49
107
  type: @type,
50
108
  comments: @comments,
51
109
  proposed_change: @proposed_change,
52
- observations: @observations
53
- }
110
+ observations: @observations,
111
+ github: @github.empty? ? nil : @github
112
+ }.compact
54
113
  end
55
114
 
56
115
  def to_yaml_h
@@ -4,7 +4,7 @@ require_relative "comment"
4
4
 
5
5
  module Commenter
6
6
  class CommentSheet
7
- attr_accessor :version, :date, :document, :project, :comments
7
+ attr_accessor :version, :date, :document, :project, :stage, :comments
8
8
 
9
9
  def initialize(attributes = {})
10
10
  # Normalize input to symbols
@@ -14,6 +14,7 @@ module Commenter
14
14
  @date = attrs[:date]
15
15
  @document = attrs[:document]
16
16
  @project = attrs[:project]
17
+ @stage = attrs[:stage]
17
18
  @comments = (attrs[:comments] || []).map { |c| c.is_a?(Comment) ? c : Comment.from_hash(c) }
18
19
  end
19
20
 
@@ -27,6 +28,7 @@ module Commenter
27
28
  date: @date,
28
29
  document: @document,
29
30
  project: @project,
31
+ stage: @stage,
30
32
  comments: @comments.map(&:to_h)
31
33
  }
32
34
  end
@@ -57,13 +59,13 @@ module Commenter
57
59
  hash.each_with_object({}) do |(key, value), result|
58
60
  new_key = key.to_s
59
61
  new_value = case value
60
- when Hash
61
- stringify_keys(value)
62
- when Array
63
- value.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
64
- else
65
- value
66
- end
62
+ when Hash
63
+ stringify_keys(value)
64
+ when Array
65
+ value.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
66
+ else
67
+ value
68
+ end
67
69
  result[new_key] = new_value
68
70
  end
69
71
  end
@@ -15,7 +15,7 @@ module Commenter
15
15
  template_row = table.rows.first
16
16
 
17
17
  # Add new rows for each comment by copying the template row
18
- comments.each_with_index do |comment, index|
18
+ comments.each_with_index do |comment, _index|
19
19
  # Convert comment to symbol keys for consistent access
20
20
  comment_data = symbolize_keys(comment)
21
21
 
@@ -24,7 +24,7 @@ module Commenter
24
24
  new_row = template_row.copy
25
25
  new_row.insert_before(template_row)
26
26
  row = new_row
27
- rescue => e
27
+ rescue StandardError => e
28
28
  puts "Warning: Could not add row for comment #{comment_data[:id]}: #{e.message}"
29
29
  next
30
30
  end
@@ -64,27 +64,25 @@ module Commenter
64
64
  paragraph.each_text_run do |text_run|
65
65
  # Get current text and substitute it with new text
66
66
  current_text = text_run.text
67
- if current_text && !current_text.empty?
68
- text_run.substitute(current_text, text)
69
- text_set = true
70
- return # Only substitute in the first text run found
71
- end
67
+ next unless current_text && !current_text.empty?
68
+
69
+ text_run.substitute(current_text, text)
70
+ text_set = true
71
+ return # Only substitute in the first text run found
72
72
  end
73
73
  end
74
74
 
75
75
  # If no text runs with content were found, add text to the first paragraph
76
- unless text_set
77
- if cell.paragraphs.any?
78
- paragraph = cell.paragraphs.first
79
- # Try to add a text run to the paragraph
80
- if paragraph.respond_to?(:add_text)
81
- paragraph.add_text(text)
82
- elsif paragraph.respond_to?(:text=)
83
- paragraph.text = text
84
- end
76
+ if !text_set && cell.paragraphs.any?
77
+ paragraph = cell.paragraphs.first
78
+ # Try to add a text run to the paragraph
79
+ if paragraph.respond_to?(:add_text)
80
+ paragraph.add_text(text)
81
+ elsif paragraph.respond_to?(:text=)
82
+ paragraph.text = text
85
83
  end
86
84
  end
87
- rescue => e
85
+ rescue StandardError => e
88
86
  puts "Warning: Could not set text '#{text}' in cell: #{e.message}"
89
87
  end
90
88
 
@@ -99,9 +97,65 @@ module Commenter
99
97
  end
100
98
 
101
99
  def apply_shading(cell, observation)
102
- # Shading functionality is not fully supported by the docx gem
103
- # This is a placeholder for future implementation
104
- puts "Shading requested for: #{observation}" if observation
100
+ return unless observation && !observation.empty?
101
+
102
+ # Determine shading color based on status patterns
103
+ shading_color = determine_shading_color(observation)
104
+ return unless shading_color
105
+
106
+ puts "Applying #{shading_color} cell shading for: #{observation.strip}"
107
+
108
+ # Apply shading to the table cell itself
109
+ apply_cell_shading(cell, shading_color)
110
+ rescue StandardError => e
111
+ puts "Warning: Could not apply shading to cell: #{e.message}"
112
+ end
113
+
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
+ def apply_cell_shading(cell, color)
132
+ # Access the cell's XML node
133
+ cell_node = cell.node
134
+
135
+ # Find or create the table cell properties (tcPr) element
136
+ tcpr_node = cell_node.at_xpath(".//w:tcPr", "w" => "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
137
+
138
+ unless tcpr_node
139
+ # Create table cell properties if they don't exist
140
+ tcpr_node = cell_node.document.create_element("tcPr")
141
+ cell_node.prepend_child(tcpr_node)
142
+ end
143
+
144
+ # Remove existing shading if present
145
+ existing_shd = tcpr_node.at_xpath(".//w:shd", "w" => "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
146
+ existing_shd&.remove
147
+
148
+ # Create new shading element for the cell
149
+ shd_node = cell_node.document.create_element("shd")
150
+ shd_node["w:val"] = "clear"
151
+ shd_node["w:color"] = "auto"
152
+ shd_node["w:fill"] = color
153
+
154
+ # Add namespace declaration
155
+ shd_node.namespace = cell_node.document.root.namespace_definitions.find { |ns| ns.prefix == "w" }
156
+
157
+ # Add the shading to table cell properties
158
+ tcpr_node.add_child(shd_node)
105
159
  end
106
160
  end
107
161
  end