berkeley_library-tind 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (162) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +20 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/tind.iml +138 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.rubocop.yml +334 -0
  10. data/.ruby-version +1 -0
  11. data/.simplecov +8 -0
  12. data/.yardopts +1 -0
  13. data/CHANGES.md +58 -0
  14. data/Dockerfile +57 -0
  15. data/Gemfile +3 -0
  16. data/Jenkinsfile +18 -0
  17. data/LICENSE.md +21 -0
  18. data/README.md +73 -0
  19. data/Rakefile +20 -0
  20. data/berkeley_library-tind.gemspec +50 -0
  21. data/bin/tind-export +14 -0
  22. data/docker-compose.yml +15 -0
  23. data/lib/berkeley_library/tind.rb +3 -0
  24. data/lib/berkeley_library/tind/api.rb +1 -0
  25. data/lib/berkeley_library/tind/api/api.rb +132 -0
  26. data/lib/berkeley_library/tind/api/api_exception.rb +131 -0
  27. data/lib/berkeley_library/tind/api/collection.rb +82 -0
  28. data/lib/berkeley_library/tind/api/date_range.rb +67 -0
  29. data/lib/berkeley_library/tind/api/format.rb +32 -0
  30. data/lib/berkeley_library/tind/api/search.rb +100 -0
  31. data/lib/berkeley_library/tind/config.rb +103 -0
  32. data/lib/berkeley_library/tind/export.rb +1 -0
  33. data/lib/berkeley_library/tind/export/column.rb +54 -0
  34. data/lib/berkeley_library/tind/export/column_group.rb +144 -0
  35. data/lib/berkeley_library/tind/export/column_group_list.rb +131 -0
  36. data/lib/berkeley_library/tind/export/column_width_calculator.rb +76 -0
  37. data/lib/berkeley_library/tind/export/config.rb +154 -0
  38. data/lib/berkeley_library/tind/export/csv_exporter.rb +29 -0
  39. data/lib/berkeley_library/tind/export/export.rb +47 -0
  40. data/lib/berkeley_library/tind/export/export_command.rb +168 -0
  41. data/lib/berkeley_library/tind/export/export_exception.rb +8 -0
  42. data/lib/berkeley_library/tind/export/export_format.rb +67 -0
  43. data/lib/berkeley_library/tind/export/exporter.rb +105 -0
  44. data/lib/berkeley_library/tind/export/filter.rb +52 -0
  45. data/lib/berkeley_library/tind/export/no_results_error.rb +7 -0
  46. data/lib/berkeley_library/tind/export/ods_exporter.rb +138 -0
  47. data/lib/berkeley_library/tind/export/row.rb +24 -0
  48. data/lib/berkeley_library/tind/export/row_metrics.rb +18 -0
  49. data/lib/berkeley_library/tind/export/table.rb +175 -0
  50. data/lib/berkeley_library/tind/export/table_metrics.rb +116 -0
  51. data/lib/berkeley_library/tind/marc.rb +1 -0
  52. data/lib/berkeley_library/tind/marc/xml_reader.rb +144 -0
  53. data/lib/berkeley_library/tind/module_info.rb +14 -0
  54. data/lib/berkeley_library/util/arrays.rb +178 -0
  55. data/lib/berkeley_library/util/logging.rb +1 -0
  56. data/lib/berkeley_library/util/ods/spreadsheet.rb +170 -0
  57. data/lib/berkeley_library/util/ods/xml/content_doc.rb +26 -0
  58. data/lib/berkeley_library/util/ods/xml/document_node.rb +57 -0
  59. data/lib/berkeley_library/util/ods/xml/element_node.rb +106 -0
  60. data/lib/berkeley_library/util/ods/xml/loext/table_protection.rb +26 -0
  61. data/lib/berkeley_library/util/ods/xml/manifest/file_entry.rb +42 -0
  62. data/lib/berkeley_library/util/ods/xml/manifest/manifest.rb +73 -0
  63. data/lib/berkeley_library/util/ods/xml/manifest_doc.rb +26 -0
  64. data/lib/berkeley_library/util/ods/xml/namespace.rb +46 -0
  65. data/lib/berkeley_library/util/ods/xml/office/automatic_styles.rb +181 -0
  66. data/lib/berkeley_library/util/ods/xml/office/body.rb +17 -0
  67. data/lib/berkeley_library/util/ods/xml/office/document_content.rb +98 -0
  68. data/lib/berkeley_library/util/ods/xml/office/document_styles.rb +39 -0
  69. data/lib/berkeley_library/util/ods/xml/office/font_face_decls.rb +30 -0
  70. data/lib/berkeley_library/util/ods/xml/office/scripts.rb +17 -0
  71. data/lib/berkeley_library/util/ods/xml/office/spreadsheet.rb +37 -0
  72. data/lib/berkeley_library/util/ods/xml/office/styles.rb +39 -0
  73. data/lib/berkeley_library/util/ods/xml/style/cell_style.rb +58 -0
  74. data/lib/berkeley_library/util/ods/xml/style/column_style.rb +36 -0
  75. data/lib/berkeley_library/util/ods/xml/style/default_style.rb +31 -0
  76. data/lib/berkeley_library/util/ods/xml/style/family.rb +85 -0
  77. data/lib/berkeley_library/util/ods/xml/style/font_face.rb +46 -0
  78. data/lib/berkeley_library/util/ods/xml/style/paragraph_properties.rb +30 -0
  79. data/lib/berkeley_library/util/ods/xml/style/row_style.rb +37 -0
  80. data/lib/berkeley_library/util/ods/xml/style/style.rb +44 -0
  81. data/lib/berkeley_library/util/ods/xml/style/table_cell_properties.rb +40 -0
  82. data/lib/berkeley_library/util/ods/xml/style/table_column_properties.rb +30 -0
  83. data/lib/berkeley_library/util/ods/xml/style/table_properties.rb +25 -0
  84. data/lib/berkeley_library/util/ods/xml/style/table_row_properties.rb +28 -0
  85. data/lib/berkeley_library/util/ods/xml/style/table_style.rb +27 -0
  86. data/lib/berkeley_library/util/ods/xml/style/text_properties.rb +52 -0
  87. data/lib/berkeley_library/util/ods/xml/styles_doc.rb +26 -0
  88. data/lib/berkeley_library/util/ods/xml/table/named_expressions.rb +17 -0
  89. data/lib/berkeley_library/util/ods/xml/table/repeatable.rb +38 -0
  90. data/lib/berkeley_library/util/ods/xml/table/table.rb +193 -0
  91. data/lib/berkeley_library/util/ods/xml/table/table_cell.rb +46 -0
  92. data/lib/berkeley_library/util/ods/xml/table/table_column.rb +43 -0
  93. data/lib/berkeley_library/util/ods/xml/table/table_row.rb +136 -0
  94. data/lib/berkeley_library/util/ods/xml/text/p.rb +118 -0
  95. data/lib/berkeley_library/util/paths.rb +111 -0
  96. data/lib/berkeley_library/util/stringios.rb +30 -0
  97. data/lib/berkeley_library/util/strings.rb +42 -0
  98. data/lib/berkeley_library/util/sys_exits.rb +15 -0
  99. data/lib/berkeley_library/util/times.rb +22 -0
  100. data/lib/berkeley_library/util/uris.rb +44 -0
  101. data/lib/berkeley_library/util/uris/appender.rb +162 -0
  102. data/lib/berkeley_library/util/uris/requester.rb +62 -0
  103. data/lib/berkeley_library/util/uris/validator.rb +32 -0
  104. data/rakelib/bundle.rake +8 -0
  105. data/rakelib/coverage.rake +11 -0
  106. data/rakelib/gem.rake +54 -0
  107. data/rakelib/rubocop.rake +18 -0
  108. data/rakelib/spec.rake +2 -0
  109. data/spec/.rubocop.yml +40 -0
  110. data/spec/berkeley_library/tind/api/api_exception_spec.rb +91 -0
  111. data/spec/berkeley_library/tind/api/api_spec.rb +143 -0
  112. data/spec/berkeley_library/tind/api/collection_spec.rb +74 -0
  113. data/spec/berkeley_library/tind/api/date_range_spec.rb +110 -0
  114. data/spec/berkeley_library/tind/api/format_spec.rb +54 -0
  115. data/spec/berkeley_library/tind/api/search_spec.rb +364 -0
  116. data/spec/berkeley_library/tind/config_spec.rb +86 -0
  117. data/spec/berkeley_library/tind/export/column_group_spec.rb +29 -0
  118. data/spec/berkeley_library/tind/export/column_spec.rb +43 -0
  119. data/spec/berkeley_library/tind/export/config_spec.rb +206 -0
  120. data/spec/berkeley_library/tind/export/export_command_spec.rb +169 -0
  121. data/spec/berkeley_library/tind/export/export_format_spec.rb +59 -0
  122. data/spec/berkeley_library/tind/export/export_matcher.rb +112 -0
  123. data/spec/berkeley_library/tind/export/export_spec.rb +150 -0
  124. data/spec/berkeley_library/tind/export/exporter_spec.rb +125 -0
  125. data/spec/berkeley_library/tind/export/row_spec.rb +118 -0
  126. data/spec/berkeley_library/tind/export/table_spec.rb +322 -0
  127. data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +93 -0
  128. data/spec/berkeley_library/util/arrays_spec.rb +340 -0
  129. data/spec/berkeley_library/util/ods/spreadsheet_spec.rb +124 -0
  130. data/spec/berkeley_library/util/ods/xml/content_doc_spec.rb +121 -0
  131. data/spec/berkeley_library/util/ods/xml/manifest/file_entry_spec.rb +27 -0
  132. data/spec/berkeley_library/util/ods/xml/manifest/manifest_spec.rb +33 -0
  133. data/spec/berkeley_library/util/ods/xml/office/document_content_spec.rb +60 -0
  134. data/spec/berkeley_library/util/ods/xml/style/automatic_styles_spec.rb +37 -0
  135. data/spec/berkeley_library/util/ods/xml/style/family_spec.rb +57 -0
  136. data/spec/berkeley_library/util/ods/xml/table/table_row_spec.rb +179 -0
  137. data/spec/berkeley_library/util/ods/xml/table/table_spec.rb +218 -0
  138. data/spec/berkeley_library/util/paths_spec.rb +90 -0
  139. data/spec/berkeley_library/util/stringios_spec.rb +34 -0
  140. data/spec/berkeley_library/util/strings_spec.rb +27 -0
  141. data/spec/berkeley_library/util/times_spec.rb +39 -0
  142. data/spec/berkeley_library/util/uris_spec.rb +118 -0
  143. data/spec/data/collection-names.txt +438 -0
  144. data/spec/data/collections.json +4827 -0
  145. data/spec/data/disjoint-records.xml +187 -0
  146. data/spec/data/record-184453.xml +58 -0
  147. data/spec/data/record-184458.xml +63 -0
  148. data/spec/data/record-187888.xml +78 -0
  149. data/spec/data/records-api-search-cjk-p1.xml +6381 -0
  150. data/spec/data/records-api-search-cjk-p2.xml +5 -0
  151. data/spec/data/records-api-search-p1.xml +4506 -0
  152. data/spec/data/records-api-search-p2.xml +4509 -0
  153. data/spec/data/records-api-search-p3.xml +4506 -0
  154. data/spec/data/records-api-search-p4.xml +4509 -0
  155. data/spec/data/records-api-search-p5.xml +4506 -0
  156. data/spec/data/records-api-search-p6.xml +2436 -0
  157. data/spec/data/records-api-search-p7.xml +5 -0
  158. data/spec/data/records-api-search.xml +234 -0
  159. data/spec/data/records-manual-search.xml +547 -0
  160. data/spec/spec_helper.rb +30 -0
  161. data/test/profile/table_from_records_profile.rb +46 -0
  162. metadata +585 -0
@@ -0,0 +1,193 @@
1
+ require 'berkeley_library/util/ods/xml/element_node'
2
+ require 'berkeley_library/util/ods/xml/loext/table_protection'
3
+ require 'berkeley_library/util/ods/xml/style/column_style'
4
+ require 'berkeley_library/util/ods/xml/table/table_column'
5
+ require 'berkeley_library/util/ods/xml/table/table_row'
6
+
7
+ module BerkeleyLibrary
8
+ module Util
9
+ module ODS
10
+ module XML
11
+ module Table
12
+ class Table < XML::ElementNode
13
+ # ------------------------------------------------------------
14
+ # Constants
15
+
16
+ MIN_COLUMNS = 1024
17
+ MIN_ROWS = 1_048_576
18
+
19
+ # ------------------------------------------------------------
20
+ # Accessors
21
+
22
+ attr_reader :table_name
23
+
24
+ attr_reader :table_style
25
+
26
+ # @return [XML::Office::AutomaticStyles] the document styles
27
+ attr_reader :styles
28
+
29
+ # ------------------------------------------------------------
30
+ # Initializers
31
+
32
+ # Initializes a new table
33
+ #
34
+ # @param name [String] the table name
35
+ # @param style [XML::Style::TableStyle] the table style, if other than default
36
+ # @param styles [XML::Office::AutomaticStyles] the document styles
37
+ # @param protected [Boolean] whether the table is protected
38
+ def initialize(table_name, table_style = nil, styles:, protected: true)
39
+ super(:table, 'table', doc: styles.doc)
40
+
41
+ @table_name = table_name
42
+ @table_style = table_style || styles.default_style(:table)
43
+ @styles = styles
44
+
45
+ set_attribute('name', self.table_name)
46
+ set_attribute('style-name', self.table_style.style_name)
47
+
48
+ protect! if protected
49
+ end
50
+
51
+ # ------------------------------------------------------------
52
+ # Accessors and utility methods
53
+
54
+ def column_count
55
+ @column_count ||= 0
56
+ end
57
+
58
+ def row_count
59
+ @row_count ||= 0
60
+ end
61
+
62
+ def get_value_at(row_index, column_index)
63
+ (row = rows[row_index]) && row.get_value_at(column_index)
64
+ end
65
+
66
+ def add_column(header, width = nil, protected: false)
67
+ add_column_with_styles(
68
+ header,
69
+ column_style: styles.find_or_create_column_style(width),
70
+ default_cell_style: styles.find_or_create_cell_style(protected)
71
+ )
72
+ end
73
+
74
+ def add_column_with_styles(header, column_style:, default_cell_style: nil, header_cell_style: nil)
75
+ cell_style = default_cell_style || styles.find_or_create_cell_style
76
+ add_or_repeat_column(column_style, cell_style).tap do
77
+ header_row = rows[0] || add_row
78
+ header_row.set_value_at(column_count, header, header_cell_style)
79
+ self.column_count += 1
80
+ end
81
+ end
82
+
83
+ def add_empty_columns(number_repeated, width = nil, protected: false)
84
+ column_style = styles.find_or_create_column_style(width)
85
+ default_cell_style = styles.find_or_create_cell_style(protected)
86
+
87
+ TableColumn.new(column_style, default_cell_style, number_repeated, table: self).tap do |col|
88
+ columns << col
89
+ self.column_count += number_repeated
90
+ end
91
+ end
92
+
93
+ # Adds a new row with the specified height.
94
+ # @param height [String] the row height. Defaults to {XML::Style::RowStyle::DEFAULT_HEIGHT}.
95
+ # @param number_repeated [Integer] the number of identical rows to repeat
96
+ # @return [TableRow] the new row
97
+ def add_row(height = nil, number_repeated = 1)
98
+ row_style = styles.find_or_create_row_style(height)
99
+ TableRow.new(row_style, number_repeated, table: self).tap do |row|
100
+ rows << row
101
+ self.row_count += number_repeated
102
+ end
103
+ end
104
+
105
+ # ------------------------------------------------------------
106
+ # Public XML::ElementNode overrides
107
+
108
+ def add_child(child)
109
+ return child.tap { |column| columns << column } if child.is_a?(TableColumn)
110
+ return child.tap { |row| rows << row } if child.is_a?(TableRow)
111
+
112
+ child.tap { |c| other_children << c }
113
+ end
114
+
115
+ # ------------------------------------------------------------
116
+ # Protected methods
117
+
118
+ protected
119
+
120
+ # ----------------------------------------
121
+ # Protected XML::ElementNode overrides
122
+
123
+ def children
124
+ [other_children, columns, rows].flatten
125
+ end
126
+
127
+ def create_element
128
+ ensure_empty_columns!
129
+ ensure_empty_rows!
130
+
131
+ super
132
+ end
133
+
134
+ # ------------------------------------------------------------
135
+ # Private methods
136
+
137
+ private
138
+
139
+ # ------------------------------
140
+ # Private writers
141
+
142
+ attr_writer :column_count
143
+
144
+ attr_writer :row_count
145
+
146
+ # ------------------------------
147
+ # Private readers
148
+
149
+ def columns
150
+ @columns ||= []
151
+ end
152
+
153
+ def rows
154
+ @rows ||= []
155
+ end
156
+
157
+ def other_children
158
+ @other_children ||= []
159
+ end
160
+
161
+ # ------------------------------
162
+ # Private utility methods
163
+
164
+ def protect!
165
+ set_attribute('protected', 'true')
166
+ add_child(LOExt::TableProtection.new(doc: doc))
167
+ end
168
+
169
+ def add_or_repeat_column(column_style, default_cell_style)
170
+ if (last_column = columns.last).nil? || !last_column.has_styles?(column_style, default_cell_style)
171
+ TableColumn.new(column_style, default_cell_style, table: self).tap { |c| columns << c }
172
+ else
173
+ last_column.tap(&:increment_repeats!)
174
+ end
175
+ end
176
+
177
+ # TODO: do we really need to use the LibreOffice default width/height?
178
+
179
+ def ensure_empty_columns!
180
+ empty_required = MIN_COLUMNS - column_count
181
+ add_empty_columns(empty_required, '0.889in') if empty_required > 0
182
+ end
183
+
184
+ def ensure_empty_rows!
185
+ empty_required = MIN_ROWS - row_count
186
+ add_row('0.178in', empty_required) if empty_required > 0
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,46 @@
1
+ require 'berkeley_library/util/ods/xml/table/repeatable'
2
+ require 'berkeley_library/util/ods/xml/text/p'
3
+
4
+ module BerkeleyLibrary
5
+ module Util
6
+ module ODS
7
+ module XML
8
+ module Table
9
+ class TableCell < Repeatable
10
+ attr_reader :value
11
+ attr_reader :cell_style
12
+
13
+ def initialize(value = nil, cell_style = nil, number_repeated = 1, table:)
14
+ super('table-cell', 'number-columns-repeated', number_repeated, table: table)
15
+
16
+ @value = value
17
+ @cell_style = cell_style
18
+
19
+ set_default_attributes!
20
+ add_default_children!
21
+ end
22
+
23
+ class << self
24
+ def repeat_empty(number_repeated, cell_style = nil, table:)
25
+ TableCell.new(nil, cell_style, number_repeated, table: table)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def set_default_attributes!
32
+ set_attribute('style-name', cell_style.style_name) if cell_style
33
+ set_attribute(:office, 'value-type', 'string') if value
34
+ set_attribute(:calcext, 'value-type', 'string') if value
35
+ end
36
+
37
+ def add_default_children!
38
+ children << XML::Text::P.new(value, doc: doc) if value
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ require 'berkeley_library/util/ods/xml/element_node'
2
+ require 'berkeley_library/util/ods/xml/table/repeatable'
3
+
4
+ module BerkeleyLibrary
5
+ module Util
6
+ module ODS
7
+ module XML
8
+ module Table
9
+ class TableColumn < Repeatable
10
+
11
+ attr_reader :column_style
12
+ attr_reader :default_cell_style
13
+
14
+ # Initializes a new column
15
+ #
16
+ # @param column_style [XML::Style::ColumnStyle] the column style
17
+ # @param default_cell_style [XML::Style::CellStyle] the default cell style for this column
18
+ def initialize(column_style, default_cell_style, number_repeated = 1, table:)
19
+ super('table-column', 'number-columns-repeated', number_repeated, table: table)
20
+ @column_style = column_style
21
+ @default_cell_style = default_cell_style
22
+
23
+ set_default_attributes!
24
+ end
25
+
26
+ # rubocop:disable Naming/PredicateName
27
+ def has_styles?(column_style, default_cell_style)
28
+ self.column_style == column_style && self.default_cell_style == default_cell_style
29
+ end
30
+ # rubocop:enable Naming/PredicateName
31
+
32
+ private
33
+
34
+ def set_default_attributes!
35
+ set_attribute('style-name', column_style.style_name)
36
+ set_attribute('default-cell-style-name', default_cell_style.style_name)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,136 @@
1
+ require 'berkeley_library/util/arrays'
2
+ require 'berkeley_library/util/ods/xml/element_node'
3
+ require 'berkeley_library/util/ods/xml/table/repeatable'
4
+ require 'berkeley_library/util/ods/xml/table/table_cell'
5
+
6
+ module BerkeleyLibrary
7
+ module Util
8
+ module ODS
9
+ module XML
10
+ module Table
11
+ class TableRow < Repeatable
12
+
13
+ # ------------------------------------------------------------
14
+ # Accessors
15
+
16
+ attr_reader :row_style
17
+ attr_reader :default_cell_style
18
+
19
+ # ------------------------------------------------------------
20
+ # Initializer
21
+
22
+ # @param table [Table] the table
23
+ # @param default_cell_style [Style::CellStyle] the default cell style
24
+ def initialize(row_style, number_repeated = 1, table:, default_cell_style: nil)
25
+ super('table-row', 'number-rows-repeated', number_repeated, table: table)
26
+ @row_style = row_style
27
+ @default_cell_style = default_cell_style
28
+
29
+ set_default_attributes!
30
+ end
31
+
32
+ # ------------------------------------------------------------
33
+ # Public utility methods
34
+
35
+ def set_value_at(column_index, value = nil, cell_style = nil)
36
+ explicit_cells[column_index] = TableCell.new(value, cell_style || default_cell_style, table: table)
37
+ end
38
+
39
+ def get_value_at(column_index)
40
+ (cell = explicit_cells[column_index]) && cell.value
41
+ end
42
+
43
+ # ------------------------------------------------------------
44
+ # Public XML::ElementNode overrides
45
+
46
+ def add_child(child)
47
+ return add_table_cell(child) if child.is_a?(TableCell)
48
+
49
+ child.tap { |c| other_children << c }
50
+ end
51
+
52
+ # ------------------------------------------------------------
53
+ # Protected methods
54
+
55
+ protected
56
+
57
+ # ----------------------------------------
58
+ # Protected utility methods
59
+
60
+ def add_table_cell(cell)
61
+ return cell.tap { |c| explicit_cells << c } if explicit_cell_count < table.column_count
62
+
63
+ raise ArgumentError, "Can't add cell at column index #{explicit_cell_count} to table with only #{table.column_count} columns"
64
+ end
65
+
66
+ # ----------------------------------------
67
+ # Protected XML::ElementNode overrides
68
+
69
+ def children
70
+ [].tap do |cc|
71
+ each_cell { |c| cc << c }
72
+ cc.concat(other_children)
73
+ end
74
+ end
75
+
76
+ # ------------------------------------------------------------
77
+ # Private methods
78
+
79
+ private
80
+
81
+ def column_count_actual
82
+ [table.column_count, Table::MIN_COLUMNS].max
83
+ end
84
+
85
+ def explicit_cells
86
+ @explicit_cells ||= []
87
+ end
88
+
89
+ def set_default_attributes!
90
+ set_attribute('style-name', row_style.style_name)
91
+ end
92
+
93
+ def other_children
94
+ @other_children ||= []
95
+ end
96
+
97
+ def explicit_cell_count
98
+ explicit_cells.size
99
+ end
100
+
101
+ def each_cell(columns_yielded = 0, remaining = explicit_cells, &block)
102
+ columns_yielded, remaining = yield_while_non_nil(columns_yielded, remaining, &block)
103
+ columns_yielded, remaining = yield_while_nil(columns_yielded, remaining, &block)
104
+ each_cell(columns_yielded, remaining, &block) unless remaining.empty?
105
+ end
106
+
107
+ def yield_while_non_nil(columns_yielded, remaining, &block)
108
+ non_nil_cells = remaining.take_while { |c| !c.nil? }
109
+ non_nil_cells.each(&block)
110
+ non_nil_cell_count = non_nil_cells.size
111
+
112
+ [columns_yielded + non_nil_cell_count, remaining[non_nil_cell_count..]]
113
+ end
114
+
115
+ def yield_while_nil(columns_yielded, remaining, &block)
116
+ nil_cell_count = Arrays.count_while(values: remaining, &:nil?)
117
+ remaining = remaining[nil_cell_count..]
118
+
119
+ empty_required = remaining.empty? ? (column_count_actual - columns_yielded) : nil_cell_count
120
+ yield_repeat_empty(empty_required, &block)
121
+ columns_yielded += empty_required
122
+
123
+ [columns_yielded, remaining]
124
+ end
125
+
126
+ def yield_repeat_empty(num_repeats, &block)
127
+ empty_cell = TableCell.repeat_empty(num_repeats, default_cell_style, table: table)
128
+ block.call(empty_cell)
129
+ end
130
+
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,118 @@
1
+ require 'berkeley_library/util/ods/xml/element_node'
2
+
3
+ module BerkeleyLibrary
4
+ module Util
5
+ module ODS
6
+ module XML
7
+ module Text
8
+ class P < XML::ElementNode
9
+
10
+ # ------------------------------------------------------------
11
+ # Constant
12
+
13
+ ESCAPABLE = ["\t", "\n", ' '].freeze
14
+
15
+ # ------------------------------------------------------------
16
+ # Accessors
17
+
18
+ attr_reader :text
19
+
20
+ # ------------------------------------------------------------
21
+ # Initializer
22
+
23
+ def initialize(text, doc:)
24
+ super(:text, 'p', doc: doc)
25
+
26
+ @text = text
27
+ add_default_children!
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # Private methods
32
+
33
+ private
34
+
35
+ def add_default_children!
36
+ each_child_element_or_string { |c| children << c }
37
+ end
38
+
39
+ def each_child_element_or_string(last_char = nil, text_remaining = text, &block)
40
+ last_char, text_remaining = yield_while_escaped(last_char, text_remaining, &block)
41
+ last_char, text_remaining = yield_while_unescaped(last_char, text_remaining, &block)
42
+ each_child_element_or_string(last_char, text_remaining, &block) unless text_remaining.empty?
43
+ end
44
+
45
+ def yield_while_unescaped(last_char, text_remaining, &block)
46
+ unescaped, last_char = take_while_unescaped(last_char, text_remaining)
47
+ unless unescaped.empty?
48
+ block.call(unescaped)
49
+ text_remaining = text_remaining[unescaped.size..]
50
+ end
51
+
52
+ [last_char, text_remaining]
53
+ end
54
+
55
+ def yield_while_escaped(last_char, text_remaining, &block)
56
+ escaped_char_count = 0
57
+ text_remaining.each_char do |c|
58
+ break unless escape?(c, last_char)
59
+
60
+ # TODO: collapse contiguous spaces with attribute 'text:c'
61
+ # https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#attribute-text_c
62
+ block.call(escape_element_for(c))
63
+ escaped_char_count += 1
64
+ last_char = c
65
+ end
66
+ text_remaining = text_remaining[escaped_char_count..] unless escaped_char_count == 0
67
+
68
+ [last_char, text_remaining]
69
+ end
70
+
71
+ def take_while_unescaped(last_char, text_remaining)
72
+ unescaped = text_remaining.each_char.with_object('') do |c, result|
73
+ break result if escape?(c, last_char)
74
+
75
+ result << c
76
+ last_char = c
77
+ end
78
+ [unescaped, last_char]
79
+ end
80
+
81
+ def escape?(c, last_char)
82
+ ESCAPABLE.include?(c) && (last_char == ' ' || c != ' ')
83
+ end
84
+
85
+ def escape_element_for(c)
86
+ raise ArgumentError, "Not an escapable character: #{c}" unless ESCAPABLE.include?(c)
87
+
88
+ return S.new(doc: doc) if c == ' '
89
+ return Tab.new(doc: doc) if c == "\t"
90
+ return LineBreak.new(doc: doc) if c == "\n"
91
+ end
92
+ end
93
+
94
+ # ------------------------------------------------------------
95
+ # Helper classes
96
+
97
+ class S < XML::ElementNode
98
+ def initialize(doc:)
99
+ super(:text, 's', doc: doc)
100
+ end
101
+ end
102
+
103
+ class Tab < XML::ElementNode
104
+ def initialize(doc:)
105
+ super(:text, 'tab', doc: doc)
106
+ end
107
+ end
108
+
109
+ class LineBreak < XML::ElementNode
110
+ def initialize(doc:)
111
+ super(:text, 'line-break', doc: doc)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end