git ssb

16+

cel / patchfoo



Tree: ea522127885486018de1547de0db04f3d3144eb6

Files: ea522127885486018de1547de0db04f3d3144eb6 / lib / render.js

21570 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(String(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
349Render.prototype.privateLine = function (recps, isAuthorRecp, cb) {
350 var done = multicb({pluck: 1, spread: true})
351 var self = this
352 var el = h('div.recps',
353 self.lockIcon(),
354 !isAuthorRecp ? [
355 h('span', {title: 'Author is not a recipient'}, '[!]'), ' '
356 ] : '',
357 Array.isArray(recps)
358 ? recps.map(function (recp) {
359 return [' ', self.idLink(recp, done())]
360 }) : '')
361 done(cb)
362 return el
363}
364
365Render.prototype.msgIdLink = function (id, cb) {
366 var self = this
367 var el = h('span')
368 var a = h('a', {href: self.toUrl(id)}, id)
369 self.app.getMsgDecrypted(id, function (err, msg) {
370 if (err) return el.appendChild(u.renderError(err)), cb()
371 var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve})
372 renderMsg.title(function (err, title) {
373 if (err) return el.appendChild(u.renderError(err)), cb()
374 a.childNodes[0].textContent = title
375 cb()
376 })
377 })
378 return a
379}
380
381Render.prototype.phMsgLink = function (msg) {
382 var self = this
383 return u.readNext(function (cb) {
384 self.app.unboxMsg(msg, function (err, msg) {
385 if (err) return cb(err)
386 var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve})
387 renderMsg.title(function (err, title) {
388 if (err) return cb(err)
389 cb(null, ph('a', {href: self.toUrl(msg.key)}, u.escapeHTML(title)))
390 })
391 })
392 })
393}
394
395Render.prototype.renderMsg = function (msg, opts, cb) {
396 var self = this
397 self.app.filterMsg(msg, opts, function (err, show) {
398 if (err) return cb(err)
399 if (show) new RenderMsg(self, self.app, msg, opts).message(cb)
400 else cb(null, '')
401 })
402}
403
404Render.prototype.renderFeeds = function (opts) {
405 var self = this
406 var limit = Number(opts.limit)
407 return pull(
408 paramap(function (msg, cb) {
409 self.renderMsg(msg, opts, cb)
410 }, 4),
411 pull.filter(Boolean),
412 limit && pull.take(limit)
413 )
414}
415
416Render.prototype.gitCommitBody = function (body) {
417 if (!body) return ''
418 var isMarkdown = !/^# Conflicts:$/m.test(body)
419 return isMarkdown
420 ? h('div', {innerHTML: this.markdown('\n' + body)})
421 : h('pre', this.linkify('\n' + body))
422}
423
424Render.prototype.getName = function (id, cb) {
425 // TODO: consolidate the get name/link functions
426 var self = this
427 switch (id && id[0]) {
428 case '%':
429 return self.app.getMsgDecrypted(id, function (err, msg) {
430 if (err && err.name == 'NotFoundError')
431 return cb(null, String(id).substring(0, 8) + '…(missing)')
432 if (err) return fallback()
433 new RenderMsg(self, self.app, msg, {wrap: false}).title(cb)
434 })
435 case '@': // fallthrough
436 case '&':
437 return self.app.getAbout(id, function (err, about) {
438 if (err || !about || !about.name) return fallback()
439 cb(null, about.name)
440 })
441 default:
442 return cb(null, String(id))
443 }
444 function fallback() {
445 cb(null, String(id).substr(0, 8) + '…')
446 }
447}
448
449Render.prototype.getNameLink = function (id, opts, cb) {
450 if (!cb && typeof opts === 'function') cb = opts, opts = null
451 var length = opts && opts.length || Infinity
452 var self = this
453 self.getName(id, function (err, name) {
454 if (err) return cb(err)
455 cb(null, h('a', {href: self.toUrl(id)}, u.truncate(name, length)))
456 })
457}
458
459Render.prototype.npmAuthorLink = function (author) {
460 if (!author) return
461 var url = u.ifString(author.url)
462 var email = u.ifString(author.email)
463 var name = u.ifString(author.name)
464 var title
465 if (!url && u.isRef(name)) url = name, name = null
466 if (!url && !email) return name || JSON.stringify(author)
467 if (!url && email) url = 'mailto:' + email, email = null
468 if (!name && email) name = email, email = null
469 var feed = u.isRef(url) && url[0] === '@' && url
470 if (feed && name) title = this.app.getNameSync(feed)
471 if (feed && name && name[0] != '@') name = '@' + name
472 if (feed && !name) name = this.app.getNameSync(feed) // TODO: async
473 if (url && !name) name = url
474 var secondaryLink = email && h('a', {href: this.toUrl('mailto:' + email)}, email)
475 return [
476 h('a', {href: this.toUrl(url), title: title}, name),
477 secondaryLink ? [' (', secondaryLink, ')'] : ''
478 ]
479}
480
481// auto-highlight is slow
482var useAutoHighlight = false
483
484Render.prototype.highlight = function (code, lang) {
485 if (code.length > 100000) return u.escapeHTML(code)
486 if (!lang && /^#!\/bin\/[^\/]*sh$/m.test(code)) lang = 'sh'
487 try {
488 return lang
489 ? Highlight.highlight(lang, code).value
490 : useAutoHighlight
491 ? Highlight.highlightAuto(code).value
492 : u.escapeHTML(code)
493 } catch(e) {
494 if (!/^Unknown language/.test(e.message)) console.trace(e)
495 return u.escapeHTML(code)
496 }
497}
498
499Render.prototype.npmPackageMentions = function (links, cb) {
500 var self = this
501 var pkgLinks = u.toArray(links).filter(function (link) {
502 return /^npm:/.test(link.name)
503 })
504 if (pkgLinks.length === 0) return cb(null, '')
505 var done = multicb({pluck: 1})
506 pkgLinks.forEach(function (link) {
507 self.npmPackageMention(link, {}, done())
508 })
509 done(function (err, mentionEls) {
510 cb(null, h('table',
511 h('thead', h('tr',
512 h('th', 'package'),
513 h('th', 'version'),
514 h('th', 'tag'),
515 h('th', 'size'),
516 h('th', 'tarball'),
517 h('th', 'readme')
518 )),
519 h('tbody', mentionEls)
520 ))
521 })
522}
523
524Render.prototype.npmPrebuildMentions = function (links, cb) {
525 var self = this
526 var prebuildLinks = u.toArray(links).filter(function (link) {
527 return /^prebuild:/.test(link.name)
528 })
529 if (prebuildLinks.length === 0) return cb(null, '')
530 var done = multicb({pluck: 1})
531 prebuildLinks.forEach(function (link) {
532 self.npmPrebuildMention(link, {}, done())
533 })
534 done(function (err, mentionEls) {
535 cb(null, h('table',
536 h('thead', h('tr',
537 h('th', 'name'),
538 h('th', 'version'),
539 h('th', 'runtime'),
540 h('th', 'abi'),
541 h('th', 'platform+libc'),
542 h('th', 'arch'),
543 h('th', 'size'),
544 h('th', 'tarball')
545 )),
546 h('tbody', mentionEls)
547 ))
548 })
549}
550
551Render.prototype.npmPackageMention = function (link, opts, cb) {
552 var nameRegex = /'prebuild:{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz'/
553 var parts = String(link.name).replace(/\.tgz$/, '').split(':')
554 var name = parts[1]
555 var version = parts[2]
556 var distTag = parts[3]
557 var self = this
558 var done = multicb({pluck: 1, spread: true})
559 var base = '/npm/' + (opts.author ? u.escapeId(link.author) + '/' : '')
560 var pathWithAuthor = opts.withAuthor ? '/npm/' +
561 u.escapeId(link.author) +
562 (opts.name ? '/' + opts.name +
563 (opts.version ? '/' + opts.version +
564 (opts.distTag ? '/' + opts.distTag + '/' : '') : '') : '') : ''
565 self.app.getAbout(link.author, done())
566 self.app.getBlobState(link.link, done())
567 done(function (err, about, blobState) {
568 if (err) return cb(err)
569 cb(null, h('tr', [
570 opts.withAuthor ? h('td', h('a', {
571 href: self.toUrl(pathWithAuthor),
572 title: 'publisher'
573 }, about.name || u.truncate(link.author, 8)), ' ') : '',
574 h('td', h('a', {
575 href: self.toUrl(base + name),
576 title: 'package name'
577 }, name), ' '),
578 h('td', version ? [h('a', {
579 href: self.toUrl(base + name + '/' + version),
580 title: 'package version'
581 }, version), ' '] : ''),
582 h('td', distTag ? [h('a', {
583 href: self.toUrl(base + name + '//' + distTag),
584 title: 'dist-tag'
585 }, distTag), ' '] : ''),
586 h('td', {align: 'right'}, link.size != null ? [h('span', {
587 title: 'tarball size'
588 }, self.formatSize(link.size)), ' '] : ''),
589 h('td', typeof link.link === 'string' ? h('code', h('a', {
590 href: self.toUrl('/links/' + link.link),
591 title: 'package tarball'
592 }, link.link.substr(0, 8) + '…')) : ''),
593 h('td',
594 blobState === 'wanted' ?
595 'fetching...'
596 : blobState ? h('a', {
597 href: self.toUrl('/npm-readme/' + encodeURIComponent(link.link)),
598 title: 'package contents'
599 }, 'readme')
600 : self.blobFetchForm(link.link))
601 ]))
602 })
603}
604
605Render.prototype.blobFetchForm = function (id) {
606 return h('form', {action: '', method: 'post'},
607 h('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
608 h('input', {type: 'hidden', name: 'async_want', value: '1'}),
609 h('input', {type: 'hidden', name: 'blob_ids', value: id}),
610 h('input', {type: 'submit', value: 'fetch'})
611 )
612}
613
614Render.prototype.npmPrebuildNameRegex = /^prebuild:(.*?)-v([0-9]+\.[0-9]+.*?)-(.*?)-v(.*?)-(.*?)-(.*?)\.tar\.gz$/
615
616Render.prototype.npmPrebuildMention = function (link, opts, cb) {
617 var m = this.npmPrebuildNameRegex.exec(link.name)
618 if (!m) return cb(null, '')
619 var name = m[1], version = m[2], runtime = m[3],
620 abi = m[4], platformlibc = m[5], arch = m[6]
621 var self = this
622 var done = multicb({pluck: 1, spread: true})
623 var base = '/npm-prebuilds/' + (opts.author ? u.escapeId(link.author) + '/' : '')
624 self.app.getAbout(link.author, done())
625 self.app.getBlobState(link.link, done())
626 done(function (err, about, blobState) {
627 if (err) return cb(err)
628 cb(null, h('tr', [
629 opts.withAuthor ? h('td', h('a', {
630 href: self.toUrl(link.author)
631 }, about.name || u.truncate(link.author, 8)), ' ') : '',
632 h('td', h('a', {
633 href: self.toUrl(base + name)
634 }, name), ' '),
635 h('td', h('a', {
636 href: self.toUrl('/npm/' + name + '/' + version)
637 }, version), ' '),
638 h('td', runtime, ' '),
639 h('td', abi, ' '),
640 h('td', platformlibc, ' '),
641 h('td', arch, ' '),
642 h('td', {align: 'right'}, link.size != null ? [
643 self.formatSize(link.size), ' '
644 ] : ''),
645 h('td', typeof link.link === 'string' ? h('code', h('a', {
646 href: self.toUrl('/links/' + link.link)
647 }, link.link.substr(0, 8) + '…')) : ''),
648 h('td',
649 blobState === 'wanted' ?
650 'fetching...'
651 : blobState ? ''
652 : self.blobFetchForm(link.link))
653 ]))
654 })
655}
656
657Render.prototype.friendsList = function (prefix) {
658 prefix = prefix || '/'
659 var self = this
660 return pull(
661 paramap(function (item, cb) {
662 if (typeof item === 'string') item = {feed: item}
663 var id = item.feed
664 self.app.getAbout(item.feed, function (err, about) {
665 var name = about && about.name || id.substr(0, 8) + '…'
666 cb(null, [
667 h('a', {href: self.toUrl(prefix + id)}, name),
668 item.msg ? h('a', {href: self.toUrl(item.msg.key)}, '₁') : '',
669 item.msg2 ? h('a', {href: self.toUrl(item.msg2.key)}, '₂') : ''
670 ])
671 })
672 }, 8),
673 function (read) {
674 var count = 0
675 var ended
676 return function (abort, cb) {
677 if (ended) return cb(ended)
678 read(abort, function (end, el) {
679 if (end === true) {
680 ended = true
681 cb(null, '(' + count + ')')
682 } else {
683 count++
684 cb(end, u.toHTML(el) + ' ')
685 }
686 })
687 }
688 }
689 )
690}
691

Built with git-ssb-web