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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zip"
4
+
5
+ # Builds minimal single-workbook XLSX files for specs, without needing a
6
+ # spreadsheet-writing dependency. Strings are stored via sharedStrings; numbers
7
+ # are written as plain numeric cells. Empty/nil cells are omitted (sparse
8
+ # rows), matching how real exports represent empty cells.
9
+ module XlsxBuilder
10
+ SHEET_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
11
+ RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
12
+ OFFICE_REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
13
+ private_constant :SHEET_NS, :RELS_NS, :OFFICE_REL_NS
14
+
15
+ # sheets: array of [name, rows] pairs, where rows is an array of arrays.
16
+ # Cell values: String or Numeric. nil and "" cells are skipped.
17
+ def self.write(path, sheets)
18
+ shared = []
19
+ worksheets = sheets.map { |name, rows| [name, worksheet_xml(rows, shared)] }
20
+
21
+ zip = Zip::File.new(path, create: true)
22
+ write_entry(zip, "[Content_Types].xml", content_types_xml(worksheets.size))
23
+ write_entry(zip, "_rels/.rels", root_rels_xml)
24
+ write_entry(zip, "xl/workbook.xml", workbook_xml(worksheets.map(&:first)))
25
+ write_entry(zip, "xl/_rels/workbook.xml.rels", workbook_rels_xml(worksheets.size))
26
+ write_entry(zip, "xl/sharedStrings.xml", shared_strings_xml(shared))
27
+ write_entry(zip, "xl/styles.xml", styles_xml)
28
+ worksheets.each_with_index do |(_name, xml), index|
29
+ write_entry(zip, "xl/worksheets/sheet#{index + 1}.xml", xml)
30
+ end
31
+ zip.close
32
+ path
33
+ end
34
+
35
+ def self.write_entry(zip, name, content)
36
+ zip.get_output_stream(name) { |stream| stream.write(content) }
37
+ end
38
+
39
+ def self.worksheet_xml(rows, shared)
40
+ row_elements = rows.each_with_index.map do |values, row_number|
41
+ cells = values.each_with_index.map do |value, column_index|
42
+ cell_xml(value, row_number + 1, column_index, shared)
43
+ end
44
+ "<row r=\"#{row_number + 1}\">#{cells.compact.join}</row>"
45
+ end
46
+
47
+ <<~XML
48
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
49
+ <worksheet xmlns="#{SHEET_NS}"><sheetData>#{row_elements.join}</sheetData></worksheet>
50
+ XML
51
+ end
52
+
53
+ def self.cell_xml(value, row_number, column_index, shared)
54
+ return if value.nil? || (value.is_a?(String) && value.empty?)
55
+
56
+ ref = "#{column_name(column_index)}#{row_number}"
57
+ return "<c r=\"#{ref}\"><v>#{value}</v></c>" if value.is_a?(Numeric)
58
+
59
+ text = value.to_s
60
+ index = shared.index(text)
61
+ if index.nil?
62
+ shared << text
63
+ index = shared.length - 1
64
+ end
65
+ "<c r=\"#{ref}\" t=\"s\"><v>#{index}</v></c>"
66
+ end
67
+
68
+ def self.column_name(index)
69
+ name = +""
70
+ remainder = index
71
+ loop do
72
+ name.prepend((remainder % 26 + 65).chr)
73
+ remainder = remainder / 26 - 1
74
+ break if remainder.negative?
75
+ end
76
+ name
77
+ end
78
+
79
+ def self.shared_strings_xml(shared)
80
+ entries = shared.map { |text| "<si><t>#{escape(text)}</t></si>" }
81
+ <<~XML
82
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
83
+ <sst xmlns="#{SHEET_NS}" count="#{shared.length}" uniqueCount="#{shared.length}">#{entries.join}</sst>
84
+ XML
85
+ end
86
+
87
+ def self.content_types_xml(sheet_count)
88
+ overrides = [
89
+ "<Override PartName=\"/xl/workbook.xml\" " \
90
+ "ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>",
91
+ "<Override PartName=\"/xl/sharedStrings.xml\" " \
92
+ "ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\"/>",
93
+ "<Override PartName=\"/xl/styles.xml\" " \
94
+ "ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>"
95
+ ]
96
+ (1..sheet_count).each do |index|
97
+ overrides << "<Override PartName=\"/xl/worksheets/sheet#{index}.xml\" " \
98
+ "ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>"
99
+ end
100
+ <<~XML
101
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
102
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>#{overrides.join}</Types>
103
+ XML
104
+ end
105
+
106
+ def self.root_rels_xml
107
+ <<~XML
108
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
109
+ <Relationships xmlns="#{RELS_NS}"><Relationship Id="rId1" Type="#{OFFICE_REL_NS}/officeDocument" Target="xl/workbook.xml"/></Relationships>
110
+ XML
111
+ end
112
+
113
+ def self.workbook_xml(sheet_names)
114
+ sheets = sheet_names.each_with_index.map do |name, index|
115
+ "<sheet name=\"#{escape(name)}\" sheetId=\"#{index + 1}\" r:id=\"rId#{index + 1}\"/>"
116
+ end
117
+ <<~XML
118
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
119
+ <workbook xmlns="#{SHEET_NS}" xmlns:r="#{OFFICE_REL_NS}"><sheets>#{sheets.join}</sheets></workbook>
120
+ XML
121
+ end
122
+
123
+ def self.workbook_rels_xml(sheet_count)
124
+ relationships = (1..sheet_count).map do |index|
125
+ "<Relationship Id=\"rId#{index}\" Type=\"#{OFFICE_REL_NS}/worksheet\" Target=\"worksheets/sheet#{index}.xml\"/>"
126
+ end
127
+ relationships << "<Relationship Id=\"rId#{sheet_count + 1}\" Type=\"#{OFFICE_REL_NS}/sharedStrings\" " \
128
+ "Target=\"sharedStrings.xml\"/>"
129
+ relationships << "<Relationship Id=\"rId#{sheet_count + 2}\" Type=\"#{OFFICE_REL_NS}/styles\" " \
130
+ "Target=\"styles.xml\"/>"
131
+ <<~XML
132
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
133
+ <Relationships xmlns="#{RELS_NS}">#{relationships.join}</Relationships>
134
+ XML
135
+ end
136
+
137
+ def self.styles_xml
138
+ <<~XML
139
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
140
+ <styleSheet xmlns="#{SHEET_NS}"><fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts><fills count="1"><fill><patternFill patternType="none"/></fill></fills><borders count="1"><border/></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs></styleSheet>
141
+ XML
142
+ end
143
+
144
+ def self.escape(text)
145
+ text.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
146
+ end
147
+ end
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.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-04 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '6.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: roo
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.10'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '2.10'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: thor
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,8 +108,8 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '1.0'
97
- description: Convert between ISO comment sheet DOCX and structured YAML with schema
98
- validation.
111
+ description: Convert between ISO comment sheets (DOCX and XLSX) and structured YAML
112
+ with schema validation.
99
113
  email:
100
114
  - open.source@ribose.com
101
115
  executables:
@@ -109,6 +123,7 @@ files:
109
123
  - ".rspec"
110
124
  - ".rubocop.yml"
111
125
  - ".rubocop_todo.yml"
126
+ - CLAUDE.md
112
127
  - CODE_OF_CONDUCT.md
113
128
  - Gemfile
114
129
  - README.adoc
@@ -127,14 +142,20 @@ files:
127
142
  - lib/commenter/filler.rb
128
143
  - lib/commenter/github_integration.rb
129
144
  - lib/commenter/parser.rb
145
+ - lib/commenter/parser/osd_xlsx_parser.rb
130
146
  - lib/commenter/version.rb
131
147
  - schema/iso_comment_2012-03.yaml
148
+ - schema/iso_comment_osd.yaml
132
149
  - sig/commenter.rbs
150
+ - spec/commenter/cli_spec.rb
133
151
  - spec/commenter/comment_sheet_spec.rb
134
152
  - spec/commenter/comment_spec.rb
135
153
  - spec/commenter/github_integration_spec.rb
154
+ - spec/commenter/osd_xlsx_parser_spec.rb
136
155
  - spec/commenter_spec.rb
137
156
  - spec/spec_helper.rb
157
+ - spec/support/osd_fixtures.rb
158
+ - spec/support/xlsx_builder.rb
138
159
  homepage: https://github.com/metanorma/commenter
139
160
  licenses:
140
161
  - BSD-2-Clause
@@ -160,5 +181,5 @@ requirements: []
160
181
  rubygems_version: 3.5.22
161
182
  signing_key:
162
183
  specification_version: 4
163
- summary: Library to work with ISO comment sheets in DOCX format.
184
+ summary: Library to work with ISO comment sheets in DOCX and XLSX formats.
164
185
  test_files: []