infod 0.0.2 → 0.0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/infod.rb +51 -12
  2. data/infod/{Th/404.rb → 404.rb} +4 -16
  3. data/infod/500.rb +53 -0
  4. data/infod/GET.rb +104 -0
  5. data/infod/HEAD.rb +23 -0
  6. data/infod/HTTP.rb +105 -0
  7. data/infod/{Th/PATCH.rb → PATCH.rb} +0 -0
  8. data/infod/POST.rb +34 -0
  9. data/infod/audio.rb +30 -0
  10. data/infod/blog.rb +34 -0
  11. data/infod/cal.rb +72 -0
  12. data/infod/{Es/code.rb → code.rb} +7 -4
  13. data/infod/constants.rb +55 -0
  14. data/infod/{Es/css.rb → css.rb} +0 -0
  15. data/infod/{Es/csv.rb → csv.rb} +0 -0
  16. data/infod/{Es/du.rb → du.rb} +0 -0
  17. data/infod/edit.rb +73 -0
  18. data/infod/{H/facets.rb → facets.rb} +20 -11
  19. data/infod/{Es/feed.rb → feed.rb} +17 -16
  20. data/infod/{Es/find.rb → find.rb} +2 -3
  21. data/infod/forum.rb +13 -0
  22. data/infod/{Es/fs.rb → fs.rb} +5 -2
  23. data/infod/glob.rb +26 -0
  24. data/infod/graph.rb +131 -0
  25. data/infod/{Es/grep.rb → grep.rb} +3 -3
  26. data/infod/{Es/groonga.rb → groonga.rb} +35 -26
  27. data/infod/{H/histogram.rb → histogram.rb} +23 -16
  28. data/infod/html.rb +231 -0
  29. data/infod/{Es/image.rb → image.rb} +16 -26
  30. data/infod/{Es/index.rb → index.rb} +44 -49
  31. data/infod/infod.rb +51 -12
  32. data/infod/json.rb +38 -0
  33. data/infod/{Es/kv.rb → kv.rb} +3 -9
  34. data/infod/{Y.rb → lambda.rb} +18 -1
  35. data/infod/ls.rb +49 -0
  36. data/infod/mail.rb +108 -0
  37. data/infod/{Es/man.rb → man.rb} +3 -15
  38. data/infod/{H/microblog.rb → microblog.rb} +22 -31
  39. data/infod/{K.rb → mime.rb} +68 -52
  40. data/infod/{N.rb → names.rb} +77 -56
  41. data/infod/page.rb +13 -0
  42. data/infod/postscript.rb +26 -0
  43. data/infod/rdf.rb +51 -0
  44. data/infod/{Rb.rb → ruby.rb} +18 -33
  45. data/infod/{Es/schema.rb → schema.rb} +23 -8
  46. data/infod/{Es/search.rb → search.rb} +5 -11
  47. data/infod/{Es/sh.rb → sh.rb} +0 -0
  48. data/infod/{Es/text.rb → text.rb} +33 -29
  49. data/infod/{H/threads.rb → threads.rb} +20 -27
  50. data/infod/{H/time.rb → time.rb} +14 -34
  51. data/infod/{H/wiki.rb → wiki.rb} +0 -0
  52. metadata +53 -64
  53. data/config.ru +0 -3
  54. data/infod/Es.rb +0 -31
  55. data/infod/Es/filter.rb +0 -75
  56. data/infod/Es/glob.rb +0 -22
  57. data/infod/Es/html.rb +0 -271
  58. data/infod/Es/in.rb +0 -68
  59. data/infod/Es/json.rb +0 -68
  60. data/infod/Es/ls.rb +0 -58
  61. data/infod/Es/mail.rb +0 -87
  62. data/infod/Es/mime.rb +0 -59
  63. data/infod/Es/out.rb +0 -52
  64. data/infod/Es/pager.rb +0 -34
  65. data/infod/Es/pdf.rb +0 -19
  66. data/infod/Es/rdf.rb +0 -35
  67. data/infod/H.rb +0 -15
  68. data/infod/H/audio.rb +0 -19
  69. data/infod/H/blog.rb +0 -15
  70. data/infod/H/cal.rb +0 -81
  71. data/infod/H/edit.rb +0 -88
  72. data/infod/H/forum.rb +0 -4
  73. data/infod/H/hf.rb +0 -114
  74. data/infod/H/mail.rb +0 -92
  75. data/infod/H/who.rb +0 -30
  76. data/infod/Th.rb +0 -36
  77. data/infod/Th/500.rb +0 -41
  78. data/infod/Th/GET.rb +0 -62
  79. data/infod/Th/HEAD.rb +0 -5
  80. data/infod/Th/POST.rb +0 -39
  81. data/infod/Th/perf.rb +0 -37
  82. data/infod/Th/uid.rb +0 -24
  83. data/infod/Th/util.rb +0 -89
@@ -13,6 +13,10 @@ class E
13
13
  end
14
14
  alias_method :no, :node
15
15
 
16
+ def inside
17
+ node.expand_path.to_s.index(FSbase) == 0
18
+ end
19
+
16
20
  def siblings
17
21
  parent.c
18
22
  end
@@ -113,7 +117,6 @@ class E
113
117
  if f
114
118
  p ? (JSON.parse readFile) : readFile
115
119
  else
116
- puts "tried to open #{d}"
117
120
  nil
118
121
  end
119
122
  rescue Exception => e
@@ -150,7 +153,7 @@ class Pathname
150
153
  end
151
154
 
152
155
  def deleteNode
153
- FileUtils.send file? ? :rm : :rmdir, self
156
+ FileUtils.send (file?||symlink?) ? :rm : :rmdir, self
154
157
  parent.deleteNode if parent.c.empty?
155
158
  end
156
159
 
data/infod/glob.rb ADDED
@@ -0,0 +1,26 @@
1
+ #watch __FILE__
2
+ class E
3
+
4
+ # glob :: pattern -> [E]
5
+ def glob p=""
6
+ (Pathname.glob d + p).map &:E
7
+ end
8
+
9
+ fn 'set/glob',->d,e=nil,_=nil{
10
+ p = [d,d.pathSegment].compact.map(&:glob).flatten[0..4e2].compact.partition &:inside
11
+ p[0] }
12
+
13
+ fn 'req/randomFile',->e,r{
14
+ g = F['set/glob'][e]
15
+ !g.empty? ? [302, {Location: g[rand g.length].uri}, []] : [404]}
16
+
17
+ def docs
18
+ base = docBase
19
+ [(base if pathSegment!='/' && base.e),
20
+ (self if base != self && e && uri[-1]!='/'),
21
+ base.glob(".{e,html,n3,nt,owl,rdf,ttl,txt}"), # docs
22
+ ((d? && uri[-1]=='/' && uri.size>1) ? c : []) # trailing slash = children
23
+ ].flatten.compact
24
+ end
25
+
26
+ end
data/infod/graph.rb ADDED
@@ -0,0 +1,131 @@
1
+ #watch __FILE__
2
+ class E
3
+ =begin
4
+ graph resolution is two-pass
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>
8
+
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..
11
+
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
15
+
16
+ =end
17
+
18
+ def fromStream m,*i
19
+ send(*i) do |s,p,o|
20
+ m[s] = {'uri' => s} unless m[s].class == Hash
21
+ m[s][p] ||= []
22
+ m[s][p].push o unless m[s][p].member? o
23
+ end; m
24
+ end
25
+
26
+ =begin
27
+ * stream triples into graph (memory)
28
+ * import missing resources to store (fs)
29
+ * behave as normal triplr to caller, with
30
+ side-effect of import/indexing to knowledgebase
31
+ =end
32
+ def insertDocs triplr, host, p=[], &b
33
+ graph = fromStream({},triplr)
34
+ docs = {}
35
+ 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
53
+ self
54
+ end
55
+
56
+ # default protograph - identity + resource-thunks
57
+ # Resource, Query, Graph -> graphID
58
+ fn 'protograph/',->e,q,g{
59
+ g['#'] = {'uri' => '#'}
60
+ set = (q['set'] && F['set/'+q['set']] || F['set/'])[e,q,g]
61
+ if set.empty?
62
+ g.delete '#'
63
+ else
64
+ g['#'][RDFs+'member'] = set
65
+ g['#'][Type] = E[HTTP+'Response']
66
+ set.map{|u| g[u.uri] = u } # thunk
67
+ end
68
+ F['docsID'][g,q]}
69
+
70
+ fn 'set/',->e,q,g{
71
+ s = []
72
+ s.concat e.docs
73
+ e.pathSegment.do{|p| s.concat p.docs }
74
+ # day-dir hinted pagination
75
+ e.env['REQUEST_PATH'].match(/(.*?\/)([0-9]{4})\/([0-9]{2})\/([0-9]{2})(.*)/).do{|m|
76
+ u = g['#']
77
+ t = ::Date.parse "#{m[2]}-#{m[3]}-#{m[4]}"
78
+ pp = m[1] + (t-1).strftime('%Y/%m/%d') + m[5]
79
+ np = m[1] + (t+1).strftime('%Y/%m/%d') + m[5]
80
+ u[Prev] = {'uri' => pp} if pp.E.e || E['http://' + e.env['SERVER_NAME'] + pp].e
81
+ u[Next] = {'uri' => np} if np.E.e || E['http://' + e.env['SERVER_NAME'] + np].e }
82
+ s }
83
+
84
+ # fs-derived ID for a resource-set
85
+ fn 'docsID',->g,q{
86
+ [q.has_key?('nocache').do{|_|rand},
87
+ g.sort.map{|u,r|
88
+ [u, r.respond_to?(:m) && r.m]}].h }
89
+
90
+ # default graph (filesystem store)
91
+ # to use a different default-graph function (w/o patching here, or querystring param), define a GET handler on / (or a subdir),
92
+ # update configuration such as q['graph'] = 'hexastore' and return false or call #response..
93
+ fn 'graph/',->e,q,m{
94
+ # force thunks
95
+ m.values.map{|r|(r.env e.env).graphFromFile m if r.class == E }
96
+ # cleanup unexpanded thunks
97
+ m.delete_if{|u,r|r.class==E}}
98
+
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
+ 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
121
+ end
122
+ end
123
+ g.mergeGraph _.r true
124
+ end
125
+
126
+ def graph g={}
127
+ docs.map{|d|d.graphFromFile g} # tripleStream -> graph
128
+ g
129
+ end
130
+
131
+ end
@@ -2,10 +2,10 @@
2
2
  class E
3
3
 
4
4
  fn 'set/grep',->e,q,m{
5
- q['i'] ||= true # normal people want case-insensitive, i think - comment to unstick
5
+ q['i'] ||= true # case-insensitive by default
6
6
  q['q'].do{|query|
7
7
  [e,e.pathSegment].compact.select(&:e).map{|e|
8
- grep = "grep -rl#{q.has_key?('i') && 'i'} #{query.sh} #{e.sh}" # ;puts grep
8
+ grep = "grep -rl#{q.has_key?('i') ? 'i' : ''} #{query.sh} #{e.sh}"
9
9
  `#{grep}`}.map{|r|r.lines.to_a.map{|r|r.chomp.unpathFs}}.flatten}}
10
10
 
11
11
  fn 'view/grep',->d,e{
@@ -25,7 +25,7 @@ class E
25
25
  p = /#{w.join '.*'}/i
26
26
 
27
27
  [H.css('/css/search'), H.css('/css/grep'),
28
- F['view/search/form'][e.q,e],
28
+ F['view/'+Search][e.q,e],
29
29
  {_: :style, c: c.values.map{|i|
30
30
  # word color
31
31
  b = rand(16777216)
@@ -1,10 +1,13 @@
1
1
  #watch __FILE__
2
2
  class E
3
- # ruby search-engine & column-store
3
+
4
+ # adaptor for ruby text-search-engine & column-store
4
5
  # http://groonga.org/ http://ranguba.org/
5
6
 
6
7
  # query
7
8
  fn 'protograph/roonga',->d,e,m{
9
+
10
+ # groonga engine
8
11
  ga = E.groonga
9
12
 
10
13
  # search expression
@@ -13,38 +16,43 @@ class E
13
16
  # context
14
17
  g = e["context"] || d.env['SERVER_NAME']
15
18
 
16
- # exec expression
17
- r = q ? ga.select{|r|(r['graph'] == g) & r["content"].match(q)} : # expression if exists
18
- ga.select{|r| r['graph'] == g} # ordered set (index date-range)
19
-
20
- # offset, size
21
- start = e['start'].do{|c| c.to_i.max(r.size - 1).min 0 } || 0
22
- c = (e['c']||e['count']).do{|c|c.to_i.max(10000).min(0)} || 8
23
-
24
- # are further results traversible?
25
- down = r.size > start+c
26
- up = !(start<=0)
19
+ begin
20
+ # execute
21
+ r = (q && !q.empty?) ? ga.select{|r|(r['graph'] == g) & r["content"].match(q)} : # expression if exists
22
+ ga.select{|r| r['graph'] == g} # ordered set (index date-range)
27
23
 
28
- # sort results
29
- r = r.sort(e.has_key?('score') ? [["_score"]] : [["time", "descending"]],:offset => start,:limit => c)
24
+ # offset, size
25
+ start = e['start'].do{|c| c.to_i.max(r.size - 1).min 0 } || 0
26
+ c = (e['c']||e['count']).do{|c|c.to_i.max(10000).min(0)} || 8
30
27
 
31
- # pagination resources
32
- m['prev']={'uri' => 'prev','url' => '/search','start' => start + c, 'c' => c} if down
33
- m['next']={'uri' => 'next','url' => '/search','start' => start - c, 'c' => c} if up
28
+ # are further results traversible?
29
+ down = r.size > start+c
30
+ up = !(start<=0)
34
31
 
35
- # search-result identifiers
36
- r = r.map{|r| r['.uri'].E }
32
+ # sort results
33
+ r = r.sort(e.has_key?('score') ? [["_score"]] : [["time", "descending"]],:offset => start,:limit => c)
37
34
 
38
- # fragment identifiers
39
- m['frag'] = {'uri' => 'frag', 'res' => r}
35
+ # results -> graph
36
+ r = r.map{|r| r['.uri'].E }
37
+ (r.map &:docs).flatten.uniq.map{|r| m[r.uri] = r.env e}
40
38
 
41
- # containing documents
42
- r.map(&:docs).flatten.uniq.map{|r| m[r.uri] = r.env e}
39
+ m['#'] = {'uri' => '#',
40
+ RDFs+'member' => r,
41
+ Type=>E[HTTP+'Response']}
42
+ m['#'][Prev]={'uri' => '/search' + {'q' => q, 'start' => start + c, 'c' => c}.qs} if down
43
+ m['#'][Next]={'uri' => '/search' + {'q' => q, 'start' => start - c, 'c' => c}.qs} if up
44
+ m['/search'] = {Type => E[Search]}
43
45
 
44
- # no 404 on 0 results - searchbox view
45
- m['/s']={'uri'=>'/s'} if m.empty?
46
+ rescue Groonga::SyntaxError => x
47
+ m['/search'] = {Type => E[Search]}
48
+ m['#'] = {
49
+ Type => E[COGS+'Exception'],
50
+ Title => "bad expression",
51
+ Content => CGI.escapeHTML(x.message)}
52
+ e['nocache']=true
53
+ end
46
54
 
47
- F['docsID'][m]}
55
+ F['docsID'][m,e]}
48
56
 
49
57
  def E.groonga
50
58
  @groonga ||= (require 'groonga'
@@ -72,6 +80,7 @@ class E
72
80
 
73
81
  # index resource
74
82
  def roonga graph="global", m = self.graph
83
+ puts "text #{graph} < #{uri} #{m.keys.join ' '}"
75
84
  g = E.groonga # db
76
85
  m.map{|u,i|
77
86
  r = g[u] || g.add(u) # create or load entry
@@ -1,7 +1,11 @@
1
1
  #watch __FILE__
2
2
  class E
3
3
 
4
- fn 'view/histogram',->d,e{
4
+ fn 'view/histogram',->m,e{
5
+ e.q['a'].do{|a|Fn 'histogram/main',m,e} ||
6
+ (Fn 'view/facetSelect',m,e)}
7
+
8
+ fn 'histogram/main',->d,e{
5
9
 
6
10
  # a :: attribute to chart
7
11
  a = e.q['a'].do{|e|e.expand} || Date
@@ -10,13 +14,13 @@ class E
10
14
  n = e.q['bins'].do{|b| b.to_f.max(999.0).min(1)} || 64.0
11
15
 
12
16
  # hv :: bin template
13
- v = F['view/'+e.q['hv']]
17
+ v = F['view/'+(e.q['hv']||'title')]
14
18
 
15
19
  # construct histogram bins
16
- (Fn 'view/histogram/bins',d,a,n).do{|h,m|
20
+ (Fn 'histogram/bins',d,a,n).do{|h,m|
17
21
 
18
22
  [H.css('/css/hist'),%w{mu hist}.map{|s|H.js('/js/'+s)},
19
- (Fn 'view/histogram/render',h),{style: "width: 100%; height: 5em"},
23
+ (Fn 'histogram',h),{style: "width: 100%; height: 5em"},
20
24
  h.map{|b,r|
21
25
  # skip empty bins
22
26
  r.empty? ? ' ' :
@@ -27,21 +31,20 @@ class E
27
31
  { class: 'histBin b'+b.to_s,
28
32
  c: [# label bin
29
33
  {_: :h3, c: from + ' &rarr; ' + to },
30
- # bin children view
31
- (v.(r,e) if v)]})}]}}
34
+ # bin-scoped view
35
+ v[r,e]]})}]}}
32
36
 
33
37
  F['view/h']=F['view/histogram']
34
38
 
35
39
  # Graph, property, numBins -> {bin -> Graph}
36
- fn 'view/histogram/bins',->m,p,nb{
40
+ fn 'histogram/bins',->m,p,nb{
37
41
  h = {}; bw = 0; min = 0; max = 0
38
42
  m.map{|u,r|
39
43
  # attribute accessor
40
44
  r[p]
41
45
  }.flatten.do{|v|
42
46
  # values
43
- v = v.compact.map{|v|
44
- ( p == Date ? v.to_time : v ).to_f}
47
+ v = v.map{|v| p == Date ? v.to_time : v }.select{|v| v.respond_to? :to_f}.map &:to_f
45
48
  max = v.max || 0
46
49
  min = v.min || 0
47
50
  width = (max-min).do{|w| w.zero? ? 1 : w}
@@ -56,17 +59,21 @@ class E
56
59
  # binnable properties
57
60
  r[p].do{|v|
58
61
  v.each{|v|
59
- # bin selector
60
- b = (((p == Date ? v.to_time : v).to_f - min) / bw).floor
61
-
62
- # append to bin
63
- h[b][u] = r }}}
62
+ # date handling
63
+ v = p == Date ? v.to_time : v
64
+ if v.respond_to? :to_f
65
+ # bin select
66
+ b = ((v.to_f - min) / bw).floor
67
+ # append
68
+ h[b][u] = r
69
+ end
70
+ }}}
64
71
 
65
72
  # histogram model
66
73
  [h, {min: min, max: max, bw: bw}] }
67
74
 
68
- fn 'view/histogram/render',->h{
69
- scale = 255 / h.map{|b,r|r.keys.size}.max.do{|m|m.zero? ? 1 : m}.to_f
75
+ fn 'histogram',->h{
76
+ scale = 255 / h.map{|b,r|r.keys.size}.max.do{|m|m.zero? ? 1 : m}.do{|m|m.respond_to?(:to_f) ? m.to_f : 1}
70
77
  bins = h.keys.sort
71
78
  ['<table class=histogram><tr>',
72
79
  bins.map{|b|
data/infod/html.rb ADDED
@@ -0,0 +1,231 @@
1
+ #watch __FILE__
2
+
3
+ # Ruby to HTML
4
+ def H _
5
+ case _
6
+ when Hash
7
+ '<'+(_[:_]||:div).to_s+(_.keys-[:_,:c]).map{|a|
8
+ ' '+a.to_s+'='+"'"+_[a].to_s.chars.map{|c|{"'"=>'%27','>'=>'%3E','<'=>'%3C'}[c]||c}.join+"'"}.join+'>'+
9
+ (_[:c] ? (H _[:c]) : '')+
10
+ (_[:_] == :link ? '' : ('</'+(_[:_]||:div).to_s+'>'))
11
+ when Array
12
+ _.map{|n|H n}.join
13
+ else
14
+ _.to_s if _
15
+ end
16
+ end
17
+
18
+ class H
19
+
20
+ def H.[] h; H h end
21
+
22
+ def H.js a,inline=false
23
+ p=a+'.js'
24
+ inline ? {_: :script, c: p.E.r} :
25
+ {_: :script, type: "text/javascript", src: p}
26
+ end
27
+
28
+ def H.once e,n,*h
29
+ return if e[n]
30
+ e[n]=true
31
+ h
32
+ end
33
+ end
34
+
35
+ class Array
36
+ def html v=nil
37
+ map{|e|e.html v}.join ' '
38
+ end
39
+ end
40
+
41
+ class Object
42
+ def html *a
43
+ name = self.class
44
+ href = "https://duckduckgo.com/?q=ruby+#{name}"
45
+ "<a href=#{href}><b>#{name}</b></a>"
46
+ end
47
+ end
48
+
49
+ class String
50
+ def br
51
+ gsub(/\n/,"<br>\n")
52
+ end
53
+ def href name=nil
54
+ '<a href="'+self+'">' + (name||abbrURI) + '</a>'
55
+ end
56
+ def abbrURI
57
+ sub /(?<scheme>[a-z]+:\/\/)?(?<abbr>.*?)(?<frag>[^#\/]*)$/,
58
+ '<span class="abbr"><span class="scheme">\k<scheme></span>\k<abbr></span><span class="frag">\k<frag></span>'
59
+ end
60
+ def html e=nil
61
+ # CGI.escapeHTML self
62
+ self
63
+ end
64
+ end
65
+
66
+ class Fixnum
67
+ def html e=nil; H({_: :input, type: :number, value: to_s}) end
68
+ end
69
+
70
+ class Float
71
+ def html e=nil; H({_: :input, type: :number, value: to_s}) end
72
+ end
73
+
74
+ class TrueClass
75
+ def html e=nil; H({_: :input, type: :checkbox, title: :True, checked: :checked}) end
76
+ end
77
+
78
+ class FalseClass
79
+ def html e=nil; H({_: :input, type: :checkbox, title: :False}) end
80
+ end
81
+
82
+ class Hash
83
+ def html e={'SERVER_NAME'=>'localhost'}, key=true
84
+ (keys.size == 1 && has_key?('uri')) ? url.href :
85
+ H({_: :table, class: :html, c:
86
+ map{|k,v|
87
+ {_: :tr, property: k, c:
88
+ [({_: :td,
89
+ c: [{_: :a, name: k, href: (k == 'uri' ? v : k), c: k.to_s.abbrURI}], class: :key} if key),
90
+ {_: :td,
91
+ c: (case k
92
+ when E::Content
93
+ v
94
+ when 'uri'
95
+ u = v.E
96
+ {_: :a, id: u, href: u.url, c: v}
97
+ else
98
+ v.html e
99
+ end), class: :val}]}}})
100
+ end
101
+ end
102
+
103
+ class E
104
+
105
+ def html *a
106
+ url.href
107
+ end
108
+
109
+ fn 'view',->d,e{
110
+ d.values.sort_by{|r| r[Date].do{|d| d[0].to_s} || ''}.reverse.
111
+ map{|r| Fn 'view/select',r,e }}
112
+
113
+ fn 'view/base',->d,e,k=true{
114
+ [H.once(e,'base',H.css('/css/html')),
115
+ d.values.map{|v|v.html e,k}]}
116
+
117
+ fn 'view/select',->r,e{
118
+ graph = {r.uri => r}
119
+ view = F['view/base']
120
+ # find types, skipping malformed/missing info
121
+ if r.class == Hash
122
+ (r[Type].class==Array ? r[Type] : [r[Type]]).do{|types|
123
+ views = types.map{|t|
124
+ # discard non-URIs
125
+ t.uri if t.respond_to? :uri}.
126
+ compact.map{|t|
127
+ subtype = t
128
+ type = subtype.split(/\//)[-2]
129
+ [F['view/' + subtype],
130
+ (F['view/' + type] if type)]}.
131
+ flatten.compact
132
+ view = views[0] unless views.empty?}
133
+ end
134
+ view[graph,e]}
135
+
136
+ # multiple views (comma-separated)
137
+ fn 'view/multi',->d,e{
138
+ e.q['views'].do{|vs|
139
+ vs.split(',').map{|v|
140
+ F['view/'+v].do{|f|f[d,e]}}}}
141
+
142
+ # enumerate available views
143
+ fn 'view/?',->d,e{
144
+ F.keys.grep(/^view\/(?!application|text\/x-)/).map{|v|
145
+ v = v[5..-1] # eat selector
146
+ [{_: :a, href: e['REQUEST_PATH']+e.q.merge({'view'=>v}).qs, c: v},"<br>\n"]}}
147
+
148
+ def triplrBlob
149
+ glob.select(&:f).do{|f|f.map{|r|
150
+ yield r.uri,Type,E('blob')
151
+ yield r.uri,Content,r.r}} end
152
+
153
+ def triplrHref enc=nil
154
+ yield uri,Content,(f && read).do{|r|enc ? r.force_encoding(enc).to_utf8 : r}.hrefs
155
+ end
156
+
157
+ def contentURIresolve *f
158
+ send(*f){|s,p,o|
159
+ yield s, p, p == Content ?
160
+ (Nokogiri::HTML.parse o).do{|o|
161
+ o.css('a').map{|a|
162
+ if a.has_attribute? 'href'
163
+ (a.set_attribute 'href', (URI.join s, (a.attr 'href'))) rescue nil
164
+ end}
165
+ o.to_s} : o}
166
+ end
167
+
168
+ fn Render+'text/html',->d,e{ u = d['#']||{}
169
+ titles = d.map{|u,r| r[Title] if r.class==Hash }.flatten.compact
170
+ view = F['view/'+e.q['view'].to_s] || F['view']
171
+ H ['<!DOCTYPE html>',{_: :html,
172
+ c: [{_: :head, c: ['<meta charset="utf-8" />',
173
+ {_: :title, c: titles.size==1 ? titles[0] : e.uri},
174
+ {_: :link, rel: :icon, href:'/css/misc/favicon.ico'},
175
+ u[Next].do{|n|{_: :link, rel: :next, href: n.uri}},
176
+ u[Prev].do{|p|{_: :link, rel: :prev, href: p.uri}}]},
177
+ {_: :body, c: view[d,e]}]}]}
178
+
179
+ # property-selector toolbar - utilizes RDFa view
180
+ fn 'view/p',->d,e{
181
+ #TODO fragmentURI scheme for selection-state
182
+ [H.once(e,'property.toolbar',H.once(e,'p',(H.once e,:mu,H.js('/js/mu')),
183
+ H.js('/js/p'),
184
+ H.css('/css/table')),
185
+ {_: :a, href: '#', c: '-', id: :hideP},
186
+ {_: :a, href: '#', c: '+', id: :showP},
187
+ {_: :span, id: 'properties',
188
+ c: E.graphProperties(d).map{|k|
189
+ {_: :a, class: :n, href: k, c: k.label+' '}}},
190
+ {_: :style, id: :pS},
191
+ {_: :style, id: :lS}),
192
+ (Fn 'view/'+(e.q['pv']||'table'),d,e)]}
193
+
194
+ # table-cell placement on sparse matrix of rows/columns
195
+ # cal.rb contains an example usage
196
+ fn 'view/t',->d,e,l=nil,a=nil{
197
+ layout = e.q['table'] || l
198
+ if layout
199
+ [H.once(e,'table',H.css('/css/table')),
200
+ {_: :table, c:
201
+ {_: :tbody, c: (Fn 'table/'+layout,d).do{|t|
202
+ rx = t.keys.max
203
+ rm = t.keys.min
204
+ c = t.values.map(&:keys)
205
+ cm = c.map(&:min).min
206
+ cx = c.map(&:max).max
207
+ rm && rx && (rm..rx).map{|r|
208
+ {_: :tr, c:
209
+ t[r].do{|r|
210
+ (cm..cx).map{|c|
211
+ r[c].do{|c|
212
+ {_: :td, class: :cell, c:(Fn 'view/'+(a||e.q['cellview']||'title'),c,e)}
213
+ }||{_: :td}}}}} || ''
214
+ }}}]
215
+ else
216
+ "table= layout arg required"
217
+ end}
218
+
219
+ fn 'view/table',->i,e{[H.css('/css/table'),(Fn 'table',i.values,e)]}
220
+
221
+ fn 'table',->es,q=nil{ p = {}
222
+ es.map{|e|e.respond_to?(:keys) &&
223
+ e.keys.map{|k|p[k]=true}}
224
+ keys = p.keys
225
+ keys.empty? ? es.html :
226
+ H({_: :table,:class => :tab,
227
+ c: [{_: :tr, c: keys.map{|k|{_: :th, class: :label, property: k, c: k.abbrURI}}},
228
+ *es.map{|e|
229
+ {_: :tr, about: e.uri, c: keys.map{|k| {_: :td, property: k, c: k=='uri' ? e.E.html : e[k].html}}}}]})}
230
+
231
+ end