Files: d2a64eaa0249b9f9b937741f6bb64eae15d044bd / modules / page / html / render / search.js
5646 bytesRaw
1 | const { h, Struct, Value, when, computed } = require('mutant') |
2 | const pull = require('pull-stream') |
3 | const Scroller = require('pull-scroll') |
4 | const TextNodeSearcher = require('text-node-searcher') |
5 | const whitespace = /\s+/ |
6 | var nest = require('depnest') |
7 | var Next = require('pull-next') |
8 | |
9 | exports.needs = nest({ |
10 | 'sbot.pull.search': 'first', |
11 | 'sbot.pull.log': 'first', |
12 | 'keys.sync.id': 'first', |
13 | 'message.html.render': 'first' |
14 | }) |
15 | |
16 | exports.gives = nest('page.html.render') |
17 | |
18 | exports.create = function (api) { |
19 | return nest('page.html.render', function channel (path) { |
20 | if (path[0] !== '?') return |
21 | |
22 | var queryStr = path.substr(1).trim() |
23 | var query = queryStr.split(whitespace) |
24 | var matchesQuery = searchFilter(query) |
25 | |
26 | const search = Struct({ |
27 | isLinear: Value(), |
28 | linear: Struct({ |
29 | checked: Value(0) |
30 | }), |
31 | fulltext: Struct({ |
32 | isDone: Value(false) |
33 | }), |
34 | matches: Value(0) |
35 | }) |
36 | |
37 | const hasNoFulltextMatches = computed([ |
38 | search.fulltext.isDone, search.matches |
39 | ], (done, matches) => { |
40 | return done && matches === 0 |
41 | }) |
42 | |
43 | const searchHeader = h('Search', [ |
44 | h('div', {className: 'PageHeading'}, [ |
45 | h('h1', [h('strong', 'Search Results:'), ' ', query.join(' ')]), |
46 | when(search.isLinear, |
47 | h('div.meta', ['Searched: ', search.linear.checked]), |
48 | h('section.details', when(hasNoFulltextMatches, h('div.matches', 'No matches'))) |
49 | ) |
50 | ]), |
51 | when(search.matches, null, h('Loading -large')) |
52 | ]) |
53 | |
54 | var content = h('section.content') |
55 | var container = h('Scroller', { |
56 | style: { overflow: 'auto' } |
57 | }, [ |
58 | h('div.wrapper', [ |
59 | searchHeader, |
60 | content |
61 | ]) |
62 | ]) |
63 | |
64 | pull( |
65 | api.sbot.pull.log({old: false}), |
66 | pull.filter(matchesQuery), |
67 | Scroller(container, content, renderMsg, true, false) |
68 | ) |
69 | |
70 | // pull( |
71 | // nextStepper(api.sbot.pull.search, {query: queryStr, reverse: true, limit: 500, live: false}), |
72 | // fallback((err) => { |
73 | // if (err === true) { |
74 | // search.fulltext.isDone.set(true) |
75 | // } else if (/^no source/.test(err.message)) { |
76 | // search.isLinear.set(true) |
77 | // return pull( |
78 | // nextStepper(api.sbot.pull.log, {reverse: true, limit: 500, live: false}), |
79 | // pull.through((msg) => search.linear.checked.set(search.linear.checked() + 1)), |
80 | // pull.filter(matchesQuery) |
81 | // ) |
82 | // } |
83 | // }), |
84 | // pull.through(() => search.matches.set(search.matches() + 1)), |
85 | // Scroller(container, content, renderMsg, false, false) |
86 | // ) |
87 | |
88 | // disable full text for now |
89 | search.isLinear.set(true) |
90 | pull( |
91 | nextStepper(api.sbot.pull.log, {reverse: true, limit: 500, live: false}), |
92 | pull.through((msg) => search.linear.checked.set(search.linear.checked() + 1)), |
93 | pull.filter(matchesQuery), |
94 | pull.through(() => search.matches.set(search.matches() + 1)), |
95 | Scroller(container, content, renderMsg, false, false) |
96 | ) |
97 | |
98 | return h('div', {className: 'SplitView'}, [ |
99 | h('div.main', container) |
100 | ]) |
101 | |
102 | // scoped |
103 | |
104 | function renderMsg (msg) { |
105 | var el = api.message.html.render(msg) |
106 | highlight(el, createOrRegExp(query)) |
107 | return el |
108 | } |
109 | }) |
110 | } |
111 | |
112 | function andSearch (terms, inputs) { |
113 | for (var i = 0; i < terms.length; i++) { |
114 | var match = false |
115 | for (var j = 0; j < inputs.length; j++) { |
116 | if (terms[i].test(inputs[j])) match = true |
117 | } |
118 | // if a term was not matched by anything, filter this one |
119 | if (!match) return false |
120 | } |
121 | return true |
122 | } |
123 | |
124 | function searchFilter (terms) { |
125 | return function (msg) { |
126 | var c = msg && msg.value && msg.value.content |
127 | return c && ( |
128 | msg.key === terms[0] || andSearch(terms.map(function (term) { |
129 | return new RegExp('\\b' + term + '\\b', 'i') |
130 | }), [c.text, c.name, c.title]) |
131 | ) |
132 | } |
133 | } |
134 | |
135 | function createOrRegExp (ary) { |
136 | return new RegExp(ary.map(function (e) { |
137 | return '\\b' + e + '\\b' |
138 | }).join('|'), 'i') |
139 | } |
140 | |
141 | function highlight (el, query) { |
142 | if (el) { |
143 | var searcher = new TextNodeSearcher({container: el}) |
144 | searcher.query = query |
145 | searcher.highlight() |
146 | return el |
147 | } |
148 | } |
149 | |
150 | function fallback (reader) { |
151 | var fallbackRead |
152 | return function (read) { |
153 | return function (abort, cb) { |
154 | read(abort, function next (end, data) { |
155 | if (end && reader && (fallbackRead = reader(end))) { |
156 | reader = null |
157 | read = fallbackRead |
158 | read(abort, next) |
159 | } else { |
160 | cb(end, data) |
161 | } |
162 | }) |
163 | } |
164 | } |
165 | } |
166 | |
167 | function nextStepper (createStream, opts, property, range) { |
168 | range = range || (opts.reverse ? 'lt' : 'gt') |
169 | property = property || 'timestamp' |
170 | |
171 | var last = null |
172 | var count = -1 |
173 | |
174 | return Next(function () { |
175 | if (last) { |
176 | if (count === 0) return |
177 | var value = opts[range] = get(last, property) |
178 | if (value == null) return |
179 | last = null |
180 | } |
181 | return pull( |
182 | createStream(clone(opts)), |
183 | pull.through(function (msg) { |
184 | count++ |
185 | if (!msg.sync) { |
186 | last = msg |
187 | } |
188 | }, function (err) { |
189 | // retry on errors... |
190 | if (err) { |
191 | count = -1 |
192 | return count |
193 | } |
194 | // end stream if there were no results |
195 | if (last == null) last = {} |
196 | }) |
197 | ) |
198 | }) |
199 | } |
200 | |
201 | function get (obj, path) { |
202 | if (!obj) return undefined |
203 | if (typeof path === 'string') return obj[path] |
204 | if (Array.isArray(path)) { |
205 | for (var i = 0; obj && i < path.length; i++) { |
206 | obj = obj[path[i]] |
207 | } |
208 | return obj |
209 | } |
210 | } |
211 | |
212 | function clone (obj) { |
213 | var _obj = {} |
214 | for (var k in obj) _obj[k] = obj[k] |
215 | return _obj |
216 | } |
217 |
Built with git-ssb-web