render.jsView |
---|
| 1 … | +var path = require('path'); |
| 2 … | +var pull = require("pull-stream"); |
| 3 … | +var marked = require("ssb-marked"); |
| 4 … | +var htime = require("human-time"); |
| 5 … | +var emojis = require("emoji-named-characters"); |
| 6 … | +var cat = require("pull-cat"); |
| 7 … | + |
| 8 … | +var emojiDir = path.join(require.resolve("emoji-named-characters"), "../pngs"); |
| 9 … | + |
| 10 … | +exports.wrapPage = wrapPage; |
| 11 … | +exports.MdRenderer = MdRenderer; |
| 12 … | +exports.renderEmoji = renderEmoji; |
| 13 … | +exports.formatMsgs = formatMsgs; |
| 14 … | +exports.renderThread = renderThread; |
| 15 … | + |
| 16 … | +function MdRenderer(opts) { |
| 17 … | + marked.Renderer.call(this, {}); |
| 18 … | + this.opts = opts; |
| 19 … | +} |
| 20 … | + |
| 21 … | +MdRenderer.prototype = new marked.Renderer(); |
| 22 … | + |
| 23 … | +MdRenderer.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 … | + |
| 39 … | +MdRenderer.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 … | + |
| 53 … | +function 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 … | + |
| 70 … | +function escape(str) { |
| 71 … | + return String(str) |
| 72 … | + .replace(/&/g, "&") |
| 73 … | + .replace(/</g, "<") |
| 74 … | + .replace(/>/g, ">") |
| 75 … | + .replace(/"/g, """); |
| 76 … | +} |
| 77 … | + |
| 78 … | +function 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 … | + |
| 91 … | +function wrap(before, after) { |
| 92 … | + return function(read) { |
| 93 … | + return cat([pull.once(before), read, pull.once(after)]); |
| 94 … | + }; |
| 95 … | +} |
| 96 … | + |
| 97 … | +function 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 … | + |
| 114 … | +function 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 … | + |
| 128 … | +var 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 … | + |
| 243 … | +function wrapJSON() { |
| 244 … | + var first = true; |
| 245 … | + return pull(pull.map(JSON.stringify), join(","), wrap("[", "]")); |
| 246 … | +} |
| 247 … | + |
| 248 … | +function 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 … | + |
| 256 … | +function rewriteBase(token) { |
| 257 … | + |
| 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 … | + |
| 272 … | +function 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 … | + |
| 281 … | +function replace(re, rep) { |
| 282 … | + return pull.map(function(val) { |
| 283 … | + return String(val).replace(re, rep); |
| 284 … | + }); |
| 285 … | +} |
| 286 … | + |
| 287 … | +function docWrite(str) { |
| 288 … | + return "document.write(" + JSON.stringify(str) + ")\n"; |
| 289 … | +} |
| 290 … | + |
| 291 … | +function 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 … | + |
| 318 … | +function 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 … | + |
| 331 … | +function formatDate(date) { |
| 332 … | + |
| 333 … | + return htime(date); |
| 334 … | +} |
| 335 … | + |
| 336 … | +function 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 … | + |
| 433 … | +function renderPost(opts, c) { |
| 434 … | + return '<section>' + marked(c.text, opts.marked) + "</section>"; |
| 435 … | +} |
| 436 … | + |
| 437 … | +function renderDefault(c) { |
| 438 … | + return "<pre>" + JSON.stringify(c, 0, 2) + "</pre>"; |
| 439 … | +} |