git ssb

0+

Daan Patchwork / ssb-viewer



forked from cel / ssb-viewer

Tree: b21b4ce09db70de58fef3c10705ba430079c9b26

Files: b21b4ce09db70de58fef3c10705ba430079c9b26 / render.js

15415 bytesRaw
1var path = require('path');
2var pull = require("pull-stream");
3var marked = require("ssb-marked");
4var htime = require("human-time");
5var emojis = require("emoji-named-characters");
6var cat = require("pull-cat");
7
8var emojiDir = path.join(require.resolve("emoji-named-characters"), "../pngs");
9
10exports.wrapPage = wrapPage;
11exports.MdRenderer = MdRenderer;
12exports.renderEmoji = renderEmoji;
13exports.formatMsgs = formatMsgs;
14exports.renderThread = renderThread;
15exports.renderAbout = renderAbout;
16exports.renderItem = renderItem;
17exports.wrapRss = wrapRss;
18exports.renderRssContent = renderRssContent;
19
20function MdRenderer(opts) {
21 marked.Renderer.call(this, {});
22 this.opts = opts;
23}
24
25MdRenderer.prototype = new marked.Renderer();
26
27MdRenderer.prototype.urltransform = function(href) {
28 if (!href) return false;
29 switch (href[0]) {
30 case "#":
31 return this.opts.base + "channel/" + href.slice(1);
32 case "%":
33 return this.opts.msg_base + encodeURIComponent(href);
34 case "@":
35 return this.opts.feed_base + encodeURIComponent(href);
36 case "&":
37 return this.opts.blob_base + encodeURIComponent(href);
38 }
39 if (href.indexOf("javascript:") === 0) return false;
40 return href;
41};
42
43MdRenderer.prototype.image = function(href, title, text) {
44 return (
45 '<img src="' +
46 this.opts.img_base +
47 escape(href) +
48 '"' +
49 ' alt="' +
50 text +
51 '"' +
52 (title ? ' title="' + title + '"' : "") +
53 (this.options.xhtml ? "/>" : ">")
54 );
55};
56
57function renderEmoji(emoji) {
58 var opts = this.renderer.opts;
59 return emoji in emojis
60 ? '<img src="' +
61 opts.emoji_base +
62 escape(emoji) +
63 '.png"' +
64 ' alt=":' +
65 escape(emoji) +
66 ':"' +
67 ' title=":' +
68 escape(emoji) +
69 ':"' +
70 ' class="ssb-emoji" height="16" width="16">'
71 : ":" + emoji + ":";
72}
73
74function escape(str) {
75 return String(str)
76 .replace(/&/g, "&amp;")
77 .replace(/</g, "&lt;")
78 .replace(/>/g, "&gt;")
79 .replace(/"/g, "&quot;");
80}
81
82function formatMsgs(id, ext, opts) {
83 switch (ext || "html") {
84 case "html":
85 return pull(renderThread(opts), wrapPage(id));
86 case "js":
87 return pull(renderThread(opts), wrapJSEmbed(opts));
88 case "json":
89 return wrapJSON();
90 case "rss":
91 return pull(renderItem(opts), wrapRss(id, opts));
92 default:
93 return null;
94 }
95}
96
97function wrap(before, after) {
98 return function(read) {
99 return cat([pull.once(before), read, pull.once(after)]);
100 };
101}
102
103function renderAbout(opts, about, showAllHTML = "") {
104 return pull(
105 pull.map(renderMsg.bind(this, opts)),
106 wrap(
107 '<span class="top-tip">You are reading content from ' +
108 '<a href="https://www.scuttlebutt.nz">Scuttlebutt</a>' +
109 '</span>' +
110 '<main>' +
111 '<article><header><figure>' +
112 '<img src="' + opts.img_base + escape(about.image) + '" ' +
113 'height="200" width="200"><figcaption>' +
114 'Feed of ' + about.name + '<br/>' +
115 (about.description != undefined ?
116 marked(about.description, opts.marked) : '') +
117 '</figcaption></figure></header></article>',
118
119 showAllHTML + '</main>' +
120 '<a class="call-to-action" href="https://www.scuttlebutt.nz">' +
121 'Join Scuttlebutt now' +
122 '</a>'
123 )
124 );
125}
126
127function renderThread(opts, showAllHTML = "") {
128 return pull(
129 pull.map(renderMsg.bind(this, opts)),
130 wrap(
131 '<span class="top-tip">You are reading content from ' +
132 '<a href="https://www.scuttlebutt.nz">Scuttlebutt</a>' +
133 '</span>' +
134 '<main>',
135
136 showAllHTML + '</main>' +
137 '<a class="call-to-action" href="https://www.scuttlebutt.nz">' +
138 'Join Scuttlebutt now' +
139 '</a>'
140 )
141 );
142}
143
144function renderItem(opts) {
145 return pull(
146 pull.map(renderRss.bind(this, opts))
147 );
148}
149
150function wrapPage(id) {
151 return wrap(
152 "<!doctype html><html><head>" +
153 "<meta charset=utf-8>" +
154 "<title>" +
155 id + " | ssb-viewer" +
156 "</title>" +
157 '<meta name=viewport content="width=device-width,initial-scale=1">' +
158 styles +
159 "</head><body>",
160 "</body></html>"
161 );
162}
163
164function wrapRss(id, opts) {
165 return wrap(
166 '<?xml version="1.0" encoding="UTF-8" ?>' +
167 '<rss version="2.0">' +
168 '<channel>' +
169 '<title>' + id + ' | ssb-viewer</title>',
170
171 '</channel>'+
172 '</rss>'
173 );
174}
175
176var styles = `
177 <style>
178 html { background-color: #f1f3f5; }
179 body {
180 color: #212529;
181 font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif;
182 -webkit-font-smoothing: antialiased;
183 -moz-osx-font-smoothing: grayscale;
184 letter-spacing: 0.02em;
185 padding-top: 30px;
186 padding-bottom: 50px;
187 }
188 a { color: #364fc7; }
189
190 .top-tip, .top-tip a {
191 color: #868e96;
192 }
193 .top-tip {
194 text-align: center;
195 display: block;
196 margin-bottom: 10px;
197 font-size: 14px;
198 }
199 main { margin: 0 auto; max-width: 40rem; }
200 main article:first-child { border-radius: 3px 3px 0 0; }
201 main article:last-child { border-radius: 0 0 3px 3px; }
202 article {
203 background-color: white;
204 padding: 20px;
205 box-shadow: 0 1px 3px #949494;
206 position: relative;
207 }
208 .top-right { position: absolute; top: 20px; right: 20px; }
209 article > header { margin-bottom: 20px; }
210 article > header > figure {
211 margin: 0; display: flex;
212 }
213 article > header > figure > img {
214 border-radius: 2px; margin-right: 10px;
215 }
216 article > header > figure > figcaption {
217 display: flex; flex-direction: column; justify-content: space-around;
218 }
219 .ssb-avatar-name { font-size: 1.2em; font-weight: bold; }
220 time a { color: #868e96; }
221 .ssb-avatar-name, time a {
222 text-decoration: none;
223 }
224 .ssb-avatar-name:hover, time:hover a {
225 text-decoration: underline;
226 }
227 section p { line-height: 1.45em; }
228 section p img {
229 max-width: 100%;
230 max-height: 50vh;
231 margin: 0 auto;
232 }
233 .status {
234 font-style: italic;
235 }
236
237 code {
238 display: inline;
239 padding: 2px 5px;
240 font-weight: 600;
241 background-color: #e9ecef;
242 border-radius: 3px;
243 color: #495057;
244 }
245 blockquote {
246 padding-left: 1.2em;
247 margin: 0;
248 color: #868e96;
249 border-left: 5px solid #ced4da;
250 }
251 pre {
252 background-color: #212529;
253 color: #ced4da;
254 font-weight: bold;
255 padding: 5px;
256 border-radius: 3px;
257 position: relative;
258 }
259 pre::before {
260 content: "METADATA";
261 position: absolute;
262 top: -7px;
263 left: 0px;
264 background-color: #212529;
265 padding: 2px 4px 0;
266 border-radius: 2px;
267 font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif;
268 font-size: 9px;
269 }
270 .call-to-action {
271 display: block;
272 margin: 0 auto;
273 width: 13em;
274 text-align: center;
275 text-decoration: none;
276 margin-top: 20px;
277 margin-bottom: 60px;
278 background-color: #5c7cfa;
279 padding: 15px 0;
280 color: #edf2ff;
281 border-radius: 3px;
282 border-bottom: 3px solid #3b5bdb;
283 }
284 .call-to-action:hover {
285 background-color: #748ffc;
286 border-bottom: 3px solid #4c6ef5;
287 }
288 </style>
289`;
290
291function wrapJSON() {
292 var first = true;
293 return pull(pull.map(JSON.stringify), join(","), wrap("[", "]"));
294}
295
296function wrapJSEmbed(opts) {
297 return pull(
298 wrap('<link rel=stylesheet href="' + opts.base + 'static/base.css">', ""),
299 pull.map(docWrite),
300 opts.base_token && rewriteBase(new RegExp(opts.base_token, "g"))
301 );
302}
303
304function rewriteBase(token) {
305 // detect the origin of the script and rewrite the js/html to use it
306 return pull(
307 replace(token, '" + SSB_VIEWER_ORIGIN + "/'),
308 wrap(
309 "var SSB_VIEWER_ORIGIN = (function () {" +
310 'var scripts = document.getElementsByTagName("script")\n' +
311 "var script = scripts[scripts.length-1]\n" +
312 "if (!script) return location.origin\n" +
313 'return script.src.replace(/\\/%.*$/, "")\n' +
314 "}())\n",
315 ""
316 )
317 );
318}
319
320function join(delim) {
321 var first = true;
322 return pull.map(function(val) {
323 if (!first) return delim + String(val);
324 first = false;
325 return val;
326 });
327}
328
329function replace(re, rep) {
330 return pull.map(function(val) {
331 return String(val).replace(re, rep);
332 });
333}
334
335function docWrite(str) {
336 return "document.write(" + JSON.stringify(str) + ")\n";
337}
338
339function renderMsg(opts, msg) {
340 var c = msg.value.content || {};
341 var name = encodeURIComponent(msg.key);
342 return (
343 '<article id="' +
344 name +
345 '">' +
346 '<header>' +
347 '<figure>' +
348 '<img alt="" ' +
349 'src="' + opts.img_base + escape(msg.author.image) + '" ' +
350 'height="50" width="50">' +
351 '<figcaption>' +
352 '<a class="ssb-avatar-name"' +
353 ' href="' + opts.base +
354 escape(msg.value.author) +
355 '"' +
356 ">" + msg.author.name + "</a>" +
357 msgTimestamp(msg, name) +
358 '</figcaption>' +
359 '</figure>' +
360 '</header>' +
361 render(opts, c) +
362 "</article>"
363 );
364}
365
366function renderRssContent(opts, c) {
367 var base = opts.base;
368 if (c.type === "post") {
369 return (c.channel ? `Posted in #${c.channel}:\n\n` : '') + c.text;
370 } else if (c.type == "vote" && c.vote.expression == "Dig") {
371 var linkedText = "a post";
372 if (typeof c.vote.linkedText != "undefined") {
373 linkedText = c.vote.linkedText.substring(0, 75);
374 }
375
376 return `Liked ${linkedText}`;
377 } else if (c.type == "vote") {
378 var linkedText = "a post";
379 if (typeof c.vote.linkedText != "undefined") {
380 linkedText = c.vote.linkedText.substring(0, 75);
381 }
382
383 return `Voted ${linkedText}`;
384 } else if (c.type == "contact" && c.following) {
385 var name = c.contact;
386 if (typeof c.contactAbout != "undefined") {
387 name = c.contactAbout.name;
388 }
389
390 return `Followed ${name}`;
391 } else if (c.type == "contact" && !c.following) {
392 var name = c.contact;
393 if (typeof c.contactAbout != "undefined") {
394 name = c.contactAbout.name;
395 }
396
397 return `Unfollowed ${name}`;
398 } else if (typeof c == "string") {
399 // Don't show private messages in RSS.
400 }
401 else if (c.type == "about") {
402 // Don't show about messages in RSS.
403 }
404 else if (c.type == "issue") {
405 return `Created a git issue ${(c.repoName != undefined ? " in repo " + c.repoName : "")}`;
406 }
407 else if (c.type == "git-update") {
408 return `Did a git update ${(c.repoName != undefined ? " in repo " + c.repoName : "")}
409
410 ${(c.commits != undefined ? c.commits.map(com => { return "-" +com.title; }).join("\n") : "")}
411 `;
412 }
413 else if (c.type == "ssb-dns") {
414 // Don't show for RSS.
415 }
416 else if (c.type == "pub") {
417 return `Connected to the pub ${c.address.host}`;
418 }
419 else if (c.type == "channel" && c.subscribed) {
420 return `Subscribed to channel #${c.channel}`;
421 }
422 else if (c.type == "channel" && !c.subscribed) {
423 return `Unsubscribed from channel #${c.channel}`;
424 }
425
426}
427
428function renderRss(opts, msg) {
429 var c = msg.value.content || {};
430 var name = encodeURIComponent(msg.key);
431
432 let content = renderRssContent(opts, c);
433
434 if (!content) {
435 return null;
436 }
437
438 return (
439 '<item>' +
440 '<title>' + msg.author.name + '</title>' +
441 '<description><![CDATA[' + content + ']]></description>' +
442 '<link>' + opts.base + escape(name) + '</link>' +
443 '<pubDate>' + new Date(msg.value.timestamp).toUTCString() + '</pubDate>' +
444 '<guid>' + msg.key + '</guid>' +
445 '</item>'
446 );
447}
448
449function msgTimestamp(msg, name) {
450 var date = new Date(msg.value.timestamp);
451 var isoStr = date.toISOString();
452 return (
453 '<time class="ssb-timestamp" datetime="' + isoStr + '">' +
454 '<a ' +
455 'href="#' + name + '" ' +
456 'title="' + isoStr + '" ' +
457 '>' + formatDate(date) + '</a>' +
458 '</time>'
459 );
460}
461
462function formatDate(date) {
463 // return date.toISOString().replace('T', ' ')
464 return htime(date);
465}
466
467function render(opts, c) {
468 var base = opts.base;
469 if (c.type === "post") {
470 var channel = c.channel
471 ? '<div class="top-right"><a href="' + base + 'channel/' + c.channel + '">#' + c.channel + "</a></div>"
472 : "";
473 return channel + renderPost(opts, c);
474 } else if (c.type == "vote" && c.vote.expression == "Dig") {
475 var channel = c.channel
476 ? ' in <a href="' + base + 'channel/' + c.channel + '">#' + c.channel + "</a>"
477 : "";
478 var linkedText = "this";
479 if (typeof c.vote.linkedText != "undefined")
480 linkedText = c.vote.linkedText.substring(0, 75);
481 return ('<span class="status">' +
482 'Liked ' +
483 '<a href="' + base +
484 c.vote.link +
485 '">' +
486 linkedText +
487 "</a>" +
488 channel +
489 '</span>'
490 );
491 } else if (c.type == "vote") {
492 var linkedText = "this";
493 if (typeof c.vote.linkedText != "undefined")
494 linkedText = c.vote.linkedText.substring(0, 75);
495 return '<span class="status">' +
496 'Voted <a href="' + base + c.vote.link + '">' + linkedText + "</a>" +
497 '</span>';
498 } else if (c.type == "contact" && c.following) {
499 var name = c.contact;
500 if (typeof c.contactAbout != "undefined") name = c.contactAbout.name;
501 return '<span class="status">' +
502 'Followed <a href="' + base + c.contact + '">' + name + "</a>" +
503 '</span>';
504 } else if (c.type == "contact" && !c.following) {
505 var name = c.contact;
506 if (typeof c.contactAbout != "undefined") name = c.contactAbout.name;
507 return '<span class="status">' +
508 'Unfollowed <a href="' + base + c.contact + '">' + name + "</a>" +
509 '</span>';
510 } else if (typeof c == "string") {
511 return '<span class="status">' +
512 "Wrote something private" +
513 '</span>';
514 }
515 else if (c.type == "about") {
516 return '<span class="status">' +
517 "Changed something in about" +
518 '</span>' +
519 renderDefault(c);
520 }
521 else if (c.type == "issue") {
522 return '<span class="status">' +
523 "Created a git issue" + (c.repoName != undefined ? " in repo " + c.repoName : "") + renderPost(opts, c) +
524 '</span>';
525 }
526 else if (c.type == "git-update") {
527 return '<span class="status">' +
528 "Did a git update " + (c.repoName != undefined ? " in repo " + c.repoName : "") +
529 "<br/>" +
530 (c.commits != undefined ? c.commits.map(com => { return "-" +com.title; }).join("<br/>") : "") +
531 '</span>'
532 }
533 else if (c.type == "ssb-dns") {
534 return '<span class="status">' +
535 "Updated DNS" +
536 '</span>' +
537 renderDefault(c);
538 }
539 else if (c.type == "pub") {
540 return '<span class="status">' +
541 "Connected to the pub " + c.address.host +
542 '</span>'
543 }
544 else if (c.type == "channel" && c.subscribed)
545 return '<span class="status">' +
546 'Subscribed to channel <a href="' + base + 'channel/' +
547 c.channel +
548 '">#' +
549 c.channel +
550 "</a>" +
551 '</span>';
552 else if (c.type == "channel" && !c.subscribed)
553 return '<span class="status">' +
554 'Unsubscribed from channel <a href="' + base + 'channel/' +
555 c.channel +
556 '">#' +
557 c.channel +
558 "</a>" +
559 '</span>';
560 else return renderDefault(c);
561}
562
563function renderPost(opts, c) {
564 return '<section>' + marked(String(c.text), opts.marked) + "</section>";
565}
566
567function renderDefault(c) {
568 return "<pre>" + JSON.stringify(c, 0, 2) + "</pre>";
569}
570

Built with git-ssb-web