git ssb

16+

cel / patchfoo



Tree: 8fd6613c73b3cc03e594c3a4ad89e3303cfd5131

Files: 8fd6613c73b3cc03e594c3a4ad89e3303cfd5131 / lib / render.js

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

Built with git-ssb-web