dynarex 1.2.97 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/dynarex.rb +1045 -1040
  5. metadata +18 -18
  6. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba9cee7b9e693f188f46af066d7c6fd9eb39cc7d
4
- data.tar.gz: 7032bcc79105fbb554201512466a04330846787e
3
+ metadata.gz: d35a52b2f8e685ee91957c366b83bbb7bf05b237
4
+ data.tar.gz: 3ccf814b746c54b613568cd2f9d16322df70fd3c
5
5
  SHA512:
6
- metadata.gz: c513aea9c1c9232135e3dc03733cf271f0f6bfeaa8990f37d14faf849d55fde313a375122b5d5fcafad77e5b3dd22adcfc20b0f34729c1d1a340920dc6a8ccfb
7
- data.tar.gz: 1d6c8040b0a24fc118fa61847551a40d670d480318a968797973289a59d6f65a1e49294f10a5fba58ec87bafb57546d67ce940f769e1fe8fb37dfadb65d7a124
6
+ metadata.gz: 03edaa4dbc3987611526bb9c37dc4b86db88dc58afdd0eb63901aecf95af3dbc750fed94b2f5057a0058ec47eab70e81e1e3e625226657f08a74bbf4c54d2570
7
+ data.tar.gz: f429218c0a3ba6f1e80f013b0d1b7cf093b608020c456ad56cb97e1bfa6d511ec97f26521f1c39556ab8ad4f398957eca4ce3387d07c3cb3c5d072a2743d5ef2
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -1,1043 +1,1048 @@
1
- #!/usr/bin/env ruby
2
-
3
- # file: dynarex.rb
4
-
5
- require 'open-uri'
6
- require 'dynarex-import'
7
- require 'line-tree'
8
- require 'rexle'
9
- require 'rexle-builder'
10
- require 'rexslt'
11
- require 'dynarex-xslt'
12
- require 'recordx'
13
- require 'rxraw-lineparser'
14
- require 'yaml'
15
- require 'rowx'
16
- require 'nokogiri'
17
- require 'ostruct'
18
- require 'table-formatter'
19
-
20
-
21
- class Dynarex
22
-
23
- attr_accessor :format_mask, :delimiter, :xslt_schema, :schema,
24
- :order, :type, :limit_by, :xslt
25
-
26
-
27
- #Create a new dynarex document from 1 of the following options:
28
- #* a local file path
29
- #* a URL
30
- #* a schema string
31
- # Dynarex.new 'contacts[title,description]/contact(name,age,dob)'
32
- #* an XML string
33
- # Dynarex.new '<contacts><summary><schema>contacts/contact(name,age,dob)</schema></summary><records/></contacts>'
34
-
35
- def initialize(rawx=nil)
36
- #puts Rexle.version
37
- @delimiter = ''
38
- openx(rawx.clone) if rawx
39
- if @order == 'descending' then
40
- @records = records_to_h(:descending)
41
- rebuild_doc
42
- end
43
- #jr240913 @dirty_flag = false
44
- end
45
-
46
- def add(x)
47
- @doc.root.add x
48
- @dirty_flag = true
49
- self
50
- end
51
-
52
- def all()
53
- @doc.root.xpath("records/*").map {|x| recordx_to_record x}
54
- end
55
-
56
- def delimiter=(separator)
57
-
58
- @delimiter = separator
59
-
60
- if separator.length > 0 then
61
- @summary[:delimiter] = separator
62
- else
63
- @summary.delete :delimiter
64
- end
65
-
66
- @format_mask = @format_mask.to_s.gsub(/\s/, separator)
67
- @summary[:format_mask] = @format_mask
68
- end
69
-
70
- def foreign_import(options={})
71
- o = {xml: '', schema: ''}.merge(options)
72
- h = {xml: o[:xml], schema: @schema, foreign_schema: o[:schema]}
73
- buffer = DynarexImport.new(h).to_xml
74
-
75
- openx(buffer)
76
- self
77
- end
78
-
79
- def fields
80
- @fields
81
- end
82
-
83
- def format_mask=(s)
84
- @format_mask = s
85
- @summary[:format_mask] = @format_mask
86
- end
87
-
88
- def insert(raw_params)
89
- record = make_record(raw_params)
90
- @doc.root.element('records/*').insert_before record
91
- @dirty_flag = true
92
- end
93
-
94
- def inspect()
95
- "<object #%s>" % [self.object_id]
96
- end
97
-
98
- def order=(value)
99
-
100
- self.summary.merge!({order: value})
101
- if @order == 'ascending' and value == 'descending' then
102
- sort_records
103
- elsif @order == 'descending' and value == 'ascending'
104
- sort_records
105
- end
106
- @order = value
107
- end
108
-
109
- def schema=(s)
110
- openx s
111
- end
112
-
113
- def type=(v)
114
- @order = 'descending' if v == 'feed'
115
- @type = v
116
- @summary[:type] = v
117
- end
118
-
119
- def limit_by=(val)
120
- @limit_by = val.to_i
121
- end
122
-
123
- # Returns the hash representation of the document summary.
124
- #
125
- def summary
126
- @summary
127
- end
128
-
129
- # Return a Hash (which can be edited) containing all records.
130
- #
131
- def records
132
-
133
- load_records if @dirty_flag == true
134
-
135
- if block_given? then
136
- yield(@records)
137
- rebuild_doc
138
- @dirty_flag = true
139
- else
140
- @records
141
- end
142
-
143
- end
144
-
145
- # Returns a ready-only snapshot of records as a simple Hash.
146
- #
147
- def flat_records
148
- load_records if @dirty_flag == true
149
- @flat_records
150
- end
151
-
152
- alias to_h flat_records
153
- alias to_a flat_records
154
-
155
- # Returns an array snapshot of OpenStruct records
156
- #
157
- def ro_records
158
- flat_records.map {|record| OpenStruct.new record }
159
- end
160
-
161
- # Returns all records as a string format specified by the summary format_mask field.
162
-
163
- def to_doc
164
- @doc
165
- end
166
-
167
- def to_s
168
-
169
- xsl_buffer =<<EOF
170
- <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
171
- <xsl:output encoding="UTF-8"
172
- method="text"
173
- indent="no"
174
- omit-xml-declaration="yes"/>
175
-
176
- <xsl:template match="*">
177
- <xsl:for-each select="records/*">[!regex_values]<xsl:text>
178
- </xsl:text>
179
- </xsl:for-each>
180
- </xsl:template>
181
- </xsl:stylesheet>
182
- EOF
183
-
184
-
185
- #format_mask = XPath.first(@doc.root, 'summary/format_mask/text()').to_s
186
- #format_mask = @doc.root.element('summary/format_mask/text()')
187
-
188
- raw_summary_fields = self.summary[:schema][/^\w+\[([^\]]+)\]/,1]
189
- sumry = ''
190
-
191
- if raw_summary_fields then
192
- summary_fields = raw_summary_fields.split(',') # .map(&:to_sym)
193
- sumry = summary_fields.map {|x| x.strip!; x + ': ' + \
194
- self.summary[x.to_sym]}.join("\n") + "\n"
195
- end
196
-
197
- if @raw_header then
198
- declaration = @raw_header
199
- else
200
-
201
- smry_fields = %i(schema)
202
- if self.delimiter.length > 0 then
203
- smry_fields << :delimiter
204
- else
205
- smry_fields << :format_mask
206
- end
207
- s = smry_fields.map {|x| "%s=\"%s\"" % \
208
- [x, self.send(x).gsub('"', '\"') ]}.join ' '
209
- #declaration = "<?dynarex %s ?>" % s
210
- declaration = %Q(<?dynarex %s?>\n) % s
211
- end
212
-
213
- header = declaration + sumry
214
-
215
- if self.summary[:rawdoc_type] == 'rowx' then
216
- a = self.fields.map do |field|
217
- "<xsl:if test=\"%s != ''\">
218
- <xsl:text>\n</xsl:text>%s: <xsl:value-of select='%s'/>
219
- </xsl:if>" % ([field]*3)
220
- end
221
-
222
- xslt_format = a.join
223
-
224
- xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
225
- xslt = Nokogiri::XSLT(xsl_buffer)
226
- out = xslt.transform(Nokogiri::XML(@doc.to_s))
227
-
228
- header + "\n--+\n" + out.text
229
-
230
- elsif self.delimiter.length > 0 then
231
-
232
- tfo = TableFormatter.new border: false, nowrap: true, divider: self.delimiter
233
- tfo.source = self.to_h.map{|x| x.values}
234
- header + tfo.display
235
-
236
- else
237
-
238
- format_mask = self.format_mask
239
- format_mask.gsub!(/\[[^!\]]+\]/) {|x| x[1] }
240
- xslt_format = format_mask.gsub(/\s(?=\[!\w+\])/,'<xsl:text> </xsl:text>')
241
- .gsub(/\[!(\w+)\]/, '<xsl:value-of select="\1"/>')
242
-
243
- xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
244
- xslt = Nokogiri::XSLT(xsl_buffer)
245
-
246
- out = xslt.transform(Nokogiri::XML(self.to_xml))
247
- header + "\n" + out.text
248
- end
249
-
250
- #xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
251
- #xslt = Nokogiri::XSLT(xsl_buffer)
252
- #out = xslt.transform(Nokogiri::XML(@doc.to_s))
253
- #jr250811 puts 'xsl_buffer: ' + xsl_buffer
254
- #jr250811 puts 'doc_to_s: ' + @doc.to_s
255
- #out.text
256
- #jr231211 Rexslt.new(xsl_buffer, @doc.to_s).to_s
257
-
258
- end
259
-
260
- def to_xml(opt={})
261
- opt = {pretty: true} if opt == :pretty
262
- display_xml(opt)
263
- end
264
-
265
- #Save the document to a local file.
266
-
267
- def save(filepath=nil, options={})
268
-
269
- opt = {pretty: true}.merge options
270
- filepath ||= @local_filepath
271
- @local_filepath = filepath
272
- xml = display_xml(opt)
273
- buffer = block_given? ? yield(xml) : xml
274
- File.open(filepath,'w'){|f| f.write buffer }
275
- end
276
-
277
- #Parses 1 or more lines of text to create or update existing records.
278
-
279
- def parse(x=nil)
280
- if x.is_a? String then
281
- buffer = x.clone
282
- buffer = yield if block_given?
283
- string_parse buffer
284
- else
285
- foreign_import x
286
- end
287
- end
288
-
289
-
290
- alias import parse
291
-
292
- #Create a record from a hash containing the field name, and the field value.
293
- # dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
294
- # dynarex.create name: Bob, age: 52
295
-
296
- def create(arg, id=nil)
297
- raise 'Dynarex#create(): input error: no arg provided' unless arg
298
- #jr291012 rebuild_doc()
299
- #jr291012 (load_records; rebuild_doc) if @dirty_flag == true
300
- methods = {Hash: :hash_create, String: :create_from_line}
301
- send (methods[arg.class.to_s.to_sym]), arg, id
302
-
303
- #jr291012load_records
304
- @dirty_flag = true
305
-
306
- self
307
- end
308
-
309
- #Create a record from a string, given the dynarex document contains a format mask.
310
- # dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
311
- # dynarex.create_from_line 'Tracy 37 15-Jun-1972'
312
-
313
- def create_from_line(line, id=nil)
314
- t = @format_mask.to_s.gsub(/\[!(\w+)\]/, '(.*)').sub(/\[/,'\[').sub(/\]/,'\]')
315
- line.match(/#{t}/).captures
316
-
317
- a = line.match(/#{t}/).captures
318
- h = Hash[@fields.zip(a)]
319
- create h
320
- self
321
- end
322
-
323
- def default_key=(id)
324
- @default_key = id.to_sym
325
- @summary[:default_key] = id
326
- @fields << id.to_sym
327
- end
328
-
329
-
330
- #Updates a record from an id and a hash containing field name and field value.
331
- # dynarex.update 4, name: Jeff, age: 38
332
-
333
- def update(id, params={})
334
- fields = capture_fields(params)
335
-
336
-
337
- # for each field update each record field
338
- record = @doc.root.element("records/#{@record_name}[@id='#{id.to_s}']")
339
- fields.each {|k,v| record.element(k.to_s).text = v if v}
340
- record.add_attribute(last_modified: Time.now.to_s)
341
-
342
- @dirty_flag = true
343
-
344
- self
345
-
346
- end
347
-
348
-
349
- #Delete a record.
350
- # dyarex.delete 3 # deletes record with id 3
351
-
352
- def delete(x)
353
-
354
- if x.to_i.to_s == x.to_s and x[/[0-9]/] then
355
- @doc.root.delete("records/*[@id='#{x}']")
356
- else
357
- @doc.delete x
358
- end
359
- @dirty_flag = true
360
- self
361
- end
362
-
363
- def element(x)
364
- @doc.root.element x
365
- end
366
-
367
- def sort_by!(&element_blk)
368
- refresh_doc
369
- a = @doc.root.xpath('records/*').sort_by &element_blk
370
- @doc.root.delete('records')
371
-
372
- records = Rexle::Element.new 'records'
373
-
374
- a.each {|record| records.add record}
375
-
376
- @doc.root.add records
377
-
378
- load_records
379
- self
380
- end
381
-
382
- def rebuild_doc(state=:internal)
383
-
384
- reserved_keywords = (
385
- Object.public_methods | \
386
- Kernel.public_methods | \
387
- public_methods + [:method_missing]
388
- )
389
-
390
- xml = RexleBuilder.new
391
-
392
- a = xml.send @root_name do
393
-
394
- xml.summary do
395
-
396
- @summary.each do |key,value|
397
-
398
- xml.send key, value.gsub('>','&gt;')\
399
- .gsub('<','&lt;')\
400
- .gsub(/(&\s|&[a-zA-Z\.]+;?)/) {|x| x[-1] == ';' ? x : x.sub('&','&amp;')}
401
-
402
- end
403
- end
404
-
405
- records = @records.to_a
406
-
407
- if records then
408
-
409
- records.reverse! if @order == 'descending' and state == :external
410
-
411
- xml.records do
412
-
413
- records.each do |k, item|
414
- #p 'foo ' + item.inspect
415
- xml.send(@record_name, {id: item[:id], created: item[:created], \
416
- last_modified: item[:last_modified]}, '') do
417
- item[:body].each do |name,value|
418
- #name = name.to_s.prepend('._').to_sym if reserved_keywords.include? name
419
- name = ('._' + name.to_s).to_sym if reserved_keywords.include? name
420
- val = value.send(value.is_a?(String) ? :to_s : :to_yaml)
421
- xml.send(name, val.gsub('>','&gt;')\
422
- .gsub('<','&lt;')\
423
- .gsub(/(&\s|&[a-zA-Z\.]+;?)/) do |x|
424
- x[-1] == ';' ? x : x.sub('&','&amp;')
425
- end
426
- )
427
- end
428
- end
429
- end
430
-
431
- end
432
- else
433
- xml.records
434
- end # end of if @records
435
- end
436
-
437
- doc = Rexle.new(a)
438
-
439
- if @xslt then
440
- doc.instructions = [['xml-stylesheet',
441
- "title='XSL_formatting' type='text/xsl' href='#{@xslt}'"]]
442
- end
443
-
444
- return doc if state != :internal
445
- @doc = doc
446
- end
447
-
448
- def record(id)
449
- recordx_to_record @doc.root.element("records/*[@id='#{id}']")
450
- end
451
-
452
- alias find record
453
-
454
- def record_exists?(id)
455
- !@doc.root.element("records/*[@id='#{id}']").nil?
456
- end
457
-
458
- def to_xslt(opt={})
459
-
460
- h = {limit: -1}.merge(opt)
461
- xslt_schema = @xslt_schema || self.summary[:xslt_schema]
462
- raise 'to_xsl(): xslt_schema nil' unless xslt_schema
463
-
464
- xslt = DynarexXSLT.new(schema: @schema, xslt_schema: @xslt_schema ).to_xslt
465
- return xslt
466
- end
467
-
468
- def to_rss(opt={}, xslt=nil)
469
-
470
- unless xslt then
471
-
472
- h = {limit: 11}.merge(opt)
473
- doc = Rexle.new(self.to_xslt)
474
-
475
- e = doc.element('//xsl:apply-templates[2]')
476
- doc2 = Rexle.new "<xsl:sort order='descending' data-type='number' select='@id'/>"
477
- e.add doc2.root
478
-
479
- e2 = doc.root.element('xsl:template[3]')
480
- item = e2.element('item')
481
- new_item = item.deep_clone
482
- item.delete
483
-
484
- xslif = Rexle.new("<xsl:if test='position() &lt; #{h[:limit]}'/>").root
485
-
486
-
487
- pubdate = Rexle.new("<pubDate><xsl:value-of select='pubDate'></xsl:value-of></pubDate>").root
488
- new_item.add pubdate
489
- xslif.add new_item
490
-
491
- e2.add xslif.root
492
- xslt = doc.xml
493
- end
494
-
495
- doc = self.to_doc
496
- doc.root.xpath('records/*').each do |x|
497
- raw_dt = DateTime.parse x.attributes[:created]
498
- dt = raw_dt.strftime("%a, %d %b %Y %H:%M:%S %z")
499
- x.add Rexle::Element.new('pubDate').add_text dt.to_s
500
- end
501
-
502
- #File.open('dynarex.xsl','w'){|f| f.write xslt}
503
- #File.open('dynarex.xml','w'){|f| f.write doc.xml}
504
- #xml = Rexslt.new(xslt, doc.xml).to_s
505
- #=begin
506
- xslt = Nokogiri::XSLT(xslt)
507
- out = xslt.transform(Nokogiri::XML(doc.root.xml))
508
- #=end
509
-
510
- #Rexle.new("<rss version='2.0'>%s</rss>" % xml).xml(pretty: true)
511
- Rexle.new("<rss version='2.0'>%s</rss>" % out).xml(pretty: true)
512
- end
513
-
514
- def xpath(x)
515
- @doc.root.xpath x
516
- end
517
-
518
- def xslt=(value)
519
-
520
- self.summary.merge!({xslt: value})
521
- @xslt = value
522
- end
523
-
524
- private
525
-
526
- def add_id(a)
527
- @default_key = :uid
528
- @summary[:default_key] = 'uid'
529
- @fields << :uid
530
- a.each.with_index{|x,i| x << (i+1).to_s}
531
- end
532
-
533
- def create_find(fields)
534
-
535
- methods = fields.map do |field|
536
- "def find_by_#{field}(value) findx_by('#{field}', value) end\n" + \
537
- "def find_all_by_#{field}(value) findx_all_by('#{field}', value) end"
538
- end
539
- self.instance_eval(methods.join("\n"))
540
- end
541
-
542
- def findx_by(field, value)
543
- (load_records; rebuild_doc) if @dirty_flag == true
544
- r = @doc.root.element("records/*[#{field}='#{value}']")
545
- r ? recordx_to_record(r) : nil
546
- end
547
-
548
- def findx_all_by(field, value)
549
- @doc.root.xpath("records/*[#{field}='#{value}']").map {|x| recordx_to_record x}
550
- end
551
-
552
- def recordx_to_record(recordx)
553
- RecordX.new(Hash[*@fields.zip(recordx.xpath("*/text()")).flatten], self, recordx.attributes[:id])
554
- end
555
-
556
- def hash_create(raw_params={}, id=nil)
557
-
558
- record = make_record(raw_params)
559
- @doc.root.element('records').add record
560
-
561
- end
562
-
563
- def capture_fields(params)
564
- fields = Hash[@fields.map {|x| [x,nil]}]
565
- fields.keys.each {|key| fields[key] = params[key.to_sym] if params.has_key? key.to_sym}
566
- fields
567
- end
568
-
569
- def display_xml(options={})
570
-
571
- opt = {unescape_html: false}.merge options
572
- load_records if @dirty_flag == true
573
- doc = rebuild_doc(:external)
574
- if opt[:unescape_html] == true then
575
- doc.content(opt) #jr230711 pretty: true
576
- else
577
- doc.xml(opt) #jr230711 pretty: true
578
- end
579
- end
580
-
581
- def make_record(raw_params)
582
-
583
- id = (@doc.root.xpath('max(records/*/attribute::id)') || '0').succ unless id
584
- raw_params.merge!(uid: id) if @default_key == :uid
585
- params = Hash[raw_params.keys.map(&:to_sym).zip(raw_params.values)]
586
-
587
- fields = capture_fields(params)
588
- record = Rexle::Element.new @record_name
589
-
590
- fields.each do |k,v|
591
- element = Rexle::Element.new(k.to_s)
592
- element.root.text = v.to_s.gsub('<','&lt;').gsub('>','&gt;') if v
593
- record.add element if record
594
- end
595
-
596
-
597
-
598
- attributes = {id: id.to_s, created: Time.now.to_s, last_modified: nil}
599
- attributes.each {|k,v| record.add_attribute(k, v)}
600
-
601
- record
602
- end
603
-
604
- alias refresh_doc display_xml
605
-
606
- def string_parse(buffer)
607
-
608
- buffer.gsub!("\r",'')
609
- buffer.gsub!(/\n-{4,}\n/,"\n\n")
610
- buffer.gsub!(/---\n/m, "--- ")
611
-
612
- buffer.gsub!(/.>/) {|x| x[0] != '?' ? x.sub(/>/,'&gt;') : x }
613
- buffer.gsub!(/<./) {|x| x[1] != '?' ? x.sub(/</,'&lt;') : x }
614
-
615
- @raw_header = buffer[/<\?dynarex[^>]+>/]
616
-
617
- if buffer[/<\?/] then
618
-
619
- raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
620
- @xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
621
- @raw_header = buffer.slice!(/<\?dynarex[^>]+>/) + "\n"
622
-
623
- header = @raw_header[/<?dynarex (.*)?>/,1]
624
-
625
- r1 = /([\w\-]+\s*\=\s*'[^']*)'/
626
- r2 = /([\w\-]+\s*\=\s*"[^"]*)"/
627
-
628
- r = header.scan(/#{r1}|#{r2}/).map(&:compact).flatten
629
- r.each do |x|
630
- attr, val = x.split(/\s*=\s*["']/,2)
631
- self.method((attr + '=').to_sym).call(unescape val)
632
- end
633
-
634
- end
635
-
636
- # if records already exist find the max id
637
- i = @doc.root.xpath('max(records/*/attribute::id)').to_i
638
-
639
- raw_summary = schema[/\[([^\]]+)/,1]
640
- #rowx = buffer[/--\+.*/m]
641
-
642
- #buffer = rowx if rowx
643
-
644
- #jr061013 raw_lines = buffer.gsub(/^\s*#[^\n]+/,'').lines.to_a
645
- raw_lines = buffer.lines.to_a
646
-
647
- if raw_summary then
648
-
649
- a_summary = raw_summary.split(',').map(&:strip)
650
-
651
- @summary ||= {}
652
- raw_lines.shift while raw_lines.first.strip.empty?
653
-
654
- # fetch any summary lines
655
- while not raw_lines.empty? and \
656
- raw_lines.first[/#{a_summary.join('|')}:\s+\S+/] do
657
-
658
- label, val = raw_lines.shift.chomp.match(/(\w+):\s+([^$]+)$/).captures
659
- @summary[label.to_sym] = val
660
- end
661
-
662
- self.xslt = @summary[:xslt] || @summary[:xsl] if @summary[:xslt]\
663
- or @summary[:xsl]
664
- end
665
-
666
-
667
- if @type == 'checklist' then
668
-
669
- # extract the brackets from the line
670
-
671
- checked = []
672
- raw_lines.map! do |x|
673
- raw_checked, raw_line = x.partition(/\]/).values_at 0,2
674
- checked << (raw_checked[/x/] ? true : false)
675
- raw_line
676
- end
677
-
678
- end
679
-
680
- if @order == 'descending' then
681
- rl = raw_lines
682
-
683
- if rl.first =~ /--/ then
684
- raw_lines = [rl[0]] + rl[1..-1].each_slice(@fields.count).inject([])\
685
- {|r,x| r += x.reverse }.reverse
686
- else
687
- raw_lines = rl.each_slice(@fields.count).inject([])\
688
- {|r,x| r += x.reverse }.reverse
689
- end
690
- checked.reverse! if @type == 'checklist'
691
-
692
- end
693
-
694
- @summary[:recordx_type] = 'dynarex'
695
- @summary[:schema] = @schema
696
- @summary[:format_mask] = @format_mask
697
-
698
- raw_lines.shift while raw_lines.first.strip.empty?
699
-
700
- lines = case raw_lines.first.chomp
701
-
702
- when '---'
703
-
704
- yaml = YAML.load raw_lines.join("\n")
705
-
706
- yamlize = lambda {|x| (x.is_a? Array) ? x.to_yaml : x}
707
-
708
- yprocs = {
709
- Hash: lambda {|y|
710
- y.map do |k,v|
711
- procs = {Hash: proc {|x| x.values}, Array: proc {v}}
712
- values = procs[v.class.to_s.to_sym].call(v).map(&yamlize)
713
- [k, *values]
714
- end
715
- },
716
- Array: lambda {|y| y.map {|x2| x2.map(&yamlize)} }
717
- }
718
-
719
- yprocs[yaml.class.to_s.to_sym].call yaml
720
-
721
- when '--+'
722
-
723
- self.summary[:rawdoc_type] = 'rowx'
724
- raw_lines.shift
725
-
726
- a3 = raw_lines.join.strip.split(/\n\n(?=\w+:)/)
727
-
728
- # get the fields
729
- a4 = a3.map{|x| x.scan(/^\w+(?=:)/)}.flatten(1).uniq
730
-
731
- abbrv_fields = a4.all? {|x| x.length == 1}
732
-
1
+ #!/usr/bin/env ruby
2
+
3
+ # file: dynarex.rb
4
+
5
+ require 'open-uri'
6
+ require 'dynarex-import'
7
+ require 'line-tree'
8
+ require 'rexle'
9
+ require 'rexle-builder'
10
+ require 'rexslt'
11
+ require 'dynarex-xslt'
12
+ require 'recordx'
13
+ require 'rxraw-lineparser'
14
+ require 'yaml'
15
+ require 'rowx'
16
+ require 'nokogiri'
17
+ require 'ostruct'
18
+ require 'table-formatter'
19
+
20
+
21
+ class DynarexException < Exception
22
+ end
23
+
24
+
25
+ class Dynarex
26
+
27
+ attr_accessor :format_mask, :delimiter, :xslt_schema, :schema,
28
+ :order, :type, :limit_by, :xslt
29
+
30
+
31
+ #Create a new dynarex document from 1 of the following options:
32
+ #* a local file path
33
+ #* a URL
34
+ #* a schema string
35
+ # Dynarex.new 'contacts[title,description]/contact(name,age,dob)'
36
+ #* an XML string
37
+ # Dynarex.new '<contacts><summary><schema>contacts/contact(name,age,dob)</schema></summary><records/></contacts>'
38
+
39
+ def initialize(rawx=nil)
40
+ #puts Rexle.version
41
+ @delimiter = ''
42
+ openx(rawx.clone) if rawx
43
+ if @order == 'descending' then
44
+ @records = records_to_h(:descending)
45
+ rebuild_doc
46
+ end
47
+ #jr240913 @dirty_flag = false
48
+ end
49
+
50
+ def add(x)
51
+ @doc.root.add x
52
+ @dirty_flag = true
53
+ self
54
+ end
55
+
56
+ def all()
57
+ @doc.root.xpath("records/*").map {|x| recordx_to_record x}
58
+ end
59
+
60
+ def delimiter=(separator)
61
+
62
+ @delimiter = separator
63
+
64
+ if separator.length > 0 then
65
+ @summary[:delimiter] = separator
66
+ else
67
+ @summary.delete :delimiter
68
+ end
69
+
70
+ @format_mask = @format_mask.to_s.gsub(/\s/, separator)
71
+ @summary[:format_mask] = @format_mask
72
+ end
73
+
74
+ def foreign_import(options={})
75
+ o = {xml: '', schema: ''}.merge(options)
76
+ h = {xml: o[:xml], schema: @schema, foreign_schema: o[:schema]}
77
+ buffer = DynarexImport.new(h).to_xml
78
+
79
+ openx(buffer)
80
+ self
81
+ end
82
+
83
+ def fields
84
+ @fields
85
+ end
86
+
87
+ def format_mask=(s)
88
+ @format_mask = s
89
+ @summary[:format_mask] = @format_mask
90
+ end
91
+
92
+ def insert(raw_params)
93
+ record = make_record(raw_params)
94
+ @doc.root.element('records/*').insert_before record
95
+ @dirty_flag = true
96
+ end
97
+
98
+ def inspect()
99
+ "<object #%s>" % [self.object_id]
100
+ end
101
+
102
+ def order=(value)
103
+
104
+ self.summary.merge!({order: value})
105
+ if @order == 'ascending' and value == 'descending' then
106
+ sort_records
107
+ elsif @order == 'descending' and value == 'ascending'
108
+ sort_records
109
+ end
110
+ @order = value
111
+ end
112
+
113
+ def schema=(s)
114
+ openx s
115
+ end
116
+
117
+ def type=(v)
118
+ @order = 'descending' if v == 'feed'
119
+ @type = v
120
+ @summary[:type] = v
121
+ end
122
+
123
+ def limit_by=(val)
124
+ @limit_by = val.to_i
125
+ end
126
+
127
+ # Returns the hash representation of the document summary.
128
+ #
129
+ def summary
130
+ @summary
131
+ end
132
+
133
+ # Return a Hash (which can be edited) containing all records.
134
+ #
135
+ def records
136
+
137
+ load_records if @dirty_flag == true
138
+
139
+ if block_given? then
140
+ yield(@records)
141
+ rebuild_doc
142
+ @dirty_flag = true
143
+ else
144
+ @records
145
+ end
146
+
147
+ end
148
+
149
+ # Returns a ready-only snapshot of records as a simple Hash.
150
+ #
151
+ def flat_records
152
+ load_records if @dirty_flag == true
153
+ @flat_records
154
+ end
155
+
156
+ alias to_h flat_records
157
+ alias to_a flat_records
158
+
159
+ # Returns an array snapshot of OpenStruct records
160
+ #
161
+ def ro_records
162
+ flat_records.map {|record| OpenStruct.new record }
163
+ end
164
+
165
+ # Returns all records as a string format specified by the summary format_mask field.
166
+
167
+ def to_doc
168
+ @doc
169
+ end
170
+
171
+ def to_s
172
+
173
+ xsl_buffer =<<EOF
174
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
175
+ <xsl:output encoding="UTF-8"
176
+ method="text"
177
+ indent="no"
178
+ omit-xml-declaration="yes"/>
179
+
180
+ <xsl:template match="*">
181
+ <xsl:for-each select="records/*">[!regex_values]<xsl:text>
182
+ </xsl:text>
183
+ </xsl:for-each>
184
+ </xsl:template>
185
+ </xsl:stylesheet>
186
+ EOF
187
+
188
+
189
+ #format_mask = XPath.first(@doc.root, 'summary/format_mask/text()').to_s
190
+ #format_mask = @doc.root.element('summary/format_mask/text()')
191
+
192
+ raw_summary_fields = self.summary[:schema][/^\w+\[([^\]]+)\]/,1]
193
+ sumry = ''
194
+
195
+ if raw_summary_fields then
196
+ summary_fields = raw_summary_fields.split(',') # .map(&:to_sym)
197
+ sumry = summary_fields.map {|x| x.strip!; x + ': ' + \
198
+ self.summary[x.to_sym]}.join("\n") + "\n"
199
+ end
200
+
201
+ if @raw_header then
202
+ declaration = @raw_header
203
+ else
204
+
205
+ smry_fields = %i(schema)
206
+ if self.delimiter.length > 0 then
207
+ smry_fields << :delimiter
208
+ else
209
+ smry_fields << :format_mask
210
+ end
211
+ s = smry_fields.map {|x| "%s=\"%s\"" % \
212
+ [x, self.send(x).gsub('"', '\"') ]}.join ' '
213
+ #declaration = "<?dynarex %s ?>" % s
214
+ declaration = %Q(<?dynarex %s?>\n) % s
215
+ end
216
+
217
+ header = declaration + sumry
218
+
219
+ if self.summary[:rawdoc_type] == 'rowx' then
220
+ a = self.fields.map do |field|
221
+ "<xsl:if test=\"%s != ''\">
222
+ <xsl:text>\n</xsl:text>%s: <xsl:value-of select='%s'/>
223
+ </xsl:if>" % ([field]*3)
224
+ end
225
+
226
+ xslt_format = a.join
227
+
228
+ xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
229
+ xslt = Nokogiri::XSLT(xsl_buffer)
230
+ out = xslt.transform(Nokogiri::XML(@doc.to_s))
231
+
232
+ header + "\n--+\n" + out.text
233
+
234
+ elsif self.delimiter.length > 0 then
235
+
236
+ tfo = TableFormatter.new border: false, nowrap: true, divider: self.delimiter
237
+ tfo.source = self.to_h.map{|x| x.values}
238
+ header + tfo.display
239
+
240
+ else
241
+
242
+ format_mask = self.format_mask
243
+ format_mask.gsub!(/\[[^!\]]+\]/) {|x| x[1] }
244
+ xslt_format = format_mask.gsub(/\s(?=\[!\w+\])/,'<xsl:text> </xsl:text>')
245
+ .gsub(/\[!(\w+)\]/, '<xsl:value-of select="\1"/>')
246
+
247
+ xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
248
+ xslt = Nokogiri::XSLT(xsl_buffer)
249
+
250
+ out = xslt.transform(Nokogiri::XML(self.to_xml))
251
+ header + "\n" + out.text
252
+ end
253
+
254
+ #xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
255
+ #xslt = Nokogiri::XSLT(xsl_buffer)
256
+ #out = xslt.transform(Nokogiri::XML(@doc.to_s))
257
+ #jr250811 puts 'xsl_buffer: ' + xsl_buffer
258
+ #jr250811 puts 'doc_to_s: ' + @doc.to_s
259
+ #out.text
260
+ #jr231211 Rexslt.new(xsl_buffer, @doc.to_s).to_s
261
+
262
+ end
263
+
264
+ def to_xml(opt={})
265
+ opt = {pretty: true} if opt == :pretty
266
+ display_xml(opt)
267
+ end
268
+
269
+ #Save the document to a local file.
270
+
271
+ def save(filepath=nil, options={})
272
+
273
+ opt = {pretty: true}.merge options
274
+ filepath ||= @local_filepath
275
+ @local_filepath = filepath
276
+ xml = display_xml(opt)
277
+ buffer = block_given? ? yield(xml) : xml
278
+ File.open(filepath,'w'){|f| f.write buffer }
279
+ end
280
+
281
+ #Parses 1 or more lines of text to create or update existing records.
282
+
283
+ def parse(x=nil)
284
+ if x.is_a? String then
285
+ buffer = x.clone
286
+ buffer = yield if block_given?
287
+ string_parse buffer
288
+ else
289
+ foreign_import x
290
+ end
291
+ end
292
+
293
+
294
+ alias import parse
295
+
296
+ #Create a record from a hash containing the field name, and the field value.
297
+ # dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
298
+ # dynarex.create name: Bob, age: 52
299
+
300
+ def create(arg, id=nil)
301
+ raise 'Dynarex#create(): input error: no arg provided' unless arg
302
+ #jr291012 rebuild_doc()
303
+ #jr291012 (load_records; rebuild_doc) if @dirty_flag == true
304
+ methods = {Hash: :hash_create, String: :create_from_line}
305
+ send (methods[arg.class.to_s.to_sym]), arg, id
306
+
307
+ #jr291012load_records
308
+ @dirty_flag = true
309
+
310
+ self
311
+ end
312
+
313
+ #Create a record from a string, given the dynarex document contains a format mask.
314
+ # dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
315
+ # dynarex.create_from_line 'Tracy 37 15-Jun-1972'
316
+
317
+ def create_from_line(line, id=nil)
318
+ t = @format_mask.to_s.gsub(/\[!(\w+)\]/, '(.*)').sub(/\[/,'\[').sub(/\]/,'\]')
319
+ line.match(/#{t}/).captures
320
+
321
+ a = line.match(/#{t}/).captures
322
+ h = Hash[@fields.zip(a)]
323
+ create h
324
+ self
325
+ end
326
+
327
+ def default_key=(id)
328
+ @default_key = id.to_sym
329
+ @summary[:default_key] = id
330
+ @fields << id.to_sym
331
+ end
332
+
333
+
334
+ #Updates a record from an id and a hash containing field name and field value.
335
+ # dynarex.update 4, name: Jeff, age: 38
336
+
337
+ def update(id, params={})
338
+ fields = capture_fields(params)
339
+
340
+
341
+ # for each field update each record field
342
+ record = @doc.root.element("records/#{@record_name}[@id='#{id.to_s}']")
343
+ fields.each {|k,v| record.element(k.to_s).text = v if v}
344
+ record.add_attribute(last_modified: Time.now.to_s)
345
+
346
+ @dirty_flag = true
347
+
348
+ self
349
+
350
+ end
351
+
352
+
353
+ #Delete a record.
354
+ # dyarex.delete 3 # deletes record with id 3
355
+
356
+ def delete(x)
357
+
358
+ if x.to_i.to_s == x.to_s and x[/[0-9]/] then
359
+ @doc.root.delete("records/*[@id='#{x}']")
360
+ else
361
+ @doc.delete x
362
+ end
363
+ @dirty_flag = true
364
+ self
365
+ end
366
+
367
+ def element(x)
368
+ @doc.root.element x
369
+ end
370
+
371
+ def sort_by!(&element_blk)
372
+ refresh_doc
373
+ a = @doc.root.xpath('records/*').sort_by &element_blk
374
+ @doc.root.delete('records')
375
+
376
+ records = Rexle::Element.new 'records'
377
+
378
+ a.each {|record| records.add record}
379
+
380
+ @doc.root.add records
381
+
382
+ load_records
383
+ self
384
+ end
385
+
386
+ def rebuild_doc(state=:internal)
387
+
388
+ reserved_keywords = (
389
+ Object.public_methods | \
390
+ Kernel.public_methods | \
391
+ public_methods + [:method_missing]
392
+ )
393
+
394
+ xml = RexleBuilder.new
395
+
396
+ a = xml.send @root_name do
397
+
398
+ xml.summary do
399
+
400
+ @summary.each do |key,value|
401
+
402
+ xml.send key, value.gsub('>','&gt;')\
403
+ .gsub('<','&lt;')\
404
+ .gsub(/(&\s|&[a-zA-Z\.]+;?)/) {|x| x[-1] == ';' ? x : x.sub('&','&amp;')}
405
+
406
+ end
407
+ end
408
+
409
+ records = @records.to_a
410
+
411
+ if records then
412
+
413
+ records.reverse! if @order == 'descending' and state == :external
414
+
415
+ xml.records do
416
+
417
+ records.each do |k, item|
418
+ #p 'foo ' + item.inspect
419
+ xml.send(@record_name, {id: item[:id], created: item[:created], \
420
+ last_modified: item[:last_modified]}, '') do
421
+ item[:body].each do |name,value|
422
+ #name = name.to_s.prepend('._').to_sym if reserved_keywords.include? name
423
+ name = ('._' + name.to_s).to_sym if reserved_keywords.include? name
424
+ val = value.send(value.is_a?(String) ? :to_s : :to_yaml)
425
+ xml.send(name, val.gsub('>','&gt;')\
426
+ .gsub('<','&lt;')\
427
+ .gsub(/(&\s|&[a-zA-Z\.]+;?)/) do |x|
428
+ x[-1] == ';' ? x : x.sub('&','&amp;')
429
+ end
430
+ )
431
+ end
432
+ end
433
+ end
434
+
435
+ end
436
+ else
437
+ xml.records
438
+ end # end of if @records
439
+ end
440
+
441
+ doc = Rexle.new(a)
442
+
443
+ if @xslt then
444
+ doc.instructions = [['xml-stylesheet',
445
+ "title='XSL_formatting' type='text/xsl' href='#{@xslt}'"]]
446
+ end
447
+
448
+ return doc if state != :internal
449
+ @doc = doc
450
+ end
451
+
452
+ def record(id)
453
+ recordx_to_record @doc.root.element("records/*[@id='#{id}']")
454
+ end
455
+
456
+ alias find record
457
+
458
+ def record_exists?(id)
459
+ !@doc.root.element("records/*[@id='#{id}']").nil?
460
+ end
461
+
462
+ def to_xslt(opt={})
463
+
464
+ h = {limit: -1}.merge(opt)
465
+ xslt_schema = @xslt_schema || self.summary[:xslt_schema]
466
+ raise 'to_xsl(): xslt_schema nil' unless xslt_schema
467
+
468
+ xslt = DynarexXSLT.new(schema: @schema, xslt_schema: @xslt_schema ).to_xslt
469
+ return xslt
470
+ end
471
+
472
+ def to_rss(opt={}, xslt=nil)
473
+
474
+ unless xslt then
475
+
476
+ h = {limit: 11}.merge(opt)
477
+ doc = Rexle.new(self.to_xslt)
478
+
479
+ e = doc.element('//xsl:apply-templates[2]')
480
+ doc2 = Rexle.new "<xsl:sort order='descending' data-type='number' select='@id'/>"
481
+ e.add doc2.root
482
+
483
+ e2 = doc.root.element('xsl:template[3]')
484
+ item = e2.element('item')
485
+ new_item = item.deep_clone
486
+ item.delete
487
+
488
+ xslif = Rexle.new("<xsl:if test='position() &lt; #{h[:limit]}'/>").root
489
+
490
+
491
+ pubdate = Rexle.new("<pubDate><xsl:value-of select='pubDate'></xsl:value-of></pubDate>").root
492
+ new_item.add pubdate
493
+ xslif.add new_item
494
+
495
+ e2.add xslif.root
496
+ xslt = doc.xml
497
+ end
498
+
499
+ doc = self.to_doc
500
+ doc.root.xpath('records/*').each do |x|
501
+ raw_dt = DateTime.parse x.attributes[:created]
502
+ dt = raw_dt.strftime("%a, %d %b %Y %H:%M:%S %z")
503
+ x.add Rexle::Element.new('pubDate').add_text dt.to_s
504
+ end
505
+
506
+ #File.open('dynarex.xsl','w'){|f| f.write xslt}
507
+ #File.open('dynarex.xml','w'){|f| f.write doc.xml}
508
+ #xml = Rexslt.new(xslt, doc.xml).to_s
509
+ #=begin
510
+ xslt = Nokogiri::XSLT(xslt)
511
+ out = xslt.transform(Nokogiri::XML(doc.root.xml))
512
+ #=end
513
+
514
+ #Rexle.new("<rss version='2.0'>%s</rss>" % xml).xml(pretty: true)
515
+ Rexle.new("<rss version='2.0'>%s</rss>" % out).xml(pretty: true)
516
+ end
517
+
518
+ def xpath(x)
519
+ @doc.root.xpath x
520
+ end
521
+
522
+ def xslt=(value)
523
+
524
+ self.summary.merge!({xslt: value})
525
+ @xslt = value
526
+ end
527
+
528
+ private
529
+
530
+ def add_id(a)
531
+ @default_key = :uid
532
+ @summary[:default_key] = 'uid'
533
+ @fields << :uid
534
+ a.each.with_index{|x,i| x << (i+1).to_s}
535
+ end
536
+
537
+ def create_find(fields)
538
+
539
+ methods = fields.map do |field|
540
+ "def find_by_#{field}(value) findx_by('#{field}', value) end\n" + \
541
+ "def find_all_by_#{field}(value) findx_all_by('#{field}', value) end"
542
+ end
543
+ self.instance_eval(methods.join("\n"))
544
+ end
545
+
546
+ def findx_by(field, value)
547
+ (load_records; rebuild_doc) if @dirty_flag == true
548
+ r = @doc.root.element("records/*[#{field}='#{value}']")
549
+ r ? recordx_to_record(r) : nil
550
+ end
551
+
552
+ def findx_all_by(field, value)
553
+ @doc.root.xpath("records/*[#{field}='#{value}']").map {|x| recordx_to_record x}
554
+ end
555
+
556
+ def recordx_to_record(recordx)
557
+ RecordX.new(Hash[*@fields.zip(recordx.xpath("*/text()")).flatten], self, recordx.attributes[:id])
558
+ end
559
+
560
+ def hash_create(raw_params={}, id=nil)
561
+
562
+ record = make_record(raw_params)
563
+ @doc.root.element('records').add record
564
+
565
+ end
566
+
567
+ def capture_fields(params)
568
+ fields = Hash[@fields.map {|x| [x,nil]}]
569
+ fields.keys.each {|key| fields[key] = params[key.to_sym] if params.has_key? key.to_sym}
570
+ fields
571
+ end
572
+
573
+ def display_xml(options={})
574
+
575
+ opt = {unescape_html: false}.merge options
576
+ load_records if @dirty_flag == true
577
+ doc = rebuild_doc(:external)
578
+ if opt[:unescape_html] == true then
579
+ doc.content(opt) #jr230711 pretty: true
580
+ else
581
+ doc.xml(opt) #jr230711 pretty: true
582
+ end
583
+ end
584
+
585
+ def make_record(raw_params)
586
+
587
+ id = (@doc.root.xpath('max(records/*/attribute::id)') || '0').succ unless id
588
+ raw_params.merge!(uid: id) if @default_key == :uid
589
+ params = Hash[raw_params.keys.map(&:to_sym).zip(raw_params.values)]
590
+
591
+ fields = capture_fields(params)
592
+ record = Rexle::Element.new @record_name
593
+
594
+ fields.each do |k,v|
595
+ element = Rexle::Element.new(k.to_s)
596
+ element.root.text = v.to_s.gsub('<','&lt;').gsub('>','&gt;') if v
597
+ record.add element if record
598
+ end
599
+
600
+
601
+
602
+ attributes = {id: id.to_s, created: Time.now.to_s, last_modified: nil}
603
+ attributes.each {|k,v| record.add_attribute(k, v)}
604
+
605
+ record
606
+ end
607
+
608
+ alias refresh_doc display_xml
609
+
610
+ def string_parse(buffer)
611
+
612
+ buffer.gsub!("\r",'')
613
+ buffer.gsub!(/\n-{4,}\n/,"\n\n")
614
+ buffer.gsub!(/---\n/m, "--- ")
615
+
616
+ buffer.gsub!(/.>/) {|x| x[0] != '?' ? x.sub(/>/,'&gt;') : x }
617
+ buffer.gsub!(/<./) {|x| x[1] != '?' ? x.sub(/</,'&lt;') : x }
618
+
619
+ @raw_header = buffer[/<\?dynarex[^>]+>/]
620
+
621
+ if buffer[/<\?/] then
622
+
623
+ raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
624
+ @xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
625
+ @raw_header = buffer.slice!(/<\?dynarex[^>]+>/) + "\n"
626
+
627
+ header = @raw_header[/<?dynarex (.*)?>/,1]
628
+
629
+ r1 = /([\w\-]+\s*\=\s*'[^']*)'/
630
+ r2 = /([\w\-]+\s*\=\s*"[^"]*)"/
631
+
632
+ r = header.scan(/#{r1}|#{r2}/).map(&:compact).flatten
633
+ r.each do |x|
634
+ attr, val = x.split(/\s*=\s*["']/,2)
635
+ self.method((attr + '=').to_sym).call(unescape val)
636
+ end
637
+
638
+ end
639
+
640
+ # if records already exist find the max id
641
+ i = @doc.root.xpath('max(records/*/attribute::id)').to_i
642
+
643
+ raw_summary = schema[/\[([^\]]+)/,1]
644
+ #rowx = buffer[/--\+.*/m]
645
+
646
+ #buffer = rowx if rowx
647
+
648
+ #jr061013 raw_lines = buffer.gsub(/^\s*#[^\n]+/,'').lines.to_a
649
+ raw_lines = buffer.lines.to_a
650
+
651
+ if raw_summary then
652
+
653
+ a_summary = raw_summary.split(',').map(&:strip)
654
+
655
+ @summary ||= {}
656
+ raw_lines.shift while raw_lines.first.strip.empty?
657
+
658
+ # fetch any summary lines
659
+ while not raw_lines.empty? and \
660
+ raw_lines.first[/#{a_summary.join('|')}:\s+\S+/] do
661
+
662
+ label, val = raw_lines.shift.chomp.match(/(\w+):\s+([^$]+)$/).captures
663
+ @summary[label.to_sym] = val
664
+ end
665
+
666
+ self.xslt = @summary[:xslt] || @summary[:xsl] if @summary[:xslt]\
667
+ or @summary[:xsl]
668
+ end
669
+
670
+
671
+ if @type == 'checklist' then
672
+
673
+ # extract the brackets from the line
674
+
675
+ checked = []
676
+ raw_lines.map! do |x|
677
+ raw_checked, raw_line = x.partition(/\]/).values_at 0,2
678
+ checked << (raw_checked[/x/] ? true : false)
679
+ raw_line
680
+ end
681
+
682
+ end
683
+
684
+ if @order == 'descending' then
685
+ rl = raw_lines
686
+
687
+ if rl.first =~ /--/ then
688
+ raw_lines = [rl[0]] + rl[1..-1].each_slice(@fields.count).inject([])\
689
+ {|r,x| r += x.reverse }.reverse
690
+ else
691
+ raw_lines = rl.each_slice(@fields.count).inject([])\
692
+ {|r,x| r += x.reverse }.reverse
693
+ end
694
+ checked.reverse! if @type == 'checklist'
695
+
696
+ end
697
+
698
+ @summary[:recordx_type] = 'dynarex'
699
+ @summary[:schema] = @schema
700
+ @summary[:format_mask] = @format_mask
701
+
702
+ raw_lines.shift while raw_lines.first.strip.empty?
703
+
704
+ lines = case raw_lines.first.chomp
705
+
706
+ when '---'
707
+
708
+ yaml = YAML.load raw_lines.join("\n")
709
+
710
+ yamlize = lambda {|x| (x.is_a? Array) ? x.to_yaml : x}
711
+
712
+ yprocs = {
713
+ Hash: lambda {|y|
714
+ y.map do |k,v|
715
+ procs = {Hash: proc {|x| x.values}, Array: proc {v}}
716
+ values = procs[v.class.to_s.to_sym].call(v).map(&yamlize)
717
+ [k, *values]
718
+ end
719
+ },
720
+ Array: lambda {|y| y.map {|x2| x2.map(&yamlize)} }
721
+ }
722
+
723
+ yprocs[yaml.class.to_s.to_sym].call yaml
724
+
725
+ when '--+'
726
+
727
+ self.summary[:rawdoc_type] = 'rowx'
728
+ raw_lines.shift
729
+
730
+ a3 = raw_lines.join.strip.split(/\n\n(?=\w+:)/)
731
+
732
+ # get the fields
733
+ a4 = a3.map{|x| x.scan(/^\w+(?=:)/)}.flatten(1).uniq
734
+
735
+ abbrv_fields = a4.all? {|x| x.length == 1}
736
+
733
737
  a5 = a3.map do |xlines|
734
-
738
+
735
739
  missing_fields = a4 - xlines.scan(/^\w+(?=:)/)
736
-
740
+
737
741
  r = xlines.split(/\n(\w+:.*)/m)
738
- missing_fields.map!{|x| x + ":"}
739
- key = (abbrv_fields ? @fields[0].to_s[0] : @fields.first.to_s) + ':'
740
-
741
- if missing_fields.include? key
742
- r.unshift key
743
- missing_fields.delete key
744
- end
745
-
746
- r += missing_fields
747
- r.join("\n")
748
-
749
- end
750
-
751
- xml = RowX.new(a5.join("\n").strip, level: 0).to_xml
752
-
753
- a2 = Rexle.new(xml).root.xpath('item').inject([]) do |r,x|
754
-
755
- r << @fields.map do |field|
756
- x.text(abbrv_fields ? field.to_s.chr : field.to_s )
757
- end
758
-
759
- end
760
-
761
- a2.compact!
762
-
763
- # if there is no field value for the first field then
764
- # the default_key is invalid. The default_key is changed to an ID.
765
- if a2.detect {|x| x.first == ''} then
766
- add_id(a2)
767
- else
768
-
769
- a3 = a2.map(&:first)
770
- add_id(a2) if a3 != a3.uniq
771
-
772
- end
773
-
774
- a2
775
-
776
- else
777
-
778
- raw_lines = raw_lines.join("\n").gsub(/^\s*#[^\n]+/,'').lines.to_a
779
- a2 = raw_lines.map.with_index do |x,i|
780
-
781
- next if x[/^\s+$|\n\s*#/]
782
-
783
- begin
784
-
785
- field_names, field_values = RXRawLineParser.new(@format_mask).parse(x)
786
- rescue
787
- raise "input file parser error at line " + (i + 1).to_s + ' --> ' + x
788
- end
789
- field_values
790
- end
791
-
792
- a2.compact!
793
- a3 = a2.compact.map(&:first)
794
- add_id(a2) if a3 != a3.uniq
795
-
796
- a2
797
- end
798
-
799
- a = lines.map.with_index do |x,i|
800
- created = Time.now.to_s
801
-
802
- h = Hash[
803
- @fields.zip(
804
- x.map do |t|
805
-
806
- t.to_s[/^---(?:\s|\n)/] ? YAML.load(t[/^---(?:\s|\n)(.*)/,1]) : unescape(t.to_s)
807
- end
808
- )
809
- ]
810
- h[@fields.last] = checked[i].to_s if @type == 'checklist'
811
- [h[@default_key], {id: '', created: created, last_modified: '', body: h}]
812
- end
813
-
814
- h2 = Hash[a]
815
-
816
- #replace the existing records hash
817
- h = @records
818
- i = 0
819
- h2.each do |key,item|
820
- if h.has_key? key then
821
-
822
- # overwrite the previous item and change the timestamps
823
- h[key][:last_modified] = item[:created]
824
- item[:body].each do |k,v|
825
- h[key][:body][k.to_sym] = v
826
- end
827
- else
828
- item[:id] = (@order == 'descending' ? (h2.count) - i : i+ 1).to_s
829
- i += 1
830
- h[key] = item.clone
831
- end
832
- end
833
-
834
- h.each {|key, item| h.delete(key) if not h2.has_key? key}
835
- #refresh_doc
836
- #load_records
837
- @flat_records = @records.values.map{|x| x[:body]}
838
- @flat_records = @flat_records.take @limit_by if @limit_by
839
-
840
- rebuild_doc
841
- self
842
- end
843
-
844
- def unescape(s)
845
- s.gsub('&lt;', '<').gsub('&gt;','>')
846
- end
847
-
848
- def dynarex_new(s)
849
- @schema = s
850
- ptrn = %r((\w+)\[?([^\]]+)?\]?\/(\w+)\(([^\)]+)\))
851
-
852
- if s.match(ptrn) then
853
- @root_name, raw_summary, record_name, raw_fields = s.match(ptrn).captures
854
- summary, fields = [raw_summary || '',raw_fields].map {|x| x.split(/,/).map &:strip}
855
- create_find fields
856
-
857
- reserved = %w(require parent)
858
- raise 'reserved keyword' if (fields & reserved).any?
859
-
860
- else
861
- ptrn = %r((\w+)\[?([^\]]+)?\]?)
862
- @root_name, raw_summary = s.match(ptrn).captures
863
- summary = raw_summary.split(/,/).map &:strip
864
-
865
- end
866
-
867
- format_mask = fields ? fields.map {|x| "[!%s]" % x}.join(' ') : ''
868
-
869
- @summary = Hash[summary.zip([''] * summary.length).flatten.each_slice(2)\
870
- .map{|x1,x2| [x1.to_sym,x2]}]
871
- @summary.merge!({recordx_type: 'dynarex', format_mask: format_mask, schema: s})
872
- @records = {}
873
- @flat_records = {}
874
-
875
- rebuild_doc
876
-
877
- end
878
-
879
- def attach_record_methods()
880
- create_find @fields
881
- end
882
-
883
- def openx(s)
884
-
885
- if s[/</] then # xml
886
-
887
- buffer = s
888
- elsif s[/[\[\(]/] # schema
889
- dynarex_new(s)
890
- elsif s[/^https?:\/\//] then # url
891
- buffer = Kernel.open(s, 'UserAgent' => 'Dynarex-Reader'){|x| x.read}
892
- else # local file
893
- @local_filepath = s
894
- buffer = File.open(s,'r').read
895
- end
896
-
897
- if buffer then
898
- raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
899
- @xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
900
-
901
- @doc = Rexle.new(buffer) unless @doc
902
- end
903
-
904
- @schema = @doc.root.text('summary/schema')
905
- @root_name = @doc.root.name
906
- @summary = summary_to_h
907
-
908
- @order = @summary[:order] if @summary.has_key? :order
909
-
910
- @default_key = @doc.root.element('summary/default_key/text()')
911
- @format_mask = @doc.root.element('summary/format_mask/text()')
912
-
913
- @fields = @schema[/([^(]+)\)$/,1].split(/\s*,\s*/).map(&:to_sym)
914
-
915
- @fields << @default_key if @default_key and \
916
- !@fields.include? @default_key.to_sym
917
-
918
- if @schema and @schema.match(/(\w+)\(([^\)]+)/) then
919
- @record_name, raw_fields = @schema.match(/(\w+)\(([^\)]+)/).captures
920
- @fields = raw_fields.split(',').map{|x| x.strip.to_sym} unless @fields
921
- end
922
-
923
- if @fields then
924
-
925
- @default_key = @fields[0] unless @default_key
926
- # load the record query handler methods
927
- attach_record_methods
928
- else
929
-
930
- #jr080912 @default_key = @doc.root.xpath('records/*/*').first.name
931
- @default_key = @doc.root.element('records/./.[1]').name
932
- end
933
-
934
- @summary[:default_key] = @default_key.to_s
935
-
936
- if @doc.root.xpath('records/*').length > 0 then
937
- @record_name = @doc.root.element('records/*[1]').name
938
- #jr240913 load_records
939
- @dirty_flag = true
940
- end
941
-
942
- end
943
-
944
- def load_records
945
-
946
- @records = records_to_h
947
-
948
- @records = @records.take @limit_by if @limit_by
949
-
950
- @records.instance_eval do
951
- def delete_item(i)
952
- self.delete self.keys[i]
953
- end
954
- end
955
-
956
- #Returns a ready-only snapshot of records as a simple Hash.
957
- @flat_records = @records.values.map{|x| x[:body]}
958
- @dirty_flag = false
959
- end
960
-
961
- def display()
962
- puts @doc.to_s
963
- end
964
-
965
- def records_to_h(state=:ascending)
966
-
967
- i = @doc.root.xpath('max(records/*/attribute::id)') || 0
968
- #jr090813 fields = @doc.root.text('summary/schema')[/\(.*\)/].scan(/\w+/)
969
- records = @doc.root.xpath('records/*')
970
-
971
- recs = (state == :descending ? records.reverse : records)
972
- a = recs.inject({}) do |result,row|
973
-
974
- created = Time.now.to_s
975
- last_modified = ''
976
-
977
- if row.attributes[:id] then
978
- id = row.attributes[:id]
979
- else
980
- i += 1; id = i.to_s
981
- end
982
-
983
- created = row.attributes[:created] if row.attributes[:created]
984
- last_modified = row.attributes[:last_modified] if row.attributes[:last_modified]
985
-
986
- body = @fields.inject({}) do |r,field|
987
-
988
- node = row.element field.to_s
989
-
990
- if node then
991
- text = node.text ? node.text.unescape : ''
992
-
993
- r.merge node.name.to_sym => (text[/^---(?:\s|\n)/] ? YAML.load(text[/^---(?:\s|\n)(.*)/,1]) : text)
994
- else
995
- r
996
- end
997
- end
998
-
999
- result.merge body[@default_key.to_sym] => {id: id, created: created, last_modified: last_modified, body: body}
1000
- end
1001
-
1002
- end
1003
-
1004
- def sort_records
1005
- xsl =<<XSL
1006
- <?xml version="1.0" encoding="UTF-8"?>
1007
- <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
1008
-
1009
- <xsl:template match="*">
1010
- <xsl:element name="{name()}"><xsl:text>
1011
- </xsl:text>
1012
- <xsl:copy-of select="summary"/><xsl:text>
1013
- </xsl:text>
1014
- <xsl:apply-templates select="records"/>
1015
- </xsl:element>
1016
- </xsl:template>
1017
- <xsl:template match="records">
1018
- <records><xsl:text>
1019
- </xsl:text>
1020
- <xsl:for-each select="child::*">
1021
- <xsl:sort order="descending"/>
1022
- <xsl:text> </xsl:text><xsl:copy-of select="."/><xsl:text>
1023
- </xsl:text>
1024
- </xsl:for-each>
1025
- </records><xsl:text>
1026
- </xsl:text>
1027
- </xsl:template>
1028
-
1029
- </xsl:stylesheet>
1030
- XSL
1031
-
1032
- @doc = Rexle.new(Rexslt.new(xsl, self.to_xml).to_s)
1033
- @dirty_flag = true
1034
- end
1035
-
1036
- def summary_to_h
1037
-
1038
- @doc.root.xpath('summary/*').inject({}) do |r,node|
1039
- r.merge node.name.to_s.to_sym => node.text.to_s
1040
- end
1041
- end
1042
-
1043
- end
742
+ missing_fields.map!{|x| x + ":"}
743
+ key = (abbrv_fields ? @fields[0].to_s[0] : @fields.first.to_s) + ':'
744
+
745
+ if missing_fields.include? key
746
+ r.unshift key
747
+ missing_fields.delete key
748
+ end
749
+
750
+ r += missing_fields
751
+ r.join("\n")
752
+
753
+ end
754
+
755
+ xml = RowX.new(a5.join("\n").strip, level: 0).to_xml
756
+
757
+ a2 = Rexle.new(xml).root.xpath('item').inject([]) do |r,x|
758
+
759
+ r << @fields.map do |field|
760
+ x.text(abbrv_fields ? field.to_s.chr : field.to_s )
761
+ end
762
+
763
+ end
764
+
765
+ a2.compact!
766
+
767
+ # if there is no field value for the first field then
768
+ # the default_key is invalid. The default_key is changed to an ID.
769
+ if a2.detect {|x| x.first == ''} then
770
+ add_id(a2)
771
+ else
772
+
773
+ a3 = a2.map(&:first)
774
+ add_id(a2) if a3 != a3.uniq
775
+
776
+ end
777
+
778
+ a2
779
+
780
+ else
781
+
782
+ raw_lines = raw_lines.join("\n").gsub(/^\s*#[^\n]+/,'').lines.to_a
783
+ a2 = raw_lines.map.with_index do |x,i|
784
+
785
+ next if x[/^\s+$|\n\s*#/]
786
+
787
+ begin
788
+
789
+ field_names, field_values = RXRawLineParser.new(@format_mask).parse(x)
790
+ rescue
791
+ raise "input file parser error at line " + (i + 1).to_s + ' --> ' + x
792
+ end
793
+ field_values
794
+ end
795
+
796
+ a2.compact!
797
+ a3 = a2.compact.map(&:first)
798
+ add_id(a2) if a3 != a3.uniq
799
+
800
+ a2
801
+ end
802
+
803
+ a = lines.map.with_index do |x,i|
804
+ created = Time.now.to_s
805
+
806
+ h = Hash[
807
+ @fields.zip(
808
+ x.map do |t|
809
+
810
+ t.to_s[/^---(?:\s|\n)/] ? YAML.load(t[/^---(?:\s|\n)(.*)/,1]) : unescape(t.to_s)
811
+ end
812
+ )
813
+ ]
814
+ h[@fields.last] = checked[i].to_s if @type == 'checklist'
815
+ [h[@default_key], {id: '', created: created, last_modified: '', body: h}]
816
+ end
817
+
818
+ h2 = Hash[a]
819
+
820
+ #replace the existing records hash
821
+ h = @records
822
+ i = 0
823
+ h2.each do |key,item|
824
+ if h.has_key? key then
825
+
826
+ # overwrite the previous item and change the timestamps
827
+ h[key][:last_modified] = item[:created]
828
+ item[:body].each do |k,v|
829
+ h[key][:body][k.to_sym] = v
830
+ end
831
+ else
832
+ item[:id] = (@order == 'descending' ? (h2.count) - i : i+ 1).to_s
833
+ i += 1
834
+ h[key] = item.clone
835
+ end
836
+ end
837
+
838
+ h.each {|key, item| h.delete(key) if not h2.has_key? key}
839
+ #refresh_doc
840
+ #load_records
841
+ @flat_records = @records.values.map{|x| x[:body]}
842
+ @flat_records = @flat_records.take @limit_by if @limit_by
843
+
844
+ rebuild_doc
845
+ self
846
+ end
847
+
848
+ def unescape(s)
849
+ s.gsub('&lt;', '<').gsub('&gt;','>')
850
+ end
851
+
852
+ def dynarex_new(s)
853
+ @schema = s
854
+ ptrn = %r((\w+)\[?([^\]]+)?\]?\/(\w+)\(([^\)]+)\))
855
+
856
+ if s.match(ptrn) then
857
+ @root_name, raw_summary, record_name, raw_fields = s.match(ptrn).captures
858
+ summary, fields = [raw_summary || '',raw_fields].map {|x| x.split(/,/).map &:strip}
859
+ create_find fields
860
+
861
+ reserved = %w(require parent)
862
+ raise 'reserved keyword' if (fields & reserved).any?
863
+
864
+ else
865
+ ptrn = %r((\w+)\[?([^\]]+)?\]?)
866
+ @root_name, raw_summary = s.match(ptrn).captures
867
+ summary = raw_summary.split(/,/).map &:strip
868
+
869
+ end
870
+
871
+ format_mask = fields ? fields.map {|x| "[!%s]" % x}.join(' ') : ''
872
+
873
+ @summary = Hash[summary.zip([''] * summary.length).flatten.each_slice(2)\
874
+ .map{|x1,x2| [x1.to_sym,x2]}]
875
+ @summary.merge!({recordx_type: 'dynarex', format_mask: format_mask, schema: s})
876
+ @records = {}
877
+ @flat_records = {}
878
+
879
+ rebuild_doc
880
+
881
+ end
882
+
883
+ def attach_record_methods()
884
+ create_find @fields
885
+ end
886
+
887
+ def openx(s)
888
+
889
+ if s[/</] then # xml
890
+
891
+ buffer = s
892
+ elsif s[/[\[\(]/] # schema
893
+ dynarex_new(s)
894
+ elsif s[/^https?:\/\//] then # url
895
+ buffer = Kernel.open(s, 'UserAgent' => 'Dynarex-Reader'){|x| x.read}
896
+ else # local file
897
+ @local_filepath = s
898
+ raise DynarexException, 'file not found: ' + s
899
+ buffer = File.read s
900
+ end
901
+
902
+ if buffer then
903
+ raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
904
+ @xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
905
+
906
+ @doc = Rexle.new(buffer) unless @doc
907
+ end
908
+
909
+ @schema = @doc.root.text('summary/schema')
910
+ @root_name = @doc.root.name
911
+ @summary = summary_to_h
912
+
913
+ @order = @summary[:order] if @summary.has_key? :order
914
+
915
+ @default_key = @doc.root.element('summary/default_key/text()')
916
+ @format_mask = @doc.root.element('summary/format_mask/text()')
917
+
918
+ @fields = @schema[/([^(]+)\)$/,1].split(/\s*,\s*/).map(&:to_sym)
919
+
920
+ @fields << @default_key if @default_key and \
921
+ !@fields.include? @default_key.to_sym
922
+
923
+ if @schema and @schema.match(/(\w+)\(([^\)]+)/) then
924
+ @record_name, raw_fields = @schema.match(/(\w+)\(([^\)]+)/).captures
925
+ @fields = raw_fields.split(',').map{|x| x.strip.to_sym} unless @fields
926
+ end
927
+
928
+ if @fields then
929
+
930
+ @default_key = @fields[0] unless @default_key
931
+ # load the record query handler methods
932
+ attach_record_methods
933
+ else
934
+
935
+ #jr080912 @default_key = @doc.root.xpath('records/*/*').first.name
936
+ @default_key = @doc.root.element('records/./.[1]').name
937
+ end
938
+
939
+ @summary[:default_key] = @default_key.to_s
940
+
941
+ if @doc.root.xpath('records/*').length > 0 then
942
+ @record_name = @doc.root.element('records/*[1]').name
943
+ #jr240913 load_records
944
+ @dirty_flag = true
945
+ end
946
+
947
+ end
948
+
949
+ def load_records
950
+
951
+ @records = records_to_h
952
+
953
+ @records = @records.take @limit_by if @limit_by
954
+
955
+ @records.instance_eval do
956
+ def delete_item(i)
957
+ self.delete self.keys[i]
958
+ end
959
+ end
960
+
961
+ #Returns a ready-only snapshot of records as a simple Hash.
962
+ @flat_records = @records.values.map{|x| x[:body]}
963
+ @dirty_flag = false
964
+ end
965
+
966
+ def display()
967
+ puts @doc.to_s
968
+ end
969
+
970
+ def records_to_h(state=:ascending)
971
+
972
+ i = @doc.root.xpath('max(records/*/attribute::id)') || 0
973
+ #jr090813 fields = @doc.root.text('summary/schema')[/\(.*\)/].scan(/\w+/)
974
+ records = @doc.root.xpath('records/*')
975
+
976
+ recs = (state == :descending ? records.reverse : records)
977
+ a = recs.inject({}) do |result,row|
978
+
979
+ created = Time.now.to_s
980
+ last_modified = ''
981
+
982
+ if row.attributes[:id] then
983
+ id = row.attributes[:id]
984
+ else
985
+ i += 1; id = i.to_s
986
+ end
987
+
988
+ created = row.attributes[:created] if row.attributes[:created]
989
+ last_modified = row.attributes[:last_modified] if row.attributes[:last_modified]
990
+
991
+ body = @fields.inject({}) do |r,field|
992
+
993
+ node = row.element field.to_s
994
+
995
+ if node then
996
+ text = node.text ? node.text.unescape : ''
997
+
998
+ r.merge node.name.to_sym => (text[/^---(?:\s|\n)/] ? YAML.load(text[/^---(?:\s|\n)(.*)/,1]) : text)
999
+ else
1000
+ r
1001
+ end
1002
+ end
1003
+
1004
+ result.merge body[@default_key.to_sym] => {id: id, created: created, last_modified: last_modified, body: body}
1005
+ end
1006
+
1007
+ end
1008
+
1009
+ def sort_records
1010
+ xsl =<<XSL
1011
+ <?xml version="1.0" encoding="UTF-8"?>
1012
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
1013
+
1014
+ <xsl:template match="*">
1015
+ <xsl:element name="{name()}"><xsl:text>
1016
+ </xsl:text>
1017
+ <xsl:copy-of select="summary"/><xsl:text>
1018
+ </xsl:text>
1019
+ <xsl:apply-templates select="records"/>
1020
+ </xsl:element>
1021
+ </xsl:template>
1022
+ <xsl:template match="records">
1023
+ <records><xsl:text>
1024
+ </xsl:text>
1025
+ <xsl:for-each select="child::*">
1026
+ <xsl:sort order="descending"/>
1027
+ <xsl:text> </xsl:text><xsl:copy-of select="."/><xsl:text>
1028
+ </xsl:text>
1029
+ </xsl:for-each>
1030
+ </records><xsl:text>
1031
+ </xsl:text>
1032
+ </xsl:template>
1033
+
1034
+ </xsl:stylesheet>
1035
+ XSL
1036
+
1037
+ @doc = Rexle.new(Rexslt.new(xsl, self.to_xml).to_s)
1038
+ @dirty_flag = true
1039
+ end
1040
+
1041
+ def summary_to_h
1042
+
1043
+ @doc.root.xpath('summary/*').inject({}) do |r,node|
1044
+ r.merge node.name.to_s.to_sym => node.text.to_s
1045
+ end
1046
+ end
1047
+
1048
+ end