git ssb

16+

cel / patchfoo



Tree: 2eb748d24d3ee0d13f990d2b7eab167a1b41b930

Files: 2eb748d24d3ee0d13f990d2b7eab167a1b41b930 / lib / render.js

14209 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 )),
412 h('tbody', mentionEls)
413 ))
414 })
415}
416
417Render.prototype.npmPackageMention = function (link, opts, cb) {
418 var parts = String(link.name).replace(/\.tgz$/, '').split(':')
419 var name = parts[1]
420 var version = parts[2]
421 var distTag = parts[3]
422 var self = this
423 var done = multicb({pluck: 1, spread: true})
424 var base = '/npm/' + (opts.author ? u.escapeId(link.author) + '/' : '')
425 var pathWithAuthor = opts.withAuthor ? '/npm/' +
426 u.escapeId(link.author) + '/' +
427 (opts.name ? opts.name + '/' +
428 (opts.version ? opts.version + '/' +
429 (opts.distTag ? opts.distTag + '/' : '') : '') : '') : ''
430 self.app.getAbout(link.author, done())
431 self.app.getBlobState(link.link, done())
432 done(function (err, about, blobState) {
433 if (err) return cb(err)
434 cb(null, h('tr', [
435 opts.withAuthor ? h('td', h('a', {
436 href: self.toUrl(pathWithAuthor),
437 title: 'publisher'
438 }, about.name), ' ') : '',
439 h('td', h('a', {
440 href: self.toUrl(base + name),
441 title: 'package name'
442 }, name), ' '),
443 h('td', version ? [h('a', {
444 href: self.toUrl(base + name + '/' + version + '/'),
445 title: 'package version'
446 }, version), ' '] : ''),
447 h('td', distTag ? [h('a', {
448 href: self.toUrl(base + name + '//' + distTag + '/'),
449 title: 'dist-tag'
450 }, distTag), ' '] : ''),
451 h('td', {align: 'right'}, link.size != null ? [h('span', {
452 title: 'tarball size'
453 }, self.formatSize(link.size)), ' '] : ''),
454 h('td', typeof link.link === 'string' ? h('code', h('a', {
455 href: self.toUrl('/links/' + link.link),
456 title: 'package tarball'
457 }, link.link.substr(0, 8) + '…')) : ''),
458 h('td',
459 blobState === 'wanted' ?
460 'fetching...'
461 : blobState ? h('a', {
462 href: self.toUrl('/npm-readme/' + encodeURIComponent(link.link)),
463 title: 'package contents'
464 }, 'readme')
465 : h('form', {action: '', method: 'post'},
466 h('input', {type: 'hidden', name: 'action', value: 'want-blobs'}),
467 h('input', {type: 'hidden', name: 'async_want', value: '1'}),
468 h('input', {type: 'hidden', name: 'blob_ids', value: link.link}),
469 h('input', {type: 'submit', value: 'fetch'})
470 ))
471 ]))
472 })
473}
474

Built with git-ssb-web