git ssb

0+

Daan Patchwork / ssb-viewer



forked from cel / ssb-viewer

Tree: eb408e8545d033be2ffe5456e27eb7972717e2d4

Files: eb408e8545d033be2ffe5456e27eb7972717e2d4 / render.js

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

Built with git-ssb-web