git ssb

16+

cel / patchfoo



Tree: 5a8a31c7e4523ef4de4c20e28443f9ea18961ce4

Files: 5a8a31c7e4523ef4de4c20e28443f9ea18961ce4 / lib / render.js

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

Built with git-ssb-web