git ssb

16+

cel / patchfoo



Tree: a36543131c127121355391b4c743f49b7280b2aa

Files: a36543131c127121355391b4c743f49b7280b2aa / lib / render.js

22457 bytesRaw
1var fs = require('fs')
2var path = require('path')
3var pull = require('pull-stream')
4var cat = require('pull-cat')
5var paramap = require('pull-paramap')
6var h = require('hyperscript')
7var ph = require('pull-hyperscript')
8var marked = require('ssb-marked')
9var emojis = require('emoji-named-characters')
10var qs = require('querystring')
11var u = require('./util')
12var multicb = require('multicb')
13var RenderMsg = require('./render-msg')
14var Highlight = require('highlight.js')
15var md = require('ssb-markdown')
16
17module.exports = Render
18
19function MdRenderer(render) {
20 marked.Renderer.call(this, {})
21 this.render = render
22}
23MdRenderer.prototype = new marked.Renderer()
24
25MdRenderer.prototype.urltransform = function (href) {
26 return this.render.toUrl(href)
27}
28
29MdRenderer.prototype.image = function (ref, title, text) {
30 if (ref[0] !== '&') return this.link(ref, title, text)
31 var alt = this.render.getImageAlt(ref, text)
32 return (/^video:/.test(text) ? h('video', {
33 controls: 'controls',
34 src: this.render.toUrl(ref),
35 title: u.unescapeHTML(title) || undefined
36 }, '\n' + alt) : /^audio:/.test(text) ? h('audio', {
37 controls: 'controls',
38 src: this.render.toUrl(ref),
39 title: u.unescapeHTML(title) || undefined
40 }, '\n' + alt) : h('img', {
41 src: this.render.imageUrl(ref),
42 alt: alt,
43 title: u.unescapeHTML(title) || undefined
44 })).outerHTML
45}
46
47MdRenderer.prototype.link = function (ref, title, text) {
48 var href = this.urltransform(ref)
49 var name = href && /^\/(&|%26)/.test(href) && (title || text)
50 if (u.isRef(ref)) {
51 var myName = this.render.app.getNameSync(ref)
52 if (myName) title = title ? title + ' (' + myName + ')' : myName
53 }
54 var hrefToken = href !== false ? u.token() : undefined
55 var a = h('a', {
56 class: href === false ? 'bad' : undefined,
57 href: href !== false ? hrefToken : undefined,
58 title: title || undefined,
59 download: name ? encodeURIComponent(name) : undefined
60 })
61 // text is already html-escaped
62 a.innerHTML = text
63 var html = a.outerHTML
64 // href is already html-escaped
65 if (hrefToken) html = html.replace(hrefToken, href)
66
67 var link = this.render._mentionsByLink[ref]
68 if (link && link.type === 'text/x-markdown') {
69 html += h('sup', ' [', h('a', {
70 href: this.render.toUrl('/markdown/' + ref),
71 title: 'view rendered markdown'
72 }, 'md'), ']').outerHTML
73 }
74
75 return html
76}
77
78MdRenderer.prototype.mention = function (preceding, id) {
79 var href = this.urltransform(id)
80 var myName = this.render.app.getNameSync(id)
81 var html = (preceding||'') + h('a', {
82 class: href === false ? 'bad' : undefined,
83 href: href !== false ? href : undefined,
84 title: myName || undefined,
85 }, id.length > 50 ? id.slice(0, 8) + '…' : id).outerHTML
86
87 var link = this.render._mentionsByLink[id]
88 if (link && link.type === 'text/x-markdown') {
89 html += h('sup', ' [', h('a', {
90 href: this.render.toUrl('/markdown/' + id),
91 title: 'view rendered markdown'
92 }, 'md'), ']').outerHTML
93 }
94
95 return html
96}
97
98MdRenderer.prototype.code = function (code, lang, escaped) {
99 if (this.render.opts.codeInTextareas) {
100 return h('div', h('textarea', {
101 cols: 80,
102 rows: u.rows(code),
103 style: 'font-family: monospace',
104 innerHTML: escaped ? code : u.escapeHTML(code)
105 })).outerHTML
106 } else {
107 return marked.Renderer.prototype.code.call(this, code, lang, escaped)
108 }
109}
110
111function lexerRenderEmoji(emoji) {
112 var el = this.renderer.render.emoji(emoji)
113 return el && el.outerHTML || el
114}
115
116function Render(app, opts) {
117 this.app = app
118 this.opts = opts
119
120 this.markedOpts = {
121 gfm: true,
122 mentions: true,
123 tables: true,
124 breaks: true,
125 pedantic: false,
126 sanitize: true,
127 smartLists: true,
128 smartypants: false,
129 emoji: lexerRenderEmoji,
130 renderer: new MdRenderer(this),
131 highlight: this.highlight.bind(this),
132 }
133}
134
135Render.prototype.emoji = function (emoji) {
136 var name = ':' + emoji + ':'
137 var link = this._mentions && this._mentions[name]
138 if (link && link.link) {
139 this.app.reverseEmojiNameCache.set(emoji, link.link)
140 return h('img.ssb-emoji', {
141 src: this.opts.img_base + link.link,
142 alt: name
143 + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : ''),
144 height: 17,
145 title: name,
146 })
147 }
148 if (emoji in emojis) {
149 return h('img.ssb-emoji', {
150 src: this.opts.emoji_base + emoji + '.png',
151 alt: name,
152 height: 17,
153 align: 'absmiddle',
154 title: name,
155 })
156 }
157 return name
158}
159
160/* disabled until it can be done safely without breaking html
161function fixSymbols(str) {
162 // Dillo doesn't do fallback fonts, so specifically render fancy characters
163 // with Symbola
164 return str.replace(/[^\u0000-\u00ff]+/, function ($0) {
165 return '<span class="symbol">' + $0 + '</span>'
166 })
167}
168*/
169
170Render.prototype.markdown = function (text, mentions, opts) {
171 if (!text) return ''
172 var ssbcMd = opts && opts.ssbcMd
173 var mentionsObj = this._mentions = {}
174 var mentionsByLink = this._mentionsByLink = {}
175 if (Array.isArray(mentions)) mentions.forEach(function (link) {
176 if (!link) return
177 else if (link.emoji)
178 mentionsObj[':' + link.name + ':'] = link
179 else if (link.name && link.link && link.link[0] === '@')
180 mentionsObj['@' + link.name] = link.link
181 else if (link.host === 'http://localhost:7777')
182 mentionsObj[link.href] = link.link
183 if (link.link)
184 mentionsByLink[link.link +
185 (link.query && typeof link.query.unbox === 'string' ?
186 '?unbox=' + link.query.unbox.replace(/\s/g, '+') : '') +
187 (link.key ? '#' + link.key : '')] = link
188 })
189 var self = this
190 var out = ssbcMd ? md.block(String(text), {
191 toUrl: function (ref) { return self.toUrl(ref) },
192 imageLink: function (ref) { return self.imageUrl(ref) }
193 }) : marked(u.toString(text), this.markedOpts)
194 delete this._mentions
195 delete this._mentionsByLink
196 return out //fixSymbols(out)
197}
198
199Render.prototype.imageUrl = function (ref) {
200 var m = /^blobstore:(.*)/.exec(ref)
201 if (m) ref = m[1]
202 ref = ref.replace(/#/, '%23')
203 return this.opts.img_base + ref
204}
205
206Render.prototype.getImageAlt = function (id, fallback) {
207 var link = this._mentionsByLink[id]
208 if (!link) return fallback
209 var name = String(u.unescapeHTML(link.name || fallback))
210 .replace(/^(video|audio):/, '')
211 return name
212 + (link.type && !/\.\S+$/.test(name) ? ' [' + link.type + ']' : '')
213 + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : '')
214}
215
216Render.prototype.formatSize = function (size) {
217 if (size < 1024) return size + ' B'
218 size /= 1024
219 if (size < 1024) return size.toFixed(2) + ' KB'
220 size /= 1024
221 return size.toFixed(2) + ' MB'
222}
223
224Render.prototype.linkify = function (text, opts) {
225 var ellipsis = opts && opts.ellipsis
226 var arr = text.split(u.ssbRefEncRegex)
227 for (var i = 1; i < arr.length; i += 2) {
228 var id = arr[i]
229 var text = ellipsis && id.length > 8 ? id.substr(0, 8) + '…' : id
230 arr[i] = h('a', {href: this.toUrlEnc(id)}, text)
231 }
232 return arr
233}
234
235Render.prototype.toUrlEnc = function (href) {
236 var url = this.toUrl(href)
237 if (url) return url
238 try { href = decodeURIComponent(href) }
239 catch (e) { return false }
240 return this.toUrl(href)
241}
242
243Render.prototype.toUrl = function (href) {
244 if (!href) return href
245 var mentions = this._mentions
246 if (mentions && href in this._mentions) href = this._mentions[href]
247 if (/^ssb:\/\//.test(href)) href = href.substr(6)
248 if (/^ssb-blob:\/\//.test(href)) {
249 return this.opts.base + 'zip/' + href.substr(11)
250 }
251 switch (href[0]) {
252 case '%':
253 var parts = href.split('?')
254 var hash = parts.shift()
255 var query = parts.join('?')
256 if (!u.isRef(hash)) return false
257 return this.opts.base +
258 (this.opts.encode_msgids ? encodeURIComponent(hash) : hash)
259 + (query ? '?' + query : '')
260 case '@':
261 if (!u.isRef(href.replace(/\?.*/, ''))) return false
262 return this.opts.base + href
263 case '&':
264 var parts = href.split('#')
265 var hash = parts.shift()
266 var key = parts.shift()
267 var fragment = parts.join('#')
268 parts = hash.split('?')
269 hash = parts.shift()
270 query = parts.join('?')
271 if (!u.isRef(hash)) return false
272 return this.opts.blob_base + hash
273 + (query ? '?' + query : '')
274 + (key ? encodeURIComponent('#' + key) : '')
275 + (fragment ? '#' + fragment : '')
276 case '#': return this.opts.base + 'channel/' +
277 encodeURIComponent(href.substr(1).toLowerCase())
278 case '/': return this.opts.base + href.substr(1)
279 case '?': return this.opts.base + 'search?q=' + encodeURIComponent(href)
280 }
281 var m = /^blobstore:(.*)/.exec(href)
282 if (m) return this.opts.blob_base + m[1]
283 if (/^javascript:/.test(href)) return false
284 if (!/^[a-z0-9]*:/.test(href)) return 'http://' + href
285 return href
286}
287
288Render.prototype.lockIcon = function () {
289 return this.emoji('lock')
290}
291
292Render.prototype.avatarImage = function (link, cb) {
293 var self = this
294 if (!link) return cb(), ''
295 if (typeof link === 'string') link = {link: link}
296 var img = h('img.ssb-avatar-image', {
297 width: 72,
298 alt: ' '
299 })
300 if (link.image) gotAbout(null, link)
301 else self.app.getAbout(link.link, gotAbout)
302 function gotAbout(err, about) {
303 if (err) console.trace(err)
304 img.src = about && about.image ? self.imageUrl(about.image)
305 : self.toUrl('/static/fallback.png')
306 cb()
307 }
308 return img
309}
310
311Render.prototype.prepareLink = function (link, cb) {
312 if (typeof link === 'string') link = {link: link}
313 if (link.name || !link.link) cb(null, link)
314 else this.app.getAbout(link.link, function (err, about) {
315 if (err) return cb(null, link)
316 link.name = about.name || about.title || (link.link.substr(0, 8) + '…')
317 if (link.name && link.name[0] === link.link[0]) {
318 link.name = link.name.substr(1)
319 }
320 cb(null, link)
321 })
322}
323
324Render.prototype.prepareLinks = function (links, cb) {
325 var self = this
326 if (!links) return cb()
327 var done = multicb({pluck: 1})
328 if (Array.isArray(links)) links.forEach(function (link) {
329 self.prepareLink(link, done())
330 })
331 done(cb)
332}
333
334Render.prototype.idLink = function (link, cb) {
335 var self = this
336 if (!link) return cb(), ''
337 var a = h('a', ' ')
338 self.prepareLink(link, function (err, link) {
339 if (err) return cb(err)
340 a.href = self.toUrl(link.link)
341 var sigil = link.link && link.link[0] || '@'
342 var name = link.name || String(link.link).substr(1, 8) + '…'
343 a.childNodes[0].textContent = sigil + name
344 cb()
345 })
346 return a
347}
348
349// %NM8tXGBBDKKcpRbbyd/5uN1p/2OtBMFDylLMDPGoq8Q=.sha256
350var idRegex = /^[A-Za-z0-9._\-+=/]*[A-Za-z0-9_\-+=/]$/
351
352Render.prototype.idLinkCopyable = function (link, cb) {
353 var self = this
354 if (!self.app.copyableIds) return idLink(link, cb)
355 if (!link) return cb(), ''
356 var a = h('a', ' ')
357 self.prepareLink(link, function (err, link) {
358 if (err) return cb(err)
359 a.href = self.toUrl(link.link)
360 var name = link.name || String(link.link).substr(1, 8) + '…'
361 if (idRegex.test(name)) a.childNodes[0].textContent = '@' + name
362 else {
363 a.className = 'id-copyable-link'
364 a.innerHTML = h('span', [
365 h('span.id-deemphasize', '['),
366 h('span.id-name', '@' + link.name),
367 h('span.id-deemphasize', '](', link.link, ')'),
368 ]).innerHTML
369 }
370 cb()
371 })
372 return a
373}
374
375Render.prototype.privateLine = function (opts, cb) {
376 var recps = opts.recps
377 var isAuthorRecp = opts.isAuthorRecp
378 var done = multicb({pluck: 1, spread: true})
379 var self = this
380 var el = h('div.recps',
381 opts.noLockIcon ? '' : self.lockIcon(),
382 !isAuthorRecp ? [
383 h('span', {title: 'Author is not a recipient'}, '[!]'), ' '
384 ] : '',
385 Array.isArray(recps)
386 ? recps.map(function (recp) {
387 return [' ', self.idLink(recp, done())]
388 }) : '')
389 done(cb)
390 return el
391}
392
393Render.prototype.msgIdLink = function (id, cb) {
394 var self = this
395 var el = h('span')
396 var a = h('a', {href: self.toUrl(id)}, id)
397 self.app.getMsgDecrypted(id, function (err, msg) {
398 if (err) return el.appendChild(u.renderError(err)), cb()
399 var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve})
400 renderMsg.title(function (err, title) {
401 if (err) return el.appendChild(u.renderError(err)), cb()
402 a.childNodes[0].textContent = title
403 cb()
404 })
405 })
406 return a
407}
408
409Render.prototype.phMsgLink = function (msg) {
410 var self = this
411 return u.readNext(function (cb) {
412 self.app.unboxMsg(msg, function (err, msg) {
413 if (err) return cb(err)
414 var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve})
415 renderMsg.title(function (err, title) {
416 if (err) return cb(err)
417 cb(null, ph('a', {href: self.toUrl(msg.key)}, u.escapeHTML(title)))
418 })
419 })
420 })
421}
422
423Render.prototype.renderMsg = function (msg, opts, cb) {
424 var self = this
425 self.app.filterMsg(msg, opts, function (err, show) {
426 if (err) return cb(err)
427 if (show) new RenderMsg(self, self.app, msg, opts).message(cb)
428 else cb(null, '')
429 })
430}
431
432Render.prototype.renderFeeds = function (opts) {
433 var self = this
434 var limit = Number(opts.limit)
435 return pull(
436 paramap(function (msg, cb) {
437 self.renderMsg(msg, opts, cb)
438 }, 4),
439 pull.filter(Boolean),
440 limit && pull.take(limit)
441 )
442}
443
444Render.prototype.gitCommitBody = function (body) {
445 if (!body) return ''
446 var isMarkdown = !/^# Conflicts:$/m.test(body)
447 return isMarkdown
448 ? h('div', {innerHTML: this.markdown('\n' + body)})
449 : h('pre', this.linkify('\n' + body))
450}
451
452Render.prototype.getName = function (id, cb) {
453 // TODO: consolidate the get name/link functions
454 var self = this
455 switch (id && id[0]) {
456 case '%':
457 return self.app.getMsgDecrypted(id, function (err, msg) {
458 if (err && err.name == 'NotFoundError')
459 return cb(null, String(id).substring(0, 8) + '…(missing)')
460 if (err) return fallback()
461 new RenderMsg(self, self.app, msg, {wrap: false}).title(cb)
462 })
463 case '@': // fallthrough
464 case '&':
465 return self.app.getAbout(id, function (err, about) {
466 if (err || !about || !about.name) return fallback()
467 cb(null, about.name)
468 })
469 default:
470 return cb(null, String(id))
471 }
472 function fallback() {
473 cb(null, String(id).substr(0, 8) + '…')
474 }
475}
476
477Render.prototype.getNameLink = function (id, opts, cb) {
478 if (!cb && typeof opts === 'function') cb = opts, opts = null
479 var length = opts && opts.length || Infinity
480 var self = this
481 self.getName(id, function (err, name) {
482 if (err) return cb(err)
483 cb(null, h('a', {href: self.toUrl(id)}, u.truncate(name, length)))
484 })
485}
486
487Render.prototype.npmAuthorLink = function (author) {
488 if (!author) return
489 var url = u.ifString(author.url)
490 var email = u.ifString(author.email)
491 var name = u.ifString(author.name)
492 var title
493 if (!url && u.isRef(name)) url = name, name = null
494 if (!url && !email) return name || JSON.stringify(author)
495 if (!url && email) url = 'mailto:' + email, email = null
496 if (!name && email) name = email, email = null
497 var feed = u.isRef(url) && url[0] === '@' && url
498 if (feed && name) title = this.app.getNameSync(feed)
499 if (feed && name && name[0] != '@') name = '@' + name
500 if (feed && !name) name = this.app.getNameSync(feed) // TODO: async
501 if (url && !name) name = url
502 var secondaryLink = email && h('a', {href: this.toUrl('mailto:' + email)}, email)
503 return [
504 h('a', {href: this.toUrl(url), title: title}, name),
505 secondaryLink ? [' (', secondaryLink, ')'] : ''
506 ]
507}
508
509// auto-highlight is slow
510var useAutoHighlight = false
511
512Render.prototype.highlight = function (code, lang) {
513 if (code.length > 100000) return u.escapeHTML(code)
514 if (!lang && /^#!\/bin\/[^\/]*sh$/m.test(code)) lang = 'sh'
515 try {
516 return lang
517 ? Highlight.highlight(lang, code).value
518 : useAutoHighlight
519 ? Highlight.highlightAuto(code).value
520 : u.escapeHTML(code)
521 } catch(e) {
522 if (!/^Unknown language/.test(e.message)) console.trace(e)
523 return u.escapeHTML(code)
524 }
525}
526
527Render.prototype.npmPackageMentions = function (links, cb) {
528 var self = this
529 var pkgLinks = u.toArray(links).filter(function (link) {
530 return /^npm:/.test(link.name)
531 })
532 if (pkgLinks.length === 0) return cb(null, '')
533 var done = multicb({pluck: 1})
534 pkgLinks.forEach(function (link) {
535 self.npmPackageMention(link, {}, done())
536 })
537 done(function (err, mentionEls) {
538 cb(null, h('table',
539 h('thead', h('tr',
540 h('th', 'package'),
541 h('th', 'version'),
542 h('th', 'tag'),
543 h('th', 'size'),
544 h('th', 'tarball'),
545 h('th', 'readme')
546 )),
547 h('tbody', mentionEls)
548 ))
549 })
550}
551
552Render.prototype.npmPrebuildMentions = function (links, cb) {
553 var self = this
554 var prebuildLinks = u.toArray(links).filter(function (link) {
555 return /^prebuild:/.test(link.name)
556 })
557 if (prebuildLinks.length === 0) return cb(null, '')
558 var done = multicb({pluck: 1})
559 prebuildLinks.forEach(function (link) {
560 self.npmPrebuildMention(link, {}, done())
561 })
562 done(function (err, mentionEls) {
563 cb(null, h('table',
564 h('thead', h('tr',
565 h('th', 'name'),
566 h('th', 'version'),
567 h('th', 'runtime'),
568 h('th', 'abi'),
569 h('th', 'platform+libc'),
570 h('th', 'arch'),
571 h('th', 'size'),
572 h('th', 'tarball')
573 )),
574 h('tbody', mentionEls)
575 ))
576 })
577}
578
579Render.prototype.npmPackageMention = function (link, opts, cb) {
580 var nameRegex = /'prebuild:{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz'/
581 var parts = String(link.name).replace(/\.tgz$/, '').split(':')
582 var name = parts[1]
583 var version = parts[2]
584 var distTag = parts[3]
585 var self = this
586 var done = multicb({pluck: 1, spread: true})
587 var base = '/npm/' + (opts.author ? u.escapeId(link.author) + '/' : '')
588 var pathWithAuthor = opts.withAuthor ? '/npm/' +
589 u.escapeId(link.author) +
590 (opts.name ? '/' + opts.name +
591 (opts.version ? '/' + opts.version +
592 (opts.distTag ? '/' + opts.distTag + '/' : '') : '') : '') : ''
593 self.app.getAbout(link.author, done())
594 self.app.getBlobState(link.link, done())
595 done(function (err, about, blobState) {
596 if (err) return cb(err)
597 cb(null, h('tr', [
598 opts.withAuthor ? h('td', h('a', {
599 href: self.toUrl(pathWithAuthor),
600 title: 'publisher'
601 }, about.name || u.truncate(link.author, 8)), ' ') : '',
602 h('td', h('a', {
603 href: self.toUrl(base + name),
604 title: 'package name'
605 }, name), ' '),
606 h('td', version ? [h('a', {
607 href: self.toUrl(base + name + '/' + version),
608 title: 'package version'
609 }, version), ' '] : ''),
610 h('td', distTag ? [h('a', {
611 href: self.toUrl(base + name + '//' + distTag),
612 title: 'dist-tag'
613 }, distTag), ' '] : ''),
614 h('td', {align: 'right'}, link.size != null ? [h('span', {
615 title: 'tarball size'
616 }, self.formatSize(link.size)), ' '] : ''),
617 h('td', typeof link.link === 'string' ? h('code', h('a', {
618 href: self.toUrl('/links/' + link.link),
619 title: 'package tarball'
620 }, link.link.substr(0, 8) + '…')) : ''),
621 h('td',
622 blobState === 'wanted' ?
623 'fetching...'
624 : blobState ? h('a', {
625 href: self.toUrl('/npm-readme/' + encodeURIComponent(link.link)),
626 title: 'package contents'
627 }, 'readme')
628 : self.blobFetchForm(link.link))
629 ]))
630 })
631}
632
633Render.prototype.blobFetchForm = function (id) {
634 return h('form', {action: '', method: 'post'},
635 h('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
636 h('input', {type: 'hidden', name: 'async_want', value: '1'}),
637 h('input', {type: 'hidden', name: 'blob_ids', value: id}),
638 h('input', {type: 'submit', value: 'fetch'})
639 )
640}
641
642Render.prototype.npmPrebuildNameRegex = /^prebuild:(.*?)-v([0-9]+\.[0-9]+.*?)-(.*?)-v(.*?)-(.*?)-(.*?)\.tar\.gz$/
643
644Render.prototype.npmPrebuildMention = function (link, opts, cb) {
645 var m = this.npmPrebuildNameRegex.exec(link.name)
646 if (!m) return cb(null, '')
647 var name = m[1], version = m[2], runtime = m[3],
648 abi = m[4], platformlibc = m[5], arch = m[6]
649 var self = this
650 var done = multicb({pluck: 1, spread: true})
651 var base = '/npm-prebuilds/' + (opts.author ? u.escapeId(link.author) + '/' : '')
652 self.app.getAbout(link.author, done())
653 self.app.getBlobState(link.link, done())
654 done(function (err, about, blobState) {
655 if (err) return cb(err)
656 cb(null, h('tr', [
657 opts.withAuthor ? h('td', h('a', {
658 href: self.toUrl(link.author)
659 }, about.name || u.truncate(link.author, 8)), ' ') : '',
660 h('td', h('a', {
661 href: self.toUrl(base + name)
662 }, name), ' '),
663 h('td', h('a', {
664 href: self.toUrl('/npm/' + name + '/' + version)
665 }, version), ' '),
666 h('td', runtime, ' '),
667 h('td', abi, ' '),
668 h('td', platformlibc, ' '),
669 h('td', arch, ' '),
670 h('td', {align: 'right'}, link.size != null ? [
671 self.formatSize(link.size), ' '
672 ] : ''),
673 h('td', typeof link.link === 'string' ? h('code', h('a', {
674 href: self.toUrl('/links/' + link.link)
675 }, link.link.substr(0, 8) + '…')) : ''),
676 h('td',
677 blobState === 'wanted' ?
678 'fetching...'
679 : blobState ? ''
680 : self.blobFetchForm(link.link))
681 ]))
682 })
683}
684
685Render.prototype.friendsList = function (prefix) {
686 prefix = prefix || '/'
687 var self = this
688 return pull(
689 paramap(function (item, cb) {
690 if (typeof item === 'string') item = {feed: item}
691 var id = item.feed
692 self.app.getAbout(item.feed, function (err, about) {
693 var name = about && about.name || id.substr(0, 8) + '…'
694 cb(null, [
695 h('a', {href: self.toUrl(prefix + id)}, name),
696 item.msg ? h('a', {href: self.toUrl(item.msg.key)}, '₁') : '',
697 item.msg2 ? h('a', {href: self.toUrl(item.msg2.key)}, '₂') : ''
698 ])
699 })
700 }, 8),
701 function (read) {
702 var count = 0
703 var ended
704 return function (abort, cb) {
705 if (ended) return cb(ended)
706 read(abort, function (end, el) {
707 if (end === true) {
708 ended = true
709 cb(null, '(' + count + ')')
710 } else {
711 count++
712 cb(end, u.toHTML(el) + ' ')
713 }
714 })
715 }
716 }
717 )
718}
719

Built with git-ssb-web