git ssb

9+

cel / ssb-viewer



Tree: 4f7d162bf1625d6dc4e8e518f55ee5d842d12bc2

Files: 4f7d162bf1625d6dc4e8e518f55ee5d842d12bc2 / render.js

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

Built with git-ssb-web