commenter 0.2.1 → 0.2.3

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,86 @@
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 template for GitHub issues
65
+ # The {{ unique_id }} variable is required for duplicate detection
66
+ title: "custom_title_template.liquid"
67
+ body: "custom_body_template.liquid"
68
+
69
+ # Unique identifier template for duplicate detection
70
+ # This is rendered and used to search for existing issues
71
+ # Default: "[{{ stage | upcase }}] {{ comment_id }}"
72
+ # The unique_id is also available as {{ unique_id }} in title/body templates
73
+ unique_id: "[{{ stage | upcase }}] {{ comment_id }}"
74
+
75
+ # Retrieval configuration for github-retrieve command
76
+ retrieval:
77
+ # Magic comment markers to look for in GitHub issue comments
78
+ observation_markers:
79
+ - "**OBSERVATION:**"
80
+ - "**COMMENTER OBSERVATION:**"
81
+
82
+ # Fallback to last comment if no magic comment found
83
+ fallback_to_last_comment: true
84
+
85
+ # Only retrieve from closed issues (recommended)
86
+ closed_issues_only: true
@@ -0,0 +1,35 @@
1
+ # {% if stage %}[{{ stage | upcase }}] {% endif %}{{ comment_id }} {% if document %}({{ document }}){% endif %}
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
+ {{ unique_id }}: {{ brief_summary }} {% if document %}({{ document }}){% endif %}
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"
@@ -33,7 +34,8 @@ module Commenter
33
34
 
34
35
  # Only copy if source and target are different
35
36
  unless File.expand_path(schema_source) == File.expand_path(schema_target)
36
- FileUtils.cp(schema_source, schema_target)
37
+ FileUtils.cp(schema_source,
38
+ schema_target)
37
39
  end
38
40
 
39
41
  puts "Converted #{input_docx} to #{output_yaml}"
@@ -67,6 +69,140 @@ module Commenter
67
69
  puts "Filled template to #{output_docx}"
68
70
  end
69
71
 
72
+ desc "github-create INPUT.yaml", "Create GitHub issues from comments"
73
+ option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
74
+ option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
75
+ option :stage, type: :string, desc: "Override approval stage (WD/CD/DIS/FDIS/PRF/PUB)"
76
+ option :milestone, type: :string, desc: "Override milestone name or number"
77
+ option :assignee, type: :string, desc: "Override assignee GitHub handle"
78
+ option :title_template, type: :string, desc: "Custom title template file"
79
+ option :body_template, type: :string, desc: "Custom body template file"
80
+ option :dry_run, type: :boolean, desc: "Preview issues without creating them"
81
+ def github_create(input_yaml)
82
+ creator = GitHubIssueCreator.new(
83
+ options[:config],
84
+ options[:title_template],
85
+ options[:body_template]
86
+ )
87
+
88
+ github_options = {
89
+ stage: options[:stage],
90
+ milestone: options[:milestone],
91
+ assignee: options[:assignee],
92
+ dry_run: options[:dry_run],
93
+ output: options[:output]
94
+ }.compact
95
+
96
+ results = creator.create_issues_from_yaml(input_yaml, github_options)
97
+
98
+ if options[:dry_run]
99
+ puts "DRY RUN - Preview of issues to be created:"
100
+ puts "=" * 50
101
+ results.each do |result|
102
+ puts "\nComment ID: #{result[:comment_id]}"
103
+ puts "Title: #{result[:title]}"
104
+ puts "Labels: #{result[:labels].join(", ")}" if result[:labels]&.any?
105
+ puts "Assignees: #{result[:assignees].join(", ")}" if result[:assignees]&.any?
106
+ puts "Milestone: #{result[:milestone]}" if result[:milestone]
107
+ puts "\nBody preview (first 200 chars):"
108
+ puts result[:body][0...200] + (result[:body].length > 200 ? "..." : "")
109
+ puts "-" * 30
110
+ end
111
+ else
112
+ puts "GitHub issue creation results:"
113
+ puts "=" * 40
114
+
115
+ created_count = 0
116
+ skipped_count = 0
117
+ error_count = 0
118
+
119
+ results.each do |result|
120
+ case result[:status]
121
+ when :created
122
+ created_count += 1
123
+ puts "✓ #{result[:comment_id]}: Created issue ##{result[:issue_number]}"
124
+ puts " URL: #{result[:issue_url]}"
125
+ when :skipped
126
+ skipped_count += 1
127
+ puts "- #{result[:comment_id]}: Skipped (#{result[:message]})"
128
+ puts " URL: #{result[:issue_url]}" if result[:issue_url]
129
+ when :error
130
+ error_count += 1
131
+ puts "✗ #{result[:comment_id]}: Error - #{result[:message]}"
132
+ end
133
+ end
134
+
135
+ puts "\nSummary:"
136
+ puts "Created: #{created_count}, Skipped: #{skipped_count}, Errors: #{error_count}"
137
+ end
138
+ rescue StandardError => e
139
+ puts "Error: #{e.message}"
140
+ exit 1
141
+ end
142
+
143
+ desc "github-retrieve INPUT.yaml", "Retrieve observations from GitHub issues"
144
+ option :config, type: :string, aliases: :c, required: true, desc: "GitHub configuration YAML file"
145
+ option :output, type: :string, aliases: :o, desc: "Output YAML file (default: update original)"
146
+ option :include_open, type: :boolean, desc: "Include observations from open issues"
147
+ option :dry_run, type: :boolean, desc: "Preview observations without updating"
148
+ def github_retrieve(input_yaml)
149
+ retriever = GitHubIssueRetriever.new(options[:config])
150
+
151
+ retrieve_options = {
152
+ output: options[:output],
153
+ include_open: options[:include_open],
154
+ dry_run: options[:dry_run]
155
+ }.compact
156
+
157
+ results = retriever.retrieve_observations_from_yaml(input_yaml, retrieve_options)
158
+
159
+ if options[:dry_run]
160
+ puts "DRY RUN - Preview of observations to be retrieved:"
161
+ puts "=" * 50
162
+ results.each do |result|
163
+ puts "\nComment ID: #{result[:comment_id]}"
164
+ puts "Issue ##{result[:issue_number]}: #{result[:status]}"
165
+ if result[:observation]
166
+ puts "Observation preview (first 200 chars):"
167
+ puts result[:observation][0...200] + (result[:observation].length > 200 ? "..." : "")
168
+ else
169
+ puts "No observation found"
170
+ end
171
+ puts "-" * 30
172
+ end
173
+ else
174
+ puts "GitHub observation retrieval results:"
175
+ puts "=" * 40
176
+
177
+ retrieved_count = 0
178
+ skipped_count = 0
179
+ error_count = 0
180
+
181
+ results.each do |result|
182
+ case result[:status]
183
+ when :retrieved
184
+ retrieved_count += 1
185
+ puts "✓ #{result[:comment_id]}: Retrieved observation from issue ##{result[:issue_number]}"
186
+ when :skipped
187
+ skipped_count += 1
188
+ puts "- #{result[:comment_id]}: Skipped (#{result[:message]})"
189
+ when :error
190
+ error_count += 1
191
+ puts "✗ #{result[:comment_id]}: Error - #{result[:message]}"
192
+ end
193
+ end
194
+
195
+ puts "\nSummary:"
196
+ puts "Retrieved: #{retrieved_count}, Skipped: #{skipped_count}, Errors: #{error_count}"
197
+
198
+ output_file = options[:output] || input_yaml
199
+ puts "Updated YAML file: #{output_file}"
200
+ end
201
+ rescue StandardError => e
202
+ puts "Error: #{e.message}"
203
+ exit 1
204
+ end
205
+
70
206
  def self.exit_on_failure?
71
207
  true
72
208
  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
@@ -11,10 +11,20 @@ module Commenter
11
11
  @id = attrs[:id]
12
12
  @body = attrs[:body]
13
13
  @locality = symbolize_keys(attrs[:locality] || {})
14
- @type = attrs[:type]
14
+ @type = expand_comment_type(attrs[:type])
15
15
  @comments = attrs[:comments]
16
16
  @proposed_change = attrs[:proposed_change]
17
17
  @observations = attrs[:observations]
18
+ @github = symbolize_keys(attrs[:github] || {})
19
+ end
20
+
21
+ def expand_comment_type(type)
22
+ case type&.downcase
23
+ when "ge" then "general"
24
+ when "te" then "technical"
25
+ when "ed" then "editorial"
26
+ else type
27
+ end
18
28
  end
19
29
 
20
30
  def line_number
@@ -41,6 +51,63 @@ module Commenter
41
51
  @locality[:element] = value
42
52
  end
43
53
 
54
+ def brief_summary(max_length = 80)
55
+ parts = []
56
+
57
+ # Add locality information first
58
+ parts << "Clause #{clause}" if clause && !clause.strip.empty?
59
+ parts << element if element && !element.strip.empty?
60
+ parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
61
+
62
+ locality_text = parts.join(", ")
63
+
64
+ # Add description from comment text
65
+ if @comments && !@comments.strip.empty?
66
+ # Extract first sentence or truncate
67
+ clean_text = @comments.strip.gsub(/\s+/, " ")
68
+ first_sentence = clean_text.split(/[.!?]/).first&.strip
69
+ description = if first_sentence && first_sentence.length < max_length
70
+ first_sentence
71
+ else
72
+ clean_text[0...50]
73
+ end
74
+
75
+ if locality_text.empty?
76
+ description
77
+ else
78
+ # Combine locality + description, respecting max_length
79
+ combined = "#{locality_text}: #{description}"
80
+ combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
81
+ end
82
+ else
83
+ locality_text.empty? ? "No description" : locality_text
84
+ end
85
+ end
86
+
87
+ def github_issue_number
88
+ @github[:issue_number]
89
+ end
90
+
91
+ def github_issue_url
92
+ @github[:issue_url]
93
+ end
94
+
95
+ def github_status
96
+ @github[:status]
97
+ end
98
+
99
+ def github_created_at
100
+ @github[:created_at]
101
+ end
102
+
103
+ def github_updated_at
104
+ @github[:updated_at]
105
+ end
106
+
107
+ def has_github_issue?
108
+ !@github[:issue_number].nil?
109
+ end
110
+
44
111
  def to_h
45
112
  {
46
113
  id: @id,
@@ -49,8 +116,9 @@ module Commenter
49
116
  type: @type,
50
117
  comments: @comments,
51
118
  proposed_change: @proposed_change,
52
- observations: @observations
53
- }
119
+ observations: @observations,
120
+ github: @github.empty? ? nil : @github
121
+ }.compact
54
122
  end
55
123
 
56
124
  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