ezframe 0.0.1 → 0.0.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +11 -4
  4. data/app_template/asset/image/favicon.ico +0 -0
  5. data/app_template/asset/js/ezframe.js +288 -0
  6. data/app_template/config/generic.yml +3 -0
  7. data/app_template/config/materialize.yml +5 -0
  8. data/{example/chat → app_template}/config.ru +2 -10
  9. data/app_template/pages/basic.rb +5 -0
  10. data/exe/create_table.rb +1 -0
  11. data/exe/setup.rb +15 -0
  12. data/ezframe.gemspec +3 -4
  13. data/lib/ezframe/auth.rb +15 -12
  14. data/lib/ezframe/column_set.rb +68 -28
  15. data/lib/ezframe/column_type.rb +231 -68
  16. data/lib/ezframe/config.rb +4 -0
  17. data/lib/ezframe/controller.rb +20 -10
  18. data/lib/ezframe/database.rb +10 -3
  19. data/lib/ezframe/ht.rb +167 -0
  20. data/lib/ezframe/html.rb +28 -4
  21. data/lib/ezframe/japanese_utils.rb +40 -0
  22. data/lib/ezframe/{pages.rb → loader.rb} +3 -0
  23. data/lib/ezframe/materialize.rb +55 -20
  24. data/lib/ezframe/model.rb +0 -2
  25. data/lib/ezframe/page_base.rb +18 -12
  26. data/lib/ezframe/page_kit.rb +12 -33
  27. data/lib/ezframe/template.rb +20 -15
  28. data/lib/ezframe/util.rb +5 -0
  29. data/lib/ezframe/version.rb +1 -1
  30. data/lib/ezframe.rb +5 -4
  31. metadata +27 -70
  32. data/example/auth/Gemfile +0 -8
  33. data/example/auth/asset/css/materialize.min.css +0 -13
  34. data/example/auth/asset/js/common.js +0 -200
  35. data/example/auth/asset/js/htmlgen.js +0 -79
  36. data/example/auth/columns/user.yml +0 -12
  37. data/example/auth/config/view_conf.yml +0 -3
  38. data/example/auth/config.ru +0 -26
  39. data/example/auth/pages/app.rb +0 -61
  40. data/example/auth/template/base.html +0 -12
  41. data/example/chat/Gemfile +0 -9
  42. data/example/chat/asset/css/materialize.min.css +0 -13
  43. data/example/chat/asset/js/common.js +0 -200
  44. data/example/chat/asset/js/htmlgen.js +0 -79
  45. data/example/chat/columns/belong.yml +0 -6
  46. data/example/chat/columns/channel.yml +0 -3
  47. data/example/chat/columns/talk.yml +0 -6
  48. data/example/chat/columns/user.yml +0 -12
  49. data/example/chat/config/view_conf.yml +0 -3
  50. data/example/chat/pages/app.rb +0 -59
  51. data/example/chat/template/base.html +0 -12
  52. data/example/todo/Gemfile +0 -8
  53. data/example/todo/asset/css/datatable.css +0 -54
  54. data/example/todo/asset/css/materialize.min.css +0 -13
  55. data/example/todo/asset/js/common.js +0 -135
  56. data/example/todo/asset/js/datatable.js +0 -1814
  57. data/example/todo/asset/js/htmlgen.js +0 -79
  58. data/example/todo/asset/js/init.js +0 -3
  59. data/example/todo/asset/js/materialize.min.js +0 -6
  60. data/example/todo/asset/js/mydatatable.js +0 -9
  61. data/example/todo/asset/js/mymaterialize.js +0 -22
  62. data/example/todo/columns/todo.yml +0 -9
  63. data/example/todo/config/view_conf.yml +0 -3
  64. data/example/todo/config.ru +0 -15
  65. data/example/todo/pages/app.rb +0 -93
  66. data/example/todo/template/base.html +0 -12
  67. data/exe/myrackup +0 -5
  68. data/lib/ezframe/hthash.rb +0 -116
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
+ require "date"
2
3
 
3
4
  module Ezframe
4
5
  class TypeBase
5
- attr_accessor :attribute, :parent
6
- attr_writer :value
6
+ attr_accessor :attribute, :parent, :error
7
7
 
8
8
  def self.get_class(key)
9
9
  return nil unless key
@@ -25,13 +25,13 @@ module Ezframe
25
25
  end
26
26
 
27
27
  def initialize(attr = nil)
28
- @attribute = attr if attr
28
+ @attribute = attr || {}
29
29
  end
30
30
 
31
31
  def key
32
- @attribute[:key]
33
- end
34
-
32
+ @attribute[:key]
33
+ end
34
+
35
35
  def label
36
36
  return nil if @attribute[:hidden]
37
37
  @attribute[:label]
@@ -41,8 +41,8 @@ module Ezframe
41
41
  @value
42
42
  end
43
43
 
44
- def view
45
- @value
44
+ def value=(v)
45
+ @value = v
46
46
  end
47
47
 
48
48
  def db_type
@@ -53,134 +53,295 @@ module Ezframe
53
53
  value
54
54
  end
55
55
 
56
- def form
56
+ def form(opts = {})
57
57
  nil
58
58
  end
59
+
60
+ def form_html(opts = {})
61
+ form_h = form(opts)
62
+ return nil unless form_h
63
+ return Html.convert(form_h)
64
+ end
65
+
66
+ def view(opts = {})
67
+ return nil if no_view?
68
+ @value
69
+ end
70
+
71
+ def validate
72
+ if !@value || @value.empty?
73
+ if @attribute[:must]
74
+ @error = "必須項目です。"
75
+ return @error
76
+ end
77
+ end
78
+ return nil
79
+ end
80
+
81
+ def no_edit?
82
+ return ((@attribute[:hidden] || @attribute[:no_edit]) && !@attribute[:force])
83
+ end
84
+
85
+ def no_view?
86
+ return (@attribute[:hidden] && !@attribute[:force])
87
+ end
59
88
  end
60
89
 
61
- class StringType < TypeBase
90
+ class TextType < TypeBase
62
91
  def normalize
63
- @value.gsub!(/ /, ' ')
92
+ return unless @value
93
+ @value = @value.to_s
94
+ @value.gsub!(/ /, " ")
95
+ @value.gsub!(/\s+/, " ")
96
+ @value.strip!
64
97
  end
65
98
 
66
- def form
67
- return nil if @attribute[:hidden] && !@attribute[:force]
68
- { tag: 'input', type: 'text', name: @attribute[:key], key: @attribute[:key], label: @attribute[:label], value: @value }
99
+ def value=(v)
100
+ super(v)
101
+ normalize
102
+ end
103
+
104
+ def form(opts = {})
105
+ return nil if no_edit? && !opts[:force]
106
+ h = { tag: "input", type: "text", name: @attribute[:key], key: @attribute[:key], label: @attribute[:label], value: @value || "" }
107
+ h[:size] = @attribute[:size] if @attribute[:size]
108
+ h
69
109
  end
70
110
 
71
111
  def db_type
72
- "string"
112
+ "text"
73
113
  end
74
114
  end
75
115
 
76
- class IntType < StringType
77
- def view
78
- return nil if @attribute[:hidden]
79
- Util.add_comma(@value.to_i)
116
+ class IntType < TextType
117
+ def view(opts = {})
118
+ return nil if no_view? && !opts[:force]
119
+ return Util.add_comma(@value.to_i)
80
120
  end
81
121
 
82
- def form
83
- return nil if @attribute[:hidden]
84
- { tag: 'input', type: 'number', key: @attribute[:key], label: @attribute[:label], value: @value }
122
+ def value=(v)
123
+ if v.nil?
124
+ @value = nil
125
+ return
126
+ end
127
+ if v.is_a?(String)
128
+ v = v.tr("0-9", "0-9").strip
129
+ end
130
+ @value = v.to_i
131
+ end
132
+
133
+ def form(opts = {})
134
+ return nil if no_edit? && !opts[:force]
135
+ { tag: "input", type: "number", key: @attribute[:key], label: @attribute[:label], value: @value || "" }
85
136
  end
86
137
 
87
138
  def db_type
88
139
  "int"
89
140
  end
141
+
142
+
90
143
  end
91
144
 
92
145
  class ForeignType < IntType
93
- def view
146
+ def view(opts = {})
147
+ return nil if no_view? && !opts[:force]
94
148
  dataset = @parent.db.dataset[self.type.inner]
95
149
  data = dataset.get(id: @value)
96
- data[@attribute[:view]]
150
+ return data[@attribute[:view]]
151
+ end
152
+
153
+ def form
154
+ return nil
97
155
  end
98
156
  end
99
-
157
+
100
158
  class IdType < IntType
101
159
  def label
102
- return nil if @attribute[:hidden] && !@attribute[:force]
103
- "ID"
160
+ return nil if no_view?
161
+ return "ID"
162
+ end
163
+
164
+ def form(opts = {})
165
+ return nil
104
166
  end
105
167
  end
106
168
 
107
- class PasswordType < StringType
108
- def form
109
- { tag: "input", type: "password", label: @attribute[:label], value: @value}
169
+ class PasswordType < TextType
170
+ def form(opts = {})
171
+ return nil if no_edit? && !opts[:force]
172
+ return { tag: "input", type: "password", label: @attribute[:label], value: @value || "" }
110
173
  end
111
174
 
112
175
  def db_value
113
- value
176
+ return value
114
177
  end
115
178
  end
116
179
 
117
180
  class SelectType < TypeBase
118
- def form
119
- return nil if @attribute[:hidden]
120
- { tag: 'select', key: @attribute[:key], label: @attribute[:label], items: @attribute[:items], value: @value }
181
+ def form(opts = {})
182
+ return nil if no_edit? && !opts[:force]
183
+ # puts "selectType: #{@attribute[:items].inspect}"
184
+ return { tag: "select", key: @attribute[:key], label: @attribute[:label], items: @attribute[:items], value: @value }
121
185
  end
122
186
 
123
187
  def db_type
124
- "string"
188
+ return "text"
125
189
  end
126
190
  end
127
191
 
128
192
  class CheckboxType < TypeBase
129
- def form
130
- return nil if @attribute[:hidden]
131
- { tag: "checkbox", key: @attribute[:key], name: @attribute[:key], value: parent[:id].value, label: @attribute[:label] }
193
+ def form(opts = {})
194
+ return nil if no_edit? && !opts[:force]
195
+ return { tag: "checkbox", key: @attribute[:key], name: @attribute[:key], value: parent[:id].value, label: @attribute[:label] }
132
196
  end
133
197
 
134
198
  def db_type
135
- "int"
199
+ return "int"
136
200
  end
137
201
  end
138
202
 
139
- class DateType < StringType
140
- def form
203
+ class DateType < TextType
204
+ def form(opts = {})
205
+ return nil if no_edit? && !opts[:force]
141
206
  h = super
142
- h[:type] = 'date' if h
143
- h
207
+ if h
208
+ # h[:type] = 'date'
209
+ h[:type] = "text"
210
+ h[:class] = "datepicker"
211
+ h[:value] = value || ""
212
+ end
213
+ return h
144
214
  end
145
215
 
146
216
  def db_type
147
- "datetime"
217
+ "date"
218
+ end
219
+
220
+ def value
221
+ if @value.is_a?(Date) || @value.is_a?(Time)
222
+ return "%d-%02d-%02d" % [@value.year, @value.mon, @value.mday]
223
+ end
224
+ return @value
225
+ end
226
+
227
+ def value=(v)
228
+ if v.nil?
229
+ @value = nil
230
+ return
231
+ end
232
+ if v.is_a?(String)
233
+ if v.strip.empty?
234
+ @value = nil
235
+ return
236
+ end
237
+ y, m, d = v.split(/[\-\/]/)
238
+ # puts "date=#{v.inspect}"
239
+ @value = Date.new(y.to_i, m.to_i, d.to_i)
240
+ return
241
+ end
242
+ if v.is_a?(Date) || v.is_a?(Time)
243
+ @value = v
244
+ else
245
+ mylog "[WARN] illegal value for date type: #{v.inspect}"
246
+ end
247
+ end
248
+
249
+ def view(opts = {})
250
+ return nil if no_view? && !opts[:force]
251
+ if @value.is_a?(Time) || @value.is_a?(Date)
252
+ return "#{@value.year}/#{@value.mon}/#{@value.mday}"
253
+ else
254
+ return @value
255
+ end
148
256
  end
149
257
  end
150
-
151
- class EmailType < StringType
152
- def form
153
- h = super
154
- h[:type] = 'email' if h
155
- h
258
+
259
+ class TimeType < TextType
260
+ end
261
+
262
+ class DatetimeType < DateType
263
+ def db_type
264
+ "timestamp"
156
265
  end
157
266
  end
158
267
 
159
- class TelephoneType < StringType
160
- def form
268
+ class BirthdayType < DateType
269
+ def form(opts = {})
270
+ return nil if no_edit? && !opts[:force]
271
+ prefix = @attribute[:key]
272
+ now = Time.now
273
+ year_list = []
274
+ 110.times do |y|
275
+ year = now.year - y - 10
276
+ year_list.push [year, "#{year}年 (#{Japanese.convert_wareki(year)})"]
277
+ end
278
+ mon_list = (1..12).map { |m| [m, "#{m}月"] }
279
+ mon_list.unshift([0, "(月)"])
280
+ mday_list = (1..31).map { |d| [d, "#{d}日"] }
281
+ mday_list.unshift([0, "(日)"])
282
+ return [Ht.select(name: "#{prefix}_year", items: year_list),
283
+ Ht.select(name: "#{prefix}_mon", items: mon_list),
284
+ Ht.select(name: "#{prefix}_mday", items: mday_list)]
285
+ end
286
+ end
287
+
288
+ class EmailType < TextType
289
+ def form(opts = {})
290
+ return nil if no_edit? && !opts[:force]
161
291
  h = super
162
- h[:type] = 'tel' if h
163
- h
292
+ h[:type] = "email" if h
293
+ return h
294
+ end
295
+
296
+ def normalize
297
+ return unless @value
298
+ @value = NKF.nkf('-w -Z4', @value)
299
+ end
300
+
301
+ def validate
302
+ super
303
+ return @error if @error
304
+ if email_format?
305
+ @error = "形式が正しくありません"
306
+ return @error
307
+ end
308
+ return nil
309
+ end
310
+
311
+ def email_format?
312
+ return nil unless @value
313
+ return @value =~ /^[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
164
314
  end
165
315
  end
166
316
 
167
- class JpnameType < StringType
317
+ class TelType < TextType
318
+ def validate
319
+ super
320
+ return @error if @error
321
+ unless /^0\d{9,10}$/ =~ @value
322
+ @error = "形式が正しくありません"
323
+ return @error
324
+ end
325
+ end
168
326
  end
169
327
 
170
- class JpnameKanaType < StringType
171
- def set(val)
172
- val = val.tr('ァ-ン', 'ぁ-ん')
173
- super(val)
328
+ class JpnameType < TextType
329
+ end
330
+
331
+ class JpnameKanaType < TextType
332
+ def normalize
333
+ return unless @value
334
+ super
335
+ @value.tr!("ァ-ン", "ぁ-ん")
174
336
  end
175
337
 
176
338
  def validation
177
339
  unless /^[ぁ-ん ]+$/ =~ @value
178
- 'ひらがなのみで入力してください。'
340
+ "ひらがなのみで入力してください。"
179
341
  end
180
342
  end
181
343
  end
182
344
 
183
- #
184
345
  class PrefectureType < SelectType
185
346
  def initialize(attr)
186
347
  super(attr)
@@ -195,26 +356,28 @@ module Ezframe
195
356
  @pref_a.each_with_index { |p, i| @pref_h[i] = p }
196
357
  end
197
358
 
198
- def form
359
+ def form(opts = {})
360
+ return nil if no_edit? && !opts[:force]
199
361
  h = super
200
362
  h[:items] = @pref_h
201
- h
363
+ return h
202
364
  end
203
365
 
204
366
  def view
205
- @pref_h[@value.to_i]
367
+ return @pref_h[@value.to_i]
206
368
  end
207
369
  end
208
370
 
209
371
  # Japanese Zipcode type column
210
- class ZipcodeType < StringType
211
- def view
372
+ class ZipcodeType < TextType
373
+ def view(opts = {})
374
+ return nil if no_view? && !opts[:force]
212
375
  return "" unless @value
213
- @value.to_s.gsub(/(\d{3})(\d{4})/) { "#{$1}-#{$2}" }
376
+ return @value.to_s.gsub(/(\d{3})(\d{4})/) { "#{$1}-#{$2}" }
214
377
  end
215
378
 
216
379
  def db_type
217
- "string"
380
+ return "text"
218
381
  end
219
382
  end
220
383
  end
@@ -28,6 +28,10 @@ module Ezframe
28
28
  @value_h||={}
29
29
  @value_h[k]=v
30
30
  end
31
+
32
+ def inspect
33
+ @value_h.inspect
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -9,22 +9,33 @@ module Ezframe
9
9
  Model.init
10
10
  model = Model.get_clone
11
11
  Auth.init_warden
12
- Auth.model = model
12
+ @request.env["model"] = model
13
+ # Auth.model = model
13
14
 
14
15
  mylog("exec: path=#{request.path_info} params=#{request.params}")
15
16
  klass, method = PageBase::decide_route(request.path_info)
16
- mylog "klass=#{klass}, method=#{method}"
17
+ unless klass
18
+ response.status = 404
19
+ response['Content-Type'] = 'text/html; charset=utf-8'
20
+ response.body = [ Html.convert(Ht.p("file not found")) ]
21
+ return
22
+ end
23
+ method = "default" if !method || method=="undefined"
17
24
  page = klass.new(request, model)
18
25
  if request.post?
19
26
  method_full_name = "public_#{method}_post"
20
27
  else
21
28
  method_full_name = "public_#{method}_page"
22
29
  end
23
- warden.authenticate! if page.auth
30
+ if page.auth
31
+ warden.authenticate!
32
+ end
24
33
  # request.env["rack.session"]["kamatest"]="usable"
25
- mylog "method: #{klass}.#{method_full_name}"
26
- mylog "rack.session.id=#{request.env['rack.session'].id}"
34
+ # mylog "method: #{klass}.#{method_full_name}"
35
+ #mylog "rack.session.id=#{request.env['rack.session'].id}"
27
36
  mylog "rack.session.keys=#{request.env['rack.session'].keys}"
37
+ #mylog "warden=#{request.env['warden'].inspect}"
38
+ mylog "klass=#{klass}, method=#{method_full_name}"
28
39
  body = if page.respond_to?(method_full_name)
29
40
  page.send(method_full_name)
30
41
  else
@@ -41,11 +52,10 @@ module Ezframe
41
52
  response.status = 200
42
53
  end
43
54
 
44
-
45
- def file_not_found(response)
46
- response.body = ['path not found']
47
- response.status = 404
48
- end
55
+ # def file_not_found(response)
56
+ # response.body = ['path not found']
57
+ # response.status = 404
58
+ # end
49
59
 
50
60
  def warden
51
61
  @request.env["warden"]
@@ -5,13 +5,15 @@ module Ezframe
5
5
  class Database
6
6
  attr_accessor :sequel
7
7
 
8
- def initialize(dbfile = "db/devel.sqlite")
8
+ def initialize(dbfile = nil)
9
9
  @dbfile = dbfile
10
10
  connect
11
11
  end
12
12
 
13
13
  def connect
14
- @sequel = Sequel.connect("sqlite://#{@dbfile}", loggers: [Logger.new($stdout)])
14
+ @dbfile ||= ENV["EZFRAME_DB"] || Config[:database] || "sqlite://db/devel.sqlite"
15
+ puts "Database.connect: dbfile=#{@dbfile}"
16
+ @sequel = Sequel.connect(@dbfile, loggers: [Logger.new($stdout)])
15
17
  end
16
18
 
17
19
  def exec(sql)
@@ -26,8 +28,13 @@ module Ezframe
26
28
  %w[id created_at updated_at].each do |key|
27
29
  dbtype_h.delete(key.to_sym)
28
30
  end
31
+ puts "create_table: #{table_name}"
29
32
  @sequel.create_table(table_name) do
30
- primary_key :id, auto_increment: true
33
+ # if @dbfile.index("postgresql")
34
+ serial :id
35
+ # else
36
+ #primary_key :id, auto_increment: true
37
+ #end
31
38
  dbtype_h.each do |key, dbtype|
32
39
  column(key, dbtype)
33
40
  end
data/lib/ezframe/ht.rb ADDED
@@ -0,0 +1,167 @@
1
+ # HTMLを中間表現としてのハッシュであるhthashを生成するためのクラス
2
+ module Ezframe
3
+ module Ht
4
+ class << self
5
+ # メソッド名の名前のタグのhthashを生成
6
+ def wrap_tag(opts = {})
7
+ if opts.is_a?(String) || opts.is_a?(Array)
8
+ h = { child: opts }
9
+ elsif opts.is_a?(Hash)
10
+ if opts[:tag] && !__callee__.to_s.index("wrap_tag")
11
+ h = { child: opts }
12
+ else
13
+ h = opts.dup
14
+ end
15
+ else
16
+ mylog("wrap_tag: unknown type: #{opts.inspect}")
17
+ return nil
18
+ end
19
+ h[:tag] ||= __callee__.to_s
20
+ raise "no tag" if h[:tag] == "wrap_tag"
21
+ return h
22
+ end
23
+
24
+ alias_method :script, :wrap_tag
25
+
26
+ alias_method :h1, :wrap_tag
27
+ alias_method :h2, :wrap_tag
28
+ alias_method :h3, :wrap_tag
29
+ alias_method :h4, :wrap_tag
30
+ alias_method :h5, :wrap_tag
31
+ alias_method :h6, :wrap_tag
32
+ alias_method :p, :wrap_tag
33
+ alias_method :br, :wrap_tag
34
+ alias_method :hr, :wrap_tag
35
+ alias_method :div, :wrap_tag
36
+ alias_method :span, :wrap_tag
37
+ alias_method :i, :wrap_tag
38
+ alias_method :strong, :wrap_tag
39
+ alias_method :ul, :wrap_tag
40
+ alias_method :ol, :wrap_tag
41
+ alias_method :li, :wrap_tag
42
+ alias_method :table, :wrap_tag
43
+ alias_method :thead, :wrap_tag
44
+ alias_method :tbody, :wrap_tag
45
+ alias_method :tr, :wrap_tag
46
+ alias_method :th, :wrap_tag
47
+ alias_method :td, :wrap_tag
48
+ alias_method :img, :wrap_tag
49
+ alias_method :a, :wrap_tag
50
+ alias_method :form, :wrap_tag
51
+ alias_method :input, :wrap_tag
52
+ alias_method :select, :wrap_tag
53
+ alias_method :textarea, :wrap_tag
54
+ alias_method :label, :wrap_tag
55
+ alias_method :fieldset, :wrap_tag
56
+ alias_method :nav, :wrap_tag
57
+ alias_method :footer, :wrap_tag
58
+
59
+ alias_method :small, :wrap_tag
60
+
61
+ alias_method :checkbox, :wrap_tag
62
+ alias_method :radio, :wrap_tag
63
+
64
+ # materialize用のiconメソッド
65
+ # 引数が文字列だったら、それをname属性とする
66
+ def icon(arg)
67
+ if arg.is_a?(Hash)
68
+ h = arg.clone
69
+ h[:tag] = "icon"
70
+ return wrap_tag(h)
71
+ elsif arg.is_a?(String)
72
+ return { tag: "icon", name: arg }
73
+ end
74
+ end
75
+
76
+ # buttonタグにはデフォルトでtype=button属性を付ける
77
+ def button(arg)
78
+ arg[:tag] = "button"
79
+ unless arg[:type]
80
+ arg[:type] = "button"
81
+ end
82
+ wrap_tag(arg)
83
+ end
84
+
85
+ def multi_div(class_a, child)
86
+ class_a.reverse.each do |klass|
87
+ child = Ht.div(class: klass, child: child)
88
+ end
89
+ return child
90
+ end
91
+ end
92
+
93
+ # 配列を<UL><OL>要素に変換するためのクラス
94
+ class List < Array
95
+ attr_accessor :tag
96
+ def to_h(opts = {})
97
+ return nil if self.empty?
98
+ child = self.map do |elem|
99
+ { tag: "li", child: elem }
100
+ end
101
+ h = { tag: @tag, child: child }
102
+ h.update(opts)
103
+ return h
104
+ end
105
+ end
106
+
107
+ # 配列を<UL>要素に変換するためのクラス
108
+ class Ul < List
109
+ def to_h(opts = {})
110
+ @tag = "ul"
111
+ return super(opts)
112
+ end
113
+ end
114
+
115
+ # 配列を<OL>要素に変換するためのクラス
116
+ class Ol < List
117
+ def to_h(opts = {})
118
+ @tag = "ol"
119
+ return super(opts)
120
+ end
121
+ end
122
+
123
+ # テーブルを生成するためのクラス
124
+ # @matrix ... テーブルの内容となる二次元配列
125
+ # @header ... テーブルの先頭に付ける項目名の配列
126
+ # @class_a ... <table><tr><td>の各ノードにそれぞれ設定したいclass属性を配列として定義
127
+ class Table
128
+ attr_accessor :class_a, :header, :matrix
129
+
130
+ def initialize(matrix = nil)
131
+ set(matrix) if matrix
132
+ @matrix ||= []
133
+ end
134
+
135
+ def set(matrix)
136
+ @matrix = matrix
137
+ end
138
+
139
+ def add_row(row)
140
+ @matrix.push(row)
141
+ end
142
+
143
+ def to_h
144
+ table_class, tr_class, td_class = @class_a
145
+ max_col = 0
146
+ @matrix.each { |row| max_col = row.length if max_col < row.length }
147
+ tr_a = @matrix.map do |row|
148
+ add_attr = nil
149
+ add_attr = { colspan: max_col - row.length + 1 } if row.length < max_col
150
+ td_a = row.map do |v|
151
+ td = Ht.td(child: v)
152
+ td.add_class(td_class) if td_class
153
+ td
154
+ end
155
+ td_a[0].update(add_attr) if add_attr
156
+ tr = Ht.tr(class: tr_class, child: td_a)
157
+ tr.add_class(tr_class) if tr_class
158
+ tr
159
+ end
160
+ tr_a.unshift( Ht.thead(child: Ht.tr(child: @header.map {|v| Ht.th(child: v) }) )) if @header
161
+ tb = Ht.table(child: tr_a)
162
+ tb.add_class(table_class) if table_class
163
+ tb
164
+ end
165
+ end
166
+ end
167
+ end