infod 0.0.2 → 0.0.3

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 +52 -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 +123 -0
  25. data/infod/{Es/grep.rb → grep.rb} +2 -2
  26. data/infod/{Es/groonga.rb → groonga.rb} +41 -33
  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} +38 -25
  31. data/infod/infod.rb +52 -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 +90 -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} +78 -45
  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} +22 -7
  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} +17 -27
  50. data/infod/{H/time.rb → time.rb} +14 -34
  51. data/infod/{H/who.rb → whois.rb} +6 -4
  52. data/infod/{H/wiki.rb → wiki.rb} +0 -0
  53. metadata +54 -64
  54. data/config.ru +0 -3
  55. data/infod/Es.rb +0 -31
  56. data/infod/Es/filter.rb +0 -75
  57. data/infod/Es/glob.rb +0 -22
  58. data/infod/Es/html.rb +0 -271
  59. data/infod/Es/in.rb +0 -68
  60. data/infod/Es/json.rb +0 -68
  61. data/infod/Es/ls.rb +0 -58
  62. data/infod/Es/mail.rb +0 -87
  63. data/infod/Es/mime.rb +0 -59
  64. data/infod/Es/out.rb +0 -52
  65. data/infod/Es/pager.rb +0 -34
  66. data/infod/Es/pdf.rb +0 -19
  67. data/infod/Es/rdf.rb +0 -35
  68. data/infod/H.rb +0 -15
  69. data/infod/H/audio.rb +0 -19
  70. data/infod/H/blog.rb +0 -15
  71. data/infod/H/cal.rb +0 -81
  72. data/infod/H/edit.rb +0 -88
  73. data/infod/H/forum.rb +0 -4
  74. data/infod/H/hf.rb +0 -114
  75. data/infod/H/mail.rb +0 -92
  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,123 @@
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 implementation 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. info"daemon 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 thus necessitating more roundtrips and latency via marshalling/unmarshalling, parsing, loopback network-abstraction, nascent RDF-via-SPARQL-as-ORM-libs.. by all means knock yourself out experimenting w/ defining graph-handlers for this stuff though - i know 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, h=nil, p=[], &b
33
+ graph = fromStream({},triplr)
34
+ graph.map{|u,r| # stream -> graph
35
+ e = u.E # resource
36
+ j = e.ef # doc
37
+ j.e || # exists?
38
+ (j.w({u=>r},true) ;puts '< '+u # insert doc
39
+ p.map{|p| # each indexable property
40
+ r[p].do{|v| # values exists?
41
+ v.map{|o| # each value
42
+ e.index p,o}}} # property index
43
+ e.roonga h if h)} # full-text index
44
+ graph.triples &b if b # emit the triples
45
+ self
46
+ end
47
+
48
+ # default protograph - identity + resource-thunks
49
+ # Resource, Query, Graph -> graphID
50
+ fn 'protograph/',->e,q,g{
51
+ g['#'] = {'uri' => '#'}
52
+ set = (q['set'] && F['set/'+q['set']] || F['set/'])[e,q,g]
53
+ if set.empty?
54
+ g.delete '#'
55
+ else
56
+ g['#'][RDFs+'member'] = set
57
+ g['#'][Type] = E[HTTP+'Response']
58
+ set.map{|u| g[u.uri] = u } # thunk
59
+ end
60
+ F['docsID'][g,q]}
61
+
62
+ fn 'set/',->e,q,g{
63
+ s = []
64
+ s.concat e.docs
65
+ e.pathSegment.do{|p| s.concat p.docs }
66
+ # day-dir hinted pagination
67
+ e.env['REQUEST_PATH'].match(/(.*?\/)([0-9]{4})\/([0-9]{2})\/([0-9]{2})(.*)/).do{|m|
68
+ u = g['#']
69
+ t = ::Date.parse "#{m[2]}-#{m[3]}-#{m[4]}"
70
+ pp = m[1] + (t-1).strftime('%Y/%m/%d') + m[5]
71
+ np = m[1] + (t+1).strftime('%Y/%m/%d') + m[5]
72
+ u[Prev] = {'uri' => pp} if pp.E.e || E['http://' + e.env['SERVER_NAME'] + pp].e
73
+ u[Next] = {'uri' => np} if np.E.e || E['http://' + e.env['SERVER_NAME'] + np].e }
74
+ s }
75
+
76
+ # fs-derived ID for a resource-set
77
+ fn 'docsID',->g,q{
78
+ [q.has_key?('nocache').do{|_|rand},
79
+ g.sort.map{|u,r|
80
+ [u, r.respond_to?(:m) && r.m]}].h }
81
+
82
+ # default graph (filesystem store)
83
+ # to use a different default-graph function (w/o patching here, or querystring param), define a GET handler on / (or a subdir),
84
+ # update configuration such as q['graph'] = 'hexastore' and return false or call #response..
85
+ fn 'graph/',->e,q,m{
86
+ # force thunks
87
+ m.values.map{|r|(r.env e.env).graphFromFile m if r.class == E }
88
+ # cleanup unexpanded thunks
89
+ m.delete_if{|u,r|r.class==E}}
90
+
91
+ fn 'filter/set',->e,m,r{
92
+ # filter to RDFs set-members, gone will be:
93
+ # data about docs containing the data
94
+ # other fragments in a doc not matching search when indexed per-fragment
95
+ f = m['#'].do{|c| c[RDFs+'member'].do{|m| m.map &:uri }} || [] # members
96
+ m.keys.map{|u| m.delete u unless f.member? u}} # trim
97
+
98
+ def graphFromFile g={}
99
+ if !e
100
+ puts "missing file! "+d
101
+ return
102
+ end
103
+ _ = self
104
+ triplr = @r.do{|r|
105
+ r.q['triplr'].do{|t|
106
+ respond_to?(t) && t }} || :triplrMIME
107
+ unless ext=='e' # native graph-format
108
+ _ = E '/E/rdf/' + [triplr,uri].h.dive
109
+ unless _.e && _.m > m; # up to date?
110
+ e = {} ; puts "< #{uri}"
111
+ [:triplrInode,triplr].each{|t| fromStream e, t }
112
+ _.w e, true
113
+ end
114
+ end
115
+ g.mergeGraph _.r true
116
+ end
117
+
118
+ def graph g={}
119
+ docs.map{|d|d.graphFromFile g} # tripleStream -> graph
120
+ g
121
+ end
122
+
123
+ 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{
@@ -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)
27
-
28
- # sort results
29
- r = r.sort(e.has_key?('score') ? [["_score"]] : [["time", "descending"]],:offset => start,:limit => c)
30
-
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
34
-
35
- # search-result identifiers
36
- r = r.map{|r| r['.uri'].E }
37
-
38
- # fragment identifiers
39
- m['frag'] = {'uri' => 'frag', 'res' => r}
40
-
41
- # containing documents
42
- r.map(&:docs).flatten.uniq.map{|r| m[r.uri] = r.env e}
43
-
44
- # no 404 on 0 results - searchbox view
45
- m['/s']={'uri'=>'/s'} if m.empty?
46
-
47
- F['docsID'][m]}
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)
23
+
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
27
+
28
+ # are further results traversible?
29
+ down = r.size > start+c
30
+ up = !(start<=0)
31
+
32
+ # sort results
33
+ r = r.sort(e.has_key?('score') ? [["_score"]] : [["time", "descending"]],:offset => start,:limit => c)
34
+
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}
38
+
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]}
45
+
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
54
+
55
+ F['docsID'][m,e]}
48
56
 
49
57
  def E.groonga
50
58
  @groonga ||= (require 'groonga'
@@ -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