infod 0.0.3.1 → 0.0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,73 +1,89 @@
1
- #watch __FILE__
1
+ watch __FILE__
2
2
  class E
3
3
 
4
4
  Prototypes = {
5
5
  SIOCt+'MicroblogPost' => [Content],
6
- SIOCt+'BlogPost' => [Date, Title, Content], nil=>nil,
7
- 'blank'=>[]
6
+ SIOCt+'BlogPost' => [Title, Content],
7
+ SIOCt+'WikiArticle' => [Title, Content],
8
8
  }
9
9
 
10
- F['protograph/editable'] = F['protograph/_']
10
+ # 404 -> create resource
11
+ F['protograph/create'] = -> e,env,g {
12
+ env['view'] = 'create'
13
+ F['protograph/blank'][e,env,g]}
11
14
 
12
- fn 'graph/editable',->e,env,g{
13
- e.fromStream g, :triplrFsStore}
14
-
15
- # select a prototype graph
16
- # , or go blank
15
+ # prototype select
17
16
  fn 'view/create',->g,e{
18
- [H.css('/css/create'),{_: :b, c: :create},
19
- Prototypes.map{|s,_| s.nil? ? {_: :b, c: ' '} : {_: :a, href: e['REQUEST_PATH']+'?graph=edit&prototype='+(CGI.escape s), c: s.label}}]}
17
+ [{_: :style, c: 'a {display:block;font-size:2em}'},{_: :b, c: :type},
18
+ Prototypes.map{|t,ps|
19
+ {_: :a, href: e['REQUEST_PATH']+'?graph=edit&prototype='+(CGI.escape t), c: t.label}}]}
20
20
 
21
+ # editable triples
22
+ F['protograph/edit'] = -> e,env,g {
23
+ env['view'] ||= 'edit' # use edit-view
24
+ g[e.uri+'#'] = {} # add current resource
25
+ rand.to_s.h}
26
+
27
+ fn 'graph/edit',->e,env,g{
28
+ e.fromStream g, :triplrDoc} # add fs-sourced triples
29
+
30
+ =begin HTML <form> RDF editor
31
+ arg
32
+ prototype - initialize fields for a resource-type
33
+ predicate - initialize field for a particular predicate
34
+ =end
21
35
  fn 'view/edit',->g,e{
36
+
37
+ # render a triple
22
38
  triple = ->s,p,o{
23
39
  if s && p && o
24
40
  s = s.E
25
41
  p = p.E
26
- oE = p.literal o
27
- (id = s.concatURI(p).concatURI oE
28
- [(case p
42
+ oE = p.literal o # cast literal to URI
43
+ id = s.concatURI(p).concatURI oE # triple identifier
44
+ [(case p.uri # more to support here.. http://dev.w3.org/html5/markup/input.html#input
29
45
  when Content
30
- {_: :textarea, name: id, c: o, rows: 24, cols: 80}
46
+ [{_: :textarea, name: id, c: o, rows: 16, cols: 80},
47
+ '<br>',o]
31
48
  when Date
32
49
  {_: :input, name: id, type: :datetime, value: o.empty? ? Time.now.iso8601 : o}
33
50
  else
34
51
  {_: :input, name: id, value: o, size: 54}
35
52
  end
36
- ),"<br>\n"]) if oE
53
+ ),"<br>\n"]
37
54
  end}
38
-
39
- ps = []
40
- e.q['prototype'].do{|pr| Prototypes[pr].do{|v|
41
- g[e['uri']+'#'] ||= {}
42
- ps.concat v }}
43
- e.q['p'].do{|p| ps.push p }
55
+
56
+ ps = [] # predicates to go editable on
57
+ e.q['prototype'].do{|pr|
58
+ Prototypes[pr].do{|v|ps.concat v }} # prototype imports
59
+ e.q['predicate'].do{|p|ps.push p } # explicit predicate
44
60
 
45
- [(H.once e, 'edit', (H.css '/css/edit')),
61
+ [{_: :style, c: ".abbr {display: none}\ntd {vertical-align:top}"},
46
62
  {_: :form, name: :editor, method: :POST, action: e['REQUEST_PATH'],
47
63
 
48
- # each resource
49
- c: [ g.map{|s,r|
50
- url = s.E.localURL e
51
- # per-resource links
52
- {class: :resource, c:
53
- [{_: :a, class: :uri, id: s, c: s, href: url},
54
- {_: :a, class: :edit, c: '+predicate', href: url+'?graph=_&view=addP'},'<br><br>',
55
-
56
- # each property
57
- # r.keys.concat(ps).uniq.-(['uri']).map{|p|
58
- (r.keys.concat(ps).uniq.map{|p|
59
- [{_: :b, c: p}, '<br>',
60
- r[p].do{|o| [*o].map{|o|triple[s,p,o]}}, # existing triples
61
- triple[e['uri'],p,''], '<br>']} if r.class==Hash)]} if s.match(/#/)}, # create triple
62
- {_: :input, type: :submit, value: 'save'}]}]}
64
+ c: [{_: :a, class: :edit, c: '+add field',
65
+ href: e['uri']+'?graph=blank&view=addP', style: 'background-color:#0f0;border-radius:5em;color:#000;padding:.5em'},
66
+ g.map{|s,r| # subject
67
+ uri = s.E.localURL e
68
+ {_: :table, style: 'background-color:#eee',
69
+ c: [{_: :tr, c: {_: :td, colspan: 2, c: {_: :a, class: :uri, id: s, c: s, href: uri}}},
70
+ {_: :input, type: :hidden, name: s.E.concatURI(Edit.E).concatURI(E['/']), value: e['uri']+'?graph=edit'},
71
+ r.keys.-([Edit]).concat(ps).uniq.map{|p| # resource + prototype/initialize predicates
72
+ {_: :tr,
73
+ c: [{_: :td, c: {_: :a, title: p, href: p, c: p.abbrURI}}, # property
74
+ {_: :td,
75
+ c: [r[p].do{|o| # objects
76
+ (o.class == Array ? o : [o]).map{|o| # each object
77
+ triple[s,p,o]}}, # existing triples
78
+ triple[s,p,'']]}]}}]}}, # new triple
79
+ {_: :input, type: :submit, value: 'save'}]}]}
63
80
 
64
81
  # select a property to edit
65
82
  fn 'view/addP',->g,e{
66
- [(H.once e, 'edit', (H.css '/css/edit')),
67
- [Date,Title,Creator,Content,Label].map{|p|{_: :a, href: p, c: p.label+' '}},
68
-
83
+ [[Date,Title,Creator,Content,Label].map{|p|[{_: :a, href: p, c: p},'<br>']},
69
84
  {_: :form, action: e['REQUEST_PATH'], method: :GET,
70
- c: [{_: :input, type: :url, name: :p, pattern: '^http.*$', size: 64},
85
+ c: [{_: :input, type: :url, name: :predicate, pattern: '^http.*$', size: 64},
86
+ {_: :input, type: :hidden, name: :graph, value: :edit},
71
87
  {_: :input, type: :submit, value: 'property'}]}]}
72
88
 
73
89
  end
@@ -14,33 +14,45 @@ module FeedParse
14
14
  end) end
15
15
 
16
16
  def parse
17
-
18
- #prefix table
19
- x={}
17
+ x={} # populate XMLns prefix table
20
18
  match(/<(rdf|rss|feed)([^>]+)/i)[2].scan(/xmlns:?([a-z]+)?=["']?([^'">\s]+)/){|m|x[m[0]]=m[1]}
21
19
 
22
- #items
23
- scan(%r{<(rss:|atom:)?(item|entry)([\s][^>]*)?>(.*?)</\1?\2>}mi){|m|
24
-
25
- # identifier select -> RDF URI || <id> || <link>
26
- u = m[2] && (u = m[2].match /about=["']?([^'">\s]+)/) && u[1] ||
27
- m[3] && (((u = m[3].match /<(gu)?id[^>]*>([^<]+)/) || (u = m[3].match /<(link)>([^<]+)/)) && u[2])
28
-
29
- yield u, E::Type, (E::SIOCt+'BlogPost').E
30
- yield u, E::Type, (E::SIOC+'Post').E
31
-
32
- #links
33
- m[3].scan(%r{<(link|enclosure|media)([^>]+)>}mi){|e|
34
- e[1].match(/(href|url|src)=['"]?([^'">\s]+)/).do{|url|
35
- yield(u,E::Atom+'/link/'+((r=e[1].match(/rel=['"]?([^'">\s]+)/)) ? r[1] : e[0]), url[2].E)}}
36
-
37
- #elements
38
- m[3].scan(%r{<([a-z]+:)?([a-z]+)([\s][^>]*)?>(.*?)</\1?\2>}mi){|e|
39
- yield u, # s
40
- (x[e[0]&&e[0].chop]||E::RSS)+e[1], # p
41
- e[3].extend(FeedParse).guess.do{|o|# o
42
- o.match(/\A(\/|http)[\S]+\Z/) ? o.E : o
43
- }}}
20
+ # scan for resources
21
+ scan(%r{<(?<ns>rss:|atom:)?(?<tag>item|entry)(?<attrs>[\s][^>]*)?>(?<inner>.*?)</\k<ns>?\k<tag>>}mi){|m|
22
+ # identifier search
23
+ attrs = m[2]
24
+ inner = m[3]
25
+ u = attrs.do{|a| # RDF-style identifier (RSS 1.0)
26
+ a.match(/about=["']?([^'">\s]+)/).do{|s|
27
+ s[1] }} ||
28
+ (inner.match(/<link>([^<]+)/) || # <link> child-node or href attribute
29
+ inner.match(/<link[^>]+rel=["']?alternate["']?[^>]+href=["']?([^'">\s]+)/) ||
30
+ inner.match(/<(?:gu)?id[^>]*>([^<]+)/)).do{|s| s[1]} # <id> child
31
+
32
+ if u
33
+ if !u.match /^http/
34
+ puts "no HTTP URIs found #{u}"
35
+ u = '/junk/'+u.gsub('/','.')
36
+ end
37
+ yield u, E::Type, (E::SIOCt+'BlogPost').E
38
+ yield u, E::Type, (E::SIOC+'Post').E
39
+
40
+ #links
41
+ inner.scan(%r{<(link|enclosure|media)([^>]+)>}mi){|e|
42
+ e[1].match(/(href|url|src)=['"]?([^'">\s]+)/).do{|url|
43
+ yield(u,E::Atom+'/link/'+((r=e[1].match(/rel=['"]?([^'">\s]+)/)) ? r[1] : e[0]), url[2].E)}}
44
+
45
+ #elements
46
+ inner.scan(%r{<([a-z]+:)?([a-z]+)([\s][^>]*)?>(.*?)</\1?\2>}mi){|e|
47
+ yield u, # s
48
+ (x[e[0]&&e[0].chop]||E::RSS)+e[1], # p
49
+ e[3].extend(FeedParse).guess.do{|o|# o
50
+ o.match(/\A(\/|http)[\S]+\Z/) ? o.E : E::F['cleanHTML'][o]
51
+ }}
52
+ else
53
+ puts "no post-identifiers found #{u}"
54
+ end
55
+ }
44
56
 
45
57
  nil
46
58
  end
@@ -56,8 +68,24 @@ class E
56
68
  def listFeeds; (nokogiri.css 'link[rel=alternate]').map{|u|E (URI uri).merge(u.attr :href)} end
57
69
  alias_method :feeds, :listFeeds
58
70
 
59
- def getFeed g; insertDocs :triplrFeed, g end
60
- def getFeedReddit g; insertDocs :triplrFeedReddit, g end
71
+ # add existing resources to index
72
+ #
73
+ # 'http:/'.E.take.select{|e|e.ext=='e'}.map{|r|E::FeedArchiver[r,r.graph,'localhost']}
74
+
75
+ FeedArchiver = -> doc, graph, host {
76
+ doc.roonga host
77
+ graph.map{|u,r|
78
+ r[Date].do{|t| # link doc to date-index
79
+ t = t[0].gsub(/[-T]/,'/').sub /(.00.00|Z)$/, '' # trim normalized timezones and non-unique symbols
80
+ stop = /\b(at|blog|com(ments)?|html|info|org|photo|p|post|r|status|tag|twitter|wordpress|www|1999|2005)\b/
81
+ b = (u.sub(/http:\/\//,'.').gsub(/\W/,'..').gsub(stop,'').sub(/\d{12,}/,'')+'.').gsub /\.+/,'.'
82
+ doc.ln E["http://#{host}/news/#{t}#{b}e"]}}
83
+ doc}
84
+
85
+ GREP_DIRS.push /^\/news\/\d{4}/
86
+
87
+ def getFeed g; addDocs :triplrFeed, g, nil, FeedArchiver end
88
+ def getFeedReddit g; addDocs :triplrFeedReddit, g, nil, FeedArchiver end
61
89
 
62
90
  def triplrFeed &f
63
91
  dateNorm :contentURIresolve,:triplrFeedNormalize,:triplrFeedRaw,&f
@@ -50,10 +50,12 @@ class E
50
50
  end
51
51
  alias_method :m, :mtime
52
52
 
53
- def triplrInode children=true
54
- e && (d? && (yield uri, Posix + 'dir#parent', parent
55
- children && c.map{|c| yield uri, Posix + 'dir#child', c})
56
- node.stat.do{|s|[:size,:ftype,:mtime].map{|p| yield uri, Stat+p.to_s, (s.send p)}})
53
+ def triplrInode
54
+ if d?
55
+ yield uri, Posix+'dir#parent', parent
56
+ c.map{|c| yield uri, Posix + 'dir#child', E[c.uri.gsub('?','%3F').gsub('#','23')]}
57
+ end
58
+ node.stat.do{|s|[:size,:ftype,:mtime].map{|p| yield uri, Stat+p.to_s, (s.send p)}}
57
59
  end
58
60
 
59
61
  def triplrSymlink
@@ -1,7 +1,6 @@
1
1
  #watch __FILE__
2
2
  class E
3
3
 
4
- # glob :: pattern -> [E]
5
4
  def glob p=""
6
5
  (Pathname.glob d + p).map &:E
7
6
  end
@@ -16,10 +15,10 @@ class E
16
15
 
17
16
  def docs
18
17
  base = docBase
19
- [(base if pathSegment!='/' && base.e),
20
- (self if base != self && e && uri[-1]!='/'),
18
+ [(base if pathSegment!='/' && base.e), # doc-base
19
+ (self if base != self && e && uri[-1]!='/'), # requested path
21
20
  base.glob(".{e,html,n3,nt,owl,rdf,ttl,txt}"), # docs
22
- ((d? && uri[-1]=='/' && uri.size>1) ? c : []) # trailing slash = children
21
+ ((d? && uri[-1]=='/' && uri.size>1) ? c : []) # trailing slash -> child resources
23
22
  ].flatten.compact
24
23
  end
25
24
 
@@ -1,17 +1,15 @@
1
1
  #watch __FILE__
2
2
  class E
3
3
  =begin
4
- graph resolution is two-pass
4
+ graph construction is two-pass:
5
5
 
6
- protograph/
7
- the first-pass will determine if the second-pass needs to run. an eTag will be derived from the return-value and any graph additions preserved for the next pass. ideal fingerprint sources include filestats, mtime checks, extremely trivial SPARQL queries, SHA160 hashes of in-RAM entities. you can define only a second or first-pass and get default behaviour for the other. for more ideas see <http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-25#section-2.3>
6
+ the first-pass will signify if the second-pass needs to be run. an eTag is be derived from the return-value, ideal fingerprint sources include filestats, mtime checks, extremely trivial SPARQL queries, SHA160 hashes of in-RAM entities.. <http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-25#section-2.3>
8
7
 
9
- graph/
10
- a second-pass might query a CBD (concise-bounded description) from a SPARQL store. infod was originally developed as an alternative to fragility & latency of relying on (large, hard-to-implement, must be running, configured & connectable) SPARQL stores by using the filesystem as much as possible, to experiment with hybrids like "touch" a file on successful POSTs so a store only has to be queried occasionally, and to facilitate simply hooking up bits of Ruby code to names rather than try to shoehorn what you're trying to say into some QueryLang where you're additionally without standard library functions necessitating more roundtrips and latency via marshalling/unmarshalling, parsing, loopback network-abstraction, nascent RDF-via-SPARQL-as-ORM-libs.. but go nuts experimenting w/ graph-handlers for this stuff,,i do..
8
+ second-pass might fetch RDF from a SPARQL store. this lib was developed as an alternative to relying on (large, hard-to-implement, must be running, configured & connectable) SPARQL stores by using the filesystem as much as possible, to experiment with hybrids like SPARQLING up a set of files to be returned in standard Apache-as-static-fileserver fashion, and to webize all sorts of non-RDF like email, directories, plain-text etc
11
9
 
12
- triple streams are functions which yield triples
13
- s,p - URI in String , o - Literal or URI (object must respond to #uri such as '/path'.E or {'uri' => '/some.n3'}
14
- these can be formed into pipelines. the data ingesting/massaging stream-processors in feed.rb are modularized this way
10
+ triple streams - a source function yields triples up to the caller as it finds them,
11
+ a function providing just a block (consume yielded values) is a sink, both is a filter
12
+ these can be stacked into pipelines. see the data-massaging stream-processors in feed.rb
15
13
 
16
14
  =end
17
15
 
@@ -29,36 +27,32 @@ class E
29
27
  * behave as normal triplr to caller, with
30
28
  side-effect of import/indexing to knowledgebase
31
29
  =end
32
- def insertDocs triplr, host, p=[], &b
30
+ def addDocs triplr, host, p=nil, hook=nil, &b
33
31
  graph = fromStream({},triplr)
34
32
  docs = {}
35
33
  graph.map{|u,r|
36
- e = u.E # resource
37
- doc = e.ef # doc
38
- doc.e || # exists?
39
- (docs[doc.uri] ||= {} # init doc graph
40
- docs[doc.uri][u] = r # add to graph
41
- p.map{|p| # each indexable property
42
- r[p].do{|v| # values exists?
43
- v.map{|o| # each value
44
- e.index p,o}}})}# add to property index
45
- docs.map{|doc,g|
46
- d = doc.E
47
- if !d.e
48
- d.w g, true # write doc
49
- d.roonga host # text index
50
- puts "#{doc} < #{g.keys.join ' '}"
51
- end}
52
- graph.triples &b if b # emit the triples
34
+ e = u.E # resource
35
+ doc = e.ef # doc
36
+ doc.e || # exists - we're nondestructive here
37
+ (docs[doc.uri] ||= {} # init doc-graph
38
+ docs[doc.uri][u] = r # add to graph
39
+ p && p.map{|p| # index predicate
40
+ r[p].do{|v|v.map{|o| # values exist?
41
+ e.index p,o}}})} # index triple
42
+ docs.map{|d,g| # resources in docs
43
+ d = d.E; puts "+doc #{d}"
44
+ d.w g,true # write
45
+ hook[d,g,host] if hook} # insert-hook
46
+ graph.triples &b if b # emit triples
53
47
  self
54
48
  end
55
49
 
56
- # default protograph - identity + resource-thunks
50
+ # default protograph - identity < lazy-expandable resource-thunks
57
51
  # Resource, Query, Graph -> graphID
58
52
  fn 'protograph/',->e,q,g{
59
53
  g['#'] = {'uri' => '#'}
60
54
  set = (q['set'] && F['set/'+q['set']] || F['set/'])[e,q,g]
61
- if set.empty?
55
+ if !set || set.empty?
62
56
  g.delete '#'
63
57
  else
64
58
  g['#'][RDFs+'member'] = set
@@ -96,31 +90,19 @@ class E
96
90
  # cleanup unexpanded thunks
97
91
  m.delete_if{|u,r|r.class==E}}
98
92
 
99
- fn 'filter/set',->e,m,r{
100
- # filter to RDFs set-members, gone will be:
101
- # data about docs containing the data
102
- # other fragments in a doc not matching search when indexed per-fragment
103
- f = m['#'].do{|c| c[RDFs+'member'].do{|m| m.map &:uri }} || [] # members
104
- m.keys.map{|u| m.delete u unless f.member? u}} # trim
105
-
106
93
  def graphFromFile g={}
107
- if !e
108
- puts "missing file! "+d
109
- return
110
- end
111
- _ = self
112
- triplr = @r.do{|r|
113
- r.q['triplr'].do{|t|
114
- respond_to?(t) && t }} || :triplrMIME
115
- unless ext=='e' # native graph-format
116
- _ = E '/E/rdf/' + [triplr,uri].h.dive
117
- unless _.e && _.m > m; # up to date?
118
- e = {} ; puts "< #{uri}"
119
- [:triplrInode,triplr].each{|t| fromStream e, t }
120
- _.w e, true
94
+ return unless e
95
+ doc = self
96
+ unless ext=='e' # already native-format
97
+ triplr = @r.do{|r|r.q['triplr'].do{|t| (respond_to? t) && t }} || :triplrMIME
98
+ doc = E '/E/rdf/' + [triplr,uri].h.dive
99
+ unless doc.e && doc.m > m; # freshness check
100
+ graph = {}
101
+ [:triplrInode,triplr].each{|t| fromStream graph, t }
102
+ doc.w graph, true
121
103
  end
122
104
  end
123
- g.mergeGraph _.r true
105
+ g.mergeGraph doc.r true
124
106
  end
125
107
 
126
108
  def graph g={}
@@ -128,4 +110,38 @@ class E
128
110
  g
129
111
  end
130
112
 
113
+ def triplrJSON
114
+ yield uri, '/application/json', (JSON.parse read) if e
115
+ rescue Exception => e
116
+ end
117
+
118
+ def to_json *a
119
+ to_h.to_json *a
120
+ end
121
+
122
+ fn Render+'application/json',->d,_=nil{[d].to_json}
123
+
124
+ end
125
+
126
+ class Hash
127
+
128
+ def graph g
129
+ g.merge!({uri=>self})
130
+ end
131
+
132
+ def mergeGraph g
133
+ g.triples{|s,p,o|
134
+ self[s] = {'uri' => s} unless self[s].class == Hash
135
+ self[s][p] ||= []
136
+ self[s][p].push o unless self[s][p].member? o } if g
137
+ self
138
+ end
139
+
140
+ def triples &f
141
+ map{|s,r|
142
+ r.map{|p,o|
143
+ o.class == Array ? o.each{|o| yield s,p,o} : yield(s,p,o) unless p=='uri'} if r.class == Hash
144
+ }
145
+ end
146
+
131
147
  end
@@ -1,12 +1,17 @@
1
1
  #watch __FILE__
2
2
  class E
3
3
 
4
+ fn 'view/'+Posix+'util#grep',-> d,e {{_: :form, c: [{_: :input, name: :q, style: 'font-size:2em'},{_: :input, type: :hidden, name: :set, value: :grep}]}}
5
+
6
+ GREP_DIRS=[]
7
+
4
8
  fn 'set/grep',->e,q,m{
5
- q['i'] ||= true # case-insensitive by default
6
- q['q'].do{|query|
7
- [e,e.pathSegment].compact.select(&:e).map{|e|
8
- grep = "grep -rl#{q.has_key?('i') ? 'i' : ''} #{query.sh} #{e.sh}"
9
- `#{grep}`}.map{|r|r.lines.to_a.map{|r|r.chomp.unpathFs}}.flatten}}
9
+ q['q'].do{|query| m[e.uri+'#grep'] = {Type => E[Posix+'util#grep']}
10
+ path = e.pathSegment
11
+ GREP_DIRS.find{|p|path.uri.match p}.do{|allow|
12
+ [e,path].compact.select(&:e).map{|e|
13
+ `grep -irl #{query.sh} #{e.sh} | head -n 200`}.map{|r|r.lines.to_a.map{|r|r.chomp.unpathFs}}.flatten
14
+ }||(puts "no grep available on #{path}")}}
10
15
 
11
16
  fn 'view/grep',->d,e{
12
17
  w = e.q['q']
@@ -24,8 +29,7 @@ class E
24
29
  # sequential pattern
25
30
  p = /#{w.join '.*'}/i
26
31
 
27
- [H.css('/css/search'), H.css('/css/grep'),
28
- F['view/'+Search][e.q,e],
32
+ [H.css('/css/grep'),
29
33
  {_: :style, c: c.values.map{|i|
30
34
  # word color
31
35
  b = rand(16777216)
@@ -54,8 +58,7 @@ class E
54
58
  # exerpt
55
59
  l[0..403].gsub(a){|g|
56
60
  H({_: :span, class: "w w#{c[g.downcase]}",c: g})}
57
- },"<br>"]]},
58
- {_: :a, class: :down, href: e['REQUEST_PATH']+e.q.except('view').qs, style: "background-color: #{E.cs}",c: '&darr;'}]
61
+ },"<br>"]]}]
59
62
  end }
60
63
 
61
64
  end