git ssb

16+

cel / patchfoo



Tree: 6a3032b17d025bd77eb816648c49ffc01d7a8361

Files: 6a3032b17d025bd77eb816648c49ffc01d7a8361 / lib / render.js

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

Built with git-ssb-web