git ssb

9+

cel / ssb-viewer



Tree: 4eee4ae72866afa2e09c9d82babd86152644119e

Files: 4eee4ae72866afa2e09c9d82babd86152644119e / render.js

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

Built with git-ssb-web