dynarex 1.2.97 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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