git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: 9bb4dd6572b0113dd462049c31d372b6062f9ac5

Files: 9bb4dd6572b0113dd462049c31d372b6062f9ac5 / modules / page / html / render / search.js

5189 bytesRaw
1const { h, Struct, Value, when, computed } = require('mutant')
2const pull = require('pull-stream')
3const Scroller = require('pull-scroll')
4const TextNodeSearcher = require('text-node-searcher')
5const whitespace = /\s+/
6var nest = require('depnest')
7var Next = require('pull-next')
8
9exports.needs = nest({
10 'sbot.pull.search': 'first',
11 'sbot.pull.log': 'first',
12 'keys.sync.id': 'first',
13 'message.html.render': 'first'
14})
15
16exports.gives = nest('page.html.render')
17
18exports.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 return h('div', {className: 'SplitView'}, [
89 h('div.main', container)
90 ])
91
92 // scoped
93
94 function renderMsg (msg) {
95 var el = api.message.html.render(msg)
96 highlight(el, createOrRegExp(query))
97 return el
98 }
99 })
100}
101
102function andSearch (terms, inputs) {
103 for (var i = 0; i < terms.length; i++) {
104 var match = false
105 for (var j = 0; j < inputs.length; j++) {
106 if (terms[i].test(inputs[j])) match = true
107 }
108 // if a term was not matched by anything, filter this one
109 if (!match) return false
110 }
111 return true
112}
113
114function searchFilter (terms) {
115 return function (msg) {
116 var c = msg && msg.value && msg.value.content
117 return c && (
118 msg.key === terms[0] || andSearch(terms.map(function (term) {
119 return new RegExp('\\b' + term + '\\b', 'i')
120 }), [c.text, c.name, c.title])
121 )
122 }
123}
124
125function createOrRegExp (ary) {
126 return new RegExp(ary.map(function (e) {
127 return '\\b' + e + '\\b'
128 }).join('|'), 'i')
129}
130
131function highlight (el, query) {
132 if (el) {
133 var searcher = new TextNodeSearcher({container: el})
134 searcher.query = query
135 searcher.highlight()
136 return el
137 }
138}
139
140function fallback (reader) {
141 var fallbackRead
142 return function (read) {
143 return function (abort, cb) {
144 read(abort, function next (end, data) {
145 if (end && reader && (fallbackRead = reader(end))) {
146 reader = null
147 read = fallbackRead
148 read(abort, next)
149 } else {
150 cb(end, data)
151 }
152 })
153 }
154 }
155}
156
157function nextStepper (createStream, opts, property, range) {
158 range = range || (opts.reverse ? 'lt' : 'gt')
159 property = property || 'timestamp'
160
161 var last = null
162 var count = -1
163
164 return Next(function () {
165 if (last) {
166 if (count === 0) return
167 var value = opts[range] = get(last, property)
168 if (value == null) return
169 last = null
170 }
171 return pull(
172 createStream(clone(opts)),
173 pull.through(function (msg) {
174 count++
175 if (!msg.sync) {
176 last = msg
177 }
178 }, function (err) {
179 // retry on errors...
180 if (err) {
181 count = -1
182 return count
183 }
184 // end stream if there were no results
185 if (last == null) last = {}
186 })
187 )
188 })
189}
190
191function get (obj, path) {
192 if (!obj) return undefined
193 if (typeof path === 'string') return obj[path]
194 if (Array.isArray(path)) {
195 for (var i = 0; obj && i < path.length; i++) {
196 obj = obj[path[i]]
197 }
198 return obj
199 }
200}
201
202function clone (obj) {
203 var _obj = {}
204 for (var k in obj) _obj[k] = obj[k]
205 return _obj
206}
207

Built with git-ssb-web