git ssb

16+

cel / patchfoo



Tree: a448b261d6bffa420d1f0c4d57ebe852146001bf

Files: a448b261d6bffa420d1f0c4d57ebe852146001bf / lib / render.js

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

Built with git-ssb-web