Files: d185716fb11842271c37ff499725b2b66ba2a5e3 / lib / render.js
25214 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 ph = require('pull-hyperscript') |
8 | var marked = require('../vendor/ssb-marked') |
9 | var qs = require('querystring') |
10 | var u = require('./util') |
11 | var multicb = require('multicb') |
12 | var RenderMsg = require('./render-msg') |
13 | var Highlight = require('highlight.js') |
14 | var md = require('ssb-markdown') |
15 | var Diff = require('diff') |
16 | var lru = require('hashlru') |
17 | |
18 | var maxWordLength = 80 |
19 | var truncateWordsRe = new RegExp('[^\\s<>&"]{' + (maxWordLength+1) + ',}') |
20 | function truncateWord(word) { |
21 | var len = maxWordLength - 5 // "…[##]" |
22 | var truncatedLength = word.length - len |
23 | return word.substr(0, len) + '…<sub>[' + truncatedLength + ']</sub>' |
24 | } |
25 | function truncateWords(text) { |
26 | // truncate long words to avoid horizontal scrollbar |
27 | return text.replace(truncateWordsRe, truncateWord) |
28 | } |
29 | |
30 | module.exports = Render |
31 | |
32 | function MdRenderer(render) { |
33 | marked.Renderer.call(this, {}) |
34 | this.render = render |
35 | } |
36 | MdRenderer.prototype = new marked.Renderer() |
37 | |
38 | MdRenderer.prototype.urltransform = function (href) { |
39 | return this.render.toUrl(href) |
40 | } |
41 | |
42 | MdRenderer.prototype.image = function (ref, title, text) { |
43 | if (ref[0] !== '&') return this.link(ref, title, text) |
44 | var alt = this.render.getImageAlt(ref, text) |
45 | if (alt.length > 60) alt = alt.substr(0, 60) + '…' |
46 | return (/^video:/.test(text) ? h('video', { |
47 | controls: 'controls', |
48 | src: this.render.toUrl(ref), |
49 | title: u.unescapeHTML(title) || undefined |
50 | }, '\n' + alt) : /^audio:/.test(text) ? h('audio', { |
51 | controls: 'controls', |
52 | src: this.render.toUrl(ref), |
53 | title: u.unescapeHTML(title) || undefined |
54 | }, '\n' + alt) : h('img', { |
55 | src: this.render.imageUrl(ref), |
56 | alt: alt, |
57 | title: u.unescapeHTML(title) || undefined |
58 | })).outerHTML |
59 | } |
60 | |
61 | MdRenderer.prototype.link = function (ref, title, text) { |
62 | var href = this.urltransform(ref) |
63 | var name = href && /^\/(&|%26)/.test(href) && (title || text) |
64 | if (name) { |
65 | href += (/\?/.test(href) ? '&' : '?') + |
66 | 'filename=' + encodeURIComponent(name) |
67 | } |
68 | if (u.isRef(ref)) { |
69 | var myName = this.render.app.getNameSync(ref) |
70 | if (myName) title = title ? title + ' (' + myName + ')' : myName |
71 | } |
72 | var hrefToken = href !== false ? u.token() : undefined |
73 | var a = h('a', { |
74 | class: href === false ? 'bad' : undefined, |
75 | href: href !== false ? hrefToken : undefined, |
76 | title: title || undefined |
77 | }) |
78 | text = truncateWords(text) |
79 | // text is already html-escaped |
80 | a.innerHTML = text |
81 | var html = a.outerHTML |
82 | // href is already html-escaped |
83 | if (hrefToken) html = html.replace(hrefToken, href) |
84 | |
85 | var link = this.render._mentionsByLink[ref] |
86 | if (link && link.type === 'text/x-markdown') { |
87 | html += h('sup', ' [', h('a', { |
88 | href: this.render.toUrl('/markdown/' + ref), |
89 | title: 'view rendered markdown' |
90 | }, 'md'), ']').outerHTML |
91 | } |
92 | |
93 | return html |
94 | } |
95 | |
96 | MdRenderer.prototype.mention = function (preceding, id) { |
97 | var href = this.urltransform(id) |
98 | var myName = this.render.app.getNameSync(id) |
99 | var html = (preceding||'') + h('a', { |
100 | class: href === false ? 'bad' : undefined, |
101 | href: href !== false ? href : undefined, |
102 | title: myName || undefined, |
103 | }, id.length > 50 ? id.slice(0, 8) + '…' : id).outerHTML |
104 | |
105 | var link = this.render._mentionsByLink[id] |
106 | if (link && link.type === 'text/x-markdown') { |
107 | html += h('sup', ' [', h('a', { |
108 | href: this.render.toUrl('/markdown/' + id), |
109 | title: 'view rendered markdown' |
110 | }, 'md'), ']').outerHTML |
111 | } |
112 | |
113 | return html |
114 | } |
115 | |
116 | MdRenderer.prototype.code = function (code, lang, escaped) { |
117 | if (this.render.opts.codeInTextareas) { |
118 | return h('div', h('textarea', { |
119 | cols: 80, |
120 | rows: u.rows(code), |
121 | style: 'font-family: monospace', |
122 | innerHTML: escaped ? code : u.escapeHTML(code) |
123 | })).outerHTML |
124 | } else { |
125 | return marked.Renderer.prototype.code.call(this, code, lang, escaped) |
126 | } |
127 | } |
128 | |
129 | function lexerRenderEmoji(emoji) { |
130 | var el = this.renderer.render.emoji(emoji) |
131 | return el && el.outerHTML || el |
132 | } |
133 | |
134 | function Render(app, opts) { |
135 | this.app = app |
136 | this.opts = opts |
137 | |
138 | this.markedOpts = { |
139 | gfm: true, |
140 | mentions: true, |
141 | tables: true, |
142 | breaks: true, |
143 | pedantic: false, |
144 | sanitize: true, |
145 | smartLists: true, |
146 | smartypants: false, |
147 | emoji: lexerRenderEmoji, |
148 | renderer: new MdRenderer(this), |
149 | highlight: this.highlight.bind(this), |
150 | } |
151 | } |
152 | |
153 | Render.prototype.emoji = function (emoji) { |
154 | var name = ':' + emoji + ':' |
155 | var link = this._mentions && this._mentions[name] |
156 | if (link && link.link) { |
157 | this.app.reverseEmojiNameCache.set(emoji, link.link) |
158 | return h('img.ssb-emoji', { |
159 | src: this.opts.img_base + link.link, |
160 | alt: name |
161 | + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : ''), |
162 | height: 17, |
163 | title: name, |
164 | }) |
165 | } |
166 | return name |
167 | } |
168 | |
169 | /* disabled until it can be done safely without breaking html |
170 | function fixSymbols(str) { |
171 | // Dillo doesn't do fallback fonts, so specifically render fancy characters |
172 | // with Symbola |
173 | return str.replace(/[^\u0000-\u00ff]+/, function ($0) { |
174 | return '<span class="symbol">' + $0 + '</span>' |
175 | }) |
176 | } |
177 | */ |
178 | |
179 | Render.prototype.symbolify = function (text) { |
180 | var arr = text.split(u.symbolRegex) |
181 | for (var i = 1; i < arr.length; i += 2) { |
182 | arr[i] = h('span.symbol', arr[i]) |
183 | } |
184 | return arr |
185 | } |
186 | |
187 | Render.prototype.markdown = function (text, mentions, opts) { |
188 | if (!text) return '' |
189 | var ssbcMd = opts && opts.ssbcMd |
190 | var mentionsObj = this._mentions = {} |
191 | var mentionsByLink = this._mentionsByLink = {} |
192 | if (Array.isArray(mentions)) mentions.forEach(function (link) { |
193 | if (!link) return |
194 | else if (link.emoji) |
195 | mentionsObj[':' + link.name + ':'] = link |
196 | else if (link.name && link.link && link.link[0] === '@') |
197 | mentionsObj['@' + link.name] = link.link |
198 | else if (link.host === 'http://localhost:7777') |
199 | mentionsObj[link.href] = link.link |
200 | if (link.link) |
201 | mentionsByLink[link.link + |
202 | (link.query && typeof link.query.unbox === 'string' ? |
203 | '?unbox=' + link.query.unbox.replace(/\s/g, '+') : '') + |
204 | (link.key ? '#' + link.key : '')] = link |
205 | }) |
206 | var self = this |
207 | var out = ssbcMd ? md.block(String(text), { |
208 | toUrl: function (ref) { return self.toUrl(ref) }, |
209 | imageLink: function (ref) { return self.imageUrl(ref) } |
210 | }) : marked(u.toString(text), this.markedOpts) |
211 | delete this._mentions |
212 | delete this._mentionsByLink |
213 | return out //fixSymbols(out) |
214 | } |
215 | |
216 | Render.prototype.imageUrl = function (ref) { |
217 | var m = /^blobstore:(.*)/.exec(ref) |
218 | if (m) ref = m[1] |
219 | ref = String(ref).replace(/#/, '%23') |
220 | return this.opts.img_base + ref |
221 | } |
222 | |
223 | Render.prototype.getImageAlt = function (id, fallback) { |
224 | var link = this._mentionsByLink[id] |
225 | if (!link) return fallback |
226 | var name = String(u.unescapeHTML(link.name || fallback)) |
227 | .replace(/^(video|audio):/, '') |
228 | return name |
229 | + (link.type && !/\.\S+$/.test(name) ? ' [' + link.type + ']' : '') |
230 | + (link.size != null ? ' (' + this.formatSize(link.size) + ')' : '') |
231 | } |
232 | |
233 | Render.prototype.formatSize = function (size) { |
234 | if (size < 1024) return size + ' B' |
235 | size /= 1024 |
236 | if (size < 1024) return size.toFixed(2) + ' KB' |
237 | size /= 1024 |
238 | return size.toFixed(2) + ' MB' |
239 | } |
240 | |
241 | Render.prototype.linkify = function (text, opts) { |
242 | var ellipsis = opts && opts.ellipsis |
243 | var arr = text.split(u.ssbRefEncRegex) |
244 | for (var i = 1; i < arr.length; i += 2) { |
245 | var id = arr[i] |
246 | var text = ellipsis && id.length > 8 ? id.substr(0, 8) + '…' : id |
247 | arr[i] = h('a', {href: this.toUrlEnc(id)}, text) |
248 | } |
249 | return arr |
250 | } |
251 | |
252 | Render.prototype.toUrlEnc = function (href) { |
253 | var url = this.toUrl(href) |
254 | if (url) return url |
255 | try { href = decodeURIComponent(href) } |
256 | catch (e) { return false } |
257 | return this.toUrl(href) |
258 | } |
259 | |
260 | Render.prototype.toUrl = function (href) { |
261 | if (!href) return href |
262 | var mentions = this._mentions |
263 | if (mentions && href in this._mentions) href = this._mentions[href] |
264 | if (/^ssb:/.test(href)) href = u.translateFromURI(href) || href |
265 | if (/^ssb-blob:\/\//.test(href)) { |
266 | return this.opts.base + 'zip/' + href.substr(11) |
267 | } |
268 | switch (href[0]) { |
269 | case '%': |
270 | var parts = href.split('?') |
271 | var hash = parts.shift() |
272 | var query = parts.join('?') |
273 | if (!u.isRef(hash)) return false |
274 | return this.opts.base + |
275 | (this.opts.encode_msgids ? encodeURIComponent(hash) : hash) |
276 | + (query ? '?' + query : '') |
277 | case '@': |
278 | if (!u.isRef(href.replace(/\?.*/, ''))) return false |
279 | return this.opts.base + href |
280 | case '&': |
281 | var parts = href.split('#') |
282 | var hash = parts.shift() |
283 | var key = parts.shift() |
284 | var fragment = parts.join('#') |
285 | parts = hash.split('?') |
286 | hash = parts.shift() |
287 | query = parts.join('?') |
288 | if (!u.isRef(hash)) return false |
289 | return this.opts.blob_base + hash |
290 | + (query ? '?' + query : '') |
291 | + (key ? encodeURIComponent('#' + key) : '') |
292 | + (fragment ? '#' + fragment : '') |
293 | case '#': return this.opts.base + 'channel/' + |
294 | encodeURIComponent(href.substr(1).toLowerCase()) |
295 | case '/': return this.opts.base + href.substr(1) |
296 | case '?': return this.opts.base + 'search?q=' + encodeURIComponent(href) |
297 | } |
298 | var m = /^blobstore:(.*)/.exec(href) |
299 | if (m) return this.opts.blob_base + m[1] |
300 | if (/^javascript:/.test(href)) return false |
301 | if (!/^[a-z0-9]*:/.test(href)) return 'http://' + href |
302 | return href |
303 | } |
304 | |
305 | Render.prototype.lockIcon = function () { |
306 | return h('span.symbol', '🔒') |
307 | } |
308 | |
309 | Render.prototype.avatarImage = function (link, cb) { |
310 | var self = this |
311 | if (!link) return cb(), '' |
312 | if (typeof link === 'string') link = {link: link} |
313 | var img = h('img.ssb-avatar-image', { |
314 | width: 72, |
315 | alt: ' ' |
316 | }) |
317 | if (link.image) gotAbout(null, link) |
318 | else self.app.getAbout(link.link, gotAbout) |
319 | function gotAbout(err, about) { |
320 | if (err) console.trace(err) |
321 | img.src = about && about.image ? self.imageUrl(about.image) |
322 | : self.toUrl('/static/fallback.png') |
323 | cb() |
324 | } |
325 | return img |
326 | } |
327 | |
328 | const errors = lru(100) |
329 | |
330 | function logUniqueError(err) { |
331 | // Avoid logging the same error many times. |
332 | if (errors.get(err.stack)) return |
333 | errors.set(err.stack, true) |
334 | console.error(err) |
335 | } |
336 | |
337 | Render.prototype.prepareLink = function (link, cb) { |
338 | if (typeof link === 'string') link = {link: link} |
339 | if (link.name || !link.link) cb(null, link) |
340 | else this.app.getAbout(link.link, function (err, about) { |
341 | // Don't let this function fail, instead, log the error once. |
342 | if (err) logUniqueError(err) |
343 | link.name = about && (about.name || about.title) || (link.link.substr(0, 8) + '…') |
344 | if (link.name && link.name[0] === link.link[0]) { |
345 | link.name = link.name.substr(1) |
346 | } |
347 | cb(null, link) |
348 | }) |
349 | } |
350 | |
351 | Render.prototype.prepareLinks = function (links, cb) { |
352 | var self = this |
353 | if (!links) return cb() |
354 | var done = multicb({pluck: 1}) |
355 | if (Array.isArray(links)) links.forEach(function (link) { |
356 | self.prepareLink(link, done()) |
357 | }) |
358 | done(cb) |
359 | } |
360 | |
361 | Render.prototype.idLink = function (link, cb) { |
362 | var self = this |
363 | if (!link) return cb(), '' |
364 | var a = h('a', ' ') |
365 | self.prepareLink(link, function (err, link) { |
366 | if (err) return cb(err) |
367 | a.href = self.toUrl(link.link) |
368 | var sigil = link.link && link.link[0] || '@' |
369 | var name = link.name || String(link.link).substr(1, 8) + '…' |
370 | a.childNodes[0].textContent = sigil + name |
371 | cb() |
372 | }) |
373 | return a |
374 | } |
375 | |
376 | // %NM8tXGBBDKKcpRbbyd/5uN1p/2OtBMFDylLMDPGoq8Q=.sha256 |
377 | var idRegex = /^[A-Za-z0-9._\-+=/]*[A-Za-z0-9_\-+=/]$/ |
378 | |
379 | Render.prototype.idLinkCopyable = function (link, cb) { |
380 | var self = this |
381 | if (!self.app.copyableIds) return idLink(link, cb) |
382 | if (!link) return cb(), '' |
383 | var a = h('a', ' ') |
384 | self.prepareLink(link, function (err, link) { |
385 | if (err) return cb(err) |
386 | a.href = self.toUrl(link.link) |
387 | var name = link.name || String(link.link).substr(1, 8) + '…' |
388 | if (idRegex.test(name)) a.childNodes[0].textContent = '@' + name |
389 | else { |
390 | var name = link.name.startsWith('~') ? link.name : '@' + link.name |
391 | a.className = 'id-copyable-link' |
392 | a.innerHTML = h('span', [ |
393 | h('span.id-deemphasize', '['), |
394 | h('span.id-name', name), |
395 | h('span.id-deemphasize', ']' |
396 | + '(', h('span.id-inner', link.link), ')'), |
397 | ]).innerHTML |
398 | } |
399 | cb() |
400 | }) |
401 | return a |
402 | } |
403 | |
404 | Render.prototype.privateLine = function (opts, cb) { |
405 | var recps = opts.recps |
406 | var isAuthorRecp = opts.isAuthorRecp |
407 | var done = multicb({pluck: 1, spread: true}) |
408 | var self = this |
409 | var el = h('div.recps', |
410 | opts.noLockIcon ? '' : self.lockIcon(), |
411 | !isAuthorRecp ? [ |
412 | h('span', {title: 'Author is not a recipient'}, '[!]'), ' ' |
413 | ] : '', |
414 | Array.isArray(recps) |
415 | ? recps.map(function (recp) { |
416 | return [' ', self.idLink(recp, done())] |
417 | }) : '') |
418 | done(cb) |
419 | return el |
420 | } |
421 | |
422 | Render.prototype.msgIdLink = function (id, cb) { |
423 | var self = this |
424 | var el = h('span') |
425 | var a = h('a', {href: self.toUrl(id)}, id) |
426 | self.app.getMsgDecrypted(id, function (err, msg) { |
427 | if (err) return el.appendChild(u.renderError(err)), cb() |
428 | var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve}) |
429 | renderMsg.title(function (err, title) { |
430 | if (err) return el.appendChild(u.renderError(err)), cb() |
431 | a.childNodes[0].textContent = title |
432 | cb() |
433 | }) |
434 | }) |
435 | return a |
436 | } |
437 | |
438 | Render.prototype.phMsgLink = function (msg) { |
439 | var self = this |
440 | return u.readNext(function (cb) { |
441 | self.app.unboxMsg(msg, function (err, msg) { |
442 | if (err) return cb(err) |
443 | var renderMsg = new RenderMsg(self, self.app, msg, {wrap: false, serve: self.serve}) |
444 | renderMsg.title(function (err, title) { |
445 | if (err) return cb(err) |
446 | cb(null, ph('a', {href: self.toUrl(msg.key)}, u.escapeHTML(title))) |
447 | }) |
448 | }) |
449 | }) |
450 | } |
451 | |
452 | Render.prototype.renderMsg = function (msg, opts, cb) { |
453 | var self = this |
454 | self.app.filterMsg(msg, opts, function (err, show) { |
455 | if (err) return cb(err) |
456 | if (show) new RenderMsg(self, self.app, msg, opts).message(cb) |
457 | else cb(null, '') |
458 | }) |
459 | } |
460 | |
461 | Render.prototype.renderFeeds = function (opts) { |
462 | var self = this |
463 | var limit = Number(opts.limit) |
464 | return pull( |
465 | paramap(function (msg, cb) { |
466 | self.renderMsg(msg, opts, cb) |
467 | }, 4), |
468 | pull.filter(Boolean), |
469 | limit && pull.take(limit) |
470 | ) |
471 | } |
472 | |
473 | Render.prototype.gitCommitBody = function (body) { |
474 | if (!body) return '' |
475 | var isMarkdown = !/^# Conflicts:$/m.test(body) |
476 | return isMarkdown |
477 | ? h('div', {innerHTML: this.markdown('\n' + body)}) |
478 | : h('pre', this.linkify('\n' + body)) |
479 | } |
480 | |
481 | Render.prototype.getName = function (id, cb) { |
482 | // TODO: consolidate the get name/link functions |
483 | var self = this |
484 | switch (id && id[0]) { |
485 | case '%': |
486 | return self.app.getMsgDecrypted(id, function (err, msg) { |
487 | if (err && err.name == 'NotFoundError') |
488 | return cb(null, String(id).substring(0, 8) + '…(missing)') |
489 | if (err) return fallback() |
490 | new RenderMsg(self, self.app, msg, {wrap: false}).title(cb) |
491 | }) |
492 | case '@': // fallthrough |
493 | case '&': |
494 | return self.app.getAbout(id, function (err, about) { |
495 | if (err || !about || !about.name) return fallback() |
496 | cb(null, about.name) |
497 | }) |
498 | default: |
499 | return cb(null, String(id)) |
500 | } |
501 | function fallback() { |
502 | cb(null, String(id).substr(0, 8) + '…') |
503 | } |
504 | } |
505 | |
506 | Render.prototype.getNameLink = function (id, opts, cb) { |
507 | if (!cb && typeof opts === 'function') cb = opts, opts = null |
508 | if (typeof id === 'object' && id !== null && typeof id.link === 'string') { |
509 | var link = id |
510 | id = link.link |
511 | if (typeof link.name === 'string') { |
512 | return cb(null, h('a', {href: self.toUrl(id)}, |
513 | u.truncate(link.name, length))) |
514 | } |
515 | } |
516 | var length = opts && opts.length || Infinity |
517 | var self = this |
518 | self.getName(id, function (err, name) { |
519 | if (err) return cb(err) |
520 | cb(null, h('a', {href: self.toUrl(id)}, u.truncate(name, length))) |
521 | }) |
522 | } |
523 | |
524 | Render.prototype.npmAuthorLink = function (author) { |
525 | if (!author) return |
526 | var url = u.ifString(author.url) |
527 | var email = u.ifString(author.email) |
528 | var name = u.ifString(author.name) |
529 | var title |
530 | if (!url && u.isRef(name)) url = name, name = null |
531 | if (!url && !email) return name || JSON.stringify(author) |
532 | if (!url && email) url = 'mailto:' + email, email = null |
533 | if (!name && email) name = email, email = null |
534 | var feed = u.isRef(url) && url[0] === '@' && url |
535 | if (feed && name) title = this.app.getNameSync(feed) |
536 | if (feed && name && name[0] != '@') name = '@' + name |
537 | if (feed && !name) name = this.app.getNameSync(feed) // TODO: async |
538 | if (url && !name) name = url |
539 | var secondaryLink = email && h('a', {href: this.toUrl('mailto:' + email)}, email) |
540 | return [ |
541 | h('a', {href: this.toUrl(url), title: title}, name), |
542 | secondaryLink ? [' (', secondaryLink, ')'] : '' |
543 | ] |
544 | } |
545 | |
546 | // auto-highlight is slow |
547 | var useAutoHighlight = false |
548 | |
549 | Render.prototype.highlight = function (code, lang) { |
550 | if (code.length > 100000) return u.escapeHTML(code) |
551 | if (!lang && /^#!\/bin\/[^\/]*sh$/m.test(code)) lang = 'sh' |
552 | try { |
553 | return lang |
554 | ? Highlight.highlight(lang, code).value |
555 | : useAutoHighlight |
556 | ? Highlight.highlightAuto(code).value |
557 | : u.escapeHTML(code) |
558 | } catch(e) { |
559 | if (!/^Unknown language/.test(e.message)) console.trace(e) |
560 | return u.escapeHTML(code) |
561 | } |
562 | } |
563 | |
564 | Render.prototype.npmPackageMentions = function (links, cb) { |
565 | var self = this |
566 | var pkgLinks = u.toArray(links).filter(function (link) { |
567 | return /^npm:/.test(link.name) |
568 | }) |
569 | if (pkgLinks.length === 0) return cb(null, '') |
570 | var done = multicb({pluck: 1}) |
571 | pkgLinks.forEach(function (link) { |
572 | self.npmPackageMention(link, {}, done()) |
573 | }) |
574 | done(function (err, mentionEls) { |
575 | cb(null, h('table', |
576 | h('thead', h('tr', |
577 | h('th', 'package'), |
578 | h('th', 'version'), |
579 | h('th', 'tag'), |
580 | h('th', 'size'), |
581 | h('th', 'tarball'), |
582 | h('th', 'readme') |
583 | )), |
584 | h('tbody', mentionEls) |
585 | )) |
586 | }) |
587 | } |
588 | |
589 | Render.prototype.npmPrebuildMentions = function (links, cb) { |
590 | var self = this |
591 | var prebuildLinks = u.toArray(links).filter(function (link) { |
592 | return /^prebuild:/.test(link.name) |
593 | }) |
594 | if (prebuildLinks.length === 0) return cb(null, '') |
595 | var done = multicb({pluck: 1}) |
596 | prebuildLinks.forEach(function (link) { |
597 | self.npmPrebuildMention(link, {}, done()) |
598 | }) |
599 | done(function (err, mentionEls) { |
600 | cb(null, h('table', |
601 | h('thead', h('tr', |
602 | h('th', 'name'), |
603 | h('th', 'version'), |
604 | h('th', 'runtime'), |
605 | h('th', 'abi'), |
606 | h('th', 'platform+libc'), |
607 | h('th', 'arch'), |
608 | h('th', 'size'), |
609 | h('th', 'tarball') |
610 | )), |
611 | h('tbody', mentionEls) |
612 | )) |
613 | }) |
614 | } |
615 | |
616 | Render.prototype.npmPackageMention = function (link, opts, cb) { |
617 | var nameRegex = /'prebuild:{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz'/ |
618 | var parts = String(link.name).replace(/\.tgz$/, '').split(':') |
619 | var name = parts[1] |
620 | var version = parts[2] |
621 | var distTag = parts[3] |
622 | var self = this |
623 | var done = multicb({pluck: 1, spread: true}) |
624 | var base = '/npm/' + (opts.author ? u.escapeId(link.author) + '/' : '') |
625 | var pathWithAuthor = opts.withAuthor ? '/npm/' + |
626 | u.escapeId(link.author) + |
627 | (opts.name ? '/' + opts.name + |
628 | (opts.version ? '/' + opts.version + |
629 | (opts.distTag ? '/' + opts.distTag + '/' : '') : '') : '') : '' |
630 | self.app.getAbout(link.author, done()) |
631 | self.app.getBlobState(link.link, done()) |
632 | done(function (err, about, blobState) { |
633 | if (err) return cb(err) |
634 | cb(null, h('tr', [ |
635 | opts.withAuthor ? h('td', h('a', { |
636 | href: self.toUrl(pathWithAuthor), |
637 | title: 'publisher' |
638 | }, about.name || u.truncate(link.author, 8)), ' ') : '', |
639 | h('td', h('a', { |
640 | href: self.toUrl(base + name), |
641 | title: 'package name' |
642 | }, name), ' '), |
643 | h('td', version ? [h('a', { |
644 | href: self.toUrl(base + name + '/' + version), |
645 | title: 'package version' |
646 | }, version), ' '] : ''), |
647 | h('td', distTag ? [h('a', { |
648 | href: self.toUrl(base + name + '//' + distTag), |
649 | title: 'dist-tag' |
650 | }, distTag), ' '] : ''), |
651 | h('td', {align: 'right'}, link.size != null ? [h('span', { |
652 | title: 'tarball size' |
653 | }, self.formatSize(link.size)), ' '] : ''), |
654 | h('td', typeof link.link === 'string' ? h('code', h('a', { |
655 | href: self.toUrl('/links/' + link.link), |
656 | title: 'package tarball' |
657 | }, link.link.substr(0, 8) + '…')) : ''), |
658 | h('td', |
659 | blobState === 'wanted' ? |
660 | 'fetching...' |
661 | : blobState ? h('a', { |
662 | href: self.toUrl('/npm-readme/' + encodeURIComponent(link.link)), |
663 | title: 'package contents' |
664 | }, 'readme') |
665 | : self.blobFetchForm(link.link)) |
666 | ])) |
667 | }) |
668 | } |
669 | |
670 | Render.prototype.blobFetchForm = function (id) { |
671 | return h('form', {action: '', method: 'post'}, |
672 | h('input', {type: 'hidden', name: 'action', value: 'want-blobs'}), |
673 | h('input', {type: 'hidden', name: 'async_want', value: '1'}), |
674 | h('input', {type: 'hidden', name: 'blob_ids', value: id}), |
675 | h('input', {type: 'submit', value: 'fetch'}) |
676 | ) |
677 | } |
678 | |
679 | Render.prototype.npmPrebuildNameRegex = /^prebuild:(.*?)-v([0-9]+\.[0-9]+.*?)-(.*?)-v(.*?)-(.*?)-(.*?)\.tar\.gz$/ |
680 | |
681 | Render.prototype.npmPrebuildMention = function (link, opts, cb) { |
682 | var m = this.npmPrebuildNameRegex.exec(link.name) |
683 | if (!m) return cb(null, '') |
684 | var name = m[1], version = m[2], runtime = m[3], |
685 | abi = m[4], platformlibc = m[5], arch = m[6] |
686 | var self = this |
687 | var done = multicb({pluck: 1, spread: true}) |
688 | var base = '/npm-prebuilds/' + (opts.author ? u.escapeId(link.author) + '/' : '') |
689 | self.app.getAbout(link.author, done()) |
690 | self.app.getBlobState(link.link, done()) |
691 | done(function (err, about, blobState) { |
692 | if (err) return cb(err) |
693 | cb(null, h('tr', [ |
694 | opts.withAuthor ? h('td', h('a', { |
695 | href: self.toUrl(link.author) |
696 | }, about.name || u.truncate(link.author, 8)), ' ') : '', |
697 | h('td', h('a', { |
698 | href: self.toUrl(base + name) |
699 | }, name), ' '), |
700 | h('td', h('a', { |
701 | href: self.toUrl('/npm/' + name + '/' + version) |
702 | }, version), ' '), |
703 | h('td', runtime, ' '), |
704 | h('td', abi, ' '), |
705 | h('td', platformlibc, ' '), |
706 | h('td', arch, ' '), |
707 | h('td', {align: 'right'}, link.size != null ? [ |
708 | self.formatSize(link.size), ' ' |
709 | ] : ''), |
710 | h('td', typeof link.link === 'string' ? h('code', h('a', { |
711 | href: self.toUrl('/links/' + link.link) |
712 | }, link.link.substr(0, 8) + '…')) : ''), |
713 | h('td', |
714 | blobState === 'wanted' ? |
715 | 'fetching...' |
716 | : blobState ? '' |
717 | : self.blobFetchForm(link.link)) |
718 | ])) |
719 | }) |
720 | } |
721 | |
722 | Render.prototype.friendsList = function (prefix) { |
723 | prefix = prefix || '/' |
724 | var self = this |
725 | return pull( |
726 | paramap(function (item, cb) { |
727 | if (typeof item === 'string') item = {feed: item} |
728 | var id = item.feed |
729 | self.app.getAbout(item.feed, function (err, about) { |
730 | var name = about && about.name || id.substr(0, 8) + '…' |
731 | cb(null, [ |
732 | h('a', {href: self.toUrl(prefix + id)}, name), |
733 | item.msg ? h('a', {href: self.toUrl(item.msg.key)}, '₁') : '', |
734 | item.msg2 ? h('a', {href: self.toUrl(item.msg2.key)}, '₂') : '' |
735 | ]) |
736 | }) |
737 | }, 8), |
738 | function (read) { |
739 | var count = 0 |
740 | var ended |
741 | return function (abort, cb) { |
742 | if (ended) return cb(ended) |
743 | read(abort, function (end, el) { |
744 | if (end === true) { |
745 | ended = true |
746 | cb(null, '(' + count + ')') |
747 | } else { |
748 | count++ |
749 | cb(end, u.toHTML(el) + ' ') |
750 | } |
751 | }) |
752 | } |
753 | } |
754 | ) |
755 | } |
756 | |
757 | Render.prototype.textEditDiffTable = function (oldMsg, newMsg) { |
758 | var oldC = oldMsg && oldMsg.value.content || {} |
759 | var newC = newMsg && newMsg.value.content || {} |
760 | var oldText = String(oldC.text || oldC.description || '') |
761 | var newText = String(newC.text || newC.description || '') |
762 | var diff = Diff.structuredPatch('', '', oldText, newText) |
763 | var self = this |
764 | // note: this structure is duplicated in lib/render.js |
765 | return h('table', {class: 'diff-table'}, [ |
766 | diff.hunks.map(function (hunk) { |
767 | var oldLine = hunk.oldStart |
768 | var newLine = hunk.newStart |
769 | return [ |
770 | h('tr', [ |
771 | h('td', {colspan: 2}), |
772 | h('td', {colspan: 2}, h('pre', |
773 | '@@ -' + oldLine + ',' + hunk.oldLines + ' ' + |
774 | '+' + newLine + ',' + hunk.newLines + ' @@')) |
775 | ]), |
776 | hunk.lines.map(function (line) { |
777 | var s = line[0] |
778 | if (s == '\\') return |
779 | var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++] |
780 | return [ |
781 | h('tr', { |
782 | class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : '' |
783 | }, [ |
784 | lineNums.map(function (num, i) { |
785 | return h('td', String(num)) |
786 | }), |
787 | h('td', {class: 'diff-sigil'}, h('code', s)), |
788 | h('td', {class: 'diff-line'}, {innerHTML: |
789 | u.unwrapP(self.markdown(line.substr(1), |
790 | s == '-' ? oldC.mentions : newC.mentions)) |
791 | }) |
792 | ]) |
793 | ] |
794 | }) |
795 | ] |
796 | }) |
797 | ]) |
798 | } |
799 | |
800 |
Built with git-ssb-web