opencdd 0.1.1

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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +486 -0
  3. data/README.md +304 -0
  4. data/bin/lint-no-raw-mdc +41 -0
  5. data/lib/cdd.rb +11 -0
  6. data/lib/opencdd/alias_table.rb +52 -0
  7. data/lib/opencdd/cddal/ast.rb +151 -0
  8. data/lib/opencdd/cddal/builder.rb +374 -0
  9. data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
  10. data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
  11. data/lib/opencdd/cddal/fetcher.rb +18 -0
  12. data/lib/opencdd/cddal/generated_parser.rb +805 -0
  13. data/lib/opencdd/cddal/lexer.rb +193 -0
  14. data/lib/opencdd/cddal/parser.rb +19 -0
  15. data/lib/opencdd/cddal/resolver.rb +100 -0
  16. data/lib/opencdd/cddal/serializer.rb +210 -0
  17. data/lib/opencdd/cddal/value_serializer.rb +60 -0
  18. data/lib/opencdd/cddal.rb +63 -0
  19. data/lib/opencdd/class_tree.rb +80 -0
  20. data/lib/opencdd/class_type.rb +33 -0
  21. data/lib/opencdd/codegen/ts.rb +185 -0
  22. data/lib/opencdd/codegen.rb +7 -0
  23. data/lib/opencdd/composition_tree.rb +119 -0
  24. data/lib/opencdd/condition.rb +120 -0
  25. data/lib/opencdd/data_type.rb +143 -0
  26. data/lib/opencdd/database.rb +719 -0
  27. data/lib/opencdd/effective_properties.rb +119 -0
  28. data/lib/opencdd/entity/field_reader.rb +141 -0
  29. data/lib/opencdd/entity/field_registry.rb +99 -0
  30. data/lib/opencdd/entity/version_history.rb +62 -0
  31. data/lib/opencdd/entity.rb +255 -0
  32. data/lib/opencdd/exporters/json.rb +235 -0
  33. data/lib/opencdd/exporters/mermaid.rb +62 -0
  34. data/lib/opencdd/exporters/yaml.rb +16 -0
  35. data/lib/opencdd/exporters.rb +9 -0
  36. data/lib/opencdd/guid.rb +27 -0
  37. data/lib/opencdd/instance_rule.rb +70 -0
  38. data/lib/opencdd/irdi.rb +128 -0
  39. data/lib/opencdd/klass.rb +230 -0
  40. data/lib/opencdd/languages.rb +70 -0
  41. data/lib/opencdd/meta_class.rb +274 -0
  42. data/lib/opencdd/model/entity_store.rb +85 -0
  43. data/lib/opencdd/model/yaml_database.rb +49 -0
  44. data/lib/opencdd/model/yaml_entity.rb +196 -0
  45. data/lib/opencdd/model.rb +13 -0
  46. data/lib/opencdd/parcel/csv_reader.rb +35 -0
  47. data/lib/opencdd/parcel/csv_writer.rb +114 -0
  48. data/lib/opencdd/parcel/entity_manifest.rb +119 -0
  49. data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
  50. data/lib/opencdd/parcel/layout_detector.rb +115 -0
  51. data/lib/opencdd/parcel/metadata.rb +112 -0
  52. data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
  53. data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
  54. data/lib/opencdd/parcel/selector.rb +115 -0
  55. data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
  56. data/lib/opencdd/parcel/sheet.rb +287 -0
  57. data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
  58. data/lib/opencdd/parcel/sheet_schema.rb +253 -0
  59. data/lib/opencdd/parcel/workbook.rb +172 -0
  60. data/lib/opencdd/parcel/workbook_reader.rb +200 -0
  61. data/lib/opencdd/parcel/writer.rb +185 -0
  62. data/lib/opencdd/parcel.rb +175 -0
  63. data/lib/opencdd/parse_helpers.rb +73 -0
  64. data/lib/opencdd/property.rb +120 -0
  65. data/lib/opencdd/property_data_element_type.rb +44 -0
  66. data/lib/opencdd/property_ids.rb +202 -0
  67. data/lib/opencdd/reader.rb +103 -0
  68. data/lib/opencdd/relation.rb +88 -0
  69. data/lib/opencdd/relation_tree.rb +74 -0
  70. data/lib/opencdd/relation_type.rb +58 -0
  71. data/lib/opencdd/structured_values.rb +183 -0
  72. data/lib/opencdd/unit.rb +16 -0
  73. data/lib/opencdd/validator/class_reference_rule.rb +77 -0
  74. data/lib/opencdd/validator/condition_rule.rb +27 -0
  75. data/lib/opencdd/validator/data_type_rule.rb +26 -0
  76. data/lib/opencdd/validator/enum_rule.rb +27 -0
  77. data/lib/opencdd/validator/format_rule.rb +54 -0
  78. data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
  79. data/lib/opencdd/validator/irdi_rule.rb +57 -0
  80. data/lib/opencdd/validator/mandatory_rule.rb +25 -0
  81. data/lib/opencdd/validator/pattern_rule.rb +26 -0
  82. data/lib/opencdd/validator/reference_rule.rb +36 -0
  83. data/lib/opencdd/validator/rule.rb +65 -0
  84. data/lib/opencdd/validator/runner.rb +101 -0
  85. data/lib/opencdd/validator/set_rule.rb +41 -0
  86. data/lib/opencdd/validator/synonym_rule.rb +30 -0
  87. data/lib/opencdd/validator/type_rule.rb +102 -0
  88. data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
  89. data/lib/opencdd/validator.rb +68 -0
  90. data/lib/opencdd/value_format.rb +71 -0
  91. data/lib/opencdd/value_list.rb +42 -0
  92. data/lib/opencdd/value_term.rb +24 -0
  93. data/lib/opencdd/version.rb +5 -0
  94. data/lib/opencdd/view_control.rb +10 -0
  95. data/lib/opencdd/visitor.rb +93 -0
  96. data/lib/opencdd.rb +57 -0
  97. metadata +325 -0
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Opencdd
6
+ module Parcel
7
+ class Workbook
8
+ SheetMapEntry = Struct.new(:project_id, :parcel_id, :class_irdi, :content_no,
9
+ :sheet_no, :sheet_name, :type, :target, keyword_init: true)
10
+ ProjectInfo = Struct.new(:project_id, :parcel_id, :multi_language, :base_language,
11
+ keyword_init: true)
12
+
13
+ HEADER_ROW_NAMES = %i[
14
+ class_id class_name source_language translation_language
15
+ property_id property_name datatype value_format pattern
16
+ default_value requirement unit
17
+ ].freeze
18
+
19
+ MANDATORY_HEADER_ROWS = %i[class_id property_id].freeze
20
+
21
+ attr_reader :sheets, :sheetmap, :project, :source_path
22
+
23
+ def initialize(sheets:, sheetmap: [], project: nil, source_path: nil,
24
+ hidden_header_rows: nil)
25
+ @sheets = sheets
26
+ @sheetmap = sheetmap
27
+ @project = project
28
+ @source_path = source_path
29
+ @hidden_header_rows = validate_hidden(hidden_header_rows)
30
+ reindex
31
+ freeze
32
+ end
33
+
34
+ def hidden_header_rows
35
+ @hidden_header_rows.dup
36
+ end
37
+
38
+ def hide_header_row!(name)
39
+ sym = name.to_sym
40
+ raise ArgumentError, "unknown header row: #{sym.inspect}" unless HEADER_ROW_NAMES.include?(sym)
41
+ raise ArgumentError, "cannot hide mandatory header row: #{sym.inspect}" if MANDATORY_HEADER_ROWS.include?(sym)
42
+ @hidden_header_rows << sym
43
+ self
44
+ end
45
+
46
+ def show_header_row!(name)
47
+ @hidden_header_rows.delete(name.to_sym)
48
+ self
49
+ end
50
+
51
+ def register_sheet(sheet)
52
+ raise ArgumentError, "sheet already registered: #{sheet.name}" if @sheets_by_name.key?(sheet.name.to_s)
53
+ @sheets << sheet
54
+ @sheets_by_name[sheet.name.to_s] = sheet
55
+ @sheetmap << SheetMapEntry.new(
56
+ project_id: @project&.project_id || "LOCAL",
57
+ parcel_id: @project&.parcel_id,
58
+ class_irdi: sheet.meta_class_irdi,
59
+ content_no: 0,
60
+ sheet_no: @sheets.size,
61
+ sheet_name: sheet.name,
62
+ type: parcel_type_label_for(sheet.type),
63
+ target: "",
64
+ )
65
+ self
66
+ end
67
+
68
+ def import_into(sheet_name, file_path, format: nil)
69
+ target = sheet(sheet_name)
70
+ raise ArgumentError, "unknown sheet: #{sheet_name.inspect}" unless target
71
+
72
+ format ||= detect_format(file_path)
73
+ temp = read_external_sheet(file_path, format, target.meta_class_irdi)
74
+ unless meta_codes_match?(target.meta_class_irdi, temp.meta_class_irdi)
75
+ raise ArgumentError,
76
+ "meta-class mismatch: target=#{target.meta_class_irdi} source=#{temp.meta_class_irdi}"
77
+ end
78
+ target.merge_rows_from(temp)
79
+ self
80
+ end
81
+
82
+ def sheet(name)
83
+ @sheets_by_name[name.to_s]
84
+ end
85
+
86
+ def sheets_of_type(type)
87
+ type = type.to_sym
88
+ @sheets.select { |s| s.type == type }
89
+ end
90
+
91
+ def class_sheet ; sheets_of_type(:class).first ; end
92
+ def property_sheet ; sheets_of_type(:property).first ; end
93
+ def unit_sheet ; sheets_of_type(:unit).first ; end
94
+ def value_list_sheet ; sheets_of_type(:value_list).first ; end
95
+ def value_term_sheet ; sheets_of_type(:value_term).first ; end
96
+ def relation_sheet ; sheets_of_type(:relation).first ; end
97
+ def view_control_sheet; sheets_of_type(:view_control).first; end
98
+
99
+ def each_sheet(&block)
100
+ @sheets.each(&block)
101
+ end
102
+
103
+ def parcel_id
104
+ @project&.parcel_id
105
+ end
106
+
107
+ def project_id
108
+ @project&.project_id
109
+ end
110
+
111
+ def base_language
112
+ @project&.base_language || "en"
113
+ end
114
+
115
+ def merge(other)
116
+ merged_sheets = @sheets + other.sheets
117
+ merged_map = @sheetmap + other.sheetmap
118
+ merged_proj = @project || other.project
119
+ Workbook.new(sheets: merged_sheets, sheetmap: merged_map, project: merged_proj,
120
+ source_path: @source_path)
121
+ end
122
+
123
+ private
124
+
125
+ def validate_hidden(rows)
126
+ return Set.new if rows.nil?
127
+ set = rows.to_set
128
+ invalid = set - HEADER_ROW_NAMES.to_set
129
+ raise ArgumentError, "unknown header rows: #{invalid.to_a.inspect}" unless invalid.empty?
130
+ mandatory = set & MANDATORY_HEADER_ROWS.to_set
131
+ raise ArgumentError, "cannot hide mandatory rows: #{mandatory.to_a.inspect}" unless mandatory.empty?
132
+ set
133
+ end
134
+
135
+ def detect_format(path)
136
+ case File.extname(path.to_s).downcase
137
+ when ".csv" then :csv
138
+ when ".txt" then :csv_tab
139
+ when ".xlsx", ".xlsm" then :xlsx
140
+ when ".xls" then :xls
141
+ else raise ArgumentError, "cannot detect format for: #{path.inspect}"
142
+ end
143
+ end
144
+
145
+ def read_external_sheet(path, format, meta_class_irdi)
146
+ case format
147
+ when :csv then Opencdd::Parcel::CsvReader.read(path, meta_class_irdi: meta_class_irdi)
148
+ when :csv_tab then Opencdd::Parcel::CsvReader.read(path, meta_class_irdi: meta_class_irdi, col_sep: "\t")
149
+ when :xlsx, :xls
150
+ Opencdd::Parcel::WorkbookReader.new(path).read_workbook.sheets.first
151
+ else raise ArgumentError, "unknown format: #{format.inspect}"
152
+ end
153
+ end
154
+
155
+ def parcel_type_label_for(type)
156
+ return nil if type.nil?
157
+ Opencdd::MetaClasses.sheet_type_for_type(type) || type.to_s.upcase
158
+ end
159
+
160
+ def meta_codes_match?(a, b)
161
+ return false if a.nil? || b.nil?
162
+ a_code = a.to_s.split("#").last
163
+ b_code = b.to_s.split("#").last
164
+ a_code == b_code
165
+ end
166
+
167
+ def reindex
168
+ @sheets_by_name = @sheets.to_h { |s| [s.name.to_s, s] }
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Parcel
5
+ # Reads a single Parcel workbook file (.xlsx, .xlsm, .xltx, or
6
+ # legacy single-sheet .xls) and produces a +Opencdd::Parcel::Workbook+.
7
+ #
8
+ # This is the canonical IEC 62656-1 Parcel container: one file
9
+ # with Project / sheetmap / pcls_LOCAL / data sheets. Use
10
+ # +Opencdd::Parcel::FlatDirReader+ for the legacy 6-file-per-directory
11
+ # layout, and +Opencdd::Parcel::ShardedDirReader+ for the per-class
12
+ # sharded layout.
13
+ class WorkbookReader
14
+ LEGACY_TYPE_BY_PREFIX = {
15
+ "CLASS" => :class,
16
+ "PROPERTY" => :property,
17
+ "RELATION" => :relation,
18
+ "UNIT" => :unit,
19
+ "VALUELIST" => :value_list,
20
+ "VALUETERMS" => :value_term,
21
+ }.freeze
22
+
23
+ LEGACY_TYPE_TO_PARCEL_NAME = {
24
+ class: "CLASS",
25
+ property: "PROPERTY",
26
+ value_list: "ENUM",
27
+ value_term: "TERMINOLOGY",
28
+ unit: "UoM",
29
+ relation: "RELATION",
30
+ }.freeze
31
+
32
+ META_CLASS_BY_LEGACY_TYPE = Opencdd::MetaClasses::TYPE_BY_META_CLASS.invert.freeze
33
+
34
+ attr_reader :path
35
+
36
+ def initialize(path)
37
+ @path = path
38
+ end
39
+
40
+ def read_workbook
41
+ sheets = []
42
+ sheetmap = []
43
+ project = nil
44
+
45
+ workbook = open_workbook
46
+
47
+ if workbook.sheet_names.include?("sheetmap")
48
+ sheetmap = parse_sheetmap(workbook.rows_for("sheetmap"))
49
+ end
50
+
51
+ if workbook.sheet_names.include?("Project")
52
+ project = parse_project(workbook.rows_for("Project"))
53
+ end
54
+
55
+ workbook.sheet_names.each do |sheet_name|
56
+ next if ["Project", "sheetmap", "pcls_LOCAL"].include?(sheet_name)
57
+ sheet = Opencdd::Parcel::Sheet.from_rows(workbook.rows_for(sheet_name).each, name: sheet_name)
58
+ sheets << sheet if sheet
59
+ end
60
+
61
+ if sheets.empty? && legacy?
62
+ sheets = [Opencdd::Parcel::Sheet.from_rows(
63
+ workbook.rows_for(workbook.sheet_names.first).each,
64
+ name: File.basename(@path.to_s, ".*"),
65
+ )].compact
66
+ sheetmap = sheets.map { |s|
67
+ type = s.type
68
+ next unless type
69
+ Workbook::SheetMapEntry.new(
70
+ project_id: project&.project_id || "LOCAL",
71
+ parcel_id: project&.parcel_id,
72
+ class_irdi: Opencdd::IRDI.parse("0112/2///62656_1##{META_CLASS_BY_LEGACY_TYPE[type]}"),
73
+ content_no: 0,
74
+ sheet_no: nil,
75
+ sheet_name: s.name,
76
+ type: LEGACY_TYPE_TO_PARCEL_NAME[type] || type.to_s.upcase,
77
+ target: "",
78
+ )
79
+ }.compact
80
+ end
81
+
82
+ Opencdd::Parcel::Workbook.new(
83
+ sheets: sheets,
84
+ sheetmap: sheetmap,
85
+ project: project,
86
+ source_path: @path.to_s,
87
+ )
88
+ end
89
+
90
+ def load_into(database)
91
+ workbook = read_workbook
92
+ database.add_workbook(workbook)
93
+ database.finalize!
94
+ database
95
+ end
96
+
97
+ private
98
+
99
+ def open_workbook
100
+ ext = File.extname(@path.to_s).downcase
101
+ if ext == ".xls"
102
+ SpreadsheetSource.new(@path.to_s)
103
+ else
104
+ RooSource.new(@path.to_s, ext)
105
+ end
106
+ end
107
+
108
+ def legacy?
109
+ File.basename(@path.to_s) =~ /\Aexport_(CLASS|PROPERTY|RELATION|UNIT|VALUELIST|VALUETERMS)_.*\z/i
110
+ end
111
+
112
+ def parse_sheetmap(rows)
113
+ header = rows.first&.map { |c| c.to_s.strip.downcase } || []
114
+
115
+ rows[1..].map do |r|
116
+ vals = r.to_a
117
+ h = {}
118
+ header.each_with_index do |k, i|
119
+ next if k.nil? || k.empty?
120
+ v = vals[i]
121
+ h[k.to_sym] = v.nil? || v.to_s.strip.empty? ? nil : v.to_s.strip
122
+ end
123
+ next unless h[:sheetname]
124
+ Workbook::SheetMapEntry.new(
125
+ project_id: h[:projectid],
126
+ parcel_id: h[:parcelid],
127
+ class_irdi: h[:classid] && Opencdd::IRDI.parse(h[:classid]),
128
+ content_no: h[:contentno]&.to_i,
129
+ sheet_no: h[:sheetno]&.to_i,
130
+ sheet_name: h[:sheetname],
131
+ type: h[:type],
132
+ target: h[:target] || "",
133
+ )
134
+ end.compact
135
+ end
136
+
137
+ def parse_project(rows)
138
+ header = rows.first&.map(&:to_s) || []
139
+ vals = rows[1]&.to_a || []
140
+ h = header.each_with_index.to_h { |k, i| [k.to_s.strip, vals[i]] }
141
+ Workbook::ProjectInfo.new(
142
+ project_id: h["Project ID"].to_s,
143
+ parcel_id: h["Parcel ID"].to_s,
144
+ multi_language: h["Multi language"].to_s,
145
+ base_language: (h["Base language"].to_s.empty? ? "en" : h["Base language"].to_s),
146
+ )
147
+ end
148
+
149
+ class RooSource < Struct.new(:path, :extension)
150
+ def initialize(path, ext)
151
+ require "roo"
152
+ roo_ext =
153
+ case ext
154
+ when ".xlsx" then :xlsx
155
+ when ".xlsm" then :xlsm
156
+ when ".xls" then :xls
157
+ when ".csv" then :csv
158
+ else :xlsx
159
+ end
160
+ super(path, roo_ext)
161
+ @roo = Roo::Spreadsheet.open(path, extension: extension)
162
+ end
163
+
164
+ def sheet_names
165
+ @sheet_names ||= @roo.sheets
166
+ end
167
+
168
+ def rows_for(name)
169
+ sheet = @roo.sheet(name)
170
+ (1..sheet.last_row.to_i).map { |i| sheet.row(i) }
171
+ end
172
+ end
173
+
174
+ class SpreadsheetSource < Struct.new(:path)
175
+ def initialize(path)
176
+ require "spreadsheet"
177
+ super(path)
178
+ @book = Spreadsheet.open(path)
179
+ end
180
+
181
+ def sheet_names
182
+ @sheet_names ||= @book.worksheets.map { |ws| decode_name(ws.name) }
183
+ end
184
+
185
+ def rows_for(name)
186
+ ws = @book.worksheets.find { |w| decode_name(w.name) == name } || @book.worksheet(0)
187
+ ws.rows.map(&:to_a)
188
+ end
189
+
190
+ private
191
+
192
+ def decode_name(s)
193
+ s.to_s.encode("UTF-8", "UTF-16LE", invalid: :replace, undef: :replace).scrub("")
194
+ rescue Encoding::ConverterNotFoundError
195
+ s.to_s
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "set"
5
+ require "caxlsx"
6
+
7
+ module Opencdd
8
+ module Parcel
9
+ class Writer
10
+ DEFAULT_PROJECT_ID = "LOCAL"
11
+ DEFAULT_SOURCE_LANGUAGE = "en"
12
+
13
+ attr_reader :database
14
+
15
+ def initialize(database)
16
+ @database = database
17
+ end
18
+
19
+ def write(target, parcel_id:, project_id: DEFAULT_PROJECT_ID,
20
+ source_language: DEFAULT_SOURCE_LANGUAGE, translation_languages: [],
21
+ sheet_types: :all, selector: nil)
22
+ package = build_package(
23
+ parcel_id: parcel_id, project_id: project_id,
24
+ source_language: source_language, translation_languages: translation_languages,
25
+ sheet_types: sheet_types, selector: selector,
26
+ )
27
+ serialize(package, target)
28
+ target
29
+ end
30
+
31
+ def write_sheet(sheet, entities, target, source_language: DEFAULT_SOURCE_LANGUAGE)
32
+ emitter = SheetEmitter.new(source_language: source_language)
33
+ rows = emitter.emit(sheet, entities)
34
+ package = Axlsx::Package.new
35
+ package.workbook.add_worksheet(name: sheet.name) do |ws|
36
+ rows.each { |row| ws.add_row(row) }
37
+ end
38
+ serialize(package, target)
39
+ target
40
+ end
41
+
42
+ private
43
+
44
+ def build_package(parcel_id:, project_id:, source_language:, translation_languages:, sheet_types:, selector: nil)
45
+ package = Axlsx::Package.new
46
+ workbook = package.workbook
47
+
48
+ effective_entities = selector ? selector.resolve(@database) : nil
49
+
50
+ built_sheets = build_sheets(
51
+ parcel_id: parcel_id, source_language: source_language,
52
+ translation_languages: translation_languages, sheet_types: sheet_types,
53
+ effective_entities: effective_entities,
54
+ )
55
+ hidden_directives = hidden_header_rows_from_database
56
+
57
+ add_project_sheet(workbook, parcel_id, project_id, source_language, translation_languages)
58
+ add_sheetmap_sheet(workbook, built_sheets, parcel_id, project_id)
59
+ add_data_sheets(workbook, built_sheets, source_language, hidden_directives)
60
+
61
+ package
62
+ end
63
+
64
+ ALL_SHEET_TYPES = %i[class property value_list value_term unit relation view_control].freeze
65
+
66
+ def resolve_types(sheet_types)
67
+ return ALL_SHEET_TYPES if sheet_types == :all
68
+ Array(sheet_types).map(&:to_sym)
69
+ end
70
+
71
+ def build_sheets(parcel_id:, source_language:, translation_languages:, sheet_types:, effective_entities: nil)
72
+ types = resolve_types(sheet_types)
73
+ if @database.workbooks.any?
74
+ build_sheets_from_workbook(types, parcel_id, source_language, translation_languages, effective_entities)
75
+ else
76
+ build_scaffolds(types, parcel_id, source_language, translation_languages, effective_entities)
77
+ end
78
+ end
79
+
80
+ def build_sheets_from_workbook(types, parcel_id, source_language, translation_languages, effective_entities)
81
+ source_workbook = @database.workbooks.first
82
+ picked = source_workbook.sheets.select { |s| s.type && types.include?(s.type) }
83
+ picked.group_by(&:type).flat_map do |_type, sheets|
84
+ primary = sheets.first
85
+ entities = filtered_entities_of_type(primary.type, effective_entities)
86
+ [BuiltSheet.new(sheet: primary, entities: entities, type: primary.type)]
87
+ end
88
+ end
89
+
90
+ def build_scaffolds(types, parcel_id, source_language, translation_languages, effective_entities)
91
+ types.filter_map do |type|
92
+ meta_code = Opencdd::MetaClasses.meta_class_for_type(type)
93
+ next nil unless meta_code
94
+ sheet = Sheet.scaffold(
95
+ meta_class_irdi: meta_code,
96
+ parcel_id: parcel_id,
97
+ source_language: source_language,
98
+ translation_languages: translation_languages,
99
+ )
100
+ entities = filtered_entities_of_type(type, effective_entities)
101
+ BuiltSheet.new(sheet: sheet, entities: entities, type: type)
102
+ end
103
+ end
104
+
105
+ def filtered_entities_of_type(type, effective_entities)
106
+ all = @database.entities_of_type(type)
107
+ return all if effective_entities.nil?
108
+ allowed = effective_entities.select { |e| e.type == type }.to_set
109
+ all.select { |e| allowed.include?(e) }
110
+ end
111
+
112
+ def add_project_sheet(workbook, parcel_id, project_id, source_language, translation_languages)
113
+ workbook.add_worksheet(name: "Project") do |ws|
114
+ ws.add_row(["Project ID", "Parcel ID", "Multi language", "Base language"])
115
+ multi = translation_languages.empty? ? "" : translation_languages.join(",")
116
+ ws.add_row([project_id, parcel_id, multi, source_language])
117
+ end
118
+ end
119
+
120
+ def add_sheetmap_sheet(workbook, built_sheets, parcel_id, project_id)
121
+ workbook.add_worksheet(name: "sheetmap") do |ws|
122
+ ws.add_row(%w[projectid parcelid classid contentno sheetno sheetname type target])
123
+ ws.add_row([project_id, "", "", 0, 2, "pcls_LOCAL", "PARCEL_LIST", ""])
124
+ built_sheets.each_with_index do |built, idx|
125
+ sheet = built.sheet
126
+ ws.add_row([
127
+ project_id,
128
+ parcel_id,
129
+ "#{sheet.meta_class_irdi&.to_s}##1",
130
+ 0,
131
+ idx + 3,
132
+ sheet.name,
133
+ parcel_type_label(built.type),
134
+ "",
135
+ ])
136
+ end
137
+ end
138
+ end
139
+
140
+ def parcel_type_label(type)
141
+ Opencdd::MetaClasses.sheet_type_for_type(type) || type.to_s.upcase
142
+ end
143
+
144
+ def add_data_sheets(workbook, built_sheets, source_language, hidden_directives = Set.new)
145
+ emitter = SheetEmitter.new(source_language: source_language)
146
+ built_sheets.each do |built|
147
+ rows = emitter.emit(built.sheet, built.entities)
148
+ workbook.add_worksheet(name: built.sheet.name) do |ws|
149
+ rows.each do |row|
150
+ emitted = ws.add_row(row)
151
+ emitted.hidden = true if row_hidden?(row, hidden_directives)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def hidden_header_rows_from_database
158
+ wb = @database.workbooks.first
159
+ return Set.new unless wb
160
+ wb.hidden_header_rows
161
+ end
162
+
163
+ def row_hidden?(row, hidden_directives)
164
+ return false if hidden_directives.empty?
165
+ first = row.first.to_s
166
+ return false unless first.start_with?("#")
167
+ base = first.sub(/\A#/, "").split(/[:=.]/).first
168
+ return false if base.nil? || base.empty?
169
+ hidden_directives.include?(base.downcase.to_sym)
170
+ end
171
+
172
+ def serialize(package, target)
173
+ case target
174
+ when String then package.serialize(target)
175
+ when Pathname then package.serialize(target.to_s)
176
+ when IO, StringIO then target.write(package.to_stream.read)
177
+ else
178
+ raise ArgumentError, "unsupported write target: #{target.inspect}"
179
+ end
180
+ end
181
+
182
+ BuiltSheet = Struct.new(:sheet, :entities, :type, keyword_init: true)
183
+ end
184
+ end
185
+ end