tangled
alpha
login
or
join now
ansxor.ca
/
markup2
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
1
pulls
pipelines
better video/audio players, i hope
12Me21
3 years ago
c717ec6b
41cd015d
+114
-138
4 changed files
expand all
collapse all
unified
split
markup.css
parse.js
render.js
runtime.js
+32
-3
markup.css
···
297
/** Media **/
298
/***********/
299
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
300
.M-image-wrapper {
301
aspect-ratio: 16/9;
302
contain: strict;
···
315
background: black;
316
}
317
318
-
.Markup media-player {
0
0
0
0
0
0
319
display: flex;
320
flex-flow: column;
321
max-width: 100%;
···
328
padding-right: 3px;
329
}
330
331
-
.M-audio-player, .M-video-player > .M-media-controls {
332
border: 2px solid var(--T-border-color);
333
background: slategray;
334
border-radius: 0 3px 3px 3px;
···
336
width: max-content;
337
}
338
339
-
.Markup media-player > * {
340
flex: none;
341
}
342
···
297
/** Media **/
298
/***********/
299
300
+
y12-audio {
301
+
display: contents;
302
+
}
303
+
y12-audio > audio {
304
+
display: block;
305
+
width: 100%;
306
+
}
307
+
y12-audio > a {
308
+
display: flex;
309
+
width: 100%;
310
+
align-items: center;
311
+
padding: 3px 0.5rem;
312
+
box-sizing: border-box;
313
+
height: 40px;
314
+
background: #555;
315
+
color: silver;
316
+
line-break: anywhere;
317
+
text-decoration: none;
318
+
}
319
+
y12-audio > a > span {
320
+
padding-left: 0.25rem;
321
+
}
322
+
323
.M-image-wrapper {
324
aspect-ratio: 16/9;
325
contain: strict;
···
338
background: black;
339
}
340
341
+
y12-video > figure > span {
342
+
z-index: 1;
343
+
color: white;
344
+
overflow-y: scroll;
345
+
}
346
+
347
+
y12-video {
348
display: flex;
349
flex-flow: column;
350
max-width: 100%;
···
357
padding-right: 3px;
358
}
359
360
+
y12-video > .M-media-controls {
361
border: 2px solid var(--T-border-color);
362
background: slategray;
363
border-radius: 0 3px 3px 3px;
···
365
width: max-content;
366
}
367
368
+
y12-video > * {
369
flex: none;
370
}
371
+3
-3
parse.js
···
110
args.alt = rargs.named.alt
111
// todo: improve this
112
if (!type) {
113
-
if (/[.](mp3|ogg|wav|m4a)\b/i.test(url))
114
type = 'audio'
115
-
else if (/[.](mp4|mkv|mov)\b/i.test(url))
116
type = 'video'
117
else if (/^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/]|youtube.com[/]shorts[/])[\w-]{11}/.test(url)) {
118
// todo: accept [start-end] args maybe?
···
293
}
294
const ARG_REGEX = /.*?(?=])/y
295
const WORD_REGEX = /[^\s`^()+=\[\]{}\\|"';:,.<>/?!*]*/y
296
-
const CODE_REGEX = /(?: *([-\w.+#$ ]+?) *(?![^\n]))?\n?([^]*?)(?:```|$)/y // ack
297
298
const parse=(text)=>{
299
let tree = {type: 'ROOT', content: [], prev: 'all_newline'}
···
110
args.alt = rargs.named.alt
111
// todo: improve this
112
if (!type) {
113
+
if (/[.](mp3|ogg|wav|m4a|flac)\b/i.test(url))
114
type = 'audio'
115
+
else if (/[.](mp4|mkv|mov|webm)\b/i.test(url))
116
type = 'video'
117
else if (/^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/]|youtube.com[/]shorts[/])[\w-]{11}/.test(url)) {
118
// todo: accept [start-end] args maybe?
···
293
}
294
const ARG_REGEX = /.*?(?=])/y
295
const WORD_REGEX = /[^\s`^()+=\[\]{}\\|"';:,.<>/?!*]*/y
296
+
const CODE_REGEX = /(?: *([-\w.+#$ ]+?) *(?![^\n]))?\n?([^]*?)(?:\n?```|$)/y // ack
297
298
const parse=(text)=>{
299
let tree = {type: 'ROOT', content: [], prev: 'all_newline'}
+43
-98
render.js
···
121
error: 𐀶`<div class='error'><code>🕯error🕯</code>🕯message🕯<pre>🕯stack🕯`,
122
123
audio: function({url}) {
0
124
let e = this()
125
-
let src = filter_url(url, 'audio')
126
-
let c2 = e.lastChild
127
-
let c1 = c2.previousSibling
128
-
let [time, save, vol, volume] = c2.childNodes
129
-
let [play, progress, , loop] = c1.childNodes
130
-
save.href = src
131
-
132
-
let audio
133
-
function setup() {
134
-
audio = document.createElement('audio')
135
-
audio.preload = 'none'
136
-
audio.src = src
137
-
138
-
time.textContent = 'loading'
139
-
140
-
volume.oninput = e=>{
141
-
audio.volume = +volume.value
142
-
}
143
-
function anim() {
144
-
time.textContent = format_time(audio.currentTime)+" / "+format_time(audio.duration)
145
-
progress.value = Math.round(audio.currentTime*10)/10
146
-
}
147
-
loop.onchange = e=>{ audio.loop = loop.checked }
148
-
audio.onpause = e=>{
149
-
play.textContent = "▶️"
150
-
}
151
-
audio.onpause()
152
-
audio.onplay = e=>{
153
-
play.textContent = "⏸️"
154
-
}
155
-
audio.onerror = e=>{
156
-
time.textContent = "Error"
157
-
}
158
-
function format_time(dur) {
159
-
let s = dur
160
-
let m = Math.floor(s / 60)
161
-
s = s % 60
162
-
return m+":"+(s+100).toFixed(1).substring(1)
163
-
}
164
-
audio.onvolumechange = e=>{
165
-
let volume = audio.volume
166
-
vol.textContent = volume ? ["🔈", "🔉", "🔊"][volume*2.99|0] : "🔇"
167
-
}
168
-
if (volume.value==1) {
169
-
volume.value = audio.volume
170
-
audio.onvolumechange()
171
-
} else {
172
-
volume.oninput()
173
-
}
174
-
audio.ondurationchange = e=>{
175
-
progress.max = Math.round(audio.duration*10)/10
176
-
time.textContent = format_time(audio.currentTime)+" / "+format_time(audio.duration)
177
-
}
178
-
audio.ontimeupdate = e=>{
179
-
anim()
180
-
}
181
-
progress.onchange = e=>{
182
-
audio.currentTime = progress.value
183
-
}
184
}
185
-
186
-
play.onclick = e=>{
187
-
if (!audio)
188
-
setup()
189
-
if (audio.paused)
190
-
audio.play()
191
-
else
192
-
audio.pause()
193
-
}
194
return e
195
-
}.bind(𐀶`
196
-
<media-player class='M-audio-player'>
197
-
<div class='M-media-controls'>
198
-
<button>▶️</button>
199
-
<input type=range max=100 step=0.1 value=0>
200
-
🔁<input type=checkbox title=loop></input>
201
-
</div>
202
-
<div class='M-media-controls'>
203
-
<span class='M-media-time'>‒‒/‒‒</span>
204
-
<a target=_blank>💾</a>
205
-
<span>🔊</span>
206
-
<input type=range max=1 step=0.01 value=1 class='M-media-volume'>
207
-
</div>
208
-
</media-player>
209
-
`),
210
video: function({url}) {
211
let e = this()
212
let media = document.createElement('video')
0
213
media.preload = 'none'
214
media.dataset.shrink = "video"
215
media.src = filter_url(url, 'video')
216
e.firstChild.append(media)
0
217
let cl = e.lastChild
218
let [play, progress, time] = cl.childNodes
219
play.onclick = e=>{
220
-
if (media.paused) {
221
media.play()
222
-
//let e2 = new Event('videoclicked', {bubbles: true, cancellable: true})
223
-
//media.dispatchEvent(e2)
224
-
} else
225
media.pause()
226
e.stopPropagation()
227
}
0
0
0
0
0
0
228
media.onresize = ev=>{
229
media.onresize = null
230
media.parentNode.style.aspectRatio = media.videoWidth+"/"+media.videoHeight
231
media.parentNode.style.height = media.videoHeight+"px"
232
media.parentNode.style.width = media.videoWidth+"px"
233
}
0
0
0
234
media.ondurationchange = e=>{
235
let s = media.duration
0
0
236
let m = Math.floor(s / 60)
237
s = s % 60
238
time.textContent = m+":"+(s+100).toFixed(2).substring(1)
239
}
240
media.ontimeupdate = e=>{
241
-
progress.value = media.currentTime / media.duration * 100
242
}
243
progress.onchange = e=>{
244
-
media.currentTime = progress.value/100 * media.duration
245
}
246
return e
247
}.bind(𐀶`
248
-
<media-player class='M-video-player'>
249
-
<div class='M-image-wrapper'></div>
250
-
<div class='M-media-controls'>
251
-
<button>Play</button>
252
-
<input type=range max=100 value=0>
253
-
<span>not loaded</span>
254
-
</div>
255
-
</media-player>
256
`),
257
258
italic: 𐀶`<i>`,
···
315
let e = this()
316
e.firstChild.textContent = url
317
e.firstChild.href = url
318
-
e.setAttribute('href', url)
319
return e
320
}.bind(𐀶`<youtube-embed><a target=_blank></a></youtube-embed>`),
321
···
121
error: 𐀶`<div class='error'><code>🕯error🕯</code>🕯message🕯<pre>🕯stack🕯`,
122
123
audio: function({url}) {
124
+
url = filter_url(url, 'audio')
125
let e = this()
126
+
e.dataset.src = url
127
+
e.onclick = ev=>{
128
+
ev.preventDefault()
129
+
let e = ev.currentTarget
130
+
let audio = document.createElement('audio')
131
+
audio.controls = true
132
+
audio.autoplay = true
133
+
audio.src = e.dataset.src
134
+
e.replaceChildren(audio)
135
+
e.onclick = null
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
136
}
137
+
let link = e.firstChild
138
+
link.href = url
139
+
link.title = url
140
+
link.lastChild.textContent = url.replace(/.*[/]/, "…/")
0
0
0
0
0
141
return e
142
+
}.bind(𐀶`<y12-audio><a>🎵️<span></span></a></y12-audio>`),
143
+
0
0
0
0
0
0
0
0
0
0
0
0
0
144
video: function({url}) {
145
let e = this()
146
let media = document.createElement('video')
147
+
media.setAttribute('tabindex', 0)
148
media.preload = 'none'
149
media.dataset.shrink = "video"
150
media.src = filter_url(url, 'video')
151
e.firstChild.append(media)
152
+
153
let cl = e.lastChild
154
let [play, progress, time] = cl.childNodes
155
play.onclick = e=>{
156
+
if (media.paused)
157
media.play()
158
+
else
0
0
159
media.pause()
160
e.stopPropagation()
161
}
162
+
media.onpause = e=>{
163
+
play.textContent = "▶️"
164
+
}
165
+
media.onplay = e=>{
166
+
play.textContent = "⏸️"
167
+
}
168
media.onresize = ev=>{
169
media.onresize = null
170
media.parentNode.style.aspectRatio = media.videoWidth+"/"+media.videoHeight
171
media.parentNode.style.height = media.videoHeight+"px"
172
media.parentNode.style.width = media.videoWidth+"px"
173
}
174
+
media.onerror = ev=>{
175
+
time.textContent = 'Error'
176
+
}
177
media.ondurationchange = e=>{
178
let s = media.duration
179
+
progress.disabled = false
180
+
progress.max = s
181
let m = Math.floor(s / 60)
182
s = s % 60
183
time.textContent = m+":"+(s+100).toFixed(2).substring(1)
184
}
185
media.ontimeupdate = e=>{
186
+
progress.value = media.currentTime
187
}
188
progress.onchange = e=>{
189
+
media.currentTime = progress.value
190
}
191
return e
192
}.bind(𐀶`
193
+
<y12-video>
194
+
<figure class='M-image-wrapper'></figure>
195
+
<div class='M-media-controls'>
196
+
<button>▶️</button>
197
+
<input type=range min=0 max=1 step=any value=0 disabled>
198
+
<span>not loaded</span>
199
+
</div>
200
+
</y12-video>
201
`),
202
203
italic: 𐀶`<i>`,
···
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
+36
-34
runtime.js
···
19
</youtube-embed>
20
That way, it's still accessible if the custom element isn't installed.
21
**/
22
-
class YoutubeEmbedElement extends HTMLElement {
23
constructor() {
24
super()
25
this.attachShadow({mode: 'open'})
26
let e = this.constructor.template()
27
for (let elem of e.querySelectorAll("[id]"))
28
-
this["_"+elem.id] = elem
29
-
this._link.onclick = e=>{
30
-
e.preventDefault()
31
this.show_youtube(true)
32
}
33
-
this._close.onclick = e=>{
34
this.show_youtube(false)
35
}
36
this.shadowRoot.append(e)
37
}
38
show_youtube(state) {
39
-
this._close.hidden = !state
40
this.toggleAttribute('data-big', state)
41
-
if (!this._iframe == !state)
42
return
43
if (state) {
44
-
this._iframe = document.createElement('iframe')
45
let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0&modestbranding=1`
46
if (this._query)
47
src += `&${this._query}`
48
-
this._iframe.src = src
49
-
this._link.replaceWith(this._iframe)
50
} else {
51
-
this._iframe.replaceWith(this._link)
52
-
this._iframe.src = "about:blank"
53
-
this._iframe = null
54
}
55
}
56
connectedCallback() {
57
-
this.update_href(this.getAttribute('href'))
58
}
59
disconnectedCallback() {
60
this._id = null
···
66
if (this._href == url)
67
return
68
this._href = url
69
-
this._title.textContent = url
70
-
this._author.textContent = ""
71
-
this._link.href = url
72
73
let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url)
74
if (query) {
···
85
// display video info
86
if (this._id == id)
87
return
88
-
this._link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)`
89
this._id = id
90
// only do one at a time
91
-
let f = YoutubeEmbedElement.requests[id]
92
if (!f) {
93
// todo: cancel these when node is disconnected?
94
-
YoutubeEmbedElement.requests[id] = f = fetch(`https://www.youtube.com/oembed?url=https%3A//youtube.com/watch%3Fv%3D${id}&format=json`).then(x=>x.json()).catch(x=>null)
95
}
96
f.then(data=>{
97
if (this._id != id)
98
return // if the video changed
99
if (!data)
100
-
data = {title: "unknown video"}
101
-
this._title.textContent = data.title
102
-
this._author.textContent = data.author_name
103
})
104
}
105
attributeChangedCallback(name, old, value) {
···
108
}
109
}
110
// intern these?
111
-
YoutubeEmbedElement.requests = {}
112
-
YoutubeEmbedElement.observedAttributes = ['href']
113
{
114
let template = ([html])=>{
115
let temp = document.createElement('template')
116
-
temp.innerHTML = html.replace(/\s*\n\s*/g, "")
117
return document.importNode.bind(document, temp.content, true)
118
}
119
-
YoutubeEmbedElement.template = template`
120
<a target=_blank id=link>
121
<cite id=caption>
122
<span id=title></span>
···
124
<span id=author></span>
125
</cite>
126
</a>
127
-
<button hidden id=close>❌</button>
128
<style>
129
:host {
130
-
border: 2px solid gray;
131
display: flex !important;
0
132
--height: 135px;
133
-
flex-direction: column;
134
}
135
:host([data-big]) {
136
--height: 270px;
137
}
138
#close {
139
-
width: 25px
140
flex-shrink: 0;
141
}
142
iframe {
···
149
height: var(--height);
150
padding: 4px;
151
overflow-y: auto;
152
-
background-repeat: no-repeat;
153
-
background-size: contain;
154
overflow-wrap: break-word;
155
white-space: pre-wrap;
156
flex-grow: 1;
···
162
font-family: sans-serif;
163
display: inline;
164
}
0
0
0
165
#caption > div {
166
height: 5px;
167
}
···
173
`
174
}
175
176
-
customElements.define('youtube-embed', YoutubeEmbedElement)
···
19
</youtube-embed>
20
That way, it's still accessible if the custom element isn't installed.
21
**/
22
+
class Markup_YoutubeElement extends HTMLElement {
23
constructor() {
24
super()
25
this.attachShadow({mode: 'open'})
26
let e = this.constructor.template()
27
for (let elem of e.querySelectorAll("[id]"))
28
+
this["$"+elem.id] = elem
29
+
this.$link.onclick = ev=>{
30
+
ev.preventDefault()
31
this.show_youtube(true)
32
}
33
+
this.$close.onclick = ev=>{
34
this.show_youtube(false)
35
}
36
this.shadowRoot.append(e)
37
}
38
show_youtube(state) {
39
+
this.$close.hidden = !state
40
this.toggleAttribute('data-big', state)
41
+
if (!this.$iframe == !state)
42
return
43
if (state) {
44
+
this.$iframe = document.createElement('iframe')
45
let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0&modestbranding=1`
46
if (this._query)
47
src += `&${this._query}`
48
+
this.$iframe.src = src
49
+
this.$link.replaceWith(this.$iframe)
50
} else {
51
+
this.$iframe.replaceWith(this.$link)
52
+
this.$iframe.src = "about:blank"
53
+
this.$iframe = null
54
}
55
}
56
connectedCallback() {
57
+
this.update_href(this.dataset.href)
58
}
59
disconnectedCallback() {
60
this._id = null
···
66
if (this._href == url)
67
return
68
this._href = url
69
+
this.$title.textContent = url
70
+
this.$author.textContent = ""
71
+
this.$link.href = url
72
73
let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url)
74
if (query) {
···
85
// display video info
86
if (this._id == id)
87
return
88
+
this.$link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)`
89
this._id = id
90
// only do one at a time
91
+
let f = Markup_YoutubeElement.requests[id]
92
if (!f) {
93
// todo: cancel these when node is disconnected?
94
+
Markup_YoutubeElement.requests[id] = f = fetch(`https://www.youtube.com/oembed?url=https%3A//youtube.com/watch%3Fv%3D${id}&format=json`).then(x=>x.json()).catch(x=>null)
95
}
96
f.then(data=>{
97
if (this._id != id)
98
return // if the video changed
99
if (!data)
100
+
data = {title: url, author_name: "(metadata request failed)"}
101
+
this.$title.textContent = data.title
102
+
this.$author.textContent = data.author_name
103
})
104
}
105
attributeChangedCallback(name, old, value) {
···
108
}
109
}
110
// intern these?
111
+
Markup_YoutubeElement.requests = {}
112
+
Markup_YoutubeElement.observedAttributes = ['data-href']
113
{
114
let template = ([html])=>{
115
let temp = document.createElement('template')
116
+
temp.innerHTML = html.replace(/\s*?\n\s*/g, "")
117
return document.importNode.bind(document, temp.content, true)
118
}
119
+
Markup_YoutubeElement.template = template`
120
<a target=_blank id=link>
121
<cite id=caption>
122
<span id=title></span>
···
124
<span id=author></span>
125
</cite>
126
</a>
127
+
<button hidden id=close>❌ close</button>
128
<style>
129
:host {
0
130
display: flex !important;
131
+
border: 2px solid gray;
132
--height: 135px;
133
+
flex-direction: column;
134
}
135
:host([data-big]) {
136
--height: 270px;
137
}
138
#close {
139
+
/*width: 25px;*/
140
flex-shrink: 0;
141
}
142
iframe {
···
149
height: var(--height);
150
padding: 4px;
151
overflow-y: auto;
152
+
background: no-repeat 0 / contain;
0
153
overflow-wrap: break-word;
154
white-space: pre-wrap;
155
flex-grow: 1;
···
161
font-family: sans-serif;
162
display: inline;
163
}
164
+
#caption > span {
165
+
padding: 0 0.25rem;
166
+
}
167
#caption > div {
168
height: 5px;
169
}
···
175
`
176
}
177
178
+
customElements.define('youtube-embed', Markup_YoutubeElement)