Files: a3dca9e424477c2745c59f482342a1580ed807f0 / lib / render.js
9836 bytesRaw
1 | var fs = require('fs') |
2 | var path = require('path') |
3 | var pull = require('pull-stream') |
4 | var cat = require('pull-cat') |
5 | var paramap = require('pull-paramap') |
6 | var h = require('hyperscript') |
7 | var marked = require('ssb-marked') |
8 | var emojis = require('emoji-named-characters') |
9 | var qs = require('querystring') |
10 | var u = require('./util') |
11 | var multicb = require('multicb') |
12 | var RenderMsg = require('./render-msg') |
13 | |
14 | module.exports = Render |
15 | |
16 | function MdRenderer(render) { |
17 | marked.Renderer.call(this, {}) |
18 | this.render = render |
19 | } |
20 | MdRenderer.prototype = new marked.Renderer() |
21 | |
22 | MdRenderer.prototype.urltransform = function (href) { |
23 | return this.render.toUrl(href) |
24 | } |
25 | |
26 | MdRenderer.prototype.image = function (ref, title, text) { |
27 | var href = this.render.imageUrl(ref) |
28 | var name = text || title |
29 | if (name) href += '?name=' + encodeURIComponent(name) |
30 | return h('img', { |
31 | src: href, |
32 | alt: this.render.getImageAlt(ref, text), |
33 | title: title || undefined |
34 | }).outerHTML |
35 | } |
36 | |
37 | MdRenderer.prototype.link = function (ref, title, text) { |
38 | var href = this.urltransform(ref) |
39 | var name = href && /^\/(&|%26)/.test(href) && (title || text) |
40 | if (u.isRef(ref)) { |
41 | var myName = this.render.app.getNameSync(ref) |
42 | if (myName) title = title ? title + ' (' + myName + ')' : myName |
43 | } |
44 | var a = h('a', { |
45 | class: href === false ? 'bad' : undefined, |
46 | href: href !== false ? href : undefined, |
47 | title: title || undefined, |
48 | download: name ? encodeURIComponent(name) : undefined |
49 | }) |
50 | // text is already html-escaped |
51 | a.innerHTML = text |
52 | return a.outerHTML |
53 | } |
54 | |
55 | MdRenderer.prototype.mention = function (preceding, id) { |
56 | var href = this.urltransform(id) |
57 | var myName = this.render.app.getNameSync(id) |
58 | if (id.length > 50) id = id.slice(0, 8) + '…' |
59 | return (preceding||'') + h('a', { |
60 | class: href === false ? 'bad' : undefined, |
61 | href: href !== false ? href : undefined, |
62 | title: myName || undefined, |
63 | }, id).outerHTML |
64 | } |
65 | |
66 | function lexerRenderEmoji(emoji) { |
67 | var el = this.renderer.render.emoji(emoji) |
68 | return el && el.outerHTML || el |
69 | } |
70 | |
71 | function Render(app, opts) { |
72 | this.app = app |
73 | this.opts = opts |
74 | |
75 | this.markedOpts = { |
76 | gfm: true, |
77 | mentions: true, |
78 | tables: true, |
79 | breaks: true, |
80 | pedantic: false, |
81 | sanitize: true, |
82 | smartLists: true, |
83 | smartypants: false, |
84 | emoji: lexerRenderEmoji, |
85 | renderer: new MdRenderer(this), |
86 | } |
87 | } |
88 | |
89 | Render.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 |
115 | function 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 | |
124 | Render.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 |
138 | }) |
139 | var out = marked(String(text), this.markedOpts) |
140 | delete this._mentions |
141 | delete this._mentionsByLink |
142 | return out //fixSymbols(out) |
143 | } |
144 | |
145 | Render.prototype.imageUrl = function (ref) { |
146 | var m = /^blobstore:(.*)/.exec(ref) |
147 | if (m) ref = m[1] |
148 | return this.opts.img_base + ref |
149 | } |
150 | |
151 | Render.prototype.getImageAlt = function (id, fallback) { |
152 | var link = this._mentionsByLink[id] |
153 | if (!link) return fallback |
154 | var name = link.name || fallback |
155 | return name |
156 | + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : '') |
157 | } |
158 | |
159 | Render.prototype.formatSize = function (size) { |
160 | if (size < 1024) return size + ' B' |
161 | size /= 1024 |
162 | if (size < 1024) return size.toFixed(2) + ' KB' |
163 | size /= 1024 |
164 | return size.toFixed(2) + ' MB' |
165 | } |
166 | |
167 | Render.prototype.linkify = function (text) { |
168 | var arr = text.split(u.ssbRefEncRegex) |
169 | for (var i = 1; i < arr.length; i += 2) { |
170 | arr[i] = h('a', {href: this.toUrlEnc(arr[i])}, arr[i]) |
171 | } |
172 | return arr |
173 | } |
174 | |
175 | Render.prototype.toUrlEnc = function (href) { |
176 | var url = this.toUrl(href) |
177 | if (url) return url |
178 | try { href = decodeURIComponent(href) } |
179 | catch (e) { return false } |
180 | return this.toUrl(href) |
181 | } |
182 | |
183 | Render.prototype.toUrl = function (href) { |
184 | if (!href) return href |
185 | var mentions = this._mentions |
186 | if (mentions && href in this._mentions) href = this._mentions[href] |
187 | if (/^ssb:\/\//.test(href)) href = href.substr(6) |
188 | switch (href[0]) { |
189 | case '%': |
190 | if (!u.isRef(href)) return false |
191 | return this.opts.base + encodeURIComponent(href) |
192 | case '@': |
193 | if (!u.isRef(href)) return false |
194 | return this.opts.base + href |
195 | case '&': |
196 | if (!u.isRef(href)) return false |
197 | return this.opts.blob_base + href |
198 | case '#': return this.opts.base + 'channel/' + |
199 | encodeURIComponent(href.substr(1)) |
200 | case '/': return this.opts.base + href.substr(1) |
201 | case '?': return this.opts.base + 'search?q=' + encodeURIComponent(href) |
202 | } |
203 | var m = /^blobstore:(.*)/.exec(href) |
204 | if (m) return this.opts.blob_base + m[1] |
205 | if (/^javascript:/.test(href)) return false |
206 | return href |
207 | } |
208 | |
209 | Render.prototype.lockIcon = function () { |
210 | return this.emoji('lock') |
211 | } |
212 | |
213 | Render.prototype.avatarImage = function (link, cb) { |
214 | var self = this |
215 | if (!link) return cb(), '' |
216 | if (typeof link === 'string') link = {link: link} |
217 | var img = h('img.ssb-avatar-image', { |
218 | width: 72, |
219 | alt: ' ' |
220 | }) |
221 | if (link.image) gotAbout(null, link) |
222 | else self.app.getAbout(link.link, gotAbout) |
223 | function gotAbout(err, about) { |
224 | if (err) return cb(err) |
225 | if (!about.image) img.src = self.toUrl('/static/fallback.png') |
226 | else img.src = self.imageUrl(about.image) |
227 | cb() |
228 | } |
229 | return img |
230 | } |
231 | |
232 | Render.prototype.prepareLink = function (link, cb) { |
233 | if (typeof link === 'string') link = {link: link} |
234 | if (link.name || !link.link) cb(null, link) |
235 | else this.app.getAbout(link.link, function (err, about) { |
236 | if (err) return cb(null, link) |
237 | link.name = about.name || about.title || (link.link.substr(0, 8) + '…') |
238 | if (link.name && link.name[0] === link.link[0]) { |
239 | link.name = link.name.substr(1) |
240 | } |
241 | cb(null, link) |
242 | }) |
243 | } |
244 | |
245 | Render.prototype.prepareLinks = function (links, cb) { |
246 | var self = this |
247 | if (!links) return cb() |
248 | var done = multicb({pluck: 1}) |
249 | if (Array.isArray(links)) links.forEach(function (link) { |
250 | self.prepareLink(link, done()) |
251 | }) |
252 | done(cb) |
253 | } |
254 | |
255 | Render.prototype.idLink = function (link, cb) { |
256 | var self = this |
257 | if (!link) return cb(), '' |
258 | var a = h('a', ' ') |
259 | self.prepareLink(link, function (err, link) { |
260 | if (err) return cb(err) |
261 | a.href = self.toUrl(link.link) |
262 | var sigil = link.link && link.link[0] || '@' |
263 | a.childNodes[0].textContent = sigil + link.name |
264 | cb() |
265 | }) |
266 | return a |
267 | } |
268 | |
269 | Render.prototype.privateLine = function (recps, cb) { |
270 | var done = multicb({pluck: 1, spread: true}) |
271 | var self = this |
272 | var el = h('div.recps', |
273 | self.lockIcon(), |
274 | Array.isArray(recps) |
275 | ? recps.map(function (recp) { |
276 | return [' ', self.idLink(recp, done())] |
277 | }) : '') |
278 | done(cb) |
279 | return el |
280 | } |
281 | |
282 | Render.prototype.msgLink = function (msg, cb) { |
283 | var self = this |
284 | var el = h('span') |
285 | var a = h('a', {href: self.toUrl(msg.key)}, msg.key) |
286 | self.app.unboxMsg(msg, function (err, msg) { |
287 | if (err) return el.appendChild(u.renderError(err)), cb() |
288 | var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false}) |
289 | renderMsg.title(function (err, title) { |
290 | if (err) return el.appendChild(u.renderError(err)), cb() |
291 | a.childNodes[0].textContent = title |
292 | cb() |
293 | }) |
294 | }) |
295 | return a |
296 | } |
297 | |
298 | Render.prototype.renderMsg = function (msg, opts, cb) { |
299 | new RenderMsg(this, this.app, msg, opts).message(cb) |
300 | } |
301 | |
302 | Render.prototype.renderFeeds = function (opts) { |
303 | var self = this |
304 | return paramap(function (msg, cb) { |
305 | self.renderMsg(msg, opts, cb) |
306 | }, 4) |
307 | } |
308 | |
309 | Render.prototype.gitCommitBody = function (body) { |
310 | if (!body) return '' |
311 | var isMarkdown = !/^# Conflicts:$/m.test(body) |
312 | return isMarkdown |
313 | ? h('div', {innerHTML: this.markdown('\n' + body)}) |
314 | : h('pre', this.linkify('\n' + body)) |
315 | } |
316 | |
317 | Render.prototype.getName = function (id, cb) { |
318 | // TODO: consolidate the get name/link functions |
319 | var self = this |
320 | switch (id && id[0]) { |
321 | case '%': |
322 | return self.app.getMsgDecrypted(id, function (err, msg) { |
323 | if (err && err.name == 'NotFoundError') |
324 | return cb(null, String(id).substring(0, 8) + '…(missing)') |
325 | if (err) return fallback() |
326 | new RenderMsg(self, self.app, msg, {wrap: false}).title(cb) |
327 | }) |
328 | case '@': // fallthrough |
329 | case '&': |
330 | return self.app.getAbout(id, function (err, about) { |
331 | if (err || !about || !about.name) return fallback() |
332 | cb(null, about.name) |
333 | }) |
334 | default: |
335 | return cb(null, String(id)) |
336 | } |
337 | function fallback() { |
338 | cb(null, String(id).substr(0, 8) + '…') |
339 | } |
340 | } |
341 | |
342 | Render.prototype.getNameLink = function (id, cb) { |
343 | var self = this |
344 | self.getName(id, function (err, name) { |
345 | if (err) return cb(err) |
346 | cb(null, h('a', {href: self.toUrl(id)}, name)) |
347 | }) |
348 | } |
349 |
Built with git-ssb-web