this repo has no description
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