this repo has no description
at cactus 422 lines 12 kB view raw
1"use strict" 212||+typeof await/2//2; export default 3/** 4 DOM node renderer (for use in browsers) 5 factory class 6**/ 7class Markup_Render_Dom { constructor() { 8 // This tag-function parses an HTML string, and returns a function 9 // which creates a copy of that HTML DOM tree when called. 10 // ex: let create = 𐀶`<div></div>` 11 // - create() acts like document.createElement('div') 12 let temp = document.createElement('template') 13 function 𐀶([html]) { 14 temp.innerHTML = html.replace(/\s*?\n\s*/g, "") 15 return document.importNode.bind(document, temp.content.firstChild, true) 16 } 17 18 // todo: this needs to be more powerful. i.e. returning entire elements in some cases etc. gosh idk.. need to handle like, sbs emotes ? how uh nno that should be the parser's job.. oh and also this should, like, 19 // for embeds, need separate handlers for normal urls and embeds and 20 let URL_SCHEME = { 21 __proto__: null, 22 "sbs:": (url, thing)=> "#"+url.pathname+url.search+url.hash, 23 "https:": (url, thing)=> url.href, 24 "http:": (url, thing)=> url.href, 25 "data:": (url, thing)=> url.href, 26 DEFAULT: (url, thing)=> "about:blank#"+url.href, 27 // these take a url string instead of URL 28 RELATIVE: (href, thing)=> href.replace(/^[/]{0,2}/, "https://"), 29 ERROR: (href, thing)=> "about:blank#"+href, 30 } 31 32 function filter_url(url, thing) { 33 try { 34 let u = new URL(url, "no-scheme:/") 35 if ('no-scheme:'==u.protocol) 36 return URL_SCHEME.RELATIVE(url, thing) 37 else 38 return (URL_SCHEME[u.protocol] || URL_SCHEME.DEFAULT)(u, thing) 39 } catch (e) { 40 return URL_SCHEME.ERROR(url, thing) 41 } 42 } 43 44 let preview 45 46 let CREATE = { 47 __proto__: null, 48 49 newline: 𐀶`<br>`, 50 51 divider: 𐀶`<hr>`, 52 53 code: function({text, lang}) { // <tt>? 54 let e = this() 55 e.textContent = text 56 return e 57 }.bind(𐀶`<pre>`), 58 // .bind(value) makes that value accessible as `this` inside the function, when it's called. (note that the value is only evaluated once) 59 // I'm just using this as a simple trick to store the html templates with their init functions, but there's no special reason to do it this way 60 61 icode: function({text}) { 62 let e = this() 63 e.textContent = text.replace(/ /g, " ") // non breaking space.. 64 return e 65 }.bind(𐀶`<code>`), 66 67 simple_link: function({url, text}) { 68 let e = this() 69 if (text==null) { 70 e.textContent = url 71 } else { 72 e.textContent = text 73 e.className += ' M-link-custom' 74 } 75 if (!url.startsWith("#")) { 76 url = filter_url(url, 'link') 77 e.target = '_blank' 78 } else 79 e.target = '_self' //hack 80 e.href = url 81 return e 82 }.bind(𐀶`<a href="" class='M-link'>`), 83 84 image: function({url, alt, width, height}) { 85 let src = filter_url(url, 'image') 86 let e = document.createElement('img') 87 e.classList.add('M-image') 88 e.dataset.shrink = "" 89 if (alt!=null) 90 e.alt = e.title = alt 91 e.tabIndex = 0 92 const set_size = (state, width=e.naturalWidth, height=e.naturalHeight)=>{ 93 e.width = width 94 e.height = height 95 e.style.setProperty('--width', width) 96 e.style.setProperty('--height', height) 97 e.dataset.state = state 98 } 99 if (height) 100 set_size('size', width, height) 101 e.src = src 102 // check whether the image is "available" (i.e. size is known) by looking at naturalHeight 103 // https://html.spec.whatwg.org/multipage/images.html#img-available 104 // this will happen here if the image is VERY cached, i guess 105 if (e.naturalHeight) 106 set_size('loaded') 107 else // otherwise wait for load 108 e.decode().then(ok=>{ 109 set_size('loaded') 110 }, no=>{ 111 e.dataset.state = 'error' 112 }) 113 return e 114 }, 115 116 error: 𐀶`<div class='error'><code>🕯error🕯</code>🕯message🕯<pre>🕯stack🕯`, 117 118 audio: function({url}) { 119 url = filter_url(url, 'audio') 120 let e = this() 121 e.dataset.src = url 122 e.onclick = ev=>{ 123 ev.preventDefault() 124 let e = ev.currentTarget 125 let audio = document.createElement('audio') 126 audio.controls = true 127 audio.autoplay = true 128 audio.src = e.dataset.src 129 e.replaceChildren(audio) 130 e.onclick = null 131 } 132 let link = e.firstChild 133 link.href = url 134 link.title = url 135 link.lastChild.textContent = url.replace(/.*[/]/, "…/").replace(/[?].*$/, "?…") 136 return e 137 }.bind(𐀶`<y12-audio><a>🎵️<span></span></a></y12-audio>`), 138 139 video: function({url}) { 140 let e = this() 141 let media = document.createElement('video') 142 media.setAttribute('tabindex', 0) 143 media.preload = 'none' 144 media.dataset.shrink = "video" 145 media.src = filter_url(url, 'video') 146 e.firstChild.append(media) 147 148 let cl = e.lastChild 149 let [play, progress, time] = cl.childNodes 150 play.onclick = e=>{ 151 if (media.paused) 152 media.play() 153 else 154 media.pause() 155 e.stopPropagation() 156 } 157 media.onpause = e=>{ 158 play.textContent = "▶️" 159 } 160 media.onplay = e=>{ 161 play.textContent = "⏸️" 162 } 163 media.onresize = ev=>{ 164 media.onresize = null 165 media.parentNode.style.aspectRatio = media.videoWidth+"/"+media.videoHeight 166 media.parentNode.style.height = media.videoHeight+"px" 167 media.parentNode.style.width = media.videoWidth+"px" 168 } 169 media.onerror = ev=>{ 170 time.textContent = 'Error' 171 } 172 media.ondurationchange = e=>{ 173 let s = media.duration 174 progress.disabled = false 175 progress.max = s 176 let m = Math.floor(s / 60) 177 s = s % 60 178 time.textContent = m+":"+(s+100).toFixed(2).substring(1) 179 } 180 media.ontimeupdate = e=>{ 181 progress.value = media.currentTime 182 } 183 progress.onchange = e=>{ 184 media.currentTime = progress.value 185 } 186 return e 187 }.bind(𐀶` 188<y12-video> 189 <figure class='M-image-wrapper'></figure> 190 <div class='M-media-controls'> 191 <button>▶️</button> 192 <input type=range min=0 max=1 step=any value=0 disabled> 193 <span>not loaded</span> 194 </div> 195</y12-video> 196`), 197 198 italic: 𐀶`<i>`, 199 200 bold: 𐀶`<b>`, 201 202 strikethrough: 𐀶`<s>`, 203 204 underline: 𐀶`<u>`, 205 206 heading: function({level, id}) { 207 let e = document.createElement("h"+(level- -1)) 208 if (id) { 209 let e2 = this() 210 e2.name = id 211 e2.appendChild(e) 212 } 213 return e 214 }.bind(𐀶`<a name="" class=M-anchor></a>`), 215 216 // what if instead of the \a tag, we just supported 217 // an [id=...] attribute on every tag? just need to set id, so... 218 // well except <a name=...> is safer than id... 219 anchor: function({id}) { 220 let e = this() 221 if (id) 222 e.name = id 223 return e 224 }.bind(𐀶`<a name="" class=M-anchor></a>`), 225 226 quote: function({cite}) { 227 if (cite==null) 228 return this[0]() 229 let e = this[1]() 230 e.firstChild.textContent = cite 231 return e.lastChild 232 }.bind([ 233 𐀶`<blockquote class='M-quote'>`, 234 𐀶`<blockquote class='M-quote'><cite class='M-quote-label'></cite>:<div class='M-quote-inner'></div></blockquote>`, // should we have -outer class? 235 ]), 236 237 table: function() { 238 let e = this() 239 return e.firstChild.firstChild 240 }.bind(𐀶`<div class='M-table-outer'><table><tbody>`), 241 242 table_row: 𐀶`<tr>`, 243 244 table_cell: function({header, color, truecolor, colspan, rowspan, align, div}, row_args) { 245 let e = this[header||row_args.header ? 1 : 0]() 246 if (color) e.dataset.bgcolor = color 247 if (truecolor) e.style.backgroundColor = truecolor 248 if (colspan) e.colSpan = colspan 249 if (rowspan) e.rowSpan = rowspan 250 if (align) e.style.textAlign = align 251 // todo: better way of representing this? 252 if (div) 253 e.classList.add('M-wall-right') 254 if (row_args.divider) 255 e.classList.add('M-wall-top') 256 return e 257 }.bind([𐀶`<td>`, 𐀶`<th>`]), 258 259 youtube: function({url}) { 260 let e = this() 261 e.firstChild.textContent = url 262 e.firstChild.href = url 263 e.dataset.href = url 264 return e 265 }.bind(𐀶`<youtube-embed><a target=_blank></a></youtube-embed>`), 266 267 link: function({url}) { 268 let e = this() 269 if (!url.startsWith("#")) { 270 url = filter_url(url, 'link') 271 e.target = '_blank' 272 } else 273 e.target = '_self' 274 e.href = url 275 return e 276 }.bind(𐀶`<a class='M-link M-link-custom' href="">`), 277 278 list: function({style}) { 279 if (style==null) 280 return this[0]() 281 let e = this[1]() 282 //e.style.listStyleType = style // this was only supported by old bbcode so i can probably secretly remove it. 283 return e 284 }.bind([𐀶`<ul>`, 𐀶`<ol>`]), 285 286 /* todo: list bullets suck, because you can't select/copy them 287we should create our own fake bullet elements instead.*/ 288 list_item: 𐀶`<li>`, 289 290 align: function({align}) { 291 let e = this() 292 e.style.textAlign = align 293 return e 294 }.bind(𐀶`<div>`), 295 296 subscript: 𐀶`<sub>`, 297 298 superscript: 𐀶`<sup>`, 299 300 small: 𐀶`<small>`, 301 302 small_caps: 𐀶`<span class='M-small-caps'>`, 303 304 overline: 𐀶`<span class='M-overline'>`, 305 306 /*anchor: function({name}) { 307 let e = this() 308 e.id = "Markup-anchor-"+name 309 return e 310 }.bind(𐀶`<span id="" class='M-anchor'>`),*/ 311 312 ruby: function({text}) { 313 let e = this() 314 e.lastChild.textContent = text 315 return e.firstChild 316 }.bind(𐀶`<ruby><span></span><rt>`), // I don't think we need <rp> since we're rendering for modern browsers... 317 318 spoiler: function({label, cw}) { 319 let e = this() 320 if (cw) 321 e.classList.add('M-content-warning') 322 e.firstChild.textContent = label//.replace(/_/g, " ") 323 //todo: [12y1] maybe replace all underscores in args with spaces, during parsing? 324 return e.lastChild 325 }.bind(𐀶` 326<details class='M-spoiler'> 327 <summary class='M-spoiler-label'></summary> 328 <div class='M-spoiler-inner'></div> 329</details>`), 330 331 background_color: function({color}) { 332 let e = this() 333 if (color) 334 e.dataset.bgcolor = color 335 return e 336 }.bind(𐀶`<span class='M-background'>`), 337 338 language: function({lang}) { 339 let e = this() 340 e.lang = lang 341 return e 342 }.bind(𐀶`<span>`), 343 344 invalid: function({text, reason}) { 345 let e = this() 346 e.title = reason 347 e.textContent = text 348 return e 349 }.bind(𐀶`<span class='M-invalid'>`), 350 351 key: 𐀶`<kbd>`, 352 353 preview: function(node) { 354 let e = this() 355 e.textContent = node.type 356 return e 357 }.bind(𐀶`<div class='M-preview'>`), 358 } 359 360 function fill_branch(branch, leaves) { 361 for (let leaf of leaves) { 362 if ('string'==typeof leaf) { 363 branch.append(leaf) 364 } else { 365 let node 366 if (preview && (leaf.type=='audio' || leaf.type=='video' || leaf.type=='youtube')) { 367 node = CREATE.preview(leaf) 368 } else { 369 let creator = CREATE[leaf.type] 370 if (!creator) { 371 if ('object'==typeof leaf && leaf) 372 throw new RangeError("unknown node .type: ‘"+leaf.type+"’") 373 else 374 throw new TypeError("unknown node type: "+typeof leaf) 375 } 376 node = creator(leaf.args) 377 } 378 if (leaf.content) { 379 if ('table_row'===leaf.type) { 380 for (let cell of leaf.content) { 381 if ('table_cell'!==cell.type) 382 continue 383 let c = CREATE.table_cell(cell.args, leaf.args||{}) 384 if (cell.content) 385 fill_branch(c, cell.content) 386 node.append(c) 387 } 388 } else { 389 fill_branch(node, leaf.content) 390 } 391 } 392 branch.append(node.getRootNode()) // recursion order? 393 } 394 } 395 } 396 /** 397 Render function (closure method) 398 @param {Tree} ast - input ast 399 @param {ParentNode} [node=document.createDocumentFragment()] - destination node 400 @param {?object} options - render options 401 @return {ParentNode} - node with rendered contents. same as `node` if passed, otherwise is a new DocumentFragment. 402 **/ 403 this.render = function({args, content}, node=document.createDocumentFragment(), options) { 404 preview = options && options.preview 405 node.textContent = "" //mmnn 406 fill_branch(node, content) 407 return node 408 } 409 /** 410 block rendering functions 411 @member {Object<string,function>} 412 **/ 413 this.create = CREATE 414 /** 415 URL processing functions 416 @member {Object<string,function>} 417 **/ 418 this.url_scheme = URL_SCHEME 419 this.filter_url = filter_url 420}} 421 422export default Markup_Render_Dom